Sell/Buy/Burn Flows
All primary flows in the Accumulator system.
Seller lifecycle: from order creation (SHELL deposit) to eccUSDC payout:
Sell Shell (create sell order)
A seller creates a lot by sending ECC SHELL to the Root's receive(). The amount must correspond to exactly one denomination:
1 eccUSDC
100,000,000,000 (100 × 10⁹)
10 eccUSDC
1,000,000,000,000
100 eccUSDC
10,000,000,000,000
1000 eccUSDC
100,000,000,000,000
What happens on-chain:
Root validates
shellAmount % SHELL_PER_USDC == 0and the resulting denomination is one of {1, 10, 100, 1000}.Assigns
orderId = nextId[D], incrementsnextId[D]andavailable[D].Adds
shellAmountto_sellerShellPool.Deploys a new
ShellSellOrderLotcontract with deterministic address derived from(code, root, denom, orderId).Emits
SellOrderCreatedtwice: once to the seller's external address (for per-user subscription) and once to external address610(for global monitoring).
Result: the seller holds a lot contract and waits for a buyer to match it.
Buy Shell (deposit eccUSDC)
A buyer sends eccUSDC to the Root (directly or via Exchange). The amount must be in whole eccUSDC units (amount % 1_000_000 == 0).
Matching algorithm (largest-first FIFO):
Important details:
Matching is greedy: it takes as many lots as possible from the largest denomination first.
If sellers partially cover the amount, the rest is minted. The buyer always gets 100% of the SHELL.
The eccUSDC stays on the Root. It is tracked in
_usdcBalanceand becomes available for seller claims and NACKL redemption.Two events are emitted:
ShellPurchased(to ext addr 611) andMatchedOrders(to ext addr 617, with currentsoldPrefixvalues).
Note on buyShellFor vs receive: Both accept eccUSDC and trigger the same _processUsdcDeposit logic. The difference: receive() rejects messages carrying more than one ECC currency (require(currencies.keys().length <= 1)), while buyShellFor() only checks that eccUSDC is present. If a multi-currency message arrives via buyShellFor, the non-USDC ECC will remain on the contract.
Claim — seller collects eccUSDC
After a lot is matched (sold), the seller calls claim() on their lot contract to receive the eccUSDC payout.
Root.claimUSDC()
Verifies the caller's address matches the expected lot address (recomputed deterministically), checks orderId <= soldPrefix[D] (lot is sold), checks owedCount[D] > 0, and checks _usdcBalance >= owedTotal. If all pass:
Sends
D × USDC_DECIMALS_FACTOReccUSDC directly to the seller (not to the lot).Calls
SellOrderLot.onReceiveUSDC(payout)to confirm.Decrements
owedCount[D]and_usdcBalance.
Redeem (Burn-to-earn) NACKL
A NACKL holder sends ECC NACKL to the Root's receive() to burn it and claim a share of the "free reserve" — eccUSDC not owed to any seller.
Payout formula:
Where M(t) = T_KM × (1 - exp(-u_M × t)), capped at NACKL_T. t is seconds since _unixstart.
Key point: currentSupply is not M(t) — it's M(t) minus all NACKL burned to date. As more NACKL is burned, the denominator shrinks, so each subsequent burn receives a larger share of the remaining reserve. This is by design: later redeemers get proportionally more of whatever eccUSDC is left.
Exchange TIP3 to eccUSDC through Exchange
The Exchange also has onTransferReceived — a callback from its TIP-3 USDC wallet. When someone sends TIP-3 USDC to the Exchange's wallet, it mints equivalent eccUSDC and sends it back to the depositor's address (not to the Accumulator). The depositor can then send it to the Accumulator directly.
Last updated