How to Create a Safe Harbor Agreement

Create and configure your Safe Harbor agreement with bounty terms and scope

Overview

A Safe Harbor agreement defines the terms under which whitehats can attack your contracts. This guide covers all configuration options.

Quick Start with battlechain-lib

The fastest way to create an agreement is with battlechain-lib, which handles chain detection, scope building, and URI selection automatically:

import { BCSafeHarbor } from "battlechain-lib/BCSafeHarbor.sol";
import { Contact, BountyTerms, IdentityRequirements } from "battlechain-lib/types/AgreementTypes.sol";

contract CreateAgreement is BCSafeHarbor {
    function run() external {
        vm.startBroadcast();

        Contact[] memory contacts = new Contact[](1);
        contacts[0] = Contact({ name: "Security Team", contact: "security@myprotocol.com" });

        address[] memory contracts_ = new address[](1);
        contracts_[0] = vm.envAddress("VAULT_ADDRESS");

        // Creates agreement with correct scope and URI for the current chain
        address agreement = createAndAdoptAgreement(
            defaultAgreementDetails("My Protocol", contacts, contracts_, msg.sender),
            msg.sender,
            keccak256("my-protocol-v1")
        );

        vm.stopBroadcast();
    }
}

defaultAgreementDetails auto-selects:

  • BattleChain: BattleChain CAIP-2 scope + BATTLECHAIN_SAFE_HARBOR_URI
  • Other chains: Current chain's CAIP-2 scope + SAFE_HARBOR_V3_URI (SEAL)

createAndAdoptAgreement handles create + 14-day commitment window + adopt in one call.

Manual Creation with AgreementFactory

For full control, use AgreementFactory directly:

AgreementDetails memory details = AgreementDetails({
    protocolName: "My Protocol",
    contactDetails: contacts,
    chains: chains,
    bountyTerms: bountyTerms,
    agreementURI: "ipfs://QmYourAgreementHash"
});

bytes32 salt = keccak256("my-protocol-v1");
address agreement = agreementFactory.create(details, owner, salt);

Configure Contact Details

Provide emergency contacts for whitehats:

Contact[] memory contacts = new Contact[](2);

contacts[0] = Contact({
    name: "Security Lead",
    contact: "security@myprotocol.com"
});

contacts[1] = Contact({
    name: "Emergency Telegram",
    contact: "@myprotocol_security"
});

Define Chain Scope

Specify which contracts on which chains are covered:

Account[] memory accounts = new Account[](2);

accounts[0] = Account({
    accountAddress: "0x1234...MyVault",
    childContractScope: ChildContractScope.All
});

accounts[1] = Account({
    accountAddress: "0x5678...MyStrategy",
    childContractScope: ChildContractScope.None
});

Chain[] memory chains = new Chain[](1);
chains[0] = Chain({
    caip2ChainId: "eip155:325",  // BattleChain
    assetRecoveryAddress: "0xYourRecoveryMultisig",
    accounts: accounts
});

Child Contract Scope Options

ValueMeaning
NoneOnly the listed contract
ExistingOnlyContract + children created before agreement
AllContract + all children (past and future)
FutureOnlyContract + children created after agreement

Configure Bounty Terms

BountyTerms memory bountyTerms = BountyTerms({
    bountyPercentage: 10,              // 10% of recovered funds
    bountyCapUsd: 5_000_000,           // Max $5M per whitehat
    retainable: true,                  // Whitehat keeps bounty from recovered
    identity: IdentityRequirements.Anonymous,
    diligenceRequirements: "",         // Only for Named identity
    aggregateBountyCapUsd: 0           // 0 = no aggregate cap
});

Retainable vs Return-All

  • Retainable (true): Whitehat keeps bounty from recovered funds, sends rest to recovery
  • Return-All (false): Whitehat sends all funds to recovery, protocol pays bounty separately

Identity Options

LevelRequirement
AnonymousNo identity verification
PseudonymousConsistent pseudonym required
NamedLegal name verification (specify process in diligenceRequirements)

Extend Commitment Window

Agreements must commit to terms for at least 7 days:

// Extend to 30 days
agreement.extendCommitmentWindow(block.timestamp + 30 days);

During the commitment window, you cannot make unfavorable changes to whitehats.

Adopt the Agreement

Link your protocol to the agreement:

safeHarborRegistry.adoptSafeHarbor(agreement);

Modify an Existing Agreement

You can update agreements, with restrictions during commitment:

// Always allowed
agreement.setProtocolName("New Name");
agreement.setContactDetails(newContacts);
agreement.addAccounts("eip155:325", newAccounts);

// Only favorable changes during commitment
agreement.setBountyTerms(betterTerms);

// Blocked during commitment
agreement.removeChains(chainIds);        // Reverts
agreement.removeAccounts(chainId, addrs); // Reverts

How to Request Attack Mode

Next: Submit your contracts for attack mode