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.30;
import "@openzeppelin/contracts@5.6.1/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts@5.6.1/access/Ownable.sol";
contract MyToken is ERC20, Ownable {
constructor() ERC20("MyToken", "MTK") Ownable(msg.sender) {
// 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);
}
}
Want to deploy and interact with MyToken without any local setup? Use the button below to open it directly in the Remix IDE. You'll need MetaMask with Rootstock Testnet configured — see the full Remix + Rootstock guide for the exact steps.
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.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
import "@openzeppelin/contracts@5.6.1/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts@5.6.1/token/ERC20/extensions/ERC20Permit.sol";
contract MyTokenPermit is ERC20, ERC20Permit {
constructor() ERC20("MyToken", "MTK") ERC20Permit("MyToken") {
_mint(msg.sender, 1000000 * 10 ** decimals());
}
}
Want to deploy and interact with MyTokenPermit without any local setup? Use the button below to open it directly in the Remix IDE. You'll need MetaMask with Rootstock Testnet configured — see the full Remix + Rootstock guide for the exact steps.
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
);
ERC20Votes (historical balances)
OpenZeppelin v5 removed ERC20Snapshot. To track historical balances for governance (voting based on past balances) or dividend distribution, use ERC20Votes, which records checkpoints automatically on every transfer. Holders call delegate (often to themselves) to start accruing checkpoints.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
import "@openzeppelin/contracts@5.6.1/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts@5.6.1/token/ERC20/extensions/ERC20Permit.sol";
import "@openzeppelin/contracts@5.6.1/token/ERC20/extensions/ERC20Votes.sol";
import "@openzeppelin/contracts@5.6.1/access/Ownable.sol";
import "@openzeppelin/contracts@5.6.1/utils/Nonces.sol";
contract MyTokenVotes is ERC20, ERC20Permit, ERC20Votes, Ownable {
constructor()
ERC20("MyToken", "MTK")
ERC20Permit("MyToken")
Ownable(msg.sender)
{}
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}
// The following overrides are required by Solidity for ERC20Votes
function _update(address from, address to, uint256 value)
internal
override(ERC20, ERC20Votes)
{
super._update(from, to, value);
}
function nonces(address owner)
public
view
override(ERC20Permit, Nonces)
returns (uint256)
{
return super.nonces(owner);
}
}
Want to deploy and interact with MyTokenVotes without any local setup? Use the button below to open it directly in the Remix IDE. You'll need MetaMask with Rootstock Testnet configured — see the full Remix + Rootstock guide for the exact steps.
ERC20Burnable
Allows token holders to burn their own tokens, reducing total supply.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
import "@openzeppelin/contracts@5.6.1/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts@5.6.1/token/ERC20/extensions/ERC20Burnable.sol";
contract MyTokenBurnable is ERC20, ERC20Burnable {
constructor() ERC20("MyToken", "MTK") {
_mint(msg.sender, 1000000 * 10 ** decimals());
}
}
Want to deploy and interact with MyTokenBurnable without any local setup? Use the button below to open it directly in the Remix IDE. You'll need MetaMask with Rootstock Testnet configured — see the full Remix + Rootstock guide for the exact steps.
ERC20Capped
Enforces a maximum supply. Useful for creating capped tokens (like a capped sale).
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
import "@openzeppelin/contracts@5.6.1/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts@5.6.1/token/ERC20/extensions/ERC20Capped.sol";
import "@openzeppelin/contracts@5.6.1/access/Ownable.sol";
contract MyTokenCapped is ERC20, ERC20Capped, Ownable {
constructor(uint256 cap)
ERC20("MyToken", "MTK")
ERC20Capped(cap * 10 ** decimals())
Ownable(msg.sender)
{
_mint(msg.sender, 500000 * 10 ** decimals());
}
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}
}
Want to deploy and interact with MyTokenCapped without any local setup? Use the button below to open it directly in the Remix IDE. You'll need MetaMask with Rootstock Testnet configured — see the full Remix + Rootstock guide for the exact steps.