The Root Network: built-in EVM precompiles ("native contracts")
Scope: TRN mainnet (EVM chainId 7668 / 0x1df4), from the seed node source, Release 11.84.0, runtime spec 84 — the version running on mainnet. File references are to trn-seed-public. Live checks were read-only calls against a mainnet node (October 2026); no transactions were sent.
Every 4-byte selector and event topic in this document was computed with web3_sha3 from the exact signature strings in the Rust Action enums and keccak256!(...) constants. Those strings are what the runtime matches on. The macro is a plain keccak of the string (evm-precompiles/utils/macro/src/lib.rs:43-80).
1. Summary table
| # | Precompile | Address / scheme | Fronts pallet(s) | Interface | Payable? |
|---|---|---|---|---|---|
| 1 | ECRecover | 0x…0001 | (Ethereum std) | raw | no |
| 2 | SHA256 | 0x…0002 | std | raw | no |
| 3 | RIPEMD160 | 0x…0003 | std | raw | no |
| 4 | Identity | 0x…0004 | std | raw | no |
| 5 | Modexp | 0x…0005 | std | raw | no |
| 6 | BN128Add | 0x…0006 | std | raw | no |
| 7 | BN128Mul | 0x…0007 | std | raw | no |
| 8 | BN128Pairing | 0x…0008 | std | raw | no |
| 9 | Blake2F | 0x…0009 | std | raw | no |
| 10 | SHA3-256 (FIPS) | 0x…0400 (1024) | Frontier extra | raw | no |
| 11 | ECRecoverPublicKey | 0x…0402 (1026) | Frontier extra | raw | no |
| 12 | Fee proxy (not a real precompile; intercepted by the EVM runner) | 0x…04BB (1211) | pallet-fee-proxy + pallet-dex | callWithFeePreferences | n/a |
| 13 | NFT collection factory | 0x…06B9 (1721) | pallet-nft | initializeCollection | no |
| 14 | SFT collection factory | 0x…06C3 (1731) | pallet-sft | initializeCollection | no |
| 15 | Marketplace | 0x…06CD (1741) | pallet-marketplace | custom | no |
| 16 | Peg (bridge withdraw) | 0x…0793 (1939) | pallet-erc20-peg, pallet-nft-peg | custom | no |
| 17 | DEX router | 0x…DDDD (56797) | pallet-dex | IUniswapV2Router01-like | *ETH* fns yes |
| 18 | FuturePass registrar | 0x…FFFF (65535) | pallet-futurepass | create, futurepassOf | no |
| 19 | ERC-20 per asset | 0xCCCCCCCC + u32 assetId (BE) + 12 zero bytes | pallet-assets-ext (+ pallet-assets, pallet-balances for ROOT), pallet-token-approvals | IERC20 + Metadata + ERC165 | no |
| 20 | ERC-721 per NFT collection | 0xAAAAAAAA + u32 collectionUuid + 12 zero bytes | pallet-nft, pallet-token-approvals, pallet-xls20 | IERC721 + Metadata + Burnable + Ownable + ERC165 + TRN ext + ERC-5484 | no |
| 21 | ERC-1155 per SFT collection | 0xBBBBBBBB + u32 collectionUuid + 12 zero bytes | pallet-sft, pallet-token-approvals | IERC1155 + Burnable + Supply + MetadataURI + Ownable + ERC165 + TRN ext + ERC-5484 | no |
| 22 | FuturePass smart account (one per FuturePass) | 0xFFFFFFFF + u128 futurepassId (16 bytes BE) | pallet-futurepass, pallet-proxy | custom proxy + Ownable | yes (receive, proxyCall) |
Wiring: runtime/src/precompiles.rs:55-99. Address constants: evm-precompiles/utils/src/constants.rs:18-53.
Routing rules (runtime/src/precompiles.rs:58-96):
- Fixed-address precompiles (1–1026 and the TRN ones) live in a
PrecompilesInRangeInclusive<1..=65535>block. - The four prefixed sets are matched by their first 4 bytes.
- The Ethereum precompiles 1–9 are
ForbidRecursion, AllowDelegateCall(lines 64-72). - Every TRN fixed precompile and the ERC-20/721/1155 sets use the default
ForbidRecursion+ForbidDelegateCall(evm-precompiles/utils/src/precompile_set.rs:145, 310). - FuturePass is
LimitRecursionTo<1>+AllowDelegateCall(runtime/src/precompiles.rs:92-97).
Practical consequences of these rules:
- You cannot
DELEGATECALLinto any TRN precompile except a FuturePass. - A precompile cannot be re-entered while it is already on the call stack. Recursion is tracked per address for the prefixed sets (
precompile_set.rs:345-356). For example, anonERC721Receivedhook cannot call back into the same collection address duringsafeTransferFrom.
2. Address schemes
2.1 Asset / collection id → address
runtime/src/impls.rs:267-296 (ErcIdConversion):
address = prefix(4 bytes) ‖ u32_be(id)(4 bytes) ‖ 0x00 × 12
id = u32_be(address[4..8]) if address[0..4] == prefix| Prefix | Kind | Example |
|---|---|---|
0xCCCCCCCC | ERC-20 asset | ROOT (asset 1) 0xcCcCCccC00000001000000000000000000000000, XRP (asset 2) 0xCCCCcCCc00000002000000000000000000000000, VTX (asset 3) 0xcccccccc00000003… |
0xAAAAAAAA | ERC-721 NFT collection | collection 1124 → 0xAAAAAAAA00000464000000000000000000000000 |
0xBBBBBBBB | ERC-1155 SFT collection | collection 18532 → 0xBBBBBBBB00004864000000000000000000000000 |
Ids created after genesis are "UUIDs": (next_id << 10) | parachain_id, with parachain_id = WorldId = 100 (pallet/common/src/utils.rs:26-45, runtime/src/lib.rs:465). The first created asset or collection is therefore (1<<10)|100 = 1124.
- The nft pallet allocates collection UUIDs, and SFT collections use the same allocator. A given UUID is therefore either a 721 collection or a 1155 collection. Live check:
0xBBBBBBBB00000464…is not an SFT, while0xAAAAAAAA00000464…is an NFT. - The low ids 1, 2 and 3 (ROOT, XRP, VTX) are genesis constants (
runtime/src/constants.rs:39, 52, 64).
Bytes 8..20 are ignored when decoding the id. Only bytes 0..8 are read (impls.rs:271-277).
2.2 FuturePass address
pallet/futurepass/src/lib.rs:530-549: 0xFFFFFFFF ‖ u128_be(NextFuturepassId). NextFuturepassId is a u128 (lib.rs:146). FuturePass #1 is 0xFFFFFFFF00000000000000000000000000000001. You cannot compute an address from an owner. Look it up with futurepassOf(owner) on the registrar.
2.3 Pseudo-bytecode ("TRN Asset Precompile")
When an asset or collection is created, the runtime writes the 20-byte code 0x54524e20417373657420507265636f6d70696c65 ("TRN Asset Precompile") at its address. Tools that check EXTCODESIZE > 0 therefore see a contract (runtime/src/impls.rs:424-436). The hook is called from pallet/assets-ext/src/lib.rs:712, pallet/nft/src/impls.rs:633 and pallet/sft/src/impls.rs:68.
Live eth_getCode results:
| Address | Code |
|---|---|
| ROOT, XRP, asset 1124, NFT 1124, SFT 18532 | 0x54524e… |
| VTX (asset 3), a genesis asset with no create hook | 0x |
| FuturePass addresses | 0x |
Fixed precompiles (0x…DDDD, 0x…06B9, …) | 0x |
2.4 "Non-existent" ids are NOT precompiles
The ERC-20, 721 and 1155 sets only claim an address if the asset or collection exists (erc20/src/lib.rs:140-152, erc721/src/lib.rs:266-279, erc1155/src/lib.rs:262-275). Otherwise the address is an ordinary empty account, and a call to it succeeds with empty return data. Live checks:
eth_callto0xcccccccc0fffffff…name()returns0x.owner()on the non-existent0xBBBBBBBB00000464…returns0x.
Solidity callers using high-level calls will revert on ABI decode. Low-level callers must check returndata.length.
The FuturePass set is the opposite. Any address starting 0xFFFFFFFF is claimed as a precompile, with no existence check (futurepass/src/lib.rs:180-186).
3. Standard Ethereum precompiles (brief)
runtime/src/precompiles.rs:62-75:
| Address | Precompile | Notes |
|---|---|---|
| 0x01 | ECRecover | Istanbul |
| 0x02 | SHA256 | Istanbul. Live: sha256("") = 0xe3b0c442… ✓ |
| 0x03 | RIPEMD160 | Istanbul |
| 0x04 | Identity | Istanbul |
| 0x05 | Modexp | Istanbul |
| 0x06 | BN128Add | Istanbul |
| 0x07 | BN128Mul | Istanbul |
| 0x08 | BN128Pairing | Istanbul |
| 0x09 | Blake2F | Istanbul |
| 0x400 (1024) | Sha3FIPS256 | Real SHA3-256, not keccak. Live: sha3("") = 0xa7ffc6f8… ✓ |
| 0x402 (1026) | ECRecoverPublicKey | Returns the 64-byte uncompressed pubkey |
All Istanbul-era precompiles 1–9 are present. Missing:
- No 0x0a (KZG point evaluation / Cancun).
- No P-256 verifier (RIP-7212).
- No precompile at 0x401 (1025). Moonbeam has Dispatch there; TRN does not.
The TRN docs say "Solidity ≤ 0.8.20 (Shanghai)" (trn-docs/docs/build/evm/ethereum-vs-trn.md).
4. ERC-20 asset precompile: 0xCCCCCCCC[assetId]000000000000000000000000
Source: evm-precompiles/erc20/src/lib.rs. It fronts pallet-assets-ext. ROOT, asset 1, is routed by assets-ext to pallet-balances. Approvals live in pallet-token-approvals.
Interface (code-exact)
solidity
interface ITrnErc20 /* IERC20, IERC20Metadata, IERC165 */ {
event Transfer(address indexed from, address indexed to, uint256 value); // 0xddf252ad…
event Approval(address indexed owner, address indexed spender, uint256 value); // 0x8c5be1e5…
function totalSupply() external view returns (uint256); // 0x18160ddd
function balanceOf(address who) external view returns (uint256); // 0x70a08231
function allowance(address owner, address spender) external view returns (uint256); // 0xdd62ed3e
function transfer(address to, uint256 amount) external returns (bool); // 0xa9059cbb
function approve(address spender, uint256 amount) external returns (bool); // 0x095ea7b3
function transferFrom(address from, address to, uint256 amount) external returns (bool); // 0x23b872dd
function name() external view returns (string memory); // 0x06fdde03
function symbol() external view returns (string memory); // 0x95d89b41
function decimals() external view returns (uint8); // 0x313ce567
function supportsInterface(bytes4 id) external view returns (bool); // 0x01ffc9a7
}- ERC165 ids:
0x01ffc9a7(ERC165),0x36372b07(ERC20),0xa219a025(ERC20Metadata) (erc20/src/lib.rs:42-46).supportsInterface(0xffffffff)returns false per EIP-165 (lines 431-451). name()andsymbol()are written as ABIbytes, which encode identically tostring.
Behaviour and differences from standard ERC-20
balanceOfreturns the spendable balance, not the total balance. It usesreducible_balance(Expendable, Polite)(erc20/src/lib.rs:194-216). For ROOT, staked, locked or reserved funds are excluded. For example, a FuturePass's ROOT proxy deposit is reserved and does not show. Sum ofbalanceOfis therefore ≠totalSupply.transferandtransferFromdispatchassets_ext::transfer{keep_alive:false}(erc20/src/lib.rs:278-312, 315-371). The sender account may be reaped.- Allowances are a separate pallet.
approvedispatchestoken_approvals::erc20_approval(lines 242-276).transferFromfirst dispatcheserc20_update_approvalwith origin =fromand spender = caller, then transfers (lines 336-358).- Allowances set through the precompile are not visible in
pallet-assets' own approvals. - Amounts above u128 saturate to
u128::MAX(saturated_into, lines 254, 285).
- Allowances set through the precompile are not visible in
- Numbers are native decimals. ROOT, XRP and VTX have 6 decimals (live:
decimals()= 6). Bridged assets keep their own decimals: asset 1124 "ETH" has 18.- This differs from the EVM-native XRP view.
msg.valueandeth_getBalanceare 18-dp wei. XRP via0xCCCC…0002is 6-dp. Live: account0x4346…200chaseth_getBalance= 814956000000000000 wei andXRP.balanceOf= 814956.
- This differs from the EVM-native XRP view.
- Events are emitted only when the precompile is called. Substrate-side transfers, mints, bridge deposits, DEX swaps and fee payments emit no ERC-20
Transferlog. See §13. totalSupplyistotal_issuance(lines 177-191). Live values:
| Asset | totalSupply | Human-readable |
|---|---|---|
| ROOT | 12,000,000,000,000,000 | 12B ROOT |
| XRP | 424,764,270,722 | ≈424.7k XRP |
| VTX | 1,272,230,135,858 |
Fees
Gas = dispatch weight converted with GasWeightMapping, plus DB read/write and log costs (RuntimeHelper::try_dispatch, utils/src/substrate.rs:72-102). Gas is paid in XRP, or in another asset via the fee proxy (§12).
Use cases
- Treat ROOT, XRP, VTX and any bridged or created asset as an ordinary ERC-20 from Solidity, ethers or viem.
- Approve the DEX precompile or a custom contract (escrow, staking, payments) to pull tokens.
- Read balances and metadata for wallets.
- XRP as an ERC-20 is useful when a contract wants 6-dp accounting instead of
msg.value.
5. ERC-721 collection precompile: 0xAAAAAAAA[collectionUuid]000000000000000000000000
Source: evm-precompiles/erc721/src/lib.rs. It fronts pallet-nft. Approvals use pallet-token-approvals and XLS-20 uses pallet-xls20. Token id = the native SerialNumber (u32). Any tokenId > 2^32-1 reverts with "ERC721: Expected token id <= 2^32" (e.g. lines 317-322).
Interface (code-exact)
Selectors are from the Action enum, lines 87-130.
solidity
interface ITrnErc721 {
// ERC721
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); // 0xddf252ad…
event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId); // 0x8c5be1e5…
event ApprovalForAll(address indexed owner, address indexed operator, bool approved); // 0x17307eab…
function balanceOf(address owner) external view returns (uint256); // 0x70a08231
function ownerOf(uint256 tokenId) external view returns (address); // 0x6352211e
function transferFrom(address from, address to, uint256 tokenId) external; // 0x23b872dd
function safeTransferFrom(address from, address to, uint256 tokenId) external; // 0x42842e0e
function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external; // 0xb88d4fde
function approve(address to, uint256 tokenId) external; // 0x095ea7b3
function getApproved(uint256 tokenId) external view returns (address); // 0x081812fc
function setApprovalForAll(address operator, bool approved) external; // 0xa22cb465
function isApprovedForAll(address owner, address operator) external view returns (bool); // 0xe985e9c5
// Metadata
function name() external view returns (string memory); // 0x06fdde03
function symbol() external view returns (string memory); // 0x95d89b41
function tokenURI(uint256 tokenId) external view returns (string memory); // 0xc87b56dd
// Burnable
function burn(uint256 tokenId) external; // 0x42966c68
// Ownable (collection owner)
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); // 0x8be0079c…
function owner() external view returns (address); // 0x8da5cb5b
function renounceOwnership() external; // 0x715018a6 (returns bool true)
function transferOwnership(address newOwner) external; // 0xf2fde38b (returns bool true)
// TRN extensions
event MaxSupplyUpdated(uint32 maxSupply); // 0x3d50f4a7…
event BaseURIUpdated(string baseURI); // 0x6741b2fc…
event PublicMintToggled(bool enabled); // 0xa81e445d…
event MintFeeUpdated(address indexed paymentAsset, uint128 indexed mintFee); // 0x9875cf5c…
function totalSupply() external view returns (uint256); // 0x18160ddd
function mint(address to, uint32 quantity) external; // 0xd35e29d7
function setMaxSupply(uint32 maxSupply) external; // 0xf9da3224
function setBaseURI(bytes calldata baseURI) external; // 0x55e42958
function ownedTokens(address who, uint16 limit, uint32 cursor)
external view returns (uint32 nextCursor, uint32 totalOwned, uint32[] memory ids); // 0x048ed3f9
function togglePublicMint(bool enabled) external; // 0x9b7c1c83
function setMintFee(address paymentAsset, uint128 mintFee) external; // 0x6d5c3a09
// XLS-20 (XRPL NFT mirroring)
event XLS20CompatibilityEnabled(); // 0x2a05b5c4…
event XLS20MintReRequested(uint256 tokenId /* abi-encoded (uint32,uint32) */); // 0xc2c815cc…
function enableXls20Compatibility() external returns (bool); // 0x1d3f113a
function reRequestXls20Mint(uint32[] calldata serialNumbers) external returns (bool); // 0x5d18f82d
// ERC165
function supportsInterface(bytes4) external view returns (bool); // 0x01ffc9a7
// ERC-5484 soulbound
event PendingIssuanceCreated(/* see note */); // 0x2d4f5089… (!)
event Issued(address indexed from, address indexed to, uint256 indexed tokenId, uint8 burnAuth); // 0x135ffb33…
function issueSoulbound(address to, uint32 quantity, uint8 burnAuth) external; // 0x3e572a7b
function acceptSouldboundIssuance(uint32 issuanceId) external; // 0x23d44a0d (sic: "Souldbound")
function pendingIssuances(address owner)
external view returns (uint256[] memory issuanceIds, (uint256 quantity, uint8 burnAuth)[] memory); // 0x78843fa1
function burnAuth(uint256 tokenId) external view returns (uint8); // 0x0489b56f
}ERC165 ids returned true (erc721/src/lib.rs:77-84): 0x01ffc9a7 ERC165, 0x80ac58cd ERC721, 0x5b5e139f ERC721Metadata, 0x42966c68 ERC721Burnable, 0x0e083076 Ownable, 0x2a4288ec TRN721. There is no ERC721Enumerable (0x780e9d63). Use ownedTokens for enumeration.
Permissions
transferFrom,safeTransferFromandburnrequire the caller to be owner or approved, checked viatoken_approvals::is_approved_or_owner(erc721/src/lib.rs:385-391, 476-482, 1076-1082).- The pallet call is then dispatched with origin =
from(transfer) or origin = token owner (burn), at lines 395-403 and 1090-1094. An approved operator can therefore burn.
- The pallet call is then dispatched with origin =
mint: the caller must be the collection owner, unless public mint is enabled. In that case anyone can mint and the mint fee is charged (pallet/nft/src/lib.rs:611-617).setMaxSupply,setBaseURI,togglePublicMint,setMintFee,transferOwnership,renounceOwnership,enableXls20Compatibility,reRequestXls20MintandissueSoulboundare collection-owner only. The pallet enforces this withNotCollectionOwner.setMaxSupplycan be set once only, to a non-zero value (pallet/nft/src/lib.rs:434-437).
renounceOwnershipsets the owner to0x000…0. This is irreversible (erc721/src/lib.rs:999-1030).- Tokens locked by a marketplace listing, or soulbound tokens, cannot be transferred. The pallet rejects the transfer.
Notable limitations and quirks (from code)
symbol()returns the collection name. There is a// TODO: Returns same as nameaterc721/src/lib.rs:690-706. Live: collection 1124 returnsname=symbol= "bridged-collection".tokenURIdoes not revert for nonexistent tokens. It just formatsbaseURI/serial(lines 708-734). Live:tokenURI(0)on collection 1124 returnedethereum://0x6bCa…7182/0, whileownerOf(0)reverted "ERC721: Token does not exist".safeTransferFromreceiver check is based onAccountCodes(to)(lines 485-514). It skips the hook for addresses without code: EOAs, FuturePasses, and fixed precompiles. It also calls the hook on asset or collection precompile addresses, because they have dummy code, which would then fail.setMintFee(address,uint128)usesuint128in its selector. The public TRN docs showsetMintFee(address,uint256), which is a different selector. AmintFeeof 0 clears the fee (lines 938-981).- ERC-5484 selector typo. The runtime matches
acceptSouldboundIssuance(uint32)→0x23d44a0d(line 125). The docs and e2e ABIs useacceptSoulboundIssuance(uint32)→0xbe23b21b, which does not exist on chain. Callers must use the misspelled name. PendingIssuanceCreatedtopic is computed fromPendingIssuanceCreated(address,uint256,u8).u8is not a Solidity type (line 69), so the topic is0x2d4f5089…. An ABI declaring(address indexed to, uint256 issuanceId, uint256 quantity, uint8 burnAuth)would compute0x3282756e…and never match. The data actually carries(uint256 issuanceId, uint256 quantity, uint8 burnAuth).burnAuth()returns 0 for tokens with no burn authority. The comment says "default to TokenOwner", but in the enum0 = CollectionOwner,1 = TokenOwner,2 = Both,3 = Neither(lines 1329-1353;pallet/common/src/utils.rs:94-117). For ordinary non-soulbound tokens,burnAuth()==0is misleading.- Function modifier gaps.
burn,issueSoulboundandacceptSouldboundIssuanceare not in the NonPayable list, so they fall through toView(lines 182-199). Effects:- They still reject
msg.value. - They are not rejected inside a
STATICCALL.try_dispatchhas no static check (utils/src/substrate.rs), so state can change under a static context. This is inferred from code and was not exercised on chain.
- They still reject
- Mint logs.
mintemits oneTransfer(0x0,to,id)per token, ERC721A-style (lines 794-805). Nativenft.mintextrinsics emit no EVM logs (§13). transferFrom/approvereturn no data, which is fine per ERC-721.renounceOwnershipandtransferOwnershipreturnbool true.
Use cases
- NFT dapps and marketplaces built on standard ERC-721 tooling (OpenSea-style approvals, ethers/viem).
- Collection admin from a contract: mint drops via
mint, public mint with a fee in any asset viatogglePublicMint+setMintFee,setBaseURI, and ownership handover. - Paginated wallet inventory via
ownedTokens(who, limit, cursor). - Soulbound credentials via
issueSoulboundandacceptSouldboundIssuance. - Mirroring collections to XRPL via XLS-20.
6. ERC-1155 collection precompile: 0xBBBBBBBB[collectionUuid]000000000000000000000000
Source: evm-precompiles/erc1155/src/lib.rs. It fronts pallet-sft. Approvals use pallet-token-approvals (ERC1155ApprovalsForAll). id = u32 serial number within the collection; amount = u128.
Interface (code-exact)
Selectors are from lines 96-139.
solidity
interface ITrnErc1155 {
event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value); // 0xc3d58168…
event TransferBatch(address indexed operator, address indexed from, address indexed to, uint256[] ids, uint256[] values); // 0x4a39dc06…
event ApprovalForAll(address indexed account, address indexed operator, bool approved); // 0x17307eab…
function balanceOf(address owner, uint256 id) external view returns (uint256); // 0x00fdd58e
function balanceOfBatch(address[] calldata owners, uint256[] calldata ids) external view returns (uint256[] memory); // 0x4e1273f4
function setApprovalForAll(address operator, bool approved) external; // 0xa22cb465
function isApprovedForAll(address account, address operator) external view returns (bool); // 0xe985e9c5
function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes calldata data) external; // 0xf242432a
function safeBatchTransferFrom(address from, address to, uint256[] calldata ids, uint256[] calldata amounts, bytes calldata data) external; // 0x2eb2c2d6
// Burnable
function burn(address account, uint256 id, uint256 value) external; // 0xf5298aca
function burnBatch(address account, uint256[] calldata ids, uint256[] calldata values) external; // 0x6b20c454
// Supply
function totalSupply(uint256 id) external view returns (uint256); // 0xbd85b039
function exists(uint256 id) external view returns (bool); // 0x4f558e79
// MetadataURI
function uri(uint256 id) external view returns (string memory); // 0x0e89341c
// Ownable
function owner() external view returns (address); // 0x8da5cb5b
function renounceOwnership() external; // 0x715018a6
function transferOwnership(address newOwner) external; // 0xf2fde38b
// TRN extensions
event TokenCreated(uint32 indexed serialNumber); // 0x1bc01cfa…
event MaxSupplyUpdated(uint128 indexed maxSupply); // 0x22019607…
event BaseURIUpdated(string baseURI); // 0x6741b2fc…
event PublicMintToggled(uint32 indexed id, bool enabled); // 0xb1ad4a13…
event MintFeeUpdated(uint32 indexed id, address indexed paymentAsset, uint128 indexed mintFee); // 0xaa9c45dd…
function createToken(bytes calldata name, uint128 initialIssuance, uint128 maxIssuance, address tokenOwner) external returns (uint32); // 0x6d658031
function mint(address to, uint256 id, uint256 amount) external; // 0x156e29f6
function mintBatch(address to, uint256[] calldata ids, uint256[] calldata amounts) external; // 0xd81d0a15
function setMaxSupply(uint256 id, uint32 maxSupply) external; // 0x83f6e6e5
function setBaseURI(bytes calldata baseURI) external; // 0x55e42958
function togglePublicMint(uint256 id, bool enabled) external; // 0x2d141bd7
function setMintFee(uint256 id, address paymentAsset, uint128 mintFee) external; // 0x66c7fdfb
function supportsInterface(bytes4) external view returns (bool); // 0x01ffc9a7
// ERC-5484 soulbound
event PendingIssuanceCreated(address indexed to, uint256 issuanceId, uint256[] tokenIds, uint256[] balances); // 0x231130dd…
event Issued(address indexed from, address indexed to, uint256 indexed tokenId, uint8 burnAuth); // 0x135ffb33…
function setBurnAuth(uint256 id, uint8 burnAuth) external; // 0xef4595f0
function issueSoulbound(address to, uint256[] calldata ids, uint256[] calldata amounts) external; // 0x963c3e39
function acceptSouldboundIssuance(uint32 issuanceId) external; // 0x23d44a0d (sic)
function pendingIssuances(address owner) external view
returns (uint256[] memory issuanceIds, (uint256[] ids, uint256[] balances, uint8[] burnAuths)[] memory); // 0x78843fa1
function burnAsCollectionOwner(address account, uint256[] calldata ids, uint256[] calldata amounts) external; // 0xd0d44de6
function burnAuth(uint256 id) external view returns (uint8); // 0x0489b56f
}ERC165 ids (lines 85-93): 0x01ffc9a7, 0xd9b67a26 ERC1155, 0x9e094e9e Burnable, 0x0e89341c MetadataURI, 0xf2d03e40 Supply, 0x0e083076 Ownable, 0xf0f03f65 TRN1155.
Permissions
- Transfers and burns:
fromoraccountmust be the caller, or the caller must beisApprovedForAll(from, caller)(erc1155/src/lib.rs:617-625, 663-672). There is no per-token approval in ERC-1155. The pallet call is dispatched with origin =from. transfer to 0x0is rejected (line 614).createToken: the caller must be the collection owner. It callspallet_sft::do_create_token(caller, …)directly (lines 892-948).maxIssuance = 0means unlimited.tokenOwner = 0x0means no per-token owner. It returns the new serial (id).mintandmintBatch: the token owner or collection owner, or anyone if public mint is enabled for that id (pallet-sft).setMaxSupplyandsetMintFeeare per id.burnAsCollectionOwner: the collection owner burns fromaccount, subject to the token's burn authority.- Batches are capped at
MaxSerialsPerMint = 1000(runtime/src/lib.rs:544).
Quirks
- The receiver hook runs before the transfer.
safeTransferFromandsafeBatchTransferFromcallonERC1155Received/onERC1155BatchReceivedbefore moving balances (line 428 calls the check, thendo_safe_transferat 437). The hook is skipped for code-lessto, same rule as ERC-721.- A receiver that inspects its own balance inside the hook will not see the incoming tokens yet.
acceptSouldboundIssuancehas the same misspelled selector (line 133).- Wider modifier gaps. Besides the soulbound functions,
createToken,setMaxSupply,setBaseURI,transferOwnership,renounceOwnership,setBurnAuthandburnAsCollectionOwnerare typedView(lines 187-199). They are non-payable but not static-protected. createTokengas. It bypassestry_dispatch, so only log costs are recorded explicitly (lines 892-897).uri(id)= collection base URI + serial. Live: SFT 18532uri(1)=https://metadata.futureverse.app/pb-mouth/nft/1andtotalSupply(1)= 234.
Use cases
- Game items and editions: one 1155 per collection with many token ids.
- Batch airdrops (
mintBatch) and per-id public mint with a fee. - Burn-to-redeem via
burn/burnAsCollectionOwner. - Soulbound badges via
issueSoulbound+setBurnAuth.
7. NFT collection factory: 0x00000000000000000000000000000000000006B9
Source: evm-precompiles/nft/src/lib.rs. It fronts pallet_nft::do_create_collection.
solidity
interface ITrnNftFactory {
event InitializeCollection(address indexed collectionOwner, address precompileAddress); // 0xa8db0f73…
function initializeCollection(
address owner, bytes calldata name, uint32 maxIssuance, bytes calldata metadataPath,
address[] calldata royaltyAddresses, uint32[] calldata royaltyEntitlements
) external returns (address collectionAddress, uint32 collectionId); // 0xfe965bf7
}owneris arbitrary. The caller can create a collection owned by any address (nft/src/lib.rs:104-205). Initial issuance is 0, and the origin chain isRoot.- Arguments:
maxIssuance = 0means unlimited (lines 132-140).metadataPathis parsed into aMetadataScheme(URI prefix) (lines 142-146).namemust be ≤ 50 bytes (CollectionNameStringLimit,runtime/src/lib.rs:464).- Royalties:
royaltyEntitlementsare Permill parts: 1_000_000 = 100%, 25_000 = 2.5% (line 152). There are at mostMAX_COLLECTION_ENTITLEMENTSentries (line 160).
- Gas = benchmarked
create_collectionweight (lines 176-178) plus 7 log slots. - Returns
(0xAAAAAAAA… address, uuid). The event carries the address in data, not topics (log2, lines 199-205).
Use case: a launchpad or factory contract deploys native NFT collections that are immediately ERC-721-addressable. No Solidity NFT contract is deployed. The collection gets native marketplace, royalties and bridge (nft-peg, XLS-20) support.
8. SFT collection factory: 0x00000000000000000000000000000000000006C3
Source: evm-precompiles/sft/src/lib.rs. It fronts pallet_sft::do_create_collection.
solidity
interface ITrnSftFactory {
event InitializeSftCollection(address indexed collectionOwner, address indexed precompileAddress); // 0x1314f5e2…
function initializeCollection(address owner, bytes calldata name, bytes calldata metadataPath,
address[] calldata royaltyAddresses, uint32[] calldata royaltyEntitlements)
external returns (address collectionAddress, uint32 collectionId); // 0x7e64e914
}Same rules as §7: arbitrary owner, Permill royalties, no max issuance (that is per token id via createToken). Here both addresses are indexed (log3).
Use case: create an ERC-1155 collection, then call createToken on 0xBBBBBBBB… per item type.
9. Marketplace: 0x00000000000000000000000000000000000006CD
Source: evm-precompiles/marketplace/src/lib.rs. It fronts pallet-marketplace. Payment is always a native asset referenced by its ERC-20 precompile address. Funds are pulled from the caller's native balance. No msg.value: every function is NonPayable or View (lines 159-182).
solidity
interface ITrnMarketplace {
function registerMarketplace(address marketplaceAccount, uint256 entitlement) external returns (uint256 marketplaceId); // 0xb170c834
function sellNft(address collection, uint256[] calldata serials, address buyer, address paymentAsset,
uint256 fixedPrice, uint256 duration, uint32 marketplaceId) external returns (uint256 listingId); // 0x9b3808b2
function sellNftWithMarketplaceId(/* same args */) external returns (uint256); // 0x580ae278 (deprecated alias)
function sellSft(address collection, uint256[] calldata serials, uint256[] calldata quantities, address buyer,
address paymentAsset, uint256 fixedPrice, uint256 duration, uint32 marketplaceId) external returns (uint256); // 0x8ab40b96
function updateFixedPrice(uint128 listingId, uint256 newPrice) external; // 0xf14f8c36
function buy(uint128 listingId) external; // 0x58db52a2
function auctionNft(address collection, uint256[] calldata serials, address paymentAsset,
uint256 reservePrice, uint256 duration, uint32 marketplaceId) external; // 0x935d6b70
function auctionNftWithMarketplaceId(/* same */) external; // 0xed028c37 (deprecated alias)
function auctionSft(address collection, uint256[] calldata serials, uint256[] calldata quantities,
address paymentAsset, uint256 reservePrice, uint256 duration, uint32 marketplaceId) external; // 0x70ae1517
function bid(uint128 listingId, uint256 amount) external; // 0x5473427b
function cancelSale(uint128 listingId) external; // 0x70f3ab39
function makeSimpleOffer(address collection, uint32 serial, uint256 amount, address paymentAsset, uint32 marketplaceId)
external returns (uint256 offerId); // 0xf2800f37
function makeSimpleOfferWithMarketplaceId(/* same */) external returns (uint256); // 0xa9438074 (deprecated alias)
function cancelOffer(uint64 offerId) external; // 0x66948833
function acceptOffer(uint64 offerId) external; // 0xf23bc4f8
function getMarketplaceAccount(uint32 marketplaceId) external view returns (address); // 0x21622752
function getListingFromId(uint128 listingId) external view
returns (bytes memory listingType, uint32 collectionId, uint32[] memory serials, uint128 price, uint32 paymentAssetId); // 0xaa64ebdb
function getOfferFromId(uint64 offerId) external view
returns (uint32 collectionId, uint32 serial, uint128 amount, address buyer); // 0x4f22f7f2
event MarketplaceRegister(address indexed sender, uint256 indexed marketplaceId, address marketplaceAccount); // 0x65630e2d…
event FixedPriceSaleListNFT(address indexed seller, uint256 indexed listingId, uint256 indexed fixedPrice, uint256[] serialNumbers, address collectionAddress, uint128 marketplaceId); // 0x5fc6c57c…
event FixedPriceSaleListSFT(address indexed seller, uint256 indexed listingId, uint256 indexed fixedPrice, uint256[] serialNumbers, address collectionAddress, uint128 marketplaceId, uint256[] quantities); // 0xa3254d4d…
event FixedPriceSaleUpdate(uint256 indexed collectionId, uint256 indexed listingId, uint256 indexed newPrice, address sender, uint256[] serialNumbers, uint128 marketplaceId); // 0x5aa5a181…
event FixedPriceSaleComplete(uint256 indexed collectionId, uint256 indexed listingId, uint256 indexed fixedPrice, address seller, uint256[] serialNumbers, uint128 marketplaceId); // 0xee2ae890…
event AuctionOpenNFT(uint256 indexed collectionId, uint256 indexed listingId, uint256 indexed reservePrice, address sender, uint256[] serialNumbers, uint128 marketplaceId); // 0x2596edae…
event AuctionOpenSFT(/* same shape */); // 0x167f4126…
event Bid(address indexed bidder, uint256 indexed listingId, uint256 indexed amount, uint128 marketplaceId); // 0x98a1579f…
event FixedPriceSaleClose(uint256 indexed collectionId, uint256 indexed listingId, address sender, uint256[] serialNumbers, uint128 marketplaceId); // 0xc589bcad…
event AuctionClose(/* same as above */); // 0x2a5f6791…
event Offer(uint256 indexed offerId, address indexed sender, uint256 collectionId, uint256 seriesId, uint128 marketplaceId); // 0x0814d08b…
event OfferCancel(uint256 indexed offerId, address indexed sender, uint256 collectionId, uint256 seriesId, uint128 marketplaceId); // 0xc7650801…
event OfferAccept(uint256 indexed offerId, uint256 indexed amount, address indexed sender, uint256 collectionId, uint256 seriesId, uint128 marketplaceId); // 0x27a679b3…
}Notes
- Use
uint256types in your ABI. The selectors useuint256for serials, prices and durations (lines 104-135). The e2e test ABI inseed/e2e/common/index.tsdeclaresuint32[]/uint128for these. Those signatures hash to different selectors, so build your ABI from the strings above. - Argument semantics:
marketplaceId = 0means none.buyer = 0x0means an open sale.duration = 0means the default of 3 days (DefaultListingDuration,runtime/src/lib.rs:515). Durations are in blocks.entitlementinregisterMarketplaceis Permill parts (lines 229-282).marketplaceAccount = 0x0means the caller.
- Fees:
- A protocol network fee of 0.5% (
MarketplaceNetworkFeePercentage = from_perthousand(5),runtime/src/lib.rs:519) goes to the tx-fee pot. - Collection royalties come from
initializeCollection. - The marketplace entitlement is taken if a
marketplaceIdis set.
- A protocol network fee of 0.5% (
- Limits: at most 1000 tokens per listing (
MaxTokensPerListing,runtime/src/lib.rs:516). At most 100 offers per token (MaxOffers). - Asset types:
sellNft,auctionNftandmakeSimpleOfferrequire a0xAAAAAAAA(721) collection address.sellSftandauctionSftrequire0xBBBBBBBB(lines 327, 433, 648, 754, 961).- Offers are NFT-only.
getListingFromIdlabels: it returns the ASCII label"fixed_price_listing_for_nft"or"auction_listing_for_nft"even for SFT listings, and drops SFT quantities (lines 1107-1145, 1172-1194).- Events: a sale settled via
buyemits onlyFixedPriceSaleCompletefrom0x…06CD. No ERC-721Transferlog is emitted from the collection address, and no ERC-20Transferlog for the payment (lines 559-607). - Live check:
getMarketplaceAccount(1)=0x4346921602bcd591e13623e390ca06de52e0200c.
Use case: a dapp UI or a contract (aggregator, auto-lister, offer bot) lists, buys, bids on and makes offers for native NFTs and SFTs, paying in any native asset, with royalties enforced by the runtime.
10. Peg (bridge withdrawals to Ethereum): 0x0000000000000000000000000000000000000793
Source: evm-precompiles/peg/src/lib.rs. It fronts pallet-erc20-peg and pallet-nft-peg.
solidity
interface ITrnPeg {
event Erc20Withdrawal(uint64 indexed eventProofId, address indexed beneficiary, address indexed tokenAddress, uint128 balance); // 0xd7134fc1…
event Erc721Withdrawal(uint64 indexed eventProofId, address indexed beneficiary, address indexed tokenAddress, uint32[] serialNumbers); // 0xec39c66d…
function erc20Withdraw(address beneficiary, address asset, uint128 amount) external returns (uint64 eventProofId); // 0x6c4f1a68
function erc721Withdraw(address beneficiary, address[] calldata collections, uint32[][] calldata serials) external returns (uint64 eventProofId); // 0x39ab0a42
}erc20Withdrawburns or locks the caller's bridged asset and emits an Ethy event proof. The beneficiary claims it on the EthereumERC20Pegcontract.assetis the0xCCCCCCCCaddress. ROOT is transferred to the peg account and all other assets are burned (pallet/erc20-peg/src/lib.rs:570-580).- It fails if withdrawals are paused (
WithdrawalsPaused, line 529) or the asset is unsupported. - It fails if the asset has a payment delay configured and
amount ≥ min. EVM-origin delayed withdrawals are not supported, and the precompile reverts withEvmWithdrawalFailed(erc20-peg/src/lib.rs:540-558; precompilepeg/src/lib.rs:140-146).
- It fails if withdrawals are paused (
erc721Withdrawonly works for collections bridged from Ethereum. Collections must be0xAAAAAAAAaddresses, and the nft-peg pallet checksNoPermissionToBridge(pallet/nft-peg/src/lib.rs:408-426). There are per-call caps (MaxCollectionsPerWithdraw,MaxSerialsPerWithdraw).- Gas = benchmarked
withdrawweight. Bridge fees, if any, are charged by the pallet.
Use case: a "withdraw to Ethereum" button in an EVM-wallet dapp, or a contract that redeems bridged ETH/USDC/NFTs to L1.
11. DEX router: 0x000000000000000000000000000000000000DDDD
Source: evm-precompiles/dex/src/lib.rs. It fronts pallet-dex, a Uniswap-V2-style constant-product AMM with native LP tokens. The function names and signatures equal IUniswapV2Router01. Tokens are 0xCCCCCCCC addresses. "ETH" means XRP, asset 2 (GAS_TOKEN_ID, line 30).
solidity
interface ITrnDex /* IUniswapV2Router01 subset + IUniswapV2Pair events */ {
event Mint(address indexed sender, uint256 amount0, uint256 amount1); // 0x4c209b5f…
event Burn(address indexed sender, uint256 amount0, uint256 amount1, address indexed to); // 0xdccd412f…
event Swap(address indexed sender, uint256 amount0In, uint256 amount1In, uint256 amount0Out, uint256 amount1Out, address indexed to); // 0xd78ad95f…
function addLiquidity(address tokenA, address tokenB, uint256 amountADesired, uint256 amountBDesired, uint256 amountAMin, uint256 amountBMin, address to, uint256 deadline) external returns (uint256, uint256, uint256); // 0xe8e33700
function addLiquidityETH(address token, uint256 amountTokenDesired, uint256 amountTokenMin, uint256 amountETHMin, address to, uint256 deadline) external payable returns (uint256, uint256, uint256); // 0xf305d719
function removeLiquidity(address tokenA, address tokenB, uint256 liquidity, uint256 amountAMin, uint256 amountBMin, address to, uint256 deadline) external returns (uint256, uint256); // 0xbaa2abde
function removeLiquidityETH(address token, uint256 liquidity, uint256 amountTokenMin, uint256 amountETHMin, address to, uint256 deadline) external returns (uint256, uint256); // 0x02751cec
function swapExactTokensForTokens(uint256 amountIn, uint256 amountOutMin, address[] calldata path, address to, uint256 deadline) external returns (uint256[] memory); // 0x38ed1739
function swapTokensForExactTokens(uint256 amountOut, uint256 amountInMax, address[] calldata path, address to, uint256 deadline) external returns (uint256[] memory); // 0x8803dbee
function swapExactETHForTokens(uint256 amountOutMin, address[] calldata path, address to, uint256 deadline) external payable returns (uint256[] memory); // 0x7ff36ab5
function swapTokensForExactETH(uint256 amountOut, uint256 amountInMax, address[] calldata path, address to, uint256 deadline) external returns (uint256[] memory); // 0x4a25d94a
function swapExactTokensForETH(uint256 amountIn, uint256 amountOutMin, address[] calldata path, address to, uint256 deadline) external returns (uint256[] memory); // 0x18cbafe5
function swapETHForExactTokens(uint256 amountOut, address[] calldata path, address to, uint256 deadline) external payable returns (uint256[] memory); // 0xfb3bdb41
function quote(uint256 amountA, uint256 reserveA, uint256 reserveB) external view returns (uint256); // 0xad615dec
function getAmountOut(uint256 amountIn, uint256 reserveIn, uint256 reserveOut) external view returns (uint256); // 0x054d50d4
function getAmountIn(uint256 amountOut, uint256 reserveIn, uint256 reserveOut) external view returns (uint256); // 0x85f8c259
function getAmountsOut(uint256 amountIn, address[] calldata path) external view returns (uint256[] memory); // 0xd06ca61f
function getAmountsIn(uint256 amountOut, address[] calldata path) external view returns (uint256[] memory); // 0x1f00ca74
}Differences from Uniswap
- No ERC-20 approval of the router is needed. The precompile calls
pallet_dex::do_*with the caller's native account directly (e.g. lines 197-208). deadlineis a block number, not a unix timestamp. It is converted toBlockNumber(u32) and must be ≤ 2^32 (lines 51-55, 206). Passingtype(uint256).maxreverts.- The pair is a virtual pool account, not a deployed contract. Pair events are emitted from the pool address:
0xDDDDDDDD ‖ u32(min asset) ‖ u32(max asset) ‖ 8 zero bytes(lines 210-221; helper inseed/e2e/common/index.ts). There is nofactory(),WETH()or pair contract. LP tokens are native assets, addressable via0xCCCCCCCC[lpAssetId]. Look up the LP asset id withdex.tradingPairLPTokenon the Substrate side. - All amounts are native decimals, 6 for XRP and ROOT, and must fit in u128.
*ETHpayable variants usemsg.valueas an XRP amount.addLiquidityETH,swapExactETHForTokensandswapETHForExactTokensreadhandle.context().apparent_valueand pass it unscaled as the XRP (asset 2) amount pulled from the caller's native balance (lines 265, 589, 799).msg.valueis in 18-dp wei. The EVM's own value transfer to0x…DDDDis scaled to 6 dp (runtime/src/impls.rs:80-219).- The two amounts are therefore inconsistent by 10^12 on mainnet. The e2e test only passes because it minted XRP with 18-dp amounts (
e2e/test/Dex/DexPrecompile.test.ts:67-69). - This is a reading of the code, not verified with a transaction. Treat the
*ETH*payable functions as unsafe or unusable on mainnet. Use the token variants with the XRP ERC-20 address0xCCCC…0002inpath. removeLiquidityETH,swapTokensForExactETHandswapExactTokensForETHare non-payable and simply use asset 2 as the other leg.
- Live:
getAmountsOut(1_000_000, [ROOT, XRP])returned[1000000, 34], i.e. 1 ROOT → 0.000034 XRP at the time of the check.
Use case: in-contract swaps and liquidity provisioning against the native AMM, price quotes for UIs, and converting tokens to XRP for gas. The fee proxy (§12) uses the same pools.
12. Fee proxy: 0x00000000000000000000000000000000000004BB
This is not in the precompile set. The custom EVM Runner (FeePreferencesRunner, runtime/src/lib.rs:1235) intercepts top-level transactions whose to is 0x…04BB (pallet/fee-proxy/src/runner.rs:312-366).
solidity
interface ITrnFeeProxy {
function callWithFeePreferences(address paymentAsset, address target, bytes calldata input) external; // 0xf6098286
// deprecated, still accepted: callWithFeePreferences(address,uint128,address,bytes) → 0x255a3432 (maxPayment ignored)
}How it works
- The runner decodes the args (
runner.rs:140-188). It computes the max fee fromgasLimit × (baseFee + priorityFee). - It swaps just enough
paymentAsset→ XRP throughpallet-dex(do_swap_with_exact_supply). - It rewrites
to/inputtotarget/inputand runs the call normally.
msg.sender in target is still the EOA.
Limitations
- Only works as the transaction's top-level
to. A contractCALLto0x…04BBhits an empty account and silently succeeds with empty return. paymentAssetmust have a DEX path to XRP.- The TRN docs still show the deprecated 4-arg signature (
trn-docs/…/feeproxy.md).
Use case: let users pay gas in ROOT, USDC, SYLO and so on, with no XRP needed, from MetaMask.
13. FuturePass
13.1 Registrar: 0x000000000000000000000000000000000000FFFF
Source: evm-precompiles/futurepass-registrar/src/lib.rs. It fronts pallet-futurepass.
solidity
interface IFuturePassRegistrar {
event FuturepassCreated(address indexed futurepass, address owner); // 0xf0c26223…
function futurepassOf(address owner) external view returns (address); // 0x4dde5a44 (0x0 if none)
function create(address owner) external returns (address futurepass); // 0x9ed93318
}- Anyone can create a FuturePass for any
owner, but only one per owner (AccountAlreadyRegistered,pallet/futurepass/src/lib.rs:555). - The caller is the funder. It pays the
pallet-proxydeposit in ROOT (Balances), transferring any shortfall to the new FP account (runtime/src/impls.rs:628-663). The deposit is:ProxyDepositBase148 +ProxyDepositFactor126 per delegate, in ROOT base units (6 dp).- Total 274 = 0.000274 ROOT for a new FP, plus ED 1 (
runtime/src/lib.rs:1372-1374,constants.rs:68-71).
- The owner is added as delegate with
ProxyType::Owner (255)(futurepass/src/lib.rs:558). - Live:
futurepassOf(0xf4a4…0942)=0xFFFFFFFF00000000000000000000000000000001.
13.2 FuturePass account: 0xFFFFFFFF[16-byte id]
Source: evm-precompiles/futurepass/src/lib.rs. It fronts pallet-futurepass and pallet-proxy.
solidity
interface IFuturePass /* + Ownable */ {
event FuturepassDelegateRegistered(address indexed futurepass, address indexed delegate, uint8 proxyType); // 0x6baa10ae…
event FuturepassDelegateUnregistered(address indexed futurepass, address delegate); // 0x35dca1bf…
event Executed(uint8 indexed callType, address indexed target, uint256 indexed value, bytes4 data); // 0x947255a7…
event ContractCreated(uint8 indexed callType, address indexed contractAddress, uint256 indexed value, bytes32 salt); // 0xadcaa89e…
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); // 0x8be0079c…
function delegateType(address delegate) external view returns (uint8); // 0x2e51ee70
function registerDelegateWithSignature(address delegate, uint8 proxyType, uint32 deadline, bytes calldata signature) external; // 0xfb243a5c
function unregisterDelegate(address delegate) external; // 0x8376e5ae
function proxyCall(uint8 callType, address callTo, uint256 value, bytes calldata callData) external payable; // 0xc579191c
function owner() external view returns (address); // 0x8da5cb5b
function renounceOwnership() external; // 0x715018a6
function transferOwnership(address newOwner) external; // 0xf2fde38b
receive() external payable; // empty calldata + value>0 → accepted (lines 149-165)
}Proxy types (runtime/src/impls.rs:745-753): 0 NoPermission, 1 Any (default), 2 NonTransfer, 3 Governance, 4 Staking, 255 Owner.
proxyCall flow (futurepass/src/lib.rs:300-583):
- The caller must be a registered proxy of this FP with zero delay ("Futurepass: Not proxy" / "Unannounced").
- The EVM proxy-call filter (
runtime/src/impls.rs:786-818) is applied to(callTo, callData):- If
callTois the registrar0x…FFFFor any0xFFFFFFFF…address, only the selectorsregisterDelegateWithSignature,unregisterDelegateandtransferOwnershipare allowed. Additionally, the caller must be the FP owner ("Futurepass: NotFuturepassOwner", lines 311-327). - Otherwise, only
ProxyType::OwnerandProxyType::Anymay proxy. NonTransfer, Governance, Staking and NoPermission are always rejected ("Futurepass: CallFiltered").
- If
- Execution as the FP, with
msg.sender = FP. Value is taken from the FP's balance, not frommsg.value.
callType | Operation | Notes |
|---|---|---|
| 0 | STATICCALL | |
| 1 | CALL | |
| 2 | DELEGATECALL | Unsupported ("call type not supported"; live-verified) |
| 3 | CREATE | Deploys from the FP |
| 4 | CREATE2 | The salt is the last 32 bytes of callData (ERC-725X convention, lines 501-508) |
Each operation emits Executed or ContractCreated. The sub-call's revert data is bubbled up.
Delegates and ownership:
registerDelegateWithSignature: the caller is the owner or the FP itself.- Only
proxyType == 1 (Any)is accepted in V1 (PermissionDenied,pallet/futurepass/src/lib.rs:291). - The delegate must sign
keccak256(abi.encodePacked(futurepass, delegate, uint8 proxyType, uint32 deadline))as an EIP-191 personal message over the hex string of that hash (lines 567-611). deadlineis a block number.- Max 32 proxies (
MaxProxies).
- Only
unregisterDelegate: the owner or FP can remove anyone except the owner. A delegate can remove itself (lines 353-388).transferOwnership(new)moves the FP to a new owner and removes all other delegates. The new owner must not already hold an FP.renounceOwnership()passesnew_owner = None(lines 412-440 and precompile lines 596-650).
Other limits and quirks:
- No function-modifier checks at all (lines 162-174).
proxyCallacceptsmsg.value, which is credited to the FP account. It is not forwarded automatically. - Call data ≤ 64 KiB (
CALL_DATA_LIMIT, line 103). - Input-validation issue in the FuturePass proxy-call filter. An input-handling bug in the filter for calls whose target is another FuturePass or the registrar has been withheld pending disclosure to the network's maintainers.
- In practice you cannot send plain XRP from one FuturePass to another via
proxyCall. Use the Substratefuturepass.proxyExtrinsic(balances/assets transfer)instead.
- In practice you cannot send plain XRP from one FuturePass to another via
- Live checks:
delegateType(owner)on FP #1 = 255.proxyCall(1, ROOT, 0, symbol())from the owner returned "ROOT".- From a non-delegate it reverted "Futurepass: Not proxy".
Use cases:
- Account abstraction for Futureverse: a FuturePass is a stable on-chain identity that holds NFTs and tokens.
- An EOA, or several delegate EOAs, act through it via
proxyCall. - Games add session keys as delegates using an off-chain signature.
- Contracts can be deployed from the FP (CREATE/CREATE2).
14. How native assets, NFTs and SFTs appear to EVM tools
Network and native currency
- MetaMask etc. see chainId 7668 with native currency XRP at 18 decimals.
- The runtime stores XRP at 6 dp and scales ×1e12 in and out. Sub-drop fractions are rounded up by one drop (
runtime/src/impls.rs:69-219; docsethereum-vs-trn.md). - Example:
eth_getBalance= XRPbalanceOf× 1e12. - Gas is priced in XRP. Live:
eth_gasPrice=0x13a52453c00≈ 1.35e12 wei.
ROOT and other assets
- These are not native to the EVM. Users must "Import token" with the ERC-20 address, e.g. ROOT
0xcCcCCccC00000001000000000000000000000000. MetaMask will readsymbol()/decimals()= ROOT/6. - XRP can also be imported as an ERC-20 (
0xCCCCcCCc00000002…, 6 dp). It is the same balance as the native coin. Wallets or portfolio trackers that do this will double-count. balanceOfis the spendable balance, so staked or locked ROOT does not appear.
Contract detection
- Asset, collection and SFT addresses return the 20-byte pseudo-code "TRN Asset Precompile". Explorers and
isContractchecks treat them as contracts, but there is no verifiable source or bytecode. - Fixed precompiles (
0x…06B9,0x…DDDD,0x…FFFF, …), FuturePass addresses and genesis assets lacking the hook (e.g. VTX0xcccccccc00000003…) return0xand look like EOAs.- A FuturePass therefore passes "is EOA" checks and receives
safeTransferFromwithout a receiver hook.
- A FuturePass therefore passes "is EOA" checks and receives
Events and indexing (the big gotcha)
- EVM logs are produced only when an action goes through a precompile. Native Substrate extrinsics produce no
eth_getLogsentries, including:- balances, assets and assets-ext transfers
- nft/sft mint, transfer and burn
- marketplace sales via extrinsic
- bridge deposits and withdrawals via extrinsic
- DEX swaps via extrinsic
- fee payments and staking
- Even precompile-mediated marketplace settlement emits only marketplace events, not
Transferevents from the token or collection address (§9). - An explorer or indexer that reconstructs balances or ownership from
Transferlogs (Etherscan-style token trackers, The Graph subgraphs onTransfer) will be incomplete. Indexers must read Substrate events, or pollbalanceOf/ownerOf. - Log authors:
- ERC-20, 721 and 1155 logs are emitted from the token or collection address.
- Factory, marketplace and peg logs are emitted from the fixed precompile address.
- DEX
Mint/Burn/Swaplogs are emitted from the virtual pool address0xDDDDDDDD….
NFT metadata
tokenURIanduriarebaseURI + serial, with no{id}substitution and no.jsonsuffix unless the base includes it. Values can beipfs://,https://orethereum://<contract>/<id>for Ethereum-bridged collections (live, collection 1124). Wallets may not resolveethereum://.symbol()equalsname()for ERC-721 collections.- There is no ERC721Enumerable. Explorers must use
ownedTokensor Substrate RPCs. totalSupply()on ERC-721 is the collection issuance. Live, collection 1124: 1324.
Token ids
- ERC-721 and ERC-1155 ids are the native u32 serials. The collection is encoded in the contract address, not the token id.
ERC-165
- ERC-165 is implemented on all three token precompiles. Marketplaces that probe
supportsInterface(0x80ac58cd)/0xd9b67a26will detect them correctly (live-verified on collection 1124 and ROOT/XRP).
15. Discrepancies: public docs / e2e ABIs vs runtime code (trust the code)
| Item | Docs / e2e say | Code (runtime 84) |
|---|---|---|
| Soulbound accept | acceptSoulboundIssuance(uint32) → 0xbe23b21b | acceptSouldboundIssuance(uint32) → 0x23d44a0d (erc721/src/lib.rs:125, erc1155/src/lib.rs:133) |
ERC-721 setMintFee | (address,uint256) | (address,uint128) → 0x6d5c3a09 (erc721/src/lib.rs:115) |
ERC-1155 setMintFee | (uint256,address,uint256) | (uint256,address,uint128) → 0x66c7fdfb |
ERC-721 PendingIssuanceCreated topic | keccak("…(address,uint256,uint256,uint8)") = 0x3282756e… | keccak("PendingIssuanceCreated(address,uint256,u8)") = 0x2d4f5089… (erc721/src/lib.rs:69) |
| FuturePass delegate add | registerDelegate(address,uint8) (docs) | only registerDelegateWithSignature(address,uint8,uint32,bytes) |
| Fee proxy | callWithFeePreferences(address,uint128,address,bytes) (docs) | preferred (address,address,bytes) 0xf6098286; old one still accepted |
| Marketplace (e2e ABI) | uint32[] serials, uint128 prices | uint256[] / uint256 in selectors (marketplace/src/lib.rs:104-135) |
ERC-721 burnAuth default | "TokenOwner" | returns 0 = CollectionOwner enum value |
| Marketplace, Peg | not in curated docs | present at 0x…06CD, 0x…0793 |
16. Quick reference: gas and fees
- All gas is paid in XRP at the EVM base fee, or swapped from another asset via the fee proxy.
- Precompiles charge one of two ways:
- (a)
try_dispatch: the dispatch weight converted to gas, refunded down to the actual weight (utils/src/substrate.rs:72-102). - (b) Explicit benchmarked weight for
do_*calls. Used by the NFT/SFT factories, DEX, marketplace, peg and registrar. - Both add DB-read surcharges and log costs.
- (a)
is_precompilecharges one extra DB read for every call to a0xCC…/0xAA…/0xBB…address, because it has to check the id exists.- Value-bearing economic fees, beyond gas:
- marketplace network fee 0.5%, plus royalties, plus marketplace entitlement
- NFT/SFT public-mint fees set via
setMintFee - FuturePass proxy deposits in ROOT
- DEX swap fee 0.3% (
GetExchangeFee = (3, 1000),runtime/src/lib.rs:779) - bridge fees in the peg pallets