Skip to main content
BRC2.0 adds an EVM-compatible execution layer to Bitcoin. You write Solidity contracts just as you would for Ethereum, but deploy them by inscribing the compiled bytecode onto Bitcoin — no RPC endpoint or ETH required. Indexers execute the bytecode deterministically and track state.
EVM addresses in BRC2.0 have no private keys. An EVM address is derived from your Bitcoin wallet’s output script: evm_address = keccak256(pkscript_bytes)[12:]. This address cannot sign messages. Do not use ecrecover or Ethereum-style ECDSA assumptions in your contracts. Use the BIP-322 verification precompile at address 0xFE for any authentication logic that requires explicit user consent.
1

Write your Solidity contract

Write your contract using standard Solidity. BRC2.0 is compatible with Hardhat, Foundry, and Remix — use whichever toolchain you already know.Key differences from Ethereum to keep in mind:
  • block.number refers to the Bitcoin block height, not an Ethereum block number.
  • block.timestamp is the Bitcoin block time. It is less granular than Ethereum and can go backwards, so avoid relying on it for precision timing.
  • msg.value does not carry native ETH. Use the BRC2.0 deposit mechanism to move BRC-20 tokens into contracts instead.
  • Block time is approximately 10 minutes.
The example below is a simple constant-product AMM that accepts deposits of two BRC-20 tokens (ORDI and SATS), lets users swap between them, and allows withdrawals back to base BRC-20 balances:
contract SimpleDEX {
    mapping(address => uint256) public balancesA;
    mapping(address => uint256) public balancesB;
    uint256 public reserveA;
    uint256 public reserveB;

    event Swap(address indexed user, uint256 amountIn, uint256 amountOut);
    event Withdraw(address indexed user, string tick, uint256 amount);

    function deposit(string calldata tick, uint256 amount) external {
        // Called via BRC2.0 deposit operation
        if (keccak256(bytes(tick)) == keccak256("ordi")) {
            balancesA[msg.sender] += amount;
            reserveA += amount;
        } else if (keccak256(bytes(tick)) == keccak256("sats")) {
            balancesB[msg.sender] += amount;
            reserveB += amount;
        }
    }

    function swap(uint256 amountIn, bool aToB) external {
        uint256 amountOut;
        if (aToB) {
            require(balancesA[msg.sender] >= amountIn, "Insufficient balance");
            amountOut = getAmountOut(amountIn, reserveA, reserveB);
            balancesA[msg.sender] -= amountIn;
            balancesB[msg.sender] += amountOut;
            reserveA += amountIn;
            reserveB -= amountOut;
        } else {
            require(balancesB[msg.sender] >= amountIn, "Insufficient balance");
            amountOut = getAmountOut(amountIn, reserveB, reserveA);
            balancesB[msg.sender] -= amountIn;
            balancesA[msg.sender] += amountOut;
            reserveB += amountIn;
            reserveA -= amountOut;
        }
        emit Swap(msg.sender, amountIn, amountOut);
    }

    function withdraw(string calldata tick, uint256 amount) external {
        // Burns internal balance; indexer credits BRC-20
        if (keccak256(bytes(tick)) == keccak256("ordi")) {
            require(balancesA[msg.sender] >= amount, "Insufficient balance");
            balancesA[msg.sender] -= amount;
            reserveA -= amount;
        } else {
            require(balancesB[msg.sender] >= amount, "Insufficient balance");
            balancesB[msg.sender] -= amount;
            reserveB -= amount;
        }
        emit Withdraw(msg.sender, tick, amount);
    }

    function getAmountOut(
        uint256 amountIn,
        uint256 reserveIn,
        uint256 reserveOut
    ) internal pure returns (uint256) {
        uint256 amountInWithFee = amountIn * 997;
        uint256 numerator = amountInWithFee * reserveOut;
        uint256 denominator = (reserveIn * 1000) + amountInWithFee;
        return numerator / denominator;
    }
}
This DEX pattern — deposit BRC-20 tokens, execute swaps inside the EVM, withdraw back to BRC-20 — is the canonical interaction model for BRC2.0 DeFi. The deposit step moves tokens from base BRC-20 state into the contract’s internal balance. The withdraw step reverses that bridge trustlessly, with no multisig required.
2

