Reproduced Exploit

Tokemak: removing an empty destination drops 250 rewards

The exit calls rewarder.withdraw with claim false, then pops the destination from debtReportQueue. The next updateDebtReporting does not see it, so the 250 is never added to totalIdle.

Jul 2024•Other•logic•2 min read

Chain

Other

Category

logic

Date

Jul 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: 53523-toke-6-loss-of-reward-from-destinationvaultmainrewarder-if-t. Standalone Foundry PoC and full write-up: 53523-toke-6-loss-of-reward-from-destinationvaultmainrewarder-if-t_exp in the evm-hack-registry mirror.


Vulnerability classes: vuln/logic/reward-calculation · vuln/logic/incorrect-order-of-operations

Key info#

ProtocolTokemak
FindingHexens TOKE-6, AuditVault 53523
Vulnerable codeAutoPool._beforeTokenTransfer
HarmA full exit leaves 250 of accrued rewards in the main rewarder and totalIdle stays 0. The fixed path claims first and totalIdle is 250.
Chainlocal synthetic

TL;DR#

The exit calls rewarder.withdraw with claim false, then pops the destination from debtReportQueue. The next updateDebtReporting does not see it, so the 250 is never added to totalIdle.

The vulnerable code#

SOLIDITY
            // @> VULN: claim is false, so accrued reward is not transferred to the auto pool
            rewarder.withdraw(from, amount, false);

Root cause#

The queue removal assumes an empty destination has nothing left to report. The rewarder still holds rewards because the exit did not claim them.

Preconditions#

The destination is on the debt queue with 250 of rewards already accounted to the auto pool.

Attack walkthrough#

  1. Load the destination and fund the rewarder with 250. See output.txt.
  2. flashRebalanceOut sets the balance to 0.
  3. The hook withdraws the stake with claim false.
  4. The destination is popped from all three queues.
  5. updateDebtReporting returns because the queue is empty.
  6. totalIdle is 0 and the rewarder still holds 250. The fixed path claims first.

Diagrams#

flowchart TD A["Rewarder holds 250"] --> B["Exit calls withdraw claim false"] B["Exit calls withdraw claim false"] --> C["Destination is popped"] C["Destination is popped"] --> D["Debt reporting sees an empty queue"] D["Debt reporting sees an empty queue"] --> E["totalIdle stays 0"]

Impact#

The auto pool's idle balance misses 250. The fixed order claims before the pop, and totalIdle becomes 250.

Remediation#

Claim the rewarder balance and add it to totalIdle before removing the destination from the queues.

How to reproduce#

BASH
cd 53523-toke-6-loss-of-reward-from-destinationvaultmainrewarder-if-t_exp
forge test -vv

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.