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);
| Decision | Recommended | Why |
|---|---|---|
| Bounty % | 10% | Industry standard, attracts serious researchers |
| Cap | $1M - $5M | High enough to motivate deep analysis |
| Retainable | true | Simpler for whitehats — they keep bounty from recovered funds |
| Identity | Anonymous | Maximizes 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
- Whitehats extract funds and send the remainder to your recovery address
- You keep your assets minus the bounty
- 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
| Step | Action | Result |
|---|---|---|
| 1 | Deploy via bcDeployCreate* | Contracts registered (auto via BattleChainDeployer) |
| 2 | Create Safe Harbor agreement | Terms defined (auto-scoped per chain) |
| 3 | Request attack mode | DAO reviews (BattleChain only) |
| 4 | DAO approves | Whitehats can attack |
| 5 | Promote to production | Battle-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.