Wake Arena: Multi-Agent AI Audit with Graph-Driven Reasoning
Benchmarked on audit competitions and production protocols
December 2025:
Josef Gattermayer, Ph.D.1,2
Michal Převrátil1
Martin Veselý1
Andrei Shchapaniak1
Josef Bazal1
1Ackee Blockchain
2Czech Technical University in Prague
Executive Summary
We present Wake Arena, a service for discovering vulnerabilities in Solidity smart contracts through multi-agent AI analysis with graph-driven reasoning.
Wake Arena 3.1 discovered 63 of 94 critical/high-severity vulnerabilities in historical audit competitions, outperforming Zellic’s automated scanner V12 (41/94), GPT-5.2 xhigh (41/94), plain GPT-5 (24/94), and plain Opus 4.5 (21/94). When experimentally integrated into Ackee’s manual audit workflow in November 2025 for Lido, Printr, and Everstake, Wake Arena identified 26 findings.
Specifically, Wake Arena discovered 5 critical vulnerabilities and 5 unique findings beyond those discovered by human auditors during the Printr audit.
Additionally, LUKSO served as a design partner, providing valuable feedback during development. In a purely AI-driven audit, Wake Arena identified 10 findings (2 High, 6 Medium, 1 Low, 1 Warning), with only two false positives. LUKSO’s responses and validation helped refine the service’s accuracy and user experience.
Metrics across benchmarks and production audits:
- 51.4% of all reported findings and 50%+ of critical findings discovered
- False positive rate below 30%
- True positive rate above 70%
Unlike generic LLM wrappers, Wake Arena combines multi-agent AI reasoning, graph-driven analysis via Data Dependency and Control Flow graphs, and LLM-tailored static analysis from 200+ audits securing $180B+ in TVL.
Background and Motivation
Ackee Blockchain has conducted 200+ smart contract audits securing $180B+ in TVL (Lido, Aave, Axelar, Safe). As AI capabilities for code analysis have advanced, we built Wake Arena to leverage these capabilities while maintaining low false positive rates.
Wake Arena is a multi-agent AI system combining our private detector library (87 from billion-dollar audits) with deep AI-driven security reasoning that navigates Data Dependency Graphs like a senior auditor. LLM-tailored static analysis feeds deep code insights into a full AI-driven audit pipeline with graph-driven reasoning and contextual understanding from years of auditing expertise.
Problem statement
Teams need a consistent, powerful security tool that reliably finds important vulnerabilities before premium audits, helping protocols arrive with cleaner code and use audit time for deep protocol logic review. We want to help teams reduce reliance on surface-level tools while recognizing that high-quality manual audits remain essential for critical systems.
What Makes Wake Arena Different
1. Multi-agent AI system
A multi-step prompt pipeline combines:
- Advanced reasoning and validation with multi-agent cross-checking
- Deep contextual understanding from senior auditor expertise embedded in prompts
- Full AI-driven audit pipeline from compilation to report generation
2. Graph-driven reasoning
LLM-tailored Static Analysis with Data Dependency and Control Flow graphs navigates the code to find:
- Protocol-specific logical issues by tracing execution paths
- Mathematical vulnerabilities through value flow analysis
- Cross-function dependencies missed by pattern matching
Example: In the Lend protocol, Wake Arena traced how a deeply nested bug in LendStorage.borrowWithInterest affects CoreRouter and CrossChainRouter logic through getHypotheticalAccountLiquidityCollateral and other view functions, leading to denial-of-service reverts and incorrect accounting in the protocol. Tracing of the bug effects is what requires a deep graph-based reasoning.
3. Battle-tested static analysis integrated into AI workflow
LLM-tailored Static Analysis feeds deep insight about the analyzed code to the AI to:
- Provide additional information not available in the code as text
- Reference relevant code segments from different locations
- Let AI perform analysis with the same contextual information and tooling that human auditors have
Evaluation Methodology
We evaluated Wake Arena performance through two distinct approaches:
- Audit competitions — comparing against industry-standard datasets
- Production audits — integration into production security audits
1. Audit competition performance
We accepted the Zellic benchmark dataset as an industry standard: 14 protocols from historical audit competitions (Code4rena and Sherlock) with publicly available codebases and verified findings reviewed by multiple security researchers.
Reproducibility: All benchmark codebases and competition findings are public. Wake Arena reports with code references and exploit scenarios are linked in the table below.
Benchmark Protocols
The table displays the number of high-severity issues identified¹.
| Protocol | Wake Arena 3.1 | Wake Arena 3.0 | Zellic V12 | Plain GPT-5 | Plain Opus 4.5 |
|---|---|---|---|---|---|
| Basin | 2/2 | 2/2 | 2/2 | 2/2 | 2/2 |
| Blackhole | 2/2 | 2/2 | 2/2 | 1/2 | 0/2 |
| Burve | 4/9 | 2/9 | 2/9 | 2/9 | 0/9 |
| Crestal | 1/1 | 1/1 | 1/1 | 1/1 | 1/1 |
| DODO | 4/5 | 2/5 | 2/5 | 1/5 | 4/5 |
| Lambo.win | 2/4 | 2/4 | 2/4 | 2/4 | 1/4 |
| Lend | 20/28 | 13/28 | 10/28 | 4/28 | 6/28 |
| Mellow | 2/6 | 2/6 | 2/6 | 1/6 | 0/6 |
| Munchables | 5/5 | 4/5 | 4/5 | 2/5 | 3/5 |
| Notional Exponent | 4/11 | 2/11 | 2/11 | 0/11 | 0/11 |
| Phi | 6/7 | 4/7 | 6/7 | 3/7 | 3/7 |
| Superfluid | 1/2 | 1/2 | 1/2 | 1/2 | 0/2 |
| TraitForge | 4/6 | 2/6 | 1/6 | 2/6 | 0/6 |
| Virtuals | 6/6 | 4/6 | 4/6 | 2/6 | 1/6 |
| Total | 63/94 | 43/94 | 41/94 | 24/94 | 21/94 |
Notes:
¹ We used the same historical audit competition dataset as Zellic. Plain GPT-5 was launched through the Code CLI tool, and plain Opus 4.5 was launched through the Claude Code CLI. We conducted the test with the prompt “perform extensive deep Solidity smart contract security analysis” from the repository root, no special guidance. Wake Arena scans ran with a standard configuration. Testing was conducted in November 2025 for v3.0 and in March 2026 for v3.1. 49 out of the 63 discovered issues were found by more than one agent. We took issue labeling from Code4rena & Sherlock as-is, so some issues there may have been flagged incorrectly (with bad severity, for example), but we took their labeling for consistency. Also, we’re using models that may have already been trained on these projects.
Performance comparison
Wake Arena 3.1 detected 63 out of 94 high-severity vulnerabilities, achieving the highest detection rate:
- Wake Arena 3.1: 63/94 (67.0%)
- Wake Arena 3.0: 43/94 (45.7%)
- Zellic V12: 41/94 (43.6%)
- GPT-5.2 xhigh: 41/94 (43.6%)
- Plain GPT-5: 24/94 (25.5%)
- Plain Opus: 21/94 (22.3%)
2. Production audits
During November 2025, Wake Arena scans were integrated into Ackee Blockchain’s manual audit process for production protocols. Unlike isolated benchmark environments, production audits involve interconnected contracts, incomplete configurations, and the full severity spectrum from Critical to Informational.
Production audit results
| Client | Project | Delivery | Days | Found by AI / All Found | Critical | High | Medium | Low | Warning | Info |
|---|---|---|---|---|---|---|---|---|---|---|
| Lido | Stonks 2.0 | Dec 2, 2025 | 15 | 4 / 17 | 0 / 0 | 0 / 0 | 1 / 1 | 1 / 2 | 1 / 5 | 1 / 9 |
| Everstake | ETH2 Batch Deposit Contract | Nov 14, 2025 | 2 | 1 / 2 | 0 / 0 | 0 / 0 | 0 / 0 | 0 / 0 | 0 / 0 | 1 / 2 |
| Printr | Protocol | Oct 1, 2025 | 32 | 21 / 60 | 5 / 10 | 0 / 4 | 1 / 5 | 4 / 10 | 4 / 15 | 7 / 16 |
| Total | 49 | 26 / 79 | 5 / 10 | 0 / 4 | 2 / 6 | 5 / 12 | 5 / 20 | 9 / 27 |
Key insight: Wake Arena identified 26/79 or 33% of all findings. Moreover, it discovered 5 critical and 5 unique vulnerabilities beyond those found by human auditors in the Printr audit.
Notes:
¹ Check Appendix C of the report for details on the Wake Arena findings.
Key Strengths
1. Cross-chain vulnerabilities
Wake Arena excels at finding complex cross-chain security issues through graph-driven reasoning:
- Lend protocol: Cross-chain liquidation logic errors and multi-directional position handling
- DODO: Cross-chain parameter validation and refund authorization issues
2. Access control and authorization
Deep contextual understanding identifies subtle permission bugs:
- Crestal: Unauthenticated allowance drain
- Virtuals: Permissionless validator registration
- Mellow: Multisig threshold bypass via duplicate signers
3. Accounting and state management
Graph-driven analysis traces data dependencies through complex state updates:
- Lend: Repeated reward claims due to missing state decrements
- Notional: Incorrect netting logic causing accounting divergence
- Mellow: Double-counting of staked vs. LP tokens
4. Protocol-specific logic errors
Multi-agent reasoning catches context-dependent vulnerabilities:
- Munchables: Plot state management and dirty flag handling
- TraitForge: Generation counter limits and airdrop attribution
- Burve: Fee accrual checkpoint management across assets
Limitations
Wake Arena is a powerful tool for finding important vulnerabilities, but it has limitations:
What Wake Arena catches well:
- Access control and authorization bugs
- State management and accounting errors
- Cross-chain and cross-contract logic issues
- Reentrancy and callback vulnerabilities
- Parameter validation and input handling
What Wake Arena may miss:
- Novel cryptographic vulnerabilities requiring deep mathematical analysis
- Protocol design flaws requiring extensive economic modeling
- Extremely complex business logic spanning multiple contracts and off-chain systems
- Zero-day attack vectors with no similar historical patterns
Ideal use case:
Use Wake Arena before your premium audit to:
- Catch surface-level and mid-depth vulnerabilities early
- Arrive at manual audit with cleaner code
- Focus auditor time on deep protocol logic and design issues
- Reduce overall security costs through early detection
Wake Arena complements, rather than replaces, high-quality manual audits of critical systems.
How to Use Wake Arena
Wake Arena is available now at ackee.xyz/wake/arena
Once you get access, scan your protocol in 3 steps:
- Upload codebase: Connect GitHub repository or upload files
- AI-driven analysis: A multi-agent system analyzes with graph-driven reasoning
- Receive report: Comprehensive PDF with findings, severity ratings, and remediation guidance
Pricing: Entry-level audit pricing with reports delivered in hours instead of months.
Foundation plans: Admin panels for grant programs to scan multiple projects under one subscription.
Appendix
Wake Arena Performance in Historical Audit Competitions
All findings below were discovered independently by Wake Arena with no special prompting or human assistance. Each protocol scan ran through the full AI-driven audit pipeline: compilation, graph-driven analysis, multi-agent reasoning, and report generation.
Basin (Code4rena, July 2024)
Wake Arena finds all 2 out of 2 high-severity issues reported by human researchers.
[H-01] Missing owner or role gating on upgrade endpoints enables permissionless upgrades
Functions upgradeTo and upgradeToAndCall rely solely on _authorizeUpgrade for gating, which enforces only environmental checks (delegatecall context, Aquifer mapping, and UUPS proxiableUUID) but does not restrict caller by owner or any role. Any external caller can invoke upgrade endpoints and change the implementation to any candidate that satisfies environment checks, bypassing governance.
[H-02] decodeWellData checks decimal0 twice, leaving decimal1 at 0 and mis-scaling token1 when 0 should default to 18
Function decodeWellData uses the same sentinel check twice for decimal0 and never checks decimal1 before defaulting to 18. When decimal1 is encoded as 0 to signal “default to 18”, it remains 0. Downstream scaling in getScaledReserves then multiplies token 1 by 10 ** (18 - 0) = 10 ** 18, mis-scaling reserves and corrupting pricing, reserve solves, and rate calculations.
Blackhole (May 2025)
Wake Arena finds all 2 out of 2 high-severity issues reported by human researchers.
[H-01] setRouter inverts the zero-address check, only allowing router = address(0)
Function setRouter inverts the zero-address guard, requiring the new router to be the zero address (require(_router == address(0), "ZA")). This prevents the owner from configuring any valid router. If router is ever zero, calls such as IGenesisPool(_genesisPool).launch(router, MATURITY_TIME) will use an invalid address, causing failures.
[H-02] createGauge permits untrusted _algebraEternalFarming, granting arbitrary ERC20 approval and enabling theft of factory-held reward tokens
Function createGauge is externally callable without modifiers and accepts caller-supplied farmingParam.algebraEternalFarming. It forwards this to the internal createEternalFarming, which unconditionally grants an ERC20 approval of 1e10 to the user-supplied _algebraEternalFarming before making an external call to it. Because _algebraEternalFarming is not validated against a trusted registry, any actor can point it to an arbitrary contract they control, allowing them to pull tokens from the factory via transferFrom using the granted allowance.
Burve (Sherlock, April 2025)
Wake Arena finds 4 out of 9 high-severity issues reported by human researchers.
[H-02] E4626ViewAdjustor inverts real/nominal semantics vs IAdjustor, enabling mis-accounting and integration misuse
E4626ViewAdjustor implements toNominal and toReal with the opposite semantics to those documented in IAdjustor: it maps real amounts to ERC4626 shares and nominal amounts to ERC4626 assets, where the interface expects normalization around 18 decimals. Callers following IAdjustor semantics receive silently wrong values, causing incorrect pricing, fee assessment, or token transfers when the adjustor is used generically across tokens.
[H-05] Permissionless compounding at spot price via mint(0) enables MEV extraction of fee balances
mint and burn unconditionally call compoundV3Ranges, which uses the pool’s live slot0 price to convert accumulated fee balances into new Uniswap V3 liquidity. Because mint accepts mintNominalLiq == 0 after initialization, an attacker can trigger compounding for free, manipulate the spot price via flash or MEV, force the protocol to compound at the distorted price, and extract value from the resulting impermanent loss as the price reverts.
[H-06] Uninitialized return variable used as tax base in removeValueSingle results in zero fees
removeValueSingle computes realTax from the return variable removedBalance before it is assigned. Because return variables in Solidity are zero-initialized, realTax is always 0. Users withdraw the full gross amount without paying the configured tax, the protocol accrues no fee revenue on this path, and the minReceive slippage guard evaluates against the gross rather than the net-of-tax amount.
[H-07] ERC4626 donation inflation lets NoopVault accept deposits that mint 0 shares, causing depositor asset loss
NoopVault inherits OpenZeppelin’s ERC4626 unchanged, so totalAssets reflects direct token donations. An attacker can seed the vault with dust shares, then donate tokens to inflate totalAssets while totalSupply stays minimal. Subsequent victim deposits compute shares == 0 via floor rounding and still succeed, transferring assets in while minting nothing. The attacker later redeems their dust shares to withdraw a disproportionate fraction of the vault including all zero-share deposits.
Crestal (Sherlock, March 2025)
Wake Arena finds the only high-severity issue reported by human researchers.
[H-01] Unauthenticated allowance drain via public payWithERC20
Function payWithERC20 in contract Payment is public and accepts arbitrary fromAddress and toAddress. It invokes token.safeTransferFrom(fromAddress, toAddress, amount) without authenticating the caller, binding fromAddress to msg.sender, or validating any signed authorization. Any user can trigger spending of any allowance that fromAddress has granted to this contract and redirect funds to an arbitrary toAddress.
DODO Cross-Chain DEX (Sherlock, June 2025)
Wake Arena finds 4 out of 5 high-severity issues reported by human researchers.
[H-01] Unvalidated message parameters in GatewayCrossChain onCall allow draining arbitrary ZRC20 balances
GatewayCrossChain.onCall decodes MixSwapParams directly from the cross-chain message and passes them to _doMixSwap with no invariant binding params.fromToken to the received zrc20 or params.fromTokenAmount to the post-fee amount. An attacker can craft a message pointing params.fromToken to any ZRC20 the contract already holds, set a large deposit in an unrelated token to inflate the approval, and route the DODO swap to drain the third-party balance.
[H-03] Approval vs. spend mismatch in GatewayCrossChain _doMixSwap strands allowances and enables griefing
_doMixSwap in GatewayCrossChain grants approve(DODOApprove, amount) for params.fromToken but invokes mixSwap with params.fromTokenAmount. Since amount and params.fromTokenAmount are independent caller-supplied values, an attacker can set params.fromTokenAmount > amount to cause a revert via insufficient allowance, or set it below amount to strand excess allowances and tokens in the contract for future abuse.
[H-04] Unbound params.fromToken in withdrawToNativeChain enables arbitrary token draining via DODO mixSwap
withdrawToNativeChain in GatewayTransferNative approves params.fromToken for the full deposited amount before calling the DODO router, without enforcing that params.fromToken equals the zrc20 transferred in. An attacker deposits a large amount of an unrelated token to set a correspondingly large approval on any target ZRC20 the contract holds, then routes the DODO swap to drain it.
[H-05] Non-EVM refund theft via authorization bypass in claimRefund
claimRefund initializes receiver = msg.sender and only overwrites it when refundInfo.walletAddress.length == 20. For non-EVM recipients (Solana, Bitcoin), the wallet address is not 20 bytes, so receiver remains msg.sender and the check require(bots[msg.sender] || msg.sender == receiver) always passes for any caller. Any address can claim and redirect any stored non-EVM refund.
Lambo.win (Code4rena, December 2024)
Wake Arena finds 2 out of 4 high-severity issues reported by human researchers.
[H-01] ERC20-mode cashIn mints by msg.value, enabling unbacked minting and zero-credit deposits
Function cashIn mints virtual tokens based on msg.value even when the underlying asset is an ERC20 token. In the ERC20 branch, the function transfers amount ERC20 tokens from the caller but mints msg.value virtual tokens, creating two failure modes: users depositing ERC20 with msg.value == 0 receive 0 virtual tokens while their ERC20 is locked, and attackers can send ETH with amount == 0 to receive unbacked virtual tokens they can cashOut to drain others’ ERC20 deposits.
[H-02] Front-run DoS by pre-creating Uniswap pair (PAIR_EXISTS) due to predictable next clone address
Function createLaunchPad deploys a new quote token clone using Clones.clone (via CREATE), then immediately calls Uniswap V2 createPair. Because CREATE-based addresses are derived from the factory’s address and its nonce, an observer can predict the next clone address. An attacker can front-run and pre-create the pair for (virtualLiquidityToken, predictedQuoteToken), causing the victim’s createLaunchPad to revert with PAIR_EXISTS.
Lend (Sherlock, June 2025)
Wake Arena finds 20 out of 28 high-severity issues reported by human researchers.
[H-01] claimLend fails to decrement lendAccrued after grant, allowing repeated reward claims and LEND drain
Function claimLend transfers accrued LEND but never reduces the recorded accrual in storage. After a successful transfer, lendAccrued(account) remains unchanged, allowing the same accrual to be claimed repeatedly whenever the router holds enough LEND. Storage-side distribution functions only ever increase lendAccrued; no path decrements or clears it after grants.
[H-03] borrowWithInterest reverts for legitimate multi-direction positions (both arrays populated)
Function borrowWithInterest enforces that only one of crossChainBorrows or crossChainCollaterals is populated for a given user and underlying on a chain. This invariant does not hold for legitimate multi-direction positions involving the same underlying in opposite directions across different remote chains, causing denial-of-service in repay and accounting flows.
[H-04] Collateral seized before repayment; LiquidationSuccess uses foreign lToken, bricking cross-chain liquidation
Collateral is seized on the collateral chain before repayment is secured, and the follow-up LiquidationSuccess handler repays using a foreign lToken address. On the debt chain, liquidateCrossChain computes seize tokens and sends CrossChainLiquidationExecute without first escrowing or pulling repayment funds from the liquidator. On the collateral chain, _handleLiquidationExecute immediately updates accounting and reduces the borrower’s collateral before any guarantee that repayment will succeed, breaking the cross-chain liquidation flow and permanently desynchronizing borrower state across chains.
[H-05] Cross-chain liquidation computes seize amount on debt chain and applies it verbatim on collateral chain, enabling over- or under-seizure
_executeLiquidationCore calculates seizeTokens using the debt chain’s oracle prices, exchange rates, and liquidation parameters, then transmits this value to the collateral chain where _handleLiquidationExecute applies it without recomputation. If market parameters or oracle prices differ between chains, the collateral chain can seize too many tokens (harming the borrower) or too few (creating protocol bad debt).
[H-06] Cross-chain borrow capacity check forwards full sumCollateral instead of net liquidity, enabling multi-chain collateral double-counting
borrowCrossChain snapshots and sends only the borrower’s gross sumCollateral in the payload, discarding the borrow side. Each destination chain validates the new borrow independently against this full collateral value without subtracting existing debt on other chains. A borrower can initiate simultaneous cross-chain borrows to multiple destinations, each of which passes the check, reusing the same collateral across chains and creating undercollateralized positions.
[H-07] Redeem underpays by using pre-accrual exchangeRateStored; surplus underlying stranded in router
Function redeem computes expectedUnderlying using exchangeRateStored read before calling the market’s redeem. The redeem call accrues interest and uses post-accrual rate, so the router receives more underlying than it forwards to the user. The difference remains stuck in the router and accumulates over time.
[H-08] borrowWithInterest excludes destination-chain collaterals, zeroing cross-chain debt and blocking liquidation
Function borrowWithInterest fails to include cross-chain debt on destination chains because the collaterals branch erroneously requires both destEid == currentEid and srcEid == currentEid. On genuine cross-chain borrows originating on Chain A with debt on Chain B, destEid == currentEid but srcEid != currentEid, so the condition is always false. This incorrect zero balance corrupts downstream logic including liquidity calculations and liquidation limits.
[H-09] Supply credits lTokens using stale exchangeRateStored, causing accounting drift and redeem DoS
Supply uses exchangeRateStored from before mint and then credits mintTokens using the stale rate. The subsequent mint call accrues interest and uses post-accrual exchange rate, so actual lTokens minted to the router are fewer than the credited amount. This creates persistent accounting surplus that eventually causes redemption reverts.
[H-10] Non-atomic cross-chain liquidation seizes collateral before collecting repayment, enabling unpaid seizures
The liquidation protocol seizes collateral on the collateral chain first, then attempts to collect repayment from the liquidator on the debt chain. There is no escrow of the liquidator’s tokens prior to seizure, so if the liquidator cannot or will not pay, repayment fails while collateral has already been redistributed.
[H-16] Cross-chain repay corrupts same-chain borrow state and shared userBorrowedAssets, hiding or double-counting debt
CoreRouter.repayBorrowInternal always mutates the same-chain borrowBalance mapping and the shared userBorrowedAssets set regardless of whether the repayment is same-chain or cross-chain. Cross-chain repayment helpers similarly remove the lToken from userBorrowedAssets when the cross-chain portion reaches zero without checking whether same-chain debt remains. The result is either hidden debt (the asset disappears from risk enumeration while a balance remains) or double-counted debt (partial cross-chain repayments write a ghost entry into same-chain borrowBalance, which is then summed again alongside the actual cross-chain record in liquidity calculations).
[H-18] Cross-chain debt undercount and repay DoS on destination chain in borrowWithInterest
Cross-chain debt becomes invisible on the destination chain because the borrowWithInterest collaterals branch uses an unsatisfiable predicate requiring both destEid and srcEid to equal currentEid. On the destination chain, cross-chain collateral records have destEid == currentEid and srcEid set to the origin chain, so srcEid == currentEid is always false and the loop never adds any amount. This causes under-accounting of debt in helpers like getHypotheticalAccountLiquidityCollateral and repay path DoS where CoreRouter.repayBorrowInternal requires borrowedAmount > 0 but borrowWithInterest incorrectly returns 0.
[H-19] Liquidation repayment uses collateral seizeTokens instead of repayAmount
After seizing collateral, the LiquidationSuccess handler repays the borrower using payload.amount, but that field was set to seizeTokens (collateral lToken units) during _executeLiquidationCore. Repayment must be performed using the repayAmount in the borrowed asset’s underlying units, causing incorrect debt settlement.
[H-20] Stale unlocked collateral snapshot enables undercollateralized cross-chain borrowing via TOCTOU race
borrowCrossChain snapshots the borrower’s collateral at call time and includes it unmodified in the LayerZero payload without locking or reserving the collateral. During the asynchronous window before the destination chain executes, the borrower can freely redeem or borrow against the same collateral on the source chain; the source records no in-flight debt. The destination validates against the stale snapshot and approves the borrow regardless, yielding an undercollateralized position once the source-chain withdrawal completes.
[H-21] Stale source-chain collateral snapshot enables undercollateralized cross-chain borrowing
The destination-chain borrow handler trusts a collateral value snapshotted on the source chain at send time. There is no lock on source-chain collateral while the message is in flight. The borrower can withdraw source-chain collateral after initiating cross-chain borrow but before destination execution, allowing undercollateralized or uncollateralized borrowing.
[H-22] Liquidation validity check uses wrong units and wrong action model, enabling seizure of healthy accounts
The liquidation validator _checkLiquidationValid models an additional borrow in the collateral market and passes payload.amount as the borrowAmount. In this flow, payload.amount is the number of lTokens to seize, not underlying-denominated borrow amount. This mixes units and incorrectly inflates the “borrowed” side of the solvency check, enabling liquidation of healthy accounts.
[H-23] borrowWithInterest EID predicate excludes valid destination-chain records, making cross-chain debt invisible
Records in crossChainCollaterals are stored with srcEid pointing to the remote source chain, but borrowWithInterest requires collaterals[i].srcEid == currentEid on the destination-chain branch, so legitimate records are never summed. Cross-chain debt is invisible on the destination chain, breaking repay paths, liquidation validation, and solvency checks in getHypotheticalAccountLiquidityCollateral.
[H-24] Cross-chain repayment removes lToken from shared userBorrowedAssets without checking outstanding same-chain debt
When a cross-chain repayment fully settles its portion, the repayment handlers unconditionally remove the lToken from the shared userBorrowedAssets enumeration set without verifying whether same-chain debt for that lToken remains. Because getHypotheticalAccountLiquidityCollateral iterates only userBorrowedAssets, any remaining same-chain balance becomes invisible to all risk and liquidation checks.
[H-25] LiquidationSuccess uses foreign lToken and wrong EIDs; repayment never executes after collateral seized
After seizing collateral on the collateral chain, the router sends a LiquidationSuccess message back to the debt chain. The receiving handler _handleLiquidationSuccess attempts to look up the borrow position and repay the debt using the collateral-chain destlToken and mismatched endpoint IDs. This causes the handler to fail to locate the record or revert when interacting with an unknown lToken, leaving repayment unexecuted while collateral has already been seized.
[H-27] First-time borrowers bypass per-user collateral checks in borrow due to zero borrowIndex path
In function borrow, the contract computes borrowed and collateral including the new amount, but then derives borrowAmount as zero when currentBorrow.borrowIndex == 0 (first-time borrowers). The per-user collateral check collateral >= borrowAmount becomes collateral >= 0, always passing and enabling new users to borrow against other users’ collateral held by the router.
[H-28] _handleValidBorrowRequest overwrites borrowIndex without accruing prior principal, erasing cross-chain interest on each new borrow
When a cross-chain borrow confirmation arrives at the source chain, _handleValidBorrowRequest adds the new amount to the existing principal and overwrites the stored borrowIndex without first scaling the prior principal to the new index. Because debt is computed as principle * currentBorrowIndex / storedBorrowIndex, the interest accrued since the last borrow is effectively forgiven each time the user borrows again. Source-chain liquidity checks undercount the borrower’s true debt, allowing excess collateral withdrawals and additional borrows.
Mellow (Sherlock, July 2025)
Wake Arena finds 2 out of 6 high-severity issues reported by human researchers.
[H-01] Threshold bypass: duplicate signer entries counted as distinct in checkSignatures
The Consensus multisig validator counts provided signatures against threshold but does not enforce that each signature is produced by a unique signer. The same signer can be repeated in the signatures array to satisfy any threshold, degrading a k-of-N policy to effectively 1-of-N as long as one registered signer is willing or compromised.
[H-04] Protocol fee double-accrual across non-base-asset reports because timestamp is only updated for base asset
A protocol-fee accrual checkpoint is stored per vault in timestamps. Function calculateFee unconditionally adds time-based protocol fees proportional to block.timestamp - timestamps[vault] for any asset, while function updateState advances the checkpoint only when asset == baseAsset[vault] and returns early otherwise. This allows the same elapsed period to be charged multiple times across different non-base-asset reports until a base-asset report finally updates the timestamp, systematically over-minting protocol fee shares.
Munchables (Code4rena, July 2024)
Wake Arena finds 5 out of 5 high-severity issues reported by human researchers.
[H-01] Missing plotId update on transfer causes stuck occupancy and event inconsistency
Function transferToUnoccupiedPlot updates occupancy bitmaps for the old and new plots but never updates the staked token’s toilerState.plotId field. As a result, the contract retains a stale plot id in storage while the plotOccupied mapping reflects the new plot. When the renter later calls unstakeMunchable, the function frees the old plot id (which is already empty), leaving the new plot permanently marked as occupied and preventing further rentals on that plot.
[H-02] Off-by-one in invalid-plot check allows farming on removed plots
The invalid plot detection in _farmPlots uses _getNumPlots(landlord) < _toiler.plotId instead of the correct >= comparison, missing the equality case. When a landlord reduces the number of plots, a toiler whose plotId equals the new plot count should be marked invalid, but the current code fails to detect it. As a result, staked tokens on the highest removed index continue farming as if the plot still existed, causing accounting errors for both renter and landlord.
[H-03] Signed bonus arithmetic in _farmPlots can produce negative schnibbles and revert, trapping NFTs
_farmPlots computes finalBonus as the sum of a signed REALM_BONUSES entry and a RARITY_BONUSES entry cast through uint8 → int8 (values ≥ 128 become negative). The adjusted schnibbles total is then computed as uint256((int256(schnibblesTotal) + int256(schnibblesTotal) * finalBonus) / 100). Any finalBonus of −2 or below makes the numerator negative; casting a negative int256 to uint256 reverts in Solidity 0.8. Because _farmPlots runs via the forceFarmPlots modifier on every key user action, affected renters cannot farm, transfer to another plot, or unstake, leaving their NFTs permanently trapped until configuration is changed.
[H-04] Time-delta underflow in _farmPlots when using landlord lastUpdated on invalid plot causes revert and blocks user actions
When a staked token’s plotId exceeds the landlord’s available plots, _farmPlots substitutes timestamp = plotMetadata[landlord].lastUpdated and then computes the farming delta as timestamp - _toiler.lastToilDate. If plotMetadata[landlord].lastUpdated is zero (never initialized) or earlier than _toiler.lastToilDate, the subtraction underflows under Solidity 0.8 and reverts the entire transaction. Because _farmPlots is executed by the forceFarmPlots modifier on key functions (stakeMunchable, unstakeMunchable, transferToUnoccupiedPlot), this revert prevents users from farming, moving to a valid plot, or even unstaking—effectively locking assets until metadata is updated.
[H-05] Dirty flag in _farmPlots is never cleared, permanently disabling farming for affected tokens
When a staked token’s plotId is no longer valid (available plots shrank), _farmPlots sets toilerState[tokenId].dirty = true to denote a one-time adjustment. On subsequent calls, the check if (_toiler.dirty) continue; skips farming entirely for that token. There is no code path that clears the dirty flag after the adjustment, including in transferToUnoccupiedPlot, so the token will never accrue schnibbles again unless the owner fully unstakes and restakes it.
Notional Exponent (Sherlock, July 2025)
Wake Arena finds 4 out of 11 high-severity issues reported by human researchers.
[H-03] Overlapping Dinero withdraw requests redeem aggregated upxETH, enabling theft and DoS of other withdrawals
DineroWithdrawRequestManager mints all upxETH redemptions to address(this). When finalizing a request, _finalizeWithdrawImpl redeems the entire upxETH.balanceOf(address(this), i) for every batch id in the request’s [initialBatchId, finalBatchId] range and attributes the full redeemed WETH to that single requestId. Because requests are explicitly allowed to overlap on batch ids, whichever request is finalized first captures all upxETH across shared batches, including amounts that belong to other requests. Later finalizations see a zero balance and settle with totalWithdraw = 0.
[H-04] Borrowers calling Morpho directly bypass the account-aware oracle, enabling undercollateralized borrowing
MorphoLendingRouter configures Morpho markets to use the vault itself as the oracle. The vault’s IOracle.price() calls convertToAssets, which in staking strategies returns a lower, withdraw-request-aware valuation when the transient t_CurrentAccount is set. The router sets this context only during its own authenticated flows. Because Morpho’s borrow is permissionless for self-managed positions, a borrower can call Morpho directly with t_CurrentAccount unset, receiving the higher default valuation and borrowing more than the router would permit. Router-driven liquidations subsequently use the lower per-account price, risking bad debt for lenders.
[H-06] Withdrawal initiation DoS after ~65k requests due to 16-bit nonce overflow in s_batchNonce
The withdraw request identifier packs a 16-bit s_batchNonce in the high bits and uses ++s_batchNonce during initiation. In Solidity 0.8+, arithmetic on uint16 is checked. Once the counter reaches 65535, the next increment reverts, permanently denying further withdrawal initiation through this manager.
[H-09] Curve V2 exits hardcode use_eth=true, causing ETH/WETH mismatch and stranded native ETH on WETH pools
CurveConvexLib exit paths unconditionally pass use_eth = true to Curve V2’s remove_liquidity and remove_liquidity_one_coin. For pools where the coin is ERC20 WETH rather than native ETH, this causes Curve to unwrap and return native ETH. The strategy measures exit proceeds via TokenUtils.tokenBalance(asset) which tracks ERC20 WETH balances, not native ETH, so the received ETH is invisible. assetsWithdrawn is undercounted (often zero) and native ETH accumulates stranded on the vault. Entry paths correctly toggle use_eth based on whether a coin is ETH_ADDRESS, making exit asymmetric with entry.
Phi (Code4rena, October 2024)
Wake Arena finds 6 out of 7 high-severity issues reported by human researchers.
[H-01] Art creation signature lacks domain separation, enabling cross-chain replay
The createArt authorization flow verifies a personal-signature over (uint256 expiresIn, string uri, bytes credData) without binding to block.chainid or address(this). A valid art-creation signature for Chain A can be replayed on Chain B if the same phiSignerAddress is configured, creating unintended duplicate art across chains and bypassing per-chain rollout policies.
[H-02] createArt signatures do not bind CreateConfig, allowing parameter hijack and revenue redirection
The factory signature only covers (expiresIn, uri, credData) and does not bind CreateConfig fields or the caller. Any party with a valid signed payload can front-run and submit createArt with arbitrary artist, receiver, mintFee, and timing parameters. The attacker gains persistent control via onlyArtCreator modifier and redirects revenue. The first creation also fixes the per-cred ERC1155 contract address permanently.
[H-03] Unbounded EnumerableMap keys in shareBalance enable gas-DoS of distribute, permanently locking curator rewards
_updateCuratorShareBalance calls EnumerableMap.set(sender_, 0) when a curator sells to zero instead of remove, so the address remains as an enumerable key forever. CuratorRewardsDistributor.distribute always calls getCuratorAddresses(credId, 0, 0), forcing a full scan over every historical key for that credId. An attacker can inflate the key count by briefly buying from many throwaway addresses and selling out; once the scan exceeds the block gas limit, distribute becomes permanently uncallable and the rewards locked in balanceOf[credId] are irrecoverable.
[H-05] Public position-bookkeeping mutators allow arbitrary state tampering and sell-to-zero DoS
Both _addCredIdPerAddress and _removeCredIdPerAddress are declared public and accept an arbitrary sender_ address with no access control. An attacker can remove a victim’s position entry out of band, corrupting the index mapping. When the victim later sells their full balance, the internal call to _removeCredIdPerAddress reverts with WrongCredId or IndexOutofBounds because the mapping was already tampered with, permanently blocking the sell-to-zero path for that position. Callers can also inject arbitrary entries to bloat position arrays and increase gas costs.
[H-06] Reentrancy via refund in _handleTrade buy path allows sell-lock bypass
The single-trade flow updates lastTradeTimestamp[credId_][curator_] only after refunding excessPayment via external call. A malicious contract can reenter during the refund and immediately call sellShareCred before the timestamp is updated. The lock check uses the stale timestamp, allowing immediate sell after buy and bypassing the cooldown period. The function lacks nonReentrant guard.
[H-07] Uncapped royaltyBPS allows royalties to exceed sale price, breaking ERC-2981 integrations and DoSing secondary sales
CreatorRoyaltiesControl._updateRoyalties accepts any royaltyBPS value without an upper bound. When royaltyBPS > 10_000, royaltyInfo returns a royaltyAmount greater than salePrice. Marketplaces that compute seller proceeds as salePrice − royaltyAmount will revert under Solidity 0.8 checked arithmetic, making affected token listings permanently unfillable. The misconfiguration is reachable by any art creator via updateRoyalties or PhiFactory.updateArtSettings.
Superfluid (Sherlock, June 2025)
Wake Arena finds 1 out of 2 high-severity issues reported by human researchers.
[H-01] provideLiquidity can spend staked tokens (no available-balance check), causing double counting and accounting breakage
Function provideLiquidity does not enforce that supAmount is bounded by the locker’s available (non-staked) balance. Staking only updates internal _stakedBalance while tokens remain in this contract’s address, so Uniswap’s position manager can pull staked tokens when minting an LP position. This violates the invariant FLUID.balanceOf(this) >= _stakedBalance, enables double counting (same tokens accrue staking and LP rewards), and can later break accounting and unlock flows.
TraitForge (Code4rena, July 2024)
Wake Arena finds 4 out of 6 high-severity issues reported by human researchers.
[H-01] Batch mint loop uses global _tokenIds, blocking mintWithBudget after generation-1 cap
The mintWithBudget while-condition compares global token counter _tokenIds against per-generation limit maxTokensPerGen. Because _tokenIds is monotonically increasing across all generations, after the first generation mints maxTokensPerGen tokens, the condition _tokenIds < maxTokensPerGen becomes false forever, preventing any further batch mints in subsequent generations.
[H-02] Burn before airdrop start lets current holder reduce the initial minter’s airdrop allocation
In burn(uint256 tokenId), while the airdrop has not started, the contract subtracts entropy from initialOwners[tokenId]. initialOwners is set at mint time and never updated on transfer. Any current holder or approved operator can burn the token before airdrop starts and force a deduction from the original minter’s airdrop allocation.
[H-04] Pre-forging into future generations bypasses per-generation cap via count reset on rollover
forge allows minting into newGeneration = getTokenGeneration(parent1Id) + 1 without requiring that newGeneration == currentGeneration. Tokens can be created in a future generation before public minting advances currentGeneration, correctly incrementing generationMintCounts[newGeneration]. However, when the contract later rolls over, _incrementGeneration unconditionally sets generationMintCounts[currentGeneration] = 0 for the newly entered generation, erasing any count pre-accumulated via forging. This re-opens the full maxTokensPerGen capacity, enabling supply beyond the intended cap and underpricing subsequent public mints.
[H-05] Public minting ignores maxGeneration, allowing unlimited generations
maxGeneration is enforced in forge via require(newGeneration <= maxGeneration) but is absent from the public mint rollover path. When a generation fills during public minting, _mintInternal calls _incrementGeneration, which unconditionally increments currentGeneration with no bound check. Once the last intended generation is filled, public mints continue into generation 11, 12, and beyond, breaking the intended total supply cap.
Virtuals (Code4rena, April 2025)
Wake Arena finds 6 out of 6 high-severity issues reported by human researchers.
[H-01] Permissionless validator registration enables sybil set inflation, base score manipulation, and gas-based DoS
Function addValidator is publicly callable and lacks access control, allowing any account to register an arbitrary validator for any virtualId. On call, addValidator unconditionally invokes _addValidator and _initValidatorScore, which appends to the per-virtual validator array and assigns a non-zero base score tied to the DAO’s proposal count. This enables sybil inflation of the validator set, upward manipulation of aggregated validator scoring via the non-zero base score, and unbounded growth of the _validators[virtualId] array, increasing gas costs in consumer loops like totalUptimeScore with realistic out-of-gas reverts in downstream flows.
[H-02] Third-party stake call overwrites victim’s entire vote delegation
AgentVeToken.stake accepts arbitrary receiver and delegatee arguments and unconditionally calls _delegate(receiver, delegatee) without requiring the caller to be the receiver. Because _delegate updates the delegation for the receiver’s entire voting power — not just the newly minted amount — any address can dust-stake 1 wei to a victim and silently redirect all of the victim’s votes to an attacker-controlled delegatee. The attack is repeatable for as long as staking is enabled, making re-delegation an unreliable defense.
[H-03] Unbound virtualId in updateImpact enables cross-persona impact and dataset score manipulation
Function updateImpact accepts caller-controlled virtualId and proposalId and computes the baseline service using _coreServices[virtualId][_cores[proposalId]], with no binding between the two parameters. The contract does not persist any mapping from proposalId to its originating persona, so any caller can pair a victim proposalId with an unrelated virtualId to compare maturity against the wrong persona’s last service or against zero. This directly changes _impacts[proposalId] and, when the proposal has an associated dataset, also rewrites _impacts[datasetId] and _maturities[datasetId], corrupting cross-persona scoring.
[H-04] Unchecked parentId enables cross-DAO lineage forgery and unbounded children growth
The mint function accepts an arbitrary parentId and only enforces parentId != proposalId, then writes the linkage and appends to the parent’s children without validating that the parent exists, belongs to the same virtual persona, or that the caller is authorized to modify the parent’s lineage. This allows any proposer to forge cross-DAO ancestry, attach children to nonexistent parents, and bloat _children[parentId] for griefing. An attacker can make a token from DAO A appear as a child of a token minted by DAO B, or spam a popular parent with thousands of children, breaking assumptions of downstream consumers and making getChildren(parentId) heavy for indexers.
[H-05] Base-score initialization inflates validatorScore beyond totalProposals, causing reward distribution underflow DoS
_initValidatorScore seeds a newly registered validator’s base score to totalProposals at the moment of registration. If the validator address already has non-zero scoreOf (from prior governance votes), the effective validatorScore = totalProposals + preExistingVotes > totalProposals. In _distributeValidatorRewards, the participation slice is computed as validatorRewards * validatorScore / totalProposals; when the ratio exceeds 1, the subsequent validatorPoolRewards -= participationReward underflows and reverts in Solidity 0.8, permanently bricking reward distribution for that persona. Because addValidator is permissionless and validators cannot be removed, registering a single pre-voted address is enough to permanently DoS all future distributions.
[H-06] promptMulti can transfer to zero address or stale TBA because prevAgentId is never updated
The promptMulti loop initializes prevAgentId to 0 and agentTba to address(0) but never updates prevAgentId inside the loop. The refresh condition if (prevAgentId != agentId) therefore fails when agentId == 0, leaving agentTba unchanged. For the first element where agentId == 0, agentTba remains address(0) and token.safeTransferFrom(sender, agentTba, amounts[i]) attempts a transfer to the zero address, causing standard ERC20 tokens to revert. For any later element where agentId == 0 follows a non-zero agentId, the stale agentTba from the previous agent is reused, misdirecting funds to the wrong recipient.
Acknowledgments
We thank Lido, Printr, Everstake, and LUKSO for participating in the production validation of Wake Arena.
Access
Wake Arena: ackee.xyz/wake/arena
Ackee Blockchain Security