Note: DBXen did not run Olympix on these contracts before deployment. We ran our analysis AFTER the exploit had already occurred, which lets us show concretely what Olympix would have caught had it been part of the team's pre-deployment workflow. Every finding below was generated from the same contracts the attacker exploited, with no foreknowledge of the incident.
Protocol: DBXen
Network: Ethereum mainnet, BNB Smart Chain
Date of incident: March 11, 2026
Amount lost: ~$149,000
Attack class: Inconsistent sender identity under ERC-2771 meta-transactions
Affected contracts: DBXen reward distribution module on both chains
Summary of the Incident
On March 11, 2026, an attacker drained roughly $149,000 from DBXen's reward distribution contracts on Ethereum and BSC. The attack required no flash loans, no reentrancy, and no oracle manipulation. It exploited two characters of difference inside a single function: msg.sender versus _msgSender().
DBXen lets users burn XEN tokens in discrete cycles to earn DXN rewards and a share of protocol fees. Contributions are tracked in accCycleBatchesBurned, and a user's most recent activity is tracked in lastActiveCycle. To support gasless transactions, the contract integrates with an ERC-2771 trusted forwarder using OpenZeppelin's _msgSender() helper, which unwraps the real user address from forwarded calls.
BlockSec Phalcon flagged suspicious transactions hours after they began, and the Verichains team published a full technical breakdown of the exploit on March 27. The attacker walked away with rewards drawn from a single legitimate burn, repeated until the cycle's reward pool was depleted.
The exploited contract on Ethereum was the DBXen module at 0xf5c80c305803280b587f8cabbccdc4d9bf522abd, with the corresponding BSC deployment at 0x9caf6c4e5b9e3a6f83182befd782304c7a8ee6de. The attacker operated from EOAs 0x63150ac8e35c6c685e93ee4d7d5cb8eafb2f016b (Ethereum) and 0xe92fa2a5fef535479a91ab9ed90b26256ff276f1 (BSC).
Background: How the Accounting Works
DBXen's reward distribution is straightforward in principle. During each cycle, users call burnBatch() to burn XEN tokens, and their contribution is recorded in accCycleBatchesBurned[user]. At cycle end, each user's reward share is computed proportionally:
reward = accCycleBatchesBurned[user] × rewardPerCycle[lastActiveCycle[user]]
/ cycleTotalBatchesBurned[lastActiveCycle[user]]
This calculation runs inside updateStats(), which is called by both claimRewards() and claimFees(). The function gates execution on a single condition: it only credits new rewards if the user has non-zero burned batches AND the current cycle has advanced past lastActiveCycle[user]. Once rewards are credited, accCycleBatchesBurned[user] is zeroed out and lastActiveCycle[user] is bumped forward, marking the contribution as fully settled.
This pattern is safe when both pieces of state move together. The bug is that they did not.
The Bug, in Code
burnBatch() is wrapped by a gasWrapper modifier, which credits the burn under the real user's address using OpenZeppelin's meta-transaction helper:
// Inside gasWrapper modifier
accCycleBatchesBurned[_msgSender()] += batchNumber; // correct: real user
In the same execution flow, burnBatch() then calls into the XEN token contract using the raw EVM sender:
// Inside burnBatch()
IBurnableToken(xen).burn(msg.sender, batchNumber * XEN_BATCH_AMOUNT); // wrong: forwarder
In a meta-transaction, msg.sender is the trusted forwarder. XEN's burn implementation invokes a callback into DBXen's onTokenBurned(address user, uint256 amount) function, passing through whichever address was given. That handler writes:
function onTokenBurned(address user, uint256 amount) external {
require(msg.sender == xen, "DBXen: caller must be XEN");
lastActiveCycle[user] = currentCycle; // updates forwarder, not real user
}
The state was now split. accCycleBatchesBurned[realUser] was credited correctly. lastActiveCycle[realUser] was never touched. lastActiveCycle[forwarder] was updated, but no human controls the forwarder address, so that update was meaningless.
The Attack, Step by Step
- The attacker called
burnBatch() through the trusted forwarder, burning a small amount of XEN. accCycleBatchesBurned[attacker] incremented as expected.lastActiveCycle[attacker] remained stale (effectively zero or its prior value).- The attacker called
claimRewards(). Inside updateStats(), the gating condition currentCycle > lastActiveCycle[attacker] evaluated true, and the reward was credited. - Critically, because
lastActiveCycle[attacker] was driven from the forwarder callback path and not from the user-facing path, the settlement that should have advanced it never reached the attacker's address. The gating condition stayed true on the next call. - The attacker called
claimRewards() again. Same calculation. Same payout. The contract treated the original burn as still unsettled. - Repeat until the cycle's reward pool was empty.
A single legitimate burn produced unbounded reward claims. Cross-chain, the same pattern played out on both Ethereum and BSC for a combined ~$149K loss.
Root Cause
Two conditions had to hold simultaneously for this attack to succeed, and in DBXen's deployment both did:
- Mixed sender semantics inside a single execution flow. The
gasWrapper modifier and burnBatch() body sat directly adjacent in the source, yet one used _msgSender() while the other used msg.sender. Meta-transaction integrations require that every state read or write tied to user identity routes through _msgSender(). A single misplaced msg.sender is enough to break the invariant. - No idempotency check on reward settlement.
updateStats() trusted lastActiveCycle as its sole gate for whether a contribution had been processed. It did not cross-check whether accCycleBatchesBurned had been zeroed, did not record a per-burn settlement marker, and did not enforce that the cycle counter advance for the same address whose batch contribution was credited.
The broken invariant is simple to state:
If accCycleBatchesBurned[user] was incremented in cycle N, then lastActiveCycle[user] must equal N before that contribution can be claimed, and must advance past N once the claim is settled.
The code never enforced this invariant across the meta-transaction boundary.
What Olympix Found (Post-Exploit Analysis)
DBXen did not run Olympix over this codebase before the incident. After the exploit, we pointed BugPocer, our PoC-generating agent, at the exact contracts involved to test a simple question: would Olympix have caught this in a routine pre-deployment scan? The answer is yes, unambiguously.
In a single run (session ecf38818-3e9c-4e10-877c-d540a8a7a41b, April 21, 2026), BugPocer produced two true-positive findings that match the incident end to end, both with runnable Foundry tests that fail on the exploited contract. No tuning, no hand-holding, no foreknowledge of the attack. This is what Olympix is built to do.
Finding 1
Severity: High
ID: msg_sender_vs_msgSender_inconsistency
Location: DBXen.sol, claimRewards(), updateStats(), burnBatch(), onTokenBurned(), gasWrapper
Contract: 0xf5c80c305803280b587f8cabbccdc4d9bf522abd
We flagged that the gasWrapper modifier credits accCycleBatchesBurned[_msgSender()] (the real user) while burnBatch() invokes xen.burn(msg.sender, ...) (the forwarder) inside the same execution path. Our finding traced the consequence explicitly: XEN's callback would set lastActiveCycle[forwarder] instead of lastActiveCycle[realUser], leaving the real user's lastActiveCycle permanently stale and enabling repeated reward claims from a single burn.
This is the $149K exploit, identified by name, with the precise execution flow described before any attacker touched mainnet.
Finding 2
Severity: High
ID: division_by_zero_dos
Location: DBXen.sol, updateStats() Block 1
Contract: 0xf5c80c305803280b587f8cabbccdc4d9bf522abd
The same root cause produced a second high-severity issue. With lastActiveCycle[realUser] stuck at zero (because the forwarder callback never reached the real user's slot) and cycleTotalBatchesBurned[0] also zero (no burns in cycle 0), updateStats() divides by zero and reverts. Any user who burned through the forwarder before any activity in cycle 0 would have been permanently locked out of claimRewards(), claimFees(), stake(), and unstake().
A second-order denial-of-service hiding behind the same single-word bug. Same finding run, same engine, no separate analysis required.
The Proof of Concept
Critically, we did not stop at descriptions. BugPocer produced runnable PoCs for both findings.
claimRewards_msg_sender_vs_msgSender_inconsistency.t.sol deploys the DBXen contract against a mock XEN token and a minimal ERC-2771 trusted forwarder, simulates a burn through the forwarder, and asserts:
accCycleBatchesBurned[realUser] increments correctly.lastActiveCycle[realUser] matches the burn cycle after the call.
The first assertion passes. The second fails. The test then advances time, lets the real user call claimRewards() directly, and re-checks lastActiveCycle[realUser]. The value is still zero after rewards have been claimed, demonstrating the persistent staleness that enables repeated extraction.
updateStats_division_by_zero_dos.t.sol deploys the real DBXen contract, executes a forwarder-mediated burn, advances the cycle, and calls claimRewards(). The call reverts on a division by zero inside updateStats(). The test does not catch the revert. It demonstrates that the legitimate user is permanently DoS'd through the same code path the attacker exploited.
In other words, Olympix did not just pattern-match on risky code. BugPocer mechanically produced the failing invariants, proved they fail under realistic conditions, and named the exact attack primitive: ERC-2771 sender split between burn accounting and active-cycle accounting, leading to repeated reward claims and incidental DoS.
This is the gap most security tools leave open. Static analyzers flag suspicious patterns and leave engineers to decide whether they matter. Manual audits find a subset of bugs but cannot scale to every contract change, especially across meta-transaction boundaries where the relevant context is split across multiple files. Olympix closes the loop: it identifies the vulnerability, writes the exploit as a test, and hands the team a reproducible failure they can fix before merging. The DBXen case is a clean demonstration of that loop running on real, exploited code.
How Olympix Would Have Prevented This
The shortest version: had Olympix been in DBXen's pre-deployment workflow, this contract would not have shipped with the bug.
BugPocer's findings land on an engineer's desk as failing tests. Those tests are the trigger for a fix. From there, any of the following mitigations would have closed the attack surface, and the failing tests would have moved to passing only once the fix was in place:
- Replace
msg.sender with _msgSender() on the XEN burn call. The literal one-word fix. IBurnableToken(xen).burn(_msgSender(), ...) ensures the callback path lands on the real user's address, keeping lastActiveCycle aligned with accCycleBatchesBurned. - Settle
lastActiveCycle from the user-facing path, not the callback. Update lastActiveCycle[_msgSender()] = currentCycle directly inside burnBatch() before the external XEN call, removing the dependence on the callback altogether and making the function safe even if the XEN integration changes upstream. - Cross-check both state slots in
updateStats(). Require that accCycleBatchesBurned[account] != 0 AND lastActiveCycle[account] == cycleOfMostRecentBurn before crediting rewards. A consistency check makes the bug self-correcting at the settlement layer. - Lint or invariant test for sender-identity uniformity. Any function that uses
_msgSender() should not also use msg.sender in the same execution path for user-related state. This is a static rule that BugPocer's finding effectively encodes, and that a team with Olympix in CI would enforce on every commit. - Defense-in-depth: per-burn settlement marker. Track a per-cycle, per-user "settled" flag rather than relying on a comparison between two mappings that can drift out of sync. More state, but immune to identity-mismatch bugs of this class.
The decision tree without Olympix: the team did not know the bug existed, so none of these mitigations were on the table. The decision tree with Olympix: the team has failing tests in CI before deployment, picks one of the fixes above, and the contract goes live without exposure. That is the difference between a $149K cross-chain loss and a non-event.
Timeline
- Pre-incident: DBXen reward distribution contracts deployed on Ethereum and BSC with ERC-2771 meta-transaction support.
- March 11, 2026: Attacker executes the exploit. BlockSec Phalcon flags suspicious transactions targeting
@DBXen_crypto's contract within hours, estimating ~$150K loss across both chains. - March 12, 2026: Public reporting confirms the incident. Multiple security researchers identify the ERC-2771 sender mismatch as the root cause.
- March 27, 2026: Verichains publishes a full technical post-mortem walking through the
msg.sender vs _msgSender() split, the stale lastActiveCycle, and the repeated updateStats() calculation. - April 21, 2026 (41 days after the exploit): We ran BugPocer against the already-compromised contracts as a post-incident exercise. It produced two true-positive findings with reproducing PoCs that match the exploit path precisely, confirming the bug was detectable by automated analysis before deployment.
The Lesson for Web3 Teams
This was not an exotic attack. ERC-2771 sender confusion is one of the most well-documented bug classes in meta-transaction code, and the fix patterns are well understood. The bug shipped anyway, drained $149K cross-chain, and forced a public post-mortem that named the exact line of code at fault.
That is the pattern Olympix exists to break. Every protocol team is shipping faster than its audit cadence can keep up with, and meta-transaction integrations are a particularly high-risk surface because the relevant context is fragmented. The user-facing path, the modifier, the external token call, and the callback all live in different parts of the file, and reviewers cannot hold every sender reference in working memory at once. A single misplaced msg.sender survives review, slips through testing, and waits for an attacker who happens to be reading carefully.
Manual review is too slow for the surface area a modern DeFi protocol exposes. Pattern-matching tools are too noisy to act on. Olympix was built for this exact gap. BugPocer reads the contract, identifies the vulnerable invariant, writes a runnable exploit, and gives the team something they can fix and ship the same day. The DBXen case demonstrates the full loop on a real exploit, with a real attacker, a real $149K loss, and findings that would have prevented all of it.
If you are running a DeFi protocol, especially anything that touches meta-transactions, identity routing, or reward accounting, the question is not whether Olympix would have caught your last bug. It is whether you want to find your next one before an attacker does.
Run Olympix on your contracts before your next deployment. Book a demo and get your first scan FREE.