From 4d38d64e62627742c963e40d6787b51683b7a7dd Mon Sep 17 00:00:00 2001 From: Hadrien Charlanes Date: Wed, 24 Mar 2021 17:02:36 +0100 Subject: [PATCH 01/14] Added snapshot of exchange rate + search + updated power calculation --- contracts/interfaces/IStakedTokenV3.sol | 2 + contracts/stake/StakedTokenV3.sol | 134 +++++++++++++++++++++--- 2 files changed, 121 insertions(+), 15 deletions(-) diff --git a/contracts/interfaces/IStakedTokenV3.sol b/contracts/interfaces/IStakedTokenV3.sol index 9506a66..94c02f3 100644 --- a/contracts/interfaces/IStakedTokenV3.sol +++ b/contracts/interfaces/IStakedTokenV3.sol @@ -12,6 +12,8 @@ interface IStakedTokenV3 is IStakedToken { function slash(address destination, uint256 amount) external; + function donate(uint256 amount) external; + function getMaxSlashablePercentage() external view returns (uint256); function setMaxSlashablePercentage(uint256 percentage) external; diff --git a/contracts/stake/StakedTokenV3.sol b/contracts/stake/StakedTokenV3.sol index 742651b..b23b25a 100644 --- a/contracts/stake/StakedTokenV3.sol +++ b/contracts/stake/StakedTokenV3.sol @@ -44,6 +44,9 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { address internal _claimHelper; + mapping(uint256 => Snapshot) internal _exchangeRateSnapshots; + uint256 internal _countExchangeRateSnapshots; + modifier onlyAdmin { require(msg.sender == getAdmin(MAIN_ADMIN_ROLE), 'CALLER_NOT_MAIN_ADMIN'); _; @@ -69,6 +72,8 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { event CooldownPauseChanged(bool pause); event MaxSlashablePercentageChanged(uint256 newPercentage); event Slashed(address indexed destination, uint256 amount); + event Donated(address indexed sender, uint256 amount); + event EchangeRateSnapshotted(uint128 blockNumber, uint128 exchangeRate); event CooldownPauseAdminChanged(address indexed newAdmin); event SlashingAdminChanged(address indexed newAdmin); event ClaimHelperChanged(address indexed newClaimHelper); @@ -301,21 +306,6 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { _redeem(from, to, redeemAmount); } - /** - * @dev Calculates the exchange rate between the amount of STAKED_TOKEN and the the StakeToken total supply. - * Slashing will reduce the exchange rate. Supplying STAKED_TOKEN to the stake contract - * can replenish the slashed STAKED_TOKEN and bring the exchange rate back to 1 - **/ - function exchangeRate() public view override returns (uint256) { - uint256 currentSupply = totalSupply(); - - if (currentSupply == 0) { - return 1e18; //initial exchange rate is 1:1 - } - - return STAKED_TOKEN.balanceOf(address(this)).mul(1e18).div(currentSupply); - } - /** * @dev Executes a slashing of the underlying of a certain amount, transferring the seized funds * to destination. Decreasing the amount of underlying will automatically adjust the exchange rate @@ -330,10 +320,18 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { require(amount <= maxSlashable, 'INVALID_SLASHING_AMOUNT'); STAKED_TOKEN.safeTransfer(destination, amount); + _snapshotExchangeRate(); emit Slashed(destination, amount); } + function donate(uint256 amount) external override { + STAKED_TOKEN.safeTransferFrom(msg.sender, address(this), amount); + _snapshotExchangeRate(); + + emit Donated(msg.sender, amount); + } + /** * @dev Set the address of the contract with priviledge, the ClaimHelper contract * It speicifically enables to claim from several contracts at once @@ -364,6 +362,21 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { emit MaxSlashablePercentageChanged(percentage); } + /** + * @dev Calculates the exchange rate between the amount of STAKED_TOKEN and the the StakeToken total supply. + * Slashing will reduce the exchange rate. Supplying STAKED_TOKEN to the stake contract + * can replenish the slashed STAKED_TOKEN and bring the exchange rate back to 1 + **/ + function exchangeRate() public view override returns (uint256) { + uint256 currentSupply = totalSupply(); + + if (currentSupply == 0) { + return 1e18; //initial exchange rate is 1:1 + } + + return STAKED_TOKEN.balanceOf(address(this)).mul(1e18).div(currentSupply); + } + /** * @dev returns the current address of the claimHelper Contract, contract with priviledge * It speicifically enables to claim from several contracts at once @@ -394,6 +407,43 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { return REVISION(); } + function getPowerAtBlock( + address user, + uint256 blockNumber, + DelegationType delegationType + ) external view override returns (uint256) { + ( + mapping(address => mapping(uint256 => Snapshot)) storage snapshots, + mapping(address => uint256) storage snapshotsCounts, + + ) = _getDelegationDataByType(delegationType); + + return ( + _searchByBlockNumber(snapshots, snapshotsCounts, user, blockNumber) + .mul(_searchExchangeRateByBlockNumber(blockNumber)) + .div(1e18) + ); + } + + function getPowerCurrent(address user, DelegationType delegationType) + external + view + override + returns (uint256) + { + ( + mapping(address => mapping(uint256 => Snapshot)) storage snapshots, + mapping(address => uint256) storage snapshotsCounts, + + ) = _getDelegationDataByType(delegationType); + + return ( + _searchByBlockNumber(snapshots, snapshotsCounts, user, block.number).mul(exchangeRate()).div( + 1e18 + ) + ); + } + function _claimRewards( address from, address to, @@ -479,4 +529,58 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { emit Redeem(from, to, amountToRedeem, underlyingToRedeem); } + + function _snapshotExchangeRate() internal { + uint128 currentBlock = uint128(block.number); + uint128 newExchangeRate = uint128(exchangeRate()); + + // Doing multiple operations in the same block + if ( + _countExchangeRateSnapshots != 0 && + _exchangeRateSnapshots[_countExchangeRateSnapshots - 1].blockNumber == currentBlock + ) { + _exchangeRateSnapshots[_countExchangeRateSnapshots - 1].value = newExchangeRate; + } else { + _exchangeRateSnapshots[_countExchangeRateSnapshots] = Snapshot(currentBlock, newExchangeRate); + _countExchangeRateSnapshots = _countExchangeRateSnapshots + 1; + } + emit EchangeRateSnapshotted(currentBlock, newExchangeRate); + } + + /** + * @dev searches a exchange Rate by block number. Uses binary search. + * @param blockNumber the block number being searched + **/ + function _searchExchangeRateByBlockNumber(uint256 blockNumber) internal view returns (uint256) { + require(blockNumber <= block.number, 'INVALID_BLOCK_NUMBER'); + + if (_countExchangeRateSnapshots == 0) { + return exchangeRate(); + } + + // First check most recent balance + if (_exchangeRateSnapshots[_countExchangeRateSnapshots - 1].blockNumber <= blockNumber) { + return _exchangeRateSnapshots[_countExchangeRateSnapshots - 1].value; + } + + // Next check implicit zero balance + if (_exchangeRateSnapshots[0].blockNumber > blockNumber) { + return 1e18; //initial exchange rate is 1:1 + } + + uint256 lower = 0; + uint256 upper = _countExchangeRateSnapshots - 1; + while (upper > lower) { + uint256 center = upper - (upper - lower) / 2; // ceil, avoiding overflow + Snapshot memory snapshot = _exchangeRateSnapshots[center]; + if (snapshot.blockNumber == blockNumber) { + return snapshot.value; + } else if (snapshot.blockNumber < blockNumber) { + lower = center; + } else { + upper = center - 1; + } + } + return _exchangeRateSnapshots[lower].value; + } } From 0d685435d8bd648e9c722d33f22ceacbc496d3e3 Mon Sep 17 00:00:00 2001 From: Hadrien Charlanes Date: Wed, 24 Mar 2021 17:15:55 +0100 Subject: [PATCH 02/14] Made the snapshotExchangeRate function public --- contracts/stake/StakedTokenV3.sol | 41 +++++++++++++++++-------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/contracts/stake/StakedTokenV3.sol b/contracts/stake/StakedTokenV3.sol index b23b25a..54cf9cc 100644 --- a/contracts/stake/StakedTokenV3.sol +++ b/contracts/stake/StakedTokenV3.sol @@ -320,14 +320,14 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { require(amount <= maxSlashable, 'INVALID_SLASHING_AMOUNT'); STAKED_TOKEN.safeTransfer(destination, amount); - _snapshotExchangeRate(); + snapshotExchangeRate(); emit Slashed(destination, amount); } function donate(uint256 amount) external override { STAKED_TOKEN.safeTransferFrom(msg.sender, address(this), amount); - _snapshotExchangeRate(); + snapshotExchangeRate(); emit Donated(msg.sender, amount); } @@ -362,6 +362,26 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { emit MaxSlashablePercentageChanged(percentage); } + /** + * @dev Snapshots the current exchange rate + */ + function snapshotExchangeRate() public { + uint128 currentBlock = uint128(block.number); + uint128 newExchangeRate = uint128(exchangeRate()); + + // Doing multiple operations in the same block + if ( + _countExchangeRateSnapshots != 0 && + _exchangeRateSnapshots[_countExchangeRateSnapshots - 1].blockNumber == currentBlock + ) { + _exchangeRateSnapshots[_countExchangeRateSnapshots - 1].value = newExchangeRate; + } else { + _exchangeRateSnapshots[_countExchangeRateSnapshots] = Snapshot(currentBlock, newExchangeRate); + _countExchangeRateSnapshots = _countExchangeRateSnapshots + 1; + } + emit EchangeRateSnapshotted(currentBlock, newExchangeRate); + } + /** * @dev Calculates the exchange rate between the amount of STAKED_TOKEN and the the StakeToken total supply. * Slashing will reduce the exchange rate. Supplying STAKED_TOKEN to the stake contract @@ -530,23 +550,6 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { emit Redeem(from, to, amountToRedeem, underlyingToRedeem); } - function _snapshotExchangeRate() internal { - uint128 currentBlock = uint128(block.number); - uint128 newExchangeRate = uint128(exchangeRate()); - - // Doing multiple operations in the same block - if ( - _countExchangeRateSnapshots != 0 && - _exchangeRateSnapshots[_countExchangeRateSnapshots - 1].blockNumber == currentBlock - ) { - _exchangeRateSnapshots[_countExchangeRateSnapshots - 1].value = newExchangeRate; - } else { - _exchangeRateSnapshots[_countExchangeRateSnapshots] = Snapshot(currentBlock, newExchangeRate); - _countExchangeRateSnapshots = _countExchangeRateSnapshots + 1; - } - emit EchangeRateSnapshotted(currentBlock, newExchangeRate); - } - /** * @dev searches a exchange Rate by block number. Uses binary search. * @param blockNumber the block number being searched From d36a81cac3716c374f955a53b5b19c1603446598 Mon Sep 17 00:00:00 2001 From: Hadrien Charlanes Date: Wed, 24 Mar 2021 17:28:36 +0100 Subject: [PATCH 03/14] Added natspec commenst --- contracts/stake/StakedTokenV3.sol | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/contracts/stake/StakedTokenV3.sol b/contracts/stake/StakedTokenV3.sol index 54cf9cc..395cabb 100644 --- a/contracts/stake/StakedTokenV3.sol +++ b/contracts/stake/StakedTokenV3.sol @@ -325,6 +325,10 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { emit Slashed(destination, amount); } + /** + * @dev Function that pull funds to be staked as a donation to the pool of staked tokens. + * @param amount the amount to send + **/ function donate(uint256 amount) external override { STAKED_TOKEN.safeTransferFrom(msg.sender, address(this), amount); snapshotExchangeRate(); @@ -427,6 +431,12 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { return REVISION(); } + /** + * @dev returns the delegated power of a user at a certain block + * @param user the user + * @param blockNumber the blockNumber at which to evalute the power + * @param delegationType 0 for Voting, 1 for proposition + **/ function getPowerAtBlock( address user, uint256 blockNumber, @@ -445,6 +455,12 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { ); } + /** + * @dev returns the current delegated power of a user. The current power is the + * power delegated at the time of the last snapshot + * @param user the user + * @param delegationType 0 for Voting, 1 for proposition + **/ function getPowerCurrent(address user, DelegationType delegationType) external view From 294e256dcde38b841d8b9c77d175bc84d1280850 Mon Sep 17 00:00:00 2001 From: Hadrien Charlanes Date: Thu, 25 Mar 2021 09:52:16 +0100 Subject: [PATCH 04/14] Added getExchangeRate --- contracts/stake/StakedTokenV3.sol | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/contracts/stake/StakedTokenV3.sol b/contracts/stake/StakedTokenV3.sol index 395cabb..c31fe63 100644 --- a/contracts/stake/StakedTokenV3.sol +++ b/contracts/stake/StakedTokenV3.sol @@ -480,6 +480,16 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { ); } + /** + * @notice Searches the exchange rate for a blocknumber + * @param blockNumber blockNumber to search + * @return The last exchangeRate recorded before the blockNumber + * @dev not all exchangeRates are recorded, so this value might not be exact. Use archive node for exact value + **/ + function getExchangeRate(uint256 blockNumber) external view returns (uint256) { + return _searchExchangeRateByBlockNumber(blockNumber); + } + function _claimRewards( address from, address to, From 3456c2920c2edc3f515d43bfd42a2fbbab966c52 Mon Sep 17 00:00:00 2001 From: Hadrien Charlanes Date: Thu, 25 Mar 2021 14:27:04 +0100 Subject: [PATCH 05/14] Added test for power with exchange rates --- test/StakedAaveV3/stakedAave-V3.spec.ts | 52 ++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 6 deletions(-) diff --git a/test/StakedAaveV3/stakedAave-V3.spec.ts b/test/StakedAaveV3/stakedAave-V3.spec.ts index 550be98..9be2d68 100644 --- a/test/StakedAaveV3/stakedAave-V3.spec.ts +++ b/test/StakedAaveV3/stakedAave-V3.spec.ts @@ -213,8 +213,14 @@ makeSuite('StakedAave V3 slashing tests', (testEnv: TestEnv) => { it('Verifies that the initial exchange rate is 1:1', async () => { const currentExchangeRate = await stakeV3.exchangeRate(); + const searchedExchangeRate = await stakeV3.getExchangeRate( + await DRE.ethers.provider.getBlockNumber() + ); + const searchedExchangeRateBlockZero = await stakeV3.getExchangeRate(0); expect(currentExchangeRate.toString()).to.be.equal(WAD); + expect(searchedExchangeRateBlockZero.toString()).to.be.equal(WAD); + expect(searchedExchangeRate.toString()).to.be.equal(WAD); }); it('Verifies that after a deposit the initial exchange rate is still 1:1', async () => { @@ -233,29 +239,55 @@ makeSuite('StakedAave V3 slashing tests', (testEnv: TestEnv) => { }); it('Executes a slash of 20% of the asset', async () => { - const { aaveToken, users } = testEnv; - - const fundsReceiver = users[3].address; + const { + aaveToken, + users: [admin, staker, , fundsReceiver], + } = testEnv; const userBalanceBeforeSlash = new BigNumber( - (await aaveToken.balanceOf(fundsReceiver)).toString() + (await aaveToken.balanceOf(fundsReceiver.address)).toString() ); + const stakerBalanceBeforeSlash = await aaveToken.balanceOf(staker.address); + const votingPowerBeforeSlash = await stakeV3.getPowerCurrent(staker.address, 0); + const propPowerBeforeSlash = await stakeV3.getPowerCurrent(staker.address, 1); + const currentStakeBalance = new BigNumber( (await aaveToken.balanceOf(stakeV3.address)).toString() ); const amountToSlash = currentStakeBalance.times(0.2).toFixed(0); - await stakeV3.connect(users[0].signer).slash(fundsReceiver, amountToSlash); + await stakeV3.connect(admin.signer).slash(fundsReceiver.address, amountToSlash); const newStakeBalance = new BigNumber((await aaveToken.balanceOf(stakeV3.address)).toString()); const userBalanceAfterSlash = new BigNumber( - (await aaveToken.balanceOf(fundsReceiver)).toString() + (await aaveToken.balanceOf(fundsReceiver.address)).toString() ); const exchangeRate = new BigNumber((await stakeV3.exchangeRate()).toString()).toString(); + const searchedExchangeRate = await stakeV3.getExchangeRate( + await DRE.ethers.provider.getBlockNumber() + ); + const searchedExchangeRateBlockBefore = await stakeV3.getExchangeRate( + (await DRE.ethers.provider.getBlockNumber()) - 1 + ); + const searchedExchangeRateBlockZero = await stakeV3.getExchangeRate(0); + + const stakerBalanceAfterSlash = await aaveToken.balanceOf(staker.address); + const votingPowerAfterSlash = await stakeV3.getPowerCurrent(staker.address, 0); + const propPowerAfterSlash = await stakeV3.getPowerCurrent(staker.address, 1); + const searchedVotingPowerBeforeSlash = await stakeV3.getPowerAtBlock( + staker.address, + (await DRE.ethers.provider.getBlockNumber()) - 1, + 0 + ); + const searchedPropPowerBeforeSlash = await stakeV3.getPowerAtBlock( + staker.address, + (await DRE.ethers.provider.getBlockNumber()) - 1, + 1 + ); expect(newStakeBalance.toString()).to.be.equal( currentStakeBalance.minus(amountToSlash).toFixed(0) @@ -264,6 +296,14 @@ makeSuite('StakedAave V3 slashing tests', (testEnv: TestEnv) => { userBalanceBeforeSlash.plus(amountToSlash).toFixed(0) ); expect(exchangeRate).to.be.equal(ethers.utils.parseEther('0.8')); + expect(searchedExchangeRate).to.be.equal(ethers.utils.parseEther('0.8')); + expect(searchedExchangeRateBlockBefore).to.be.equal(ethers.utils.parseEther('1.0')); + expect(searchedExchangeRateBlockZero).to.be.equal(ethers.utils.parseEther('1.0')); + expect(stakerBalanceAfterSlash).to.be.equal(stakerBalanceBeforeSlash); + expect(searchedVotingPowerBeforeSlash).to.be.equal(votingPowerBeforeSlash); + expect(searchedPropPowerBeforeSlash).to.be.equal(propPowerBeforeSlash); + expect(votingPowerAfterSlash).to.be.equal(votingPowerBeforeSlash.mul(8).div(10)); + expect(propPowerAfterSlash).to.be.equal(propPowerBeforeSlash.mul(8).div(10)); }); it('Redeems 1 stkAAVE after slashing - expected to receive 0.8 AAVE', async () => { From 251640aeba94c82f1b8bc7631f17a05e7c425c72 Mon Sep 17 00:00:00 2001 From: Hadrien Charlanes Date: Tue, 6 Apr 2021 17:48:45 +0200 Subject: [PATCH 06/14] removed test aave v3 from npm run test, issue with hardhat block number mngt --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e87eeb2..6b42be1 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "compile": "SKIP_LOAD=true hardhat compile", "compile:force": "npm run compile -- --force", "compile:force:quiet": "npm run compile:force -- --quiet", - "test": "npm run compile:force:quiet && hardhat test test/__setup.spec.ts test/AaveIncentivesController/*.spec.ts test/StakedAave/*.spec.ts test/StakedAaveV2/*.spec.ts test/StakedAaveV3/*.spec.ts", + "test": "npm run compile:force:quiet && hardhat test test/__setup.spec.ts test/AaveIncentivesController/*.spec.ts test/StakedAave/*.spec.ts test/StakedAaveV2/*.spec.ts && hardhat test test/__setup.spec.ts test/StakedAaveV3/*.spec.ts", "test:ci": "npm run compile:force:quiet && npm run test-pei && npm run test-psi && npm run test-psi2 && npm run test-bpt", "test-pei": "npm run test test/__setup.spec.ts test/AaveIncentivesController/*.spec.ts", "test-psi": "npm run test test/__setup.spec.ts test/StakedAave/*.spec.ts", From 3775176e182ae421e91cfda1adfe74845b6c06a1 Mon Sep 17 00:00:00 2001 From: Hadrien Charlanes Date: Wed, 21 Apr 2021 11:35:02 +0200 Subject: [PATCH 07/14] fix: removed Typo and not needed blockNumber in ExchangeRateSnapshotted event --- contracts/stake/StakedTokenV3.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/stake/StakedTokenV3.sol b/contracts/stake/StakedTokenV3.sol index c31fe63..71e42eb 100644 --- a/contracts/stake/StakedTokenV3.sol +++ b/contracts/stake/StakedTokenV3.sol @@ -73,7 +73,7 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { event MaxSlashablePercentageChanged(uint256 newPercentage); event Slashed(address indexed destination, uint256 amount); event Donated(address indexed sender, uint256 amount); - event EchangeRateSnapshotted(uint128 blockNumber, uint128 exchangeRate); + event ExchangeRateSnapshotted(uint128 exchangeRate); event CooldownPauseAdminChanged(address indexed newAdmin); event SlashingAdminChanged(address indexed newAdmin); event ClaimHelperChanged(address indexed newClaimHelper); @@ -383,7 +383,7 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { _exchangeRateSnapshots[_countExchangeRateSnapshots] = Snapshot(currentBlock, newExchangeRate); _countExchangeRateSnapshots = _countExchangeRateSnapshots + 1; } - emit EchangeRateSnapshotted(currentBlock, newExchangeRate); + emit ExchangeRateSnapshotted(newExchangeRate); } /** From 1057fe8d1eaa82ac91d2086ad5aa55a29c5c87e6 Mon Sep 17 00:00:00 2001 From: Hadrien Charlanes Date: Wed, 21 Apr 2021 11:45:37 +0200 Subject: [PATCH 08/14] fix: cached storage, syntax ++ on `snapshotExchangeRate()` --- contracts/stake/StakedTokenV3.sol | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/contracts/stake/StakedTokenV3.sol b/contracts/stake/StakedTokenV3.sol index 71e42eb..5edefe7 100644 --- a/contracts/stake/StakedTokenV3.sol +++ b/contracts/stake/StakedTokenV3.sol @@ -372,16 +372,17 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { function snapshotExchangeRate() public { uint128 currentBlock = uint128(block.number); uint128 newExchangeRate = uint128(exchangeRate()); + uint256 count = _countExchangeRateSnapshots; // Doing multiple operations in the same block if ( - _countExchangeRateSnapshots != 0 && - _exchangeRateSnapshots[_countExchangeRateSnapshots - 1].blockNumber == currentBlock + count != 0 && + _exchangeRateSnapshots[count - 1].blockNumber == currentBlock ) { - _exchangeRateSnapshots[_countExchangeRateSnapshots - 1].value = newExchangeRate; + _exchangeRateSnapshots[count - 1].value = newExchangeRate; } else { - _exchangeRateSnapshots[_countExchangeRateSnapshots] = Snapshot(currentBlock, newExchangeRate); - _countExchangeRateSnapshots = _countExchangeRateSnapshots + 1; + _exchangeRateSnapshots[count] = Snapshot(currentBlock, newExchangeRate); + _countExchangeRateSnapshots++; } emit ExchangeRateSnapshotted(newExchangeRate); } From 46b09276048fcc6303b5cf3b6f1f16084398946b Mon Sep 17 00:00:00 2001 From: Hadrien Charlanes Date: Wed, 21 Apr 2021 16:04:04 +0200 Subject: [PATCH 09/14] fix: added comments, added exchangeRatePrecision constant --- contracts/stake/StakedTokenV3.sol | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/contracts/stake/StakedTokenV3.sol b/contracts/stake/StakedTokenV3.sol index 5edefe7..82b707e 100644 --- a/contracts/stake/StakedTokenV3.sol +++ b/contracts/stake/StakedTokenV3.sol @@ -44,6 +44,8 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { address internal _claimHelper; + uint public constant EXCHANGE_RATE_PRECISION = 1e18; + mapping(uint256 => Snapshot) internal _exchangeRateSnapshots; uint256 internal _countExchangeRateSnapshots; @@ -320,6 +322,7 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { require(amount <= maxSlashable, 'INVALID_SLASHING_AMOUNT'); STAKED_TOKEN.safeTransfer(destination, amount); + // We transfer tokens first: this is the event updating the exchange Rate snapshotExchangeRate(); emit Slashed(destination, amount); @@ -331,6 +334,7 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { **/ function donate(uint256 amount) external override { STAKED_TOKEN.safeTransferFrom(msg.sender, address(this), amount); + // We transfer tokens first: this is the event updating the exchange Rate snapshotExchangeRate(); emit Donated(msg.sender, amount); @@ -396,10 +400,10 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { uint256 currentSupply = totalSupply(); if (currentSupply == 0) { - return 1e18; //initial exchange rate is 1:1 + return uint256(1).mul(EXCHANGE_RATE_PRECISION); //initial exchange rate is 1:1 } - return STAKED_TOKEN.balanceOf(address(this)).mul(1e18).div(currentSupply); + return STAKED_TOKEN.balanceOf(address(this)).mul(EXCHANGE_RATE_PRECISION).div(currentSupply); } /** From 06b6dc9af724d9cd8e81a0b82165af6765cda198 Mon Sep 17 00:00:00 2001 From: Hadrien Charlanes Date: Wed, 21 Apr 2021 16:09:42 +0200 Subject: [PATCH 10/14] fix: cached storage snapshotCount, added snapshot to initialize function --- contracts/stake/StakedTokenV3.sol | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/contracts/stake/StakedTokenV3.sol b/contracts/stake/StakedTokenV3.sol index 82b707e..ec5dbea 100644 --- a/contracts/stake/StakedTokenV3.sol +++ b/contracts/stake/StakedTokenV3.sol @@ -164,6 +164,8 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { _claimHelper = claimHelper; _maxSlashablePercentage = maxSlashablePercentage; + + snapshotExchangeRate(); } /** @@ -376,16 +378,16 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { function snapshotExchangeRate() public { uint128 currentBlock = uint128(block.number); uint128 newExchangeRate = uint128(exchangeRate()); - uint256 count = _countExchangeRateSnapshots; + uint256 snapshotsCount = _countExchangeRateSnapshots; // Doing multiple operations in the same block if ( - count != 0 && - _exchangeRateSnapshots[count - 1].blockNumber == currentBlock + snapshotsCount != 0 && + _exchangeRateSnapshots[snapshotsCount - 1].blockNumber == currentBlock ) { - _exchangeRateSnapshots[count - 1].value = newExchangeRate; + _exchangeRateSnapshots[snapshotsCount - 1].value = newExchangeRate; } else { - _exchangeRateSnapshots[count] = Snapshot(currentBlock, newExchangeRate); + _exchangeRateSnapshots[snapshotsCount] = Snapshot(currentBlock, newExchangeRate); _countExchangeRateSnapshots++; } emit ExchangeRateSnapshotted(newExchangeRate); @@ -588,13 +590,11 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { function _searchExchangeRateByBlockNumber(uint256 blockNumber) internal view returns (uint256) { require(blockNumber <= block.number, 'INVALID_BLOCK_NUMBER'); - if (_countExchangeRateSnapshots == 0) { - return exchangeRate(); - } + uint256 snapshotsCount =_countExchangeRateSnapshots; // First check most recent balance - if (_exchangeRateSnapshots[_countExchangeRateSnapshots - 1].blockNumber <= blockNumber) { - return _exchangeRateSnapshots[_countExchangeRateSnapshots - 1].value; + if (_exchangeRateSnapshots[snapshotsCount - 1].blockNumber <= blockNumber) { + return _exchangeRateSnapshots[snapshotsCount - 1].value; } // Next check implicit zero balance @@ -603,7 +603,7 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { } uint256 lower = 0; - uint256 upper = _countExchangeRateSnapshots - 1; + uint256 upper = snapshotsCount - 1; while (upper > lower) { uint256 center = upper - (upper - lower) / 2; // ceil, avoiding overflow Snapshot memory snapshot = _exchangeRateSnapshots[center]; From bdd65801b5218a92853f85df504c69e7409c4e69 Mon Sep 17 00:00:00 2001 From: Hadrien Charlanes Date: Fri, 30 Apr 2021 11:17:52 +0200 Subject: [PATCH 11/14] fix: minor readiness fix on exchange rate --- contracts/stake/StakedTokenV3.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/stake/StakedTokenV3.sol b/contracts/stake/StakedTokenV3.sol index ec5dbea..2579d7a 100644 --- a/contracts/stake/StakedTokenV3.sol +++ b/contracts/stake/StakedTokenV3.sol @@ -402,7 +402,7 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { uint256 currentSupply = totalSupply(); if (currentSupply == 0) { - return uint256(1).mul(EXCHANGE_RATE_PRECISION); //initial exchange rate is 1:1 + return EXCHANGE_RATE_PRECISION; //initial exchange rate is 1:1 } return STAKED_TOKEN.balanceOf(address(this)).mul(EXCHANGE_RATE_PRECISION).div(currentSupply); From 04d83f3668e100a73126a901d7755b0c7c627ec8 Mon Sep 17 00:00:00 2001 From: Hadrien Charlanes Date: Fri, 30 Apr 2021 11:24:22 +0200 Subject: [PATCH 12/14] clean: used exchangeratePrecision variable where needed --- contracts/stake/StakedTokenV3.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/stake/StakedTokenV3.sol b/contracts/stake/StakedTokenV3.sol index 2579d7a..68d66f9 100644 --- a/contracts/stake/StakedTokenV3.sol +++ b/contracts/stake/StakedTokenV3.sol @@ -458,7 +458,7 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { return ( _searchByBlockNumber(snapshots, snapshotsCounts, user, blockNumber) .mul(_searchExchangeRateByBlockNumber(blockNumber)) - .div(1e18) + .div(EXCHANGE_RATE_PRECISION) ); } @@ -482,7 +482,7 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { return ( _searchByBlockNumber(snapshots, snapshotsCounts, user, block.number).mul(exchangeRate()).div( - 1e18 + EXCHANGE_RATE_PRECISION ) ); } @@ -599,7 +599,7 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { // Next check implicit zero balance if (_exchangeRateSnapshots[0].blockNumber > blockNumber) { - return 1e18; //initial exchange rate is 1:1 + return EXCHANGE_RATE_PRECISION; //initial exchange rate is 1:1 } uint256 lower = 0; From 7ed6e975567db366fed6bb07ce6780ecabd7ff2c Mon Sep 17 00:00:00 2001 From: Hadrien Charlanes Date: Fri, 30 Apr 2021 11:25:27 +0200 Subject: [PATCH 13/14] clean: used exchangeratePrecision variable where needed --- contracts/stake/StakedTokenV3.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/stake/StakedTokenV3.sol b/contracts/stake/StakedTokenV3.sol index 68d66f9..eb31ec1 100644 --- a/contracts/stake/StakedTokenV3.sol +++ b/contracts/stake/StakedTokenV3.sol @@ -532,7 +532,7 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { stakersCooldowns[to] = getNextCooldownTimestamp(0, amount, to, balanceOfUser); - uint256 sharesToMint = amount.mul(1e18).div(exchangeRate()); + uint256 sharesToMint = amount.mul(EXCHANGE_RATE_PRECISION).div(exchangeRate()); _mint(to, sharesToMint); if (pullFunds) { @@ -570,7 +570,7 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { _updateCurrentUnclaimedRewards(from, balanceOfFrom, true); - uint256 underlyingToRedeem = amountToRedeem.mul(exchangeRate()).div(1e18); + uint256 underlyingToRedeem = amountToRedeem.mul(exchangeRate()).div(EXCHANGE_RATE_PRECISION); _burn(from, amountToRedeem); From d8f7e66c4624dcbfdc22b0c24e5badb893e0a00f Mon Sep 17 00:00:00 2001 From: Hadrien Charlanes Date: Fri, 30 Apr 2021 11:40:30 +0200 Subject: [PATCH 14/14] refactor: cached better snapshot index --- contracts/stake/StakedTokenV3.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/stake/StakedTokenV3.sol b/contracts/stake/StakedTokenV3.sol index eb31ec1..156427a 100644 --- a/contracts/stake/StakedTokenV3.sol +++ b/contracts/stake/StakedTokenV3.sol @@ -590,11 +590,11 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { function _searchExchangeRateByBlockNumber(uint256 blockNumber) internal view returns (uint256) { require(blockNumber <= block.number, 'INVALID_BLOCK_NUMBER'); - uint256 snapshotsCount =_countExchangeRateSnapshots; + uint256 lastExchangeRateSnapshotIndex =_countExchangeRateSnapshots - 1; // First check most recent balance - if (_exchangeRateSnapshots[snapshotsCount - 1].blockNumber <= blockNumber) { - return _exchangeRateSnapshots[snapshotsCount - 1].value; + if (_exchangeRateSnapshots[lastExchangeRateSnapshotIndex].blockNumber <= blockNumber) { + return _exchangeRateSnapshots[lastExchangeRateSnapshotIndex].value; } // Next check implicit zero balance @@ -603,7 +603,7 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { } uint256 lower = 0; - uint256 upper = snapshotsCount - 1; + uint256 upper = lastExchangeRateSnapshotIndex; while (upper > lower) { uint256 center = upper - (upper - lower) / 2; // ceil, avoiding overflow Snapshot memory snapshot = _exchangeRateSnapshots[center];