Filling Methods
Different methods for executing DCA orders
DCA.fun supports two methods for executing orders, each optimized for different use cases and capital requirements. Both methods use Permit2 for secure token transfers.
Prerequisites: Permit2 Approval
Before filling any orders, you must approve the Permit2 contract to transfer tokens on your behalf:
const PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
// One-time approval for each output token
async function approvePermit2(tokenAddress) {
const token = new Contract(tokenAddress, ERC20_ABI, signer);
const allowance = await token.allowance(signer.address, PERMIT2_ADDRESS);
if (allowance.eq(0)) {
const tx = await token.approve(
PERMIT2_ADDRESS,
ethers.constants.MaxUint256
);
await tx.wait();
console.log(`Approved Permit2 for ${tokenAddress}`);
}
}Method 1: Direct Fill
The simplest method for filling orders, requiring upfront capital.
Function Signature
function fillOrder(
bytes calldata encodedData,
address recipient
) external nonReentrant whenOrderFillNotPausedParameters
encodedData: ABI-encoded order data from the quote APIrecipient: Address to receive the input tokens (usually the filler's address)
Implementation Flow
- Quote order using the API to get encodedData
- Ensure token balance for the output amount
- Approve Permit2 (one-time per token)
- Call fillOrder with the quote data
- Receive input tokens directly to recipient address
Example Implementation
async function directFill(orderId, chainId) {
try {
// 1. Get quote from API
const response = await fetch(
`https://api.dca.fun/quote?orderId=${orderId}&chainId=${chainId}`
);
const quote = await response.json();
// 2. Calculate profitability
const profit = calculateProfit(quote);
if (profit.profitUSD < minProfitThreshold) {
console.log("Order not profitable enough");
return;
}
// 3. Check output token balance
const outputToken = new Contract(outputTokenAddress, ERC20_ABI, signer);
const balance = await outputToken.balanceOf(signer.address);
const requiredAmount = quote.orderValidation.amountOfTokenOut;
if (balance.lt(requiredAmount)) {
throw new Error("Insufficient output token balance");
}
// 4. Ensure Permit2 approval
await approvePermit2(outputTokenAddress);
// 5. Execute fill order
const tx = await dcaDotFun.fillOrder(
quote.encodedData,
signer.address // recipient of input tokens
);
const receipt = await tx.wait();
console.log(`Order ${orderId} filled! TX: ${receipt.transactionHash}`);
// Input tokens are now in your wallet
// The protocol transfers output tokens via Permit2
} catch (error) {
console.error(`Failed to fill order ${orderId}:`, error);
}
}Transaction Flow
- Protocol transfers output tokens from filler to order vault (via Permit2)
- Protocol transfers input tokens from vault to recipient
- Protocol takes fee from output tokens to fee collector
- Order state is updated (repeats decreased, lastRun updated)
Pros and Cons
Pros:
- Simple implementation
- Lower gas costs (no callback overhead)
- Direct control over execution
- Predictable behavior
Cons:
- Requires upfront capital
- Capital locked until arbitrage execution
- Limited to available balance
Method 2: Callback Fill
Advanced execution method allowing for atomic arbitrage and flash loans.
⚠️ Warning: Risk Of Fund Loss
fillOrder with callback is designed for maximum implementation flexibility. The fillOrderCallback comes with risks that need to be well understood based on your use case.
Function Signature
function fillOrder(
bytes calldata encodedData,
address callback,
address recipient,
bytes calldata data
) external nonReentrant whenOrderFillNotPausedParameters
encodedData: ABI-encoded order data from the quote APIcallback: Address of your callback contractrecipient: Address to receive the input tokensdata: Custom data passed to your callback
Callback Interface
Your callback contract must implement the IFillOrderCallback interface:
interface IFillOrderCallback {
/// @notice Callback function executed during order fill
/// @param data The custom data passed from fillOrder call
/// @return success Must return true for fill to complete
function fillOrderCallback(
bytes calldata data
) external returns (bool);
}Implementation Flow
- Quote order using the API
- Call fillOrder with callback parameters
- Protocol sends input tokens to recipient
- Protocol calls your callback contract
- Callback executes arbitrage logic
- Callback approves output tokens to Permit2
- Protocol pulls output tokens via Permit2
- Transaction completes atomically
Example Callback Contract
The following snippet demonstrates a transient lock pattern to prevent unauthorized callback execution which could drain contract funds upto the permit2 allowance.
This is unaudited code not meant for production.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.25;
contract SwapRouter is IFillOrderCallback {
/*///////////////////////////////////////////////////////////////
State Variables
///////////////////////////////////////////////////////////////*/
// The slot holding the unlocked state, transiently. keccak256("locked")
bytes32 internal constant LOCKED_SLOT = 0xab99c6d7581cbb37d2e578d3097bfdd3323e05447f1fd7670b6c3a3fb9d9ff79;
...
/*///////////////////////////////////////////////////////////////
Errors
///////////////////////////////////////////////////////////////*/
...
/*///////////////////////////////////////////////////////////////
Events
///////////////////////////////////////////////////////////////*/
...
/*///////////////////////////////////////////////////////////////
Modifiers
///////////////////////////////////////////////////////////////*/
/// @notice CRITICAL: This modifier prevents unauthorized callback execution
modifier onlyWhenUnlocked() {
if (!isUnlocked()) revert NotUnlocked();
_;
}
/*///////////////////////////////////////////////////////////////
Constructor
///////////////////////////////////////////////////////////////*/
...
/*///////////////////////////////////////////////////////////////
Transient Lock Functions
///////////////////////////////////////////////////////////////*/
/// @notice Sets the transient lock to unlocked state
function unlock() internal {
assembly ("memory-safe") {
tstore(LOCKED_SLOT, true)
}
}
/// @notice Sets the transient lock to locked state
function lock() internal {
assembly ("memory-safe") {
tstore(LOCKED_SLOT, false)
}
}
/// @notice Checks if the contract is in unlocked state
function isUnlocked() internal view returns (bool unlocked) {
assembly ("memory-safe") {
unlocked := tload(LOCKED_SLOT)
}
}
/*///////////////////////////////////////////////////////////////
Token Management
///////////////////////////////////////////////////////////////*/
...
/*///////////////////////////////////////////////////////////////
Execute Swap
///////////////////////////////////////////////////////////////*/
/// @notice Executes a swap against a DcaDotFun order
/// @dev CRITICAL: unlock() MUST be called before fillOrder, lock() after
/// @param encodedData_ The encoded order data used to fill the order
/// @param data_ The data passed back to address(this) fillOrderCallback
function executeSwap(bytes calldata encodedData_, bytes calldata data_) external {
// 1. UNLOCK before calling fillOrder
unlock();
// 2. Call fillOrder - this will trigger fillOrderCallback
IDcaDotFun(dcaDotFun).fillOrder(
encodedData_,
address(this), // callback address
address(this), // recipient for tokenIn
data_
);
// 3. LOCK after fillOrder completes
lock();
}
/// @notice PROTECTED by onlyWhenUnlocked modifier
function fillOrderCallback(bytes calldata data_) external onlyWhenUnlocked returns (bool) {
...
}
/*///////////////////////////////////////////////////////////////
Withdraw
///////////////////////////////////////////////////////////////*/
...
receive() external payable {}
}Key Security Components
| Component | Purpose |
|---|---|
LOCKED_SLOT | Transient storage slot for the lock state |
unlock() | Sets transient lock before calling fillOrder |
lock() | Clears transient lock after fillOrder completes |
onlyWhenUnlocked | Modifier that reverts if callback is called without lock |
JavaScript Integration
async function callbackFill(orderId, chainId, callbackAddress) {
try {
// 1. Get quote
const response = await fetch(
`https://api.dca.fun/quote?orderId=${orderId}&chainId=${chainId}`
);
const quote = await response.json();
// 2. Prepare callback data
const callbackData = ethers.utils.defaultAbiCoder.encode(
["address", "address", "uint256", "uint256", "bytes"],
[
tokenInAddress,
tokenOutAddress,
quote.orderValidation.fillableAmount,
quote.orderValidation.amountOfTokenOut,
swapCalldata, // Your DEX swap data
]
);
// 3. Execute fill with callback
const tx = await dcaDotFun.fillOrder(
quote.encodedData,
callbackAddress, // Your deployed callback contract
callbackAddress, // Recipient of input tokens
callbackData // Data for your callback
);
const receipt = await tx.wait();
console.log(
`Order ${orderId} filled with callback! TX: ${receipt.transactionHash}`
);
} catch (error) {
console.error(`Callback fill failed:`, error);
}
}Pros and Cons
Pros:
- No upfront capital required
- Atomic arbitrage execution
- Flash loan compatible
- Complex routing strategies possible
- Maximum capital efficiency
Cons:
- Requires deploying callback contract
- Higher gas costs
- More complex implementation
- Additional smart contract risk
Security Considerations
For Both Methods:
- Always validate quote data before execution
- Monitor gas prices
- Use MEV protection (flashbots, etc.)
- Implement proper error handling
- Track transaction status
For Callbacks Specifically:
- Audit your callback contract thoroughly
- Implement reentrancy guards
- Validate all external calls
- Handle reverts gracefully
- Include emergency withdrawal functions
- Restrict access to owner-only functions
- Understand your use case risks
Gas Optimization Tips
- Batch Operations: Fill multiple orders in one transaction when possible
- Optimize Callbacks: Minimize external calls and storage operations
- Pre-calculate: Do calculations off-chain when possible
- Use Events: Emit events for tracking instead of storing data
- Efficient Routing: Find optimal swap paths before execution