Compile to bytecode

Compile your contract to produce the deployment bytecode. BRC2.0 accepts standard EVM creation bytecode — the same artifact Hardhat or Foundry would deploy to Ethereum.
With Hardhat, run npx hardhat compile. The deployment bytecode is in artifacts/contracts/YourContract.sol/YourContract.json under the "bytecode" key. That value (including the 0x prefix) is what you put in the deploy inscription’s d field.With Foundry, run forge build. The bytecode is in out/YourContract.sol/YourContract.json under .bytecode.object.
npx hardhat compile
# Bytecode at: artifacts/contracts/SimpleDEX.sol/SimpleDEX.json → "bytecode"
The bytecode value looks like 0x608060405234801561001057600080fd5b50.... Copy the full hex string — you will need it in the next step.
3

Create the deploy inscription

Construct the BRC2.0 deploy inscription JSON. Paste the full bytecode from the previous step as the value of the d field.
{
  "p": "brc20-prog",
  "op": "deploy",
  "d": "0x608060405234801561001057600080fd5b50..."
}
  • p must be "brc20-prog" — this identifies the BRC2.0 programmable module protocol.
  • op is "deploy" (you can also use "d" as a shorthand).
  • d is the full EVM creation bytecode, including the 0x prefix.
Inscribe this JSON as a text/plain inscription. The inscription ID will be used to derive the contract’s EVM address.
4

Send to the module address

Inscribing the bytecode alone does not activate the contract. You must send the inscription to the BRC20PROG module address (OP_RETURN "BRC20PROG") in a Bitcoin transaction. This is the trigger that tells indexers to execute the constructor and register the contract.Until this send transaction is confirmed, the contract does not exist from the indexer’s perspective.
This two-step pattern — inscribe, then send to the module address — is consistent across all BRC2.0 operations. The inscription carries the data; the send transaction is the execution trigger.
5

Wait for Bitcoin confirmation

As with all BRC2.0 operations, execution is anchored to confirmed Bitcoin blocks. After the send transaction broadcasts, wait for it to be included in a Bitcoin block.Bitcoin produces a block approximately every 10 minutes. One confirmation is sufficient for indexers to process the deploy, execute the constructor, and assign the contract address.
6

Verify deployment

Once the block confirms, your contract is live. The contract address is deterministically derived from the inscription ID used in the deploy transaction.Use a BRC2.0 block explorer to confirm the contract exists and to retrieve its address. The explorer will show:
  • The derived EVM address
  • Constructor execution status (success or revert)
  • Any events emitted during construction
  • The deployed bytecode
Keep the contract address — you will need it to call the contract in the next step.
7

Call your contract

Invoke a deployed contract by inscribing a call operation and sending it to the module address.
{
  "p": "brc20-prog",
  "op": "call",
  "c": "<contract_address>",
  "b": "<abi_encoded_calldata>"
}
  • c is the EVM address of the deployed contract.
  • b is the ABI-encoded calldata for the function you want to invoke (the same encoding you would use on Ethereum).
  • op can be "call" or "c" as a shorthand.
For more complex interactions — such as depositing BRC-20 tokens and calling a function in one step — use the transact operation:
{
  "p": "brc20-prog",
  "op": "transact",
  "b": "<signed_evm_tx>"
}
transact accepts a signed EVM transaction blob, which combines a deposit with a contract call in a single inscription.
{
  "p": "brc20-prog",
  "op": "call",
  "c": "0xYourContractAddress",
  "b": "0xabcdef12..."
}
As with deploy, inscribe the JSON and send the inscription to the BRC20PROG module address. Indexers will execute the call, update contract state, and emit any logs. State changes are committed only if execution succeeds — reverted calls leave state unchanged.