Part 2

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

A malicious user can frontRun reward distribution and backrun it immediately earning reward unfairly

[H-3] A malicious user can frontRun reward distribution and backrun it immediately earning reward unfairly

Description: In the stake function there's no minimum stake time requirement., and also no how to track how long a user have staked which means a user can sandwich reward distribution by staking right before reward distribution and unstaking immediately reward have been distributed

Impact:
Users will earn reward unfairly , Assuming there are 4000 shares staked in the protocol, users can just watch the mempool for reward distribution stake 4000 shares and after the reward distribution unstake and get half of the reward distributed

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);
}
User could:
0. watch mempool for reward distribution
1. Stake 1000 shares
2. Get rewards distributed
3. Unstake immediately
4. Keep rewards despite staking for minimal time

Recommended Mitigation:

Add a minimum stake time requirement.

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.