Skip to content

Commit 35043e4

Browse files
authored
Merge pull request #93 from RootstockCollective/dao-709
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.
2 parents 4aae0af + 30a19ac commit 35043e4

7 files changed

+280
-2
lines changed

contracts/StRIFToken.sol

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Ini
1111
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
1212
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
1313
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
14+
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
15+
import {ERC165Checker} from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol";
16+
17+
import {ICollectiveRewardsCheck} from "./interfaces/ICollectiveRewardsCheck.sol";
1418

1519
contract StRIFToken is
1620
Initializable,
@@ -21,6 +25,24 @@ contract StRIFToken is
2125
OwnableUpgradeable,
2226
UUPSUpgradeable
2327
{
28+
using Address for address;
29+
using ERC165Checker for address;
30+
31+
/// @notice The address of the CollectiveRewards Contract
32+
address public collectiveRewardsCheck;
33+
/// @notice The flag indicating that the CollectiveRewards error
34+
/// is desired to be skipped
35+
bool private _shouldErrorBeSkipped;
36+
37+
error STRIFStakedInCollectiveRewardsCanWithdraw(bool canWithdraw);
38+
error STRIFSupportsERC165(bool _supports);
39+
error STRIFSupportsICollectiveRewardsCheck(bool _supports);
40+
error CollectiveRewardsErrored(string reason);
41+
error CollectiveRewardsErroredBytes(bytes reason);
42+
43+
event STRIFCollectiveRewardsErrorSkipChangedTo(bool shouldBeSkipped);
44+
event CollectiveRewardsAddressHasBeenChanged(address collectiveRewardsAddress);
45+
2446
/// @custom:oz-upgrades-unsafe-allow constructor
2547
constructor() {
2648
_disableInitializers();
@@ -87,6 +109,44 @@ contract StRIFToken is
87109
_delegate(to, to);
88110
}
89111

112+
//checks CollectiveRewards for stake
113+
modifier _checkCollectiveRewardsForStake(address staker, uint256 value) {
114+
_;
115+
if (collectiveRewardsCheck != address(0)) {
116+
try ICollectiveRewardsCheck(collectiveRewardsCheck).canWithdraw(staker, value) returns (
117+
bool canWithdraw
118+
) {
119+
if (!canWithdraw) {
120+
revert STRIFStakedInCollectiveRewardsCanWithdraw(false);
121+
}
122+
} catch Error(string memory reason) {
123+
if (!_shouldErrorBeSkipped) {
124+
revert CollectiveRewardsErrored(reason);
125+
}
126+
} catch (bytes memory reason) {
127+
if (!_shouldErrorBeSkipped) {
128+
revert CollectiveRewardsErroredBytes(reason);
129+
}
130+
}
131+
}
132+
}
133+
134+
// checks that received address has method which can successfully be called
135+
// before setting it to state
136+
function setCollectiveRewardsAddress(address collectiveRewardsAddress) public onlyOwner {
137+
if (!collectiveRewardsAddress.supportsInterface(type(ICollectiveRewardsCheck).interfaceId)) {
138+
revert STRIFSupportsICollectiveRewardsCheck(false);
139+
}
140+
141+
collectiveRewardsCheck = collectiveRewardsAddress;
142+
emit CollectiveRewardsAddressHasBeenChanged(collectiveRewardsAddress);
143+
}
144+
145+
function setCollectiveRewardsErrorSkipFlag(bool shouldBeSkipped) public onlyOwner {
146+
_shouldErrorBeSkipped = shouldBeSkipped;
147+
emit STRIFCollectiveRewardsErrorSkipChangedTo(shouldBeSkipped);
148+
}
149+
90150
// The following functions are overrides required by Solidity.
91151

92152
//solhint-disable-next-line no-empty-blocks
@@ -100,10 +160,17 @@ contract StRIFToken is
100160
address from,
101161
address to,
102162
uint256 value
103-
) internal override(ERC20Upgradeable, ERC20VotesUpgradeable) {
163+
) internal override(ERC20Upgradeable, ERC20VotesUpgradeable) _checkCollectiveRewardsForStake(from, value) {
104164
super._update(from, to, value);
105165
}
106166

