Tokens are the lifeblood of DeFi. Rootstock is fully EVM-compatible, so all Ethereum token standards work seamlessly. This section covers ERC-20, ERC-721, and important extensions like ERC-20 Permit, ERC-4626, and RBTC wrapping.
Token Standards & Best Practices
1. ERC-20 Tokens
The ERC-20 standard is the foundation of fungible tokens on Ethereum-compatible blockchains like Rootstock. It defines a common interface that wallets, exchanges, and DeFi protocols can rely on.
Basic ERC-20 Implementation
OpenZeppelin provides battle-tested, audited implementations. Always use these instead of writing your own from scratch.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyToken is ERC20, Ownable {
constructor() ERC20("MyToken", "MTK") {
// Mint initial supply to the contract deployer
_mint(msg.sender, 1000000 * 10 ** decimals());
}
// Optional: allow owner to mint more tokens
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}
}
Key points:
-
decimals() defaults to 18; you can override if needed.
-
_mint is internal; you control minting logic through public functions.
-
Ownable restricts minting to the owner; you can use AccessControl for more granular permissions.
Important Extensions
OpenZeppelin provides several extensions that add functionality while maintaining security.
ERC20Permit (EIP-2612)
Allows users to approve token spending with a signature, enabling gasless transactions. This is essential for meta-transactions and improving user experience.
How it works: Users sign a message off-chain containing approval details (spender, amount, deadline, nonce). Anyone can submit that signature to the permit function, which sets the allowance without requiring the token holder to pay gas.
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
contract MyTokenPermit is ERC20, ERC20Permit {
constructor() ERC20("MyToken", "MTK") ERC20Permit("MyToken") {
_mint(msg.sender, 1000000 * 10 ** decimals());
}
}
Usage example (frontend):
// User signs a permit message
const domain = {
name: "MyToken",
version: "1",
chainId: 31, // Rootstock Testnet
verifyingContract: token.address,
};
const types = {
Permit: [
{ name: "owner", type: "address" },
{ name: "spender", type: "address" },
{ name: "value", type: "uint256" },
{ name: "nonce", type: "uint256" },
{ name: "deadline", type: "uint256" },
],
};
const message = {
owner: user.address,
spender: dapp.address,
value: ethers.utils.parseEther("100"),
nonce: await token.nonces(user.address),
deadline: Math.floor(Date.now() / 1000) + 3600,
};
const signature = await user._signTypedData(domain, types, message);
// Someone else (or a relayer) submits the permit
await token.permit(
message.owner,
message.spender,
message.value,
message.deadline,
signature.v,
signature.r,
signature.s
);
ERC20Snapshot
Creates snapshots of token balances at different points in time. Useful for governance (voting based on past balances) or dividend distribution.
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Snapshot.sol";
contract MyTokenSnapshot is ERC20, ERC20Snapshot, Ownable {
constructor() ERC20("MyToken", "MTK") {}
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}
function snapshot() public onlyOwner returns (uint256) {
return _snapshot();
}
// Override required functions
function _beforeTokenTransfer(address from, address to, uint256 amount)
internal
override(ERC20, ERC20Snapshot)
{
super._beforeTokenTransfer(from, to, amount);
}
}
ERC20Burnable
Allows token holders to burn their own tokens, reducing total supply.
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
contract MyTokenBurnable is ERC20, ERC20Burnable {
constructor() ERC20("MyToken", "MTK") {
_mint(msg.sender, 1000000 * 10 ** decimals());
}
}