Pools and tokens

Concepts

Pools and tokens

The seven pools and their addresses, why six of them draw every six hours, the seeded stakes per token, the token Hearth refuses, and the per-pool routes.

Hearth is not one pool. It is seven, one per confidential token on Zama's Sepolia address book, and each one is its own HearthVault, its own HearthPrizePool and its own SponsoredYieldSource, with its own savers, its own prize money and its own keeper.

The contracts are the same code, deployed seven times with different constructor arguments. Nothing is shared on chain: no registry, no router, no shared balance. A saver in the WETH pool cannot see, touch or be touched by the USDC pool, and a paused vault or a stalled keeper on one token leaves the other six running.

#The seven pools

Token Slug Draw every Vault Prize pool Yield source
Confidential USDC (Mock) usdc 1 hour 0x0F93e5db6027b4FB1C76566d24aA2D2E417fAF52 0xA0785AacF30B6FE46EDc53CD8A9db1d94FeF5Df2 0xCC49DF69eAB6884fD8DD9260902B8A0Abc9D6b91
Confidential USDT (Mock) usdt 6 hours 0xe54F44dE64F8A7abc0647eaae547dD59ce0EFfac 0x6a83Beb2Dc3f258107Cad5e17BC57657fAd4fbd1 0x5bb1Cd5380Cb9f2B15569030fF0dB7a445cF54cA
Confidential WETH (Mock) weth 6 hours 0x3D1A182782B68fE270A66294C9adaC7F005c4f14 0x1a11e7C689F244fA8Dd5f4abA8F2F3131090cc1C 0x40DF298f15c6136294eC651aD7b0c1C6F221DE8F
Confidential BRON (Mock) bron 6 hours 0x18086DC8271f8A73c5Ea985fd519527Dbb991279 0x2Ed982979CD184494B947a1E38E597494a38ACe4 0x0cD1155D752bD81b3a437a6f0B3965CAA2A1C8e9
Confidential ZAMA (Mock) zama 6 hours 0xEEC26386F273c6678cA538AcA18e1d9384eA9F09 0x873B285404199D46325a294Aa0EC7a79C30A7fF7 0xdD352D70311E834ab75307f53d5C276060081d23
Confidential tGBP (Mock) tgbp 6 hours 0xCe95dAa01f5354aA8887A5952E403D26d452c323 0xC531D54ee2c695e0eBfe8b8258e9Fd80fd507095 0xDEa2BD6351072F735B6ea83c357bF157d83c01af
Confidential XAUt (Mock) xaut 6 hours 0x77f701101d66FbD522A3bFdC2c00DB09a4F57daE 0x9a2888aca42c707A3BC0D561FdF6ff8Abfda5201 0x03fDdAA7C4323C53CE511CC49D4c33B26B492af7

Every contract above is verified on Etherscan. The token pair each pool holds is Zama's, not ours, and is listed in try it on Sepolia.

The USDC pool was deployed first, on 2 September 2026 in block 11622398, and has run hourly draws ever since, which is why it is the pool with a history behind it and the pool the prove-it transcript in the README was recorded against. The other six were deployed on 5 September 2026, in blocks 11641314 through 11641523.

#Why six hours, and not an hour for all seven

Gas. One draw on a five-saver pool is 8,456,388 gas measured on live Sepolia receipts: a close, an award, two evaluation batches, a finalize and one reconcile per tier. At 1 gwei that is 0.0085 ETH. Seven pools drawing every hour would be 168 draws a day, about 1.43 ETH, which cannot be kept funded from public faucets through a judging window. A ten-saver pool is 12,582,923 gas a draw and the bill grows with it.

So the six pools deployed later draw every six hours. That is four draws a day each, so all seven pools together cost about 0.41 ETH a day instead of 1.43, and four draws a day is still often enough that a visitor sees one land inside a single visit. The USDC pool keeps its hourly clock and the draws of history that came with it.

The odds are set against each pool's own period rather than carried over, so the feel of the product is the same on both clocks:

Tier Hourly pool (usdc) Six-hour pools
Grand count 1, odds 1 in 24, shares 40 count 1, odds 1 in 4, shares 40
Mid count 1, odds 1 in 6, shares 20 count 1, odds 1 in 2, shares 20
Frequent count 4, odds 1 in 1, shares 40 count 4, odds 1 in 1, shares 40

The grand prize therefore pays about once a day in every pool. The mid tier is the one place the two clocks differ: about four times a day on the hourly pool and about twice a day on the six-hour ones, because halving the odds does not quite make up for a sixth as many draws. Every tier of every pool reconciles every draw, for the reason in prizes and tiers.

#Decimals, and what an amount means

Every confidential wrapper on Zama's Sepolia address book reads six decimals, whatever the public token underneath reads, because the wrapper caps itself at six and charges the difference to its rate(). Confidential WETH is the clearest case: its underlying holds 18 decimals, so the wrapper's rate() is a million million, and one base unit of the wrapper is a million million base units of the public token.

