Skip to main content
A stablecoin issuer’s most important number is its collateralization ratio - is every token in circulation backed by reserves? Today that assurance usually arrives as a quarterly attestation PDF. Between reports, holders, treasurers, and risk teams are flying blind. This guide replaces the PDF with a live signal. Turbo tracks circulating supply as it changes; Compose continuously checks it against custodial reserves and attests the result on-chain - the monitoring and verification counterpart to the NAV oracle publisher.

What you get

  • Real-time circulating supply per token and per chain, from mint/burn events.
  • A continuous collateralization ratio - onchain supply checked against off-chain reserves on a schedule.
  • An automatic alert (and optional pause) the moment backing slips below a threshold.
  • Treasury analytics - velocity, holder concentration, and float - in your warehouse.

How it works

Step 1: Stream supply changes with Turbo

TIP-20 emits explicit Mint, Burn, SupplyCapUpdate, and RewardDistributed events. Decode them from raw_logs to build a supply ledger.
supply-monitor.yaml
name: tip20-supply-monitor
resource_size: s

sources:
  tempo_tip20_logs:
    type: dataset
    dataset_name: tempo.raw_logs
    version: 1.0.0
    start_at: latest
    filter: address = lower('0xYOUR_TIP20_TOKEN')

transforms:
  decoded:
    type: sql
    primary_key: id
    sql: |
      SELECT
        id,
        address AS token,
        _gs_log_decode(
          _gs_fetch_abi('https://raw.githubusercontent.com/your-org/abis/main/tip20.json', 'raw'),
          topics,
          data
        ) AS decoded,
        block_timestamp,
        transaction_hash,
        _gs_op
      FROM tempo_tip20_logs

  supply_events:
    type: sql
    primary_key: id
    sql: |
      SELECT
        id,
        token,
        decoded.event_signature AS event,
        CAST(decoded.event_params[2] AS DECIMAL(38, 0)) AS amount,
        to_timestamp(block_timestamp) AS at,
        transaction_hash,
        _gs_op
      FROM decoded
      WHERE decoded IS NOT NULL
        AND decoded.event_signature IN ('Mint', 'Burn')

sinks:
  supply_ledger:
    type: postgres
    from: supply_events
    schema: treasury
    table: supply_events
    secret_name: MY_POSTGRES
    primary_key: id
For standard ERC-20 stablecoins that mint and burn via transfers to and from the zero address, use the curated erc20_transfers dataset (tip20_transfers on Tempo) and detect sender = '0x0000000000000000000000000000000000000000' (mint) or recipient = '0x0000000000000000000000000000000000000000' (burn) instead of decoding custom events.
Circulating supply is then a running total of mints minus burns - maintain it incrementally with the PostgreSQL aggregate sink, or as a view:
SELECT
  token,
  SUM(CASE WHEN event = 'Mint' THEN amount ELSE -amount END) AS circulating_supply
FROM treasury.supply_events
GROUP BY token;
For deeper treasury analytics - velocity, holder concentration, per-chain float - route the same stream to ClickHouse or your warehouse.

Step 2: Attest collateralization with Compose

A scheduled Compose task closes the loop: read on-chain supply, fetch reserves, compare, and act. Running it in a TEE means the attestation proves the exact reconciliation logic ran.
src/tasks/reserve-check.ts
import type { TaskContext } from "compose";

const TOKEN = "0xYOUR_TIP20_TOKEN";
const ATTESTOR = "0xYOUR_RESERVE_ATTESTOR";
const MIN_RATIO = 1.0; // 100% backing

export async function main(context: TaskContext) {
  const { fetch, evm } = context;

  const wallet = await evm.wallet({ name: "reserve-attestor", sponsorGas: true });

  // 1. On-chain circulating supply - readContract is a wallet method (see Contracts)
  const supply = await wallet.readContract<bigint>(
    evm.chains.tempo,
    TOKEN,
    "totalSupply() returns (uint256)",
    [],
  );

  // 2. Off-chain reserves from the custodian, retried durably
  const reserves = await fetch<{ totalReservesUsd: number; asOf: string }>(
    "https://custodian.example.com/v1/reserves",
    { max_attempts: 3, initial_interval_ms: 1000, backoff_factor: 2 },
  );

  const ratio = reserves.totalReservesUsd / Number(supply / 10n ** 6n); // 6-decimal USD token

  if (ratio < MIN_RATIO) {
    // 3a. Breach - alert, and optionally pause the token (requires PAUSE_ROLE)
    await fetch("https://ops.example.com/alerts/reserve-breach", {
      method: "POST",
      body: JSON.stringify({ token: TOKEN, ratio, reserves, supply: supply.toString() }),
    });
    return { healthy: false, ratio };
  }

  // 3b. Healthy - record the ratio on-chain
  await wallet.writeContract(
    evm.chains.tempo,
    ATTESTOR,
    "recordReserveRatio(address,uint256,uint64)",
    [TOKEN, BigInt(Math.round(ratio * 1e18)), BigInt(Math.floor(new Date(reserves.asOf).getTime() / 1000))],
  );

  return { healthy: true, ratio };
}
Schedule it in compose.yaml:
triggers:
  - type: "cron"
    expression: "*/5 * * * *"   # every 5 minutes
On-chain reads go through wallet.readContract (see Smart Contracts) - view calls return the decoded value directly and cost no gas, so the read reuses the same sponsored wallet. That wallet’s writes are gas-sponsored (sponsorGas: true), so the attestor never needs funding.

Step 3: Publish the feed (optional)

To expose reserves to other applications with a Chainlink-compatible interface and an operator kill-switch, follow the multi-chain NAV oracle guide. It is the publishing complement to the monitoring in this guide: this task decides whether reserves are healthy; the NAV oracle broadcasts the figure to every chain that needs it.

Circuit breaker pattern

Because the check runs continuously, you can wire the breach branch to a real control instead of just an alert - pause the token via PAUSE_ROLE, halt a payout keeper, or throttle minting. Compose’s durable execution guarantees the breaker actually fires and completes even through transient failures, and the whole sequence is traced for the post-incident review.

Business outcomes

  • Continuous assurance replaces a quarterly PDF. Collateralization is a live number, attested on-chain.
  • Regulatory readiness. Reserve monitoring and attestation map directly onto stablecoin reserve-reporting regimes.
  • Faster incident response. A breach triggers an alert - or a pause - in minutes, with a full audit trail.
  • One source of treasury truth. Supply, velocity, and reserves live in your own database and warehouse.

Resources

Can’t find what you’re looking for? Reach out to us at support@goldsky.com for help.