Deploy Your First Contract
Deploy a vulnerable vault to BattleChain, create a Safe Harbor agreement, and open it for attack.
Prerequisites
Git
git --version
If not installed: git-scm.com
Foundry
curl -L https://foundry.paradigm.xyz | bash
foundryup
forge --version
If you hit issues, see the Foundry installation guide.
Just
just is a command runner used to execute the repo's scripts cleanly. Install it with:
brew install just
Verify: just --version
A Wallet with BattleChain Testnet ETH
You'll need an EVM wallet with ETH on BattleChain to pay for gas. Three steps:
1. Add BattleChain to your wallet
| Field | Value |
|---|---|
| Network Name | BattleChain Testnet |
| RPC URL | https://testnet.battlechain.com:3051 |
| Chain ID | 627 |
See Add BattleChain to MetaMask for a step-by-step guide.
2. Get Sepolia ETH
You need Sepolia ETH to bridge to BattleChain. Go to the Google Cloud faucet, paste your wallet address, and request testnet ETH. You only need a small amount â 0.05 ETH is plenty for this tutorial.
3. Bridge to BattleChain
Open the BattleChain Portal, connect your wallet, and bridge Sepolia ETH to BattleChain Testnet:
- Select Sepolia as the source network and BattleChain Testnet as the destination
- Enter the amount to bridge
- Confirm the transaction in your wallet
Wait for the bridge transaction to finalize â this usually takes a few minutes. Once your BattleChain balance shows ETH, you're ready to deploy.
Setup
Clone the starter repo and install dependencies:
git clone https://github.com/Cyfrin/battlechain-starter
cd battlechain-starter
forge install
Import your key into Foundry's keystore
Never put your private key in a file. Instead, import it into Foundry's encrypted keystore:
cast wallet import battlechain --interactive
You'll be prompted to paste your private key and set an encryption password. The key is stored encrypted at ~/.foundry/keystores/battlechain â the password is required every time you run a script. Your raw private key is never written to disk.
Verify it imported correctly:
cast wallet list
Configure .env
cp .env.example .env
Open .env and set your values. Note that SENDER_ADDRESS is your public wallet address â no private key goes here:
# Your public wallet address
SENDER_ADDRESS=0x...your_wallet_address...
# Filled in after: just setup
TOKEN_ADDRESS=
VAULT_ADDRESS=
# Filled in after: just create-agreement
AGREEMENT_ADDRESS=
# Set to your wallet address (same as SENDER_ADDRESS for this tutorial)
RECOVERY_ADDRESS=
What You're Deploying
Before running anything, understand what you're putting on-chain. Open src/VulnerableVault.sol and look at withdrawAll():
function withdrawAll() external {
uint256 amount = balances[msg.sender];
require(amount > 0, "nothing to withdraw");
// â INTERACTION before EFFECT
TOKEN.transfer(msg.sender, amount); // external call â may trigger a hook
// â Balance cleared after the transfer â too late
balances[msg.sender] = 0;
}
This is a CEI (Checks-Effects-Interactions) violation. The vault transfers tokens before zeroing the caller's balance. If the token notifies the recipient on transfer â which MockToken does via its hook system â an attacker can re-enter withdrawAll() before the balance ever hits zero, draining the vault entirely.
The correct order is:
// â
Effect first, then interaction
balances[msg.sender] = 0;
TOKEN.transfer(msg.sender, amount);
You've shipped the wrong version. That's the point.
Step 1 â Deploy
Known issue: out-of-gas failures
Forge estimates gas locally rather than via an RPC call, which can significantly underestimate costs on BattleChain. Transactions may fail with a generic error that doesn't mention gas.
If any just command fails unexpectedly, retry with one of these workarounds:
-g 300â tells Forge to use 3Ã the estimated gas:forge script ... -g 300--skip-simulationâ skips gas estimation entirely:forge script ... --skip-simulation
If the justfile doesn't expose these flags directly, add the flag to the underlying forge script call in the relevant justfile recipe.
just setup deploys MockToken and VulnerableVault, then seeds the vault with 1,000 tokens to simulate protocol liquidity:
just setup
Expected output:
MockToken deployed: 0x...
VulnerableVault deployed: 0x...
Vault seeded with 1000 tokens
--- Add to your .env ---
TOKEN_ADDRESS=0x...
VAULT_ADDRESS=0x...
Copy both addresses into your .env.
Your vault is live on BattleChain and registered with the AttackRegistry.
To verify your contracts on the block explorer, see Verifying Contracts.
Step 2 â Create a Safe Harbor Agreement
The Safe Harbor agreement defines the rules of engagement: which contracts are in scope, what bounty whitehats earn, and where recovered funds go.
Make sure VAULT_ADDRESS is set in .env, then:
just create-agreement
This script:
- Puts your vault in scope on BattleChain (
eip155:627) - Sets a 10% bounty, retainable from recovered funds
- Sets your wallet as the recovery address â drained funds come back here
- Locks terms for 30 days
Expected output:
Agreement created: 0x...
Commitment window extended 30 days
Safe Harbor adopted
--- Add to your .env ---
AGREEMENT_ADDRESS=0x...
Copy AGREEMENT_ADDRESS into your .env and set RECOVERY_ADDRESS to your deployer wallet address.
Step 3 â Request Attack Mode
Submit your vault for DAO review:
just request-attack-mode
Your agreement is now in ATTACK_REQUESTED state. The DAO will verify:
- This is a new contract, not a copy of a live mainnet deployment
- Bounty terms are reasonable
- Scope is clearly defined
Poll for approval:
just check-state
| Output | Meaning |
|---|---|
2 | ATTACK_REQUESTED â waiting for DAO |
3 | UNDER_ATTACK â approved, vault is open |
Approval on testnet typically happens within a few hours. Reach out in Discord if you're waiting.
Once you see 3, your vault is live and attackable. Head to Execute Your First Attack to see what happens next.
What You Just Accomplished
You deployed a contract, created a Safe Harbor agreement, and opened it to whitehat attack â all on a testnet built for exactly this workflow.
This matters beyond BattleChain. Safe Harbor agreements protect protocols and whitehats on mainnet too. Every production contract holding real funds benefits from a clear agreement that defines scope, bounties, and recovery rules before something goes wrong. The process you just walked through is the same one you'd follow to protect a live deployment.
You now know how to:
- Deploy contracts to a custom chain with Foundry
- Import keys securely using
cast wallet import(no plaintext private keys) - Create and configure a Safe Harbor agreement on-chain
- Register contracts for coordinated security testing
You've deployed your first contract to BattleChain and set up a Safe Harbor agreement. These are skills you'll use on mainnet â not just here.