Reproduced Exploit

A 101-token front-run makes one block pay 100,101 incentive tokens

addIncentive stores the caller's rate when amountRemaining <= minIncentiveRate. The minimum is 100 tokens. An attacker deposits 101 tokens at rate 1e36 while the vault is empty, so the rate is pinned at the maximum. The victim then deposits 100,000 tokens at 100 tokens per BGT. Remaining is already…

Oct 2024•Ethereum•defi•4 min read

Loss

One 0.5 BGT block pays 100,101 incentive tokens. The fair rate pays 50

Chain

Ethereum

Category

defi

Date

Oct 2024

Source

AuditVault

EVM Playground

Source-level debugger — step opcodes and Solidity in sync

evm-hack-analyzer

The attack is replayed in an in-browser EVM preloaded with the exact dumped fork state. The execution tree shows every call; step by Solidity line or by opcode across all depths — source, Stack, Memory, Storage, Balances (native / ERC-20 / NFT), Transient storage and Return value stay in sync. Click a tree node, opcode, or source line to jump. No backend, no live RPC.

Loading fork state…

Source & credit. Reproduction of a public audit finding curated by AuditVault — the original finding: 52870-validators-can-front-run-calls-to-addincentive-to-drain-the. Standalone Foundry PoC and full write-up: 52870-validators-can-front-run-calls-to-addincentive-to-drain-the_exp in the evm-hack-registry mirror.


Vulnerability classes: vuln/defi/sandwich-attack · vuln/logic/wrong-condition

Reproduction: self-contained synthetic Foundry reduction; see output.txt.

Key info#

FieldValue
LossOne 0.5 BGT block pays 100,101 incentive tokens. The fair rate pays 50
Vulnerable contractRewardsVault.addIncentive
Attacker EOA0x1111111111111111111111111111111111111111
Attack contractExploit
Attack txExploit.run()
Chain / block / dateEthereum model · block 0 · 2024-10
Compilersolc 0.8.27, cancun, via IR
Bug classThe first caller pins incentiveRate while amountRemaining is below the minimum

TL;DR#

addIncentive stores the caller's rate when amountRemaining <= minIncentiveRate. The minimum is 100 tokens. An attacker deposits 101 tokens at rate 1e36 while the vault is empty, so the rate is pinned at the maximum. The victim then deposits 100,000 tokens at 100 tokens per BGT. Remaining is already 101, so the victim's rate is ignored. The pot is 100,101 tokens. One block of 0.5 BGT pays min(100101e18, 0.5e18 * 1e36 / 1e18), which is the whole pot. If the victim's deposit lands first, the same 0.5 BGT pays 50 tokens.

Background#

Spearbit reviewed Berachain POL in October 2024. BerachainRewardsVault.addIncentive lets any caller set the incentive rate when the remaining inventory is at or below minIncentiveRate. The report's sandwich is: deposit a hair over the minimum at MAX_INCENTIVE_RATE, let the victim's large deposit at rate 100 succeed without changing the rate, then distributeFor one block and take min(amountRemaining, bgtEmitted * rate / PRECISION). The numbers below are that example: 101 tokens, then 100,000 tokens, then one block. Both calls run in this transaction in that order. The control vault runs the victim's deposit first.

The vulnerable code#

SOLIDITY
function addIncentive(uint256 amount, uint256 incentiveRate) external {
    require(incentiveRate <= MAX_RATE, "max");
    require(token.transferFrom(msg.sender, address(this), amount), "pull");
    if (amountRemaining <= MIN_INCENTIVE_RATE && incentiveRate >= MIN_INCENTIVE_RATE) {
        // @> VULN: any caller can pin the rate while remaining is at or below the minimum
        incentiveRateStored = incentiveRate;
    }
    amountRemaining += amount;
}

MIN_INCENTIVE_RATE is 100 tokens. MAX_RATE is 1e36. PRECISION is 1e18. distributeFor pays min(remaining, bgtEmitted * rate / PRECISION).

Root cause#

The rate is a permissionless write whenever the remaining inventory is small. A 101 token deposit is enough to move remaining above the minimum, so the victim's later deposit adds tokens and does not replace the rate. The payout then uses the attacker's rate against the victim's tokens.

Preconditions#

  • The vault's remaining inventory starts at 0, which is <= 100 tokens.
  • The front-run amount is 101 tokens at rate 1e36. The victim amount is 100,000 tokens at 100 tokens per BGT.
  • The distributed block is 0.5 BGT, the 0.5e18 term in the report's min(...) expression.
  • The control vault receives the victim's deposit while remaining is still 0.

Attack walkthrough#

  1. distributeFor pays 100101000000000000000000 tokens (output.txt:9).
  2. The fair block pays 50000000000000000000 (output.txt:11).
  3. The extra versus that fair payment is 100051000000000000000000 (output.txt:13).
  4. [PASS] (output.txt:6).

Diagrams#

flowchart TD Empty["Remaining is 0"] --> Pin["101 tokens set the rate to the max"] Pin --> Victim["Victim adds 100000 tokens and the rate stays"] Victim --> Block["0.5 BGT pays the whole 100101"] Block --> Fair["Victim-first path pays 50 for the same block"]

Impact#

The validator receives 100,101 incentive tokens for validating a single 0.5 BGT block. 100,000 of those tokens were the victim's deposit, which was meant to stream at 100 tokens per BGT. At that rate the same block pays 50 tokens. The difference on this block is 100,051 tokens, and the vault is empty.

Remediation#

Do not let an arbitrary caller replace incentiveRate just because remaining inventory is low. The report's later fix makes addIncentive permissioned to the vault manager. A smaller fix is to revert when the caller passes a rate other than the stored rate unless remaining is 0 and the caller is the manager.

DIFF
-    if (amountRemaining <= MIN_INCENTIVE_RATE && incentiveRate >= MIN_INCENTIVE_RATE) {
-        incentiveRateStored = incentiveRate;
-    }
+    if (incentiveRate != incentiveRateStored) {
+        require(amountRemaining == 0, "rate");
+        require(incentiveRate >= MIN_INCENTIVE_RATE, "min");
+        incentiveRateStored = incentiveRate;
+    }

That still needs an access check if depositors must not set the opening rate.

How to reproduce#

BASH
cd 52870-validators-can-front-run-calls-to-addincentive-to-drain-the_exp
forge test -vv

Sources#


Sources & further analysis#

Reproductions & code

Alerts & third-party analyses

  • Web3Sec X hacked database: search.
  • Rekt leaderboard: search.
  • Solodit incident search: search.

These dashboards index community alerts tweets, post-mortems, and independent write-ups. Reach them through the protocol name above to cross-check this reproduction against other analyses.