Skip to content
This repository was archived by the owner on Dec 10, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from 6 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
163 changes: 148 additions & 15 deletions contracts/stake/StakedTokenV3.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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');
_;
Expand All @@ -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);
Expand Down Expand Up @@ -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
Expand All @@ -330,10 +320,22 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager {
require(amount <= maxSlashable, 'INVALID_SLASHING_AMOUNT');

STAKED_TOKEN.safeTransfer(destination, amount);
snapshotExchangeRate();

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();

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 +366,41 @@ 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
* 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
Expand Down Expand Up @@ -394,6 +431,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(1e18)
);
}

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

/**
* @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 @@ -479,4 +575,41 @@ 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');

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;
}
}
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