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.

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.
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.

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, thepreOps
hook is skipped. the dApp control should check the validity of the user operation (whether it can supportuserOp.dapp
anduserOp.data
) in thepreOps
hook. -
trackPreOpsReturnData
The return data from the
preOps
hook is passed to the next call phase. If false,preOps
return data is discarded. If bothtrackPreOpsReturnData
andtrackUserReturnData
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
andtrackUserReturnData
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 usingcall
. -
requirePreSolver
The
preSolver
hook is executed before the solver operation is executed. If false, thepreSolver
hook is skipped. -
requirePostSolver
The
postSolver
hook is executed after the solver operation is executed. If false, thepostSolver
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 usualsignatory[]
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 actualcallChainHash
as calculated inAtlasVerification
. -
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 asvalue
,gas
,maxFeePerGas
,nonce
,deadline
, anddata
, 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, andsolverOp.bidAmount
is used as the max bid. IfsolverOp.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.
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
.
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.
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.
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.
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.
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.
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.
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.
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).
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.