Security Checklist
Security best practices for building Totems mods. Use this checklist before deploying to mainnet.
Built-in Modifiers
The TotemMod base contract provides two essential modifiers. Always use both on hook functions:
function onTransfer(
string calldata ticker,
address from,
address to,
uint256 amount,
string calldata memo
) external onlyTotems onlyLicensed(ticker) {
// Safe to execute
} onlyTotems
Ensures the caller is the Totems contract or Proxy Mod:
modifier onlyTotems() {
if(msg.sender != totemsContract){
if(msg.sender != ITotemsProxyModGetter(totemsContract).getProxyMod()){
revert InvalidModEventOrigin();
}
}
_;
} onlyLicensed
Verifies your mod is licensed for the Totem:
modifier onlyLicensed(string calldata ticker) {
if (!TotemsLibrary.hasLicense(totemsContract, ticker, address(this))) {
revert NotLicensed();
}
_;
} Caution
Without these modifiers, anyone could call your mod’s hook functions directly, bypassing the Totems contract entirely.
State Change Timing
State changes (balance updates, supply changes) occur before hooks are called. Your mod sees the post-operation state.
function onTransfer(
string calldata ticker,
address from,
address to,
uint256 amount,
string calldata memo
) external onlyTotems onlyLicensed(ticker) {
// At this point:
// - `from` balance has already decreased by `amount`
// - `to` balance has already increased by `amount`
uint256 recipientBalance = TotemsLibrary.getBalance(totems, ticker, to);
// This returns the NEW balance, after the transfer
} Minter Security
Validate Mint Requests
Minter mods control token creation. Validate all inputs:
function onMint(
string calldata ticker,
address minter,
uint256 amount,
string calldata memo,
uint256 payment
) external onlyTotems onlyLicensed(ticker) {
// Validate the minter is allowed
if (!allowedMinters[minter]) {
revert MinterNotAllowed(minter);
}
// Validate payment if required
if (payment < minPayment) {
revert InsufficientPayment(minPayment, payment);
}
// Validate amount bounds
if (amount == 0 || amount > maxMintPerTx) {
revert InvalidAmount(amount);
}
} Unlimited Minter Considerations
Unlimited minters have no supply cap. Extra caution is required:
// Track total minted to implement soft caps
mapping(string => uint256) public totalMinted;
mapping(string => uint256) public mintCaps;
function onMint(
string calldata ticker,
address minter,
uint256 amount,
string calldata memo,
uint256 payment
) external onlyTotems onlyLicensed(ticker) {
uint256 newTotal = totalMinted[ticker] + amount;
if (newTotal > mintCaps[ticker]) {
revert MintCapExceeded(mintCaps[ticker], newTotal);
}
totalMinted[ticker] = newTotal;
} Self Notifications
It’s entirely possible that you could get notified about your own transfers. Make sure you handle this case correctly:
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract MyMod is TotemMod, IModTransfer, ReentrancyGuard {
constructor(address _totemsContract, address payable _seller)
TotemMod(_totemsContract, _seller) {}
function isSetupFor(string calldata ticker) external view override returns (bool) {
return true;
}
function onTransfer(
string calldata ticker,
address from,
address to,
uint256 amount,
string calldata memo
) external nonReentrant onlyTotems onlyLicensed(ticker) {
if (from == address(this) || to == address(this)) {
// Handle self-transfer case
} else {
// Normal transfer logic
}
}
} Excluding Minter Mods from Restrictive Logic
When building mods that restrict transfers (blocklists, allowlists, whale limits, etc.), you must consider how minter mods interact with your logic.
The Problem
Minter mods hold allocated tokens and transfer them to users during minting. If your mod blocks or restricts transfers involving minter addresses, you could break the minting flow entirely.
For example, a whale-blocking mod that limits how many tokens an address can hold would incorrectly flag minter mods (which may hold large allocations) as “whales.”
The Solution
Use TotemsLibrary.isMinter() to check if an address is a minter mod, and exclude it from restrictive logic:
function onTransfer(
string calldata ticker,
address from,
address to,
uint256 amount,
string calldata memo
) external onlyTotems onlyLicensed(ticker) {
// Minters are excluded from allowlist checks
if (TotemsLibrary.isMinter(totemsContract, ticker, from)) return;
if (TotemsLibrary.isMinter(totemsContract, ticker, to)) return;
// Your restrictive logic here
require(isAllowed[from][to], "Transfer not allowed");
} When to Exclude Minters
You should typically exclude minter addresses from:
- Blocklists - Minters need to transfer tokens to users
- Allowlists - Minters should be implicitly allowed
- Whale limits - Minters may legitimately hold large allocations
- Transfer cooldowns - Minters need to process multiple mints
- Membership requirements - Minters should be treated as trusted
Helper Pattern
For mods with multiple hooks, create a helper function:
function _isExcludedFromRestrictions(string calldata ticker, address account) internal view returns (bool) {
if (account == address(this)) return true;
if (TotemsLibrary.isMinter(totemsContract, ticker, account)) return true;
return false;
} Caution
Forgetting to exclude minters can completely break minting for any Totem using your mod. Always test your mod with minter allocations.
Deployment Checklist
Before deploying to mainnet: