Attack a Contract
Find the vulnerability in a BattleChain vault, exploit it, collect your bounty, and walk away clean under Safe Harbor.
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:
- Verified the vault was in attack mode with a valid Safe Harbor agreement
- Found the reentrancy vulnerability (CEI violation + token hooks)
- Exploited it to drain the vault
- 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