This is a lower bound: only assets with reliable historical USD prices are counted, so the actual loss may be higher.
0x914a5af790e55b8ea140a79da931fc037cb4c4457704d184ad21f54fb808bc370xf5c80c305803280b587f8cabbccdc4d9bf522abdEthereumOn Ethereum mainnet block 24637401, transaction 0x914a5af790e55b8ea140a79da931fc037cb4c4457704d184ad21f54fb808bc37 exploited DBXen's trusted-forwarder integration. The attacker cluster used helper contract 0xeb233deaeb7f594e2a73e914ee1f6ae79b491866 to buy exactly 13,900,000,000 XEN, move that XEN into trusted forwarder 0xf3281221ba95af0c5bbcbd2474ce8c090233133b, and then submit self-signed forwarder requests on behalf of signer 0x425d3ec2dcebe2c04ba1687504d43afc6be7328d. DBXen 0xf5c80c305803280b587f8cabbccdc4d9bf522abd burned the forwarder's XEN, but credited the fresh signer inside DBXen and immediately paid that signer 65.361960326939766177 ETH plus 2305.427706597006261143 DXN.
The root cause is an identity split across the trusted forwarder boundary. Forwarder.execute always calls the target as the forwarder while appending req.from to calldata, but DBXen mixes msg.sender and _msgSender() across its burn and reward paths. That lets a fresh signer keep lastActiveCycle=0 and lastFeeUpdateCycle=0 while accCycleBatchesBurned is credited to that signer, causing DBXen to replay cycle-0 reward math and the historical fee accumulator against newly burned XEN.
DBXen is a burn-to-earn protocol that accepts XEN burns and tracks the burner through per-account lifecycle variables such as lastActiveCycle, lastFeeUpdateCycle, accCycleBatchesBurned, accRewards, and accAccruedFees. It uses ERC2771Context, so _msgSender() resolves to the appended signer when calls arrive through trusted forwarder 0xf3281221ba95af0c5bbcbd2474ce8c090233133b.
The forwarder is not specific to DBXen. Its execute function appends req.from to arbitrary calldata and low-level calls req.to as the forwarder itself. Contracts that are not ERC-2771-aware therefore still observe msg.sender == forwarder.
XEN 0x06450dee7fd2fb8e39061434babcfc05599a6fb8 is not ERC-2771-aware. When the forwarder executes approve, XEN records allowance for the forwarder address, not for the recovered signer. DBXen then consumes the forwarder's XEN allowance in burnBatch.
The exploitable pre-state at block 24637400 is fully public and deterministic:
DBXen.currentCycle() = 1085
DBXen.lastStartedCycle() = 1084
DBXen.lastActiveCycle(0x425d...) = 0
DBXen.lastFeeUpdateCycle(0x425d...) = 0
DBXen.accCycleBatchesBurned(0x425d...) = 0
DBXen.rewardPerCycle(0) = 10000000000000000000000
DBXen.cycleTotalBatchesBurned(0) = 24117
DBXen.cycleFeesPerStakeSummed(1085) = 283513380792231356278253387153403159416
DBXen.SCALING_FACTOR() = 10000000000000000000000000000000000000000
This incident is an ATTACK, not a benign MEV unwind. DBXen assumes that the account funding the XEN burn is the same logical account whose reward state is updated, but that assumption breaks when calls arrive through the trusted forwarder. burnBatch validates and burns XEN from msg.sender, while gasWrapper, claimFees, and claimRewards use _msgSender(). XEN's burn callback then updates lastActiveCycle for the burn source, not for the rewarded signer. The result is that the forwarder becomes the burned account, but the signer becomes the rewarded account. Because the signer is still fresh at cycle 0, updateStats computes rewards with rewardPerCycle(0) and fees with the entire historical cycleFeesPerStakeSummed path. That identity split is the decisive code-level breakpoint.
The vulnerable component set is DBXen itself, its trusted forwarder, and the XEN burn target they compose with. The violated security principles are identity consistency across meta-transactions, avoiding generic trusted-forwarder use against non-ERC2771 targets, and binding the burned-asset source to the rewarded beneficiary. The exploit remains ACT because any unprivileged actor can acquire XEN, load the permissionless forwarder, self-sign forwarder requests, and use a fresh or similarly stale DBXen signer account.
The forwarder code makes the trust-boundary issue explicit:
// Verified Forwarder::execute
function execute(
ForwardRequest calldata req,
bytes32 domainSeparator,
bytes32 requestTypeHash,
bytes calldata suffixData,
bytes calldata sig
) external payable returns (bool success, bytes memory ret) {
_verifySig(req, domainSeparator, requestTypeHash, suffixData, sig);
_verifyAndUpdateNonce(req);
bytes memory callData = abi.encodePacked(req.data, req.from);
(success, ret) = req.to.call{gas: req.gas, value: req.value}(callData);
}
That behavior is safe only for targets that intentionally decode ERC-2771 calldata. DBXen partially does so, but not consistently. The critical DBXen paths are:
// Verified DBXen burn / credit / payout paths
modifier gasWrapper(uint256 batchNumber) {
_;
accCycleBatchesBurned[_msgSender()] += batchNumber;
sendViaCall(payable(msg.sender), msg.value - protocolFee);
}
function onTokenBurned(address user, uint256 amount) external {
updateStats(user);
lastActiveCycle[user] = currentCycle;
}
function burnBatch(uint256 batchNumber) external payable gasWrapper(batchNumber) {
require(xen.balanceOf(msg.sender) >= batchNumber * XEN_BATCH_AMOUNT);
IBurnableToken(xen).burn(msg.sender, batchNumber * XEN_BATCH_AMOUNT);
}
function claimFees() external {
updateStats(_msgSender());
sendViaCall(payable(_msgSender()), accAccruedFees[_msgSender()]);
}
function claimRewards() external {
updateStats(_msgSender());
dxn.mintReward(_msgSender(), reward);
}
XEN's burn implementation then closes the loop by burning the forwarder's balance while reporting the forwarder-owned burn source back to DBXen:
// Verified XEN::burn
function burn(address user, uint256 amount) public {
_spendAllowance(user, _msgSender(), amount);
_burn(user, amount);
IBurnRedeemable(_msgSender()).onTokenBurned(user, amount);
}
The seed trace proves the exact exploit sequence. The helper first registers domain separator ("W","1"), swaps ETH for XEN on Uniswap, and transfers the purchased XEN into the forwarder. It then calls forwarder.execute to run XEN.approve(DBXen, 13_900_000_000 XEN); because XEN is not ERC-2771-aware, the allowance becomes allowance[forwarder][dbxen]. A second forwarded call invokes DBXen.burnBatch(5560), which burns 5560 * 2,500,000 XEN = 13,900,000,000 XEN from the forwarder while gasWrapper credits accCycleBatchesBurned[0x425d...] = 5560. onTokenBurned updates lastActiveCycle[forwarder], leaving lastActiveCycle[0x425d...] = 0.
Once the signer remains fresh, DBXen's reward formulas become deterministic:
expected_reward = 5560 * rewardPerCycle(0) / cycleTotalBatchesBurned(0)
= 2305427706597006261143
expected_fees = expected_reward * cycleFeesPerStakeSummed(1085) / SCALING_FACTOR
= 65361960326939766177
The trace and balance diff match those values exactly. DBXen::claimFees sends 65361960326939766177 wei to 0x425d..., and DBXen::claimRewards mints 2305427706597006261143 DXN to the same signer. The helper then immediately calls DBXenERC20::permit and DBXenERC20::transferFrom to move the minted DXN from 0x425d... to funding EOA 0x63150ac8e35c6c685e93ee4d7d5cb8eafb2f016b, which explains why the final token balance diff lands on 0x63150... even though the mint occurred to 0x425d....
0x63150ac8e35c6c685e93ee4d7d5cb8eafb2f016b calls helper contract 0xeb233deaeb7f594e2a73e914ee1f6ae79b491866 in transaction 0x914a5af790e55b8ea140a79da931fc037cb4c4457704d184ad21f54fb808bc37.13,900,000,000 XEN through Uniswap router 0x7a250d5630b4cf539739df2c5dacb4c659f2488d.0xf3281221ba95af0c5bbcbd2474ce8c090233133b.0x425d3ec2dcebe2c04ba1687504d43afc6be7328d, the helper asks the forwarder to call XEN.approve(DBXen, burnAmount) and then DBXen.burnBatch(5560).accCycleBatchesBurned to the signer while leaving the signer at cycle-0 baseline.DBXen.claimFees() and DBXen.claimRewards(). These pay ETH and mint DXN to signer 0x425d....DBXenERC20.permit plus transferFrom to sweep the minted DXN to 0x63150..., then refunds residual ETH back to the same funding EOA.Every step is permissionless. No attacker-specific contract bytecode, privileged role, or private key beyond the attacker's own signer is needed. The helper contract only batches public calls that a clean EOA or local PoC contract can reproduce.
The adversary cluster in the incident consists of funding EOA 0x63150ac8e35c6c685e93ee4d7d5cb8eafb2f016b, forwarder signer 0x425d3ec2dcebe2c04ba1687504d43afc6be7328d, and helper contract 0xeb233deaeb7f594e2a73e914ee1f6ae79b491866. The primary victim is DBXen, while the trusted forwarder and XEN contract are stakeholder contracts whose public behavior makes the exploit realizable.
The immediate protocol loss is 65.361960326939766177 ETH from DBXen's fee payout path plus 2305.427706597006261143 DXN minted without the intended burn lifecycle. The signer 0x425d... receives the ETH payout directly, while the helper contract forwards the DXN to 0x63150... before the transaction ends. The funding EOA pays 0.127266312795464389 ETH in setup and gas, so the attacker cluster's net ETH delta across the two EOAs is still a positive 65.234694014144301788 ETH before valuing the DXN.
The loss is deterministic on the observed pre-state because rewardPerCycle(0), cycleTotalBatchesBurned(0), and cycleFeesPerStakeSummed(1085) are already fixed before the exploit transaction. Any fresh signer routed through the same forwarder path can realize the same stale-account replay property.
0x914a5af790e55b8ea140a79da931fc037cb4c4457704d184ad21f54fb808bc370xf5c80c305803280b587f8cabbccdc4d9bf522abd0xf3281221ba95af0c5bbcbd2474ce8c090233133b0x06450dee7fd2fb8e39061434babcfc05599a6fb80x80f0c1c49891dcfdd40b6e0f960f84e6042bcb6f0x63150ac8e35c6c685e93ee4d7d5cb8eafb2f016b0x425d3ec2dcebe2c04ba1687504d43afc6be7328d0xeb233deaeb7f594e2a73e914ee1f6ae79b491866