Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 27 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,10 +208,10 @@ Live addresses (chainId 11155111, deployer/keeper

| | address |
|---|---|
| cUSDT | `0x485b62eEB1931091FA8bBfb37d1a7B9A18EA345b` |
| faucet | `0xDACEa85f7f8A2F9D80A7278f207C0dFAe16417B0` |
| demo pool / draw / yield | `0x92aF68…15aF` / `0x89efaA…40A6` / `0x575Cf6…0243` |
| standard pool / draw / yield | `0x1d7E3E…395F` / `0xD79fAd…2240` / `0x7B5227…C421` |
| cUSDT | `0x0E8c04AFd8d4483b0925aF1b4E5a88dde28F0Ff0` |
| faucet | `0x5550e92d4C252763797948Fb751c7116809F7cdb` |
| demo pool / draw / yield | `0x223897…8F97` / `0x30E3eF…1E62` / `0x7C2dF9…2237` |
| standard pool / draw / yield | `0xCCE648…f990` / `0x31d445…d39e` / `0x41E2a4…1048` |

`packages/contracts/deployments/sepolia.json` is the canonical record. Read
addresses from there or from the generated
Expand Down Expand Up @@ -302,7 +302,7 @@ What landed:
on. Renders nothing when disconnected: no wallet is not the wrong network.
- `components/app/connect-button.tsx`, `(app)/` route group with its own layout,
`config/app.ts`.
- `/faucet`: live `drip()` against `0xDACEa8…17B0`, reading `dripAmount` and
- `/faucet`: live `drip()` against the generated Sepolia faucet address, reading `dripAmount` and
`readyAt(address)`, with a ticking local countdown, mapped revert messages
(`CooldownNotElapsed`, `OnlyMinter`, user rejection, gas), and an Etherscan
link on success. Deliberately **not** wrapped in `EncryptedGate`: minting is
Expand Down Expand Up @@ -477,7 +477,28 @@ points to `/pool`. Keep this explicit directory when adding pool-related links:
the `(app)` route group does not contribute a URL segment.


### Next: Phase 11
### Phase 11 — Verification page & prizes (complete)

`/verify/[roundId]` reads the public Ernie event trail for both Sepolia pools,
including frozen ticket count, published total, random value, settlement prize,
and explicit rollover state. It requires no wallet and bounds public-RPC history
to the latest 100,000 blocks.

`/app/prizes` adds the private prize envelope: the connected user decrypts their
encrypted claimable handle with the existing in-memory EIP-712 session, then
encrypts the chosen amount and claims through `SortisPool.claim`. Rollover and
zero-prize outcomes are presented distinctly from a winning reveal.

`SortisPool.claim` was added as the encrypted claim path. It clamps an encrypted
request to the caller's encrypted claimable balance, updates the claimable slot,
and transfers the confidential token without publishing the amount.

Web typegen, typecheck, ESLint, and diff checks pass. Contract compilation was
verified successfully. On restricted Windows runners, Hardhat's compiler child
process can fail with `HH505`/`spawn EPERM`; run the compile with the workspace's
approved elevated command policy. The Solidity compiler itself is healthy.

### Next: Phase 12

Build `/verify/[roundId]` from the public draw event trail, then add `/app/prizes` with the authenticated private claim/decryption flow and distinct rollover presentation. Do not relabel landing-page illustrative draw data as live until the keeper completes at least one full real Sepolia round.

Expand Down
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,24 +255,24 @@ Shared:

| Contract | Address |
|---|---|
| Confidential token (cUSDT) | [`0x485b62eEB1931091FA8bBfb37d1a7B9A18EA345b`](https://sepolia.etherscan.io/address/0x485b62eEB1931091FA8bBfb37d1a7B9A18EA345b) |
| `SortisFaucet` | [`0xDACEa85f7f8A2F9D80A7278f207C0dFAe16417B0`](https://sepolia.etherscan.io/address/0xDACEa85f7f8A2F9D80A7278f207C0dFAe16417B0) |
| Confidential token (cUSDT) | [`0x0E8c04AFd8d4483b0925aF1b4E5a88dde28F0Ff0`](https://sepolia.etherscan.io/address/0x0E8c04AFd8d4483b0925aF1b4E5a88dde28F0Ff0) |
| `SortisFaucet` | [`0x5550e92d4C252763797948Fb751c7116809F7cdb`](https://sepolia.etherscan.io/address/0x5550e92d4C252763797948Fb751c7116809F7cdb) |

Demo pool, one round every 5 minutes, so a reviewer arriving at a random moment is never far from a complete draw:

| Contract | Address |
|---|---|
| `SortisPool` | [`0x92aF68E6823D22D5Bd5B8746f1c52b87CE3315aF`](https://sepolia.etherscan.io/address/0x92aF68E6823D22D5Bd5B8746f1c52b87CE3315aF) |
| `SortisDraw` | [`0x89efaA363468478aeB5CAaf80e21680B129F40A6`](https://sepolia.etherscan.io/address/0x89efaA363468478aeB5CAaf80e21680B129F40A6) |
| `MockYieldSource` | [`0x575Cf61C0FB339D469582fE45219E4bF40e60243`](https://sepolia.etherscan.io/address/0x575Cf61C0FB339D469582fE45219E4bF40e60243) |
| `SortisPool` | [`0x2238975477e0F90B2084C5C64c61E41fFe7B8F97`](https://sepolia.etherscan.io/address/0x2238975477e0F90B2084C5C64c61E41fFe7B8F97) |
| `SortisDraw` | [`0x30E3eF09C9Ec68bd228f4593382dFF3fe9521E62`](https://sepolia.etherscan.io/address/0x30E3eF09C9Ec68bd228f4593382dFF3fe9521E62) |
| `MockYieldSource` | [`0x7C2dF99d75a14c224d8AD7d24311334f16B92237`](https://sepolia.etherscan.io/address/0x7C2dF99d75a14c224d8AD7d24311334f16B92237) |

Standard pool, one round every 24 hours, the round length a real savings product would use:

| Contract | Address |
|---|---|
| `SortisPool` | [`0x1d7E3ED492D6204A25b7B7a0bbE6C9943555395F`](https://sepolia.etherscan.io/address/0x1d7E3ED492D6204A25b7B7a0bbE6C9943555395F) |
| `SortisDraw` | [`0xD79fAd0748D999e9274470f8F3b64571D9e12240`](https://sepolia.etherscan.io/address/0xD79fAd0748D999e9274470f8F3b64571D9e12240) |
| `MockYieldSource` | [`0x7B5227267356f91ff2Cb3306C3ee5b6aBa97C421`](https://sepolia.etherscan.io/address/0x7B5227267356f91ff2Cb3306C3ee5b6aBa97C421) |
| `SortisPool` | [`0xCCE648f68874F3bf15Bc713a02b8264a2fD0f990`](https://sepolia.etherscan.io/address/0xCCE648f68874F3bf15Bc713a02b8264a2fD0f990) |
| `SortisDraw` | [`0x31d445D5A59f74F139926Ccbc56C18ed506ed39e`](https://sepolia.etherscan.io/address/0x31d445D5A59f74F139926Ccbc56C18ed506ed39e) |
| `MockYieldSource` | [`0x41E2a43f1Bff35b230686B2B58121eA1CDfb1048`](https://sepolia.etherscan.io/address/0x41E2a43f1Bff35b230686B2B58121eA1CDfb1048) |

`MorphoYieldSource` is intentionally not deployed. It is the documented mainnet path, and a contract that reverts on every call would only be noise on a block explorer.

Expand Down
21 changes: 21 additions & 0 deletions packages/contracts/contracts/SortisPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ contract SortisPool is ZamaEthereumConfig, Ownable, ReentrancyGuardTransient {
*/
error DepositNotApproved(address depositor);

/// @notice Thrown when a claim request decrypts to zero.
error NothingToClaim();

// ---------------------------------------------------------------------
// Storage
// ---------------------------------------------------------------------
Expand Down Expand Up @@ -204,6 +207,24 @@ contract SortisPool is ZamaEthereumConfig, Ownable, ReentrancyGuardTransient {
return _claimable[account];
}

/**
* @notice Claim part or all of the caller's encrypted winnings.
* @dev The requested amount stays encrypted. The pool clamps it to the
* caller's claimable balance, updates the encrypted slot, and sends
* the confidential token without exposing the amount onchain.
*/
function claim(externalEuint64 encryptedAmount, bytes calldata inputProof) external nonReentrant {
euint64 requested = FHE.fromExternal(encryptedAmount, inputProof);
euint64 available = _claimable[msg.sender];
ebool canClaim = FHE.le(requested, available);
euint64 amount = FHE.select(canClaim, requested, available);
FHE.allowThis(amount);
_claimable[msg.sender] = FHE.sub(available, amount);
FHE.allowThis(_claimable[msg.sender]);
FHE.allow(_claimable[msg.sender], msg.sender);
IERC7984(asset).confidentialTransfer(msg.sender, amount);
}

/// @notice Public ticket metadata. Encrypted fields are returned as handles.
function ticketAt(
uint256 ticketId
Expand Down
32 changes: 16 additions & 16 deletions packages/contracts/deployments/sepolia.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@
"chainId": 11155111,
"deployer": "0x70f77A5C36eBD667360F6021bF4A95d274B3530e",
"keeper": "0x70f77A5C36eBD667360F6021bF4A95d274B3530e",
"deployedAt": "2026-08-22T08:05:14.167Z",
"deployedAt": "2026-08-24T06:47:50.321Z",
"token": {
"address": "0x485b62eEB1931091FA8bBfb37d1a7B9A18EA345b",
"address": "0x0E8c04AFd8d4483b0925aF1b4E5a88dde28F0Ff0",
"args": [
"0x70f77A5C36eBD667360F6021bF4A95d274B3530e"
]
},
"faucet": {
"address": "0xDACEa85f7f8A2F9D80A7278f207C0dFAe16417B0",
"address": "0x5550e92d4C252763797948Fb751c7116809F7cdb",
"args": [
"0x485b62eEB1931091FA8bBfb37d1a7B9A18EA345b",
"0x0E8c04AFd8d4483b0925aF1b4E5a88dde28F0Ff0",
1000000,
3600,
"0x70f77A5C36eBD667360F6021bF4A95d274B3530e"
Expand All @@ -22,25 +22,25 @@
"demo": {
"roundDuration": 300,
"yieldSource": {
"address": "0x575Cf61C0FB339D469582fE45219E4bF40e60243",
"address": "0x7C2dF99d75a14c224d8AD7d24311334f16B92237",
"args": [
"0x485b62eEB1931091FA8bBfb37d1a7B9A18EA345b",
"0x0E8c04AFd8d4483b0925aF1b4E5a88dde28F0Ff0",
2000,
"0x70f77A5C36eBD667360F6021bF4A95d274B3530e"
]
},
"pool": {
"address": "0x92aF68E6823D22D5Bd5B8746f1c52b87CE3315aF",
"address": "0x2238975477e0F90B2084C5C64c61E41fFe7B8F97",
"args": [
"0x485b62eEB1931091FA8bBfb37d1a7B9A18EA345b",
"0x0E8c04AFd8d4483b0925aF1b4E5a88dde28F0Ff0",
300,
"0x70f77A5C36eBD667360F6021bF4A95d274B3530e"
]
},
"draw": {
"address": "0x89efaA363468478aeB5CAaf80e21680B129F40A6",
"address": "0x30E3eF09C9Ec68bd228f4593382dFF3fe9521E62",
"args": [
"0x92aF68E6823D22D5Bd5B8746f1c52b87CE3315aF",
"0x2238975477e0F90B2084C5C64c61E41fFe7B8F97",
"0x70f77A5C36eBD667360F6021bF4A95d274B3530e",
"0x70f77A5C36eBD667360F6021bF4A95d274B3530e"
]
Expand All @@ -49,25 +49,25 @@
"standard": {
"roundDuration": 86400,
"yieldSource": {
"address": "0x7B5227267356f91ff2Cb3306C3ee5b6aBa97C421",
"address": "0x41E2a43f1Bff35b230686B2B58121eA1CDfb1048",
"args": [
"0x485b62eEB1931091FA8bBfb37d1a7B9A18EA345b",
"0x0E8c04AFd8d4483b0925aF1b4E5a88dde28F0Ff0",
2000,
"0x70f77A5C36eBD667360F6021bF4A95d274B3530e"
]
},
"pool": {
"address": "0x1d7E3ED492D6204A25b7B7a0bbE6C9943555395F",
"address": "0xCCE648f68874F3bf15Bc713a02b8264a2fD0f990",
"args": [
"0x485b62eEB1931091FA8bBfb37d1a7B9A18EA345b",
"0x0E8c04AFd8d4483b0925aF1b4E5a88dde28F0Ff0",
86400,
"0x70f77A5C36eBD667360F6021bF4A95d274B3530e"
]
},
"draw": {
"address": "0xD79fAd0748D999e9274470f8F3b64571D9e12240",
"address": "0x31d445D5A59f74F139926Ccbc56C18ed506ed39e",
"args": [
"0x1d7E3ED492D6204A25b7B7a0bbE6C9943555395F",
"0xCCE648f68874F3bf15Bc713a02b8264a2fD0f990",
"0x70f77A5C36eBD667360F6021bF4A95d274B3530e",
"0x70f77A5C36eBD667360F6021bF4A95d274B3530e"
]
Expand Down
5 changes: 5 additions & 0 deletions packages/web/app/(app)/app/prizes/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { Metadata } from "next";
import { PrizesPanel } from "@/components/app/prizes-panel";

export const metadata: Metadata = { title: "Your prizes", description: "Privately reveal and claim Sortis prizes." };
export default function PrizesPage() { return <PrizesPanel />; }
11 changes: 11 additions & 0 deletions packages/web/app/verify/[roundId]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { Metadata } from "next";
import { formatTokenAmount, explorerAddressUrl } from "@/lib/contracts";
import { getDrawAddress } from "@/lib/draws";
import { readVerification } from "@/lib/verification";

export const metadata: Metadata = { title: "Verify a draw", description: "Independently verify a settled Sortis draw." };

export default async function VerifyPage({ params }: { params: Promise<{ roundId: string }> }) {
const { roundId: raw } = await params; const roundId = BigInt(raw); const trails = await readVerification(roundId);
return <main className="section-shell space-y-8"><div className="space-y-3"><p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">Public draw proof</p><h1 className="font-heading text-4xl tracking-tight md:text-5xl">Round {roundId.toString()}</h1><p className="max-w-2xl text-base leading-7 text-muted-foreground">Everything below comes from public event data. No wallet or private key is required.</p></div><div className="grid gap-6 lg:grid-cols-2">{trails.map((trail) => <article key={trail.poolId} className="space-y-5 rounded-xl border bg-card p-5 sm:p-6"><div className="flex items-start justify-between gap-4"><div><p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">{trail.poolId === "demo" ? "Demo pool" : "Standard pool"}</p><h2 className="mt-2 text-2xl font-medium">{trail.rolledOver ? "Rolled over" : trail.settled ? "Settled" : "Not settled"}</h2></div><a className="font-mono text-xs text-brand hover:underline" href={explorerAddressUrl(getDrawAddress(trail.poolId))} target="_blank" rel="noreferrer">draw contract</a></div>{trail.rolledOver ? <p role="status" className="border-y py-3 text-sm font-medium">No active ticket held the random range. The prize carries forward.</p> : null}<dl className="grid grid-cols-2 gap-4 text-sm"><div><dt className="text-xs text-muted-foreground">Tickets frozen</dt><dd className="mt-1 font-mono">{trail.frozenTicketCount?.toString() ?? "-"}</dd></div><div><dt className="text-xs text-muted-foreground">Published total</dt><dd className="mt-1 font-mono">{trail.total?.toString() ?? "-"}</dd></div><div><dt className="text-xs text-muted-foreground">Random value</dt><dd className="mt-1 font-mono">{trail.random?.toString() ?? "-"}</dd></div><div><dt className="text-xs text-muted-foreground">Prize</dt><dd className="mt-1 font-mono">{trail.prize === undefined ? trail.carriedPrize === undefined ? "-" : `${formatTokenAmount(trail.carriedPrize)} cUSDT carried` : `${formatTokenAmount(trail.prize)} cUSDT`}</dd></div></dl><p className="text-xs text-muted-foreground">Close, oracle reveal, random draw, and settlement are indexed directly from the draw engine.</p></article>)}</div></main>;
}
Loading
Loading