Skip to content

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 ​

#PrecompileAddress / schemeFronts pallet(s)InterfacePayable?
1ECRecover0x…0001(Ethereum std)rawno
2SHA2560x…0002stdrawno
3RIPEMD1600x…0003stdrawno
4Identity0x…0004stdrawno
5Modexp0x…0005stdrawno
6BN128Add0x…0006stdrawno
7BN128Mul0x…0007stdrawno
8BN128Pairing0x…0008stdrawno
9Blake2F0x…0009stdrawno
10SHA3-256 (FIPS)0x…0400 (1024)Frontier extrarawno
11ECRecoverPublicKey0x…0402 (1026)Frontier extrarawno
12Fee proxy (not a real precompile; intercepted by the EVM runner)0x…04BB (1211)pallet-fee-proxy + pallet-dexcallWithFeePreferencesn/a
13NFT collection factory0x…06B9 (1721)pallet-nftinitializeCollectionno
14SFT collection factory0x…06C3 (1731)pallet-sftinitializeCollectionno
15Marketplace0x…06CD (1741)pallet-marketplacecustomno
16Peg (bridge withdraw)0x…0793 (1939)pallet-erc20-peg, pallet-nft-pegcustomno
17DEX router0x…DDDD (56797)pallet-dexIUniswapV2Router01-like*ETH* fns yes
18FuturePass registrar0x…FFFF (65535)pallet-futurepasscreate, futurepassOfno
19ERC-20 per asset0xCCCCCCCC + u32 assetId (BE) + 12 zero bytespallet-assets-ext (+ pallet-assets, pallet-balances for ROOT), pallet-token-approvalsIERC20 + Metadata + ERC165no
20ERC-721 per NFT collection0xAAAAAAAA + u32 collectionUuid + 12 zero bytespallet-nft, pallet-token-approvals, pallet-xls20IERC721 + Metadata + Burnable + Ownable + ERC165 + TRN ext + ERC-5484no
21ERC-1155 per SFT collection0xBBBBBBBB + u32 collectionUuid + 12 zero bytespallet-sft, pallet-token-approvalsIERC1155 + Burnable + Supply + MetadataURI + Ownable + ERC165 + TRN ext + ERC-5484no
22FuturePass smart account (one per FuturePass)0xFFFFFFFF + u128 futurepassId (16 bytes BE)pallet-futurepass, pallet-proxycustom proxy + Ownableyes (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 DELEGATECALL into 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, an onERC721Received hook cannot call back into the same collection address during safeTransferFrom.

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
PrefixKindExample
0xCCCCCCCCERC-20 assetROOT (asset 1) 0xcCcCCccC00000001000000000000000000000000, XRP (asset 2) 0xCCCCcCCc00000002000000000000000000000000, VTX (asset 3) 0xcccccccc00000003…
0xAAAAAAAAERC-721 NFT collectioncollection 1124 → 0xAAAAAAAA00000464000000000000000000000000
0xBBBBBBBBERC-1155 SFT collectioncollection 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, while 0xAAAAAAAA00000464… 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:

AddressCode
ROOT, XRP, asset 1124, NFT 1124, SFT 185320x54524e…
VTX (asset 3), a genesis asset with no create hook0x
FuturePass addresses0x
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_call to 0xcccccccc0fffffff… name() returns 0x.
  • owner() on the non-existent 0xBBBBBBBB00000464… returns 0x.

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:

AddressPrecompileNotes
0x01ECRecoverIstanbul
0x02SHA256Istanbul. Live: sha256("") = 0xe3b0c442… ✓
0x03RIPEMD160Istanbul
0x04IdentityIstanbul
0x05ModexpIstanbul
0x06BN128AddIstanbul
0x07BN128MulIstanbul
0x08BN128PairingIstanbul
0x09Blake2FIstanbul
0x400 (1024)Sha3FIPS256Real SHA3-256, not keccak. Live: sha3("") = 0xa7ffc6f8… ✓
0x402 (1026)ECRecoverPublicKeyReturns 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() and symbol() are written as ABI bytes, which encode identically to string.

Behaviour and differences from standard ERC-20 ​

  • balanceOf returns the spendable balance, not the total balance. It uses reducible_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 of balanceOf is therefore ≠ totalSupply.
  • transfer and transferFrom dispatch assets_ext::transfer{keep_alive:false} (erc20/src/lib.rs:278-312, 315-371). The sender account may be reaped.
  • Allowances are a separate pallet. approve dispatches token_approvals::erc20_approval (lines 242-276). transferFrom first dispatches erc20_update_approval with origin = from and 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).
  • 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.value and eth_getBalance are 18-dp wei. XRP via 0xCCCC…0002 is 6-dp. Live: account 0x4346…200c has eth_getBalance = 814956000000000000 wei and XRP.balanceOf = 814956.
  • Events are emitted only when the precompile is called. Substrate-side transfers, mints, bridge deposits, DEX swaps and fee payments emit no ERC-20 Transfer log. See §13.
  • totalSupply is total_issuance (lines 177-191). Live values:
