Reproduced Exploit
Early Armada redeemers take Carol's 300,000 USDC because her unclaimed ARM is dropped from the denominator
circulatingSupply subtracts the entire RevenueLock balance, including ARM that is already entitled to Carol and only waiting on release. The denominator is 1.2M instead of 3M. Alice and Bob redeem 600,000 ARM each and take 250,000 USDC each, emptying a 500,000 USDC treasury. Carol then releases 1.8…
Loss
Carol is paid 0 instead of 300,000 USDC. Alice and Bob each receive 250,000 USDC
Chain
Ethereum
Category
logic
Date
May 2026
Source
AuditVault
EVM Playground
Source-level debugger — step opcodes and Solidity in sync
The attack is replayed in an in-browser EVM preloaded with the exact dumped fork state. The execution tree shows every call; step by Solidity line or by opcode across all depths — source, Stack, Memory, Storage, Balances (native / ERC-20 / NFT), Transient storage and Return value stay in sync. Click a tree node, opcode, or source line to jump. No backend, no live RPC.
Source & credit. Reproduction of a public audit finding curated by AuditVault — the original finding: 65988-armadaredemptioncirculatingsupply-excludes-entitled-but-uncl. Standalone Foundry PoC and full write-up: 65988-armadaredemptioncirculatingsupply-excludes-entitled-but-uncl_exp in the
evm-hack-registrymirror.
Vulnerability classes: vuln/logic/wrong-condition · vuln/logic/reward-calculation
Key info#
| Field | Value |
|---|---|
| Loss | Carol is paid 0 instead of 300,000 USDC. Alice and Bob each receive 250,000 USDC |
| Vulnerable contract | ArmadaRedemption.circulatingSupply |
| Attacker EOA | 0x0000000000000000000000000000000000065988 (early redeemers) |
| Attack contract | Exploit |
| Attack tx | Exploit.run() |
| Chain / block / date | Ethereum model · block 0 · 2026-05 |
| Compiler | solc 0.8.27, cancun, via IR |
| Bug class | Redemption denominator excludes entitled-but-unclaimed ARM |
TL;DR#
circulatingSupply subtracts the entire RevenueLock balance, including ARM that is already entitled to Carol and only waiting on release. The denominator is 1.2M instead of 3M. Alice and Bob redeem 600,000 ARM each and take 250,000 USDC each, emptying a 500,000 USDC treasury. Carol then releases 1.8M ARM and her redeem reverts. Counting her entitled ARM in the denominator pays her 300,000 USDC and limits Alice and Bob to 100,000 each.
Background#
Cyfrin's May 2026 Armada crowdfund and governance review. After wind-down, holders redeem ARM against treasury assets. The spec says crowdfund participants and revenue-lock beneficiaries can still claim and then redeem. The on-chain denominator treats their still-unclaimed ARM as if it were not in circulation.
The vulnerable code#
function circulatingSupply() public returns (uint256 supply) {
// @> VULN: the full RevenueLock balance is subtracted, including ARM already entitled to a user who has not released
supply = arm.totalSupply() - arm.balanceOf(treasury) - arm.balanceOf(revenueLock) - arm.balanceOf(crowdfund)
- arm.balanceOf(address(this));
lastSupply = supply;
}
Carol's 1.8M ARM sits in RevenueLock because her milestone was met and she has not called release. Subtracting that balance shrinks the denominator from 3M eligible ARM to 1.2M.
Root cause#
Entitled ARM is already owed to a named user. Treating the lock contract's whole balance as non-circulating lets whoever redeems first consume the treasury against too small a base.
Preconditions#
- Wind-down has opened redemption. This run does not warp time. The delay has already elapsed.
- Treasury assets are 500,000 USDC.
- Alice and Bob each hold 600,000 ARM. Carol is entitled to 1.8M ARM that is still in the lock.
releaseandredeemare both callable. No admin key is required to redeem.
Attack walkthrough#
circulatingSupplyreturns 1.2M because Carol's 1.8M is subtracted.- Alice redeems 600,000 ARM and is paid 250,000 USDC. Her fair share was 100,000.
- Bob redeems 600,000 ARM and is paid the other 250,000 USDC.
- The treasury balance is 0.
RevenueLockreleases Carol's 1.8M ARM. Her redeem reverts with no assets available.- The control leaves her entitled ARM in the denominator. Alice and Bob get 100,000 USDC each and Carol gets 300,000.
Diagrams#
Impact#
Carol loses her entire 300,000 USDC treasury share, 60 percent of the 500,000 USDC pool. Alice and Bob each receive 150,000 USDC more than the pro-rata amount. The control pays the three shares as 100,000, 100,000, and 300,000 and leaves the redemption contract empty.
Remediation#
Do not subtract ARM that is already entitled to a beneficiary. Subtract only the unentitled residue of RevenueLock and ArmadaCrowdfund, or count a pending release and claim as circulating until the user redeems.
How to reproduce#
cd 65988-armadaredemptioncirculatingsupply-excludes-entitled-but-uncl_exp
forge test -vv
test_exploit_early_redeemers_take_carols_share asserts Alice and Bob receive 250,000 USDC each and Carol receives 0. test_control_entitled_arm_stays_in_the_denominator asserts the fair 100,000 / 100,000 / 300,000 split. Captured output is output.txt.
Sources#
- AuditVault finding: https://github.com/Auditware/AuditVault/blob/main/findings/65988-armadaredemptioncirculatingsupply-excludes-entitled-but-uncl.md
- Cyfrin report: https://github.com/solodit/solodit_content/blob/main/reports/Cyfrin/2026-05-13-cyfrin-armada-crowdfund-governance-v2.0.md
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 65988-armadaredemptioncirculatingsupply-excludes-entitled-but-uncl_exp (evm-hack-registry mirror).
- AuditVault finding: 65988-armadaredemptioncirculatingsupply-excludes-entitled-but-uncl.
Alerts & third-party analyses
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.