DApp Module Development
The DAppControl
system within the Atlas Protocol provides a structured approach for integrating dApps. This guide walks you through understanding the hierarchy, implementing necessary functionalities, and ensuring your dApp aligns with Atlas Protocol standards.
DApp Module Overview
The core of your integration with Atlas Protocol is the CustomDAppModule, which is your concrete implementation of the DAppControl contract. This module serves as a crucial bridge between the Atlas Protocol and your DApp, enabling meta-transaction functionality without significant modifications to your core DApp logic.
Key Features of Your DApp Module:
- Interprets and Validates Atlas UserOperations: Implements logic to accurately interpret incoming
UserOperation
data and perform necessary validations. - Translates Meta-Transactions: Converts Atlas meta-transactions into corresponding function calls within your DApp.
- Manages State Transitions and Data Flow: Oversees the flow of data and state changes between the Atlas Protocol and your DApp.
- Handles Bid Processing and Value Allocation: Manages bid-related functionalities and allocates values according to your DApp's economic model.
Your DApp module inherits from a hierarchical structure that ensures adherence to protocol standards while allowing flexibility for custom implementations:
Contracts Overview
-
DAppControlTemplate
- Description: The most abstract contract in the hierarchy, defining essential internal virtual functions that serve as the foundation for dApp modules.
- Role: Acts as a blueprint, ensuring that all dApp modules implement necessary functionalities.
-
DAppControl
- Description: Inherits from
DAppControlTemplate
and introduces external functions that wrap the abstract internal functions. These external functions apply necessary modifiers for access control and execution phase management. - Role: Provides a structured interface for dApp modules to interact with the Atlas Protocol, ensuring that all operations are executed within the correct context and with appropriate permissions.
- Description: Inherits from
-
CustomDAppModule (Your DApp Control Contract)
- Description: A concrete contract that inherits from
DAppControl
. It implements the abstract functions to bridge Atlas meta-transactions with the actual DApp's transactions. - Role: Acts as an intermediary layer between the Atlas Protocol and the DApp, translating meta-transactions into standard DApp interactions.
- Description: A concrete contract that inherits from
Prerequisites
Before diving into module development, ensure you have the following:
- Solidity Knowledge: Familiarity with Solidity programming language and smart contract development.
- Development Environment: Setup with tools foundy in the Getting Started with Atlas guide.
- Atlas Protocol Understanding: Basic knowledge of Atlas Protocol's architecture and functionalities.
Step-by-Step Implementation
1. Inherit from DAppControl
Begin by creating a new contract that inherits from DAppControl. This establishes the foundational structure required for your dApp module.
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.25;
import { DAppControl } from "./DAppControl.sol";
import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol";
contract MyDAppControl 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,
requirePostOps: true,
zeroSolvers: true, // Enabled to allow transactions with zero solvers
reuseUserOp: true, // Enabled because userOps can be reused
userAuctioneer: true,
solverAuctioneer: false,
unknownAuctioneer: false,
verifyCallChainHash: true,
forwardReturnData: false,
requireFulfillment: false,
trustedOpHash: false,
invertBidValue: false,
exPostBids: false,
allowAllocateValueFailure: false
})
)
{
// Additional initialization if necessary
}
// Implement abstract functions here
}
Explanation
-
Inheritance:
MyDAppControl
inherits fromDAppControl
, leveraging its foundational functionalities. -
Constructor Parameters:
atlas
: Address of the Atlas contract. (see deployments for the latest addresses)msg.sender
: Sets the deployer as the initial governance.CallConfig
: Configures various operational parameters for your dApp.
-
CallConfig Fields:
- customizable parameters for your dApp module. (see DApp Configuration Guide for more details)
2. Implement Required Hook Functions
These functions are abstract or virtual in the base DAppControl contract (or its parent contracts) and do not have implementations within DAppControl itself. Any contract inheriting from DAppControl must provide concrete implementations for these functions to ensure the dApp operates as intended.
a. _preOpsCall
function _preOpsCall(UserOperation calldata userOp) internal virtual returns (bytes memory);
- Purpose: Executes custom logic before a user operation is processed.
- Implementation Requirement: Must handle any pre-operation checks or preparations and return necessary data for subsequent phases.
b. _allocateValueCall
function _allocateValueCall(address bidToken, uint256 bidAmount, bytes calldata data) internal virtual;
- Purpose: Manages the allocation of bid values, such as distributing tokens to designated recipients after a successful solver operation.
- Implementation Requirement: Must define how bid amounts are allocated, ensuring secure and accurate distribution.
c. _preSolverCall
function _preSolverCall(SolverOperation calldata solverOp, bytes calldata returnData) internal virtual;
- Purpose: Prepares necessary assets or state before executing a solver operation.
- Implementation Requirement: Must handle any preparatory steps, such as transferring assets to solvers.
d. _postSolverCall
function _postSolverCall(SolverOperation calldata solverOp, bytes calldata returnData) internal virtual;
- Purpose: Verifies the results of a solver operation after its execution.
- Implementation Requirement: Must validate the success of the solver operation and handle any post-execution logic.
e. _postOpsCall
function _postOpsCall(bool solved, bytes calldata data) internal virtual;
- Purpose: Executes logic after all operations have been processed, such as emitting events based on transaction outcomes.
- Implementation Requirement: Must handle any finalization steps, like logging or state updates.
3. Implement Public View Functions
These functions are used by the backend to verify bid format and by the factory and DAppVerification to verify the backend. They are typically marked as virtual and may be abstract in the base contract, requiring concrete implementations in the inheriting contract. These functions provide specific configurations or values related to the dApp's operations, ensuring proper integration with the Atlas Protocol.
Key functions in this category include:
getBidFormat
: Specifies the expected ERC20 token for bids.getBidValue
: Retrieves the bid amount from a solver operation.getSolverGasLimit
: Defines the gas limit for solver operations.
Implementing these functions correctly is crucial for the proper functioning of your dApp within the Atlas ecosystem.
a. getBidFormat
function getBidFormat(UserOperation calldata userOp) public view virtual returns (address bidToken);
- Purpose: Specifies the ERC20 token address expected for bids.
- Implementation Requirement: Must return the address of the token used for bidding in your dApp.
b. getBidValue
function getBidValue(SolverOperation calldata solverOp) public view virtual returns (uint256);
- Purpose: Retrieves the bid amount from a solver operation.
- Implementation Requirement: Must return the bid amount relevant to the solver operation.
c. getSolverGasLimit (optional)
function getSolverGasLimit() public view virtual returns (uint32);
- Purpose: Defines the gas limit for solver operations.
- Implementation Requirement: Must return the gas limit appropriate for the solver operation.
- Override If: You have a different gas limit requirement per solver operation.
DEFAULT_SOLVER_GAS_LIMIT
(1_000_000)
4. Optional Functions to Override
These functions come with default implementations in the DAppControl contract. While they are not required to be overridden, doing so can provide custom behaviors tailored to your dApp's specific needs.
Governance Management Functions
These functions handle the transfer and acceptance of governance. The base implementations ensure standard governance behavior, but you may override them to introduce additional checks or custom logic.
a. transferGovernance
function transferGovernance(address newGovernance) external override mustBeCalled {
super.transferGovernance(newGovernance);
}
- Purpose: Initiates the transfer of governance to a new address.
- Default Behavior: Calls the parent contract's transferGovernance method.
- Override If: You need to add extra validations or emit additional events during governance transfer.
b. acceptGovernance
function acceptGovernance() external override mustBeCalled {
super.acceptGovernance();
}
- Purpose: Allows the new governance address to accept governance.
- Default Behavior: Calls the parent contract's acceptGovernance method.
- Override If: You require custom actions upon governance acceptance.
Configuration Retrieval Functions
These functions provide access to the dApp's configuration settings. While they have default implementations, you might override them to modify how configurations are retrieved or processed.
a. getCallConfig
function getCallConfig() external view returns (CallConfig memory) {
return _getCallConfig();
}
- Purpose: Returns the current CallConfig settings of the dApp.
- Default Behavior: Decodes and returns the CALL_CONFIG state variable.
- Override If: You need to alter the configuration retrieval process or expose additional configuration details.
b. getDAppSignatory
function getDAppSignatory() external view mustBeCalled returns (address) {
return governance;
}
- Purpose: Retrieves the current governance address.
- Default Behavior: Returns the governance state variable.
- Override If: You have a different mechanism for determining the signatory or need to include additional logic.
Additional Utility Functions
These functions provide supplementary information about the dApp's operational requirements. They are generally straightforward and may not require overriding unless specific behaviors are needed.
a. userDelegated
function userDelegated() external view returns (bool delegated) {
delegated = CALL_CONFIG.needsDelegateUser();
}
- Purpose: Indicates whether user operations are delegated.
- Default Behavior: Returns the result based on CALL_CONFIG.
- Override If: You need to change how delegation is determined.
b. requireSequentialUserNonces
function requireSequentialUserNonces() external view returns (bool isSequential) {
isSequential = CALL_CONFIG.needsSequentialUserNonces();
}
- Purpose: Indicates whether user nonces must be sequential.
- Default Behavior: Returns the result based on CALL_CONFIG.
- Override If: Your dApp has different nonce requirements.
c. requireSequentialDAppNonces
function requireSequentialDAppNonces() external view returns (bool isSequential) {
isSequential = CALL_CONFIG.needsSequentialDAppNonces();
}
- Purpose: Indicates whether dApp nonces must be sequential.
- Default Behavior: Returns the result based on CALL_CONFIG.
- Override If: Your dApp has different nonce requirements.
5. Implementation Notes
- The
DAppControl
contract will be called as delegatecall by the Atlas Protocol which means only constants and immutable variables can be safely accessed during the hook execution.
How to implement a updatable state variable
Example if you want to allow some values to be updated
- make sure to use the
onlyGovernance
modifier - make the variable or function
public
contract MyDAppControl is DAppControl {
uint256 public myCustomVaule;
event MyCustomValueUpdated(uint256 oldValue, uint256 newValue);
constructor(
address _atlas,
address _myCustomVaule,
)
DAppControl(
_atlas,
msg.sender,
CallConfig({
// all other config
})
)
{
// Set bidToken to constant ETH if zero address
myCustomVaule = _myCustomVaule;
}
/**
* @notice This function is called by the owner to set myCustomVaule
* @param _myCustomVaule The unit256 of the bid token
* @dev This function is only callable by the owner
*/
function setMyCustomVaule(unit256 _myCustomVaule) external onlyGovernance {
require(_myCustomVaule != _myCustomVaule, "myCustomVaule is already set");
emit MyCustomValueUpdated(myCustomVaule, _myCustomVaule);
myCustomVaule = _myCustomVaule;
}
// other functions
// Modifier to check if the caller is the governance address
modifier onlyGovernance() {
address _dAppGov = CamelotDAppControl(this).getDAppSignatory();
if (msg.sender != _dAppGov) revert OnlyGovernance();
_;
}
How to access the public myCustomValue during hook execution
We need to access the function via CONTROL.staticcall and decode the data otherwise values will be 0 or empty since those state value haven't been set in the contect of the execution environment.
- if you need to access multiple variables consider using a struct or a helper function
A common use case would be if you want to allow some values to be updated after deployment such as
- Dapp Address references
- reward payout address
- percentage allocation etc
// override _allocateValueCall
function _allocateValueCall(address _bidToken, uint256 bidAmount, bytes calldata data) internal virtual override {
// read myCustomValue from the contract
(bool success, bytes memory _myCustomValueData) =
CONTROL.staticcall(abi.encodeWithSelector(this.myCustomValue.selector));
//Check if the call was successful
if (!success) revert InvalidMyCustomValue();
// Decode the custom value from the calldata
(uint256 _myCustomValue) = abi.decode(_myCustomValueData, (uint256));
//do something with the custom value
}
Example Dummy Implementation
Below is a complete example of a concrete DAppControl contract named MyDAppControl. This contract implements all required abstract functions and integrates custom logic specific to the dApp's needs.
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.25;
import { DAppControl } from "./DAppControl.sol";
import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol";
contract MyDAppControl is DAppControl {
event CustomEvent(bool solved, bytes data);
constructor(address atlas, address initialGovernance, CallConfig memory callConfig)
DAppControl(atlas, initialGovernance, callConfig)
{}
// Override _preOpsCall to perform preliminary checks
function _preOpsCall(UserOperation calldata userOp) internal override returns (bytes memory) {
// Example: Validate user operation parameters
require(userOp.someParameter == expectedValue, "Invalid parameter");
// Return data for next phase
return abi.encodePacked("PreOpsData");
}
// Override _allocateValueCall to distribute bid amounts
function _allocateValueCall(address bidToken, uint256 bidAmount, bytes calldata data) internal override {
// Decode recipient address from data
address recipient = abi.decode(data, (address));
// Transfer bidAmount of bidToken to recipient
SafeTransferLib.safeTransfer(bidToken, recipient, bidAmount);
}
// Override _preSolverCall to prepare for solver operation
function _preSolverCall(SolverOperation calldata solverOp, bytes calldata returnData) internal override {
// Example: Transfer required ETH to solver
require(address(this).balance >= solverOp.requiredAmount, "Insufficient balance");
SafeTransferLib.safeTransferETH(solverOp.solverAddress, solverOp.requiredAmount);
}
// Override _postSolverCall to validate solver execution
function _postSolverCall(SolverOperation calldata solverOp, bytes calldata returnData) internal override {
// Decode success flag from returnData
bool success = abi.decode(returnData, (bool));
require(success, "Solver operation failed");
}
// Override _postOpsCall to perform final actions
function _postOpsCall(bool solved, bytes calldata data) internal override {
// Emit a custom event based on the outcome
emit CustomEvent(solved, data);
}
// Define the expected bid token format
function getBidFormat(UserOperation calldata userOp) public view override returns (address bidToken) {
// Return the address of the ERC20 token used for bids
return 0xYourBidTokenAddress;
}
// Define how to retrieve the bid value from a solver operation
function getBidValue(SolverOperation calldata solverOp) public view override returns (uint256) {
// Return the bid amount specified in the solver operation
return solverOp.bidAmount;
}
}
Explanation:
- CustomEvent: An example of a custom event emitted in the _postOpsCall function.
- Constructor: Initializes the contract by passing necessary parameters to the DAppControl constructor.
- Overridden Functions: Each abstract function from DAppControlTemplate is implemented with dApp-specific logic.
- _preOpsCall: Validates user operation parameters.
- _allocateValueCall: Distributes bid amounts to a designated recipient.
- _preSolverCall: Transfers required ETH to the solver.
- _postSolverCall: Validates the success of the solver operation.
- _postOpsCall: Emits a custom event based on the transaction outcome.
- Getters: Defines the expected bid token and retrieves the bid value from solver operations.
Note: This guide assumes familiarity with Solidity, smart contract development, and the Atlas Protocol's architecture. For a deeper understanding, refer to the Atlas Documentation and explore related contracts and libraries within the protocol's ecosystem.