Part 2

Zaros
PerpetualsDEXFoundrySolidity
70,000 USDC
View results
Submission Details
Severity: medium
Valid

Users can exploit reward distribution through sandwich attacks without minimum stake time

[H-1] Users can exploit reward distribution through sandwich attacks without minimum stake time

Description:
The stake() function lacks minimum stake time requirements, allowing malicious users to sandwich reward distributions. This is possible because:

  1. Rewards are distributed in unstake settleVaultsDebt() which is publicly visible

  2. A user can monitor the mempool for user unstaking or keeper calls to settleVaultsDebt()

  3. User can then:

- Frontrun: stake() with large amount
- user unstaking or Keeper tx executes: distributes rewards
- Backrun: unstake() immediately

Example:

Initial state:
- Vault has 1000 WETH rewards to distribute
- 10 legitimate stakers with 100 shares each
Attack:
1. Attacker sees keeper tx
2. Stakes 1000 shares (50% of total)
3. Gets 500 WETH from distribution
4. Unstakes immediately

Impact:

  • Unfair reward distribution

  • Legitimate long-term stakers get less rewards

Proof of Concept:

/// @notice Stakes a given amount of index tokens in the contract.
/// @dev Index token holders must stake in order to earn fees distributions from the market making engine.
/// @dev Invariants involved in the call:
/// The sum of all staked assets SHOULD always equal the total stake value
/// The Vault MUST exist.
/// The Vault MUST be live.
/// @param vaultId The vault identifier.
/// @param shares The amount of index tokens to stake, in 18 decimals.
function stake(uint128 vaultId, uint128 shares) external {
// to prevent safe cast overflow errors
if (shares < Constants.MIN_OF_SHARES_TO_STAKE) {
revert Errors.QuantityOfSharesLessThanTheMinimumAllowed(Constants.MIN_OF_SHARES_TO_STAKE, uint256(shares));
}
// fetch storage slot for vault by id
Vault.Data storage vault = Vault.loadLive(vaultId);
// prepare the `Vault::recalculateVaultsCreditCapacity` call
uint256[] memory vaultsIds = new uint256[](1);
vaultsIds[0] = uint256(vaultId);
// updates the vault's credit capacity and perform all vault state
// transitions before updating `msg.sender` staked shares Vault.recalculateVaultsCreditCapacity(vaultsIds);
// load distribution data
Distribution.Data storage wethRewardDistribution = vault.wethRewardDistribution;
// cast actor address to bytes32
bytes32 actorId = bytes32(uint256(uint160(msg.sender)));
// accumulate the actor's pending reward before staking
wethRewardDistribution.accumulateActor(actorId);
// load actor distribution data
Distribution.Actor storage actor = wethRewardDistribution.actor[actorId];
// calculate actor updated shares amount
UD60x18 updatedActorShares = ud60x18(actor.shares).add(ud60x18(shares));
// update actor staked shares
wethRewardDistribution.setActorShares(actorId, updatedActorShares);
// transfer shares from actor
IERC20(vault.indexToken).safeTransferFrom(msg.sender, address(this), shares);
// emit an event
emit LogStake(vaultId, msg.sender, shares);
}

lets walk through this attack path recalculateVaultsCreditCapacity is a function that can be called to distribute rewards although is an internal function but is been called by other external functions such as settleVaultsDebt() for example,

Now A Malicious actor can

  1. Monitor mempool for settleVaultsDebt transaction

  2. See user submitted tx

  3. Frontrun with high gas stake() call

  4. Get included in reward distribution

  5. Backrun with unstake()

Recommended Mitigation:

include a vesting period mechanism

Updates

Lead Judging Commences

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Staking design is not fair for users who staked earlier and longer, frontrun fee distribution with big stake then unstake

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.