Dollar-cost averaging is dead simple as a concept — buy a fixed amount of asset X every Y days. But implement it on a blockchain non-custodially at scale, and you'll hit a wall fast. This is the story of how we built TON DCA — a fully non-custodial automatic investment protocol on The Open Network — and the architectural decision that made it actually work: the MasterChef index model.
The Problem with Naive DCA on Blockchain
Most early DCA protocols make the same mistake: they store each user's last execution timestamp, iterate over all pending orders on every tick, and calculate rewards/swaps per-user in a loop.
This gives you O(n) complexity where n = number of active strategies. Deploy that on TON with 10,000 users and you've got a protocol that bricks itself under load.
The naive Tact pseudocode looks something like this:
// ❌ Naive approach — O(n) loop
fun processAllStrategies(strategies: map<Address, Strategy>) {
foreach strategy in strategies {
if (now() >= strategy.nextExecution) {
executeSwap(strategy.user, strategy.amount);
strategy.nextExecution = now() + strategy.interval;
}
}
}
Every time the contract runs, it touches every active strategy. This doesn't scale. Gas costs explode, and in TON's actor model — where each contract handles its own messages — this kind of loop creates cascading message queues that stall the whole chain.
We needed something smarter.
Enter the MasterChef Pattern
If you've worked with DeFi yield farming, you've seen MasterChef — the pattern pioneered by SushiSwap that calculates rewards per staked token in O(1) using a global accumulator index.
The core insight: instead of tracking each user's individual state relative to every other user, you maintain a single global index that accumulates over time. Each user stores only their "checkpoint" — the index value at the time of their last interaction. The difference between the current global index and their checkpoint gives you exactly what they're owed.
For DCA, we adapted this:
// ✅ MasterChef-style — O(1) per user
struct GlobalState {
accIndexPerToken: Int; // Global accumulator
lastUpdateTime: Int;
totalActiveVolume: Int;
}
struct UserStrategy {
checkpointIndex: Int; // User's last-seen accumulator
amount: Int;
interval: Int;
owner: Address;
}
fun updateGlobalIndex(state: GlobalState): GlobalState {
let elapsed = now() - state.lastUpdateTime;
let deltaIndex = elapsed * PRECISION / state.totalActiveVolume;
return GlobalState {
accIndexPerToken: state.accIndexPerToken + deltaIndex,
lastUpdateTime: now(),
totalActiveVolume: state.totalActiveVolume
};
}
fun getPendingSwapAmount(state: GlobalState, strategy: UserStrategy): Int {
let indexDelta = state.accIndexPerToken - strategy.checkpointIndex;
return strategy.amount * indexDelta / PRECISION;
}
The result: calculating any individual user's pending DCA execution is now constant time, regardless of how many other users are in the system. No loops. No iteration. Just math.
TON-Specific Challenges: proxy-ton and STON.fi Integration
TON's architecture adds a layer of complexity that Ethereum devs aren't used to. Native TON (toncoin) isn't a Jetton — it doesn't implement the standard token interface. To swap it on STON.fi v2.1 (TON's primary DEX), you need the proxy-ton mechanism: wrap native TON into a Jetton-compatible wrapper that STON.fi can handle.
Here's how the swap message flow works:
// Sending a DCA swap through STON.fi v2.1
message SwapParams {
tokenWallet: Address; // User's jetton wallet
minAskAmount: Int; // Slippage protection
askJettonAddress: Address;
refundAddress: Address;
excessesAddress: Address;
deadline: Int;
}
fun executeSwap(strategy: UserStrategy, swapParams: SwapParams) {
// 1. Calculate amount from DCA index
let swapAmount = getPendingSwapAmount(globalState, strategy);
// 2. Wrap TON → proxy-ton if needed
if (strategy.sellToken == TON_ADDRESS) {
send(SendParameters {
to: PROXY_TON_MASTER,
value: swapAmount + GAS_BUFFER,
body: beginCell()
.storeUint(OP_PROXY_TON_MINT, 32)
.storeAddress(strategy.owner)
.endCell()
});
}
// 3. Route through STON.fi router
send(SendParameters {
to: STONFI_ROUTER_V2,
value: GAS_FOR_SWAP,
body: buildSwapMessage(swapAmount, swapParams)
});
// 4. Update user checkpoint
strategy.checkpointIndex = globalState.accIndexPerToken;
}
The proxy-ton approach lets us treat native TON identically to any Jetton in our swap pipeline. STON.fi handles the actual market execution; our contract handles strategy state and timing.
The Full Stack Architecture
The smart contract is only one piece. Here's how the full system is structured:
┌─────────────────────────────────────────────────┐
│ Telegram Mini App │
│ (React + TonConnect + ton-api REST calls) │
└──────────────────┬──────────────────────────────┘
│ TonConnect 2.0
▼
┌─────────────────────────────────────────────────┐
│ Tact Smart Contracts (TON) │
│ ┌──────────────┐ ┌────────────────────────┐ │
│ │ Master DCA │ │ User Strategy Wallet │ │
│ │ (global idx) │──▶│ (per-user, isolated) │ │
│ └──────────────┘ └────────────────────────┘ │
│ │ │
│ ▼ │
│ STON.fi v2.1 Router │
└──────────────────┬──────────────────────────────┘
│ blockchain events
▼
┌─────────────────────────────────────────────────┐
│ NestJS Indexer Service │
│ (monitors on-chain events, updates DB, │
│ triggers notifications via Telegram Bot API) │
└─────────────────────────────────────────────────┘
Why NestJS for the indexer? We needed a service that could:
- Subscribe to TON blockchain events via WebSocket
- Maintain a local state mirror for fast UI queries (no waiting for blockchain confirmations)
- Send Telegram notifications when swaps execute
- Handle re-org scenarios gracefully
The indexer doesn't control any funds — it's purely observational. All value-moving operations happen on-chain. This is non-custodial by design.
User isolation via per-user contracts: In TON's actor model, each user gets their own strategy contract instance. This means a bug in one user's strategy can't affect others. The master contract holds the global index; user contracts hold individual state. Communication happens via typed messages between actors.
Lessons from the Build
1. Don't underestimate TON's async message model
Every cross-contract call in TON is asynchronous. There's no synchronous call() like in Solidity. Design your state machine to handle partial execution — if step 2 of a 3-step swap fails, you need rollback logic built into step 2's error handler.
2. The MasterChef index breaks if totalActiveVolume hits zero
Division by zero will lock your contract. We added a guard:
fun safeUpdateIndex(state: GlobalState): GlobalState {
if (state.totalActiveVolume == 0) {
return GlobalState { ...state, lastUpdateTime: now() };
}
// ... normal update
}
3. Slippage protection at the strategy level, not just the swap level
Users set a max slippage percentage when creating their strategy. The contract checks this before routing to STON.fi. If market conditions are too volatile, the execution is skipped (not failed) and retried at the next interval. Silent skips beat failed transactions.
4. TonConnect 2.0 is smooth, but test deep-linking early
The Telegram Mini App + TonConnect integration is clean in theory. In practice, deep-linking from notification → MiniApp → correct wallet connection has edge cases. We burned a week on this. Test it on actual Telegram mobile clients early.
Real-World Performance
The deployed protocol (t.me/dcatonstrategy_bot) handles strategy execution for hundreds of active users with:
- Index update cost: constant, ~0.05 TON per global tick
- Per-user execution: ~0.02 TON in gas
- Zero custodial risk: funds never leave user-controlled contracts
This is the pattern we use at Gerus-lab across all our on-chain financial tooling. A similar actor-isolation approach powers ITOhub — our peer-to-peer marketplace for social media assets on TON, where each deal runs through an isolated FSM-based vault contract (CREATED → PROGRESS_CONFIRMED → COMPLETED), and an oracle confirms delivery before funds release.
Across both projects, the principle holds: isolate state per actor, use accumulators instead of loops, and make every value-moving operation atomic or reversible.
Related Work from Our Portfolio
We've applied similar architectural thinking across different chains:
- Ruffles — Decentralized NFT raffles on Solana. Hybrid architecture: on-chain winner selection (verifiable randomness) + off-chain indexer for 1,000 concurrent raffles at <1s finality.
- AURON Bundler — Solana launch terminal using gRPC (yellowstone-grpc) and Jito MEV bundles. Same philosophy: push complex state management off-chain, keep on-chain logic minimal and auditable.
- TokenDropper — Automated token distribution for 10k+ recipients in a single run. The batching algorithm uses a priority queue, not a naive loop.
The pattern repeats: every time we've hit a scaling wall on-chain, the answer has been "find the O(n) loop and replace it with an accumulator."
Build Your Protocol with Gerus-lab
We're a product engineering team that builds on-chain protocols and real-time systems — from smart contracts to full-stack applications. If you're building on TON, Solana, or integrating AI into your product and need an engineering partner who's shipped production systems (not just proofs of concept), we'd love to talk.
gerus-lab.com — portfolio, tech stack, and contact
The TON DCA bot is live at t.me/dcatonstrategy_bot — test the UX yourself before you reach out. That's the kind of work we ship.
Top comments (0)