Skip to main content

Deploying an ERC-20 Token

The simplest kind of token contract, deployed on OXN. This guide shows the standard flow, then how to add confidentiality.

Standard ERC-20

Same as Ethereum. Use OpenZeppelin:

contracts/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);
}
}

Deploy — same as Quickstart: Hardhat.

What OXN gives you automatically

Without any code changes, your users get:

  • Encrypted calldatatransfer(recipient, amount) calls do not reveal recipient or amount to observers.
  • Encrypted eventsTransfer(from, to, amount) events are only decryptable by the caller (the sender).

Recipient does not see the transfer event via a global indexer, because event logs are encrypted per-caller. If your token app depends on notifying recipients, you need to design for that.

Adding confidential balances

For a token where balanceOf(user) returns 0 to everyone except the user themselves, gate the getter:

contracts/PrivateToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

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

contract PrivateToken is ERC20 {
// Approvals allowing others to see your balance
mapping(address => mapping(address => bool)) private readApprovals;

constructor(uint256 initialSupply) ERC20("PrivateToken", "PRIV") {
_mint(msg.sender, initialSupply);
}

// Override the standard balanceOf to gate it
function balanceOf(address account) public view virtual override returns (uint256) {
if (msg.sender == account || readApprovals[account][msg.sender]) {
return super.balanceOf(account);
}
return 0;
}

function approveReader(address reader) external {
readApprovals[msg.sender][reader] = true;
}
}

For this to work correctly, callers must query via signed queries — see Signed Queries.

Handling recipient notifications

Since encrypted Transfer events aren't visible to the recipient, options:

  1. Emit a public companion event with just the participants (not the amount):

    event TransferHint(address indexed from, address indexed to);

    Recipients can watch this to know when to fetch their balance.

  2. Store a per-user "pending" flag in the contract that the recipient reads via signed query:

    mapping(address => uint256) private pendingIncomingCount;
  3. Off-chain notification (push service, backend). Independent of the chain.

Full example with tests

See the OXN sample repository for a runnable version with Hardhat tests.

Next steps