Reproduced Exploit

transferPerpOwner can be front-run, then the new perp pulls the reserves

transferPerpOwner sets perp on the first call. Anyone can be that caller. Eve calls it with her own address. The admin's call then reverts OwnershipAlreadyClaimed. Eve is the only address allowed to pull the virtual-token reserves, and she pulls 1,000 UA to the attacker. A two-step transfer ignores…

Sep 2022•Ethereum•access-control•3 min read

Loss

1,000 UA pulled by the front-runner

Chain

Ethereum

Category

access-control

Date

Sep 2022

Source

AuditVault

EVM Playground

Source-level debugger — step opcodes and Solidity in sync

evm-hack-analyzer

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.

Loading fork state…

Source & credit. Reproduction of a public audit finding curated by AuditVault — the original finding: 17365-ownership-transfers-can-be-front-run-trailofbits-increment-f. Standalone Foundry PoC and full write-up: 17365-ownership-transfers-can-be-front-run-trailofbits-increment-f_exp in the evm-hack-registry mirror.


Vulnerability classes: vuln/access-control/missing-auth

Key info#

FieldValue
Loss1,000 UA pulled by the front-runner
Vulnerable contractPerpOwnable.transferPerpOwner
Attacker EOA0x1111111111111111111111111111111111111111
Attack contractExploit
Attack txExploit.run()
Chain / block / dateEthereum model · block 0 · 2022-09
Compilersolc 0.8.27, cancun, via IR
Bug classFirst caller of an unauthenticated transfer becomes the privileged owner

TL;DR#

transferPerpOwner sets perp on the first call. Anyone can be that caller. Eve calls it with her own address. The admin's call then reverts OwnershipAlreadyClaimed. Eve is the only address allowed to pull the virtual-token reserves, and she pulls 1,000 UA to the attacker. A two-step transfer ignores Eve: she cannot nominate and she cannot accept. The admin nominates the market, the market accepts, and the market pulls the same 1,000 UA to the treasury.

The vulnerable code#

SOLIDITY
function transferPerpOwner(address recipient) external virtual {
    if (recipient == address(0)) revert PerpOwnable_TransferZeroAddress();
    // @> VULN: the first caller sets perp. No role and no accept step.
    if (perp != address(0)) revert PerpOwnable_OwnershipAlreadyClaimed();
    perp = recipient;
    emit PerpOwnerTransferred(msg.sender, recipient);
}

pull then pays the reserves, and it requires msg.sender == perp.

Root cause#

The function is external and the only state check is that perp is still unset. The caller is not required to be the admin, the market, or a pre-committed nominee. The first transaction in the mempool wins the role, and every later claim reverts.

Preconditions#

  • perp is unset. That is the deployment state the review describes, before the admin points the virtual token at the perpetual market.
  • The contract holds 1,000 UA. pull sends that balance and is gated by perp.

Attack walkthrough#

  1. 1,000 UA is minted into PerpOwnable (output.txt). perp is address zero.
  2. Eve calls transferPerpOwner(eve). The unset check does not revert. perp becomes Eve. The transfer event records her.
  3. The admin calls transferPerpOwner(treasury). It reverts OwnershipAlreadyClaimed.
  4. Eve calls pull(attacker, 1000 ether). The msg.sender == perp check passes. The attacker balance is 1,000 UA and the contract balance is 0.
  5. Control: Eve's transferPerpOwner reverts because she is not the admin, and her acceptPerpOwner reverts because she was not nominated. Her pull reverts. The admin nominates the market. The market accepts and pulls the 1,000 UA to the treasury. The attacker's balance stays 0.

Diagrams#

flowchart TD Fund[PerpOwnable holds 1000 UA and perp is unset] --> Front[Eve calls transferPerpOwner with her own address] Front --> Set[The unset check lets her become perp] Set --> Admin[The admin claim reverts as already claimed] Admin --> Pull[Eve pulls the 1000 UA to the attacker] Pull --> Two[A two-step accept blocks Eve and the nominee pulls instead]

Impact#

The attacker receives 1,000 UA (1000000000000000000000 base units). The admin never becomes perp. On the two-step contract Eve cannot complete the transfer, and the 1,000 UA is pulled by the nominated market to the treasury instead.

Remediation#

Replace the one-shot assignment with a two-step transfer. Only the admin may nominate. Only the nominee may accept, and only that acceptance writes perp.

How to reproduce#

BASH
cd evm-hack-registry/17365-ownership-transfers-can-be-front-run-trailofbits-increment-f_exp
forge test -vv

Sources#


Sources & further analysis#

Reproductions & code

Alerts & third-party analyses

  • 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.