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
53 changes: 53 additions & 0 deletions agent/flow-trace/02_TOKENS_AND_ACTIVATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -349,3 +349,56 @@ active = registered
ENCL → returned from ExitQueue
USDC → paid out from ETK.payableBalance
```

---

## Audit Cluster 2 Changes (Tokens)

The token contracts were hardened against the following audit findings. All changes are covered by
`packages/enclave-contracts/test/Token/` and have no runtime impact outside the touched contracts.

### EnclaveTicketToken (ETK)

- **H-02 — registry initialization.** The constructor now takes
`(IERC20 baseToken, address registry_, address initialOwner_)` and assigns `registry = registry_`
directly (emitting `RegistryChanged(0, registry_)`) instead of requiring the deployer to call
`setRegistry()` later. Reverts `ZeroAddress` if `registry_ == 0`.
- **H-03 — fee-on-transfer safe deposits.** `depositFor` and `depositFrom` measure the underlying
balance before/after `safeTransferFrom` and mint the _actual_ amount received. Operators auto
self-delegate on first deposit.
- **H-16 / H-20 / M-22 — registry swap timelock.** Once `lockRegistry()` is called (one-way,
`RegistryLockAlreadySet` on repeat) further registry swaps must go through
`requestRegistryChange(addr)` → wait `REGISTRY_CHANGE_DELAY = 1 day` → `activateRegistryChange()`.
Errors: `RegistryNotLocked`, `RegistryChangeNotReady`, `NoPendingRegistry`,
`RegistryAlreadyLocked`. `cancelRegistryChange()` clears the pending swap.
- **M-11 — permit disabled.** `permit()` always reverts `PermitDisabled` so non-transferable tickets
cannot be moved via off-chain signatures.
- **M-12 — rescueERC20.** `rescueERC20(token, to, amount)` lets the owner recover stray ERC-20s but
refuses the underlying asset (`CannotRescueUnderlying`).
- **M-25 — delegation locked to self.** `delegate()` only accepts the caller's own address (else
`DelegationLocked`); `delegateBySig` always reverts.
- **M-29 — EIP-6372 timestamp clock.** `clock() = uint48(block.timestamp)`,
`CLOCK_MODE() = "mode=timestamp"`.

### EnclaveToken (ENCL)

- **H-15 — WHITELIST_ROLE separation + one-way disable.** New `WHITELIST_ROLE` gates
`toggleTransferWhitelist` and `whitelistContracts`, decoupling whitelist edits from `MINTER_ROLE`.
`disableTransferRestrictions` is `DEFAULT_ADMIN_ROLE` only and idempotent (silent no-op when
already disabled) so deployment/setup scripts can call it unconditionally.
- **M-21 — per-epoch mint cap.** New rolling cap configured via
`setMintCap(epochLength, capPerEpoch)` (`ZeroEpochLength` on zero length). Both `mintAllocation`
and `batchMintAllocations` route through `_accountForMintAgainstCap`, which rolls the epoch
(`MintEpochRolled(newStart)`) and reverts `ExceedsMintCap` on overflow. Constructor defaults to a
30-day epoch with `cap = MAX_SUPPLY` so bootstrap deployments keep working; governance is expected
to tighten this before broad distribution.
- **M-29 — EIP-6372 timestamp clock.** Same timestamp clock as ETK, aligning ENCL voting checkpoints
with timepoints used elsewhere.

### Registry coordination

- `CiphernodeRegistryOwnable.requestBlock` now stores `block.timestamp` (the storage slot and event
field names are preserved for backwards compatibility). All callers — including
`BondingRegistry.getTicketBalanceAtBlock(node, c.requestBlock - 1)` — pass the value through
unchanged; the parameter is now a timepoint per EIP-6372 rather than a block number, which is
required for the ETK timestamp clock to be valid.
43 changes: 36 additions & 7 deletions agent/flow-trace/03_E3_REQUEST_AND_COMMITTEE.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ Requester calls: Enclave.request({
├─ E3 CREATION:
│ ├─ e3Id = nexte3Id++
│ ├─ seed = uint256(keccak256(block.prevrandao, e3Id))
│ │ → Deterministic but unpredictable randomness for sortition
│ │ → On chains without `prevrandao`, the value is still deterministic
│ │ per-block; downstream sortition relies on the per-E3 snapshot of
│ │ ticket balances at `requestBlock - 1` for manipulation resistance.
│ │
│ ├─ encryptionSchemeId = e3Program.validate(
│ │ e3Id, seed, e3ProgramParams, computeProviderParams, customParams
Expand All @@ -62,7 +64,7 @@ Requester calls: Enclave.request({
│ │
│ ├─ Store E3 struct:
│ │ e3s[e3Id] = E3 {
│ │ seed, threshold, requestBlock: block.number,
│ │ seed, threshold, requestBlock: block.timestamp, // H-26: EIP-6372 clock
│ │ inputWindow, encryptionSchemeId, e3Program,
│ │ e3ProgramParams, customParams, decryptionVerifier,
│ │ requester: msg.sender
Expand All @@ -84,7 +86,7 @@ Requester calls: Enclave.request({
│ │ │ │ 3. committees[e3Id] = Committee { │
│ │ │ │ initialized: true, │
│ │ │ │ seed: seed, │
│ │ │ │ requestBlock: block.number,
│ │ │ │ requestBlock: block.timestamp, // H-26
│ │ │ │ committeeDeadline: │
│ │ │ │ block.timestamp + sortitionWindow, │
│ │ │ │ threshold: threshold │
Expand Down Expand Up @@ -299,15 +301,17 @@ CiphernodeRegistrySolWriter receives CommitteeFinalizeRequested
│ │ │ │ } │ │
│ │ │ └──────────────────────────────────────────┘ │
│ │ │
│ │ 6. Emit CommitteeFinalized(e3Id, committee) │
│ │ 6. Emit SortitionCommitteeFinalized(e3Id, committee, scores)│
│ │ [ICiphernodeRegistry event] │
│ │ } │
│ └─────────────────────────────────────────────────────────┘
```

