diff --git a/SUMMARY.md b/SUMMARY.md index 86a4d7b..9c0c3d3 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -152,6 +152,17 @@ * [Cookbook: private investor allocations with PoD](privacy-on-avalanche/cookbook-private-investor-allocations.md) * [Tutorial: private Adder on Avalanche Fuji](privacy-on-avalanche/tutorial-private-adder-fuji.md) * [Tutorial: custom privacy logic with PoD](privacy-on-avalanche/tutorial-custom-logic.md) +* [COTI confidential tokens & ERC-7984 compatibility](coti-erc7984/README.md) + * [Compatibility and divergence](coti-erc7984/compatibility-and-divergence.md) + * [On-chain data availability](coti-erc7984/on-chain-data-availability.md) + * [Decryption trust model](coti-erc7984/decryption-trust-model.md) + * [Input validation](coti-erc7984/input-validation.md) + * [Precision and decimals](coti-erc7984/precision-and-decimals.md) + * [Host-chain deployment](coti-erc7984/host-chain-deployment.md) + * [Transaction economics](coti-erc7984/transaction-economics.md) + * [Transfer semantics](coti-erc7984/transfer-semantics.md) + * [Concurrency](coti-erc7984/concurrency.md) + * [Deployed contracts](coti-erc7984/deployed-contracts.md) ## Security diff --git a/coti-erc7984/README.md b/coti-erc7984/README.md new file mode 100644 index 0000000..ece1508 --- /dev/null +++ b/coti-erc7984/README.md @@ -0,0 +1,59 @@ +# COTI confidential tokens & ERC-7984 compatibility + +[ERC-7984](https://eips.ethereum.org/EIPS/eip-7984) is a **Draft** ERC (created 3 July 2025) for confidential fungible tokens via `bytes32` pointers. The interface is technology-agnostic; the widely used reference stack is Zama FHEVM plus [OpenZeppelin confidential contracts](https://docs.openzeppelin.com/confidential-contracts/token). COTI does **not** implement ERC-7984 as a complete standard. It offers a confidential-token alternative with overlapping transfer shapes where that helps explorers and integrators, and a different ciphertext format, allowance model, and (on native COTI) decrypt path. + +This section compares ciphertext formats, input registration, decrypt paths, numeric range, and host-chain deployment. + +## Sources and versions + +Compared **6 September 2026** against: + +| Item | Version / status | +| :--- | :--------------- | +| ERC-7984 | Draft ([EIP-7984](https://eips.ethereum.org/EIPS/eip-7984)) | +| `@zama-fhe/sdk` | 3.5.1 (depends on `@fhevm/sdk` 0.13.2; TFHE WASM v1.6.2) | +| `@fhevm/solidity` | 0.13.3 | +| `@openzeppelin/confidential-contracts` | 0.5.3 (`ERC7984` stores `euint64`; `decimals()` returns `6`) | +| Zama Sepolia relayer | `https://relayer.testnet.zama.org` | +| Input-size measurement | Relayer `POST /v2/input-proof` plus live `confidentialTransfer` calldata on [cUSDCMock](https://sepolia.etherscan.io/address/0x7c5BF43B851c1dff1a4feE8dB225b87f2C223639) | + +Zama figures below are for that stack. Protocol details can change; re-check the pinned packages before treating a number as current. + +## Why COTI is not “an ERC-7984 implementation” + +| Reason | What it means | +| :----- | :------------ | +| **Draft standard** | ERC-7984 is still a Draft ERC. Implementations already disagree on crypto formats and trust assumptions. | +| **Ciphertext format** | COTI `it*` / `gt*` / `ct*` is not Zama `euint*` / handles. Drop-in interface compatibility is blocked by crypto, not only API taste. | +| **Allowances, not operators** | ERC-7984 replaces amount-bounded `approve` with time-boxed **unlimited** `setOperator`. COTI keeps encrypted allowances (ERC-20 / ERC-2612-like) because private state lives on-chain and can be compared. | + +Compatibility where useful: familiar transfer shapes, metadata, and explorer-facing confidential-transfer signals — **without** claiming full ERC-7984 compliance. + +## Comparison + +| Dimension | COTI confidential tokens | Zama / FHE ERC-7984-style (pinned stack) | +| :-------- | :----------------------- | :---------------------------------------- | +| Private state | Encrypted values **on-chain** (`gt*` / user `ct*`) | 32-byte handles on-chain; ciphertext held by coprocessors | +| Delegation | Encrypted **amount-bounded** `approve` / `transferFrom` | Public **time-boxed operator** (any amount until `until`) | +| User decrypt | Client AES decrypt of on-chain `ct*` | Relayer HTTPS → Gateway / threshold KMS re-encrypt → client | +| Contract decrypt → public logic | Native COTI: [synchronous decrypt](../how-coti-works/advanced-topics/coti-vs-others.md#advantages-over-fhe) in the same call. PoD host-chain apps: [async request/callback](../privacy-on-demand/async-private-operations.md) | Encrypted branching via `FHE.select` in the same tx. Public branching needs a **later** tx with KMS signatures ([Zama branching](https://docs.zama.org/protocol/solidity-guides/smart-contract/logics/conditions)) | +| Input validity | On-chain `validateCiphertext` (precompile `0x64`) | Client ZKPoK sent to the Relayer; coprocessors verify; host chain checks **signed handles** | +| Typical encrypted input on the host chain | `itUint256`: two 32-byte limbs + ~65-byte signature (**~192 bytes** payload) | `externalEuint64` handle (32 bytes) + `inputProof` attestation (**230 bytes** for 1 handle / 3 signers) | +| Off-chain input blob | None for native encrypt; PoD HTTP encrypt is a separate helper | Packed ciphertext + ZKPoK to Relayer: **18,794 bytes** in this measurement | +| Numeric type in the token reference | **256-bit** (`itUint256` / `gtUint256`) | **`euint64`** in OpenZeppelin `ERC7984`; `decimals()` = **6**. `euint128` has the same arithmetic ops at higher [HCU](https://docs.zama.org/protocol/solidity-guides/development-guide/hcu); `euint256` has **no** add/mul | +| Trust | MPC / garbled-circuit network (threshold network key); user AES key on the client; PoD encryption HTTP helper **does** receive plaintext | Threshold coprocessors (InputVerifier signatures); threshold KMS; Relayer is a liveness path (documented as untrusted for plaintext) | +| Host deployment | Any EVM with a PoD Inbox (computation on COTI) | FHEVM host contracts + coprocessor / Gateway assumptions | +| Concurrency | PoD: multiple in-flight requests per account, applied by request id / nonce | Host txs follow ordinary EVM account nonces; overlapping writes to the same handle are ordered by coprocessor execution of host events. Unwrap is two host txs | + +## In this section + +1. [**Compatibility and divergence**](compatibility-and-divergence.md) — draft standard, format mismatch, operators vs allowances. +2. [**On-chain data availability**](on-chain-data-availability.md) — private state as contract state; private → public control flow. +3. [**Decryption trust model**](decryption-trust-model.md) — client AES vs Relayer/KMS; encryption helper plaintext. +4. [**Input validation**](input-validation.md) — `validateCiphertext` vs Relayer ZKPoK + on-chain signatures. +5. [**Precision and decimals**](precision-and-decimals.md) — 256-bit vs `euint64` / 6 decimals / `euint128` cost. +6. [**Host-chain deployment**](host-chain-deployment.md) — PoD Inbox; token on your chain. +7. [**Transaction economics**](transaction-economics.md) — measured calldata vs off-chain proof size. +8. [**Transfer semantics**](transfer-semantics.md) — encrypted allowances and silent insufficient-balance handling. +9. [**Concurrency**](concurrency.md) — in-flight PoD requests vs Zama handle updates. +10. [**Deployed contracts**](deployed-contracts.md) — live pTokens on Fuji and Sepolia. diff --git a/coti-erc7984/compatibility-and-divergence.md b/coti-erc7984/compatibility-and-divergence.md new file mode 100644 index 0000000..c4621d8 --- /dev/null +++ b/coti-erc7984/compatibility-and-divergence.md @@ -0,0 +1,43 @@ +# Compatibility and divergence + +COTI offers confidential fungible tokens. It does **not** ship a complete [ERC-7984](https://eips.ethereum.org/EIPS/eip-7984) implementation. ERC-7984 is a **Draft** ERC (created 3 July 2025; citation still `[DRAFT]` as of 6 September 2026). + +## 1. Draft interface, multiple cryptosystems + +ERC-7984 standardises names and `bytes32` **pointers**. Pointer resolution, ciphertext format, and trust assumptions are implementation-specific. The common reference is [OpenZeppelin confidential contracts](https://docs.openzeppelin.com/confidential-contracts/token) on Zama FHEVM (`@openzeppelin/confidential-contracts` 0.5.3, balances as `euint64`). + +COTI uses a different crypto (`it*` / `gt*` / `ct*`). Selective compatibility (transfer shapes, metadata, explorer-facing confidential-transfer signals) is not byte-compatible ERC-7984. + +## 2. Encrypted data formats + +| Role | COTI | Zama / FHE ERC-7984-style (pinned stack) | +| :--- | :--- | :---------------------------------------- | +| User input | `it*` (ciphertext + signature) | `externalEuint*` handle + `inputProof` (**coprocessor signatures**, not the ZKPoK — [Input validation](input-validation.md)) | +| Network private state | `gt*` (network-key / garbled) | Handle; ciphertext with coprocessors; ACL | +| User-readable output | `ct*` (account AES key) | Handle; decrypt via Relayer / KMS ([Decryption trust model](decryption-trust-model.md)) | + +## 3. Operators vs encrypted allowances + +ERC-7984 replaces amount-bounded `approve` with: + +```solidity +// Public relationship. Operator may transfer any amount until `until`. +function setOperator(address operator, uint48 until) external; +function isOperator(address holder, address spender) external view returns (bool); +``` + +While the grant is live, the operator can move **any** amount of the holder’s token. `OperatorSet` / `isOperator` are **plaintext**. Balances and transfer amounts stay encrypted. The EIP rationale is to avoid tracking encrypted approval amounts on the external system. + +COTI keeps **encrypted, amount-bounded** allowances (ERC-20 / ERC-2612-like `approve` / `transferFrom`): + +- Allowance values are ciphertext. +- Compare-and-subtract runs in private execution ([on-chain data availability](on-chain-data-availability.md)). +- There is no public “unlimited until time T” grant as the primary delegation model. + +| | COTI | ERC-7984 / OpenZeppelin `ERC7984` | +| :- | :--- | :-------------------------------- | +| Delegation | Encrypted amount-bounded allowance | Time-boxed operator | +| Amount the delegate may spend | The approved encrypted amount | Any amount until `until` | +| Relationship on-chain | Allowance ciphertext | Public `isOperator` / `OperatorSet` | + +[Transfer semantics](transfer-semantics.md) covers silent insufficient-balance handling. Native PrivateERC20 and PoD pERC20 developer guides live under COTI Privacy Portal and Build on COTI. diff --git a/coti-erc7984/concurrency.md b/coti-erc7984/concurrency.md new file mode 100644 index 0000000..c35156d --- /dev/null +++ b/coti-erc7984/concurrency.md @@ -0,0 +1,21 @@ +# Concurrency + +How overlapping confidential transfers are ordered. + +## COTI PoD (host-chain pTokens) + +A user can submit **multiple** Inbox requests while earlier ones are still in flight. Each request has a **request id**. A **monotonic nonce** on the COTI-side token applies results in order so two in-flight transfers from the same account do not clobber each other. + +The host-chain mental model is still **async**: submit → wait → callback. See [Async private operations](../privacy-on-demand/async-private-operations.md). + +## Zama / FHEVM ERC-7984-style tokens + +Host-chain transactions follow the ordinary **EVM account nonce**. One account cannot have two pending host txs mined out of nonce order. + +Coprocessors execute FHE ops in the order of host events for a given handle. There is no PoD-style in-flight **request queue** on the token: a `confidentialTransfer` that already passed Relayer input registration is a single host transaction. + +Flows that need a **public** result take **two host transactions** (for example `unwrap` then `finalizeUnwrap`). Those two txs are sequenced by the EVM nonce and by waiting for KMS signatures in between — not by a PoD request id. + +## Transfer semantics + +Silent insufficient-balance handling (encrypted amount → effective zero) is independent of this queueing model. See [Transfer semantics](transfer-semantics.md). diff --git a/coti-erc7984/decryption-trust-model.md b/coti-erc7984/decryption-trust-model.md new file mode 100644 index 0000000..cd52f82 --- /dev/null +++ b/coti-erc7984/decryption-trust-model.md @@ -0,0 +1,69 @@ +# Decryption trust model + +Who can see plaintext, and which services sit on the path? + +**Decrypt of user `ct*` on COTI is local:** the client reads ciphertext from chain and AES-decrypts with the account key. That step does not send ciphertext to a server for plaintext recovery. + +**Encrypt is a separate path.** Native COTI SDKs can encrypt locally with the same AES key. PoD dApps that call `CotiPodCrypto.encrypt` **POST the plaintext** to the PoD encryption HTTP service (`buildEncryptedInputs`). That helper **does receive plaintext**. Do not treat “client-side decrypt” as “no server ever sees the value.” + +Zama user decrypt goes Relayer HTTPS → Gateway / threshold KMS (re-encrypt to the user’s transport key) → client. The Relayer is documented as untrusted for plaintext; the KMS and coprocessors are the cryptographic trust base. + +## Trust assumptions + +| | COTI | Zama / FHEVM (pinned stack, 6 Sep 2026) | +| :- | :--- | :-------------------------------------- | +| Who holds the network key | MPC / garbled-circuit nodes hold **threshold shares** of the network AES key. No single node is documented as able to reconstruct it ([AES keys](../how-coti-works/advanced-topics/aes-keys.md)). | Threshold **coprocessors** attest inputs (`InputVerifier` signatures). Threshold **KMS** decrypts under the FHE network key. | +| User key | Account AES key from onboarding (`GetUserKey` / wallet plugin). The client must store it; loss means those `ct*` values cannot be decrypted. | User transport key for KMS re-encrypt. ACL on handles controls who may request decrypt. | +| Encrypt-time plaintext | **PoD:** encryption HTTP service reads the plaintext. **Native COTI:** `encryptValue` can stay on the client. | Client WASM encrypts under the FHE public key. Relayer sees packed ciphertext + ZKPoK, not the plaintext. | +| Decrypt-time plaintext | Client AES on `ct*`. No Relayer decrypt hop. | Client recovers plaintext after KMS re-encrypt. Relayer/Gateway are on the path (liveness). | +| Liveness | PoD also depends on Inbox / relayer for **request and callback**. Native COTI decrypt does not. | Relayer + Gateway for **input registration and decrypt**. Failure can happen before a host tx exists ([Input validation](input-validation.md)). | + +## COTI: local AES decrypt + +1. Onboard → account AES key. +2. Contract returns or stores `ct*` for that user (`offBoardToUser` / PoD callback). +3. Client reads the ciphertext from chain and decrypts locally (AES + XOR). + +```typescript +import { CotiPodCrypto, DataType } from "@coti-io/pod-sdk"; + +// Ciphertext already on-chain (e.g. from balanceOf / offBoardToUser). +const ciphertextHex = await token.balanceOf(userAddress); + +// Decrypt is in-process with the user's AES key. +const plain = CotiPodCrypto.decrypt( + ciphertextHex.toString(), + accountAesKey, + DataType.Uint256 +); +``` + +```typescript +// PoD encrypt: plaintext is in the HTTP body. The encryption service reads it. +const enc = await CotiPodCrypto.encrypt( + "1000", + "testnet", + DataType.itUint256 +); +``` + +## Zama: Relayer HTTPS → KMS re-encrypt → client + +Typical TypeScript ([Zama encrypt & decrypt](https://docs.zama.org/protocol/sdk/guides/encrypt-decrypt)): + +```typescript +import { ZamaSDK } from "@zama-fhe/sdk"; + +const { encryptedValues, inputProof } = await sdk.encrypt({ + values: [{ value: 1000n, type: "euint64" }], + contractAddress, + userAddress, +}); + +// User decrypt of a handle: Relayer HTTPS → Gateway / KMS re-encrypt. +const decrypted = await sdk.decryption.decryptValues([ + { encryptedValue: handleFromChain, contractAddress }, +]); +``` + +**Public** decrypt (plaintext that a contract can `require` on) is a different flow: off-chain KMS signatures, then a **later** host transaction. See [On-chain data availability](on-chain-data-availability.md) and COTI native [synchronous decrypt](../how-coti-works/advanced-topics/coti-vs-others.md#advantages-over-fhe). diff --git a/coti-erc7984/deployed-contracts.md b/coti-erc7984/deployed-contracts.md new file mode 100644 index 0000000..2417359 --- /dev/null +++ b/coti-erc7984/deployed-contracts.md @@ -0,0 +1,21 @@ +# Deployed contracts + +Six PoD pTokens on public testnets (addresses as of this documentation; redeploys can change them). These are **COTI confidential wrappers**, not ERC-7984 deployments. + +**Avalanche Fuji** + +| Token | Address | Decimals | +| :------- | :------------------------------------------- | :------: | +| `p.MTT` | `0x02f284a1968160E1d3e4bC2BA3261be49725E765` | 18 | +| `p.USDC` | `0x21576D8CCE47d044C5815bd59eca1F6DA94c65A5` | 6 | +| `p.AVAX` | `0x74d47cD68203066c97BA99787Fe1e0c68Ce42b04` | 18 | + +**Ethereum Sepolia** + +| Token | Address | Decimals | +| :------- | :------------------------------------------- | :------: | +| `p.MTT` | `0x0510F0b32828D5fB472dE5A5bE30b370c5D1a056` | 18 | +| `p.USDC` | `0xD7B3D49F85000489708B7db5B0f1a8693Fc707f3` | 6 | +| `p.ETH` | `0xd33A363459c6Ee0C4F8504E380E8D3Aa4F209116` | 18 | + +Each pair is a `PrivacyPortalFactory` minimal-proxy clone: one portal and one pToken per asset. Decimals match the underlying (see [Precision and decimals](precision-and-decimals.md)). diff --git a/coti-erc7984/host-chain-deployment.md b/coti-erc7984/host-chain-deployment.md new file mode 100644 index 0000000..fdecd2b --- /dev/null +++ b/coti-erc7984/host-chain-deployment.md @@ -0,0 +1,14 @@ +# Host-chain deployment + +Privacy on Demand puts the **confidential wrapper on the host chain**. Encrypted computation runs on COTI. The host does not need FHE precompiles, a forked EVM, or an ERC-7984 stack. + +| | | +| :--- | :--- | +| Host requirement | An EVM chain with a PoD Inbox | +| Live testnets (6 Sep 2026) | Avalanche Fuji, Ethereum Sepolia | +| Token contract | Ordinary host-chain bytecode (portal + pToken clone) | +| Private execution | COTI MPC / garbled-circuit path, via Inbox request/callback | + +Zama FHEVM confidential tokens instead assume FHEVM host contracts, Relayer/Gateway, and coprocessors on that host. + +Inbox and executor roles: [Architecture and main components](../privacy-on-demand/architecture-and-components.md). Live addresses: [Deployed contracts](deployed-contracts.md). ERC-7984 relationship: [Compatibility and divergence](compatibility-and-divergence.md). diff --git a/coti-erc7984/input-validation.md b/coti-erc7984/input-validation.md new file mode 100644 index 0000000..36fa529 --- /dev/null +++ b/coti-erc7984/input-validation.md @@ -0,0 +1,98 @@ +# Input validation + +Confidential transfers need a way to prove that an encrypted input is well-formed and bound to the caller. The stacks do that in different places. + +On **COTI**, the contract (or the PoD path into COTI) calls an **on-chain precompile**. Invalid ciphertext or signature reverts in that transaction. + +On **Zama FHEVM**, the client builds a zero-knowledge proof of knowledge (ZKPoK) locally, but **that proof is not the host-chain calldata**. The client sends the packed ciphertext and ZKPoK to the **Relayer**. Coprocessors verify it on the Gateway path and return **ECDSA-signed handles**. The contract’s `inputProof` argument is those signatures, checked by `InputVerifier`. + +The practical consequences are Relayer/Gateway dependency, ZKPoK verification cost, and failures that can happen **before** any host-chain transaction is sent — not a multi-kilobyte ZK proof sitting in L1 calldata. + +## Zama: ZKPoK off-chain, signed handles on-chain + +Flow (current protocol, measured 6 September 2026): + +1. The client encrypts under the FHE public key and produces a ZKPoK (WASM; `@zama-fhe/sdk` 3.5.1 / TFHE WASM v1.6.2). Multi-thread WASM needs `SharedArrayBuffer` and COOP/COEP; otherwise the SDK falls back to single-thread proving. +2. The SDK `POST`s the blob to the Relayer (`/v2/input-proof`). Coprocessors verify the ZKPoK and store the ciphertext. +3. The Relayer returns handles plus coprocessor signatures. That byte string is what Solidity calls `inputProof`. +4. The user (or the SDK) sends the host-chain transaction. `FHE.fromExternal(handle, inputProof)` checks signatures against a threshold in `InputVerifier`. Comments in that contract state the layout: `numHandles + numSigners + handles + coprocessorSignatures` (+ `extraData`). + +If Relayer or coprocessor verification fails, **no host transaction is submitted**. Users still pay for ZKPoK verification on the Gateway path (Relayer-coordinated). Both **input registration** and **decryption** depend on that Relayer / Gateway / KMS flow. + +### What we measured + +Encrypting one `euint64` for Sepolia [cUSDCMock](https://sepolia.etherscan.io/address/0x7c5BF43B851c1dff1a4feE8dB225b87f2C223639) via `https://relayer.testnet.zama.org`: + +| Object | Size | What it is | +| :----- | ---: | :--------- | +| Relayer field `ciphertextWithInputVerification` | **18,794 bytes** | Packed ciphertext + ZKPoK (hex payload, no `0x` prefix; 37,588 hex chars) | +| Host-chain `inputProof` returned by `sdk.encrypt` | **230 bytes** | `0x01` handles, `0x03` signers, 32-byte handle, 3 × 65-byte signatures, 1-byte `extraData` | +| Live `confidentialTransfer` calldata (9 txs, 5 Sep 2026) | **452 bytes** typical | Selector + `address` + handle + ABI-encoded 230-byte `inputProof` | + +Example host transaction: [`0x1a8b317a…d2976b`](https://sepolia.etherscan.io/tx/0x1a8b317ad2593aacad3ad1a9344482e7eeac2cea6d288729fcef02052ad2976b) (`inputProof` length 230, gas used 1,464,996). + +`InputVerifier` documents the same split: the off-chain bundle is `compressedPackedCT+ZKPOK`; the handle is derived from it; calldata is handles and coprocessor signatures. + +Illustrative TypeScript ([Zama encrypt & decrypt](https://docs.zama.org/protocol/sdk/guides/encrypt-decrypt)): + +```typescript +import { ZamaSDK } from "@zama-fhe/sdk"; + +// Client WASM builds a ZKPoK. sdk.encrypt() sends it to the Relayer and +// waits for coprocessor signatures. Failures here happen before any host tx. +const { encryptedValues, inputProof } = await sdk.encrypt({ + values: [{ value: amount, type: "euint64" }], + contractAddress, + userAddress, +}); + +// inputProof is signed handles, not the ZKPoK. +await token.confidentialTransfer(to, encryptedValues[0], inputProof); +``` + +Legacy Hardhat plugin shape (`createEncryptedInput(...).encrypt()`) is the same architecture: prove locally, register via Relayer/Gateway, pass attestations on-chain. + +## COTI: `validateCiphertext` on-chain + +The user encrypts with their AES key and signs the input (`it*`: ciphertext + signature). The contract (or PoD path into COTI) calls the MPC precompile. If the ciphertext or signature is invalid, the call **reverts**. There is no client ZK proof of encryption validity and no Relayer registration step before the host transaction. + +Precompile address: `0x0000000000000000000000000000000000000064`. + +Solidity (via `MpcCore`): + +```solidity +import {MpcCore, itUint256, gtUint256} from "@coti-io/coti-contracts/contracts/utils/mpc/MpcCore.sol"; + +function transfer(address to, itUint256 calldata value) external { + // On-chain validation: bad ciphertext / signature → revert. + gtUint256 gtValue = MpcCore.validateCiphertext(value); + _transfer(msg.sender, to, gtValue); +} +``` + +Conceptually the precompile is: + +```solidity +// ExtendedOperations at address 0x64 +function ValidateCiphertext( + bytes1 metaData, + uint256 ciphertextHigh, + uint256 ciphertextLow, + bytes calldata signature +) external returns (uint256 result); +``` + +**What the client does on COTI:** encrypt and sign, then submit. **What the chain does:** verify and either accept (`it*` → `gt*`) or revert. + +Native COTI SDKs can encrypt locally with the account AES key. PoD dApps often call an HTTP encryption helper instead; that helper receives plaintext (see [Decryption trust model](decryption-trust-model.md)). + +| | COTI | Zama / FHE (pinned stack) | +| :- | :--- | :------------------------ | +| Who verifies well-formed encryption | On-chain `validateCiphertext` | Coprocessors verify ZKPoK; host chain verifies coprocessor signatures | +| Client work | Encrypt + sign | Encrypt + prove (WASM), then Relayer round-trip | +| Host-chain encrypted input | ~192-byte `itUint256` | 32-byte handle + **230-byte** attestation (1 input, 3 signers) | +| Off-chain proof blob | None | **~18.8 KB** to Relayer | +| Failure before the host tx | No Relayer gate | Relayer / coprocessor / Gateway can reject the ZKPoK first | +| Failure in the host tx | Precompile revert | `InputVerifier` revert (bad signatures, threshold, binding) | + +Related: [Decryption trust model](decryption-trust-model.md), [Transaction economics](transaction-economics.md), [Precompiles](../how-coti-works/advanced-topics/precompiles.md). diff --git a/coti-erc7984/on-chain-data-availability.md b/coti-erc7984/on-chain-data-availability.md new file mode 100644 index 0000000..02ab5aa --- /dev/null +++ b/coti-erc7984/on-chain-data-availability.md @@ -0,0 +1,61 @@ +# On-chain data availability + +Encrypted **balances** are contract state on both stacks. What differs is **where the bulk ciphertext lives**, and whether a contract can turn a **private** predicate into a **public** `if` in the **same** transaction. + +## Where private data lives + +| | COTI | Zama / FHEVM (pinned stack) | +| :- | :--- | :-------------------------- | +| What explorers see | Encrypted `gt*` / `ct*` words in storage (two 32-byte limbs for `ctUint256`) | 32-byte **handles**; ciphertext with coprocessors | +| Contract math on private values | MPC precompiles on `gt*` | FHE ops on handles (`FHE.add`, `FHE.select`, …) | +| User-readable ciphertext | `ct*` on-chain, AES-decrypt on the client | Handle on-chain; user decrypt via Relayer / KMS | + +```mermaid +flowchart LR + subgraph cotiModel [COTI] + PrivState[Encrypted gt or ct in storage] + Mpc[MPC precompile] + PrivState --> Mpc + end + subgraph zamaModel [Zama_FHEVM] + Handles[Handles in storage] + Coproc[Coprocessor ciphertext] + Handles -.-> Coproc + end +``` + +## Private vs public control flow + +Three different questions: + +1. **Encrypted branching** — pick ciphertext A or B without revealing the bit (`mux` / `FHE.select`). Both stacks do this in the transaction that runs the FHE/MPC op. +2. **Public branching** — a plaintext `if`, a public ERC-20 `transfer`, an event that discloses an amount. That needs a **plaintext**. +3. **When the plaintext exists** — same transaction, or a later one. + +| Path | Same transaction as the private compute? | Notes | +| :--- | :--- | :---- | +| **Native COTI** (gcEVM) | Yes | Contract can decrypt inside the call (`SetPublic` / garbled-circuit reveal) and then branch or send a public token. See [Advantages over FHE](../how-coti-works/advanced-topics/coti-vs-others.md#advantages-over-fhe). | +| **PoD host-chain tokens** | No | Private work runs on COTI. The host contract sees the result in an **Inbox callback** ([Async private operations](../privacy-on-demand/async-private-operations.md)). | +| **Zama FHEVM** | Encrypted: yes (`FHE.select`). Public: no | [Zama branching](https://docs.zama.org/protocol/solidity-guides/smart-contract/logics/conditions): moving from an encrypted condition to non-encrypted logic **requires off-chain public decryption**, then a **later** host tx (`FHE.checkSignatures` / `finalizeUnwrap`-style callbacks). | + +OpenZeppelin confidential wrappers follow that two-transaction public path (for example `unwrap` then `finalizeUnwrap`; swap then `finalizeSwap`). + +## Example: private tally, public payout + +On **native COTI**, a contract can hold encrypted tallies and, in `claim`, decrypt (or mux) and send a public ERC-20 in the **same** call. + +```solidity +function claim() external { + require(!claimed[msg.sender], "already claimed"); + claimed[msg.sender] = true; + // Native COTI: private result can become public in this call. + uint256 amount = /* decrypt or controlled reveal of votes[msg.sender] */; + require(rewardToken.transfer(msg.sender, amount), "transfer failed"); +} +``` + +On **Zama**, the auction/prize pattern in the FHEVM docs is: encrypted bids with `FHE.select` during the sale, then `makePubliclyDecryptable` / off-chain decrypt, then a **second** function that verifies KMS signatures and transfers the prize. + +On **PoD**, a host-chain pToken transfer is already request → COTI → callback. A public ERC-20 send gated on a private host-chain tally uses that same two-step Inbox path, not a native same-tx decrypt. + +Related: [Compatibility and divergence](compatibility-and-divergence.md), [Concurrency](concurrency.md), [Privacy on Demand architecture](../privacy-on-demand/architecture-and-components.md). diff --git a/coti-erc7984/precision-and-decimals.md b/coti-erc7984/precision-and-decimals.md new file mode 100644 index 0000000..93dc1f3 --- /dev/null +++ b/coti-erc7984/precision-and-decimals.md @@ -0,0 +1,40 @@ +# Precision and decimals + +PoD confidential wrappers are 1:1 collateralised. Lock WETH, get `p.ETH`. Lock USDC, get `p.USDC`. The private token mirrors the underlying — same decimals, same supply, same value — and unwraps back on demand. + +That is a **precision vs FHE-cost** choice on Zama, not a hard platform bit-width ceiling. + +## What the Zama token reference actually uses + +OpenZeppelin [`ERC7984`](https://docs.openzeppelin.com/confidential-contracts/token) (`@openzeppelin/confidential-contracts` 0.5.3) stores balances as **`euint64`** and implements `decimals()` as **`6`**. ERC-7984 itself only requires a plaintext `decimals()`; it does not mandate 64-bit math. `euint64` is the reference-design type. + +`uint64` max is \(2^{64}-1\) ≈ \(1.84 \times 10^{19}\): + +| Decimals | Max whole tokens in `euint64` | Fit with 18-decimal collateral | +| -------: | ----------------------------: | :----------------------------- | +| 18 | ≈ **18.45** | Does not fit a 1:1 WETH wrapper above ~18 ETH | +| 6 | ≈ **18.45 trillion** | Fits large supplies; **12 digits of token precision are dropped** vs 18-decimal ERC-20 math | + +Zama confidential wrappers for 18-decimal underlyings scale into 6 decimals (for example \(10^{12}\) on mock WETH). Arithmetic on 6-decimal amounts does not match 18-decimal ERC-20 rounding and dust behaviour. + +## `euint128` and `euint256` are not a silent 64-bit wall + +[FHEVM types](https://docs.zama.org/protocol/solidity-guides/smart-contract/types) (`@fhevm/solidity` 0.13.3): + +- **`euint64`** and **`euint128`** support the main arithmetic operators (add, sub, mul, comparisons, `select`, …). +- **`euint256`** supports bitwise ops, equality, `select`, and rand — **not** add/mul. It is not a drop-in confidential `uint256` balance type. + +[HCU](https://docs.zama.org/protocol/solidity-guides/development-guide/hcu) for non-scalar ops (higher = more coprocessor work): + +| Op | `euint64` | `euint128` | +| :- | --------: | ---------: | +| `add` | 162,000 | 259,000 | +| `mul` | 596,000 | 1,686,000 | + +A confidential 18-decimal WETH-style balance **can** be represented in `euint128` (\(2^{128}-1 / 10^{18}\) is far above any realistic ETH supply). The reference ERC-7984 token does not do that: it keeps `euint64` and 6 decimals so FHE cost stays lower. The tradeoff is **precision (and ERC-20 decimal compatibility) versus FHE cost**, not “the platform cannot exceed ~18 ETH.” + +## COTI + +COTI carries **256-bit** encrypted integers end to end (`itUint256` / `gtUint256` / `ctUint256`). PoD wrappers keep the underlying’s decimals. Four of the six live pTokens are 18-decimal, including `p.ETH` and `p.AVAX`. + +The [deployed contracts](deployed-contracts.md) page lists the decimals of each live pToken. diff --git a/coti-erc7984/transaction-economics.md b/coti-erc7984/transaction-economics.md new file mode 100644 index 0000000..2a04ef5 --- /dev/null +++ b/coti-erc7984/transaction-economics.md @@ -0,0 +1,25 @@ +# Transaction economics + +Confidentiality has a size and a **path** cost. On Zama FHEVM the client ZKPoK is sent to the Relayer. The host chain receives a short coprocessor attestation, not that proof. + +Measured **6 September 2026** (`@zama-fhe/sdk` 3.5.1, Sepolia cUSDCMock): + +| | **COTI confidential tokens** | **Zama / FHE ERC-7984-style** | +| :--------------- | :--------------------------- | :---------------------------- | +| Encrypted input on the host chain | **`itUint256` ~192-byte payload** (two 32-byte limbs + ~65-byte signature) | 32-byte handle + **230-byte** `inputProof` (1 handle, 3 signers) | +| Typical `confidentialTransfer` calldata | Same order of magnitude as a signed `it*` transfer | **452 bytes** in 8 of 9 sampled txs | +| Packed ciphertext + ZKPoK | Not used | **18,794 bytes** in Relayer `ciphertextWithInputVerification` | +| Where validity is checked | `validateCiphertext` in the same host/COTI call | Relayer / coprocessors first; `InputVerifier` signatures on-chain | +| Client-side work | Encrypt and sign (WASM ZK proving not required) | Generate ZKPoK (WASM), wait for Relayer | +| Numeric type in the token reference | 256-bit | `euint64` + 6 decimals in OpenZeppelin `ERC7984` | + +On-chain encrypted-amount size is hundreds of bytes on both stacks. What differs: + +- Users pay for **ZKPoK verification** on the Gateway path even though the ZK proof never appears in host calldata. +- Input registration and decryption **depend on the Relayer**. A Relayer or coprocessor failure aborts **before** the host transaction exists. +- Host `confidentialTransfer` gas on the sampled Sepolia txs was about **0.96M–1.85M**. +- FHE arithmetic is metered in [HCU](https://docs.zama.org/protocol/solidity-guides/development-guide/hcu) on the coprocessor, separate from calldata. + +COTI balances that are `ctUint256` occupy two ciphertext limbs (see [On-chain data availability](on-chain-data-availability.md)). Zama balances are 32-byte handles; the ciphertext lives with the coprocessors. + +For the Relayer flow, `InputVerifier` layout, and Solidity `validateCiphertext` examples, see [Input validation](input-validation.md). diff --git a/coti-erc7984/transfer-semantics.md b/coti-erc7984/transfer-semantics.md new file mode 100644 index 0000000..960d3cc --- /dev/null +++ b/coti-erc7984/transfer-semantics.md @@ -0,0 +1,20 @@ +# Transfer semantics + +## Allowances vs operators + +COTI confidential tokens keep **amount-bounded** `approve` / `transferFrom` (and ERC-2612-style permit where exposed). The allowance is an on-chain ciphertext. See [Compatibility and divergence](compatibility-and-divergence.md) for ERC-7984 `setOperator` (public, unlimited until `until`). + +## Insufficient encrypted balance + +When the transfer **amount is encrypted**, both stacks avoid reverting on insolvency so observers cannot distinguish “not enough” from a successful private transfer. + +| Stack | Mechanism | Host-visible result | +| :---- | :-------- | :------------------ | +| COTI PoD pToken (encrypted amount) | COTI-side `mux` of the effective amount to zero; callback **Success** | Request completes; balances unchanged if insolvent | +| OpenZeppelin `ERC7984` | `FHESafeMath.tryDecrease` + `FHE.select(success, amount, 0)` | Transaction succeeds; `ConfidentialTransfer` amount is an encrypted zero | + +COTI **public** amounts (for example portal withdraw where fail vs success must be real) take the other branch: decrypt the comparison and return **Failure** instead of mux-to-zero. + +Encrypted-path “success with zero moved” is therefore a **confidentiality** choice on both designs, not a COTI-only behaviour. + +Overlapping transfers: [Concurrency](concurrency.md). diff --git a/coti-privacy-portal/developer-guide/privateerc20.sol.md b/coti-privacy-portal/developer-guide/privateerc20.sol.md index 529360a..10a3956 100644 --- a/coti-privacy-portal/developer-guide/privateerc20.sol.md +++ b/coti-privacy-portal/developer-guide/privateerc20.sol.md @@ -39,6 +39,8 @@ PrivateERC20 follows the ERC20 interface, but changes how data is stored and exp | Supports plain uint256 operations | Yes | Yes | | Supports encrypted operations | No | Yes (`itUint256`, `gtUint256` variants) | +For why COTI does not implement [ERC-7984](https://eips.ethereum.org/EIPS/eip-7984) as a full standard — and how Privacy on Demand `pERC20` compares with Zama / FHE designs on data availability, allowances vs operators, decrypt trust, and input validation — see [COTI confidential tokens & ERC-7984 compatibility](../../coti-erc7984/README.md). + ### How it works #### Balances