EVM Quickstart

This guide will help you understand how to build Mods for Totems on EVM-compatible chains using Solidity.

Development Environment

You can use any Solidity development framework you’re comfortable with:

  • Hardhat - JavaScript/TypeScript based, great plugin ecosystem
  • Foundry - Rust-based, fast compilation and testing

Both work, so choose whichever fits your workflow.

Installation

npm install @totems/evm

Or with Foundry:

forge install nsjames/totems-evm

Package Structure

Hardhat / npm:

@totems/evm/
├── mods/           TotemMod.sol, TotemsLibrary.sol
├── interfaces/     ITotems.sol, IMarket.sol, ITotemTypes.sol, IRelayFactory.sol
├── test/           helpers.ts (test utilities)
└── contracts/      Totems.sol, ModMarket.sol, ProxyMod.sol, etc.

Tip

Everything you need for building mods is MIT licensed, however the core protocol contracts (used for the test helper) are under an AGPL-3.0 license which requires derivative works to also be open source to ensure the protocol remains available to all, forever.

Foundry:

After installing, add these remappings to your foundry.toml:

remappings = [
  "@totems/evm/=lib/totems-evm/package/"
]

Then use the same import paths as Hardhat:

import "@totems/evm/mods/TotemMod.sol";

Architecture Overview

The Totems EVM implementation uses a monolithic multi-token pattern:

%%{init: {'theme': 'dark', 'flowchart': { 'subGraphTitleMargin': { 'top': 10 } }, 'themeVariables': { 'primaryColor': '#1e293b', 'primaryTextColor': '#e5e7eb', 'primaryBorderColor': '#38bdf8', 'lineColor': '#38bdf8', 'clusterBkg': 'transparent', 'clusterBorder': '#38bdf8' }}}%%
flowchart LR
    User["User"] --> Totems["Totems Contract"]
    Totems -->|hooks| Mods["Mod Contracts"]
    Totems --> Market["ModMarket"]

    classDef default fill:#1e293b,stroke:#38bdf8,stroke-width:2px,color:#e5e7eb;
    classDef modStyle fill:#1e293b,stroke:#34d399,stroke-width:2px,color:#e5e7eb;
    class Mods modStyle;

All Totems share a single contract (multi-token pattern) rather than deploying separate contracts per token. This means your Mod interacts with one canonical Totems contract address per chain.

Building a Mod

Every Mod extends the TotemMod base contract and implements one or more hook interfaces.

Base Contract

import "@totems/evm/mods/TotemMod.sol";

contract MyMod is TotemMod {
    constructor(address _totemsContract, address payable _seller)
        // Pass in the Totems contract address and your seller address
        // to the parent constructor
        TotemMod(_totemsContract, _seller){}
}

The TotemMod base gives you:

NameTypeDescription
totemsContractVariableThe Totems contract instance
getSeller()Pre-implemented FunctionThe address that receives licensing fees
isSetupFor(ticker)Virtual FunctionIndicates if mod is set up for a given totem
onlyTotemsModifierRestricts calls to Totems contract only
onlyLicensedModifierRestricts calls to licensed totems only
onlyCreatorModifierRestricts calls to the totem creator

Warning

Without the getSeller() function anyone would be able to publish your mod on the market and receive the licensing fees! Make sure you publish your mod with the same address/wallet you’ll be using to receive payments, and publish your mod to the market from that wallet.

Hook Interfaces

Implement any combination of these interfaces to respond to totem events:

InterfaceWhen it’s called
IModCreatedWhen a Totem is created
IModMintAfter totems are minted
IModTransferAfter totem transfers
IModBurnAfter totems are burned
IModMinterCalled to mint totems
IModTransferOwnershipWhen totem ownership changes

Example: Transfer Freeze Mod

Here’s a simple Mod that allows the Totem creator to freeze/unfreeze transfers:

pragma solidity ^0.8.28;

import "@totems/evm/mods/TotemMod.sol";
import "@totems/evm/mods/TotemsLibrary.sol";

contract FreezerMod is TotemMod, IModTransfer {
    mapping(bytes32 => bool) public frozen;

    constructor(address _totemsContract, address payable _seller)
        TotemMod(_totemsContract, _seller){}
    
    function isSetupFor(string calldata ticker) external view override returns (bool) {
        // This mod is always ready to go for any totem
        return true;
    }

    // Modifier ensures only the Totem creator can toggle freeze
    function toggle(string calldata ticker) external onlyCreator(ticker) {
        bytes32 tickerBytes = TotemsLibrary.tickerToBytes(ticker);
        frozen[tickerBytes] = !frozen[tickerBytes];
    }

    // Called on every transfer - revert if frozen
    function onTransfer(
        string calldata ticker,
        address from,
        address to,
        uint256 amount,
        string calldata memo
    ) external onlyTotems onlyLicensed(ticker) {
        bytes32 tickerBytes = TotemsLibrary.tickerToBytes(ticker);
        require(!frozen[tickerBytes], "Transfers are frozen");
    }
}

Important

Always use the onlyTotems and onlyLicensed(ticker) modifiers on your hook functions. This ensures your mod is only called by the Totems contract and is properly licensed, preventing unauthorized use and ensuring you receive payment for your mod’s usage.

See the TotemsLibrary reference for all available helper functions.

Next Steps

These docs are structured so that each section builds on the previous one. Following them in order will give you the complete picture of how to build, test, and publish mods.

<-
Fees
Hooks
->