DEV Community

Gerus Lab
Gerus Lab

Posted on

Building a Non-Custodial DCA Protocol on TON: How We Used the MasterChef Pattern to Achieve O(1) Smart Contract Complexity

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;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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)  │
└─────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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)