AssettotalSupplyHuman-readable
ROOT12,000,000,000,000,00012B ROOT
XRP424,764,270,722≈424.7k XRP
VTX1,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, safeTransferFrom and burn require the caller to be owner or approved, checked via token_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.
  • 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, reRequestXls20Mint and issueSoulbound are collection-owner only. The pallet enforces this with NotCollectionOwner.
    • setMaxSupply can be set once only, to a non-zero value (pallet/nft/src/lib.rs:434-437).
  • renounceOwnership sets the owner to 0x000…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) ​

  1. symbol() returns the collection name. There is a // TODO: Returns same as name at erc721/src/lib.rs:690-706. Live: collection 1124 returns name = symbol = "bridged-collection".
  2. tokenURI does not revert for nonexistent tokens. It just formats baseURI/serial (lines 708-734). Live: tokenURI(0) on collection 1124 returned ethereum://0x6bCa…7182/0, while ownerOf(0) reverted "ERC721: Token does not exist".
  3. safeTransferFrom receiver check is based on AccountCodes(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.
  4. setMintFee(address,uint128) uses uint128 in its selector. The public TRN docs show setMintFee(address,uint256), which is a different selector. A mintFee of 0 clears the fee (lines 938-981).
  5. ERC-5484 selector typo. The runtime matches acceptSouldboundIssuance(uint32) → 0x23d44a0d (line 125). The docs and e2e ABIs use acceptSoulboundIssuance(uint32) → 0xbe23b21b, which does not exist on chain. Callers must use the misspelled name.
  6. PendingIssuanceCreated topic is computed from PendingIssuanceCreated(address,uint256,u8). u8 is not a Solidity type (line 69), so the topic is 0x2d4f5089…. An ABI declaring (address indexed to, uint256 issuanceId, uint256 quantity, uint8 burnAuth) would compute 0x3282756e… and never match. The data actually carries (uint256 issuanceId, uint256 quantity, uint8 burnAuth).
  7. burnAuth() returns 0 for tokens with no burn authority. The comment says "default to TokenOwner", but in the enum 0 = CollectionOwner, 1 = TokenOwner, 2 = Both, 3 = Neither (lines 1329-1353; pallet/common/src/utils.rs:94-117). For ordinary non-soulbound tokens, burnAuth()==0 is misleading.
  8. Function modifier gaps. burn, issueSoulbound and acceptSouldboundIssuance are not in the NonPayable list, so they fall through to View (lines 182-199). Effects:
    • They still reject msg.value.
    • They are not rejected inside a STATICCALL. try_dispatch has 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.
  9. Mint logs. mint emits one Transfer(0x0,to,id) per token, ERC721A-style (lines 794-805). Native nft.mint extrinsics emit no EVM logs (§13).
  10. transferFrom/approve return no data, which is fine per ERC-721. renounceOwnership and transferOwnership return bool 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 via togglePublicMint + setMintFee, setBaseURI, and ownership handover.
  • Paginated wallet inventory via ownedTokens(who, limit, cursor).
  • Soulbound credentials via issueSoulbound and acceptSouldboundIssuance.
  • 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: from or account must be the caller, or the caller must be isApprovedForAll(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 0x0 is rejected (line 614).
  • createToken: the caller must be the collection owner. It calls pallet_sft::do_create_token(caller, …) directly (lines 892-948). maxIssuance = 0 means unlimited. tokenOwner = 0x0 means no per-token owner. It returns the new serial (id).
  • mint and mintBatch: the token owner or collection owner, or anyone if public mint is enabled for that id (pallet-sft). setMaxSupply and setMintFee are per id.
  • burnAsCollectionOwner: the collection owner burns from account, 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. safeTransferFrom and safeBatchTransferFrom call onERC1155Received/onERC1155BatchReceived before moving balances (line 428 calls the check, then do_safe_transfer at 437). The hook is skipped for code-less to, same rule as ERC-721.
    • A receiver that inspects its own balance inside the hook will not see the incoming tokens yet.
  • acceptSouldboundIssuance has the same misspelled selector (line 133).
  • Wider modifier gaps. Besides the soulbound functions, createToken, setMaxSupply, setBaseURI, transferOwnership, renounceOwnership, setBurnAuth and burnAsCollectionOwner are typed View (lines 187-199). They are non-payable but not static-protected.
  • createToken gas. It bypasses try_dispatch, so only log costs are recorded explicitly (lines 892-897).
  • uri(id) = collection base URI + serial. Live: SFT 18532 uri(1) = https://metadata.futureverse.app/pb-mouth/nft/1 and totalSupply(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
}
  • owner is 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 is Root.
  • Arguments:
    • maxIssuance = 0 means unlimited (lines 132-140).
    • metadataPath is parsed into a MetadataScheme (URI prefix) (lines 142-146).
    • name must be ≤ 50 bytes (CollectionNameStringLimit, runtime/src/lib.rs:464).
    • Royalties: royaltyEntitlements are Permill parts: 1_000_000 = 100%, 25_000 = 2.5% (line 152). There are at most MAX_COLLECTION_ENTITLEMENTS entries (line 160).
  • Gas = benchmarked create_collection weight (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 uint256 types in your ABI. The selectors use uint256 for serials, prices and durations (lines 104-135). The e2e test ABI in seed/e2e/common/index.ts declares uint32[]/uint128 for these. Those signatures hash to different selectors, so build your ABI from the strings above.
  • Argument semantics:
    • marketplaceId = 0 means none.
    • buyer = 0x0 means an open sale.
    • duration = 0 means the default of 3 days (DefaultListingDuration, runtime/src/lib.rs:515). Durations are in blocks.
    • entitlement in registerMarketplace is Permill parts (lines 229-282). marketplaceAccount = 0x0 means 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 marketplaceId is set.
  • Limits: at most 1000 tokens per listing (MaxTokensPerListing, runtime/src/lib.rs:516). At most 100 offers per token (MaxOffers).
  • Asset types:
    • sellNft, auctionNft and makeSimpleOffer require a 0xAAAAAAAA (721) collection address.
    • sellSft and auctionSft require 0xBBBBBBBB (lines 327, 433, 648, 754, 961).
    • Offers are NFT-only.
  • getListingFromId labels: 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 buy emits only FixedPriceSaleComplete from 0x…06CD. No ERC-721 Transfer log is emitted from the collection address, and no ERC-20 Transfer log 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
}
  • erc20Withdraw burns or locks the caller's bridged asset and emits an Ethy event proof. The beneficiary claims it on the Ethereum ERC20Peg contract. asset is the 0xCCCCCCCC address. 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 with EvmWithdrawalFailed (erc20-peg/src/lib.rs:540-558; precompile peg/src/lib.rs:140-146).
  • erc721Withdraw only works for collections bridged from Ethereum. Collections must be 0xAAAAAAAA addresses, and the nft-peg pallet checks NoPermissionToBridge (pallet/nft-peg/src/lib.rs:408-426). There are per-call caps (MaxCollectionsPerWithdraw, MaxSerialsPerWithdraw).
  • Gas = benchmarked withdraw weight. 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).
  • deadline is a block number, not a unix timestamp. It is converted to BlockNumber (u32) and must be ≤ 2^32 (lines 51-55, 206). Passing type(uint256).max reverts.
  • 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 in seed/e2e/common/index.ts). There is no factory(), WETH() or pair contract. LP tokens are native assets, addressable via 0xCCCCCCCC[lpAssetId]. Look up the LP asset id with dex.tradingPairLPToken on the Substrate side.
  • All amounts are native decimals, 6 for XRP and ROOT, and must fit in u128.
  • *ETH payable variants use msg.value as an XRP amount. addLiquidityETH, swapExactETHForTokens and swapETHForExactTokens read handle.context().apparent_value and pass it unscaled as the XRP (asset 2) amount pulled from the caller's native balance (lines 265, 589, 799).
    • msg.value is in 18-dp wei. The EVM's own value transfer to 0x…DDDD is 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 address 0xCCCC…0002 in path.
    • removeLiquidityETH, swapTokensForExactETH and swapExactTokensForETH are 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 ​

  1. The runner decodes the args (runner.rs:140-188). It computes the max fee from gasLimit × (baseFee + priorityFee).
  2. It swaps just enough paymentAsset → XRP through pallet-dex (do_swap_with_exact_supply).
  3. It rewrites to/input to target/input and runs the call normally.

