Detailed Walkthrough
Note: This walkthrough provides a step-by-step guide for integrating with DA Builder. For a complete working example, see our sample integration repository.
For complete configuration details including endpoints and contract addresses, see the Quick Reference.
1. Implement a Trustless Proposer
DA Builder only allows transactions to be submitted by an EOA with valid EIP-7702 account code set. The EIP-7702 account code must implement the IProposer
interface.
interface IProposer {
function call(address _target, bytes calldata _data, uint256 _value) external returns (bool);
}
This enables DA Builder to aggregate transactions from multiple users and execute them as a single transaction on their behalf.
We recommend using a trustless proposer contract similar to the following example. A trustless proposer can use EIP-712 signatures as a verification process to ensure that the only person able to generate a transaction the proposer will execute is the EOA owner. DA Builder is then able to batch the transaction with those of other users to reduce costs.
contract TrustlessProposer {
bytes32 public constant CALL_TYPEHASH =
keccak256("Call(uint256 deadline,uint256 nonce,address target,uint256 value,bytes calldata)");
address public immutable PROPOSER_MULTICALL;
uint256 public nestedNonce;
function call(address _target, bytes calldata _data, uint256 _value) external returns (bool) {
// The estimated gas used is not perfect but provides a meaningful bound to know if we went over the gas limit
uint256 _startGas = gasleft();
if (msg.sender != PROPOSER_MULTICALL && address(this) != msg.sender) revert Unauthorized();
// Decode the data parameter which contains: (signature, deadline, nonce, actual_calldata, gasLimit)
(bytes memory _sig, uint256 _deadline, uint256 _nonce, bytes memory _calldata, uint256 _gasLimit) =
abi.decode(_data, (bytes, uint256, uint256, bytes, uint256));
if (block.timestamp > _deadline) revert DeadlinePassed();
if (_nonce != nestedNonce) revert NonceTooLow();
// Create the EIP-712 message hash
bytes32 _structHash = keccak256(abi.encode(CALL_TYPEHASH, _deadline, _nonce, _target, _value, _calldata, _gasLimit));
bytes32 _messageHash = _hashTypedDataV4(_structHash);
address _signer = ecrecover(_messageHash, v, r, s);
if (_signer != address(this)) revert SignatureInvalid();
(bool _success,) = _target.call{value: _value}(_calldata);
if (!_success) revert LowLevelCallFailed();
nestedNonce++;
// If gas used is greater than gasLimit, revert
if (_startGas - gasleft() > _gasLimit) {
revert GasLimitExceeded();
}
return true;
}
}
How a Trustless Proposer Works
The TrustlessProposer
above implements three core security mechanisms:
Signature Protection: The
_data
parameter must contain a signature that you created offline, which the contract verifies matches your EOA addressNonce Protection: Each call requires a unique nonce to prevent replay attacks
Deadline Protection: Each call has a deadline to prevent stale transactions
⚠️ Important Safety Note:
Custom Storage Layout: EOA contracts should use a fixed storage location (
layout at
) to prevent storage conflicts when upgrading account code. Since EIP-7702 account code can be updated but storage persists, using a predictable storage layout ensures that e.g. thenestedNonce
variable won't conflict with future or past account code. See here for more details.
For a more complete implementation: See TrustlessProposer.sol in the sample repository.
2. EIP-7702 Account Code Setup
Set up your EOA to use the proposer contract as its account code. This allows others to execute operations on your behalf - your transactions can now be combined with other users' transactions:
// Set up EIP-7702 account code using Alloy
let authorization = Authorization {
chain_id: U256::from(chain_id),
address: proposer_address,
nonce: auth_nonce,
};
let auth_hash = authorization.signature_hash();
let signature = wallet.sign_hash_sync(&auth_hash)?;
let signed_authorization = authorization.into_signed(signature);
let tx = TransactionRequest::default()
.with_to(wallet_address)
.with_authorization_list(vec![signed_authorization])
.with_input(Bytes::new())
.with_chain_id(chain_id)
.with_nonce(nonce);
provider.send_transaction(tx).await?;
For complete EIP-7702 setup: See setup_eip7702_account_code in the sample repository.
3. Gas Tank Integration
Deposit funds into the DA Builder Gas Tank to pay for the gas used by your transactions. The savings from shared transactions are split amongst participants in the aggregated transaction:
// Deposit funds to Gas Tank
gas_tank_contract.deposit()
.value(deposit_amount)
.send()
.await?;
For complete Gas Tank integration: See deposit_to_gas_tank in the sample repository.
4. DA Builder Transaction Submission
Submit transactions through DA Builder's RPC endpoint. The call you make into DA Builder to execute the underlying transaction might be different depending on how you have implemented your Proposer. We provide an example client that interfaces with a TrustlessProposer contract in the sample integration repository:
// Create transaction request
let tx_request = TransactionRequest::default()
.with_to(target_address)
.with_input(calldata)
.with_value(value);
// Submit to DA Builder
let pending_tx = da_builder_client
.send_da_builder_transaction(tx_request, deadline_secs)
.await?;
// Wait for confirmation
let receipt = pending_tx.await?;
The send_da_builder_transaction
method automatically:
Signs the transaction with EIP-712
Submits to DA Builder's aggregation service
Returns a pending transaction for monitoring
For complete transaction submission: See send_da_builder_transaction in the sample repository.
EIP-712 Signature Generation
At its core, the TrustlessProposer expects a signed message hash to prove the EOA owner is the one that generated the transaction. Using Alloy:
// Create the message hash for EIP-712 signing
let call_type_hash = keccak256("Call(uint256 deadline,uint256 nonce,address target,uint256 value,bytes calldata)");
let struct_hash = keccak256(abi::encode(&[
call_type_hash.into(),
deadline.into(),
nonce.into(),
target.into(),
value.into(),
calldata.into(),
gas_limit.into(),
]));
let message_hash = _hashTypedDataV4(struct_hash);
let signature = wallet.sign_hash_sync(&message_hash)?;
For complete EIP-712 implementation: See prepare_trustless_proposer_call in the sample repository.
5. Transaction Monitoring
When you submit a transaction to DA Builder, you receive a DA Builder Request ID (not an immediate blockchain hash). Use this ID to track your transaction's progress:
// Submit transaction and get DA Builder Request ID
let pending_tx = da_builder_client
.send_da_builder_transaction(tx_request, deadline_secs)
.await?;
// Monitor using the DA Builder Request ID
let receipt = pending_tx.await?;
// The receipt contains the actual blockchain transaction details
if receipt.status == Some(1) {
println!("Transaction successful on blockchain");
} else {
println!("Transaction failed on blockchain");
}
Your transaction is batched with others and eventually included in an aggregated on-chain transaction. The final receipt contains the actual blockchain transaction hash that you can use for further tracking if needed.
Last updated