Deploy a Token
Mine a vanity salt, then deploy a token + bonding curve pair through the Factory. Examples use ethers v6.
Prerequisites
- Native asset for the deployment fee — Base 0.06 ETH, BSC 0.18 BNB, Polygon 1,440 POL, Robinhood 0.06 ETH, Gnosis 144 xDAI
- An ethers v6 provider and signer
- The Factory and token master addresses for your chain
The factory does not pass your salt to CREATE2 — it passes keccak256(salt ‖ msg.sender). A salt mined for one deployer produces a different address for anyone else, so you must include your deploying address when mining. This is what makes a mined salt useless if copied out of the mempool.
Step 1 — Build the salt
The 32-byte salt is three fields: an 8-byte anchor block, the first 8 bytes of that block's hash, and a 16-byte nonce you mine.
import { ethers } from "ethers";
// EIP-1167 minimal proxy initcode for the token master
function proxyInitCodeHash(tokenMaster) {
const initcode = ethers.concat([
"0x3d602d80600a3d3981f3363d3d373d3d3d363d73",
tokenMaster,
"0x5af43d82803e903d91602b57fd5bf3"
]);
return ethers.keccak256(initcode);
}
async function mineSalt(provider, factory, tokenMaster, deployer) {
const anchorBlock = await provider.getBlockNumber();
const block = await provider.getBlock(anchorBlock);
// [0:8] anchor block, big-endian
const anchorField = ethers.zeroPadValue(ethers.toBeHex(anchorBlock), 8);
// [8:16] first 8 bytes of the blockhash
const hashField = ethers.dataSlice(block.hash, 0, 8);
const initCodeHash = proxyInitCodeHash(tokenMaster);
const deployerWord = ethers.zeroPadValue(deployer, 32);
for (let nonce = 0n; ; nonce++) {
// [16:32] mined nonce
const nonceField = ethers.zeroPadValue(ethers.toBeHex(nonce), 16);
const salt = ethers.concat([anchorField, hashField, nonceField]);
// The factory domain-separates the salt by msg.sender
const effectiveSalt = ethers.keccak256(ethers.concat([salt, deployerWord]));
const predicted = ethers.getCreate2Address(factory, effectiveSalt, initCodeHash);
if (predicted.toLowerCase().startsWith("0x24")) {
return { salt, predicted, anchorBlock };
}
}
}from eth_utils import keccak, to_checksum_address
from web3 import Web3
# EIP-1167 minimal proxy initcode for the token master
def proxy_initcode_hash(token_master):
initcode = (
bytes.fromhex("3d602d80600a3d3981f3363d3d373d3d3d363d73")
+ bytes.fromhex(token_master[2:])
+ bytes.fromhex("5af43d82803e903d91602b57fd5bf3")
)
return keccak(initcode)
def mine_salt(w3, factory, token_master, deployer):
anchor_block = w3.eth.block_number
block = w3.eth.get_block(anchor_block)
# [0:8] anchor block, big-endian
anchor_field = anchor_block.to_bytes(8, "big")
# [8:16] first 8 bytes of the blockhash
hash_field = bytes(block["hash"])[:8]
initcode_hash = proxy_initcode_hash(token_master)
deployer_word = bytes(12) + bytes.fromhex(deployer[2:])
factory_bytes = bytes.fromhex(factory[2:])
nonce = 0
while True:
# [16:32] mined nonce
salt = anchor_field + hash_field + nonce.to_bytes(16, "big")
# The factory domain-separates the salt by msg.sender
effective_salt = keccak(salt + deployer_word)
predicted = to_checksum_address(
keccak(b"\xff" + factory_bytes + effective_salt + initcode_hash)[12:]
)
if predicted.lower().startswith("0x24"):
return salt, predicted, anchor_block
nonce += 1One byte of prefix is a 1-in-256 hit rate — a few hundred iterations on average, effectively instant.
Step 2 — Pack socials (optional)
The fourth argument is 32 opaque bytes stored in TokenSocials. The contract takes no view on their meaning; encoding is an application-layer convention. Pass ethers.ZeroHash to skip it and set socials later.
Step 3 — Deploy
const FACTORY_ABI = [
"function deploy_pair(string,string,bytes32,bytes32) payable returns (address,address)",
"function get_deployment_fee() view returns (uint256)",
"event PairDeployed(address indexed token, address indexed curve, address indexed deployer, string name, string symbol, bytes32 packed_socials)"
];
const factory = new ethers.Contract(FACTORY_ADDRESS, FACTORY_ABI, signer);
const fee = await factory.get_deployment_fee();
const { salt, predicted } = await mineSalt(
provider, FACTORY_ADDRESS, TOKEN_MASTER, await signer.getAddress()
);
const tx = await factory.deploy_pair(
"My Token", "MTK", salt, ethers.ZeroHash, { value: fee }
);
const receipt = await tx.wait();
// Recover the deployed addresses from the log
const ev = receipt.logs
.map(l => { try { return factory.interface.parseLog(l); } catch { return null; } })
.find(e => e?.name === "PairDeployed");
const [token, curve] = [ev.args.token, ev.args.curve];
console.log(token === predicted); // truew3 = Web3(Web3.HTTPProvider(RPC_URL))
acct = w3.eth.account.from_key(PRIVATE_KEY)
factory = w3.eth.contract(address=FACTORY_ADDRESS, abi=FACTORY_ABI)
fee = factory.functions.get_deployment_fee().call()
salt, predicted, _ = mine_salt(w3, FACTORY_ADDRESS, TOKEN_MASTER, acct.address)
tx = factory.functions.deploy_pair(
"My Token", "MTK", salt, bytes(32)
).build_transaction({
"from": acct.address,
"value": fee,
"nonce": w3.eth.get_transaction_count(acct.address),
})
signed = acct.sign_transaction(tx)
tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
# Recover the deployed addresses from the log
ev = factory.events.PairDeployed().process_receipt(receipt)[0]
token, curve = ev["args"]["token"], ev["args"]["curve"]
print(token == predicted) # TrueThe salt is only valid for INCLUSION_WINDOW blocks after its anchor — Base 10 blocks, BSC 44 blocks, Polygon 12 blocks, Robinhood 200 blocks, Gnosis 5 blocks. On short-window chains, mine immediately before broadcasting and use a gas price that will land promptly. If you miss it the transaction reverts with Too late; re-mine against a fresh anchor and resubmit.
Step 4 — Seed the curve (optional)
The deployment fee is forwarded to the bonding curve as its opening balance. You are not obliged to buy, but note that the deployer can never sell — sell() and refund_claim() both reject the deployer address. Anything you buy on your own curve is locked until graduation.
Common reverts
| Revert | Cause |
|---|---|
Protocol token not yet deployed | The chain's protocol pair has not been created — the factory is not open for public deployment yet |
Incorrect fee | msg.value must equal the fee exactly; read it from get_deployment_fee() |
Vanity miss | Mined against the raw salt instead of keccak256(salt ‖ msg.sender), or against the wrong deployer address |
Blockhash mismatch | Bytes [8:16] do not match the anchor block's hash prefix |
Too late | Past the inclusion window — re-mine |
Anchor not mined | Anchor block is the current block or later |
Blockhash unavailable | Anchor is more than 256 blocks back — the transaction sat too long |