Skip to main content

Creating a new Module

This guide describes the required steps to create a module (or dApp control) from scratch. No particular use case is illustrated. Please see the bootstrapping a module guide for an example of a module with a particular use case.

Why do we need a module

A module defines the needs of a specific dApp in the format that the Atlas contract can understand.

In an Atlas transaction, the Atlas contract calls the module contract at various stages of the execution. When developing a module, we must strictly implement the interface that the Atlas contract (and possibly off-chain components) is expecting for a seamless integration.

Components Overview

Anatomy of a module

Modules MUST inherit from the official dAppControl base contract. This abstract contract contains all the necessary code related to safety and default values.

Atlas will call the module to execute customized code (hooks), security is therefore a top priority. Certain hooks must be called at specific phases of the Atlas execution. Some have to be delegatecalled, and other strict requirements. The dappControl base contract enforces all these rules, so the dApp can focus on developing the module and its desired behavior.

warning

The dAppControl contract is fully audited and battle-tested. DApps should not reimplement its features.

The module must define a set of options. Some are set during deployment (the call config boolean options), and others are set in overridable functions that Atlas will call. Both are covered in the next sections.

Lastly, the module must override some hook functions defined in the base dAppControl contract. These hooks are where the custom dApp code lives. Some hooks are optional, and others are mandatory.

Components Overview

Call config options

The call config is a set of boolean options that are set during the deployment of a module. They must be passed to the dappControl base contract in the constructor.

pragma solidity 0.8.28;

import {DAppControl} from "@atlas/dapp/DAppControl.sol";

contract ExampleModule is DAppControl {
constructor(address _atlas)
DAppControl(
_atlas,
msg.sender,
CallConfig({
userNoncesSequential: false,
dappNoncesSequential: false,
requirePreOps: true,
trackPreOpsReturnData: true,
trackUserReturnData: false,
delegateUser: false,
requirePreSolver: false,
requirePostSolver: false,
zeroSolvers: true,
reuseUserOp: false,
userAuctioneer: true,
solverAuctioneer: false,
unknownAuctioneer: true,
verifyCallChainHash: true,
forwardReturnData: false,
requireFulfillment: false,
trustedOpHash: true,
invertBidValue: false,
exPostBids: true,
multipleSuccessfulSolvers: false
})
)
{ }
}

Below is the full list of options.

  • userNoncesSequential

    The user operation nonce must be the next sequential nonce for that user's address in Atlas' nonce system. If false, the user operation nonces are allowed to be non-sequential (unordered), as long as they are unique.

  • dappNoncesSequential

    The dApp operation nonce must be the next sequential nonce for that dApp signer's address in Atlas' nonce system. If false, the dApp operation nonce is not checked, as the dApp operation is tied to its user operation's nonce via the callChainHash.

  • requirePreOps

    The preOps hook is executed before the user operation is executed. If false, the preOps hook is skipped. the dApp control should check the validity of the user operation (whether it can support userOp.dapp and userOp.data) in the preOps hook.

  • trackPreOpsReturnData

    The return data from the preOps hook is passed to the next call phase. If false, preOps return data is discarded. If both trackPreOpsReturnData and trackUserReturnData are true, they are concatenated.

  • trackUserReturnData

    The return data from the user operation call is passed to the next call phase. If false, the user operation return data is discarded. If both trackPreOpsReturnData and trackUserReturnData are true, they are concatenated.

  • delegateUser

    The user operation call is made using delegatecall from the Execution Environment. If false, the user operation is called using call.

  • requirePreSolver

    The preSolver hook is executed before the solver operation is executed. If false, the preSolver hook is skipped.

  • requirePostSolver

    The postSolver hook is executed after the solver operation is executed. If false, the postSolver hook is skipped.

  • zeroSolvers

    Allow the metacall (Atlas transaction) to proceed even if there are no solver operations. The solver operations do not necessarily need to be successful, but at least 1 must exist.

  • reuseUserOp

    If true, the metacall will revert if unsuccessful so as not to store nonce data, so the user operation can be reused.

  • userAuctioneer

    The user is allowed to be the auctioneer (the signer of the dApp operation). More than one auctioneer option can be set to true for the same dAppControl.

  • solverAuctioneer

    The solver is allowed to be the auctioneer (the signer of the dApp operation). If the solver is the auctioneer then their solver operation must be the only one. More than one auctioneer option can be set to true for the same dAppControl.

  • unknownAuctioneer

    Anyone is allowed to be the auctioneer - dAppOp.from must be the signer of the dApp operation, but the usual signatory[] checks are skipped. More than one auctioneer option can be set to true for the same dAppControl.

  • verifyCallChainHash

    Check that the dApp operation callChainHash matches the actual callChainHash as calculated in AtlasVerification.

  • forwardReturnData

    The return data from previous steps is included as calldata in the call from the Execution Environment to the solver contract. If false, return data is not passed to the solver contract.

  • requireFulfillment

    If true, a winning solver must be found, otherwise the metacall will fail.

  • trustedOpHash

    If true, the userOpHash excludes some user operation inputs such as value, gas, maxFeePerGas, nonce, deadline, and data, implying solvers trust changes made to these parts of the user operation after signing their associated solver operations.

  • invertBidValue

    If true, the solver with the lowest successful bid wins.

  • exPostBids

    Bids are found on-chain using _getBidAmount in Atlas, and solverOp.bidAmount is used as the max bid. If solverOp.bidAmount is 0, then there is no max bid limit for that solver.

  • multipleSuccessfulSolvers

    If true, the metacall will proceed even if a solver successfully pays their bid, and will be charged in gas as if it was reverted. If false, the auction ends after the first successful solver.

