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.
-
userNoncesSequentialThe 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.
-
dappNoncesSequentialThe 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. -
requirePreOpsThe
preOpshook is executed before the user operation is executed. If false, thepreOpshook is skipped. the dApp control should check the validity of the user operation (whether it can supportuserOp.dappanduserOp.data) in thepreOpshook. -
trackPreOpsReturnDataThe return data from the
preOpshook is passed to the next call phase. If false,preOpsreturn data is discarded. If bothtrackPreOpsReturnDataandtrackUserReturnDataare true, they are concatenated. -
trackUserReturnDataThe 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
trackPreOpsReturnDataandtrackUserReturnDataare true, they are concatenated. -
delegateUserThe user operation call is made using
delegatecallfrom the Execution Environment. If false, the user operation is called usingcall. -
requirePreSolverThe
preSolverhook is executed before the solver operation is executed. If false, thepreSolverhook is skipped. -
requirePostSolverThe
postSolverhook is executed after the solver operation is executed. If false, thepostSolverhook is skipped. -
zeroSolversAllow 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. -
reuseUserOpIf true, the
metacallwill revert if unsuccessful so as not to store nonce data, so the user operation can be reused. -
userAuctioneerThe 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.
-
solverAuctioneerThe 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.
-
unknownAuctioneerAnyone is allowed to be the auctioneer -
dAppOp.frommust 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. -
verifyCallChainHashCheck that the dApp operation
callChainHashmatches the actualcallChainHashas calculated inAtlasVerification. -
forwardReturnDataThe 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.
-
requireFulfillmentIf true, a winning solver must be found, otherwise the metacall will fail.
-
trustedOpHashIf true, the
userOpHashexcludes 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. -
invertBidValueIf true, the solver with the lowest successful bid wins.
-
exPostBidsBids are found on-chain using
_getBidAmountin Atlas, andsolverOp.bidAmountis used as the max bid. IfsolverOp.bidAmountis 0, then there is no max bid limit for that solver. -
multipleSuccessfulSolversIf 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.