Deployed contracts
The deployed Base mainnet addresses, ownership, oracle configuration, canary evidence, and external dependencies, for third-party review.
The protocol is live on Base mainnet. Every contract source is verified on Basescan, the governance configuration was audited on chain, and the full create, fund, execute, withdraw lifecycle was observed in the mainnet canary recorded below. New strategies are created here.
These are the addresses your deposit interacts with. Deposit is USDC; you accumulate WETH or cbBTC on a schedule you set, and only you can withdraw.
Contracts
The account implementation is cloned for each user strategy. Everything else is a shared protocol component, deployed once.
| Contract | Address | What to review |
|---|---|---|
| DCAFactory | 0xda2943c3a2548609824c04a66d9de7c57edb0dd3 | Creates strategy accounts, stores fees, treasury, minimums, and approved yield-adapter implementations. |
| BatchExecutor | 0xb781bd6f59f46768a95ee5621d435084c7151fdd | Runs pooled buys, enforces the asset allowlist and oracle floor, takes the fee, distributes the output. |
| SwapRouter | 0xa0d356850137141ab7fb0ca5df5f6e450baaf19d | Routes each swap to the adapter configured for that asset. |
| UniswapV3Adapter | 0xcb80e1d2cbc3fe6b7a9bdbf5495572b5df5016fa | The only contract that talks to Uniswap V3. Holds the per-asset route. |
| OracleDispatcher | 0x540edca41ebab29b13fb1a0aab427d7e4aa181af | Routes each asset to its price oracle. This is the BatchExecutor's priceOracle(). |
| ChainlinkPriceOracle | 0x1c0ad083a8450e45e7c8d8ec38de72ea68fad2b2 | Direct Chainlink USD feeds for WETH and cbBTC, with staleness and sequencer-uptime checks. |
| ComposedPriceOracle | 0xdb0f9212fffd41adaf99b64bbac9642d39f5c066 | Deployed for market-feed assets; no asset is routed through it at launch. |
| AaveV3YieldAdapter implementation | 0xdbb08d3adc290e78cfa90424e745dd424f1a5c87 | Approved idle-yield implementation, cloned per strategy that opts into yield. |
| UserStrategyAccount implementation | 0xf420ad09e466f76a2f52dfa35c6eb411764c9229 | Implementation behind each per-user strategy clone. |
| TimelockController | 0xd6ab44f85db23f2abc0e8d5ead04578d73e22cca | Owns every administrative contract above. Two-day minimum delay on all of them. |
Ownership and roles
Administrative power is split deliberately. The timelock holds everything that could affect where money goes, and the guardian holds only the things that are useless if delayed.
| Role | Address | Power |
|---|---|---|
| Owner (timelock) | 0xd6ab44f85db23f2abc0e8d5ead04578d73e22cca | Owns all governed contracts. Every configuration change (fees, treasury, oracle, routes, asset allowlist, swap router, approved yield implementations) is queued publicly on chain and cannot execute for two days. |
| Guardian | 0x76772411e94E207D54d6C37d9dC93010426a925D | 2-of-3 Safe. Instant pause and unpause on DCAFactory and BatchExecutor, setExecutor on BatchExecutor, and flushing accrued fees to the treasury. Nothing else. Neither power can change routing or touch a user balance. |
| Treasury | 0x310d228297ce1014b33B93F052008FEC3A0329E5 | Receives protocol fees, in USDC. No control over user strategy funds. |
| Executor | 0x596716A2554643098D989b203bFa32A2a39E3E7B | The only address allowed to call executeBatch. It can trigger a due batch and nothing else: no withdrawal, no parameter change, no ability to lower the oracle floor. It runs the scheduled automation and is swappable by the guardian via setExecutor. |
The guardian Safe also holds the timelock's proposer, executor, and canceller roles. That lets it queue and later execute a change; it does not let it shorten the two-day delay.
The deploying address holds no privilege. The same deployment transaction batch that handed ownership to the timelock also revoked the deployer's proposer, executor, and canceller roles, and the deployment reverts unless that handover is complete.
User strategy accounts are not owned by the protocol. Each account stores its own user owner, and only that user can withdraw from it.
Canary evidence
Before opening to users, the lifecycle was exercised once on mainnet, end to end, and the figures below were read back from chain rather than taken from a run log. Anyone can verify them against the transactions.
One account, WETH only, Daily tier, one non-empty buy, Aave idle yield enabled, keeper-signed execution. This is the single-account happy path.
It demonstrates the real fee, yield, and withdrawal accounting on mainnet. It does not by itself demonstrate multi-account pro-rata distribution or long-term scheduled automation; those are covered by the invariant, fork, and multi-account tests and by ongoing monitoring.
Execution
| Observation | Value |
|---|---|
| Transaction | 0x5258dfd1…91fea |
| Block, result, gas | 51329142, success, 670,479 gas |
| Caller | 0x596716A2554643098D989b203bFa32A2a39E3E7B, the configured executor |
| Function | executeBatch(address[],uint256[],address,uint256,uint256), selector 0xa3cb075e |
| Submitted account | 0xaad5c2969b5997b105dc71a57c3ee620e7bd0c4e, for exactly 5,000,000 USDC base units |
| Execution fee | 7,500 USDC base units to the treasury. Exactly 15 bps of the USDC pulled, deducted before the swap |
| Into the swap | 4,992,500 USDC |
| Output | 1,993,301,164,436,226 wei WETH, all of it delivered to the strategy owner |
| BatchExecutor balances | Unchanged: 0 USDC and 0 WETH dust, accruedFees 0 (INV-001 held) |
Because the fee is taken in USDC up front, the entire swap output belongs to the accounts that paid for it. There is no second transfer of the purchased asset to the treasury to look for.
Withdrawal and closure
| Observation | Value |
|---|---|
| Transaction | 0x7bc7c65c…cf01e |
| Block, result, gas | 51337972, success, 251,182 gas |
| Function | withdraw(), signed by the strategy owner |
| Idle yield | 5,380 USDC base units gross Aave yield; 807 yield fee (15%); 4,573 net to the owner |
| Returned | Remaining principal plus net yield to the owner; the treasury received 8,307 USDC total (execution fee plus yield fee) |
| Final reads | closed() == true, balance() == 0, depositedAmount() == 0 |
| Registry | The closed strategy left the active registry |
Withdrawal is a full exit and it is terminal. The closed strategy left the active registry and can never be redeposited into.
Supported assets
The protocol launched with the two assets that have an approved direct Uniswap route and a direct Chainlink USD feed. Governance can add more, each behind the two-day timelock, and never an asset without oracle coverage.
| Asset | Token address | Swap route | Price floor |
|---|---|---|---|
| WETH | 0x4200000000000000000000000000000000000006 | Uniswap V3 direct from USDC | Chainlink ETH/USD, direct feed |
| cbBTC | 0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf | Uniswap V3 direct from USDC | Chainlink cbBTC/USD, direct feed |
An asset with no oracle coverage is never whitelisted, so it is not possible for a batch to execute against an asset that has no price floor.
Oracle configuration
The executor submits a minimum output for each batch, and the contract independently computes its own floor. The swap must clear the stricter of the two, so the executor can only ever make slippage tighter, never looser.
On mainnet that floor comes from Chainlink direct USD feeds for WETH and cbBTC, each with its own staleness window, a conservative slippage haircut, and an L2 sequencer-uptime check with a grace period after recovery. The ComposedPriceOracle is deployed for future market-feed assets but carries no active route at launch. The full design is on the Price oracle page.
Idle yield
USDC waiting between buys can earn Aave V3 yield, chosen per strategy at creation against the approved adapter implementation above. Yield is optional: a strategy can hold idle USDC without lending it. The yield fee applies only to positive yield and only at withdrawal.
External dependencies
| Dependency | Address | Purpose |
|---|---|---|
| USDC | 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 | Deposit asset. |
| Uniswap V3 Router | 0x2626664c2603336E57B271c5C0b26F421741e481 | Swap execution. |
| Aave V3 Pool | 0xA238Dd80C259a72e81d7e4664a9801593F98d1c5 | Idle-yield venue for strategies that opt in. |
| Chainlink L2 sequencer feed | 0xBCF85224fc0756B9Fa45aA7892530B47e10b6433 | Uptime check; pricing is rejected while the sequencer is down or inside its grace period. |
Fees and controls
Fees are stored in DCAFactory and apply protocol-wide. A fee change applies to existing strategies going forward, and is bounded by a hard on-chain cap that no owner can exceed.
| Fee | Current | Hard cap | Charged when, and in what |
|---|---|---|---|
| Execution fee | 15 bps (0.15%) | 100 bps (1%) | Computed on the USDC collected for each batch and deducted in USDC before the swap. The treasury is paid in USDC; the whole swap output goes to users. |
| Yield fee | 1500 bps (15%) | 3000 bps (30%) | Only on positive idle yield, only at withdrawal. |
There is nothing else. No subscription, no management fee, no performance cut, and no referral share: no contract computes, stores, or pays one.
Example: a batch collecting 1000 USDC pays 1.50 USDC to the treasury and swaps the remaining 998.50 USDC. Every unit of the asset bought with that 998.50 is distributed to the users who funded it.
The owner can change supported assets, the swap router, the oracle, the treasury, the approved yield implementations, and fees within caps, each after the two-day delay. Pausing, unpausing, and switching the executor belong to the guardian and are instant. Neither can withdraw from a user strategy account.
Execution and monitoring
Execution is scheduled automation from the executor address. What it does when it runs: it reads the factory's active registry for accounts that are both due and able to fund an execution, groups them by target asset, reads the live on-chain swap route, takes a quote, and submits a batch with a quote-derived minimum output. A failed or zero quote skips the batch rather than submitting it with a weak minimum. Each batch is simulated with a zero-gas eth_call first, so a batch that would revert deterministically is skipped without spending gas, while one that lost a transient in-block price race is retried with a fresh quote and escalating tolerance up to a cap. The contract's own oracle floor applies on every attempt and cannot be bypassed by any of this.
Monitoring covers executor gas balance, a scheduling heartbeat, oracle health and per-feed staleness, treasury-fee reconciliation, pending-withdrawal events, repeatedly skipped accounts, empty batches, and the timelock queue, each labelled by the stack it belongs to.
If execution is unavailable for any reason, scheduled buys are delayed. Withdrawals are unaffected: they do not depend on the executor, and no pause can block them.
Reviewer checklist
- Confirm the verified source for each address above on Basescan for Base mainnet.
- Read
owner()on DCAFactory, BatchExecutor, SwapRouter, and the oracle. Each should be the TimelockController, not a Safe and not an individual key. - Read
getMinDelay()on the timelock. It should be 172800 seconds (two days). - Confirm the deploying address holds no timelock role: check
hasRolefor proposer, executor, and canceller. - Read
guardian()on DCAFactory and BatchExecutor, and confirm the guardian's reach is limited to pause, unpause,setExecutor, and fee flushing. - Read
treasury(),executionFeeBps(), andyieldFeeBps()on DCAFactory. There is no referrer-share getter to read. - Read
executor(),priceOracle(), and the whitelisted assets on BatchExecutor. - Check the SwapRouter adapter mapping and the Uniswap adapter route for WETH and cbBTC.
- Verify the canary transactions above: the caller, the selector, the fee split, and that the whole swap output reached the user.
- Review the withdrawal path in UserStrategyAccount. Withdrawal is owner-only, remains available while the protocol is paused, and is terminal.