From 9e91443ebf76d8ca5c79331ad4d5731a4baad5e7 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 12 Nov 2024 19:53:01 +0100 Subject: [PATCH 1/6] chore: add old and new stRif versions --- contracts/StRIFTokenV02.sol | 179 ++++++++++++++++++++++ hardhat.config.ts | 2 +- test/StRIFTokenV02.test.ts | 292 ++++++++++++++++++++++++++++++++++++ 3 files changed, 472 insertions(+), 1 deletion(-) create mode 100644 contracts/StRIFTokenV02.sol create mode 100644 test/StRIFTokenV02.test.ts diff --git a/contracts/StRIFTokenV02.sol b/contracts/StRIFTokenV02.sol new file mode 100644 index 0000000..cac7103 --- /dev/null +++ b/contracts/StRIFTokenV02.sol @@ -0,0 +1,179 @@ +// SPDX-License-Identifier: MIT +// Compatible with OpenZeppelin Contracts ^5.0.0 +pragma solidity ^0.8.20; + +import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import {ERC20PermitUpgradeable, NoncesUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20PermitUpgradeable.sol"; +import {ERC20VotesUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20VotesUpgradeable.sol"; +import {ERC20WrapperUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20WrapperUpgradeable.sol"; +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +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 StRIFTokenV02 is + Initializable, + ERC20Upgradeable, + ERC20PermitUpgradeable, + ERC20VotesUpgradeable, + ERC20WrapperUpgradeable, + 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(); + } + + function initialize(IERC20 rifToken, address initialOwner) public initializer { + __ERC20_init("StRIFToken", "stRIF"); + __ERC20Permit_init("StRIFToken"); + __ERC20Votes_init(); + __ERC20Wrapper_init(rifToken); + __Ownable_init(initialOwner); + __UUPSUpgradeable_init(); + } + + /** + * @dev Allows token holder to transfer tokens to another account, after which + * the recipient automatically delegates votes to themselves if they do + * not already have a delegate. + * Transfer and delegation happen within one transaction. + * @param to The address of the recipient of the token transfer + * @param value The amount of tokens being transferred + */ + function transferAndDelegate(address to, uint256 value) public virtual { + transfer(to, value); + _autoDelegate(to, value); + } + + /** + * @dev Allows a token holder to transfer tokens from one account to another account, + * after which the recipient automatically delegates votes to themselves if they do + * not already have a delegate. This function is analogous to `transferAndDelegate` and + * exists as a counterpart to the `transferFrom` function from the ERC-20 standard. + * + * @param from The address of the account to transfer tokens from + * @param to The address of the recipient of the token transfer + * @param value The amount of tokens being transferred + */ + function transferFromAndDelegate(address from, address to, uint256 value) public virtual { + transferFrom(from, to, value); + _autoDelegate(to, value); + } + + /** + * @dev Allows to mint stRIFs from underlying RIF tokens (stake) + * and delegate gained voting power to a provided address + * @param to a target address for minting and delegation + * @param value amount of RIF tokens to stake + */ + function depositAndDelegate(address to, uint256 value) public virtual { + depositFor(to, value); + _autoDelegate(to, value); + } + + /** + * @dev Internal function to automatically delegate votes to the recipient + * after a token transfer, if the recipient does not already have a delegate. + * Delegation only occurs if the transfer amount is greater than zero. + * + * @param to The address of the recipient of the token transfer. + * @param value The amount of tokens being transferred. + */ + function _autoDelegate(address to, uint256 value) internal virtual { + if (value == 0 || delegates(to) != address(0)) return; + _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 + function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} + + function decimals() public view override(ERC20Upgradeable, ERC20WrapperUpgradeable) returns (uint8) { + return super.decimals(); + } + + function _update( + address from, + address to, + uint256 value + ) 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) { + return super.nonces(owner); + } +} diff --git a/hardhat.config.ts b/hardhat.config.ts index 9356232..0165c45 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -26,7 +26,7 @@ const config: HardhatUserConfig = { ], }, gasReporter: { - enabled: true, + enabled: false, reportPureAndViewMethods: true, showUncalledMethods: false, }, diff --git a/test/StRIFTokenV02.test.ts b/test/StRIFTokenV02.test.ts new file mode 100644 index 0000000..9806244 --- /dev/null +++ b/test/StRIFTokenV02.test.ts @@ -0,0 +1,292 @@ +import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers' +import { loadFixture } from '@nomicfoundation/hardhat-toolbox/network-helpers' +import { expect } from 'chai' +import { ethers } from 'hardhat' +import { + ContractDoesNotSupportERC165andICollectiveRewardscheck, + ContractDoesNotSupportICollectiveRewardsCheck, + ContractSupportsERC165andICollectiveRewardscheck, + ContractWithErrorInCanWithdraw, + RIFToken, + StRIFTokenV02, +} from '../typechain-types' +import { deployContracts } from './deployContracts' + +describe('stRIFToken', () => { + let owner: SignerWithAddress, holder: SignerWithAddress, voter: SignerWithAddress + let rif: RIFToken + let stRIF: StRIFTokenV02 + 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 () => { + const contractBalance = await rif.balanceOf(owner) + expect(contractBalance).to.equal(ethers.parseUnits('1000000000', 18)) + }) + + describe('Wrapping RIF tokens to stRIF', () => { + it('holder should NOT initially own RIF tokens', async () => { + expect(await rif.balanceOf(holder.address)).to.equal(0) + }) + + it("owner should send some RIFs to holder's address", async () => { + const tx = await rif.transfer(holder.address, votingPower) + await tx.wait() + expect(tx) + .to.emit(rif, 'Transfer') + .withArgs(await rif.getAddress(), holder.address, votingPower) + }) + + it('holder should approve allowance for stRIF', async () => { + const tx = await rif.connect(holder).approve(stRIF.getAddress(), votingPower) + await tx.wait() + expect(tx).to.emit(rif, 'Approval').withArgs(holder.address, stRIF.getAddress(), votingPower) + }) + + it('allowance for stRIF should be set on the RIF token', async () => { + expect(await rif.allowance(holder.address, stRIF.getAddress())).to.equal(votingPower) + }) + + it('stRIF should NOT have any RIF tokens on its balance', async () => { + expect(await rif.balanceOf(stRIF.getAddress())).to.equal(0) + }) + + it('holder should NOT have any stRIF tokens on his balance', async () => { + expect(await stRIF.balanceOf(holder.address)).to.equal(0) + }) + + /** depositFor is a method for minting stRIF tokens */ + it('holder should deposit underlying tokens and mint the corresponding amount of stRIF tokens', async () => { + await expect(stRIF.connect(holder).depositFor(holder.address, votingPower)) + .to.emit(stRIF, 'Transfer') + .withArgs(ethers.ZeroAddress, holder.address, votingPower) + }) + + it('holder should NOT have RIF tokens anymore', async () => { + expect(await rif.balanceOf(holder.address)).to.equal(0) + }) + + it('stRIF now should own RIFs belonged to the holder', async () => { + expect(await rif.balanceOf(stRIF.getAddress())).to.equal(votingPower) + }) + + it('holder should have the same amount of stRIF tokens as the deposited RIF tokens', async () => { + expect(await stRIF.balanceOf(holder.address)).to.equal(votingPower) + }) + + it('holder should NOT be able to deposit more RIF tokens than he has', async () => { + await expect(stRIF.connect(holder).depositFor(holder.address, votingPower)).to.be.reverted + }) + + /** delegate */ + it('holder should NOT have vote power yet', async () => { + expect(await stRIF.getVotes(holder.address)).to.equal(0) + }) + + it('holder should delegate vote power to himself', async () => { + const tx = await stRIF.connect(holder).delegate(holder.address) + await expect(tx) + .to.emit(stRIF, 'DelegateChanged') + .withArgs(holder.address, ethers.ZeroAddress, holder.address) + }) + + it('holder should now have delegate set', async () => { + expect(await stRIF.delegates(holder.address)).to.equal(holder.address) + }) + + it('holder should have vote power', async () => { + expect(await stRIF.getVotes(holder.address)).to.equal(votingPower) + }) + }) + + describe('Unwrapping RIF tokens from stRIF tokens', () => { + /** withdrawTo is a method for burning stRIF tokens */ + it('holder should burn stRIF tokens', async () => { + const tx = stRIF.connect(holder).withdrawTo(holder.address, votingPower) + await expect(tx).to.emit(stRIF, 'Transfer').withArgs(holder.address, ethers.ZeroAddress, votingPower) + }) + + it('holder should no longer own stRIF tokens', async () => { + expect(await stRIF.balanceOf(holder.address)).to.equal(0) + }) + + it('holder should return his RIFs back', async () => { + expect(await rif.balanceOf(holder.address)).to.equal(votingPower) + }) + + it('stRIF should no longer own RIFs', async () => { + expect(await rif.balanceOf(await stRIF.getAddress())).to.equal(0) + }) + + it('stRIF should no longer have allowance for RIFs from the holder', async () => { + expect(await rif.allowance(holder.address, await stRIF.getAddress())).to.equal(0) + }) + + it('holder should still have the delegate set', async () => { + expect(await stRIF.delegates(holder.address)).to.equal(holder.address) + }) + + it('holder should no longer have voting power', async () => { + const vp = await stRIF.getVotes(holder.address) + expect(vp).to.equal(0) + }) + }) + + describe('Delegating voting power to a voter address', () => { + it('should already have 2 checkpoints because of delegation and burning operations', async () => { + const numCheckpoints = await stRIF.numCheckpoints(holder.address) + expect(numCheckpoints).to.equal(2) + }) + + it('holder should mint stRIF again', async () => { + ;(await rif.connect(holder).approve(await stRIF.getAddress(), votingPower)).wait() + await expect(stRIF.connect(holder).depositFor(holder.address, votingPower)) + .to.emit(stRIF, 'Transfer') + .withArgs(ethers.ZeroAddress, holder.address, votingPower) + const checkPoint2 = await stRIF.checkpoints(holder.address, 2) + expect(checkPoint2._value).to.equal(votingPower) + }) + + it('should have 3 checkpoints now', async () => { + const numCheckpoints = await stRIF.numCheckpoints(holder.address) + expect(numCheckpoints).to.equal(3) + }) + + it('holder should still be delegated to vote (from the previous time)', async () => { + expect(await stRIF.delegates(holder.address)).to.equal(holder.address) + }) + + it('holder should already have vote power', async () => { + expect(await stRIF.getVotes(holder.address)).to.equal(votingPower) + }) + + it('holder should delegate his voting power to the voter (another address)', async () => { + const tx = stRIF.connect(holder).delegate(voter.address) + await expect(tx) + .to.emit(stRIF, 'DelegateChanged') + .withArgs(holder.address, holder.address, voter.address) + }) + + it('should have 4 checkpoints now', async () => { + const numCheckpoints = await stRIF.numCheckpoints(holder.address) + expect(numCheckpoints).to.equal(4) + }) + + it('holder should NOT have vote power any more', async () => { + expect(await stRIF.getVotes(holder.address)).to.equal(0) + }) + + it("voter should now have holder's voting power", async () => { + 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) + }) + }) +}) From dc1778a65fd4eefb3e97f98887847fb574bcb5f7 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 12 Nov 2024 21:58:30 +0100 Subject: [PATCH 2/6] feat: add StRIFTokenV02 contract and upgrade logic for version 2 --- contracts/StRIFTokenV02.sol | 6 ++++++ ignition/modules/StRifV02Module.ts | 18 ++++++++++++++++ test/StRIFTokenV02.test.ts | 34 ++++++++++++++++++++++-------- 3 files changed, 49 insertions(+), 9 deletions(-) create mode 100644 ignition/modules/StRifV02Module.ts diff --git a/contracts/StRIFTokenV02.sol b/contracts/StRIFTokenV02.sol index cac7103..15c9f1c 100644 --- a/contracts/StRIFTokenV02.sol +++ b/contracts/StRIFTokenV02.sol @@ -57,6 +57,12 @@ contract StRIFTokenV02 is __UUPSUpgradeable_init(); } + function initializeV2() public onlyProxy reinitializer(2) {} + + function version() public pure virtual returns (uint64) { + return 2; + } + /** * @dev Allows token holder to transfer tokens to another account, after which * the recipient automatically delegates votes to themselves if they do diff --git a/ignition/modules/StRifV02Module.ts b/ignition/modules/StRifV02Module.ts new file mode 100644 index 0000000..a36ff1a --- /dev/null +++ b/ignition/modules/StRifV02Module.ts @@ -0,0 +1,18 @@ +import { buildModule } from '@nomicfoundation/hardhat-ignition/modules' + +export const stRifV02Module = buildModule('StRIFTokenV02', m => { + const stRIFTokenProxyAddress = m.getParameter('StRifAddress') + + const stRIFTokenProxy = m.contractAt('StRIFToken', stRIFTokenProxyAddress) + + const newImplementation = m.contract('StRIFTokenV02', [], { id: 'Implementation02' }) + + const reInitCall = m.encodeFunctionCall(newImplementation, 'initializeV2', []) + + m.call(stRIFTokenProxy, 'upgradeToAndCall', [newImplementation, reInitCall], { id: 'Reinitialize02' }) + + const stRifV02 = m.contractAt('StRIFTokenV02', stRIFTokenProxy, { id: 'Contract02' }) + + return { stRifV02 } +}) +export default stRifV02Module diff --git a/test/StRIFTokenV02.test.ts b/test/StRIFTokenV02.test.ts index 9806244..a985845 100644 --- a/test/StRIFTokenV02.test.ts +++ b/test/StRIFTokenV02.test.ts @@ -1,7 +1,7 @@ import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers' import { loadFixture } from '@nomicfoundation/hardhat-toolbox/network-helpers' import { expect } from 'chai' -import { ethers } from 'hardhat' +import { ethers, ignition } from 'hardhat' import { ContractDoesNotSupportERC165andICollectiveRewardscheck, ContractDoesNotSupportICollectiveRewardsCheck, @@ -11,8 +11,9 @@ import { StRIFTokenV02, } from '../typechain-types' import { deployContracts } from './deployContracts' +import stRifV02Module from '../ignition/modules/StRifV02Module' -describe('stRIFToken', () => { +describe('stRIFToken Version 2', () => { let owner: SignerWithAddress, holder: SignerWithAddress, voter: SignerWithAddress let rif: RIFToken let stRIF: StRIFTokenV02 @@ -22,15 +23,30 @@ describe('stRIFToken', () => { 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, - ]) + const contracts = await loadFixture(deployContracts) + const { stRIF: stRIFV01 } = contracts + ;({ rif } = contracts) + stRIF = ( + await ignition.deploy(stRifV02Module, { + parameters: { + StRIFTokenV02: { + StRifAddress: await stRIFV01.getAddress(), + }, + }, + }) + ).stRifV02 as unknown as StRIFTokenV02 + ContractDoesNotSupportERC165andICollectiveRewardscheck = await ethers.deployContract( + 'ContractDoesNotSupportERC165andICollectiveRewardscheck', + ) + ContractDoesNotSupportICollectiveRewardsCheck = await ethers.deployContract( + 'ContractDoesNotSupportICollectiveRewardsCheck', + ) + ContractSupportsERC165andICollectiveRewardscheck = await ethers.deployContract( + 'ContractSupportsERC165andICollectiveRewardscheck', + [holder], + ) ContractWithErrorInCanWithdraw = await ethers.deployContract('ContractWithErrorInCanWithdraw', [voter]) }) From ecff503f24094eb58355fb79207119efc33b12b6 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 12 Nov 2024 22:16:39 +0100 Subject: [PATCH 3/6] fix: restore original StRif v1 before upgrading --- contracts/StRIFToken.sol | 71 +---------------- contracts/StRIFTokenV02.sol | 1 + ignition/modules/StRifV02Module.ts | 3 + test/StRIFToken.test.ts | 117 +---------------------------- 4 files changed, 9 insertions(+), 183 deletions(-) diff --git a/contracts/StRIFToken.sol b/contracts/StRIFToken.sol index 7e39bfb..37d7a49 100644 --- a/contracts/StRIFToken.sol +++ b/contracts/StRIFToken.sol @@ -11,10 +11,6 @@ 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, @@ -25,24 +21,6 @@ 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(); @@ -109,44 +87,6 @@ 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 @@ -160,20 +100,13 @@ contract StRIFToken is address from, address to, uint256 value - ) internal override(ERC20Upgradeable, ERC20VotesUpgradeable) _checkCollectiveRewardsForStake(from, value) { + ) internal override(ERC20Upgradeable, ERC20VotesUpgradeable) { 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) { return super.nonces(owner); } -} +} \ No newline at end of file diff --git a/contracts/StRIFTokenV02.sol b/contracts/StRIFTokenV02.sol index 15c9f1c..667552e 100644 --- a/contracts/StRIFTokenV02.sol +++ b/contracts/StRIFTokenV02.sol @@ -16,6 +16,7 @@ import {ERC165Checker} from "@openzeppelin/contracts/utils/introspection/ERC165C import {ICollectiveRewardsCheck} from "./interfaces/ICollectiveRewardsCheck.sol"; + contract StRIFTokenV02 is Initializable, ERC20Upgradeable, diff --git a/ignition/modules/StRifV02Module.ts b/ignition/modules/StRifV02Module.ts index a36ff1a..c2872ee 100644 --- a/ignition/modules/StRifV02Module.ts +++ b/ignition/modules/StRifV02Module.ts @@ -1,3 +1,6 @@ +/** + * This Hardhat Ignition module upgrades StRIF contract v.1 to v.2 + */ import { buildModule } from '@nomicfoundation/hardhat-ignition/modules' export const stRifV02Module = buildModule('StRIFTokenV02', m => { diff --git a/test/StRIFToken.test.ts b/test/StRIFToken.test.ts index 020252b..2d4607c 100644 --- a/test/StRIFToken.test.ts +++ b/test/StRIFToken.test.ts @@ -2,36 +2,19 @@ import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers' import { loadFixture } from '@nomicfoundation/hardhat-toolbox/network-helpers' import { expect } from 'chai' import { ethers } from 'hardhat' -import { - ContractDoesNotSupportERC165andICollectiveRewardscheck, - ContractDoesNotSupportICollectiveRewardsCheck, - ContractSupportsERC165andICollectiveRewardscheck, - ContractWithErrorInCanWithdraw, - RIFToken, - StRIFToken, -} from '../typechain-types' +import { 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() + ;;[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 () => { @@ -195,98 +178,4 @@ 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) - }) - }) -}) +}) \ No newline at end of file From b06c194126fa3dfc2c4a72e91162fe87e8774d0f Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 12 Nov 2024 22:34:36 +0100 Subject: [PATCH 4/6] test: update tests for versioning --- contracts/StRIFToken.sol | 2 +- contracts/StRIFTokenV02.sol | 4 +++- test/StRIFTokenV02.test.ts | 6 ++++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/contracts/StRIFToken.sol b/contracts/StRIFToken.sol index 37d7a49..78a8eb1 100644 --- a/contracts/StRIFToken.sol +++ b/contracts/StRIFToken.sol @@ -109,4 +109,4 @@ contract StRIFToken is ) public view override(ERC20PermitUpgradeable, NoncesUpgradeable) returns (uint256) { return super.nonces(owner); } -} \ No newline at end of file +} diff --git a/contracts/StRIFTokenV02.sol b/contracts/StRIFTokenV02.sol index 667552e..1d1398e 100644 --- a/contracts/StRIFTokenV02.sol +++ b/contracts/StRIFTokenV02.sol @@ -16,7 +16,9 @@ import {ERC165Checker} from "@openzeppelin/contracts/utils/introspection/ERC165C import {ICollectiveRewardsCheck} from "./interfaces/ICollectiveRewardsCheck.sol"; - +/** + * @dev StRIFTokenV02 adds support for CollectiveRewards + */ contract StRIFTokenV02 is Initializable, ERC20Upgradeable, diff --git a/test/StRIFTokenV02.test.ts b/test/StRIFTokenV02.test.ts index a985845..9721833 100644 --- a/test/StRIFTokenV02.test.ts +++ b/test/StRIFTokenV02.test.ts @@ -50,6 +50,12 @@ describe('stRIFToken Version 2', () => { ContractWithErrorInCanWithdraw = await ethers.deployContract('ContractWithErrorInCanWithdraw', [voter]) }) + describe('Upon upgrade to V2', () => { + it('should have version 2', async () => { + expect(await stRIF.version()).to.equal(2) + }) + }) + it('Should assign the initial balance to the contract itself', async () => { const contractBalance = await rif.balanceOf(owner) expect(contractBalance).to.equal(ethers.parseUnits('1000000000', 18)) From 607266fd3acbd5fe30d167dade000e3cc4faaf51 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 13 Nov 2024 02:05:48 +0100 Subject: [PATCH 5/6] chore: add StRIFToken upgrade script params --- README.md | 4 ++-- ignition/modules/StRifV02Module.ts | 6 +++--- params/StRif/v2UpgradeTestnet.json | 5 +++++ test/StRIFToken.test.ts | 4 ++++ 4 files changed, 14 insertions(+), 5 deletions(-) create mode 100644 params/StRif/v2UpgradeTestnet.json diff --git a/README.md b/README.md index 2c2c48d..e960f69 100644 --- a/README.md +++ b/README.md @@ -223,8 +223,8 @@ Successfully verified contract "contracts/EarlyAdopters.sol:EarlyAdopters" for n | ---------------------------------------------- | ------------------------------------------ | | GovernorRootstockCollective impl | 0x2109FF4a9D5548a21F877cA937Ac5847Fde49694 | | GovernorRootstockCollective proxy | 0x91a8E4A070B4BA4bf2e2a51Cb42BdeDf8FFB9b5a | -| StRIFToken impl | 0x4861198e9A6814EBfb152552D1b1a37426C54D23 | -| StRIFToken proxy | 0xFff256c3451D5cF59653Cfe71950AE9ba2F5f0Ef | +| StRIFToken proxy | 0x4861198e9A6814EBfb152552D1b1a37426C54D23 | +| StRIFToken impl | 0xFff256c3451D5cF59653Cfe71950AE9ba2F5f0Ef | | DaoTimelockUpgradableRootstockCollective impl | 0x2AEdf0B35651934cF3BEC855cbCE207bBA0C4aB5 | | DaoTimelockUpgradableRootstockCollective proxy | 0x5eDA6fA73350291F7D7cFC7ad93F48189f1333ef | | TreasuryRootstockCollective | 0x47C969d7ae7A377BeaD553c2899D9B83A90e0772 | diff --git a/ignition/modules/StRifV02Module.ts b/ignition/modules/StRifV02Module.ts index c2872ee..8352932 100644 --- a/ignition/modules/StRifV02Module.ts +++ b/ignition/modules/StRifV02Module.ts @@ -8,13 +8,13 @@ export const stRifV02Module = buildModule('StRIFTokenV02', m => { const stRIFTokenProxy = m.contractAt('StRIFToken', stRIFTokenProxyAddress) - const newImplementation = m.contract('StRIFTokenV02', [], { id: 'Implementation02' }) + const newImplementation = m.contract('StRIFTokenV02', [], { id: 'Implementation' }) const reInitCall = m.encodeFunctionCall(newImplementation, 'initializeV2', []) - m.call(stRIFTokenProxy, 'upgradeToAndCall', [newImplementation, reInitCall], { id: 'Reinitialize02' }) + m.call(stRIFTokenProxy, 'upgradeToAndCall', [newImplementation, reInitCall], { id: 'Reinitialize' }) - const stRifV02 = m.contractAt('StRIFTokenV02', stRIFTokenProxy, { id: 'Contract02' }) + const stRifV02 = m.contractAt('StRIFTokenV02', stRIFTokenProxy, { id: 'Contract' }) return { stRifV02 } }) diff --git a/params/StRif/v2UpgradeTestnet.json b/params/StRif/v2UpgradeTestnet.json new file mode 100644 index 0000000..223ba51 --- /dev/null +++ b/params/StRif/v2UpgradeTestnet.json @@ -0,0 +1,5 @@ +{ + "StRIFTokenV02": { + "StRifAddress": "0x47955BbC3a077FFA59BD7aedf25fcD1f2f0360e3" + } +} \ No newline at end of file diff --git a/test/StRIFToken.test.ts b/test/StRIFToken.test.ts index 2d4607c..55ec3db 100644 --- a/test/StRIFToken.test.ts +++ b/test/StRIFToken.test.ts @@ -22,6 +22,10 @@ describe('stRIFToken', () => { expect(contractBalance).to.equal(ethers.parseUnits('1000000000', 18)) }) + it('deployer should be the owner', async () => { + expect(await stRIF.owner()).to.equal(owner.address) + }) + describe('Wrapping RIF tokens to stRIF', () => { it('holder should NOT initially own RIF tokens', async () => { expect(await rif.balanceOf(holder.address)).to.equal(0) From f5946b5bdaaca99a156dd775fed45fa4c718a4fe Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 13 Nov 2024 17:53:50 +0100 Subject: [PATCH 6/6] test: merge tests for stRif v1 and v2 into one file --- test/StRIFToken.test.ts | 147 ++++++++++++++++- test/StRIFTokenV02.test.ts | 314 ------------------------------------- 2 files changed, 144 insertions(+), 317 deletions(-) delete mode 100644 test/StRIFTokenV02.test.ts diff --git a/test/StRIFToken.test.ts b/test/StRIFToken.test.ts index 55ec3db..50fd39f 100644 --- a/test/StRIFToken.test.ts +++ b/test/StRIFToken.test.ts @@ -1,9 +1,18 @@ 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 { ethers, ignition } from 'hardhat' +import { + RIFToken, + StRIFToken, + StRIFTokenV02, + ContractDoesNotSupportERC165andICollectiveRewardscheck, + ContractDoesNotSupportICollectiveRewardsCheck, + ContractSupportsERC165andICollectiveRewardscheck, + ContractWithErrorInCanWithdraw, +} from '../typechain-types' import { deployContracts } from './deployContracts' +import stRifV02Module from '../ignition/modules/StRifV02Module' describe('stRIFToken', () => { let owner: SignerWithAddress, holder: SignerWithAddress, voter: SignerWithAddress @@ -182,4 +191,136 @@ describe('stRIFToken', () => { expect(await stRIF.getVotes(voter.address)).to.equal(votingPower) }) }) -}) \ No newline at end of file + + describe('Upgrade to version 2', () => { + let stRIFV2: StRIFTokenV02 + + let ContractSupportsERC165andICollectiveRewardscheck: ContractSupportsERC165andICollectiveRewardscheck + let ContractDoesNotSupportERC165andICollectiveRewardscheck: ContractDoesNotSupportERC165andICollectiveRewardscheck + let ContractDoesNotSupportICollectiveRewardsCheck: ContractDoesNotSupportICollectiveRewardsCheck + let ContractWithErrorInCanWithdraw: ContractWithErrorInCanWithdraw + + before(async () => { + ContractDoesNotSupportERC165andICollectiveRewardscheck = await ethers.deployContract( + 'ContractDoesNotSupportERC165andICollectiveRewardscheck', + ) + ContractDoesNotSupportICollectiveRewardsCheck = await ethers.deployContract( + 'ContractDoesNotSupportICollectiveRewardsCheck', + ) + ContractSupportsERC165andICollectiveRewardscheck = await ethers.deployContract( + 'ContractSupportsERC165andICollectiveRewardscheck', + [holder], + ) + ContractWithErrorInCanWithdraw = await ethers.deployContract('ContractWithErrorInCanWithdraw', [voter]) + }) + + it('should be upgraded to V2 by upgrade script', async () => { + stRIFV2 = ( + await ignition.deploy(stRifV02Module, { + parameters: { + StRIFTokenV02: { + StRifAddress: await stRIF.getAddress(), + }, + }, + }) + ).stRifV02 as unknown as StRIFTokenV02 + expect(await stRIFV2.version()).to.equal(2) + }) + + 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 = stRIFV2 + .connect(holder) + .setCollectiveRewardsAddress(ContractSupportsERC165andICollectiveRewardscheck) + await expect(tx).to.be.revertedWithCustomError( + { interface: stRIFV2.interface }, + 'OwnableUnauthorizedAccount', + ) + }) + + it('setting CollectiveRewards address should fail if contract does not support ERC165 with STRIFSupportsICollectiveRewardsCheck', async () => { + const tx = stRIFV2.setCollectiveRewardsAddress( + await ContractDoesNotSupportERC165andICollectiveRewardscheck.getAddress(), + ) + await expect(tx).to.be.revertedWithCustomError( + { interface: stRIFV2.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 = stRIFV2.setCollectiveRewardsAddress( + await ContractDoesNotSupportERC165andICollectiveRewardscheck.getAddress(), + ) + await expect(tx).to.be.revertedWithCustomError( + { interface: stRIFV2.interface }, + 'STRIFSupportsICollectiveRewardsCheck', + ) + }) + + it('should set CollectiveRewards address if canWithdraw returns boolean', async () => { + const address = await ContractSupportsERC165andICollectiveRewardscheck.getAddress() + await stRIFV2.setCollectiveRewardsAddress(address) + + expect(await stRIFV2.collectiveRewardsCheck()).to.equal(address) + }) + + it('should revert withdrawTo, _update with STRIFStakedInCollectiveRewardsCanWithdraw if bimCheck returns false', async () => { + expect(await stRIFV2.balanceOf(holder)).to.equal(votingPower) + + const tx = stRIFV2.connect(holder).withdrawTo(holder.address, votingPower) + await expect(tx).to.be.revertedWithCustomError( + { interface: stRIFV2.interface }, + 'STRIFStakedInCollectiveRewardsCanWithdraw', + ) + + //runs _update under the hood + const transferTx = stRIFV2.connect(holder).transfer(voter, votingPower) + await expect(transferTx).to.be.revertedWithCustomError( + { interface: stRIFV2.interface }, + 'STRIFStakedInCollectiveRewardsCanWithdraw', + ) + expect(await stRIFV2.balanceOf(holder)).to.equal(votingPower) + }) + + it('should allow withdrawTo if bimCheck returns true', async () => { + await ContractSupportsERC165andICollectiveRewardscheck.setBlockedAddress(voter) + expect(await stRIFV2.balanceOf(holder)).to.equal(votingPower) + + const value = votingPower / 2n + const tx = stRIFV2.connect(holder).withdrawTo(holder.address, value) + await expect(tx).to.emit(stRIFV2, 'Transfer').withArgs(holder.address, ethers.ZeroAddress, value) + }) + + it('should throw an error if _shouldSkipError is false', async () => { + const address = await ContractWithErrorInCanWithdraw.getAddress() + const setTX = stRIFV2.setCollectiveRewardsAddress(address) + await expect(setTX).to.emit(stRIFV2, 'CollectiveRewardsAddressHasBeenChanged').withArgs(address) + + const tx = stRIFV2.connect(holder).withdrawTo(holder.address, votingPower / 2n) + await expect(tx).to.be.revertedWithCustomError( + { interface: stRIFV2.interface }, + 'CollectiveRewardsErrored', + ) + }) + + it('should ignore Collective Rewards error if _shouldSkipError is true', async () => { + const skipTX = stRIFV2.setCollectiveRewardsErrorSkipFlag(true) + await expect(skipTX).to.emit(stRIFV2, 'STRIFCollectiveRewardsErrorSkipChangedTo').withArgs(true) + + const value = votingPower / 2n + const tx = stRIFV2.connect(holder).withdrawTo(holder.address, value) + await expect(tx).to.emit(stRIFV2, 'Transfer').withArgs(holder.address, ethers.ZeroAddress, value) + }) + }) + }) +}) diff --git a/test/StRIFTokenV02.test.ts b/test/StRIFTokenV02.test.ts deleted file mode 100644 index 9721833..0000000 --- a/test/StRIFTokenV02.test.ts +++ /dev/null @@ -1,314 +0,0 @@ -import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers' -import { loadFixture } from '@nomicfoundation/hardhat-toolbox/network-helpers' -import { expect } from 'chai' -import { ethers, ignition } from 'hardhat' -import { - ContractDoesNotSupportERC165andICollectiveRewardscheck, - ContractDoesNotSupportICollectiveRewardsCheck, - ContractSupportsERC165andICollectiveRewardscheck, - ContractWithErrorInCanWithdraw, - RIFToken, - StRIFTokenV02, -} from '../typechain-types' -import { deployContracts } from './deployContracts' -import stRifV02Module from '../ignition/modules/StRifV02Module' - -describe('stRIFToken Version 2', () => { - let owner: SignerWithAddress, holder: SignerWithAddress, voter: SignerWithAddress - let rif: RIFToken - let stRIF: StRIFTokenV02 - let ContractSupportsERC165andICollectiveRewardscheck: ContractSupportsERC165andICollectiveRewardscheck - let ContractDoesNotSupportERC165andICollectiveRewardscheck: ContractDoesNotSupportERC165andICollectiveRewardscheck - let ContractDoesNotSupportICollectiveRewardsCheck: ContractDoesNotSupportICollectiveRewardsCheck - let ContractWithErrorInCanWithdraw: ContractWithErrorInCanWithdraw - const votingPower = 10n * 10n ** 18n - - before(async () => { - ;[owner, holder, voter] = await ethers.getSigners() - const contracts = await loadFixture(deployContracts) - const { stRIF: stRIFV01 } = contracts - ;({ rif } = contracts) - stRIF = ( - await ignition.deploy(stRifV02Module, { - parameters: { - StRIFTokenV02: { - StRifAddress: await stRIFV01.getAddress(), - }, - }, - }) - ).stRifV02 as unknown as StRIFTokenV02 - ContractDoesNotSupportERC165andICollectiveRewardscheck = await ethers.deployContract( - 'ContractDoesNotSupportERC165andICollectiveRewardscheck', - ) - ContractDoesNotSupportICollectiveRewardsCheck = await ethers.deployContract( - 'ContractDoesNotSupportICollectiveRewardsCheck', - ) - ContractSupportsERC165andICollectiveRewardscheck = await ethers.deployContract( - 'ContractSupportsERC165andICollectiveRewardscheck', - [holder], - ) - ContractWithErrorInCanWithdraw = await ethers.deployContract('ContractWithErrorInCanWithdraw', [voter]) - }) - - describe('Upon upgrade to V2', () => { - it('should have version 2', async () => { - expect(await stRIF.version()).to.equal(2) - }) - }) - - it('Should assign the initial balance to the contract itself', async () => { - const contractBalance = await rif.balanceOf(owner) - expect(contractBalance).to.equal(ethers.parseUnits('1000000000', 18)) - }) - - describe('Wrapping RIF tokens to stRIF', () => { - it('holder should NOT initially own RIF tokens', async () => { - expect(await rif.balanceOf(holder.address)).to.equal(0) - }) - - it("owner should send some RIFs to holder's address", async () => { - const tx = await rif.transfer(holder.address, votingPower) - await tx.wait() - expect(tx) - .to.emit(rif, 'Transfer') - .withArgs(await rif.getAddress(), holder.address, votingPower) - }) - - it('holder should approve allowance for stRIF', async () => { - const tx = await rif.connect(holder).approve(stRIF.getAddress(), votingPower) - await tx.wait() - expect(tx).to.emit(rif, 'Approval').withArgs(holder.address, stRIF.getAddress(), votingPower) - }) - - it('allowance for stRIF should be set on the RIF token', async () => { - expect(await rif.allowance(holder.address, stRIF.getAddress())).to.equal(votingPower) - }) - - it('stRIF should NOT have any RIF tokens on its balance', async () => { - expect(await rif.balanceOf(stRIF.getAddress())).to.equal(0) - }) - - it('holder should NOT have any stRIF tokens on his balance', async () => { - expect(await stRIF.balanceOf(holder.address)).to.equal(0) - }) - - /** depositFor is a method for minting stRIF tokens */ - it('holder should deposit underlying tokens and mint the corresponding amount of stRIF tokens', async () => { - await expect(stRIF.connect(holder).depositFor(holder.address, votingPower)) - .to.emit(stRIF, 'Transfer') - .withArgs(ethers.ZeroAddress, holder.address, votingPower) - }) - - it('holder should NOT have RIF tokens anymore', async () => { - expect(await rif.balanceOf(holder.address)).to.equal(0) - }) - - it('stRIF now should own RIFs belonged to the holder', async () => { - expect(await rif.balanceOf(stRIF.getAddress())).to.equal(votingPower) - }) - - it('holder should have the same amount of stRIF tokens as the deposited RIF tokens', async () => { - expect(await stRIF.balanceOf(holder.address)).to.equal(votingPower) - }) - - it('holder should NOT be able to deposit more RIF tokens than he has', async () => { - await expect(stRIF.connect(holder).depositFor(holder.address, votingPower)).to.be.reverted - }) - - /** delegate */ - it('holder should NOT have vote power yet', async () => { - expect(await stRIF.getVotes(holder.address)).to.equal(0) - }) - - it('holder should delegate vote power to himself', async () => { - const tx = await stRIF.connect(holder).delegate(holder.address) - await expect(tx) - .to.emit(stRIF, 'DelegateChanged') - .withArgs(holder.address, ethers.ZeroAddress, holder.address) - }) - - it('holder should now have delegate set', async () => { - expect(await stRIF.delegates(holder.address)).to.equal(holder.address) - }) - - it('holder should have vote power', async () => { - expect(await stRIF.getVotes(holder.address)).to.equal(votingPower) - }) - }) - - describe('Unwrapping RIF tokens from stRIF tokens', () => { - /** withdrawTo is a method for burning stRIF tokens */ - it('holder should burn stRIF tokens', async () => { - const tx = stRIF.connect(holder).withdrawTo(holder.address, votingPower) - await expect(tx).to.emit(stRIF, 'Transfer').withArgs(holder.address, ethers.ZeroAddress, votingPower) - }) - - it('holder should no longer own stRIF tokens', async () => { - expect(await stRIF.balanceOf(holder.address)).to.equal(0) - }) - - it('holder should return his RIFs back', async () => { - expect(await rif.balanceOf(holder.address)).to.equal(votingPower) - }) - - it('stRIF should no longer own RIFs', async () => { - expect(await rif.balanceOf(await stRIF.getAddress())).to.equal(0) - }) - - it('stRIF should no longer have allowance for RIFs from the holder', async () => { - expect(await rif.allowance(holder.address, await stRIF.getAddress())).to.equal(0) - }) - - it('holder should still have the delegate set', async () => { - expect(await stRIF.delegates(holder.address)).to.equal(holder.address) - }) - - it('holder should no longer have voting power', async () => { - const vp = await stRIF.getVotes(holder.address) - expect(vp).to.equal(0) - }) - }) - - describe('Delegating voting power to a voter address', () => { - it('should already have 2 checkpoints because of delegation and burning operations', async () => { - const numCheckpoints = await stRIF.numCheckpoints(holder.address) - expect(numCheckpoints).to.equal(2) - }) - - it('holder should mint stRIF again', async () => { - ;(await rif.connect(holder).approve(await stRIF.getAddress(), votingPower)).wait() - await expect(stRIF.connect(holder).depositFor(holder.address, votingPower)) - .to.emit(stRIF, 'Transfer') - .withArgs(ethers.ZeroAddress, holder.address, votingPower) - const checkPoint2 = await stRIF.checkpoints(holder.address, 2) - expect(checkPoint2._value).to.equal(votingPower) - }) - - it('should have 3 checkpoints now', async () => { - const numCheckpoints = await stRIF.numCheckpoints(holder.address) - expect(numCheckpoints).to.equal(3) - }) - - it('holder should still be delegated to vote (from the previous time)', async () => { - expect(await stRIF.delegates(holder.address)).to.equal(holder.address) - }) - - it('holder should already have vote power', async () => { - expect(await stRIF.getVotes(holder.address)).to.equal(votingPower) - }) - - it('holder should delegate his voting power to the voter (another address)', async () => { - const tx = stRIF.connect(holder).delegate(voter.address) - await expect(tx) - .to.emit(stRIF, 'DelegateChanged') - .withArgs(holder.address, holder.address, voter.address) - }) - - it('should have 4 checkpoints now', async () => { - const numCheckpoints = await stRIF.numCheckpoints(holder.address) - expect(numCheckpoints).to.equal(4) - }) - - it('holder should NOT have vote power any more', async () => { - expect(await stRIF.getVotes(holder.address)).to.equal(0) - }) - - it("voter should now have holder's voting power", async () => { - 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) - }) - }) -})