> **Can't find what you're looking for?** Use `search_docs` on the docs MCP server at `https://docs.prividium.com/api/mcp` to find what you need.
>
> **Have feedback?** Use `submit_feedback` on the same MCP server.

Learn about the inherent security limitations of Prividium's permissioning layer and how to avoid deploying smart
contracts that expose side-channel or authorization bypass vulnerabilities.

## Overview

Prividium's architecture is centered around the Proxy RPC, the Prividium API, and role/function-level permissions.
Policy is enforced **before** a request reaches the chain — it does not imply confidential execution inside contract
code.

For every incoming RPC request, Prividium checks the top-level `to` address and the 4-byte function selector extracted
from the calldata. If the caller is authorized for that contract + method combination, the request is forwarded to the
sequencer.

What happens to the internal calls a transaction makes depends on whether the Prividium API runs the **policy
listener** (`POLICY_PORT` set):

* **Policy listener not configured.** Only the top-level call is checked. The internal `CALL`, `DELEGATECALL`, and
  `STATICCALL` opcodes a permitted entry point executes are not permission-checked. This opens the two authorization
  bypasses in sections 1 and 3.
* **Policy listener configured.** The sequencer submits every internal call, including a constructor's child calls, to
  the same per-function permissions, evaluated for the original signer. The request is rejected if any call is
  forbidden. This closes both bypasses. See
  [Enforcement across internal calls](/configuring/contracts/permissions#8-enforcement-across-internal-calls) for the
  per-call check and the **Umbrella** exemption.

In either mode the sequencer executes real EVM code and returns real results, and two observable outputs can leak
non-public contract state:

* Gas consumption is determined by the full EVM execution path and returned to the caller
* Execution timing is observable through response latency

These are inherent architectural constraints of proxy-layer permissioning. The sections below describe the categories
of risk that arise from these constraints, along with concrete examples and mitigations.

:::info[Architectural Constraints]
The limitations described in this document are not bugs — they are inherent to any system that enforces permissions at
the RPC layer rather than inside the EVM itself. Operators must account for them when reviewing contracts for
deployment. Developers must design contracts that do not inadvertently expose these attack surfaces.
:::

## 1. Multicall and Arbitrary Call Bypass

### The Risk

Without the policy listener, Prividium checks the `to` address and function selector of the **top-level RPC call
only**. If a whitelisted contract
contains multicall or arbitrary call functionality — such as `address.call(data)`, OpenZeppelin's `Multicall` mixin,
`Governor.execute()`, or a `fallback()` function that forwards via `delegatecall` — then any user authorized to call
that function can use it as a trampoline to reach **any other contract** on the network, completely bypassing
Prividium's per-function permissions.

```mermaid
sequenceDiagram
    participant User
    participant Prividium as Prividium Proxy
    participant A as Contract A (Multicall)
    participant B as Contract B (Restricted)

    User->>Prividium: eth_call(to=A, data=multicall([B.secret()]))
    Prividium->>Prividium: Authorized for A.multicall()? ✓
    Prividium->>A: Forward call
    A->>B: B.secret() — internal CALL
    Note over Prividium: Prividium never sees<br/>the call to B
    B-->>A: Returns restricted data
    A-->>User: Returns restricted data
```

### Bad Pattern

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

/// UNSAFE: Any user authorized to call executeCall() can reach
/// ANY contract on the network, bypassing Prividium permissions.
contract UnsafeRouter {
    function executeCall(
        address target,
        bytes calldata data
    ) external returns (bytes memory) {
        (bool success, bytes memory result) = target.call(data);
        require(success, "Call failed");
        return result;
    }
}
```

Other patterns to flag during contract review:

* **Generic `call` / `delegatecall`** — any function that accepts an arbitrary `address` + `bytes calldata` and forwards
  a low-level call (e.g., OpenZeppelin `Multicall`, governance `execute(address, uint256, bytes)`)
* **Module systems** — ERC-2535 Diamond proxies, ERC-7579 modules, or plugin architectures where external modules can be
  installed and called through a single entry point
* **Fallback dispatchers** — contracts with a `fallback()` that routes calls based on selector lookup tables or forwards
  via `delegatecall` to a registry-resolved implementation
* **User-controlled target addresses** — any function where the caller supplies the `to` address for an internal call,
  even if the selector is fixed
* **Factories / CREATE2 helpers** — contracts that deploy other contracts on behalf of the caller (the deployed
  contract's constructor can call anything, same as the deployment bypass in Section 3)
* **Upgradeable proxies** — proxies where the implementation can be changed behind an allowed selector; the selector
  stays the same but the underlying logic changes to call arbitrary contracts after an upgrade

### Good Pattern

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

/// SAFE: Forwarding is restricted to a single known target and method.
contract SafeRouter {
    address public immutable allowedTarget;
    bytes4 public immutable allowedSelector;

    constructor(address _target, bytes4 _selector) {
        allowedTarget = _target;
        allowedSelector = _selector;
    }

    function forwardCall(bytes calldata data) external returns (bytes memory) {
        require(bytes4(data[:4]) == allowedSelector, "Selector not allowed");
        (bool success, bytes memory result) = allowedTarget.call(data);
        require(success, "Call failed");
        return result;
    }
}
```

:::note[ERC-4337 Exception]
Even without the policy listener, UserOperations submitted through the ERC-4337 bundler **are** validated per-call.
Prividium's SSO smart account dispatcher parses the `execute(bytes32, bytes)` calldata from the UserOperation, extracts
each individual sub-call, and runs `checkContractAccess` against each one that carries calldata or targets a contract.
This is possible because the smart account's call encoding is known and can be decoded by the proxy. Raw transactions
do not have this property.

One exemption remains: a sub-call with empty calldata to a destination that has no code at validation time is treated
as a native transfer and skips the destination check. The operation executes later, in a bundle, so a contract deployed
between validation and execution runs its `receive()` or `fallback()` without that check.

The native-transfer branch of `eth_sendRawTransaction` has the same limitation: it checks code at `latest` before
submission, and a contract can be deployed before the transfer executes. These checks do not guarantee that a
destination remains an EOA until execution.
:::

### Mitigations

* Configure the policy listener, so every internal call is permission-checked and the bypass is closed entirely
* Audit all contracts for `call`, `delegatecall`, and `staticcall` usage before approving deployment
* Reject contracts with arbitrary call forwarding patterns (any function accepting `address` + `bytes calldata` that
  forwards a low-level call)
* If a contract uses multicall internally, verify that the targets and selectors are hardcoded or restricted to a known
  safe set
* If batch execution is required, restrict targets to a hardcoded allow-list within the contract itself
* Avoid inheriting OpenZeppelin `Multicall` unless all callable targets are explicitly safe

***

## 2. Gas Estimation Oracle

### The Risk

When a user calls `eth_estimateGas`, Prividium checks authorization for the top-level call and then delegates the
request to the sequencer. The sequencer **executes the full EVM code** and returns the gas estimate. If the contract's
execution path branches on secret state — for example, comparing user input to a hidden value — the gas estimate will
differ depending on which branch executes. An attacker can systematically probe different inputs and infer the secret
from the observed gas differences.

```mermaid
flowchart TD
    A["Attacker calls eth_estimateGas\nwith guess = X"] --> B{"Contract:\nX == secret?"}
    B -- "Yes" --> C["Expensive path\n(storage writes + transfer)\n~73,000 gas"]
    B -- "No" --> D["Cheap path\n(no state changes)\n~35,000 gas"]
    C --> E["Attacker observes\nhigh gas estimate"]
    D --> F["Attacker observes\nlow gas estimate"]
    E --> G["Secret value = X\nconfirmed"]
```

### Bad Pattern

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

/// UNSAFE: Gas consumption reveals whether the guess is correct.
/// An attacker can call eth_estimateGas with different inputs and
/// observe the gas difference to find the secret number.
contract UnsafeGuessing {
    uint256 private secretNumber;
    mapping(address => bool) public winners;

    constructor(uint256 _secret) {
        secretNumber = _secret;
    }

    function guess(uint256 _number) external {
        if (_number == secretNumber) {
            // This branch consumes significantly more gas:
            // storage write + ETH transfer
            winners[msg.sender] = true;
            payable(msg.sender).transfer(1 ether);
        }
        // The no-match branch does nothing — much cheaper
    }
}
```

### Good Pattern

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

/// SAFE: Commit-reveal ensures that the comparison phase has
/// constant gas cost regardless of whether the guess is correct.
/// The secret is never compared directly to user input.
contract SafeGuessing {
    bytes32 private secretHash;
    mapping(address => bytes32) public commitments;

    constructor(bytes32 _secretHash) {
        secretHash = _secretHash;
    }

    /// Phase 1: Commit a hash of your guess. Constant gas cost.
    function commit(bytes32 _commitHash) external {
        commitments[msg.sender] = _commitHash;
    }

    /// Phase 2: Reveal your guess. Gas cost is constant regardless
    /// of correctness — both branches perform the same storage writes.
    function reveal(uint256 _guess, bytes32 _salt) external {
        require(
            commitments[msg.sender] == keccak256(abi.encodePacked(_guess, _salt)),
            "Invalid reveal"
        );

        bool correct = keccak256(abi.encodePacked(_guess)) == secretHash;

        // Always write to storage — same gas cost either way
        commitments[msg.sender] = bytes32(0);

        if (correct) {
            payable(msg.sender).transfer(1 ether);
        }
    }
}
```

### Mitigations

* Flag contracts where user input is directly compared to stored secret values
* Require commit-reveal schemes for any guessing, matching, or auction-style logic
* Ensure that both branches of any comparison perform the same storage operations (constant gas cost)
* Avoid designs where a single function's gas cost varies based on hidden state

***

## 3. Contract Deployment Must Be Privileged

### The Risk

When a contract is deployed, its constructor executes as part of a single transaction sent to the null address.
Prividium verifies that the deploying account has the `contract_deployment` system permission, but without the policy
listener it **cannot inspect what the constructor does internally**. A malicious constructor can make arbitrary calls to
any contract on the network — reading secret data, executing unauthorized transactions, or exfiltrating information
into the newly deployed contract's storage.

:::warning[Deployment = Unrestricted Access (policy listener not configured)]
Without the policy listener, granting the `contract_deployment` system permission is equivalent to granting
**unrestricted call access to the entire network** for the duration of the deployment transaction. Only grant this
permission to fully trusted accounts. With the policy listener configured, a constructor's internal calls are
permission-checked like any other call, so the deployer can only reach contracts and methods it is already authorized
for.
:::

### Bad Pattern

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

/// ATTACK: A user with deployment permission deploys this contract.
/// The constructor calls a restricted contract that the deployer
/// has no direct permission to access via normal RPC calls.
contract MaliciousDeployer {
    bytes public stolenData;

    constructor(address secretContract, bytes memory callData) {
        // This call executes inside the deployment transaction.
        // Prividium only checked that the user can deploy —
        // it has no visibility into what the constructor calls.
        (bool success, bytes memory result) = secretContract.call(callData);
        require(success, "Exfiltration failed");
        stolenData = result;
    }
}
```

After deployment, the attacker reads `stolenData` from the new contract's storage — data that was never accessible to
them through normal RPC calls.

### Good Pattern

There is no "safe" way to allow untrusted users to deploy contracts while preventing constructor abuse. The mitigation
is entirely operational.

See [System Permissions](/configuring/users/system-permissions) for how to configure deployment permissions.

### Mitigations

* Configure the policy listener, so a constructor's internal calls are permission-checked like any other call
* Restrict `contract_deployment` to a dedicated deployer role with a small number of trusted accounts
* Never grant `contract_deployment` to end-user roles — without the policy listener it is not a "nice to have"
  permission, it is full network access during deployment
* Require contract source code review before each deployment
* Understand that deployment on Prividium requires elevated trust and governance review

***

## 4. Timing Side-Channel

### The Risk

This is closely related to the gas estimation oracle (Section 2) but exploits **wall-clock execution time** instead of
gas cost. If a smart contract's execution time varies based on non-public state — for example, iterating over a
variable-length array or performing heavier computation on a match — the response time of `eth_call` leaks information
about that state.

A single call may produce a negligible difference, but JSON-RPC batching allows sending many identical calls in one HTTP
request, amplifying the timing difference to a clearly measurable level.

### Bad Pattern

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

/// UNSAFE: Execution time scales with the number of entries.
/// By measuring eth_call response time, the caller can infer
/// the size of the private data.
contract UnsafeRegistry {
    mapping(address => address[]) private connections;

    function getConnectionCount(address user) external view returns (uint256) {
        address[] storage conns = connections[user];
        uint256 count = 0;
        for (uint256 i = 0; i < conns.length; i++) {
            if (conns[i] != address(0)) {
                count++;
            }
        }
        return count;
    }
}
```

Another vulnerable pattern — early returns that reveal whether (and where) a value exists:

```solidity
/// UNSAFE: Early return on match means execution time reveals
/// whether the value exists and its approximate position.
function findEntry(bytes32 key) external view returns (bool) {
    for (uint256 i = 0; i < entries.length; i++) {
        if (entries[i] == key) return true;
    }
    return false;
}
```

### Good Pattern

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

/// SAFE: Constant-time operation — a single storage read (SLOAD)
/// regardless of how many connections exist.
contract SafeRegistry {
    mapping(address => uint256) private connectionCount;
    mapping(address => mapping(uint256 => address)) private connections;

    function addConnection(address target) external {
        uint256 idx = connectionCount[msg.sender];
        connections[msg.sender][idx] = target;
        connectionCount[msg.sender] = idx + 1;
    }

    function getConnectionCount(address user) external view returns (uint256) {
        return connectionCount[user];
    }
}
```

### Mitigations

* Flag view functions that iterate over variable-length arrays or perform computation that scales with hidden data
* Prefer O(1) storage patterns: pre-computed counters, indexed mappings, avoid iteration over dynamic arrays
* Avoid early-return patterns in lookups over private data
* By default, Prividium caps JSON-RPC batches at 20 requests per HTTP request. Each item consumes one unit of the caller's RPC quota.
  Default concurrency caps allow 20 active operations per caller and 1,000 across all callers per API replica, including callers
  exempt from quotas and deployments with rate limiting disabled. A batch holds all its slots until every item and its
  upstream calls settle. These limits are configurable; increasing them permits more simultaneous work. See [Rate Limiting](/configuring/applications/rate-limiting).

***

## 5. Selective Disclosure Integrity Limits (eth\_call Disclosures)

### The Risk

The eth\_call-based selective-disclosure methods — `prividium_tokenSupplyDisclosure` and
`prividium_tokenBalanceDisclosure` — return a call result together with cryptographic proof material that a verifier
replays on a clean, trusted node. The proof and replay bind the disclosed result to on-chain state, but they do **not**
guarantee it reflects the contract's real behaviour on the Prividium chain: a contract deployer who controls the
disclosed contract's code can construct a contract whose disclosed result differs from what the chain actually returns,
in a way the verification steps cannot detect. Disclosures are also unsigned and self-presented, so a verifier has no
cryptographic assurance of who produced them.

The consequence: a token-supply or balance disclosure should be trusted only when the disclosed contract's code is
itself trusted. It is not a defense against a malicious contract author.

Account-data disclosure (`prividium_accountDataDisclosure`) is not affected — it proves account properties directly
against the L1-bound state commitment without replaying any call.

### Mitigations

* Selective disclosure is **disabled by default** (`DISCLOSURE_METHODS_ENABLED=false` on the Permissions API,
  `VITE_DISCLOSURE_METHODS_ENABLED=false` on the Admin Panel). Leave it off unless you have a concrete need
* Only enable it when every account holding the `contract_deployment` permission is fully trusted — the deployer of a
  disclosed contract controls what its disclosures can be made to say
* Review a contract's source before enabling supply or balance disclosure for it, and only disclose contracts whose code
  you have vetted
* As a consumer of a disclosure, treat an unsigned, self-presented disclosure as a claim by the presenter, not an
  attestation by the zone operator

***

## 6. Application Tokens Carry the User's RPC Authority

### The Risk

"Login with Prividium" hands a registered application a session bound to that application's OAuth client id. The
Permissions API accepts it only on the application surface: `/rpc`, `GET /api/profiles/me`, `GET /api/user-wallets`,
`GET /api/contracts/{address}/abi`, the `/api/wallet/*` token endpoints, and the session lifecycle endpoints
(`current-session`, `extend-session`, `logout`). Every other route, including all admin, organization and
account-management routes, rejects it with `401`, whatever roles the user holds.

Within that surface the token still acts as the user. An application that a zone administrator signs in to can read and
transact through `/rpc` with the administrator's full RPC authority, and services that validate a forwarded token through
`GET /api/profiles/me` (the Block Explorer API, for example) see the user's roles. There is no consent step: an already
signed-in user is redirected back to the application without interaction.

The admin panel and the Swagger docs receive the account session instead, because the admin panel needs the admin routes
and the docs login exchanges the token for a docs-only cookie. Every other client, including the built-in Block Explorer
and the CLI proxy, receives an application token.

### Mitigations

* Register only applications you trust with the users who will sign in to them; registration is an admin-only action
* Sign in to third-party applications with accounts that hold no more permissions than the application needs
* Review the **Sessions** page in the User Panel, where application sessions carry their client id, and revoke the ones
  you no longer use; deleting an application revokes every session issued to it
* Logging out of the User Panel ends the account session only. An application session keeps its own expiry, capped at
  the account session's, so use **Revoke All Others** on the **Sessions** page to end it early
* Revoking sessions does not rotate the per-user wallet RPC token an application has read through
  `GET /api/wallet/personal-rpc-token`; call `POST /api/wallet/invalidate` after revoking an application you no longer
  trust
* Services that validate forwarded tokens should call `GET /api/auth/current-session` and treat a non-null
  `oauthClientId` as an application token, not as the user's account session
* Tokens issued before this behaviour shipped keep the account's full power until they expire; revoke all sessions after
  upgrading if that window matters to you

***

## Related Documentation

* [Building Web Applications](/developers/applications/building-web-applications#authentication-and-oauth-scopes) —
  what an application token can reach
* [Minimizing Contract ABI Exposure](/developers/applications/minimizing-abi-exposure) — reducing information leakage
  from bundled ABIs
* [Selective Disclosure](/configuring/contracts/selective-disclosure) — configuring public access to specific
  contract data
* [Contract Deployment and Scripting](/developers/scripting/contract-deployment-scripting) — deployment methods and
  prerequisites
* [Contract Permissions](/configuring/contracts/permissions) — configuring per-function access rules
* [System Permissions](/configuring/users/system-permissions) — managing deployment and full-access
  permissions
