Token Standards & Best Practices on Rootstock
Tokens are the lifeblood of DeFi. This section covers ERC-20, ERC-721, and important extensions like ERC-20 Permit, ERC-4626, and rBTC wrapping.
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());
}
}
ERC20Capped
Enforces a maximum supply. Useful for creating capped tokens (like a capped sale).
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Capped.sol";
contract MyTokenCapped is ERC20, ERC20Capped, Ownable {
constructor(uint256 cap) ERC20("MyToken", "MTK") ERC20Capped(cap * 10 ** decimals()) {
_mint(msg.sender, 500000 * 10 ** decimals());
}
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}
}
Security Considerations for ERC-20
Reentrancy: While transfer and transferFrom are not typically vulnerable to reentrancy, if you call external contracts during a transfer (e.g., hooks), use ReentrancyGuard.
Approval Front-Running: The approve function can be front-run. Use increaseAllowance/decreaseAllowance instead, or use permit to avoid this.
Decimals: Always use decimals() when displaying token amounts; never assume 18.
Return Values: Some old tokens don't return a boolean. OpenZeppelin's SafeERC20 wrapper handles this.
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
contract MyContract {
using SafeERC20 for IERC20;
function safeTransfer(IERC20 token, address to, uint256 amount) external {
token.safeTransfer(to, amount);
}
}
2. Wrapping RBTC (rBTC)
Rootstock's native currency is RBTC, which is an ERC-20 compatible token? Actually, RBTC is the native coin, similar to ETH on Ethereum. It is not an ERC-20 token; it has no contract address. To use RBTC in DeFi protocols that expect ERC-20, you need wRBTC (Wrapped RBTC) – an ERC-20 token backed 1:1 by RBTC.
The official wrapped RBTC contract is deployed on Rootstock. You can interact with it to wrap and unwrap.