Skip to main content
This page covers the smart contract implementation details. See Glossary.
Modules are external contracts that extend your Multi-Vehicle’s capabilities — for example, distributing Merkl rewards or integrating custom logic. The Modules Manager provides a secure, timelock-protected registry for adding, updating, and executing modules. As an asset manager, you typically execute modules that have been registered and approved. If you are operating a platform-owned Multi-Vehicle, the platform owner manages the module registry — you can execute authorized modules using the EXEC role.

How modules work

The Modules Manager acts as both a registry and an execution gateway:
  1. Registry — Stores authorized module addresses mapped to unique bytes32 identifiers
  2. Execution gateway — Authorized callers trigger module execution on target contracts (vehicles)
  3. Target protection — Each target contract must explicitly allow a module before it can be executed on it
  4. Security — Adding or updating modules is protected by a mandatory timelock

Module lifecycle

Every module goes through a three-step process:
  1. Registration — An authorized MODULE_MANAGER calls addModule. This creates a pending request and starts the timelock countdown.
  2. Finalization — After the timelock elapses, anyone can call approvePendingModule to activate the module.
  3. Authorization — The target contract (e.g. a Vehicle) calls allowModule to permit execution of that module on itself.
The timelock is a mandatory delay between requesting a module addition and its activation. This gives stakeholders time to review the proposed module before it becomes executable.

Prerequisites

You need the following roles on your External Access Control, scoped to the Modules Manager contract:
RolePurpose
MODULE_MANAGERAdd, update, or remove modules from the registry
EXECExecute authorized modules on target contracts
CANCEL_MODULECancel a pending module operation during the timelock
UPDATE_TIMELOCKChange the timelock duration
VEHICLE_ALLOWAllow or disallow modules on a Vehicle (scoped to the Vehicle)

Add a module

Adding a module is a two-step process separated by the timelock.
1

Request module addition

Call addModule with the module’s unique ID and its contract address. The ID must match the value returned by IModule(module).MODULE_ID().Requires: MODULE_MANAGER role.
bytes32 moduleId = keccak256("MERKL_DISTRIBUTOR");
address moduleAddress = 0x...; // The module contract

// This starts the timelock countdown
modulesManager.addModule(moduleId, moduleAddress);
2

Wait for the timelock

Check the current timelock duration and wait for it to elapse.
// Query the timelock duration (in seconds)
uint256 timelockDuration = modulesManager.timelock();

// The module becomes approvable after: block.timestamp >= requestTimestamp + timelockDuration
3

Approve the pending module

After the timelock has passed, finalize the module registration. This function can be called by any account.
// Finalize the module — callable by anyone after the timelock
modulesManager.approvePendingModule(moduleId);

// Verify the module is now active
address activeModule = modulesManager.getModule(moduleId);
4

Allow the module on target contracts

Each target contract must explicitly authorize the module before it can be executed. For Vehicles, use the wrapper function that calls allowModule on the Modules Manager.Requires: VEHICLE_ALLOW role scoped to the Vehicle.
// Allow the module on a specific vehicle
vehicle.allowModule(moduleId);

// Verify authorization
bool isAllowed = modulesManager.allowed(address(vehicle), moduleId);

Execute a module

Once a module is active in the registry and allowed on the target, you can execute it. Requires: EXEC role.
address target = address(vehicle);   // The contract to execute the module on
bytes32 moduleId = keccak256("MERKL_DISTRIBUTOR");
bytes memory data = abi.encode(...); // Encoded parameters for the module

modulesManager.exec(target, moduleId, data);
The target contract must implement the IModuleTarget interface. All Railnet vehicles implement this interface by default.

Update a module

To update a module’s implementation address, the process mirrors addition — request, timelock, approve.
bytes32 moduleId = keccak256("MERKL_DISTRIBUTOR");
address newModuleAddress = 0x...; // The updated module contract

// Request the update (starts timelock)
modulesManager.updateModule(moduleId, newModuleAddress);

// After timelock elapses:
modulesManager.approvePendingModule(moduleId);
Updating a module does not require re-allowing it on target contracts. Existing allowModule authorizations carry over to the new implementation.

Remove a module

Removing a module revokes its authorization across all target contracts immediately. Requires: MODULE_MANAGER role.
bytes32 moduleId = keccak256("MERKL_DISTRIBUTOR");

// Remove the module (immediate effect)
modulesManager.removeModule(moduleId);
Removal fails if there is a pending request for the same module ID. Cancel the pending request first with cancelPendingModule, then remove.

Cancel a pending module

If you need to abort a module addition or update during the timelock period, cancel it. Requires: CANCEL_MODULE role.
bytes32 moduleId = keccak256("MERKL_DISTRIBUTOR");

// Cancel the pending addition or update
modulesManager.cancelPendingModule(moduleId);

Disallow a module on a target

Revoke a module’s authorization on a specific target without removing it from the registry.
// Disallow the module on a specific vehicle
vehicle.disallowModule(moduleId);

// The module remains in the registry but can no longer execute on this vehicle

Update the timelock duration

Change the global timelock duration for future module operations. Requires: UPDATE_TIMELOCK role.
// Set a new timelock of 48 hours
uint256 newTimelock = 48 hours;
modulesManager.updateTimelock(newTimelock);

Next steps

Configure fees

Set up fee structures for your Multi-Vehicle.

Configure rewards

Capture and distribute additional protocol rewards with interceptors.