Skip to main content
This page covers the smart contract implementation details. See Glossary.
Modules are external contracts that extend a Vehicle’s capabilities — for example, distributing Merkl rewards or integrating custom logic. The Modules Manager is a shared registry of approved module addresses, and each Vehicle opts in to the modules it wants to run. As an asset manager, you typically execute modules that have already been registered and allowed. If you are operating a platform-owned Vehicle, the platform owner manages the module registry — you can execute allowed modules using the VEHICLE_EXEC role.

How modules work

The Modules Manager is a registry plus a per-target allowlist. It never executes a module itself:
  1. RegistryModulesManager stores approved module contract addresses. A module is identified by its address; there are no module IDs.
  2. Per-target allowlist — each target contract must explicitly allow a registered module before that module can run on it. The allowlist is keyed target → module → bool.
  3. Execution on the target — the Vehicle delegatecalls the module from its own executeModule function, so module code runs against the Vehicle’s storage.
addModule and removeModule take effect immediately — the Modules Manager has no timelock. Safety comes from the two-sided opt-in instead: a module must be registered by a MODULE_MANAGER and allowed by the target Vehicle before it can execute.

Module lifecycle

Every module goes through two steps:
  1. Registration — an account with MODULE_MANAGER calls addModule(module) on the Modules Manager. The module must be a deployed contract and must not already be registered.
  2. Authorization — the target contract calls allowModule(module) on the Modules Manager, which records the allowance for msg.sender. For a Vehicle, an account with VEHICLE_ALLOW calls vehicle.allowModule(module, true) and the Vehicle forwards the call.

Prerequisites

Each role is checked on the External Access Control of the contract that carries the gated function, either globally or scoped to that contract:

Add a module

Adding a module is a two-step process: register it globally, then allow it on each Vehicle that should run it.
1

Register the module

Call addModule with the module’s contract address.Requires: MODULE_MANAGER role.
Reverts with ExistingModule if the address is already registered, or ZeroCode if it is not a deployed contract.
2

Allow the module on target contracts

Each target contract must explicitly authorize the module before it can be executed there. For Vehicles, use the Vehicle’s own allowModule wrapper, which calls the Modules Manager on the Vehicle’s behalf.Requires: VEHICLE_ALLOW role on the Vehicle.
Reverts with ModuleNotFound if the module is not registered, or AlreadyAllowed if the Vehicle already allows it.
A Vehicle exposes the manager it was deployed with through vehicle.modulesManager(). Vehicles are wired to a Modules Manager at spawn time via the modulesManager field of the factory’s SpawnParams, and the factory checks that it was deployed by the trusted ModulesManagerFactory.

Execute a module

Once a module is registered and allowed on the target, execute it on the target, not on the Modules Manager. Requires: VEHICLE_EXEC role on the Vehicle.
The Vehicle checks the caller’s role, requires modulesManager.isRegistered(module) and modulesManager.allowed(address(this), module), then delegatecalls IModule.exec(data) and emits ModuleExecuted(module, data, msg.value). It reverts with ModuleNotFound, ModuleNotAllowed, or MissingModulesManager when the Vehicle has no manager configured.
The target contract must implement the IModuleTarget interface, whose only function is executeModule(address module, bytes calldata data). All Railnet Vehicles implement it through BaseVehicle. Conduits are not module targets and have no Modules Manager.

Replace a module

There is no update function: a module is its address. To swap an implementation, remove the old address and register the new one.
Allowances are per module address, so they do not carry over. Every Vehicle that should run the replacement must call allowModule(newModuleAddress, true) again.

Remove a module

Removing a module from the registry stops it from executing anywhere immediately, because every execution re-checks isRegistered. Requires: MODULE_MANAGER role.
Removal does not clear per-target allowances. Targets that had allowed the module keep a stale allowlist entry and should disallow it explicitly if the module is gone for good.

Disallow a module on a target

Revoke a module’s authorization on a specific target without removing it from the registry. Requires: VEHICLE_ALLOW role on the Vehicle.
Reverts with AlreadyDisallowed if the Vehicle was not allowing the module.

Next steps

Configure fees

Set up fee structures for your Multi-Vehicle.

Configure rewards

Capture and distribute additional protocol rewards with interceptors.