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:
// 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 calldata —
transfer(recipient, amount)calls do not revealrecipientoramountto observers. - Encrypted events —
Transfer(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:
// 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:
-
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.
-
Store a per-user "pending" flag in the contract that the recipient reads via signed query:
mapping(address => uint256) private pendingIncomingCount; -
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
- Building a Confidential ERC-20 — hide amounts and transfers entirely
- Signed Queries — how the gated read pattern works
- Deploying Contracts