Skip to content

Commit

Permalink
Merge pull request #93 from RootstockCollective/dao-709
Browse files Browse the repository at this point in the history
I'm merging this branch to immediately open a new pull request based on it, which will include the upgrade of the stRif contract to the code presented in this branch/PR.
  • Loading branch information
shenshin authored Nov 12, 2024
2 parents 4aae0af + 30a19ac commit 35043e4
Show file tree
Hide file tree
Showing 7 changed files with 280 additions and 2 deletions.
69 changes: 68 additions & 1 deletion contracts/StRIFToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Ini
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
import {ERC165Checker} from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol";

import {ICollectiveRewardsCheck} from "./interfaces/ICollectiveRewardsCheck.sol";

contract StRIFToken is
Initializable,
Expand All @@ -21,6 +25,24 @@ contract StRIFToken is
OwnableUpgradeable,
UUPSUpgradeable
{
using Address for address;
using ERC165Checker for address;

/// @notice The address of the CollectiveRewards Contract
address public collectiveRewardsCheck;
/// @notice The flag indicating that the CollectiveRewards error
/// is desired to be skipped
bool private _shouldErrorBeSkipped;

error STRIFStakedInCollectiveRewardsCanWithdraw(bool canWithdraw);
error STRIFSupportsERC165(bool _supports);
error STRIFSupportsICollectiveRewardsCheck(bool _supports);
error CollectiveRewardsErrored(string reason);
error CollectiveRewardsErroredBytes(bytes reason);

event STRIFCollectiveRewardsErrorSkipChangedTo(bool shouldBeSkipped);
event CollectiveRewardsAddressHasBeenChanged(address collectiveRewardsAddress);

/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
Expand Down Expand Up @@ -87,6 +109,44 @@ contract StRIFToken is
_delegate(to, to);
}

//checks CollectiveRewards for stake
modifier _checkCollectiveRewardsForStake(address staker, uint256 value) {
_;
if (collectiveRewardsCheck != address(0)) {
try ICollectiveRewardsCheck(collectiveRewardsCheck).canWithdraw(staker, value) returns (
bool canWithdraw
) {
if (!canWithdraw) {
revert STRIFStakedInCollectiveRewardsCanWithdraw(false);
}
} catch Error(string memory reason) {
if (!_shouldErrorBeSkipped) {
revert CollectiveRewardsErrored(reason);
}
} catch (bytes memory reason) {
if (!_shouldErrorBeSkipped) {
revert CollectiveRewardsErroredBytes(reason);
}
}
}
}

// checks that received address has method which can successfully be called
// before setting it to state
function setCollectiveRewardsAddress(address collectiveRewardsAddress) public onlyOwner {
if (!collectiveRewardsAddress.supportsInterface(type(ICollectiveRewardsCheck).interfaceId)) {
revert STRIFSupportsICollectiveRewardsCheck(false);
}

collectiveRewardsCheck = collectiveRewardsAddress;
emit CollectiveRewardsAddressHasBeenChanged(collectiveRewardsAddress);
}

function setCollectiveRewardsErrorSkipFlag(bool shouldBeSkipped) public onlyOwner {
_shouldErrorBeSkipped = shouldBeSkipped;
emit STRIFCollectiveRewardsErrorSkipChangedTo(shouldBeSkipped);
}

// The following functions are overrides required by Solidity.

//solhint-disable-next-line no-empty-blocks
Expand All @@ -100,10 +160,17 @@ contract StRIFToken is
address from,
address to,
uint256 value
) internal override(ERC20Upgradeable, ERC20VotesUpgradeable) {
) internal override(ERC20Upgradeable, ERC20VotesUpgradeable) _checkCollectiveRewardsForStake(from, value) {
super._update(from, to, value);
}

function withdrawTo(
address account,
uint256 value
) public virtual override _checkCollectiveRewardsForStake(account, value) returns (bool) {
return super.withdrawTo(account, value);
}