Every amount in packages/contracts/hearth.config.ts is in wrapper base units, and the deploy and the tasks multiply by the rate they read on chain before they touch the public token. This is not a detail. Our own audit found a bug where a pool booked the amount a caller passed in rather than the amount the wrapper minted, which on an 18-decimal token inflated prize money by a factor of a million million. See yield source.

#What each pool is seeded with

hearth:seed --token <slug> sponsors the yield source and puts five demo savers in, from account indexes 2 to 6, so a first visitor lands on a populated pool. The stakes differ per token because a pool has to look like the asset it holds: 1,200 of a dollar stablecoin and 0.6 of ether are the same size of saver.

Pool Five demo stakes Sponsorship Prize money released
usdc 1,200 / 600 / 300 / 150 / 75 10,000 USDC 20 USDC an hour, so 19.998 a draw
usdt 1,200 / 600 / 300 / 150 / 75 10,000 USDT 20 USDT an hour, so 119.98 a draw
weth 0.6 / 0.3 / 0.15 / 0.075 / 0.04 5 WETH 0.01 WETH an hour, rounded down to 0.0432 a draw
bron 2,000 / 1,000 / 500 / 250 / 125 15,000 BRON 30 BRON an hour, so 179.99 a draw
zama 2,000 / 1,000 / 500 / 250 / 125 15,000 ZAMA 30 ZAMA an hour, so 179.99 a draw
tgbp 1,000 / 500 / 250 / 125 / 60 8,000 tGBP 16 tGBP an hour, so 95.99 a draw
xaut 0.4 / 0.2 / 0.1 / 0.05 / 0.025 3 XAUt 0.006 XAUt an hour, rounded down to 0.0216 a draw

A source's rate is whole base units a second, so the two smallest rates are rounded down: 0.01 WETH an hour is 2.77 base units a second and releases 2, and 0.006 XAUt an hour is 1.67 and releases 1. Every sponsorship is sized to last more than eighty draws, which is twenty days or more, so nobody has to top a pool up during a judging window.

#The token Hearth refuses

Zama also publishes a non-mock Confidential tGBP on Sepolia, at 0x167DC962808B32CFFFc7e14B5018c0bE06A3A208 over the public token 0xf6Ef9ADB61A48E29E36bc873070A46A3D2667ff3. Its underlying mint is restricted to the issuer, so nobody but the issuer can obtain the public token, nobody can wrap into the confidential one, and no pool can be opened on it at all.

Hearth lists it in the pool picker anyway, greyed out, with the reason written under its name. The deployment file states that reason once, in English, as mint restricted to the issuer, and the app prints it in the language the reader is on. Choosing it opens a page that names the token, links both contracts on Etherscan, says whose restriction it is, and offers no wallet action, because a deposit button that reverts is worse than no button.

Leaving the token off the list would have been easier and would have looked like Hearth had simply not got to it. A saver who goes looking for tGBP finds two entries: the mock pool that works, and the official token that does not, with the reason.

#What the row of pools shows

The landing page ends with a row of every pool, each cell carrying that pool's grand prize right now, read on the server in one multicall and shipped inside the page so the row is already filled in when the story stops scrolling. The picker inside the console shows the same figures against the same rules.

Two of those rules exist because a number can be misleading:

  • A pool whose read did not come back says unread, never 0.00. An unanswered node and an empty jackpot look identical once a zero is written down.
  • A pool that has not closed its first draw yet says First draw HH:MM UTC instead of a figure, on the shelf and under its name in the picker. Prize money only reaches the tiers when the first draw is awarded, so before that close the honest answer is a time, not 0.00. Which of the two a cell shows is decided by lastClosedDraw still reading zero, and the time itself is firstPeriodAt plus periodLength, the end of the first period and the earliest moment draw 1 can close. The clock is twenty-four hour UTC in every language.

#Where the app gets the addresses

The app never carries an address typed by hand. Every open pool in packages/web/src/lib/chain/pools.json is generated from a file the deploy script wrote, by:

node scripts/sync-pools.mjs        # from packages/web

That script reads packages/contracts/deployments/sepolia/hearth.<slug>.json, refuses any file missing an address, refuses two pools claiming the same slug, and appends the one restricted entry that has no deployment. Run it after every deploy. The environment variables that used to hold a single pool's three addresses are gone.

The pool a saver is looking at is the first segment after /app:

Route What it shows
/app Redirects to the pool the saver last used, or usdc on a first visit
/app/<slug> That pool's dashboard
/app/<slug>/deposit Mint, shield and deposit for that token
/app/<slug>/withdraw Withdraw and unshield for that token
/app/<slug>/draws That pool's draws, and the saver's own result in each
/app/<slug>/run The five permissionless draw steps for that pool
/verify?pool=<slug> The public seed, bracket and thresholds for that pool

A language code sits in front of all of them for every language but English, so the Japanese reader's dashboard is /ja/app/weth.

#One keeper per pool

Seven pools means seven keeper processes, each signing from its own account index of the same seed phrase, because two processes on one account fight over the same nonce. The table and the pm2 file are in the keeper.