diff --git a/coti-privacy-portal/developer-guide/frontend-integration.md b/coti-privacy-portal/developer-guide/frontend-integration.md
index 3f8186b..bdf1bc4 100644
--- a/coti-privacy-portal/developer-guide/frontend-integration.md
+++ b/coti-privacy-portal/developer-guide/frontend-integration.md
@@ -253,3 +253,15 @@ async function encryptedApprove(
return txHash;
}
```
+
+## Polling failed PoD / Privacy Portal requests
+
+When a pToken request leaves `Pending`, read `requests(requestId).status` and decode `failedRequests(requestId)`:
+
+1. If status is `SystemFailed` (or `failedRequests` ABI-decodes to Inbox `{ErrorData}` with `errorCode == 2`), show a system-error message. Do not retry the same request on COTI.
+2. For portal deposits, call `refundFailedDeposit(mintRequestId)` **only** after `SystemFailed`. App `raise` / `Failed` is not refundable.
+3. For portal withdrawals, call `cancelFailedWithdrawal(withdrawalId)` after the transfer request is `Failed` or `SystemFailed`.
+4. If the mint is still `Pending` but COTI Inbox `errors[id].errorCode == 1`, show an **execution failure** and offer / await **`retryFailedRequest`** (permissionless). Use **`getOutboxError(id)`** for the capped returndata and decode in the client. Do **not** refund while a retry can still mint.
+5. After `createPortal`, poll mother `isRegistered(sourceChainId, pToken)` before enabling deposits. A prior `FactoryNotAllowed` registration is recovered with allowlist + `retryFailedRequest`, not by assuming the portal is ready.
+
+For offline fee accounting, index `PrivacyPortal.OperationFeesPaid`: `portalFee` (protocol, retained), `podFee` (forwarded to inbox), and `podCallbackFee` (callback slice). `isDeposit` / `isNativeWrap` distinguish the operation.
diff --git a/coti-privacy-portal/developer-guide/troubleshooting.md b/coti-privacy-portal/developer-guide/troubleshooting.md
index cfa7e00..6b09a3c 100644
--- a/coti-privacy-portal/developer-guide/troubleshooting.md
+++ b/coti-privacy-portal/developer-guide/troubleshooting.md
@@ -11,6 +11,30 @@
| `AES key mismatch` | Wrong AES key used to decrypt | Re-onboard wallet to get correct key |
| `invalid BytesLike value` | Passing HardhatEthersSigner to `prepareIT256` | Use `new ethers.Wallet(privateKey, provider)` instead |
| `PRIVATE_AES_KEY_TESTNET not set` | Missing env var | Add 32 hex char key to `.env` (no `0x` prefix) |
+| Deposit stuck `Pending` forever | Several distinct causes (see below) | Diagnose with pToken `requests` / `failedRequests` and Inbox `errors` / `getOutboxError` |
+| Withdraw stuck `TransferPending` | Transfer-to-portal request Failed | Call `PrivacyPortal.cancelFailedWithdrawal(withdrawalId)`; do not expect underlying release |
+| Encode/`validateCiphertext` fail (`failedRequests` as `{ErrorData}` with `errorCode == 2`) | Wrong `it*` signer / encode fail | Clear UI pending; submit a **new** op (not COTI `retryFailedRequest`) |
+| Mother registration / `FactoryNotAllowed` | Factory not allowlisted on COTI mother before registration mined | Allowlist factory (`setAllowedFactory`), then permissionless COTI `retryFailedRequest` for the failed registration id |
+| Deposits before mother confirms | Portal created but mother `isRegistered` still false | Keep deposits disabled / pause until mother registration confirms |
+
+### Stuck `Pending` deposits — diagnose before refunding
+
+A deposit mint can remain **`Pending`** for different reasons. Do **not** assume every stuck mint is a system-failed encode:
+
+| Observation | Likely cause | Action |
+| --- | --- | --- |
+| `requests(id).status == SystemFailed` / `failedRequests` decodes to Inbox `{ErrorData}` code `2` | Encode / `validateCiphertext` failed before COTI mint logic | Clear UI pending; call `refundFailedDeposit` (permissionless) after SystemFailed |
+| COTI Inbox `errors[id].errorCode == 1` | Target execution reverted (retryable) | Call permissionless `retryFailedRequest` on COTI; do **not** refund while mint may still succeed |
+| No COTI incoming request / never mined | Relayer lag or miner not ingesting | Wait for miner; do not treat as SystemFailed |
+| Mother never registered / factory not allowlisted | Registration one-way failed or never confirmed | Fix allowlist + `retryFailedRequest`; keep user deposits off until `isRegistered` |
+
+**Break-glass:** some deployments expose an admin refund for still-`Pending` escrows. That is operationally dangerous if COTI mint can still succeed afterward—prefer SystemFailed refunds and retries.
+
+### Privacy Portal recovery
+
+* **System-failed deposit (mint):** underlying stays escrowed until anyone calls `refundFailedDeposit(mintRequestId)` after `pToken.requests(id).status == SystemFailed` (funds always return to the depositor). App `raise` / `Failed` is **not** refundable (mint should not raise). Portal protocol fee is kept.
+* **Failed withdraw (transfer):** after the transfer request is `Failed` or `SystemFailed`, call `cancelFailedWithdrawal(withdrawalId)` to mark the withdrawal `Failed`. Underlying is **not** released; the user still holds pTokens; portal fee is kept.
+* **Factory → mother registration:** `createPortal` **submits** a one-way registration message; it does **not** wait for the mother to confirm. Keep deposits disabled until `PodErc20CotiMother.isRegistered(sourceChainId, pToken)` is true. If registration failed with `FactoryNotAllowed` before allowlist, allowlist the factory then call COTI `retryFailedRequest`.
### Security Notes
@@ -20,3 +44,4 @@
* Self-transfers (`from == to`) are explicitly blocked at the contract level.
* `transferAndCall` is protected by `nonReentrant` but the callback contract must be trusted.
* `totalSupply()` always returns `0` for privacy — do not rely on it for supply accounting.
+* Inbox `executed` / compact response events mean the return leg was **received**, not that your app callback committed—confirm via pToken / portal status.
diff --git a/privacy-on-demand/README.md b/privacy-on-demand/README.md
index 57c028c..1f792f9 100644
--- a/privacy-on-demand/README.md
+++ b/privacy-on-demand/README.md
@@ -2,7 +2,7 @@
Privacy on Demand lets applications use **strong privacy for data and computation** while still using **ordinary EVM chains** (Ethereum, L2s, and other compatible networks) for accounts, assets, and business workflows.
-> **Development status:** This Privacy on Demand material and the **COTI PoD SDK** it describes are **under active development**. Treat them accordingly: on-chain and client code **may not yet be fully audited**, and **breaking changes** (APIs, ABIs, addresses, presets, or documentation) can occur as the stack matures. Pin versions, follow release notes, and perform your own review before relying on anything in production.
+> **Development status:** This Privacy on Demand material and the **COTI PoD SDK** it describes are **under active development**. Treat them accordingly: pin versions, follow release notes, and perform your own review before relying on anything in production. External audits for related COTI components are listed under [Audit Reports](../security/audit-reports.md); the PoD Inbox and Privacy Portal stacks also received an **internal security review** with documented hardening (see that page). **Breaking changes** (APIs, ABIs, addresses, presets, or documentation) can still occur as the stack matures.
diff --git a/privacy-on-demand/architecture-and-components.md b/privacy-on-demand/architecture-and-components.md
index 788dbf7..43f5627 100644
--- a/privacy-on-demand/architecture-and-components.md
+++ b/privacy-on-demand/architecture-and-components.md
@@ -73,7 +73,9 @@ A fuller table lives in the SDK’s [data types](https://github.com/cotitech-io/
## Trust and security highlights (for architects)
- **Callback authentication**: Your contract should only accept **Inbox-originated** callbacks for private results—otherwise anyone could try to spoof answers. The SDK’s `onlyInbox` pattern exists for this boundary ([features](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/03-features.md)).
-- **Request correlation**: Private work completes **later**; your system must track **request IDs** and statuses honestly in UX and backends ([Async private operations](async-private-operations.md)).
+- **Trusted miner / relayer**: Cross-domain delivery is operated by **registered Inbox miners**. Payload authenticity for private results still rests on **`onlyInbox` callbacks** and your application’s request-status accounting—not on an on-chain proof of the remote execution transcript. Treat miner set ownership (multisig / timelock) as part of the threat model.
+- **Request correlation**: Private work completes **later**; your system must track **request IDs** and statuses honestly in UX and backends ([Async private operations](async-private-operations.md)). Do **not** treat Inbox `executed` / compact response events alone as proof that your callback committed.
+- **Failure surfaces**: Distinguish **system error** (code `2`, not retryable), **app `raise`**, and **execution failure** (code `1`, permissionless `retryFailedRequest`). Use **`getOutboxError`** for the capped returndata bytes (decode in the client).
- **Key stewardship**: Client-side AES material is powerful; treat it like **credentials**, not analytics metadata ([TypeScript integration](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/06-typescript-integration-ux-development.md)).
## Next steps
diff --git a/privacy-on-demand/async-private-operations.md b/privacy-on-demand/async-private-operations.md
index 1a7e05b..dc0764a 100644
--- a/privacy-on-demand/async-private-operations.md
+++ b/privacy-on-demand/async-private-operations.md
@@ -20,6 +20,42 @@ Private execution happens **outside** your chain’s normal synchronous EVM fram
The SDK’s [Async execution](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/05a-async-execution.md) page lists the canonical lifecycle and common mistakes (wrong decode shape, missing `onlyInbox`, expecting same-block completion).
+### System errors vs application `raise`
+
+Both are delivered to the **same** source `errorSelector(bytes data)` (same path as `inbox.raise`). Branch with `inbox.inboxErrorType()` (`SystemError` vs `Exception`).
+
+| Kind | When | COTI target ran? | `data` layout | Retryable via `retryFailedRequest`? |
+| --- | --- | --- | --- | --- |
+| **System error** | Encode / `validateCiphertext` fails before the COTI app runs | No | Inbox `{ErrorData}`: `abi.encode(uint64 code, bytes message)` (code `2`). Sender is `SYSTEM_SENDER` | **No** |
+| **App `raise`** | COTI app calls `inbox.raise(...)` | Yes (started) | **dApp-defined** (e.g. `abi.encode(from, to, reason)`) | Submit a **new** request after pending clears |
+| **Execution failure** | Target reverts without `raise` (code `1`) | Yes | No automatic source callback; error stored on COTI Inbox | **Yes** on COTI (permissionless) |
+
+**Handler pattern** (see PodERC20 error callbacks):
+
+1. `onlyInbox`; `_errorCallbackContext()` **reverts** unless `inboxErrorType()` is `SystemError`/`Exception`, `sourceRequestId` is linked, and status is Pending.
+2. Branch on type: `SystemError` → decode Inbox `{ErrorData}`; `Exception` → decode your app `raise` layout.
+
+### One-way vs two-way error handling
+
+- **`sendOneWayMessage` rejects a non-zero `errorSelector`.** One-way jobs have no return / error callback leg. If you need an `errorSelector` handler, use a **two-way** message.
+- System-error and app-`raise` callbacks therefore apply to **two-way** flows that registered an `errorSelector`.
+
+### Execution failure, capped returndata, and `getOutboxError`
+
+When the COTI target reverts without `raise`, the miner records **error code `1`** (`ERROR_CODE_EXECUTION_FAILED`) and stores the first ≤**256** bytes of returndata in `errors[requestId].errorMessage`.
+
+- **`getOutboxError(requestId)`** returns `(code, data)` where `data` is those same raw bytes. Decode `Error(string)` / custom errors in your client (JS/TS).
+- If `data.length == 256`, the original returndata may have been longer (cap truncated it).
+
+Anyone may call permissionless **`retryFailedRequest(requestId)`** on COTI while the stored code is still `1`. A retry that fails to **re-encode** the call **reverts** and **keeps** code `1` (it does not flip the request to encode-failed / code `2`).
+
+### What `executed` and response events mean
+
+Inbox flags such as **`executed`** on an incoming request, and compact events such as **`IncomingResponseReceived`**, mean the **return / error leg was ingested** by the Inbox—not that your application callback **committed** successfully.
+
+- A return leg can still leave a **retryable** execution error (`errors[id]` with code `1`) if the callback reverted.
+- Product and indexers should treat **application events / request status** (for example pToken `requests(id).status`) as the source of truth for user-visible success or failure—not Inbox `executed` alone.
+
## What product and support teams should plan for
| Topic | Recommendation |
diff --git a/privacy-on-demand/cookbook-private-investor-allocations.md b/privacy-on-demand/cookbook-private-investor-allocations.md
index 91b774e..cd732a7 100644
--- a/privacy-on-demand/cookbook-private-investor-allocations.md
+++ b/privacy-on-demand/cookbook-private-investor-allocations.md
@@ -40,7 +40,8 @@ The public version is useful because it gives you a known baseline: owner assign
- A Solidity toolchain such as Hardhat or Foundry.
- A Sepolia wallet with test ETH for deploys, transactions, and PoD request fees.
- Node.js 18+ for scripts.
-- The PoD SDK package: `npm install "@coti/pod-sdk"`.
+- The PoD SDK package: `npm install "@coti/pod-sdk"` (ships the vendored `MpcCore.sol` under `@coti/pod-sdk/contracts/utils/mpc/`, so the COTI‑side contract no longer needs the `@coti-io/coti-contracts` package).
+- The COTI client crypto package: `npm install "@coti-io/coti-sdk-typescript@^1.0.7"` (provides `decryptUint256({ ciphertextHigh, ciphertextLow }, key)` for the 256‑bit ciphertext shape).
- A way for users to complete PoD onboarding and obtain their account AES key for local decryption.
Before implementing the private version, read:
@@ -411,14 +412,17 @@ const encryptedAllocation = await CotiPodCrypto.encrypt(
If you use `PodContract.encryptAndCallMethod`, you can pass the plaintext string plus `DataType.itUint256`; the SDK encrypts and encodes the argument before sending the transaction. If the browser or backend already encrypted the value, use `callMethod` with the ciphertext JSON.
-Investors decrypt only the ciphertext that was off-boarded to them.
+Investors decrypt only the ciphertext that was off-boarded to them. Because `ctUint256` is a struct, the contract read returns a tuple `{ ciphertextHigh, ciphertextLow }`:
```typescript
-const ct = await sepoliaAllocations.readResultByRequest(requestId);
-const ctHex = typeof ct === "bigint" ? "0x" + ct.toString(16) : String(ct);
+const raw = await sepoliaAllocations.readResultByRequest(requestId);
+const ct = {
+ ciphertextHigh: BigInt(raw.ciphertextHigh ?? raw[0]),
+ ciphertextLow: BigInt(raw.ciphertextLow ?? raw[1]),
+};
const plain = CotiPodCrypto.decrypt(
- ctHex,
+ ct,
accountAesKeyFromOnboarding,
DataType.Uint256
);
@@ -426,6 +430,8 @@ const plain = CotiPodCrypto.decrypt(
console.log("private allocation:", plain);
```
+Under the hood, the 256‑bit decrypt path calls `decryptUint256({ ciphertextHigh, ciphertextLow }, key)` from `@coti-io/coti-sdk-typescript` (`^1.0.7`). Narrower lanes (`Uint64`, `Uint128`) still take a single ciphertext word.
+
> **Warning:** Never log, persist, or transmit the account AES key as ordinary application data. Treat it as user-controlled key material.
## Part 7: Allocate and read private allocations
@@ -482,6 +488,8 @@ function onSetAllocationCompleted(bytes memory resultData) external onlyInbox {
For investor reads, the investor asks COTI to off-board their allocation to their address. The callback stores `ctUint256`, and the investor decrypts locally with their account AES key.
+`ctUint256` is a Solidity **struct** with two `ctUint128` limbs (`ciphertextHigh`, `ciphertextLow`), so the decoded local needs a `memory` location and the storage mapping holds the two‑limb tuple.
+
```solidity
mapping(bytes32 => ctUint256) public allocationReadResults;
@@ -490,7 +498,7 @@ function onAllocationRead(bytes memory resultData) external onlyInbox {
require(callerChain == COTI_TESTNET_CHAIN_ID && callerContract == cotiAllocationPeer, "not allowed");
bytes32 requestId = IInbox(inbox).inboxSourceRequestId();
- ctUint256 allocation = abi.decode(resultData, (ctUint256));
+ ctUint256 memory allocation = abi.decode(resultData, (ctUint256));
allocationReadResults[requestId] = allocation;
}
diff --git a/privacy-on-demand/for-developers-mapping-to-the-sdk.md b/privacy-on-demand/for-developers-mapping-to-the-sdk.md
index 96750f6..5140f46 100644
--- a/privacy-on-demand/for-developers-mapping-to-the-sdk.md
+++ b/privacy-on-demand/for-developers-mapping-to-the-sdk.md
@@ -28,10 +28,21 @@ Then deep dives:
| **Callback guard** | [InboxUser.sol](https://github.com/cotitech-io/coti-pod-sdk/blob/main/contracts/InboxUser.sol) (`onlyInbox`) — see [Features](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/03-features.md). |
| **PodLib** | [PodLib.sol](https://github.com/cotitech-io/coti-pod-sdk/blob/main/contracts/mpc/PodLib.sol) and width-specific libraries (`PodLib64`, `PodLib128`, `PodLib256`). |
| **PodUser / presets** | [PodUser.sol](https://github.com/cotitech-io/coti-pod-sdk/blob/main/contracts/mpc/PodUser.sol), network mixins such as [PodUserSepolia.sol](https://github.com/cotitech-io/coti-pod-sdk/blob/main/contracts/mpc/PodUserSepolia.sol) in [Getting started](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/04-getting-started.md). |
-| **Types (`it*`, `ct*`, `gt*`)** | [MpcCore.sol](https://github.com/cotitech-io/coti-pod-sdk/blob/main/contracts/utils/mpc/MpcCore.sol) and [Data types](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/contracts/01-it-ct-gt-data-types.md). |
+| **Types (`it*`, `ct*`, `gt*`)** | Vendored [MpcCore.sol](https://github.com/cotitech-io/coti-pod-sdk/blob/main/contracts/utils/mpc/MpcCore.sol) (and `MpcInterface.sol`) inside the PoD SDK — no separate `@coti-io/coti-contracts` package is needed for PoD apps. See [Data types](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/contracts/01-it-ct-gt-data-types.md). |
| **Custom COTI calls** | [MpcAbiCodec.sol](https://github.com/cotitech-io/coti-pod-sdk/blob/main/contracts/mpccodec/MpcAbiCodec.sol) and the **custom mode** section of [Writing privacy contracts](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/05-writing-privacy-contracts-on-ethereum.md). |
| **Client crypto** | [coti-pod-crypto.ts](https://github.com/cotitech-io/coti-pod-sdk/blob/main/src/coti-pod-crypto.ts) via `CotiPodCrypto` ([TypeScript integration](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/06-typescript-integration-ux-development.md)). |
+## Type model at a glance
+
+The PoD SDK ships **`MpcCore.sol`** under `@coti/pod-sdk/contracts/utils/mpc/`. In the current revision:
+
+- **`gtUint8` … `gtUint256` and `gtBool`** are **user‑defined value types** (`type gtUint256 is uint256`). Pass and assign them like `uint256` — **no `memory` / `calldata` on `gt*` parameters or locals**.
+- **`ctUint8` … `ctUint128`** are also user‑defined value types (single `uint256` word).
+- **`ctUint256`** is a **struct** `{ ctUint128 ciphertextHigh; ctUint128 ciphertextLow; }` — decoded locals and callback variables must use a `memory` location, and off‑chain reads return the two limbs as a tuple.
+- **`itUint*`** (user encrypted inputs, `ciphertext + signature`), **`utUint*`** (dual‑ciphertext), **`gtString`** and **`ctString`** remain structs — keep their `calldata` / `memory` locations.
+
+If you previously imported from **`@coti-io/coti-contracts`** in PoD code, switch the import to **`@coti/pod-sdk/contracts/utils/mpc/MpcCore.sol`**. Off‑chain decryption uses **`@coti-io/coti-sdk-typescript@^1.0.7`**, which exposes `decryptUint256({ ciphertextHigh, ciphertextLow }, accountAesKey)` for the 256‑bit lane.
+
## Implementation checklist (condensed)
Derived from the SDK’s [Writing privacy contracts](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/05-writing-privacy-contracts-on-ethereum.md) and [Async execution](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/05a-async-execution.md):
diff --git a/privacy-on-demand/glossary.md b/privacy-on-demand/glossary.md
index cbf6577..66a5dae 100644
--- a/privacy-on-demand/glossary.md
+++ b/privacy-on-demand/glossary.md
@@ -5,14 +5,28 @@ Short definitions for **Privacy on Demand** readers. Precise Solidity definition
| Term | Meaning |
| --- | --- |
| **Privacy on Demand (PoD)** | Pattern and tooling for **private computation on COTI** while **orchestrating** from **another EVM chain** via an **Inbox**. |
-| **Inbox** | **On-chain router** on your EVM chain that **forwards** private jobs toward COTI and **delivers callbacks** with results to your contract. |
-| **MPC executor** | **COTI-side contract** configured as the execution target for **library-style** PoD flows; referenced from your dApp’s routing configuration. |
-| **PodUser** | Solidity **configuration mixin** for **Inbox address**, **COTI chain id**, and **executor address**; changes should be **governed** (for example `onlyOwner`). |
-| **PodLib** | Solidity **helper library** for **common private operations** (fixed-width arithmetic and comparisons in supported paths). |
-| **`it*` (input types)** | **Encrypted user input** with required **signature material**, prepared client-side and sent to your contract. |
-| **`gt*` (garbled / compute types)** | **Internal private representation** during computation on **COTI**—not something your EVM contract should expose as a public API. |
-| **`ct*` (ciphertext types)** | **Encrypted outputs** suitable to **store on your chain**; users **decrypt locally** with **account AES** keys where applicable. |
-| **PoA fees** | In this book, **Privacy on Demand (PoD) fees** for **two-way Inbox** traffic: native token on your chain that funds **COTI-side** and **callback** execution budgets. See [How do PoA fees work?](how-poa-fees-work.md). |
-| **Two-way message** | Inbox flow: **outbound** request to COTI plus **inbound callback** to your contract; typically needs **fee** planning for both legs. |
-| **Request ID** | Correlator tying a **submission** to a **callback**; essential for **async** UX and troubleshooting. |
-| **Account AES key** | User-side secret material used to **decrypt** many `ct*` outputs after onboarding; must be **handled like credentials**. |
+| **Inbox** | On-chain **message router** on each chain that forwards private jobs toward COTI and delivers callbacks. Implementation in [`coti-pod-inbox-contracts`](https://github.com/coti-io/coti-pod-inbox-contracts). |
+| **MPC executor** | COTI-side contract configured as the execution target for **library-style** PoD flows; referenced from your dApp’s routing configuration. |
+| **PodUser** | Solidity **configuration mixin** for Inbox address, COTI chain id, and executor address; changes should be **governed** (for example `onlyOwner`). |
+| **PodUserSepolia / PodUserFuji** | Network presets that auto-wire inbox and COTI routing in the constructor. |
+| **PodLib** | Solidity **helper library** for built-in private operations at 64/128/256-bit widths. |
+| **`it*` (input types)** | Encrypted user input with signature material, prepared client-side and sent to your contract. |
+| **`gt*` (garbled / compute types)** | Internal private representation during computation on **COTI**—not something your EVM contract should expose as a public API. |
+| **`ct*` (ciphertext types)** | Encrypted outputs suitable to store on your chain; users decrypt locally with the account AES key. |
+| **PoA / PoD fees** | Native token on your chain funding COTI-side and callback execution budgets for **two-way Inbox** traffic. See [How do PoA fees work?](how-poa-fees-work.md). |
+| **Two-way message** | Outbound request to COTI plus inbound callback to your contract; typically needs fee planning for both legs. |
+| **Request ID** | 32-byte correlator tying submission to callback; indexed in compact `MessageSent` events. Essential for async UX and troubleshooting. |
+| **System error** | Pre-execution Inbox failure (encode / `validateCiphertext`). Delivered on the same `errorSelector(bytes)` as app `raise`. Attributed to `SYSTEM_SENDER`. Detect via `inboxErrorType() == SystemError`. **Not** eligible for `retryFailedRequest`. |
+| **`SYSTEM_SENDER`** | Placeholder `originalSender` / `inboxMsgSender()` for system-error return legs. Not a real contract; do not require it to equal your COTI peer. |
+| **`inboxErrorType()`** | Inbox view returning `NotErrorContext`, `SystemError`, or `Exception` for the active execution — preferred way for error handlers to branch. |
+| **Execution failure (code `1`)** | Target ran and reverted without `raise`. Stored on COTI with capped returndata; **permissionless** `retryFailedRequest` while code remains `1`. |
+| **`getOutboxError`** | View that returns `(code, data)` — for execution failures, the **raw capped returndata** (≤256 bytes). Decode in the client. |
+| **`executed` / `IncomingResponseReceived`** | Mean the **return or error leg was ingested** by the Inbox—not that the application callback committed. Confirm UX success via app status / events. |
+| **One-way message** | Outbound-only Inbox send. **Cannot** register a non-zero `errorSelector` (use two-way if you need error callbacks). |
+| **`retryFailedRequest`** | Permissionless COTI Inbox call that re-executes a request still marked execution-failed (code `1`). Encode failure on retry **reverts** and preserves code `1`. |
+| **Gas-price bounds** | Operator-configured floor / ceiling / min priority used when converting fee wei into gas-unit budgets (bounded reference price—not raw tip manipulation). |
+| **Account AES key** | 32-hex-character user secret for decrypting `ct*` outputs after onboarding; must be handled like credentials. See [Account Onboard](../build-on-coti/guides/account-onboard.md). |
+| **PodRequest** | TypeScript helper (`@coti/pod-sdk`) that polls inbox state across chains for async UX. |
+| **PodSdkConfig** | JSON config (chains, inbox addresses, RPCs, encryption network) shared by `PodContract` and `PodRequest`. |
+| **`@coti-io/coti-contracts`** | npm package with PoD Solidity libraries, interfaces, and examples. |
+| **`@coti-io/coti-pod-inbox-contracts`** | npm package with Inbox implementation, fee manager, and miner contracts. |
diff --git a/privacy-on-demand/how-a-private-request-travels-end-to-end.md b/privacy-on-demand/how-a-private-request-travels-end-to-end.md
index 5008a90..ea51971 100644
--- a/privacy-on-demand/how-a-private-request-travels-end-to-end.md
+++ b/privacy-on-demand/how-a-private-request-travels-end-to-end.md
@@ -24,6 +24,28 @@ This page describes **one full cycle** of Privacy on Demand **without assuming S
6. **The MPC Executor returns through Inbox (COTI) and Inbox (EVM)**, delivering an **ABI-encoded payload** of **`ct*` ciphertext**: encrypted outputs suitable to store on your chain.
7. **Your contract records the result** keyed by a **request ID**. The **user reads ciphertext from chain or API**, then **decrypts locally** with their **account AES key** (after proper onboarding).
+### Encode / `validateCiphertext` failure (system error)
+
+If Inbox encoding fails **before** the MPC Executor runs (most commonly a bad `it*` signature), COTI never calls your target contract. Instead the Inbox:
+
+1. Records error code `2` (`ERROR_CODE_ENCODE_FAILED`) and emits `ErrorReceived`.
+2. Automatically sends a return leg to your source `errorSelector` with Inbox `{ErrorData}` (`abi.encode(uint64 code, bytes message)`, code `2`) attributed to `SYSTEM_SENDER` — see [Async private operations](async-private-operations.md).
+3. Marks the incoming request executed so it is **not** eligible for `retryFailedRequest`.
+
+Your source error callback must clear pending state and surface failure to the UI. Users submit a **new** request after pending clears.
+
+### Execution failure (retryable) and reading errors
+
+If the COTI target **runs** and **reverts** without `raise`:
+
+1. The Inbox stores error code `1` with a **capped** returndata blob (see [Async private operations](async-private-operations.md)).
+2. There is **no** automatic source callback for this path—operators or anyone may call **`retryFailedRequest`** on COTI while the code remains `1`.
+3. Use **`getOutboxError(requestId)`** for the capped returndata bytes (same as `errors[requestId].errorMessage`); decode in the client.
+
+### Completion signals
+
+When the diagram says “Callback with result bytes,” that is the **application** success path. Inbox-level `executed` / compact response events only mean the **return leg was received**—confirm success via your contract’s status / events (see [Async private operations](async-private-operations.md)).
+
## Sequence diagram (conceptual)
**Colors:** the **cool charcoal** panel is **contracts on your host chain (Ethereum)**—**Your dApp contract** and **Inbox (EVM)**. The **warm charcoal** panel is **contracts on COTI**—**Inbox (COTI)** and **MPC Executor**. **User** and **Client app** are off-chain (default styling). Arrows **between** the two dark panels are **cross-chain / cross-domain** handoffs. The diagram uses a **dark theme** so labels stay light-on-dark and readable.
@@ -79,7 +101,7 @@ sequenceDiagram
## Fees and gas
-Private jobs that cross from your chain to COTI and back incur **network and execution costs**. Integrations typically attach **native token value** on the request and split it between **remote execution** and the **callback** leg. Operators configure **fee parameters** and **oracle** behavior on supporting contracts (see the SDK’s [Fees, gas, and oracle](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/contracts/04-fees-gas-and-oracle.md) page).
+Private jobs that cross from your chain to COTI and back incur **network and execution costs**. Integrations typically attach **native token value** on the request and split it between **remote execution** and the **callback** leg. Operators configure **fee parameters**, **gas-price bounds**, and **oracle** behavior on supporting contracts (see the SDK’s [Fees, gas, and oracle](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/contracts/04-fees-gas-and-oracle.md) page and [How do PoA fees work?](how-poa-fees-work.md)).
## Next steps
diff --git a/privacy-on-demand/how-poa-fees-work.md b/privacy-on-demand/how-poa-fees-work.md
index 9a0f9b9..f0dc0f4 100644
--- a/privacy-on-demand/how-poa-fees-work.md
+++ b/privacy-on-demand/how-poa-fees-work.md
@@ -21,7 +21,7 @@ Solidity shape (conceptually):
| Stage | Amounts |
| --- | --- |
-| **User-provided gas** |
- 0.007 ETH (
msg.value) - 0.0035 ETH (
callbackFeeLocalWei, Sepolia) - 0.0035 ETH (COTI-side)
- 5 gwei (reference)
|
+| **User-provided gas** |
- 0.007 ETH (
msg.value) - 0.0035 ETH (
callbackFeeLocalWei, Sepolia) - 0.0035 ETH (COTI-side)
- Bounded reference gas price (see below)—not the caller’s raw tip
|
| **Price at oracle** (same quote, e.g. USD) |
- COTI: 0.017 per 1 COTI.
- ETH: 2,170 per 1 ETH.
|
| **Converted to the chain gas (COTI — ETH)** |
- COTI: 446.76 COTI
- ETH: 0.0035 ETH
|
| **Convert to gas units (COTI — ETH)** |
- COTI: 16,029,096
- ETH: 615,701
|
@@ -40,6 +40,18 @@ Solidity shape (conceptually):
Read the **first table top to bottom:** user ETH is split by leg, oracles supply **COTI** and **ETH** prices, the COTI leg is expressed as **quote → COTI tokens**, then policy turns each leg into **gas-unit budgets**. The **second table** spends **COTI** first, then **Sepolia** after the result exists. Underspend remains are illustrative; production behavior depends on **InboxMiner** / **InboxFeeManager** and operator policy (see the SDK [Fees, gas, and oracle](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/contracts/04-fees-gas-and-oracle.md) doc).
+### Reference gas price and bounds
+
+When the Inbox converts fee wei into **gas-unit** budgets, it uses a **bounded reference gas price**—not an unbounded reading of the caller’s tip:
+
+- Operators configure **`setGasPriceBounds`** (min priority fee, min gas price, optional max).
+- On EIP-1559 chains the reference is typically derived from **`basefee` + min priority** (and clamped by the configured floor / ceiling).
+- Clients should still pass a realistic `gasPrice` into **off-chain** fee estimates so the UI matches what users will pay; on-chain conversion remains **bounded** so tip inflation cannot shrink budgets arbitrarily.
+
+### Oracle cache refresh
+
+Inbox fee math reads **cached** USD prices for the configured local / remote legs. Operators (or keepers) call **`refreshCache()`**, which refreshes **both** inbox legs together. Configure legs with **`setInboxTokens`**. Uniswap-based oracles derive those leg addresses from the configured pairs at construction; spot Uniswap prices remain manipulable—prefer trusted feeds or TWAP for production.
+
## Why this matters for `add(a, b) → ctUint64 c`
- **Private execution** and **callback execution** happen in **different environments**, as **separate steps in time**: COTI runs **first**; the Sepolia relay and **callback** run **after** the result exists. Each leg needs its **own** paid work (compute + relay).
diff --git a/privacy-on-demand/tutorial-custom-logic.md b/privacy-on-demand/tutorial-custom-logic.md
index 15fd783..21ea805 100644
--- a/privacy-on-demand/tutorial-custom-logic.md
+++ b/privacy-on-demand/tutorial-custom-logic.md
@@ -41,7 +41,7 @@ The COTI contract **inherits `InboxUser`**, so only the Inbox can enter `receive
pragma solidity ^0.8.19;
import "../InboxUserCotiTestnet.sol";
-import "@coti-io/coti-contracts/contracts/utils/mpc/MpcCore.sol";
+import "@coti/pod-sdk/contracts/utils/mpc/MpcCore.sol";
contract DirectMessageCotiSide is InboxUserCotiTestnet {
function receiveMessage(gtString calldata message, address sender, address recipient) external onlyInbox {
diff --git a/privacy-on-demand/tutorial-private-adder-sepolia.md b/privacy-on-demand/tutorial-private-adder-sepolia.md
index 04bb85b..6de6e67 100644
--- a/privacy-on-demand/tutorial-private-adder-sepolia.md
+++ b/privacy-on-demand/tutorial-private-adder-sepolia.md
@@ -93,7 +93,7 @@ contract PrivateAdder is PodLib, PodUserSepolia {
requestId = inbox.inboxRequestId();
}
- ctUint256 sum = abi.decode(data, (ctUint256));
+ ctUint256 memory sum = abi.decode(data, (ctUint256));
sumByRequest[requestId] = sum;
statusByRequest[requestId] = RequestStatus.Completed;
emit AddCompleted(requestId);
@@ -105,6 +105,7 @@ contract PrivateAdder is PodLib, PodUserSepolia {
- **`onDefaultMpcError`** is implemented on `PodLibBase` and forwards failures to **`ErrorRemoteCall`** on `PodUser`. Your UI can listen for that event to mark a request failed.
- **`addCallback`** must stay **`onlyInbox`** so random accounts cannot forge results.
+- **`ctUint256`** is a Solidity **struct** `{ ctUint128 ciphertextHigh; ctUint128 ciphertextLow; }`, so the decoded local must use a `memory` location and the storage mapping holds the two‑limb tuple. The narrower garbled / ciphertext types (`gtUint8…gtUint256`, `gtBool`, and `ctUint8…ctUint128`) are **user‑defined value types** — pass and assign them like `uint256` (no `memory` / `calldata`). Encrypted-input wrappers such as **`itUint256`** stay structs and keep their `calldata` / `memory` location as before.
## Step 3: Compile and deploy on Sepolia
@@ -145,8 +146,10 @@ import {
import { ethers } from "ethers";
// Minimal ABI fragment — prefer the full artifact from your build (Hardhat / Foundry).
+// itUint256 = { ctUint256 ciphertext; bytes signature }, and ctUint256 = { uint256 ciphertextHigh; uint256 ciphertextLow }
+// so each `itUint256` parameter encodes as ((uint256,uint256),bytes).
const privateAdderAbi = [
- "function add((uint256 ciphertext,bytes signature),(uint256 ciphertext,bytes signature),uint256) payable returns (bytes32)",
+ "function add(((uint256,uint256),bytes),((uint256,uint256),bytes),uint256) payable returns (bytes32)",
] as const;
const pod = new PodContract(
@@ -206,35 +209,37 @@ Private addition is **asynchronous**: the sum appears only after the Inbox invok
## Step 7: Read the encrypted sum and decrypt locally
-After status is **Completed**, read **`sumByRequest(requestId)`**. The value is **`ctUint256`** (ciphertext), not plaintext.
+After status is **Completed**, read **`sumByRequest(requestId)`**. The value is **`ctUint256`** (ciphertext), not plaintext. Because **`ctUint256`** is a Solidity **struct** with two `ctUint128` limbs, the contract read returns a tuple `{ ciphertextHigh, ciphertextLow }` (each is a single `uint256`).
```typescript
import { CotiPodCrypto, DataType } from "@coti/pod-sdk";
// accountAesKey: hex string from your app’s COTI onboarding flow (never log it)
-const ct = await privateAdder.sumByRequest(requestId); // bytes32 from extractRequestIds or return value
-const ctHex =
- typeof ct === "bigint"
- ? "0x" + ct.toString(16)
- : String(ct);
+const raw = await privateAdder.sumByRequest(requestId);
+// ethers / viem return the struct as a tuple — normalize to { ciphertextHigh, ciphertextLow }
+const ct = {
+ ciphertextHigh: BigInt(raw.ciphertextHigh ?? raw[0]),
+ ciphertextLow: BigInt(raw.ciphertextLow ?? raw[1]),
+};
const decryptedString = CotiPodCrypto.decrypt(
- ctHex,
+ ct,
accountAesKey,
- DataType.Uint64
+ DataType.Uint256
);
console.log("sum (plaintext string):", decryptedString);
// Expect "30" for plainA=10 and plainB=20
```
-`CotiPodCrypto.decrypt` delegates to `@coti-io/coti-sdk-typescript` and expects a **scalar ciphertext** as a **hex string** for `Uint64`, plus the user’s **AES key** (see SDK source [coti-pod-crypto.ts](https://github.com/cotitech-io/coti-pod-sdk/blob/main/src/coti-pod-crypto.ts)).
+`CotiPodCrypto.decrypt` delegates to **`@coti-io/coti-sdk-typescript`** (`^1.0.7`), which now exposes `decryptUint256({ ciphertextHigh, ciphertextLow }, accountAesKey)` for the 256‑bit lane. Narrower lanes (`Uint64`, `Uint128`, …) still take a single `uint256` ciphertext as a `bigint` or `0x`‑prefixed hex string (see SDK source [coti-pod-crypto.ts](https://github.com/cotitech-io/coti-pod-sdk/blob/main/src/coti-pod-crypto.ts)).
## Step 8: Sanity checks and next steps
-- **Callback decode** must stay **`(ctUint256)`** — changing the executor op or COTI-side behavior without updating the decode tuple will corrupt storage reads.
-- **Type lane** — This contract uses **`add256`** with **`itUint256`** / **`ctUint256`** on chain. **`CotiPodCrypto.decrypt`** still takes a **`DataType`** for the scalar decode; keep **`DataType.Uint64`** (or **`Uint256`**, etc.) aligned with how your app and onboarding produce the ciphertext for this flow, per your installed SDK.
+- **Callback decode** must stay **`(ctUint256)`** — and the local must use `memory` because `ctUint256` is a struct. Changing the executor op or COTI-side behavior without updating the decode tuple will corrupt storage reads.
+- **Type lane** — This contract uses **`add256`** with **`itUint256`** / **`ctUint256`** on chain, so pass **`DataType.Uint256`** to `CotiPodCrypto.decrypt` and feed it the **`{ ciphertextHigh, ciphertextLow }`** tuple read from the contract. Narrower lanes (`Uint64`, `Uint128`) still take a single ciphertext word.
+- **Type model** — In the current `MpcCore.sol`, `gtUint*`, `gtBool`, and `ctUint8…ctUint128` are **user‑defined value types** (`type X is uint256`) — drop `memory` / `calldata` on them. `ctUint256` is a struct (two `ctUint128` limbs); `itUint*` / `utUint*` are also still structs, so keep `calldata` / `memory` on those.
- **Production**: add tests for non-Inbox callers on `addCallback`, under-funded `msg.value`, and decrypt failures; follow the [first production checklist](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/04-getting-started.md) in Getting started.
## Reference links
diff --git a/privacy-on-demand/typescript-pod-sdk.md b/privacy-on-demand/typescript-pod-sdk.md
index 5a2f4af..9851588 100644
--- a/privacy-on-demand/typescript-pod-sdk.md
+++ b/privacy-on-demand/typescript-pod-sdk.md
@@ -12,7 +12,7 @@ Use these helpers from a wallet script, backend service, or dApp frontend once y
- You can also pass a full encryption service URL.
- `DataType` distinguishes plaintext types (`Uint64`, `String`, etc.) from **`it*`** types that become ciphertext tuples on chain.
-`CotiPodCrypto.decrypt` uses the user's **account AES key** and `@coti-io/coti-sdk-typescript` under the hood.
+`CotiPodCrypto.decrypt` uses the user's **account AES key** and **`@coti-io/coti-sdk-typescript`** (`^1.0.7`) under the hood.
```typescript
import { CotiPodCrypto, DataType } from "@coti/pod-sdk";
@@ -20,10 +20,31 @@ import { CotiPodCrypto, DataType } from "@coti/pod-sdk";
// Encrypt plaintext for Solidity itUint256 parameters
const enc = await CotiPodCrypto.encrypt("42", "testnet", DataType.itUint256);
-// Decrypt scalar ciphertext read from contract storage
-const plain = CotiPodCrypto.decrypt("0x...", accountAesKeyFromOnboarding, DataType.Uint64);
+// Decrypt a narrow scalar ciphertext (one uint256 word) read from contract storage
+const plain64 = CotiPodCrypto.decrypt("0x...", accountAesKeyFromOnboarding, DataType.Uint64);
+
+// Decrypt a ctUint256 value (a struct with two ctUint128 limbs)
+const plain256 = CotiPodCrypto.decrypt(
+ { ciphertextHigh, ciphertextLow },
+ accountAesKeyFromOnboarding,
+ DataType.Uint256
+);
```
+### Ciphertext shape in the current `MpcCore.sol`
+
+After the gt‑type upgrade, the on‑chain types you read back have these shapes:
+
+| Type | Solidity | Off‑chain shape |
+| -------------------------- | --------------------------------------------------------- | ---------------------------------------------- |
+| `gtUint8` … `gtUint256`, `gtBool` | User‑defined value type (`type … is uint256`), no `memory`/`calldata` | n/a — never crosses the chain boundary |
+| `ctUint8` … `ctUint128` | User‑defined value type | single `uint256` word |
+| `ctUint256` | Struct `{ ctUint128 ciphertextHigh; ctUint128 ciphertextLow; }` | tuple `{ ciphertextHigh, ciphertextLow }` (each `bigint`) |
+| `itUint*` / `utUint*` | Struct (ciphertext + signature, unchanged) | tuple / object |
+| `gtString` / `ctString` | Struct (array of `gtUint64` / `ctUint64`, unchanged) | array of words |
+
+When you read a `ctUint256` value via ethers or viem, the storage getter returns the two limbs as a tuple — normalize it to `{ ciphertextHigh, ciphertextLow }` before passing it to `CotiPodCrypto.decrypt` (or to `decryptUint256` from `@coti-io/coti-sdk-typescript`). The narrower `ct*` lanes can still be passed as a `bigint` or `0x`‑prefixed hex string.
+
## Gas estimation and method calls (`PodContract`)
`PodContract` wraps `ethers.Contract` with PoD-aware helpers:
diff --git a/security/README.md b/security/README.md
index 1ce7218..df0228d 100644
--- a/security/README.md
+++ b/security/README.md
@@ -1,10 +1,8 @@
-# Audit Roports
+# Audit Reports
-Security is a top priority for the COTI network. Below you’ll find a list of independent security audits conducted on different components of the COTI ecosystem. Each report outlines the scope, focus areas, and includes a link to the full audit documentation.
+Security is a top priority for the COTI network. Below you’ll find a list of independent security audits conducted on different components of the COTI ecosystem.
-These audits play a critical role in ensuring that the COTI protocol remains secure, reliable, and ready for real-world adoption.
-
-You can find all published audit reports here:
+> **Canonical page:** Prefer [Audit Reports](audit-reports.md) for the full table (including Privacy Portal) and the **PoD Inbox / Privacy Portal internal hardening** notes.
diff --git a/security/audit-reports.md b/security/audit-reports.md
index 0c41d48..b407aa1 100644
--- a/security/audit-reports.md
+++ b/security/audit-reports.md
@@ -16,3 +16,19 @@ You can find all published audit reports here:
| 2026 - 3 | Sayfer | Private ERC20 | [2026\_ERC20\_Sayfer](Sayfer-2026-03-Smart-Contract-Audit-Report-for-Coti.pdf) |
| 2026 - 3 | Sayfer | Metamask Snap | [2026\_Metamask](https://github.com/coti-io/coti-snap/blob/main/docs/Sayfer-2026-03-Metamask-Snap-Audit-for-Coti.pdf) |
| 2026 - 5 | Sayfer | Privacy Portal | [2026\_PrivacyPortal](Sayfer-2026-05-Smart-Contract-Audit-Report-for-Coti.pdf) |
+
+## PoD Inbox and Privacy Portal hardening (internal review)
+
+In addition to the external reports above, the PoD **Inbox** (`coti-pod-inbox-contracts`) and **PoD Privacy Portal** stack received an internal security review with follow-up fixes. Integrators should treat the following as current behavior (also reflected in the PoD / Avalanche books):
+
+| Area | What changed for integrators |
+| --- | --- |
+| **Capped returndata** | Execution failures store at most 256 bytes of returndata. **`getOutboxError`** returns `(code, data)` with those raw bytes for client-side decoding. |
+| **`retryFailedRequest`** | Permissionless while error code is `1`. Encode failure on retry **reverts** and preserves code `1` (does not flip to encode-failed). |
+| **One-way `errorSelector`** | `sendOneWayMessage` rejects non-zero `errorSelector`—use two-way for error callbacks. |
+| **`executed` / response events** | Mean the return leg was **ingested**, not that the app callback committed. |
+| **Fee gas price** | On-chain budgets use **bounded reference** gas price (`setGasPriceBounds`), not unbounded tip manipulation. |
+| **Oracle cache** | `refreshCache()` refreshes **both** inbox legs; configure with `setInboxTokens` (Uniswap oracles set legs from pairs at construction). |
+| **Portal deposits** | Prefer `refundFailedDeposit` only after **SystemFailed**. Stuck `Pending` may be retryable execution failure or miner lag—diagnose before refunding. Keep deposits off until mother registration confirms. |
+
+See [Async private operations](../privacy-on-demand/async-private-operations.md), [How do PoA fees work?](../privacy-on-demand/how-poa-fees-work.md), and [Privacy Portal troubleshooting](../coti-privacy-portal/developer-guide/troubleshooting.md).