betterERC
Docs/Building

Assembling a token

Inherit the modules you want, initialise each one, seal the registry. The order you list them in does not matter, and that is the point.

All docs

A token is an assembly. It inherits the modules it wants, calls each one's initialiser, and seals. Nothing else is required of it, and nothing about the inheritance order affects behaviour.

MyToken.sol
contract MyToken is ERC20TransferFee, ERC20TransferRestriction {
    constructor(string memory name_, string memory symbol_) initializer {
        __ERC20_init(name_, symbol_);
        __ERC20TransferFee_init();
        __ERC20TransferRestriction_init();

        _sealExtensions();   // the last step, always
    }
}

Each module's initialiser registers its own identifier and declares the behaviour it brings. Registering the same module twice is rejected, as is registering anything after the seal, as is declaring a behaviour bit outside the assigned set.

Sealing is not optional

Until the assembly seals, every discovery view reverts. An assembly that forgets has no discovery surface at all rather than a quietly unvalidated one, and it fails at deployment — in a test, on a fork, in front of the person who can still fix it. Checking a flag on the transfer path instead would tax every transfer forever, to catch a mistake that can only be made once.

Sealing is also where the declared behaviours are validated against each other, so an assembly declaring a contradiction fails inside its own constructor rather than shipping.

The three deployment shapes

ExtendedTokenExtendedTokenUpgradeableRuntime clone
DeploymentdirectERC-1967 proxy, UUPSEIP-1167 clone
Tierself-declaredself-declaredverified
UPGRADEABLEnot declareddeclaredstructurally impossible
Extensionsmetadata, fee, restriction, hookthe same fourany valid subset of all five
Storagenamespaced per modulenamespaced per modulenamespaced per module

The first two are for tokens that want the framework's structure and are content to be taken at their word. The third is for tokens that want to be checked: no admin slot, no upgrade path, and forty-five bytes that can only ever name one implementation.

Non-transferability is absent from the first two by necessity: it contradicts both the fee and the hook, so an assembly containing all five would revert in its own constructor. The runtime carries it and gates it on registration, so a soulbound token is one of the subsets a clone can be initialised with.

Storage lives in a namespaced slot per module — required for the upgradeable variant, free for the immutable one. It means a module can be mixed into a token in any position without its state moving.