|
| 1 | +# Privacy Zone Execution Environment (Draft) |
| 2 | + |
| 3 | +This document specifies how the EVM execution environment of a privacy zone differs from a standard Tempo zone. These changes are enforced at the execution level (inside the zone's TIP-20 precompile, gas accounting, and EVM configuration), not at the RPC layer. They apply to all code paths — user transactions, sequencer system calls, `eth_call` simulations, and prover re-execution. |
| 4 | + |
| 5 | +For RPC-level access controls (authentication, method filtering, event scoping), see the [Zone RPC Specification](./rpc). |
| 6 | + |
| 7 | +A reference Solidity specification of all TIP-20 modifications is available at [`PrivateZoneToken.sol`](/specs/src/zone/PrivateZoneToken.sol). |
| 8 | + |
| 9 | +## TIP-20 modifications |
| 10 | + |
| 11 | +Privacy zones modify the zone token's TIP-20 precompile in four areas: balance privacy, allowance privacy, fixed gas accounting, and system mint/burn permissions. |
| 12 | + |
| 13 | +### Balance privacy: `balanceOf` access control |
| 14 | + |
| 15 | +On a standard zone (and on Tempo), `balanceOf(address account)` is a public view function — any caller can read any account's balance. On a privacy zone, the function enforces caller restrictions: |
| 16 | + |
| 17 | +- If `msg.sender == account`, the call succeeds and returns the balance. |
| 18 | +- If `msg.sender` is the sequencer (as read from `ZoneConfig.sequencer()`), the call succeeds. |
| 19 | +- Otherwise, the call reverts with `Unauthorized()`. |
| 20 | + |
| 21 | +This means: |
| 22 | + |
| 23 | +- **User transactions**: A contract calling `balanceOf(someOtherAddress)` will revert. Only self-queries succeed. |
| 24 | +- **`eth_call` simulations**: The RPC server sets `from` to the authenticated account, so `balanceOf` only works for the caller's own address. See [RPC spec](./rpc). |
| 25 | +- **Sequencer and system calls**: The sequencer retains full read access, which is required for block production, deposit processing, and fee accounting. |
| 26 | + |
| 27 | +**Rationale**: On a public chain, anyone can read any balance. On a privacy zone, balances are private. Enforcing this at the EVM level (not just at the RPC layer) ensures that even on-chain composition cannot leak balances — a contract deployed on the zone cannot be used to read and re-emit another account's balance. |
| 28 | + |
| 29 | +### Allowance privacy: `allowance` access control |
| 30 | + |
| 31 | +The `allowance(address owner, address spender)` function is similarly restricted: |
| 32 | + |
| 33 | +- If `msg.sender == owner` or `msg.sender == spender`, the call succeeds and returns the allowance. |
| 34 | +- If `msg.sender` is the sequencer, the call succeeds. |
| 35 | +- Otherwise, the call reverts with `Unauthorized()`. |
| 36 | + |
| 37 | +**Rationale**: A non-zero allowance reveals that `owner` has interacted with `spender` — a relationship that should be private on a privacy zone. Restricting reads to the two parties involved preserves standard ERC-20 approval flows (both the owner and the spender can check the allowance) without leaking relationship information to third parties. |
| 38 | + |
| 39 | +**Unchanged views**: `totalSupply()`, `name()`, `symbol()`, `decimals()`, and other non-per-account view functions remain unrestricted. |
| 40 | + |
| 41 | +### Fixed gas: constant transfer cost |
| 42 | + |
| 43 | +All TIP-20 transfer operations on a privacy zone charge a fixed gas cost of **100,000 gas**, regardless of execution-dependent factors: |
| 44 | + |
| 45 | +| Function | Gas cost | |
| 46 | +|----------|----------| |
| 47 | +| `transfer(to, amount)` | 100,000 | |
| 48 | +| `transferFrom(from, to, amount)` | 100,000 | |
| 49 | +| `transferWithMemo(to, amount, memo)` | 100,000 | |
| 50 | +| `transferFromWithMemo(from, to, amount, memo)` | 100,000 | |
| 51 | +| `approve(spender, amount)` | 100,000 | |
| 52 | + |
| 53 | +On a standard EVM chain, gas cost varies depending on whether a transfer writes to a previously empty storage slot (zero → non-zero costs 20,000 gas more than non-zero → non-zero). This difference reveals whether the recipient has previously received tokens — a binary signal about account existence. |
| 54 | + |
| 55 | +By fixing the gas cost: |
| 56 | + |
| 57 | +- All transfer receipts have identical `gasUsed` for the TIP-20 portion, removing the side channel. |
| 58 | +- Observers (including the sender, who sees their own receipt) cannot distinguish transfers to new vs. existing accounts. |
| 59 | +- The fixed cost of 100,000 gas matches the zone's `FIXED_DEPOSIT_GAS` constant, providing a consistent gas unit across deposits and transfers. |
| 60 | + |
| 61 | +**Implementation**: The zone's TIP-20 precompile always charges exactly 100,000 gas for any transfer-family call, regardless of the actual storage operations required. If the transaction provides less than 100,000 gas to the precompile call, it reverts with out-of-gas. Excess gas beyond 100,000 is returned to the caller as usual. |
| 62 | + |
| 63 | +**Unchanged operations**: System functions (`systemTransferFrom`, `transferFeePreTx`, `transferFeePostTx`) retain their standard gas costs. These are restricted to precompile-only callers where the gas side channel is not exploitable. |
| 64 | + |
| 65 | +### System mint and burn permissions |
| 66 | + |
| 67 | +On Tempo, `mint()` and `burn()` on a TIP-20 require the caller to hold `ISSUER_ROLE`. On a zone, the token is a bridged representation — tokens are minted when deposits arrive from Tempo and burned when withdrawals are requested. `ISSUER_ROLE` is not used on zones. The sole mint/burn authorities are the zone system contracts: |
| 68 | + |
| 69 | +| Operation | Standard TIP-20 (Tempo) | Zone access | |
| 70 | +|-----------|------------------------|-------------| |
| 71 | +| `mint(to, amount)` | `ISSUER_ROLE` only | ZoneInbox (`0x1c...0001`) only | |
| 72 | +| `burn(from, amount)` | `ISSUER_ROLE` only | ZoneOutbox (`0x1c...0002`) only | |
| 73 | + |
| 74 | +Authorization is **operation-specific**: ZoneInbox access applies to `mint` only, and ZoneOutbox access applies to `burn` only. Implementations MUST NOT use a shared "inbox-or-outbox" check for both operations. |
| 75 | + |
| 76 | +**ZoneInbox mints** during deposit processing in `advanceTempo()`: |
| 77 | + |
| 78 | +- Regular deposit: `mint(deposit.to, deposit.amount)` — credits the recipient on the zone. |
| 79 | +- Encrypted deposit (decryption succeeded): `mint(decrypted.to, deposit.amount)` — credits the decrypted recipient on the zone. |
| 80 | +- Encrypted deposit (decryption failed): `mint(deposit.sender, deposit.amount)` — credits the depositor's address on the zone (same address as their L1 account). The L1 funds remain escrowed in the portal. |
| 81 | + |
| 82 | +**ZoneOutbox burns** during withdrawal requests in `requestWithdrawal()`: |
| 83 | + |
| 84 | +- The user approves the ZoneOutbox to spend `amount + fee`. |
| 85 | +- ZoneOutbox calls `transferFrom(user, self, amount + fee)`, then `burn(self, amount + fee)`. |
| 86 | +- The burned tokens are released on Tempo when the sequencer processes the withdrawal. |
| 87 | + |
| 88 | +**Gas costs**: `mint` and `burn` retain standard variable gas costs (not the fixed 100,000). These functions are only called by system contracts during sequencer operations, so there is no user-exploitable gas side channel. |
| 89 | + |
| 90 | +## EVM restrictions |
| 91 | + |
| 92 | +### Contract creation disabled |
| 93 | + |
| 94 | +Privacy zones disable the `CREATE` and `CREATE2` opcodes. The zone runs a fixed set of predeploys (system contracts and the zone token); user-deployed contracts are not supported. Any transaction or call that attempts contract creation reverts. |
| 95 | + |
| 96 | +**Rationale**: Arbitrary contract deployment would allow users to deploy contracts that circumvent execution-level privacy protections — for example, a contract that calls `balanceOf` on behalf of a third party and emits the result as an event. |
| 97 | + |
| 98 | +## Interaction with RPC |
| 99 | + |
| 100 | +These execution-level changes are the first line of defense. The [RPC specification](./rpc) adds a second layer of access control (authentication, method restrictions, event filtering). Both layers are required: |
| 101 | + |
| 102 | +- **Execution alone is insufficient**: Without RPC restrictions, a caller could use `eth_getStorageAt` to read TIP-20 balance mapping slots directly, bypassing the `balanceOf` access control entirely. |
| 103 | +- **RPC alone is insufficient**: Without execution-level changes, a caller could deploy or call a contract via `eth_call` that reads another account's balance and returns it, bypassing RPC-level filtering. |
| 104 | + |
| 105 | +The two layers are complementary: execution-level changes protect against in-EVM information leaks, and RPC-level changes protect against raw state inspection. |
0 commit comments