All incidents

Peapods Public Reward Swap

Share
Feb 08, 2025 08:26 UTCAttackLoss: 375.68 WeightedIndexPending manual check1 exploit txWindow: Atomic
Estimated Impact
375.68 WeightedIndex
Label
Attack
Exploit Tx
1
Addresses
1
Attack Window
Atomic
Feb 08, 2025 08:26 UTC → Feb 08, 2025 08:26 UTC

Exploit Transactions

TX 1Ethereum
0x2c1a19982aa88bee8a5d9a5dfeb406f2bfe1cfc1213f20e91d91ce3b55c86cc5
Feb 08, 2025 08:26 UTCExplorer

Victim Addresses

0x7d48d6d775fada207291b37e3eaa68cc865bf9ebEthereum

Loss Breakdown

375.68WeightedIndex

Similar Incidents

Root Cause Analysis

Peapods Public Reward Swap

1. Incident Overview TL;DR

On Ethereum mainnet transaction 0x2c1a19982aa88bee8a5d9a5dfeb406f2bfe1cfc1213f20e91d91ce3b55c86cc5 at block 21800591, the attacker used a flash-borrowed WeightedIndex position to distort the PEAS/WeightedIndex Uniswap V3 pool, then forced Peapods' TokenRewards contract at 0x7d48d6d775fada207291b37e3eaa68cc865bf9eb to sell its entire WeightedIndex inventory through depositFromPairedLpToken(0, 999). That public call spent treasury inventory at an attacker-selected slippage floor, moved price back in the attacker's favor, and left the attacker EOA 0xedee6379fe90bd9b85d8d0b767d4a6deb0dc9dcf with 141113923030647830889 additional WeightedIndex.

The root cause is a publicly callable treasury-conversion function that both consumes the contract's full PAIRED_LP_TOKEN balance and lets the caller override the slippage guard with _slippageOverride. In practice, passing 999 reduced amountOutMinimum to 0.1% of the TWAP-derived quote, so the only effective execution-price protection was removed.

2. Key Background

Peapods' rewards flow converts PAIRED_LP_TOKEN inventory held by TokenRewards into a rewards asset through the Uniswap V3 router. In this incident, PAIRED_LP_TOKEN was WeightedIndex at 0x88e08adb69f2618adf1a3ff6cc43c671612d1ca4, rewardsToken was PEAS at 0x02f92800f57bcd74066f5709f1daa1a4302df875, and the relevant execution venues were V2 pair 0x80e9c48ec41af7a0ed6cf4f3ac979f3538021608 and V3 pool 0x5207bc61c2717ee9c385b93d3b8beea159ddf02e.

The critical design detail is that depositFromPairedLpToken does not require protocol authorization. A caller may pass _amountTknDepositing == 0, yet the function still uses the reward contract's full on-chain WeightedIndex balance as swap inventory. The function also computes an expected output from a TWAP helper but executes the real swap against current spot liquidity in the V3 pool, so a same-transaction price manipulation directly affects realized execution.

3. Vulnerability Analysis & Root Cause Summary

This was an ATTACK class incident caused by an unsafe public treasury swap. The victim invariant should have been: reward-contract inventory conversion can execute only under protocol-controlled conditions and only with bounded deviation from the trusted quote. Instead, depositFromPairedLpToken exposed both trigger authority and slippage control to arbitrary callers. The function first sets _amountTkn to IERC20(PAIRED_LP_TOKEN).balanceOf(address(this)), so all inventory currently held by the contract becomes swapable by any caller. It then replaces the internal slippage setting with attacker input whenever _slippageOverride > 0. The attacker used 999, making amountOutMinimum = (_amountOut * 1) / 1000, which preserved call success even after deliberate pool distortion. Once that forced victim sale pushed price back, the attacker unwound and kept the spread.

The code-level breakpoint is visible in the verified TokenRewards.sol implementation:

function depositFromPairedLpToken(
  uint256 _amountTknDepositing,
  uint256 _slippageOverride
) public override {
  if (_amountTknDepositing > 0) {
    IERC20(PAIRED_LP_TOKEN).safeTransferFrom(
      _msgSender(),
      address(this),
      _amountTknDepositing
    );
  }
  uint256 _amountTkn = IERC20(PAIRED_LP_TOKEN).balanceOf(address(this));
  ...
  uint256 _slippage = _slippageOverride > 0
    ? _slippageOverride
    : _rewardsSwapSlippage;
  ISwapRouter(V3_ROUTER).exactInputSingle(... amountIn: _amountTkn, amountOutMinimum: (_amountOut * (1000 - _slippage)) / 1000 ...);
}

4. Detailed Root Cause Analysis

Before the exploit transaction, the reward contract held 375675447790437488882 WeightedIndex and 2957010461339958038602 PEAS. The attacker only needed that inventory to be present, access to the same V2 and V3 liquidity venues, and the ability to submit a transaction calling depositFromPairedLpToken with a large _slippageOverride.

