Reproduced Exploit
Tanssi: permissionless `sendCurrentOperatorsKeys()` spams the bridge for free
Chain
Other
Category
dos
Date
Jan 1970
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: Tanssi-security-review_2025-04-30. The historical source/toolchain is unavailable; this entry is documentation only and claims no executable Forge PoC.
Vulnerability classes: vuln/access-control · vuln/dos/griefing
Reproduction: a faithful minimal reproduction of the vulnerable finding — the permissionless
sendCurrentOperatorsKeys()(and the zero-cost outbound ticket) are reproduced verbatim (marked@>) with faithful minimal doubles; local deploy, no fork.
Root cause#
Middleware.sendCurrentOperatorsKeys() is declared external with no access-control modifier, so any external actor can call it. It forwards to Gateway.sendOperatorsData() (whose onlyMiddleware guard is satisfied because the Middleware itself is the caller), which submits an outbound bridge message and increments the channel's outbound nonce — and because the ticket is built with ticket.costs = Costs(0, 0), every such message is fee-less. The vulnerable function, reproduced verbatim:
// Middleware.sol
@> function sendCurrentOperatorsKeys() external returns (bytes32[] memory sortedKeys) {
address gateway = getGateway();
if (gateway == address(0)) {
revert Middleware__GatewayNotSet();
}
uint48 epoch = getCurrentEpoch();
sortedKeys = IOBaseMiddlewareReader(address(this)).sortOperatorsByPower(epoch);
IOGateway(gateway).sendOperatorsData(sortedKeys, epoch);
}
The ticket that the forwarded call encodes is fee-less by construction (also verbatim from the finding), which is what makes the permissionless spam free:
// TODO For now mock it to 0
@> ticket.costs = Costs(0, 0);
Why it's exploitable here#
- The attacker is an ordinary account holding no role and is not the Middleware.
- It calls
sendCurrentOperatorsKeys()directly. TheonlyMiddlewareguard onsendOperatorsData()passes because the Middleware itself makes the internal call — the missing guard is on the entry point, not the downstream call. - Each call builds a zero-cost ticket (
Costs(0, 0)) and submits it to the primary governance channel, incrementingoutboundNonceby 1 — nomsg.valuerequired. - Looping 100 times forces 100 fee-less outbound messages, inflating the channel nonce by 100 and flooding the bridge relayers with work they must process — griefing / resource exhaustion / DoS of the bridge infrastructure.
Attack path#
Marked-line walkthrough (Playground)#
The EVM Playground pins each step to the exact executed source line in 0xce01759b…:
- L182 — Permissionless sendCurrentOperatorsKeys entry: Root cause: sendCurrentOperatorsKeys() is external with no role or owner check, so any actor can trigger a fee-less outbound bridge message.
- L202 — Marker token reference held: Setup: the exploit keeps a handle to the GRIEF marker token, which mints one unit per outbound message the attacker forces.
- L207 — Spam count fixed at 100: Setup: SPAM_COUNT pins the attacker to 100 free unauthorized outbound messages, proving the griefing works at scale.
- L210 — Nonce-inflation counter declared: Setup: nonceInflation will record how far the attacker pushes the channel's outbound nonce, the finite bridge resource.
- L213 — Exploit constructor wires contracts: Setup: the unprivileged exploit contract's constructor begins wiring the marker, gateway, and vulnerable middleware together.
- L214 — Marker token deployed: Setup: deploys the MiniToken marker so each forced outbound message can mint one griefing unit to the sink address.
PoC#
Registry (Foundry, local deploy — verbatim vulnerable source + harm-asserting test):
cd 63290-h-01-permissionless-sendcurrentoperatorskeys-pashov-audit-gr_exp && forge test -vvv
The browser Playground replays the same synthetic opcode-for-opcode and measures the harm: an unprivileged caller forces 100 fee-less outbound bridge messages, inflating the channel's outbound nonce by 100 and minting 100 griefing units to the sink. Both gates are green (registry forge test PASS + Playground _verify-poc VERDICT: PASS).
Sources & further analysis#
Reproductions & code
- No executable Forge reproduction is claimed; the historical source/toolchain was unavailable for this finding.
- AuditVault finding: Tanssi-security-review_2025-04-30.
- Upstream DeFiHackLabs PoC directory: src/test.
Alerts & third-party analyses
- DeFiHackLabs incident explorer: search "Tanssi: permissionless
sendCurrentOperatorsKeys()spams the bridge for free". - Web3Sec X hacked database: search.
- Rekt leaderboard: search.
- Solodit incident search: search.
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.