msg.sender in target is still the EOA.

Limitations ​

  • Only works as the transaction's top-level to. A contract CALL to 0x…04BB hits an empty account and silently succeeds with empty return.
  • paymentAsset must 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-proxy deposit in ROOT (Balances), transferring any shortfall to the new FP account (runtime/src/impls.rs:628-663). The deposit is:
    • ProxyDepositBase 148 + ProxyDepositFactor 126 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):

  1. The caller must be a registered proxy of this FP with zero delay ("Futurepass: Not proxy" / "Unannounced").
  2. The EVM proxy-call filter (runtime/src/impls.rs:786-818) is applied to (callTo, callData):
    • If callTo is the registrar 0x…FFFF or any 0xFFFFFFFF… address, only the selectors registerDelegateWithSignature, unregisterDelegate and transferOwnership are allowed. Additionally, the caller must be the FP owner ("Futurepass: NotFuturepassOwner", lines 311-327).
    • Otherwise, only ProxyType::Owner and ProxyType::Any may proxy. NonTransfer, Governance, Staking and NoPermission are always rejected ("Futurepass: CallFiltered").
  3. Execution as the FP, with msg.sender = FP. Value is taken from the FP's balance, not from msg.value.
callTypeOperationNotes
0STATICCALL
1CALL
2DELEGATECALLUnsupported ("call type not supported"; live-verified)
3CREATEDeploys from the FP
4CREATE2The 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).
    • deadline is a block number.
    • Max 32 proxies (MaxProxies).
  • 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() passes new_owner = None (lines 412-440 and precompile lines 596-650).