function nonces(
address owner
) public view override(ERC20PermitUpgradeable, NoncesUpgradeable) returns (uint256) {
Expand Down
7 changes: 7 additions & 0 deletions contracts/interfaces/ICollectiveRewardsCheck.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

interface ICollectiveRewardsCheck {
function canWithdraw(address targetAddress, uint256 value) external view returns (bool);
}
11 changes: 11 additions & 0 deletions contracts/test/ContractDoesNotSupportERC165andIBIMcheck.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

contract ContractDoesNotSupportERC165andICollectiveRewardscheck {
constructor() {}

function foo() internal pure returns (string memory) {
return "foo";
}
}
17 changes: 17 additions & 0 deletions contracts/test/ContractDoesNotSupportIBIMCheck.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import "@openzeppelin/contracts/interfaces/IERC165.sol";

contract ContractDoesNotSupportICollectiveRewardsCheck is IERC165 {
constructor() {}

function foo() internal pure returns (string memory) {
return "foo";
}

function supportsInterface(bytes4 interfaceId) external pure override returns (bool) {
return interfaceId == type(IERC165).interfaceId;
}
}
32 changes: 32 additions & 0 deletions contracts/test/ContractSupportsERC165andIBIMcheck.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import "@openzeppelin/contracts/interfaces/IERC165.sol";

import {ICollectiveRewardsCheck} from "../interfaces/ICollectiveRewardsCheck.sol";

contract ContractSupportsERC165andICollectiveRewardscheck is IERC165, ICollectiveRewardsCheck {
address public blockedAddress;

constructor(address _blockedAddress) {
blockedAddress = _blockedAddress;
}

function setBlockedAddress(address _blockedAddress) external {
blockedAddress = _blockedAddress;
}

function canWithdraw(address target, uint256 value) external view returns (bool) {
if (target == blockedAddress || value < 0) {
return false;
} else {
return true;
}
}

function supportsInterface(bytes4 interfaceId) external pure override returns (bool) {
return
interfaceId == type(IERC165).interfaceId || interfaceId == type(ICollectiveRewardsCheck).interfaceId;
}
}
33 changes: 33 additions & 0 deletions contracts/test/ContractWithErrorInCanWithdraw.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import "@openzeppelin/contracts/interfaces/IERC165.sol";

import {ICollectiveRewardsCheck} from "../interfaces/ICollectiveRewardsCheck.sol";

contract ContractWithErrorInCanWithdraw is IERC165, ICollectiveRewardsCheck {
address public blockedAddress;

constructor(address _blockedAddress) {
blockedAddress = _blockedAddress;
}

function setBlockedAddress(address _blockedAddress) external {
blockedAddress = _blockedAddress;
}

function canWithdraw(address target, uint256 value) external view returns (bool) {
require(target == address(0), "JUST A DUMMY ERROR IS TARGET IS NOT A ZERO ADDRESS");
if (target == blockedAddress || value < 0) {
return false;
} else {
return true;
}
}

function supportsInterface(bytes4 interfaceId) external pure override returns (bool) {
return
interfaceId == type(IERC165).interfaceId || interfaceId == type(ICollectiveRewardsCheck).interfaceId;
}
}
113 changes: 112 additions & 1 deletion test/StRIFToken.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,36 @@ import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers'
import { loadFixture } from '@nomicfoundation/hardhat-toolbox/network-helpers'
import { expect } from 'chai'
import { ethers } from 'hardhat'
import { RIFToken, StRIFToken } from '../typechain-types'
import {
ContractDoesNotSupportERC165andICollectiveRewardscheck,
ContractDoesNotSupportICollectiveRewardsCheck,
ContractSupportsERC165andICollectiveRewardscheck,
ContractWithErrorInCanWithdraw,
RIFToken,
StRIFToken,
} from '../typechain-types'
import { deployContracts } from './deployContracts'

describe('stRIFToken', () => {
let owner: SignerWithAddress, holder: SignerWithAddress, voter: SignerWithAddress
let rif: RIFToken
let stRIF: StRIFToken
let ContractSupportsERC165andICollectiveRewardscheck: ContractSupportsERC165andICollectiveRewardscheck
let ContractDoesNotSupportERC165andICollectiveRewardscheck: ContractDoesNotSupportERC165andICollectiveRewardscheck
let ContractDoesNotSupportICollectiveRewardsCheck: ContractDoesNotSupportICollectiveRewardsCheck
let ContractWithErrorInCanWithdraw: ContractWithErrorInCanWithdraw
const votingPower = 10n * 10n ** 18n

// prettier-ignore
before(async () => {
;[owner, holder, voter] = await ethers.getSigners()
;({ rif, stRIF } = await loadFixture(deployContracts))
ContractDoesNotSupportERC165andICollectiveRewardscheck = await ethers.deployContract('ContractDoesNotSupportERC165andICollectiveRewardscheck')
ContractDoesNotSupportICollectiveRewardsCheck = await ethers.deployContract('ContractDoesNotSupportICollectiveRewardsCheck')
ContractSupportsERC165andICollectiveRewardscheck = await ethers.deployContract('ContractSupportsERC165andICollectiveRewardscheck', [
holder,
])
ContractWithErrorInCanWithdraw = await ethers.deployContract('ContractWithErrorInCanWithdraw', [voter])
})

it('Should assign the initial balance to the contract itself', async () => {
Expand Down Expand Up @@ -178,4 +195,98 @@ describe('stRIFToken', () => {
expect(await stRIF.getVotes(voter.address)).to.equal(votingPower)
})
})

describe('CollectiveRewards Check to allow withdrawal', () => {
it('blockedAddress should be set', async () => {
expect(await ContractSupportsERC165andICollectiveRewardscheck.blockedAddress()).to.be.properAddress
expect(await ContractSupportsERC165andICollectiveRewardscheck.blockedAddress()).to.equal(holder.address)
})

it('only owner should be able to set CollectiveRewardsAddress', async () => {
const tx = stRIF
.connect(holder)
.setCollectiveRewardsAddress(ContractSupportsERC165andICollectiveRewardscheck)
await expect(tx).to.be.revertedWithCustomError(
{ interface: stRIF.interface },
'OwnableUnauthorizedAccount',
)
})

it('setting CollectiveRewards address should fail if contract does not support ERC165 with STRIFSupportsICollectiveRewardsCheck', async () => {
const tx = stRIF.setCollectiveRewardsAddress(
await ContractDoesNotSupportERC165andICollectiveRewardscheck.getAddress(),
)
await expect(tx).to.be.revertedWithCustomError(
{ interface: stRIF.interface },
'STRIFSupportsICollectiveRewardsCheck',
)
})

it('setting CollectiveRewards address should fail if contract does not support ICollectiveRewardsCheck with STRIFSupportsICollectiveRewardsCheck', async () => {
expect(await ContractDoesNotSupportICollectiveRewardsCheck.supportsInterface('0x01ffc9a7')).to.be.true

const tx = stRIF.setCollectiveRewardsAddress(
await ContractDoesNotSupportERC165andICollectiveRewardscheck.getAddress(),
)
await expect(tx).to.be.revertedWithCustomError(
{ interface: stRIF.interface },
'STRIFSupportsICollectiveRewardsCheck',
)
})

it('should set CollectiveRewards address if canWithdraw returns boolean', async () => {
const address = await ContractSupportsERC165andICollectiveRewardscheck.getAddress()
await stRIF.setCollectiveRewardsAddress(address)

expect(await stRIF.collectiveRewardsCheck()).to.equal(address)
})

it('should revert withdrawTo, _update with STRIFStakedInCollectiveRewardsCanWithdraw if bimCheck returns false', async () => {
expect(await stRIF.balanceOf(holder)).to.equal(votingPower)

const tx = stRIF.connect(holder).withdrawTo(holder.address, votingPower)
await expect(tx).to.be.revertedWithCustomError(
{ interface: stRIF.interface },
'STRIFStakedInCollectiveRewardsCanWithdraw',
)

//runs _update under the hood
const transferTx = stRIF.connect(holder).transfer(voter, votingPower)
await expect(transferTx).to.be.revertedWithCustomError(
{ interface: stRIF.interface },
'STRIFStakedInCollectiveRewardsCanWithdraw',
)
expect(await stRIF.balanceOf(holder)).to.equal(votingPower)
})

it('should allow withdrawTo if bimCheck returns true', async () => {
await ContractSupportsERC165andICollectiveRewardscheck.setBlockedAddress(voter)
expect(await stRIF.balanceOf(holder)).to.equal(votingPower)

const value = votingPower / 2n
const tx = stRIF.connect(holder).withdrawTo(holder.address, value)
await expect(tx).to.emit(stRIF, 'Transfer').withArgs(holder.address, ethers.ZeroAddress, value)
})

it('should throw an error if _shouldSkipError is false', async () => {
const address = await ContractWithErrorInCanWithdraw.getAddress()
const setTX = stRIF.setCollectiveRewardsAddress(address)
await expect(setTX).to.emit(stRIF, 'CollectiveRewardsAddressHasBeenChanged').withArgs(address)

const tx = stRIF.connect(holder).withdrawTo(holder.address, votingPower / 2n)
await expect(tx).to.be.revertedWithCustomError(
{ interface: stRIF.interface },
'CollectiveRewardsErrored',
)
})

it('should ignore Collective Rewards error if _shouldSkipError is true', async () => {
const skipTX = stRIF.setCollectiveRewardsErrorSkipFlag(true)
await expect(skipTX).to.emit(stRIF, 'STRIFCollectiveRewardsErrorSkipChangedTo').withArgs(true)

const value = votingPower / 2n
const tx = stRIF.connect(holder).withdrawTo(holder.address, value)
await expect(tx).to.emit(stRIF, 'Transfer').withArgs(holder.address, ethers.ZeroAddress, value)
})
})
})

0 comments on commit 35043e4

Please sign in to comment.