Filler guide
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.
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
// SPDX-License-Identifier: MIT
pragma solidity 0.8.25;
import {IFillOrderCallback} from "./interfaces/IFillOrderCallback.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {ISwapRouter} from "./interfaces/ISwapRouter.sol";
contract FillerCallback is IFillOrderCallback {
address constant PERMIT2 = 0x000000000022D473030F116dDEE9F6B43aC78BA3;
address constant UNISWAP_ROUTER = 0x...; // Your DEX router
address public immutable owner;
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
constructor() {
owner = msg.sender;
}
function fillOrderCallback(bytes calldata data) external returns (bool) {
// Decode the data passed from your script
(
address tokenIn,
address tokenOut,
uint256 amountIn,
uint256 amountOutRequired,
bytes memory swapData
) = abi.decode(data, (address, address, uint256, uint256, bytes));
// At this point, we have received tokenIn tokens
// 1. Approve router to spend our input tokens
IERC20(tokenIn).approve(UNISWAP_ROUTER, amountIn);
// 2. Execute swap on DEX
uint256 amountReceived = ISwapRouter(UNISWAP_ROUTER).exactInputSingle(
ISwapRouter.ExactInputSingleParams({
tokenIn: tokenIn,
tokenOut: tokenOut,
fee: 3000, // 0.3% fee tier
recipient: address(this),
deadline: block.timestamp,
amountIn: amountIn,
amountOutMinimum: amountOutRequired,
sqrtPriceLimitX96: 0
})
);
// 3. Approve Permit2 to take the output tokens
IERC20(tokenOut).approve(PERMIT2, amountOutRequired);
// 4. Keep any profit
uint256 profit = amountReceived - amountOutRequired;
if (profit > 0) {
IERC20(tokenOut).transfer(owner, profit);
}
return true;
}
// Emergency withdrawal function
function withdraw(address token) external onlyOwner {
uint256 balance = IERC20(token).balanceOf(address(this));
if (balance > 0) {
IERC20(token).transfer(owner, balance);
}
}
}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);
}
}Flash Loan Integration
Callbacks enable flash loan arbitrage:
contract FlashLoanCallback is IFillOrderCallback, IFlashLoanReceiver {
function fillOrderCallback(bytes calldata data) external returns (bool) {
// Decode parameters
(address tokenOut, uint256 amountRequired) =
abi.decode(data, (address, uint256));
// 1. Initiate flash loan for output tokens
ILendingPool(AAVE_POOL).flashLoan(
address(this),
tokenOut,
amountRequired,
data
);
return true;
}
function executeOperation(
address asset,
uint256 amount,
uint256 premium,
address initiator,
bytes calldata params
) external returns (bool) {
// 2. Approve Permit2 to take flash loaned tokens
IERC20(asset).approve(PERMIT2, amount);
// 3. After fill completes, we have input tokens
// 4. Swap input tokens back to repay flash loan
// 5. Keep profit
return true;
}
}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
Choosing the Right Method
Use Direct Fill When:
- You have sufficient capital
- Simplicity is preferred
- Gas efficiency is critical
- Testing or starting out
- Small to medium order sizes
Use Callback Fill When:
- Capital efficiency is paramount
- Using flash loans
- Implementing complex arbitrage
- Large order sizes
- MEV protection needed
Security Considerations
For Both Methods:
- Always validate quote data before execution
- Monitor gas prices to maintain profitability
- 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
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