Other limits and quirks:

  • No function-modifier checks at all (lines 162-174). proxyCall accepts msg.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 Substrate futurepass.proxyExtrinsic(balances/assets transfer) instead.
  • 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; docs ethereum-vs-trn.md).
  • Example: eth_getBalance = XRP balanceOf × 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 read symbol()/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.
  • balanceOf is 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 isContract checks 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. VTX 0xcccccccc00000003…) return 0x and look like EOAs.
    • A FuturePass therefore passes "is EOA" checks and receives safeTransferFrom without a receiver hook.

Events and indexing (the big gotcha)

  • EVM logs are produced only when an action goes through a precompile. Native Substrate extrinsics produce no eth_getLogs entries, 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 Transfer events from the token or collection address (§9).
  • An explorer or indexer that reconstructs balances or ownership from Transfer logs (Etherscan-style token trackers, The Graph subgraphs on Transfer) will be incomplete. Indexers must read Substrate events, or poll balanceOf/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/Swap logs are emitted from the virtual pool address 0xDDDDDDDD….

NFT metadata

  • tokenURI and uri are baseURI + serial, with no {id} substitution and no .json suffix unless the base includes it. Values can be ipfs://, https:// or ethereum://<contract>/<id> for Ethereum-bridged collections (live, collection 1124). Wallets may not resolve ethereum://.
  • symbol() equals name() for ERC-721 collections.
  • There is no ERC721Enumerable. Explorers must use ownedTokens or 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) / 0xd9b67a26 will detect them correctly (live-verified on collection 1124 and ROOT/XRP).

15. Discrepancies: public docs / e2e ABIs vs runtime code (trust the code) ​

ItemDocs / e2e sayCode (runtime 84)
Soulbound acceptacceptSoulboundIssuance(uint32) → 0xbe23b21bacceptSouldboundIssuance(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 topickeccak("…(address,uint256,uint256,uint8)") = 0x3282756e…keccak("PendingIssuanceCreated(address,uint256,u8)") = 0x2d4f5089… (erc721/src/lib.rs:69)
FuturePass delegate addregisterDelegate(address,uint8) (docs)only registerDelegateWithSignature(address,uint8,uint32,bytes)
Fee proxycallWithFeePreferences(address,uint128,address,bytes) (docs)preferred (address,address,bytes) 0xf6098286; old one still accepted
Marketplace (e2e ABI)uint32[] serials, uint128 pricesuint256[] / uint256 in selectors (marketplace/src/lib.rs:104-135)
ERC-721 burnAuth default"TokenOwner"returns 0 = CollectionOwner enum value
Marketplace, Pegnot in curated docspresent 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.
  • is_precompile charges one extra DB read for every call to a 0xCC…/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

Curated independently. Source-attributed reference for The Root Network. Not affiliated with Futureverse / TRN Labs.