167+
function withdrawTo(
168+
address account,
169+
uint256 value
170+
) public virtual override _checkCollectiveRewardsForStake(account, value) returns (bool) {
171+
return super.withdrawTo(account, value);
172+
}
173+
107174
function nonces(
108175
address owner
109176
) public view override(ERC20PermitUpgradeable, NoncesUpgradeable) returns (uint256) {
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.20;
4+
5+
interface ICollectiveRewardsCheck {
6+
function canWithdraw(address targetAddress, uint256 value) external view returns (bool);
7+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.20;
4+
5+
contract ContractDoesNotSupportERC165andICollectiveRewardscheck {
6+
constructor() {}
7+
8+
function foo() internal pure returns (string memory) {
9+
return "foo";
10+
}
11+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.20;
4+
5+
import "@openzeppelin/contracts/interfaces/IERC165.sol";
6+
7+
contract ContractDoesNotSupportICollectiveRewardsCheck is IERC165 {
8+
constructor() {}
9+
10+
function foo() internal pure returns (string memory) {
11+
return "foo";
12+
}
13+
14+
function supportsInterface(bytes4 interfaceId) external pure override returns (bool) {
15+
return interfaceId == type(IERC165).interfaceId;
16+
}
17+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.20;
4+
5+
import "@openzeppelin/contracts/interfaces/IERC165.sol";
6+
7+
import {ICollectiveRewardsCheck} from "../interfaces/ICollectiveRewardsCheck.sol";
8+
9+
contract ContractSupportsERC165andICollectiveRewardscheck is IERC165, ICollectiveRewardsCheck {
10+
address public blockedAddress;
11+
12+
constructor(address _blockedAddress) {
13+
blockedAddress = _blockedAddress;
14+
}
15+
16+
function setBlockedAddress(address _blockedAddress) external {
17+
blockedAddress = _blockedAddress;
18+
}
19+
20+
function canWithdraw(address target, uint256 value) external view returns (bool) {
21+
if (target == blockedAddress || value < 0) {
22+
return false;
23+
} else {
24+
return true;
25+
}
26+
}
27+
28+
function supportsInterface(bytes4 interfaceId) external pure override returns (bool) {
29+
return
30+
interfaceId == type(IERC165).interfaceId || interfaceId == type(ICollectiveRewardsCheck).interfaceId;
31+
}
32+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.20;
4+
5+
import "@openzeppelin/contracts/interfaces/IERC165.sol";
6+
7+
import {ICollectiveRewardsCheck} from "../interfaces/ICollectiveRewardsCheck.sol";
8+
9+
contract ContractWithErrorInCanWithdraw is IERC165, ICollectiveRewardsCheck {
10+
address public blockedAddress;
11+
12+
constructor(address _blockedAddress) {
13+
blockedAddress = _blockedAddress;
14+
}
15+
16+
function setBlockedAddress(address _blockedAddress) external {
17+
blockedAddress = _blockedAddress;
18+
}
19+
20+
function canWithdraw(address target, uint256 value) external view returns (bool) {
21+
require(target == address(0), "JUST A DUMMY ERROR IS TARGET IS NOT A ZERO ADDRESS");
22+
if (target == blockedAddress || value < 0) {
23+
return false;
24+
} else {
25+
return true;
26+
}
27+
}
28+
29+
function supportsInterface(bytes4 interfaceId) external pure override returns (bool) {
30+
return
31+
interfaceId == type(IERC165).interfaceId || interfaceId == type(ICollectiveRewardsCheck).interfaceId;
32+
}
33+
}

test/StRIFToken.test.ts

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,36 @@ import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers'
22
import { loadFixture } from '@nomicfoundation/hardhat-toolbox/network-helpers'
33
import { expect } from 'chai'
44
import { ethers } from 'hardhat'
5-
import { RIFToken, StRIFToken } from '../typechain-types'
5+
import {
6+
ContractDoesNotSupportERC165andICollectiveRewardscheck,
7+
ContractDoesNotSupportICollectiveRewardsCheck,
8+
ContractSupportsERC165andICollectiveRewardscheck,
9+
ContractWithErrorInCanWithdraw,
10+
RIFToken,
11+
StRIFToken,
12+
} from '../typechain-types'
613
import { deployContracts } from './deployContracts'
714

815
describe('stRIFToken', () => {
916
let owner: SignerWithAddress, holder: SignerWithAddress, voter: SignerWithAddress
1017
let rif: RIFToken
1118
let stRIF: StRIFToken
19+
let ContractSupportsERC165andICollectiveRewardscheck: ContractSupportsERC165andICollectiveRewardscheck
20+
let ContractDoesNotSupportERC165andICollectiveRewardscheck: ContractDoesNotSupportERC165andICollectiveRewardscheck
21+
let ContractDoesNotSupportICollectiveRewardsCheck: ContractDoesNotSupportICollectiveRewardsCheck
22+
let ContractWithErrorInCanWithdraw: ContractWithErrorInCanWithdraw
1223
const votingPower = 10n * 10n ** 18n
1324

1425
// prettier-ignore
1526
before(async () => {
1627
;[owner, holder, voter] = await ethers.getSigners()
1728
;({ rif, stRIF } = await loadFixture(deployContracts))
29+
ContractDoesNotSupportERC165andICollectiveRewardscheck = await ethers.deployContract('ContractDoesNotSupportERC165andICollectiveRewardscheck')
30+
ContractDoesNotSupportICollectiveRewardsCheck = await ethers.deployContract('ContractDoesNotSupportICollectiveRewardsCheck')
31+
ContractSupportsERC165andICollectiveRewardscheck = await ethers.deployContract('ContractSupportsERC165andICollectiveRewardscheck', [
32+
holder,
33+
])
34+
ContractWithErrorInCanWithdraw = await ethers.deployContract('ContractWithErrorInCanWithdraw', [voter])
1835
})
1936

2037
it('Should assign the initial balance to the contract itself', async () => {
@@ -178,4 +195,98 @@ describe('stRIFToken', () => {
178195
expect(await stRIF.getVotes(voter.address)).to.equal(votingPower)
179196
})
180197
})
198+
199+
describe('CollectiveRewards Check to allow withdrawal', () => {
200+
it('blockedAddress should be set', async () => {
201+
expect(await ContractSupportsERC165andICollectiveRewardscheck.blockedAddress()).to.be.properAddress
202+
expect(await ContractSupportsERC165andICollectiveRewardscheck.blockedAddress()).to.equal(holder.address)
203+
})
204+
205+
it('only owner should be able to set CollectiveRewardsAddress', async () => {
206+
const tx = stRIF
207+
.connect(holder)
208+
.setCollectiveRewardsAddress(ContractSupportsERC165andICollectiveRewardscheck)
209+
await expect(tx).to.be.revertedWithCustomError(
210+
{ interface: stRIF.interface },
211+
'OwnableUnauthorizedAccount',
212+
)
213+
})
214+
215+
it('setting CollectiveRewards address should fail if contract does not support ERC165 with STRIFSupportsICollectiveRewardsCheck', async () => {
216+
const tx = stRIF.setCollectiveRewardsAddress(
217+
await ContractDoesNotSupportERC165andICollectiveRewardscheck.getAddress(),
218+
)
219+
await expect(tx).to.be.revertedWithCustomError(
220+
{ interface: stRIF.interface },
221+
'STRIFSupportsICollectiveRewardsCheck',
222+
)
223+
})
224+
225+
it('setting CollectiveRewards address should fail if contract does not support ICollectiveRewardsCheck with STRIFSupportsICollectiveRewardsCheck', async () => {
226+
expect(await ContractDoesNotSupportICollectiveRewardsCheck.supportsInterface('0x01ffc9a7')).to.be.true
227+
228+
const tx = stRIF.setCollectiveRewardsAddress(
229+
await ContractDoesNotSupportERC165andICollectiveRewardscheck.getAddress(),
230+
)
231+
await expect(tx).to.be.revertedWithCustomError(
232+
{ interface: stRIF.interface },
233+
'STRIFSupportsICollectiveRewardsCheck',
234+
)
235+
})
236+
237+
it('should set CollectiveRewards address if canWithdraw returns boolean', async () => {
238+
const address = await ContractSupportsERC165andICollectiveRewardscheck.getAddress()
239+
await stRIF.setCollectiveRewardsAddress(address)
240+
241+
expect(await stRIF.collectiveRewardsCheck()).to.equal(address)
242+
})
243+
244+
it('should revert withdrawTo, _update with STRIFStakedInCollectiveRewardsCanWithdraw if bimCheck returns false', async () => {
245+
expect(await stRIF.balanceOf(holder)).to.equal(votingPower)
246+
247+
const tx = stRIF.connect(holder).withdrawTo(holder.address, votingPower)
248+
await expect(tx).to.be.revertedWithCustomError(
249+
{ interface: stRIF.interface },
250+
'STRIFStakedInCollectiveRewardsCanWithdraw',
251+
)
252+
253+
//runs _update under the hood
254+
const transferTx = stRIF.connect(holder).transfer(voter, votingPower)
255+
await expect(transferTx).to.be.revertedWithCustomError(
256+
{ interface: stRIF.interface },
257+
'STRIFStakedInCollectiveRewardsCanWithdraw',
258+
)
259+
expect(await stRIF.balanceOf(holder)).to.equal(votingPower)
260+
})
261+
262+
it('should allow withdrawTo if bimCheck returns true', async () => {
263+
await ContractSupportsERC165andICollectiveRewardscheck.setBlockedAddress(voter)
264+
expect(await stRIF.balanceOf(holder)).to.equal(votingPower)
265+
266+
const value = votingPower / 2n
267+
const tx = stRIF.connect(holder).withdrawTo(holder.address, value)
268+
await expect(tx).to.emit(stRIF, 'Transfer').withArgs(holder.address, ethers.ZeroAddress, value)
269+
})
270+
271+
it('should throw an error if _shouldSkipError is false', async () => {
272+
const address = await ContractWithErrorInCanWithdraw.getAddress()
273+
const setTX = stRIF.setCollectiveRewardsAddress(address)
274+
await expect(setTX).to.emit(stRIF, 'CollectiveRewardsAddressHasBeenChanged').withArgs(address)
275+
276+
const tx = stRIF.connect(holder).withdrawTo(holder.address, votingPower / 2n)
277+
await expect(tx).to.be.revertedWithCustomError(
278+
{ interface: stRIF.interface },
279+
'CollectiveRewardsErrored',
280+
)
281+
})
282+
283+
it('should ignore Collective Rewards error if _shouldSkipError is true', async () => {
284+
const skipTX = stRIF.setCollectiveRewardsErrorSkipFlag(true)
285+
await expect(skipTX).to.emit(stRIF, 'STRIFCollectiveRewardsErrorSkipChangedTo').withArgs(true)
286+
287+
const value = votingPower / 2n
288+
const tx = stRIF.connect(holder).withdrawTo(holder.address, value)
289+
await expect(tx).to.emit(stRIF, 'Transfer').withArgs(holder.address, ethers.ZeroAddress, value)
290+
})
291+
})
181292
})

0 commit comments

Comments
 (0)