Override options

Some options are defined and retrieved by Atlas or other parties through view functions. These functions are defined in the dAppControl base contract, and are overridable. Some of them return default values, making them optional to override. Others are mandatory to override by the module contract.


getBidFormat

function getBidFormat(UserOperation calldata userOp) public view virtual returns (address bidToken);

This function returns the bid token used in auctions.

warning

Mandatory override. Not overriding this function will result in compilation failure.


getBidValue

function getBidValue(SolverOperation calldata solverOp) public pure override returns (uint256);

This function returns the desired bid value of a solver operation, in the case it needs any transformation. In most cases it will simply return solverOp.bidAmount.

warning

Mandatory override. Not overriding this function will result in compilation failure.


getSolverGasLimit

function getSolverGasLimit() public view virtual returns (uint32);

This function returns the gas limit allocated to the execution of the _preSolverCall hook, the solver operation, and the _postSolverCall hook.

info

Optional override. The default returned value is 1,000,000 (1 million gas).


getDAppGasLimit

function getDAppGasLimit() public view virtual returns (uint32);

This function returns the gas limit allocated to the execution of the _preOpsCall hook (this includes the _checkUserOperation hook, if enabled) and the _allocateValueCall hook.

info

Optional override. The default returned value is 2,000,000 (2 million gas).


Hooks

Hooks are customizable portions of code that are run at certain points of the Atlas execution. All hooks are defined in the base dAppControl contract and should be overridden in the module contract when necessary. Their default behavior is to do nothing, so there is no need to override a hook if the dApp does not want it to do anything in particular. Below is the full list of available hooks.


_checkUserOperation

function _checkUserOperation(UserOperation memory) internal virtual;

This hook is called as the first step inside the _preOpsCall hook. This means that in order to run this hook, the requirePreOps call config option must be enabled. The code in this hook should focus on validating the user operation, as per the dApp rules, if necessary.

info

Optional hook.


_preOpsCall

function _preOpsCall(UserOperation calldata) internal virtual returns (bytes memory);

This hook is called before the execution of the user operation. All pre-operations (prior to the user operation) should be run in this hook, if necessary. For this hook to run, the requirePreOps call config option must be enabled.

The returned data (bytes) from this function can be passed down to the _allocateValueCall hook, which can be very handy at times. To enable this, set the trackPreOpsReturnData call config option to true.

info

Optional hook. To enable it, set the requirePreOps call config option to true.


_preSolverCall

function _preSolverCall(SolverOperation calldata, bytes calldata) internal virtual

This hook is called before each solver operation's execution. All pre-operations (prior to the solver operation) should be run in this hook, if necessary. For this hook to run, the requirePreSolver call config option must be enabled.

info

Optional hook. To enable it, set the requirePreSolver call config option to true.


_postSolverCall

function _postSolverCall(SolverOperation calldata, bytes calldata) internal virtual

This hook is called after each solver operation's execution. All post-operations (subsequent to the solver operation) should be run in this hook, if necessary. For this hook to run, the requirePostSolver call config option must be enabled.

info

Optional hook. To enable it, set the requirePostSolver call config option to true.


_allocateValueCall

function _allocateValueCall(bool solved, address bidToken, uint256 bidAmount, bytes calldata data) internal virtual;

This hook is called after a successful solver operation. The distribution of funds (collected from the winning solver operation's bid) should be implemented in this hook. There is no call config option to enable for this hook, as it is a mandatory one.

warning

Mandatory hook. Not overriding this hook will result in compilation failure.


Initialization

Once deployed, the module needs to be enabled on Atlas. This is done by calling the initializeGovernance function on the AtlasVerification contract. The function must be called by the module's deployer (referred as governance in Atlas).

Initializing a module
interface IAtlasVerification {
function initializeGovernance(address control) external;
}

address atlasVerificationAddress = address(0x01);
address moduleAddress = address(0x02);

// Activating our module (dApp control)
IAtlasVerification(atlasVerificationAddress).initializeGovernance(moduleAddress);

Conclusion

Creating a module from scratch requires careful planning. DApps are encouraged to study existing modules to gain more context. It is also recommended to read the bootstrapping a module guide, which analyzes an example module contract.