This repository contains Solidity contract templates for interacting with the pallet-assets precompile on Polkadot Asset Hub (formerly Statemint).
The pallet-assets precompile allows Solidity smart contracts to interact with Polkadot - native assets on Polkadot Asset Hub through the Ethereum compatibility layer provided by pallet-revive.
In pallet-revive, each asset has its own unique precompile address derived from its Asset ID. This differs from traditional single-precompile approaches.
The precompile address encodes the Asset ID in the first 4 bytes and includes a precompile index at bytes 17-18:
- Format:
0x[AssetID as 8 hex chars][26 zeros][Precompile Index][2 zeros] - Formula:
address = (assetId << 128) | (0x0120 << 16) - Code source: Polkadot SDK Assets Precompile
Address Structure (20 bytes):
- Bytes 0-3: Asset ID (4 bytes)
- Bytes 4-16: Zeros (13 bytes)
- Bytes 17-18: Precompile index
0x0120(2 bytes) - Byte 19: Zero (1 byte)
The precompile index 0x0120 is for TrustBackedAssets on Passet Hub.
| Asset ID | Precompile Address |
|---|---|
| 1 | 0x0000000100000000000000000000000001200000 |
| 1000 | 0x000003e800000000000000000000000001200000 |
| 42000 | 0x0000a41000000000000000000000000001200000 |
The AssetsPrecompile library (included in IAssets.sol) handles address computation automatically:
uint256 assetId = 1000;
address precompileAddress = AssetsPrecompile.getAddress(assetId);
// Returns: 0x000003e800000000000000000000000001200000- IAssets.sol - Standard ERC20 interface for pallet-assets precompiles and the AssetsPrecompile helper library
- AssetManager.sol - Example implementation contract demonstrating common usage patterns
The interface provides a standard ERC20-compatible API for interacting with Substrate assets.
Currently Supported Functions (based on pallet-revive implementation):
totalSupply()- Get total supplybalanceOf(account)- Get account balanceallowance(owner, spender)- Get allowance
transfer(recipient, amount)- Transfer tokenstransferFrom(owner, recipient, amount)- Transfer from allowanceapprove(spender, amount)- Approve spender
Note: Additional pallet-assets functions (like name, symbol, decimals, mint, burn, freeze, etc.) are not yet exposed through the pallet-revive precompile interface. Only the core ERC20 functions listed above are currently available.
Helper library for working with asset precompiles:
getAddress(assetId)- Compute the precompile address for an Asset IDgetAsset(assetId)- Get the IERC20Assets interface instance for an Asset ID
Example contract demonstrating practical usage:
- Query total supply and balances
- Check allowances
- Transfer assets
- Approve and transferFrom patterns
- Deposit/withdraw patterns for DeFi applications
- Batch transfers
- Helper functions for address computation
import "./IAssets.sol";
contract MyContract {
using AssetsPrecompile for uint256;
function transferTokens(uint256 assetId, address recipient, uint256 amount) external {
IERC20Assets asset = assetId.getAsset();
asset.transfer(recipient, amount);
}
}import "./IAssets.sol";
contract MyContract {
function transferByAddress(uint256 assetId, address recipient, uint256 amount) external {
// Compute the precompile address
address precompileAddress = AssetsPrecompile.getAddress(assetId);
// Use the address directly
IERC20Assets asset = IERC20Assets(precompileAddress);
asset.transfer(recipient, amount);
}
}import "./IAssets.sol";
contract MyContract {
using AssetsPrecompile for uint256;
function getAssetBalance(uint256 assetId, address account) external view returns (
uint256 balance,
uint256 totalSupply,
uint256 allowance
) {
IERC20Assets asset = assetId.getAsset();
balance = asset.balanceOf(account);
totalSupply = asset.totalSupply();
allowance = asset.allowance(account, msg.sender);
}
}import "./IAssets.sol";
contract MyContract {
using AssetsPrecompile for uint256;
// User approves contract
function approveContract(uint256 assetId, uint256 amount) external {
IERC20Assets asset = assetId.getAsset();
asset.approve(address(this), amount);
}
// Contract transfers from user
function depositFromUser(uint256 assetId, address user, uint256 amount) external {
IERC20Assets asset = assetId.getAsset();
asset.transferFrom(user, address(this), amount);
}
}import "./IAssets.sol";
contract MultiAssetVault {
using AssetsPrecompile for uint256;
function depositMultiple(
uint256[] calldata assetIds,
uint256[] calldata amounts
) external {
require(assetIds.length == amounts.length, "Length mismatch");
for (uint256 i = 0; i < assetIds.length; i++) {
IERC20Assets asset = assetIds[i].getAsset();
asset.transferFrom(msg.sender, address(this), amounts[i]);
}
}
}This project is configured for deployment to Polkadot Asset Hub (Polkadot Hub) using Hardhat.
- Node.js and npm: Version 22.5+ and npm 10.9.0+ are required
- Test tokens: Get PAS test tokens from Polkadot Faucet
- Private key: Have a wallet private key ready for deployment
-
Install dependencies:
npm install
-
Set your private key securely:
npx hardhat vars set PRIVATE_KEY "your_private_key_here"
Note: Never commit your private key to version control. Hardhat vars stores it securely.
Compile contracts for PolkaVM:
npm run compile
# or
npx hardhat compileThis compiles your Solidity contracts to PolkaVM bytecode using the @parity/hardhat-polkadot plugin.
npm run deploy:testnet
# or
npx hardhat ignition deploy ./ignition/modules/AssetManager.js --network polkadotHubTestnetTestNet Details:
- Network: Passet Hub TestNet
- RPC:
https://testnet-passet-hub-eth-rpc.polkadot.io - Chain ID: 420420422
- Faucet: https://faucet.polkadot.io/?parachain=1111
- Asset IDs are numeric identifiers (uint32) for assets created on the Substrate side
- Each Asset ID maps to a unique precompile address
- Asset IDs can be queried through the Substrate RPC or block explorer
- The maximum Asset ID is
4,294,967,295(uint32 max)
The precompile address for any asset can be computed as:
address = (assetId << 128) | (0x0120 << 16)
This shifts the Asset ID to the first 4 bytes of the 20-byte address and positions the precompile index 0x0120 at bytes 17-18 (by shifting it left 16 bits).
Some functions require special permissions:
- Owner - Can mint, freeze, transfer ownership, set team, set metadata
- Admin - Can freeze/thaw accounts, force transfers
- Issuer - Can mint and burn
- Freezer - Can freeze and thaw accounts
Standard users can:
- Transfer their own assets
- Approve and use transferFrom
- Burn their own assets
- Query asset information
Assets on Polkadot have a minimum balance requirement (existential deposit). Ensure transfers maintain accounts above this threshold, or they will be reaped.