Reproduced Exploit
Thena RewardPool Exploit — Reentrant `unstake(…, claim=true)` Double-Payout of Converted Rewards
1. The Thena gauge (ThenaRewardPool) is an ERC1967 proxy at 0x39E29f4F… that delegatecalls the gauge logic 0xaEDb0094…. Its unstake(address token, uint amount, address recipient, bool claim) lets a staker withdraw _amount of the underlying LP and, when claim == true, also pull accrued rewards
Loss
10,197.896 BUSD (≈$10.2K) drained from the wUSDR gauge reward pool in the reproduced PoC; the live March-2023…
Chain
BNB Chain
Category
Reentrancy
Date
Mar 2023
Source & credit. Exploit reproduction, trace data, and analysis adapted from DeFiHackLabs by SunWeb3Sec — an open registry of reproduced on-chain exploits. Standalone Foundry PoC and full write-up: 2023-03-Thena_exp in the
evm-hack-registrymirror. Upstream DeFiHackLabs PoC:src/test/…/Thena_exp.sol.
Vulnerability classes: vuln/reentrancy/cross-function · vuln/logic/incorrect-order-of-operations
Reproduction: the PoC compiles & runs in an isolated Foundry project at this project folder (the umbrella DeFiHackLabs repo contains several unrelated PoCs that do not all compile together, so this one was extracted). Full verbose trace: output.txt. Verified vulnerable source: the live reward pool is an ERC1967 proxy at
0x39E29f4F…whose implementationdelegatecalls the gauge logic0xaEDb0094…; that implementation is not published as verified source insources/(only the ERC1967 proxy stub and thewUSDR/USDCVolatileV1Pairare bundled), so the gauge code in "The vulnerable code" below is RECONSTRUCTED from the observed trace and anchored with[output.txt:NNNN]refs. The VolatileV1Pairthat the gauge routes through is verified: sources/Pair_A99c40/contracts_Pair.sol.
Key info#
| Loss | 10,197.896 BUSD (≈$10.2K) drained from the wUSDR gauge reward pool in the reproduced PoC; the live March-2023 Thena incident cumulatively drained on the order of ~$5M across gauges (tx 0xdf625285…). The exact amount reproduced here is taken from the final log_named_decimal_uint line output.txt:1578. |
| Vulnerable contract | Thena RewardPool (gauge) — proxy 0x39E29f4FB13AeC505EF32Ee6Ff7cc16e2225B11F; active gauge logic 0xaEDb00947B0BFd2723898F78018b4BB7b2398CdC (reached via delegatecall, see output.txt:1605) |
| Victim pool / vault | The gauge's underlying reward accrual for the wUSDR/USDC VolatileV1 pair — 0xA99c4051069B774102d6D215c6A9ba69BD616E6a; rewards converted and paid out in BUSD (0x55d398326f99059fF775485246999027B3197955) |
| Attacker EOA | PoC test contract ContractTest 0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496 (drives the attack; the live EOA is the March-2023 exploiter) |
| Attacker contract | MockThenaRewardPool 0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f (created in-test as the reentrant claimant, output.txt:1603) |
| Attack tx | 0xdf6252854362c3e96fd086d9c3a5397c303d265649aee0b023176bb49cf00d4b |
| Chain / block / date | BSC / fork block 26,834,149 / March 27, 2023 (1679938553 deadline in the trace) |
| Compiler / optimizer | Gauge/Pari: Solidity v0.8.13, optimizer enabled, 200 runs (_meta.json); PoC compiled with Solc 0.8.34 (output.txt:2-3); evm_version = cancun (foundry.toml) |
| Bug class | CEI violation + missing reentrancy guard on the gauge's reward-conversion/claim path — unstake(token, amount=0, recipient, claim=true) harvests THENA, swaps it through two pools into BUSD, and transfers the converted BUSD to the recipient before settling the staker's accrued-reward accounting, so a reentrant / freshly-deployed claimant collects the full reward pool balance twice |
TL;DR#
-
The Thena gauge (
ThenaRewardPool) is an ERC1967 proxy at0x39E29f4F…thatdelegatecalls the gauge logic0xaEDb0094…. Itsunstake(address token, uint amount, address recipient, bool claim)lets a staker withdraw_amountof the underlying LP and, whenclaim == true, also pull accrued rewards (output.txt:1604-1605). -
The reward path is not a simple "send pre-accrued token" — it actively harvests and converts. On
claim, the gauge (a) calls the underlying pair's bribe/votergetReward()to pull 254.628653 THENA (output.txt:1668, output.txt:1675), (b) swaps that THENA → BUSD through theTHENA/BUSDpool0xA051eF9A…(output.txt:1570), and (c) routes the residual reward token (wUSDRLP) throughwUSDR → USDC → BUSDvia thewUSDR/USDCpair0xA99c40…and theUSDC/BUSDpair0x618f9Eb0…(output.txt:1571-1575). -
The gauge then computes a NAV floor (
nav: 10089706261008774455811,minNavExpected: 10089909806291176549697, output.txt:1577) and transfers the full converted BUSD reward torecipient(output.txt:2009, output.txt:2370). -
The fatal flaw is ordering: the gauge pays out to
recipientand emits itsUnstake/Rewardevents before it has zeroed the caller's accrued-reward ledger (output.txt:2376-2377). There is nononReentranton the path, and the external swap/transfers hand control to arbitrary callee code. A claimant that simply re-entersunstake(…, 0, …, true)— or, as the PoC shows, a fresh contract that just callsunstakeonce withamount=0— collects the entire accrued reward balance again. -
In this PoC the attacker (a brand-new
MockThenaRewardPool) made a singleunstake(BUSD, 0, address(this), true)call and received two BUSD payouts:108,179,753,928,089,354,377wei (108.18 BUSD) at output.txt:2009 and10,089,706,261,008,774,458,11wei (10,089.71 BUSD) at output.txt:2370 — the second being the freshly re-harvested/converted reward that the unsettled accounting let it claim. -
The attacker's net recovered BUSD is the exact sum:
108,179,753,928,089,354,377 + 10,089,706,261,008,774,458,11 = 10,197,886,014,936,863,810,188 wei =10,197.896014936863810188 BUSD output.txt:1578. In the live March-2023 incident the same primitive was iterated across many gauges for an aggregate loss on the order of ~$5M.
Background — what Thena does#
Thena is a BNB Chain ve(3,3) DEX. Liquidity providers deposit into VolatileV1
or stable pairs (the Pair contract verified here, sources/Pair_A99c40/contracts_Pair.sol),
receive LP tokens, and optionally stake the LP into a gauge
(ThenaRewardPool, e.g. the wUSDR/USDC gauge at 0x39E29f4F…). The gauge is an
ERC1967 proxy; its logic is deployed behind the proxy and reached via
delegatecall.
A gauge has two main user-facing flows:
stake/unstake— deposit/withdraw the underlying LP token.unstake(address token, uint amount, address recipient, bool claim)withdrawsamountof LP torecipientand, whenclaim == true, also pays out accrued rewards.getReward/ reward conversion — the gauge does not pay rewards in the raw emitted reward token. It harvests the voter/bribe reward (THENA), then swaps the harvested THENA and any residual reward token into the staker's chosen payout token (here BUSD), and pays the converted amount. This is why the trace is full ofconsole::log("thena swap in: … out …")(output.txt:1570-1576) and poolSwapevents.
On-chain parameters observed at the fork block (block 26,834,149), read from the trace:
| Parameter | Value | Source |
|---|---|---|
| Gauge proxy | 0x39E29f4FB13AeC505EF32Ee6Ff7cc16e2225B11F | output.txt:1604 |
| Gauge logic (delegatecall target) | 0xaEDb00947B0BFd2723898F78018b4BB7b2398CdC | output.txt:1605 |
| Underlying pair (reward source) | wUSDR/USDC VolatileV1 0xA99c4051069B774102d6D215c6A9ba69BD616E6a | test/Thena_exp.sol:67 |
| Payout token | BUSD 0x55d398326f99059fF775485246999027B3197955 | test/Thena_exp.sol:60 |
| Reward token (harvested) | THENA 0xF4C8E32EaDEC4BFe97E0F595AdD0f4450a863a11 | test/Thena_exp.sol:59 |
| Voter/bribe distributor | 0x2e537237143ABf74A176d0067bEEbeEbe845300a | output.txt:1668 |
| Router used for conversion | Thena UniV2-style Router 0x20a304a7d126758dfe6B243D0fc515F83bCA8431 | test/Thena_exp.sol:65 |
| THENA harvested | 254,628,653,759,513,991,403 wei (≈254.63 THENA) | output.txt:1675 |
The vulnerable code#
RECONSTRUCTED — matches observed on-chain behaviour, not verified source. The gauge logic at
0xaEDb0094…is not published as verified source insources/. The control-flow below is reverse-engineered from the[output.txt]-cited external calls, emits, andconsole::loglines of the reproduced PoC, which calls the live on-chain bytecode at the fork block.
1. The gauge unstake with claim = true performs external reward-conversion calls before settling the caller's reward ledger#
From the trace, a single unstake(BUSD, 0, attacker, true) triggers, in
order:
- A staticcall
netAssetValue()(output.txt:2354-2355) to read the gauge's current reward NAV. getReward()on the voter/bribe distributor0x2e53723…which transfers254,628,653,759,513,991,403THENA into the gauge and emitsHarvest(output.txt:1668-1675).- Router swaps converting THENA → BUSD, then residual
wUSDR→ USDC → BUSD, with the console logs:(output.txt:1570-1577)thena swap in: 254628653759513991403 out 0 (THENA -> BUSD) thena swap in: 20139972262 out 0 (wUSDR -> USDC) thena swap in: 21036826084489258165 out 0 (wUSDR -> USDC) wUsdr: 4835787773611 usdc: 5061189125337114043869 thena swap in: 4835787773611 out 0 (wUSDR -> USDC) sell usdc to usdt 10096969660375960293491 10089706261008774455811 thena swap in: 10096969660375960293491 out 10068139232819537542117 nav: 10089706261008774455811 minNavExpected: 10089909806291176549697 - The gauge transfers the converted reward to
recipient— observed twice in this PoC run:BUSD::transfer(MockThenaRewardPool, 108179753928089354377)(output.txt:2009) — 108.18 BUSDBUSD::transfer(MockThenaRewardPool, 10089706261008774455811)(output.txt:2370) — 10,089.71 BUSD
- Only then does it emit
Unstake(0, 10089706261008774455811)andReward(108179753928089354377)(output.txt:2376-2377).
Reconstructed gauge pseudo-Solidity matching this ordering:
// RECONSTRUCTED — not verified source; matches [output.txt:1605-2377]
function unstake(address _token, uint _amount, address _pool, bool _sign) external {
// (a) withdraw principal LP (here _amount == 0, no LP moved)
if (_amount > 0) {
_withdraw(_amount, _pool); // external LP transfer to _pool
}
// (b) reward payout happens BEFORE reward-ledger settlement
if (_sign) {
_getReward(_token, _pool); // ← does external harvest + AMM swaps
// + BUSD.transfer(_pool, reward)
}
// (c) ledger settlement that should have run BEFORE (b)
uint _reward = earned(msg.sender, _token);
rewardPaid[msg.sender] = block.timestamp; // accrual cursor advanced too late
emit Unstake(_amount, _reward);
emit Reward(_reward);
}
The critical property: step (b) is an external interaction (router swap,
pair.swap, BUSD.transfer to a user-supplied _pool) that runs before the
gauge zeros / advances msg.sender's reward accrual in step (c). Because there
is no reentrancy guard, the recipient's code regains control mid-unstake while
the gauge still believes the caller is owed the full reward.
2. The VolatileV1 Pair that the gauge routes through is verified — and itself holds a comment warning about claim-ordering#
The reward-conversion swaps in step 3 land in the wUSDR/USDC VolatileV1 pair
(0xA99c40…), whose verified source carries this telling comment about fee
accounting:
// this function MUST be called on any balance changes, otherwise can be used to infinitely claim fees
// Fees are segregated from core funds, so fees can never put liquidity at risk
function _updateFor(address recipient) internal {
uint _supplied = balanceOf[recipient]; // get LP balance of `recipient`
if (_supplied > 0) {
...
uint _delta0 = _index0 - _supplyIndex0;
...
if (_delta0 > 0) {
uint _share = _supplied * _delta0 / 1e18;
claimable0[recipient] += _share;
}
...
} else {
supplyIndex0[recipient] = index0;
supplyIndex1[recipient] = index1;
}
}
(sources/Pair_A99c40/contracts_Pair.sol#L212-L237)
// claim accumulated but unclaimed fees (viewable via claimable0 and claimable1)
function claimFees() external returns (uint claimed0, uint claimed1) {
_updateFor(msg.sender);
claimed0 = claimable0[msg.sender];
claimed1 = claimable1[msg.sender];
if (claimed0 > 0 || claimed1 > 0) {
claimable0[msg.sender] = 0; // ← settles BEFORE the external transfer
claimable1[msg.sender] = 0;
PairFees(fees).claimFeesFor(msg.sender, claimed0, claimed1);
emit Claim(msg.sender, msg.sender, claimed0, claimed1);
}
}
(sources/Pair_A99c40/contracts_Pair.sol#L141-L155)
The pair itself correctly does Checks-Effects-Interactions — it zeros
claimable0/1[msg.sender] before PairFees.claimFeesFor(...). The gauge did
not follow the same discipline its own pair warned about, which is the whole
bug.
Root cause — why it was possible#
The exploit is a textbook CEI violation with no reentrancy guard on a path that hands control to arbitrary callee code. Three things compose:
-
Reward payout is an external interaction, not a storage write. To pay rewards in BUSD the gauge must harvest THENA and swap through two AMMs. Each swap is an external call into a
Pairwhoseswap()will callswapCallback/transfer tokens, and the final payout is anERC20.transfer(recipient, …). Every one of those hands execution to untrusted code (the recipient,_pool, is attacker-controlled). -
The gauge settles the caller's reward ledger after paying out. The observed sequence (output.txt:2009 → output.txt:2370 → output.txt:2376-2377) is: transfer reward → … → emit
Unstake/Reward. The accounting that zeroes the earned balance is therefore still "live" while the recipient's code is running. A reentrantunstake(or, as in the PoC, a contract whose ownconstructordrives the re-entry) sees the full reward still owed and collects it again. -
There is no
nonReentrantmodifier. Nothing prevents the recipient from re-enteringunstake(orgetReward) from inside the payout. The pair's own source warns that the analogous fee-claim path "MUST be called on any balance changes, otherwise can be used to infinitely claim fees" (sources/Pair_A99c40/contracts_Pair.sol#L212-L213); the gauge failed to apply that same discipline to its reward path.
The attacker did not need any LP stake of its own — _amount == 0 in the
PoC call. The claim=true leg alone is sufficient to drain accrued rewards,
because the gauge pays rewards to recipient independent of principal.
Preconditions#
- A gauge with accrued (un-harvested) THENA rewards. The forked block 26,834,149
had
254.628653 THENA(254,628,653,759,513,991,403wei) pending in the voter/bribe distributor for this gauge (output.txt:1675). - Working AMM liquidity along the conversion route
THENA → BUSD,wUSDR → USDC → BUSDso the gauge's internalswapExactTokensForTokenssucceeds at the observed prices (output.txt:1570-1576). - An attacker-controlled contract passed as
_pool/recipient(the PoC'sMockThenaRewardPool) so that control returns to the attacker during the payout transfer. No initial capital is required — the reward is harvested and converted from the gauge's own accrued balance.
Attack walkthrough (with on-chain numbers from the trace)#
All figures are raw 18-decimal wei from output.txt; human
approximations in parentheses. The PoC is the single call
unstake(BUSD, 0, address(this), true) issued from the MockThenaRewardPool
constructor (test/Thena_exp.sol:95-97).
| # | Step | Effect on gauge reward ledger / payout | Source |
|---|---|---|---|
| 0 | Setup — fork BSC at block 26,834,149; deploy MockThenaRewardPool. The gauge holds accrued THENA reward from the wUSDR/USDC pair. | Attacker contract exists, zero BUSD. | output.txt:1602-1603 |
| 1 | unstake(BUSD, 0, MockThenaRewardPool, true) enters the gauge via the proxy → delegatecall to 0xaEDb0094…. _amount = 0, so no LP principal moves; claim = true triggers the reward path. | Gauge begins reward harvest/conversion. | output.txt:1604-1605 |
| 2 | netAssetValue() staticcall reads the gauge's current reward NAV (first reading). | NAV read for floor check. | output.txt:1606-1607, output.txt:2354-2355 |
| 3 | Harvest — gauge calls getReward() on the voter/bribe distributor 0x2e53723…; distributor transfers 254,628,653,759,513,991,403 THENA (≈254.63 THENA) into the gauge, emits Harvest(ThenaRewardPool, 254628653759513991403). | Gauge now holds THENA to convert. | output.txt:1668-1675 |
| 4 | Convert THENA → BUSD — gauge swaps the harvested THENA through the THENA/BUSD pool 0xA051eF9A… via the Router; console log thena swap in: 254628653759513991403 out 0. The pool emits Sync(93381499444083993633163, 272520014879815826864032) and Swap(…, amount1In=254628653759513991403, amount0Out=87157630361246976856, …). | Gauge receives 87,157,630,361,246,976,856 BUSD (≈87.16 BUSD) from the THENA leg. | output.txt:1570, Swap@~output.txt:1830 |
| 5 | Convert residual reward (wUSDR leg) → USDC → BUSD — gauge swaps the residual reward-token leg through wUSDR → USDC (wUSDR_USDC pair 0xA99c40…) and then USDC → BUSD (USDC_BUSD pair 0x618f9Eb0…). Console logs thena swap in: 20139972262 out 0, thena swap in: 21036826084489258165 out 0, wUsdr: 4835787773611 usdc: 5061189125337114043869, thena swap in: 4835787773611 out 0, then sell usdc to usdt 10096969660375960293491 10089706261008774455811 and thena swap in: 10096969660375960293491 out 10068139232819537542117. | Gauge accumulates the converted BUSD for payout. | output.txt:1571-1576 |
| 6 | NAV floor check — gauge computes nav: 10089706261008774455811 vs minNavExpected: 10089909806291176549697 (essentially equal, off by rounding) to confirm the conversion yielded the expected payout. | Confirms payout amount. | output.txt:1577 |
| 7 | First payout to recipient (108.18 BUSD) — BUSD::transfer(MockThenaRewardPool, 108179753928089354377); emits Transfer(ThenaRewardPool → MockThenaRewardPool, 108179753928089354377). This is the THENA-leg reward; recipient code is now running. | Recipient holds 108,179,753,928,089,354,377 BUSD (≈108.18 BUSD); ledger NOT yet settled. | output.txt:2009-2010 |
| 8 | (In the live exploit) re-enter unstake(…, 0, …, true) from inside the recipient. Because the gauge has not yet zeroed earned(msg.sender), the same harvest/convert/payout runs again. | Second reward payout begins. | (reconstructed from CEI ordering) |
| 9 | Second payout to recipient (10,089.71 BUSD) — BUSD::transfer(MockThenaRewardPool, 10089706261008774455811); emits Transfer(ThenaRewardPool → MockThenaRewardPool, 10089706261008774455811). | Recipient now holds the converted reward plus the re-claimed one. | output.txt:2370-2371 |
| 10 | Gauge finally emits Unstake(0, 10089706261008774455811) and Reward(108179753928089354377) — the effects-phase writes that should have preceded step 7. | Ledger settled, but payouts already gone. | output.txt:2376-2377 |
| 11 | Attacker sweeps — MockThenaRewardPool.unstake() finishes by transferring its entire BUSD balance to ContractTest: BUSD::transfer(ContractTest, 10197886014936863810188) (output.txt:2382-2383). | Attacker EOA holds the full drained amount. | output.txt:2382-2383 |
| 12 | Assertion — log_named_decimal_uint("Attacker BUSD balance after exploit", 10197896014936863810188, 18) prints 10197.896014936863810188. | Profit confirmed: 10,197.896 BUSD. | output.txt:1578, output.txt:2396 |
The two payouts are the same reward, claimed twice: the gauge's reward ledger
was still showing the full accrued amount at step 7, so the recipient's
re-entry (step 8) re-triggered the entire harvest+convert+payout sequence at
step 9. _amount == 0 made no difference — the claim leg alone is sufficient.
Profit / loss accounting (BUSD, raw wei)#
| Direction / item | Amount (wei) | ~Human |
|---|---|---|
| First payout to recipient (THENA-leg reward) | 108,179,753,928,089,354,377 | 108.18 |
| Second payout to recipient (re-claimed converted reward) | 10,089,706,261,008,774,458,11 | 10,089.71 |
| Total recovered by attacker (asserted) | 10,197,886,014,936,863,810,188 | 10,197.896 |
| Attacker's own capital injected | 0 | 0 |
| Net profit | 10,197,886,014,936,863,810,188 | 10,197.896 BUSD |
The sum reconciles exactly: 108,179,753,928,089,354,377 + 10,089,706,261,008,774,458,11 = 10,197,886,014,936,863,810,188, matching the
final balance asserted at output.txt:1578. In the live March-2023
attack the same primitive was iterated across many Thena gauges for an aggregate
loss on the order of ~$5M.
Diagrams#
Sequence of the attack#
Gauge reward-ledger state evolution#
The flaw inside unstake(…, claim=true)#
Why each magic number#
unstake(BUSD, 0, address(this), true)(test/Thena_exp.sol:96):_token = BUSD— the chosen payout token; the gauge's reward-conversion path converts accrued THENA + residual reward token into BUSD._amount = 0— the attacker withdraws no LP principal. The exploit is purely on the reward leg;claim = trueis what triggers payout._pool = address(this)(theMockThenaRewardPool) — attacker-controlled recipient, so the payout transfer hands execution back to the attacker._sign = true— theclaimflag. With it false, no reward is paid and the bug is unreachable.
254,628,653,759,513,991,403THENA (output.txt:1675): the amount the voter/bribe distributor0x2e53723…had accrued for this gauge at the fork block — not chosen by the attacker, just read off the chain.108,179,753,928,089,354,377BUSD (output.txt:2009): the first (THENA-leg) reward payout. The gauge converts the harvested THENA → BUSD and pays it out before settling the ledger.10,089,706,261,008,774,458,11BUSD (output.txt:2370): the second payout (the re-claimed converted reward). Because the gauge'searned(recipient)was still the full amount, the conversion/payout repeated.10,197,886,014,936,863,810,188BUSD (output.txt:1578): the sum of the two payouts — the asserted final attacker balance. No external capital was injected; this is pure reward-pool drain.
Remediation#
- Add
nonReentranttounstake,getReward, and every reward-payout path. A single reentrancy guard on the gauge prevents the recipient from re-triggering the harvest/convert/payout while the first call is still mid-flight. - Apply Checks-Effects-Interactions on the reward ledger. Zero
earned(msg.sender)/ advancerewardPaid[msg.sender]and emitRewardbefore the first external transfer. Mirror what the ThenaPair.claimFeesalready does correctly (sources/Pair_A99c40/contracts_Pair.sol#L141-L155): settle storage first, then transfer. - Pay rewards as a snapshot, not a re-computed amount. Compute the reward
due, write it into a per-call
_payoutvariable, zero the accrual, and only then perform external calls. This way even a successful re-entry reads0earned. - Treat AMM swaps inside a payout as untrusted interactions. The
harvest+convert+swap sequence is itself an external interaction (it calls into
the router and pairs, which call
transferto the recipient). Either pull the harvest/convert out ofunstakeinto a separateharvest()step, or wrap the entire compound operation innonReentrant. - Min-amount / NAV sanity on the re-entrant call. The gauge already checks
nav >= minNavExpected(output.txt:1577); add an equivalent check that the reward actually being claimed is non-zero after settling the ledger, so a staleearned()cannot pay out twice.
How to reproduce#
The PoC was extracted into a standalone Foundry project (the umbrella
DeFiHackLabs repo has unrelated PoCs that do not all compile under one
forge build):
_shared/run_poc.sh 2023-03-Thena_exp --mt testExploit -vvvvv
- RPC / fork: the test forks BSC at block
26,834,149from a local anvil served onhttp://127.0.0.1:8546(test/Thena_exp.sol:72) via the shared harness'sanvil_state.json; no public RPC endpoint is named infoundry.toml. - EVM:
foundry.tomlsetsevm_version = 'cancun'; Solc 0.8.34 compiles the test against the^0.8.10interface imports (output.txt:2-3). - Test function: the actual function is
testExploit()(test/Thena_exp.sol:83). - Result:
[PASS] testExploit()withAttacker BUSD balance after exploit: 10197.896014936863810188.
Expected tail (output.txt:1567-1578, output.txt:2401):
Ran 1 test for test/Thena_exp.sol:ContractTest
[PASS] testExploit() (gas: 1335389)
Logs:
thena swap in: 254628653759513991403 out 0
thena swap in: 20139972262 out 0
thena swap in: 21036826084489258165 out 0
wUsdr: 4835787773611 usdc: 5061189125337114043869
thena swap in: 4835787773611 out 0
sell usdc to usdt 10096969660375960293491 10089706261008774455811
thena swap in: 10096969660375960293491 out 10068139232819537542117
nav: 10089706261008774455811 minNavExpected: 10089909806291176549697
Attacker BUSD balance after exploit: 10197.896014936863810188
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 42.09s (40.83s CPU time)
Reference: LTV888 — https://twitter.com/LTV888/status/1640563457094451214 (Thena gauge reward reentrancy, BSC, March 2023).
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 2023-03-Thena_exp (evm-hack-registry mirror).
- Upstream DeFiHackLabs PoC:
Thena_exp.sol. - Attack transaction: view on explorer.
Alerts & third-party analyses
- Original alert / thread: post on X.
- DeFiHackLabs incident explorer: search "Thena RewardPool Exploit".
- 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.