### 3c. CommitteeFinalized Event Processing (Rust-Side)
### 3c. SortitionCommitteeFinalized Event Processing (Rust-Side)

```
CiphernodeRegistrySolReader decodes CommitteeFinalized event
```text
CiphernodeRegistrySolReader decodes SortitionCommitteeFinalized event
│ [ICiphernodeRegistry.SortitionCommitteeFinalized — NOT IEnclave.CommitteeFinalized]
Comment thread
coderabbitai[bot] marked this conversation as resolved.
├─ Publishes EnclaveEvent::CommitteeFinalized {
│ e3_id, committee: [addr1, addr2, ..., addrN], scores: [s1, s2, ..., sN], chain_id
Expand Down Expand Up @@ -385,3 +389,28 @@ If any deadline is missed → anyone can call markE3Failed()

6. **IMT root snapshot**: The Merkle tree root is captured at request time. Nodes that join/leave
after the request don't affect this E3's committee.

---

## Cluster 7 audit additions (post-fix semantics)

### H-04 — snapshot-based eligibility

`CiphernodeRegistryOwnable._validateNodeEligibility` derives the per-node ticket weight from
`bondingRegistry.getTicketBalanceAtBlock(node, committee.requestBlock - 1)`, which reads the
`EnclaveTicketToken` ERC20Votes checkpoint history (EIP-6372 timestamp clock). Same-block or
post-request rebalancing therefore cannot inflate a node's selection weight; the outer
`isCiphernodeEligible(msg.sender)` still gates on the current `isActive` flag for liveness.

### M-33 — `markE3Failed` grace period

When `markFailedGracePeriod > 0` (set via `Enclave.setMarkFailedGracePeriod`), calling
`markE3Failed` within `deadline … deadline + markFailedGracePeriod` is restricted to
`{ original requester, contract owner, any committee member }`. After that window any caller may
finalize the failure. Default `markFailedGracePeriod = 0` preserves the legacy permissionless flow.

### H-26 — timestamp-clock `requestBlock`

`Committee.requestBlock` stores `block.timestamp` (EIP-6372 timestamp mode) so that `getPastVotes` /
`getTicketBalanceAtBlock` lookups against the `EnclaveTicketToken` resolve consistently across L1
and L2 clocks. The field name is preserved for storage / event ABI compatibility.
90 changes: 60 additions & 30 deletions agent/flow-trace/04_DKG_AND_COMPUTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -610,14 +610,17 @@ ThresholdKeyshare receives AllThresholdSharesCollected
│ │ 3. committeeHash = keccak256(abi.encodePacked(c.topNodes)) │
│ │ c.committeeHash = committeeHash │
│ │ 4. When proofAggregationEnabled: │
│ │ e3.pkVerifier.verify(pkCommitment, committeeHash, proof) │
│ │ → BFV: `BfvPkVerifier` (DkgAggregator Honk) │
│ │ • pins `publicInputs[0]` = nodes_fold VK hash │
│ │ • pins `publicInputs[1]` = C5 VK hash │
│ │ • checks committee_hash_hi/lo vs committeeHash │
│ │ • checks last PI == pkCommitment │
│ │ Redeploy `BfvPkVerifier` / `BfvDecryptionVerifier` │
│ │ when sub-circuit VK immutables change. │
│ │ e3.pkVerifier.verify( │
│ │ e3Id, committeeRoot, c.topNodes, │
│ │ pkCommitment, committeeHash, proof │
│ │ ) │
│ │ → BFV: `BfvPkVerifier` (DkgAggregator Honk) │
│ │ • M-34: immutable nodesFold / C5 VK hashes │
│ │ checked against publicInputs[0..1] │
│ │ • C-08: committee_hash_hi/lo (slots │
│ │ [2+H] & [3+H]) vs committeeHash │
│ │ • last PI == pkCommitment │
│ │ • M-35: revert on failure (no `bool false`) │
│ │ 5. c.publicKey = pkCommitment │
│ │ publicKeyHashes[e3Id] = pkCommitment │
│ │ 6. enclave.onCommitteePublished(e3Id, pkCommitment) │
Expand All @@ -636,6 +639,14 @@ ThresholdKeyshare receives AllThresholdSharesCollected
│ └─────────────────────────────────────────────────────┘
```

> **C-08 (BfvPkVerifier domain binding) — implemented** The wrapper exposes a
> `verify(e3Id, committeeRoot, sortedNodes, pkCommitment, committeeHash, proof)` signature.
> `committeeHash` (computed on-chain as `keccak256(abi.encodePacked(c.topNodes))`) is split into
> 128-bit Noir field limbs and checked against `publicInputs[committeeHashHiIdx]` and
> `publicInputs[committeeHashLoIdx]`, binding the proof to the specific committee. The contextual
> params `(e3Id, committeeRoot, sortedNodes)` are forwarded for interface compatibility and future
> circuit-level binding.

---

## Phase 3: Encrypted Computation
Expand Down Expand Up @@ -845,35 +856,54 @@ EnclaveSolReader decodes CiphertextOutputPublished event
│ │ 2. require(now <= decryptionDeadline) │
│ │ 3. e3.plaintextOutput = output │
│ │ 4. decryptionVerifier.verify( │
│ │ e3Id, keccak256(output), proof │
│ │ e3Id, committeeRoot, │
│ │ committeeNodes, ciphertextOutput, │
│ │ committeePublicKey, │
│ │ keccak256(output), proof │
│ │ ) │
│ │ → BFV: `BfvDecryptionVerifier` │
│ │ • pins `publicInputs[0]` = c6_fold VK hash │
│ │ • pins `publicInputs[1]` = C7 VK hash │
│ │ • checks trailing 100 coeffs vs output hash │
│ │ Redeploy verifier when sub-circuit VKs change. │
│ │ → M-34: c6Fold / C7 VK hashes are immutable. │
│ │ → M-35: revert path only (no `bool false`). │
│ │ 5. stage = Complete │
│ │ 6. _distributeRewards(e3Id) │
│ │ │ │
│ │ │ ┌─ Reward Distribution ────────────────┐ │
│ │ │ │ 1. Get active committee nodes: │ │
│ │ │ │ nodes = ciphernodeRegistry │ │
│ │ │ │ .getActiveCommitteeNodes(e3Id) │ │
│ │ │ │ 2. If no active nodes: │ │
│ │ │ │ → Refund requester │ │
│ │ │ │ 3. Divide payment equally: │ │
│ │ │ │ perNode = payment / nodes.length │ │
│ │ │ │ dust → last member │ │
│ │ │ │ 4. Approve BondingRegistry │ │
│ │ │ │ 5. bondingRegistry.distributeRewards│ │
│ │ │ │ (token, nodes, amounts) │ │
│ │ │ │ → Transfers fee tokens to each │ │
│ │ │ │ registered operator │ │
│ │ │ │ 6. Emit RewardsDistributed │ │
│ │ │ └──────────────────────────────────────┘ │
│ │ │ ┌─ Reward Distribution (pull, H-01/M-02) ┐ │
│ │ │ │ 1. Get active committee nodes: │ │
│ │ │ │ nodes = ciphernodeRegistry │ │
│ │ │ │ .getActiveCommitteeNodes(e3Id) │ │
│ │ │ │ 2. If no active nodes: │ │
│ │ │ │ → push refund to requester │ │
│ │ │ │ 3. Split payment: │ │
│ │ │ │ protocolAmount = total * shareBps │ │
│ │ │ │ cnAmount = total - protocol │ │
│ │ │ │ perNode = cnAmount / nodes.length │ │
│ │ │ │ dust → nodes[e3Id % n] (M-07: │ │
│ │ │ │ rotates dust slot per E3 so the │ │
│ │ │ │ same physical node is not always │ │
│ │ │ │ favored) │ │
│ │ │ │ 4. Credit treasury (no push): │ │
│ │ │ │ _pendingTreasury[treasury][token] │ │
│ │ │ │ += protocolAmount │ │
│ │ │ │ Emit TreasuryCredited(...) │ │
│ │ │ │ 5. Credit each node (no push): │ │
│ │ │ │ _pendingRewards[e3Id][node] │ │
│ │ │ │ += perNode │ │
│ │ │ │ Emit RewardCredited(...) │ │
│ │ │ │ 6. Emit RewardsDistributed (compat) │ │
│ │ │ │ 7. e3RefundManager │ │
│ │ │ │ .distributeSlashedFundsOnSuccess │ │
│ │ │ │ (e3Id, nodes, token) │ │
│ │ │ │ (also pull-based; see flow-05) │ │
│ │ │ └────────────────────────────────────────┘ │
│ │ 7. Emit PlaintextOutputPublished(e3Id, output, C7 proof) │
│ │ 8. Emit E3StageChanged(Complete) │
│ │ } │
│ │ │
│ │ // Funds are NOT pushed at publish-time. │
│ │ // Recipients must call: │
│ │ // - enclave.claimReward(e3Id) or │
│ │ // enclave.claimRewards(e3Ids[]) │
│ │ // - enclave.treasuryClaim(token) │
│ │ // emitting RewardClaimed / TreasuryClaimed. │
│ └─────────────────────────────────────────────────────┘
```

Expand Down
Loading
Loading