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:

<-
Testing
Required Actions
->