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.
forge and cast do not currently support OXN's encrypted transaction envelope. That means:
- Contract deployment via
forge createworks. Deployment calldata (bytecode + constructor args) is always plain on OXN, so no wrapping is needed. cast sendsends 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 callsendseth_callin plain. Views that read caller-scoped state (like a privatebalanceOf(msg.sender)) will not work —msg.senderis 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
- Foundry installed (run
foundryupto keep it current) - A wallet with testnet TBLUAI — see Set Up Your Wallet and Get Testnet Tokens
- Your private key exported to
PRIVATE_KEY
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:
[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
evm_version = "paris"— OXN's runtime does not yet support thePUSH0opcode introduced in Shanghai. Contracts using PUSH0 revert at runtime withinvalid code. See EVM Compatibility.- No
eth_estimateGas— Foundry uses--gas-limiton 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:
@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/
4. Write the contract
Replace src/Counter.sol with 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:
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
- Deploy with Hardhat — the recommended path for confidential interaction
- Confidentiality Model — what the toolchain choice actually affects
- Encrypted vs Plain Calls — why
castsends plain