Going Attackable: End-to-End Protocol Tutorial

A complete tutorial for protocols to deploy, configure, and enter attack mode on BattleChain

This tutorial covers the full journey of taking your protocol from deployment to attack mode on BattleChain. You'll learn not just the mechanics, but why each step matters for your security.

ℹ️

Audience: Protocol teams and developers preparing contracts for BattleChain.

Prerequisites: An audited smart contract and test funds for liquidity.

Why Go Attackable?

Traditional security audits review code statically. BattleChain adds a dynamic layer: real attackers with real economic incentives trying to break your contracts. This means:

  • Exploits get found before mainnet — whitehats are motivated by bounties, not just reports
  • You control the risk — set your own bounty terms and liquidity levels
  • Legal protection works both ways — Safe Harbor protects whitehats, and you set the rules

Part 1: Deploy Your Contracts

The recommended approach is to use battlechain-lib, which handles BattleChainDeployer routing and AttackRegistry registration automatically:

forge install cyfrin/battlechain-lib

Deploy using the bcDeployCreate* helpers — they route through BattleChainDeployer on BattleChain (auto-registering with AttackRegistry) and through CreateX on other chains:

import { BCScript } from "battlechain-lib/BCScript.sol";
import { Contact } from "battlechain-lib/types/AgreementTypes.sol";

contract Deploy is BCScript {
    function _protocolName() internal pure override returns (string memory) { return "My Protocol"; }
    function _contacts() internal pure override returns (Contact[] memory) {
        Contact[] memory c = new Contact[](1);
        c[0] = Contact({ name: "Security Team", contact: "security@myprotocol.com" });
        return c;
    }
    function _recoveryAddress() internal view override returns (address) { return msg.sender; }

    function run() external {
        vm.startBroadcast();

        bytes32 salt = keccak256("my-vault-v1");
        address myVault = bcDeployCreate2(salt, type(MyVault).creationCode);

        // ... continue with agreement creation and attack mode
        vm.stopBroadcast();
    }
}
💡

Deploy the exact same bytecode you'll use on mainnet. Only change constructor parameters like oracle addresses or chain-specific config. Testing different code defeats the purpose.

⚠️

Required: --skip-simulation flag

Forge's local gas estimation doesn't work reliably on BattleChain. All forge script calls must include --skip-simulation or they may fail. Add it to every deployment and interaction script:

forge script ... --skip-simulation

See the deploying contracts tutorial for details on all deployment methods, including direct BattleChainDeployer usage.

Part 2: Create Your Safe Harbor Agreement

The agreement defines the rules of engagement for whitehats. With battlechain-lib, you can create an agreement with sensible defaults in one call:

// Inside your BCScript's run() function:
address agreement = createAndAdoptAgreement(
    defaultAgreementDetails(
        _protocolName(), _contacts(), getDeployedContracts(), _recoveryAddress()
    ),
    msg.sender,
    keccak256("agreement-v1")
);

defaultAgreementDetails auto-detects the chain and builds the correct scope:

  • BattleChain: Uses BattleChain CAIP-2 chain ID and BATTLECHAIN_SAFE_HARBOR_URI
  • Other chains: Uses the current chain's CAIP-2 ID and the generic SAFE_HARBOR_V3_URI

createAndAdoptAgreement handles three steps in one: creates the agreement, sets a 14-day commitment window, and adopts it.

Customizing Terms

For full control over bounty terms, build the agreement manually:

BountyTerms memory bountyTerms = BountyTerms({
    bountyPercentage: 10,
    bountyCapUsd: 5_000_000,
    retainable: true,
    identity: IdentityRequirements.Anonymous,
    diligenceRequirements: "",
    aggregateBountyCapUsd: 0
});

AgreementDetails memory details = defaultAgreementDetails(
    _protocolName(), _contacts(), getDeployedContracts(), _recoveryAddress()
);
details.bountyTerms = bountyTerms;

address agreement = createAgreement(details, msg.sender, keccak256("agreement-v1"));
setCommitmentWindow(agreement, 30); // 30 days
adoptAgreement(agreement);
DecisionRecommendedWhy
Bounty %10%Industry standard, attracts serious researchers
Cap$1M - $5MHigh enough to motivate deep analysis
RetainabletrueSimpler for whitehats — they keep bounty from recovered funds
IdentityAnonymousMaximizes participation

For the full configuration reference, see How to Create a Safe Harbor Agreement.

Part 3: Request Attack Mode

Request attack mode using the battlechain-lib helper. This is the only BattleChain-specific step — it reverts on other chains:

if (_isBattleChain()) {
    requestAttackMode(agreement);
}

Your contracts are now in ATTACK_REQUESTED state. The DAO will review and check:

  • Is this a new contract (not a mainnet copy)?
  • Are the bounty terms reasonable?
  • Is the scope clearly defined?

Once approved, state moves to UNDER_ATTACK and whitehats can begin testing.

ℹ️

On testnet, approval is instant via the MockRegistryModerator — a permissionless contract you can call yourself rather than waiting for a real DAO action. See How to Request Attack Mode for the command.

See How to Request Attack Mode for full details and troubleshooting.

Part 4: During the Attack Period

Once the DAO approves, your contracts enter UNDER_ATTACK state. Here's what to expect:

Monitor Activity

Watch for unusual transactions on your contracts. Whitehats may:

  • Drain liquidity pools
  • Exploit reentrancy or flash loan vectors
  • Test access control boundaries

If a Vulnerability is Found

  1. Whitehats extract funds and send the remainder to your recovery address
  2. You keep your assets minus the bounty
  3. Consider whether the vulnerability affects your mainnet plans

When You're Confident

After sufficient testing (see best practices for timing), promote to production:

attackRegistry.promote(agreement);
// 3-day countdown begins — contracts are still attackable
// After 3 days, contracts enter PRODUCTION

See How to Promote to Production for the full promotion flow.

Summary

StepActionResult
1Deploy via bcDeployCreate*Contracts registered (auto via BattleChainDeployer)
2Create Safe Harbor agreementTerms defined (auto-scoped per chain)
3Request attack modeDAO reviews (BattleChain only)
4DAO approvesWhitehats can attack
5Promote to productionBattle-tested and ready for mainnet

Troubleshooting

Stuck or pending transactions

Transactions can get stuck if submitted without --legacy or with insufficient gas. To replace a stuck transaction, send a no-op at the same nonce with a higher gas price:

cast send \
  --account <your-keystore-account> \
  --nonce <stuck-nonce> \
  --gas-price <higher-price-in-wei> \
  --legacy \
  --value 0 \
  0x0000000000000000000000000000000000000000

Find the stuck nonce via:

cast nonce --rpc-url https://testnet.battlechain.com:3051 <your-wallet-address>

Transaction type not supported

If you see transaction type not supported or similar errors, you're missing --legacy. BattleChain Testnet only accepts legacy (type 0) transactions — EIP-1559 is not supported.