The trace shows the attacker helper contract 0x21b1b6d675aae57684139200650c81a3686f5fc4 flash-borrowing 9420000000000000000000 WeightedIndex from the V2 pair, then routing it through the V3 pool to buy 17022705134013541201301 PEAS. That first leg intentionally moved the spot price away from the TWAP-derived quote that the reward contract relies on.

WeightedIndex V2 Pair::swap(..., 9420000000000000000000, attacker_helper, 0x61)
SwapRouter::exactInputSingle(tokenIn=WeightedIndex, tokenOut=PEAS, amountIn=9420000000000000000000)
0x7d48...::depositFromPairedLpToken(0, 999)
SwapRouter::exactInputSingle(tokenIn=PEAS, tokenOut=WeightedIndex, amountIn=17022705134013541201301)
WeightedIndex::transfer(V2 pair, 9448345035105315947844)
WeightedIndex::transfer(attacker EOA, 141113923030647830889)

Inside depositFromPairedLpToken, the victim contract took its full WeightedIndex balance, split out 18783772389521874444 WeightedIndex as admin fee, and sold the remaining 356891675400915614438 WeightedIndex into the manipulated V3 pool. The trace records that sell with a minimum output of only 1615753678059432297, consistent with the 999 override. Balance-diff evidence confirms the aftermath: the reward contract's WeightedIndex balance fell from 375675447790437488882 to 0, its PEAS balance rose by 183259900120862851563, and the protocol owner 0xc64bc02594ba7f777f26b7a1eec6e6dc4a56362b received the fee slice.

Because the victim sale pushed the V3 price back after the attacker's first trade, the attacker could immediately swap the previously acquired PEAS back into WeightedIndex at a better rate, receive 9589458958135963778733 WeightedIndex, repay the flash swap with fee (9448345035105315947844), and keep the remainder. That end-to-end path proves the exploit was not merely a bad price execution for the victim; it was a permissionless, economically complete sandwich made possible by the public treasury-swap entrypoint and attacker-controlled slippage.

5. Adversary Flow Analysis

The adversary strategy was a single-transaction, multi-stage ACT sequence:

  1. The EOA 0xedee6379fe90bd9b85d8d0b767d4a6deb0dc9dcf invoked helper contract 0x21b1b6d675aae57684139200650c81a3686f5fc4.
  2. The helper flash-borrowed 9420 WeightedIndex from V2 pair 0x80e9c48ec41af7a0ed6cf4f3ac979f3538021608.
  3. The helper sold the borrowed WeightedIndex into V3 pool 0x5207bc61c2717ee9c385b93d3b8beea159ddf02e and received 17022705134013541201301 PEAS, pushing spot price to an attacker-chosen level.
  4. The helper called TokenRewards.depositFromPairedLpToken(0, 999) on 0x7d48d6d775fada207291b37e3eaa68cc865bf9eb, forcing the reward contract to dump its full WeightedIndex inventory under the weakened slippage floor.
  5. The helper sold the accumulated PEAS back for WeightedIndex, repaid the flash borrow plus fee, and transferred 141113923030647830889 WeightedIndex profit to the EOA.

The attacker's related accounts are defensibly identified from chain evidence: the EOA is the transaction sender and final profit recipient, and the helper contract is the on-chain executor of the flash callback, victim call, unwind, and payout.

6. Impact & Losses

The direct victim was Peapods TokenRewards at 0x7d48d6d775fada207291b37e3eaa68cc865bf9eb. It lost its entire WeightedIndex inventory in the exploit transaction.

  • 356891675400915614438 WeightedIndex was sold into the manipulated V3 pool.
  • 18783772389521874444 WeightedIndex was transferred to the protocol owner as the configured admin fee.
  • Total reward-contract depletion was 375675447790437488882 WeightedIndex.
  • The attacker EOA realized 141113923030647830889 WeightedIndex net token profit while separately paying 657338321534568 wei in gas.

7. References

  • Exploit transaction: 0x2c1a19982aa88bee8a5d9a5dfeb406f2bfe1cfc1213f20e91d91ce3b55c86cc5
  • Attacker EOA: 0xedee6379fe90bd9b85d8d0b767d4a6deb0dc9dcf
  • Attacker helper: 0x21b1b6d675aae57684139200650c81a3686f5fc4
  • Victim contract: 0x7d48d6d775fada207291b37e3eaa68cc865bf9eb
  • WeightedIndex token: 0x88e08adb69f2618adf1a3ff6cc43c671612d1ca4
  • PEAS token: 0x02f92800f57bcd74066f5709f1daa1a4302df875
  • V2 pair: 0x80e9c48ec41af7a0ed6cf4f3ac979f3538021608
  • V3 pool: 0x5207bc61c2717ee9c385b93d3b8beea159ddf02e
  • Verified victim code: TokenRewards.depositFromPairedLpToken
  • Supporting evidence: seed transaction metadata, seed trace, and seed balance-diff artifacts for the exploit transaction