Reproduced Exploit
Ajna ExtraordinaryFunding — caller-supplied voter accounts drain the treasury
voteExtraordinary(account_, proposalId_) credits the nominated account's voting power rather than msg.sender. A contract can loop over every holder, reach the threshold, and execute a treasury transfer.
Chain
Other
Category
governance
Date
Apr 2023
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: 21301-extraordinary-proposal-can-be-used-to-steal-extraordinary-am. Standalone Foundry PoC and full write-up: 21301-extraordinary-proposal-steal-ajna_exp in the
evm-hack-registrymirror.
Vulnerability classes: vuln/governance/proposal-manipulation · vuln/access-control/missing-owner-check
Reproduction: local synthetic Foundry reduction; the complete passing trace is in output.txt.
Key info#
| Field | Value |
|---|---|
| Loss | One caller fabricates 1,000 votes and transfers the entire modeled AJNA treasury. |
| Vulnerable contract | ExtraordinaryFunding.voteExtraordinary in test/21301-extraordinary-proposal-steal-ajna.sol |
| Attacker EOA | 0x1111111111111111111111111111111111111111 |
| Attack contract | Exploit |
| Attack tx | Local Foundry Exploit.run() |
| Chain · block · date | Ethereum model · block 0 · synthetic |
| Compiler | Solidity ^0.8.24 |
| Bug class | Extraordinary vote account not bound to caller |
TL;DR#
voteExtraordinary(account_, proposalId_) credits the nominated account's voting power rather than msg.sender. A contract can loop over every holder, reach the threshold, and execute a treasury transfer.
Background#
Extraordinary proposals are intentionally fast and one-way. That makes correct voter identity binding essential; delegated voting power must be explicit and non-forgeable.
The vulnerable code#
function voteExtraordinary(address account, uint256 proposalId) external returns (uint256 votesCast) {
require(!voted[proposalId][account], "already voted");
// @> VULN: caller can nominate any account instead of msg.sender.
voted[proposalId][account] = true;
votesCast = votingPower[account];
}
Root cause#
The API treats account as an authenticated voter identity without requiring a delegation signature or account == msg.sender.
Preconditions#
- Holders have voting power recorded in the grant fund.
- The attacker can create an extraordinary proposal.
voteExtraordinaryaccepts arbitrary account arguments.
Attack walkthrough#
- Fund the grant with 1,000 AJNA and assign power to three holders.
- The attacker calls
voteExtraordinarythree times naming those holders. - Threshold passes and
executeExtraordinarytransfers all AJNA; see output.txt:4.
Diagrams#
Remediation#
Use msg.sender as the voter identity, or require a valid delegation signature tied to account. Add invariants that total votes cannot exceed caller-authorized voting power.
How to reproduce#
cd evm-hack-registry/21301-extraordinary-proposal-steal-ajna_exp
forge test -vvvvv
Sources#
Reference: https://github.com/trailofbits/publications/blob/master/reviews/2023-04-ajnalabs-securityreview.pdf
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 21301-extraordinary-proposal-steal-ajna_exp (evm-hack-registry mirror).
- AuditVault finding: 21301-extraordinary-proposal-can-be-used-to-steal-extraordinary-am.
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.