> For the complete documentation index, see [llms.txt](https://bluwhale.gitbook.io/bluwhaleai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://bluwhale.gitbook.io/bluwhaleai/oceanum-oxn/smart-contract-development/deploying-contracts.md).

# Deploying Contracts

Deployment on Oceanum (OXN) is nearly identical to Ethereum, with a handful of practical adjustments. This page covers the patterns you actually use in production and the pitfalls that catch teams the first time.

### The essentials <a href="#the-essentials" id="the-essentials"></a>

Every Oceanum (OXN) contract deployment must:

1. Compile with `evmVersion: "paris"`.
2. Set `gasLimit` explicitly (typically 3–5 million for a standard contract).
3. Use a wrapped signer if you want future calls to be encrypted — deployment itself is plain regardless.
4. Wait for the transaction to be included before recording the address.

### Hardhat deployment script <a href="#hardhat-deployment-script" id="hardhat-deployment-script"></a>

scripts/deploy.js

```
const hre = require("hardhat");

async function main() {
  const [deployer] = await hre.ethers.getSigners();
  console.log("Deployer:", deployer.address);
  console.log("Balance :", (await hre.ethers.provider.getBalance(deployer.address)).toString());

  const Contract = await hre.ethers.getContractFactory("MyContract");
  const contract = await Contract.deploy(
    /* constructor args */,
    { gasLimit: 3_000_000n }
  );

  await contract.waitForDeployment();
  const address = await contract.getAddress();
  console.log("Deployed:", address);
  console.log("Tx hash :", contract.deploymentTransaction().hash);

  // Verify code landed on-chain
  const code = await hre.ethers.provider.getCode(address);
  if (code === "0x") {
    throw new Error("Deployment failed — no code at address");
  }
  console.log("Code size:", (code.length - 2) / 2, "bytes");
}

main().catch((e) => { console.error(e); process.exit(1); });
```

Run:

```
npx hardhat run scripts/deploy.js --network oxn_testnet
```

### Foundry deployment <a href="#foundry-deployment" id="foundry-deployment"></a>

```
forge create src/MyContract.sol:MyContract \
  --rpc-url oxn_testnet \
  --private-key $PRIVATE_KEY \
  --constructor-args "arg1" "arg2" \
  --gas-limit 3000000
```

Or with a script:

script/Deploy.s.sol

```
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "forge-std/Script.sol";
import "../src/MyContract.sol";

contract DeployScript is Script {
    function run() external {
        vm.startBroadcast();
        new MyContract(/* args */);
        vm.stopBroadcast();
    }
}
```

Run:

```
forge script script/Deploy.s.sol --rpc-url oxn_testnet --broadcast --gas-limit 3000000
```

### Deterministic deployment (CREATE2) <a href="#deterministic-deployment-create2" id="deterministic-deployment-create2"></a>

To get the same contract address across multiple environments, use CREATE2:

```
import { Create2 } from "@openzeppelin/contracts/utils/Create2.sol";

contract Deployer {
    function deploy(bytes32 salt, bytes memory bytecode) external returns (address) {
        return Create2.deploy(0, salt, bytecode);
    }

    function computeAddress(bytes32 salt, bytes memory bytecode) external view returns (address) {
        return Create2.computeAddress(salt, keccak256(bytecode));
    }
}
```

With the same salt, bytecode, and deployer address, you get the same contract address on any EVM chain — useful for having identical contract addresses on multiple networks.

### Contract constructors with encrypted initial state <a href="#contract-constructors-with-encrypted-initial-state" id="contract-constructors-with-encrypted-initial-state"></a>

Constructor arguments are part of deployment calldata, which is plain on Oceanum (OXN) (bytecode + args are always public). So you cannot initialize secrets through the constructor.

Instead, initialize secrets in a separate encrypted call after deployment:

```
contract Vault {
    address public owner;
    bytes32 private secret;

    constructor(address _owner) {
        owner = _owner;
        // Do NOT do: secret = _secret;  <-- would be public in constructor calldata
    }

    function setSecret(bytes32 _secret) external {
        require(msg.sender == owner, "not owner");
        secret = _secret;   // this call's calldata IS encrypted
    }
}
```

Deploy plain, then call `setSecret` via a wrapped signer. The secret is then encrypted in storage.

### Proxy patterns for upgradability <a href="#proxy-patterns-for-upgradability" id="proxy-patterns-for-upgradability"></a>

OpenZeppelin's Transparent and UUPS proxies work on Oceanum (OXN) without modification. Each proxy contract's storage lives at the proxy's address; storage encryption keys derive from that address, so upgrades preserve the encrypted state.

Hardhat + OpenZeppelin upgrades

```
const { upgrades } = require("hardhat");

const Impl = await ethers.getContractFactory("MyContractV1");
const proxy = await upgrades.deployProxy(Impl, [/* init args */], {
  initializer: "initialize",
});
await proxy.waitForDeployment();
```

To upgrade:

```
const ImplV2 = await ethers.getContractFactory("MyContractV2");
await upgrades.upgradeProxy(proxyAddress, ImplV2);
```

### Recording deployment addresses <a href="#recording-deployment-addresses" id="recording-deployment-addresses"></a>

Never hardcode contract addresses across environments. Recommended:

scripts/deploy.js (with address persistence)

```
const fs = require("fs");
const path = require("path");

// ... deploy ...

const outputPath = path.join(__dirname, "..", "deployments", `${network.name}.json`);
const existing = fs.existsSync(outputPath) ? JSON.parse(fs.readFileSync(outputPath)) : {};
existing.MyContract = {
  address: await contract.getAddress(),
  txHash: contract.deploymentTransaction().hash,
  deployedAt: new Date().toISOString(),
};
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(outputPath, JSON.stringify(existing, null, 2));
console.log("Wrote", outputPath);
```

Commit `deployments/oxn_testnet.json` (and equivalents) to your repo. Every developer and every CI run reads the latest addresses from there.

### Common deployment failures <a href="#common-deployment-failures" id="common-deployment-failures"></a>

**"Contract deployed but `getCode` returns `0x`."** The transaction succeeded but the code did not land. Almost always a `gasLimit` too low. Bump to `5_000_000n` and redeploy.

**"Insufficient balance to pay fees."** Faucet more TBLUAI or use `gasPrice: 0n` on testnet.

**"Contract calls revert with `invalid code`."** Compiled with the wrong `evmVersion`. Recompile with `paris`.

**"Deployment succeeds but future calls fail with `insufficient balance`."** The deployer had enough gas to deploy, but not enough for subsequent operations. Fund the account further, or use a dedicated deployer account.

### Next steps <a href="#next-steps" id="next-steps"></a>

* [**Verifying Contracts on the Explorer**](https://docs.bout.network/build/verifying)
* [**Debugging & Troubleshooting**](https://docs.bout.network/build/debugging)
* [**Migrating an Ethereum dApp to OXN**](https://docs.bout.network/guides/migrate-from-ethereum)

[Edit this page](https://github.com/oxn-network/oxn-docs/edit/main/docs/06-build/deploying.md)[PreviousTesting Confidential Behavior](https://docs.bout.network/build/testing-confidential)[NextVerifying Contracts on the Explorer](https://docs.bout.network/build/verifying)
