Attack a Contract

Find the vulnerability in a BattleChain vault, exploit it, collect your bounty, and walk away clean under Safe Harbor.

⚔️
You are the Whitehat
There's a vulnerable vault on BattleChain. The DAO approved it. Safe Harbor is in place.
Your job is to find the bug, drain the vault, keep your bounty, and return the rest. Everything you're about to do is legal, structured, and profitable.

Choose your path

Prerequisites

You need a deployed, attackable vault to target. If you completed the Quickstart, you already have one — your .env has everything you need.

If you're attacking someone else's vault, you need their VAULT_ADDRESS and TOKEN_ADDRESS (find these on the BattleChain explorer or by querying the AttackRegistry).

Your AI coding tool with terminal access (Claude Code, Cursor, Windsurf, etc.). If you completed the quickstart, continue in the same session.


Step 1 — Verify the Vault is Attackable

Before doing anything, confirm the vault is in UNDER_ATTACK state.

Run `just check-state` and tell me the result. I need it to be 3 (UNDER_ATTACK).

Step 2 — Understand the Exploit

Open src/Attacker.sol. The attack exploits two things working together: the CEI violation in VulnerableVault and the hook system in MockToken.

MockToken's hook system: Any address can register a hook contract via setTransferHook(address hook). When tokens are transferred to that address, the token calls hook.onTokenTransfer() after the transfer completes.

The vault's CEI violation: withdrawAll() calls TOKEN.transfer() before zeroing balances[msg.sender]:

function withdrawAll() external {
    uint256 amount = balances[msg.sender];
    require(amount > 0, "nothing to withdraw");

    TOKEN.transfer(msg.sender, amount); // external call first
    balances[msg.sender] = 0;           // balance zeroed too late
}

Put them together and the reentrancy chain looks like this:

attack()
  TOKEN.setTransferHook(address(this))    register as our own hook
  vault.deposit(seedAmount)               establish a balance
  vault.withdrawAll()
    TOKEN.transfer(attacker, amount)
      onTokenTransfer()                   hook fires, balance not yet zeroed
        vault.withdrawAll()
          TOKEN.transfer(attacker, amount)
            onTokenTransfer()             still not zeroed
              ...                         repeats until vault is empty

Once the vault is drained, the Safe Harbor settlement runs automatically:

uint256 total = TOKEN.balanceOf(address(this));
uint256 bounty = (total * BOUNTY_BPS) / 10_000; // 10%

TOKEN.transfer(RECOVERY_ADDRESS, total - bounty); // return 90% to protocol
TOKEN.transfer(OWNER, bounty);                    // keep 10%

You're not stealing. The protocol gets the majority of funds back minus the agreed bounty. Everyone knew the rules when the agreement was signed.


Step 3 — Execute the Attack

Run `just attack` to execute the reentrancy exploit against the vault.
Use --skip-simulation for all forge script calls.

Enter your keystore password when prompted.

You've executed a legal reentrancy exploit on BattleChain. The vault is empty, the protocol has their funds back minus your bounty, and you're protected under Safe Harbor.


Step 4 — Verify on the Explorer

Search your VAULT_ADDRESS on the BattleChain explorer. You should see:

  • The attack transaction
  • The vault balance dropping to zero
  • The bounty transfer to your wallet
  • The remaining funds sent to the recovery address

Check the Math

The agreement set a 10% bounty (BOUNTY_BPS = 1_000). The vault had 1,000 seeded tokens plus your 100 seed tokens = 1,100 total.

  • Bounty (yours): 1,100 x 10% = 110 tokens
  • Returned to protocol: 1,100 - 110 = 990 tokens

If the numbers don't look right, check that RECOVERY_ADDRESS in your .env matches the address in the agreement.


What Just Happened

You played the whitehat side of a BattleChain engagement:

  1. Verified the vault was in attack mode with a valid Safe Harbor agreement
  2. Found the reentrancy vulnerability (CEI violation + token hooks)
  3. Exploited it to drain the vault
  4. Settled automatically — kept your 10% bounty, returned 90% to the protocol

The protocol now knows their vault has a reentrancy bug. If the same code were deployed on mainnet with real TVL, the loss would have been real. That's the whole point.


Beyond This Tutorial

Handling Funds in Real Attacks

The starter repo settles funds automatically. In real attacks, you're responsible for correct settlement:

If retainable = true (you keep bounty from recovered funds):

uint256 recovered = token.balanceOf(address(this));
uint256 bounty = (recovered * bountyPercent) / 100;
// Respect the cap if set
token.transfer(recoveryAddress, recovered - bounty);
// Keep your bounty

If retainable = false (protocol pays bounty separately):

// Send everything to recovery
token.transfer(recoveryAddress, token.balanceOf(address(this)));

Mishandling funds can void your Safe Harbor protection. See Bounty Terms for details.

Finding Real Targets

The starter repo gives you a known vulnerability. In practice, you find targets by querying the AttackRegistry:

bool attackable = attackRegistry.isTopLevelContractUnderAttack(targetContract);

Or monitor for new targets via events:

event AgreementStateChanged(address indexed agreementAddress, ContractState newState);
// newState = 3 (UNDER_ATTACK) -> newly attackable

See How to Find Attackable Contracts for advanced techniques.

Mainnet Implications

⚠️

If the vulnerability you found also exists on mainnet:

  • Do NOT publicly disclose the vulnerability
  • Contact the protocol through their security contacts
  • Consider traditional bug bounty for the mainnet instance
  • Using a BattleChain exploit on mainnet is NOT protected by Safe Harbor

Next Steps