Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added snapshot of exchange rate + search + updated power calculation #10

Open
wants to merge 14 commits into
base: feat/claimerhelper-admin-role
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions contracts/interfaces/IStakedTokenV3.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
172 changes: 155 additions & 17 deletions contracts/stake/StakedTokenV3.sol
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager {

address internal _claimHelper;

uint public constant EXCHANGE_RATE_PRECISION = 1e18;

mapping(uint256 => Snapshot) internal _exchangeRateSnapshots;
uint256 internal _countExchangeRateSnapshots;

modifier onlyAdmin {
require(msg.sender == getAdmin(MAIN_ADMIN_ROLE), 'CALLER_NOT_MAIN_ADMIN');
_;
Expand All @@ -69,6 +74,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 ExchangeRateSnapshotted(uint128 exchangeRate);
event CooldownPauseAdminChanged(address indexed newAdmin);
event SlashingAdminChanged(address indexed newAdmin);
event ClaimHelperChanged(address indexed newClaimHelper);
Expand Down Expand Up @@ -157,6 +164,8 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager {
_claimHelper = claimHelper;

_maxSlashablePercentage = maxSlashablePercentage;

snapshotExchangeRate();
}

/**
Expand Down Expand Up @@ -301,21 +310,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
Expand All @@ -330,10 +324,24 @@ 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();
dhadrien marked this conversation as resolved.
Show resolved Hide resolved

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);
// We transfer tokens first: this is the event updating the exchange Rate
snapshotExchangeRate();
dhadrien marked this conversation as resolved.
Show resolved Hide resolved

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
Expand Down Expand Up @@ -364,6 +372,42 @@ 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());
uint256 snapshotsCount = _countExchangeRateSnapshots;

// Doing multiple operations in the same block
if (
snapshotsCount != 0 &&
_exchangeRateSnapshots[snapshotsCount - 1].blockNumber == currentBlock
) {
_exchangeRateSnapshots[snapshotsCount - 1].value = newExchangeRate;
} else {
_exchangeRateSnapshots[snapshotsCount] = Snapshot(currentBlock, newExchangeRate);
_countExchangeRateSnapshots++;
}
emit ExchangeRateSnapshotted(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
* 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 EXCHANGE_RATE_PRECISION; //initial exchange rate is 1:1
}

return STAKED_TOKEN.balanceOf(address(this)).mul(EXCHANGE_RATE_PRECISION).div(currentSupply);
}

/**
* @dev returns the current address of the claimHelper Contract, contract with priviledge
* It speicifically enables to claim from several contracts at once
Expand Down Expand Up @@ -394,6 +438,65 @@ 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,
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(EXCHANGE_RATE_PRECISION)
);
}

/**
* @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
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(
EXCHANGE_RATE_PRECISION
)
);
}

/**
* @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,
Expand Down Expand Up @@ -429,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) {
Expand Down Expand Up @@ -467,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);

Expand All @@ -479,4 +582,39 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager {

emit Redeem(from, to, amountToRedeem, underlyingToRedeem);
}

/**
* @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');

uint256 lastExchangeRateSnapshotIndex =_countExchangeRateSnapshots - 1;

// First check most recent balance
if (_exchangeRateSnapshots[lastExchangeRateSnapshotIndex].blockNumber <= blockNumber) {
return _exchangeRateSnapshots[lastExchangeRateSnapshotIndex].value;
}

// Next check implicit zero balance
if (_exchangeRateSnapshots[0].blockNumber > blockNumber) {
return EXCHANGE_RATE_PRECISION; //initial exchange rate is 1:1
}

uint256 lower = 0;
uint256 upper = lastExchangeRateSnapshotIndex;
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;
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
52 changes: 46 additions & 6 deletions test/StakedAaveV3/stakedAave-V3.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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)
Expand All @@ -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 () => {
Expand Down