Skip to main content

Deploy Your First Contract (Foundry)

Foundry is a fast Rust-based toolchain popular for Solidity development. This guide covers deploying an ERC-20 to OXN Testnet with forge, and explains the important caveat about cast and confidentiality.

Foundry cannot yet send confidential transactions

forge and cast do not currently support OXN's encrypted transaction envelope. That means:

  • Contract deployment via forge create works. Deployment calldata (bytecode + constructor args) is always plain on OXN, so no wrapping is needed.
  • cast send sends transactions in plain. Sensitive calldata leaks to the mempool and the public block. For anything that must stay confidential, use Hardhat or ethers.js directly.
  • cast call sends eth_call in plain. Views that read caller-scoped state (like a private balanceOf(msg.sender)) will not work — msg.sender is the zero address in a plain call.

Recommended workflow when using Foundry: deploy with forge, then interact from an ethers.js or Hardhat script that supports encrypted calls.

Prerequisites

1. Create the project

forge init my-oxn-foundry
cd my-oxn-foundry

Foundry generates a starter template with src/Counter.sol and tests. You can replace or keep it.

2. Configure foundry.toml

Edit the generated foundry.toml:

foundry.toml
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
solc = "0.8.24"
evm_version = "paris" # required — OXN does not yet support PUSH0
optimizer = true
optimizer_runs = 200

[rpc_endpoints]
oxn_testnet = "https://rpc.bout.network"

[etherscan]
# Contract verification via Sourcify or block explorer plugin is TBD
Two settings that matter
  • evm_version = "paris" — OXN's runtime does not yet support the PUSH0 opcode introduced in Shanghai. Contracts using PUSH0 revert at runtime with invalid code. See EVM Compatibility.
  • No eth_estimateGas — Foundry uses --gas-limit on the command line. Pin it explicitly.

3. Install OpenZeppelin

forge install OpenZeppelin/openzeppelin-contracts --no-commit

Add a remapping so the imports resolve. Create remappings.txt in the project root:

remappings.txt
@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/

4. Write the contract

Replace src/Counter.sol with src/MyToken.sol:

src/MyToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract MyToken is ERC20 {
constructor(uint256 initialSupply) ERC20("MyToken", "MTK") {
_mint(msg.sender, initialSupply);
}
}

Delete or update test/Counter.t.sol if you want it to compile.

5. Compile

forge build

Expected output: Compiler run successful!

6. Deploy

export PRIVATE_KEY=0x...

forge create src/MyToken.sol:MyToken \
--rpc-url oxn_testnet \
--private-key $PRIVATE_KEY \
--constructor-args 1000000000000000000000000 \
--gas-limit 3000000

--constructor-args takes 1e24 (1,000,000 tokens with 18 decimals). Adjust as needed.

Expected output:

Deployer: 0xYourAddress...
Deployed to: 0xNewContractAddress...
Transaction hash: 0x...

Deployment worked because contract-creation calldata is always plain on OXN. The resulting contract's state remains fully encrypted.

7. Verify the deployment

Query a public function (does not depend on msg.sender):

cast call 0xNewContractAddress "totalSupply()(uint256)" --rpc-url oxn_testnet

Expected output: 1000000000000000000000000 (or your chosen initial supply).

8. What NOT to do with cast

Do not use cast send for confidential contract calls. Example of what breaks:

# BAD: sends plain calldata, exposes the transfer amount publicly.
cast send 0xNewContractAddress \
"transfer(address,uint256)" \
0xdead 100 \
--rpc-url oxn_testnet \
--private-key $PRIVATE_KEY

The transaction succeeds but the arguments (0xdead, 100) are visible in the public block. For confidential contract interaction, switch to Hardhat or a JS script using an ethers.js wrapper — see Deploy with Hardhat.

Do not use cast call for caller-scoped views. Example:

# Returns 0 for any address, because msg.sender in a plain cast call is 0x0.
cast call 0xNewContractAddress "balanceOf(address)(uint256)" 0xYourAddress \
--rpc-url oxn_testnet

Standard balanceOf(address who) on a standard ERC-20 works because it reads by address argument (not msg.sender). But a confidential ERC-20 that gates on msg.sender == who will return 0.

Bridging: Foundry deploy + JS interaction

A common pattern: use Foundry to deploy (because forge is faster than Hardhat's compile pipeline), then interact from a Node.js script using an ethers.js wrapper:

scripts/transfer.mjs
import { ethers } from "ethers";
import { wrapEthersProvider, wrapEthersSigner } from "@oasisprotocol/sapphire-ethers-v6";

const TOKEN_ADDR = "0xYourDeployedTokenAddress";
const raw = new ethers.JsonRpcProvider("https://rpc.bout.network");
const provider = wrapEthersProvider(raw);
const signer = wrapEthersSigner(new ethers.Wallet(process.env.PRIVATE_KEY, raw));

const abi = ["function transfer(address,uint256) returns (bool)"];
const token = new ethers.Contract(TOKEN_ADDR, abi, signer);

const tx = await token.transfer(
"0x000000000000000000000000000000000000dEaD",
ethers.parseUnits("100", 18),
{ gasLimit: 500_000n }
);
await tx.wait();
console.log("encrypted transfer:", tx.hash);

This gives you Foundry's fast build times plus encrypted transactions for real interaction.

Next steps