diff --git a/docs/devnet/content.md b/docs/devnet/content.md index a7e73e5a..dbb743c8 100644 --- a/docs/devnet/content.md +++ b/docs/devnet/content.md @@ -9,7 +9,3 @@ url: / ! import ./intro.md ! import ./config.md - ---- - -! import ./precompile.md diff --git a/docs/devnet/intro.md b/docs/devnet/intro.md index 59318121..961b4ae0 100644 --- a/docs/devnet/intro.md +++ b/docs/devnet/intro.md @@ -7,4 +7,7 @@ layout: single The pod devnet is a test network for developers to experiment with the pod network. It is designed to be a sandbox for testing and development purposes, allowing developers to build and test -their applications without the need for real assets or transactions. \ No newline at end of file +their applications without the need for real assets or transactions. + + +> We expect the devnet to have breaking changes or be reset (pruned completely) at any time. diff --git a/docs/devnet/precompile.md b/docs/devnet/precompile.md deleted file mode 100644 index 2ed60613..00000000 --- a/docs/devnet/precompile.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -layout: single ---- - -## Precompiles - -! table style1 -| Signature | Address | Description | -| ----------------------------------------------------- | ------------------------------------------ | ---------------------------------------------------------------------- | -| requireQuorum(boolean) | 0x4CF3F1637bfEf1534e56352B6ebAae243aF464c3 | Like `require` but passes if supermajority agrees | -| external_call([uint256, [Transaction,bytes]]) | 0x8712E00C337971f876621faB9326908fdF330d77 | Call a smart contract on another EVM-compatible chain | -| call_with_state([uint256, Header, EVMCall, EVMState]) | 0xb4bbff8874b41f97535bc8dafbaaff0dc5c72e5a | Simulate an EVM transaction execution given a particular initial state | -! table end - -> We expect the devnet to have breaking changes or be reset (pruned completely) at any time. \ No newline at end of file diff --git a/docs/menu.yaml b/docs/menu.yaml index e6bc4c53..dea7d78f 100644 --- a/docs/menu.yaml +++ b/docs/menu.yaml @@ -21,6 +21,8 @@ menu: - heading: Building on pod + - href: /precompiles + label: Precompiles - href: /json-rpc label: JSON-RPC - href: /solidity-sdk diff --git a/docs/precompiles/callWithState.md b/docs/precompiles/callWithState.md new file mode 100644 index 00000000..95db4f83 --- /dev/null +++ b/docs/precompiles/callWithState.md @@ -0,0 +1,138 @@ +--- +layout: simple +--- + +! content id="callWithState" + +! anchor callWithState go-up +## Call With State + +Simulates an EVM call against a supplied header and state on a specified chain. Executes the provided call in an ephemeral VM and returns the call's raw return bytes. + +### Address + +0xB4BBff8874b41f97535bC8dAFBaAff0DC5c72E5A + +### Inputs + +! table style1 +| Byte range | Name | Description | +| ------------------ | ---------- | --------------------------------------------------------------------------- | +| [0; I size] | input (I) | struct `EVMCallWithStateArgs` with `chainId`, `header`, `call`, and `state` | +! table end + +The EVMCallWithStateArgs struct is defined as: +```solidity +struct EVMCallWithStateArgs { + uint256 chainId; + Header header; + EVMCall call; + EVMState state; +} + +struct Header { + bytes32 parentHash; + bytes32 uncleHash; + address coinbase; + bytes32 root; + bytes32 txHash; + bytes32 receiptHash; + bytes bloom; + uint256 difficulty; + uint256 number; + uint256 gasLimit; + uint256 gasUsed; + uint256 time; + bytes extra; + bytes32 mixDigest; + bytes8 nonce; +} + +struct EVMCall { + address from; + address to; + bytes input; +} + +struct EVMState { + Account[] accounts; +} + +struct Account { + AccountProof proof; + bytes code; +} + +struct StorageProof { + bytes32 key; + bytes32 value; + bytes[] proof; +} + +struct AccountProof { + address addr; + bytes[] accountProof; + uint256 balance; + bytes32 codeHash; + uint256 nonce; + bytes32 storageHash; + StorageProof[] storageProof; +} +``` + +### Output + +! table style1 +| Byte range | Name | Description | +| ------------------ | ---------- | ---------------------------------- | +| [0; R size] | result (R) | Return bytes of the EVM execution | +! table end + +### Errors + +- Out of gas if provided gas is less than base cost. +- Input must not be empty. +- Input decoding failed. +- Chain ID must fit in `uint64`. +- EVM creation failed (invalid header / state / account proofs / storage proofs). +- Returned empty output. +- Call failed or reverted. + +### Gas Cost + +Static gas: 500 + +Dynamic gas: gas used by the simulated EVM call + +! content end + + +! content +! sticky + +### Example + +! codeblock title="" +```solidity +address internal constant EVM_CALL_WITH_STATE = address( + uint160(uint256(keccak256("POD_EVM_CALL_WITH_STATE"))) +); + +function call_precompile(EVMCallWithStateArgs memory args) public view returns (bytes memory) { + bytes memory inputData = abi.encode( + args.chainId, + args.header, + args.call, + args.state + ); + + (bool success, bytes memory returnData) = EVM_CALL_WITH_STATE.staticcall{ gas: gasleft() }(inputData); + require(success, "Precompile call failed"); + + return returnData; +} +``` +! codeblock end + +! sticky end +! content end \ No newline at end of file diff --git a/docs/precompiles/content.md b/docs/precompiles/content.md new file mode 100644 index 00000000..50e9b7dc --- /dev/null +++ b/docs/precompiles/content.md @@ -0,0 +1,39 @@ +--- +title: pod Precompiles +layout: blank + +url: /precompiles + +toc: + precompiles-overview: Overview + timestamp: timestamp + requireQuorum: requireQuorum + txInfo: txInfo + externalCall: externalCall + # callWithState: callWithState +--- + +! import ./overview.md +! import ./precompiles-table.md + +--- + +! import ./timestamp.md + +--- + +! import ./requireQuorum.md + +--- + +! import ./txInfo.md + +--- + +! import ./externalCall.md + +--- + + \ No newline at end of file diff --git a/docs/precompiles/externalCall.md b/docs/precompiles/externalCall.md new file mode 100644 index 00000000..bdb1a399 --- /dev/null +++ b/docs/precompiles/externalCall.md @@ -0,0 +1,84 @@ +--- +layout: simple +--- + +! content id="externalCall" + +! anchor externalCall go-up +## External Call + +Calls a smart contract on another EVM-compatible chain via the configured validators's RPC URL. Input specifies chain ID, transaction fields, and a block tag/number; output is the raw result of the remote `eth_call`. + +### Address + +0x8712E00C337971f876621faB9326908fdF330d77 + +### Inputs + +! table style1 +| Byte range | Name | Description | +| ------------------ | ------------------- | --------------------------------------------------------------------------------------------------------- | +| [0; I size] | input (I) | struct ExternalEthCallArgs defining the targeted chain and the arguments of the call (see details below) | +! table end + +The ExternalEthCallArgs struct is defined as: +```solidity + struct Transaction { + address from; + address to; + uint256 gas; + uint256 gasPrice; + uint256 value; + bytes data; +} + +struct EthCallArgs { + Transaction transaction; + bytes blockNumber; +} + +struct ExternalEthCallArgs { + uint256 chainId; + EthCallArgs ethCallArgs; +} +``` + +### Output + +Raw bytes returned by the remote `eth_call`. + +! table style1 +| Byte range | Name | Description | +| ------------------ | ---------- | --------------------------- | +| [0; R size] | result (R) | Remote `eth_call` return | +! table end + +### Errors + +- Out of gas if provided gas is less than base cost. +- Empty input. +- Input decoding failed. +- No RPC URL configured for the specified chain ID. +- Invalid argument: block number/tag format. +- Invalid argument: `to` is zero. + +### Gas Cost + +Static gas: 100 + +! content end + + +! content +! sticky + +### Example + +This example uses the POD_EXTERNAL_ETH_CALL precompile to run an eth_call on Ethereum mainnet, returning the USDC balance for a given account. + +! codeblock title="examples/solidity/src/EthereumERC20Balance.sol" +! codeblock import solidity "./src/EthereumERC20Balance.sol" +! codeblock end + +! sticky end +! content end \ No newline at end of file diff --git a/docs/precompiles/overview.md b/docs/precompiles/overview.md new file mode 100644 index 00000000..19bd4d2b --- /dev/null +++ b/docs/precompiles/overview.md @@ -0,0 +1,15 @@ +--- +layout: single +--- + +! anchor precompiles-overview +# Precompiles +In pod, precompiles work similarly to Ethereum. We currently support all precompiles available in the Prague version of the EVM, plus some Pod-specific ones listed in the table below. + + +## Precompile Addressing Scheme +The address of each Pod precompile is derived from the precompile name as the last 20 bytes of the `keccak256` hash of the name. + +```solidity +address constant POD_TIMESTAMP_PRECOMPILE = address(uint160(uint256(keccak256("POD_TIMESTAMP")))); +``` diff --git a/docs/precompiles/precompiles-table.md b/docs/precompiles/precompiles-table.md new file mode 100644 index 00000000..614d8d4a --- /dev/null +++ b/docs/precompiles/precompiles-table.md @@ -0,0 +1,16 @@ +--- +layout: full +--- + +! anchor precompiles-table +## Available Precompiles + +! table style1 rowLink="hash" rowLinkBy="id" rowLinkBase="/precompiles" hideFields="id" +| id | Address | Name | Minimum Gas | Description | +| ------------- | ------------------------------------------ | ----------------------- | ------------ | ------------------------------------------------------------------------------ | +| timestamp | 0x423Bb123D9d5143e662606Fd343b6766d7BCf721 | POD_TIMESTAMP | 100 | Fetches the current system timestamp | +| requireQuorum | 0x6AD9145E866c7A7DcCc6c277Ea86abBD268FBAc9 | POD_REQUIRE_QUORUM | 100 | Like `require` but passes if supermajority agrees | +| txInfo | 0x7687A3413739715807812b529f2d5f7Ef9057697 | POD_TX_INFO | 100 | Fetches information about the current transaction (nonce and transaction hash) | +| externalCall | 0x8712E00C337971f876621faB9326908fdF330d77 | POD_EXTERNAL_ETH_CALL | 100 | Call a smart contract on another EVM-compatible chain | + +! table end diff --git a/docs/precompiles/requireQuorum.md b/docs/precompiles/requireQuorum.md new file mode 100644 index 00000000..be967dc2 --- /dev/null +++ b/docs/precompiles/requireQuorum.md @@ -0,0 +1,72 @@ +--- +layout: simple +--- + +! content id="requireQuorum" + +! anchor requireQuorum go-up +## Require Quorum + +The Require Quorum precompile supports the two-round commit–execute flow used by pod. +It allows the enforcing a boolean check during the first round and skip it during the second round, once a quorum of validator approvals has been collected. + +This is ideal for scenarios where an action must first be validated by a majority of validators before it can be executed deterministically in the second round. + +#### How it works: + +**Round 1 – Commit Phase:** +Validators check the input boolean. + . If `false`, the call reverts. + . If `true`, an attestation is signed. + +**Round 2 – Execute Phase:** +The client submits a quorum of these attestations. +The precompile then accepts unconditionally, ensuring the transaction succeeds regardless of the boolean input. + +### Address + +0x6AD9145E866c7A7DcCc6c277Ea86abBD268FBAc9 + +### Inputs + +! table style1 +| Byte range | Name | Description | +| ------------------ | --------- | ------------------------------------- | +| [0; 31] (32 bytes) | input | Boolean to be evaluated by validators | +! table end + +> Note: `input` must be deterministic across validators for the same state and call data. + +### Output + +None. + +### Errors + +- Out of gas if provided gas is less than fixed base cost. +- Invalid input length (must be exactly 32 bytes). +- Round 1: reverts if `input == false` (quorum requirement not met). +- Round 2: succeeds regardless of `input` when quorum attestations are provided. + +### Gas Cost + +Static gas: 100 + +! content end + + +! content +! sticky + +### Example + +The example contract shows a balance-gated action: +- In Round 1, validators check if the caller has at least 1 ether. +- In Round 2, after quorum attestations are collected, the action can be executed without rechecking the balance. + +! codeblock title="examples/solidity/src/QuorumRestrictedAction.sol" +! codeblock import solidity "./src/QuorumRestrictedAction.sol" +! codeblock end + +! sticky end +! content end \ No newline at end of file diff --git a/docs/precompiles/src/Context.sol b/docs/precompiles/src/Context.sol new file mode 120000 index 00000000..37295c4e --- /dev/null +++ b/docs/precompiles/src/Context.sol @@ -0,0 +1 @@ +../../../solidity-sdk/src/Context.sol \ No newline at end of file diff --git a/docs/precompiles/src/EthereumERC20Balance.sol b/docs/precompiles/src/EthereumERC20Balance.sol new file mode 120000 index 00000000..e532dcea --- /dev/null +++ b/docs/precompiles/src/EthereumERC20Balance.sol @@ -0,0 +1 @@ +../../../examples/solidity/src/EthereumERC20Balance.sol \ No newline at end of file diff --git a/docs/precompiles/src/QuorumRestrictedAction.sol b/docs/precompiles/src/QuorumRestrictedAction.sol new file mode 120000 index 00000000..18dde660 --- /dev/null +++ b/docs/precompiles/src/QuorumRestrictedAction.sol @@ -0,0 +1 @@ +../../../examples/solidity/src/QuorumRestrictedAction.sol \ No newline at end of file diff --git a/docs/precompiles/src/Time.sol b/docs/precompiles/src/Time.sol new file mode 120000 index 00000000..9fbaf982 --- /dev/null +++ b/docs/precompiles/src/Time.sol @@ -0,0 +1 @@ +../../../solidity-sdk/src/Time.sol \ No newline at end of file diff --git a/docs/precompiles/timestamp.md b/docs/precompiles/timestamp.md new file mode 100644 index 00000000..2e6f0496 --- /dev/null +++ b/docs/precompiles/timestamp.md @@ -0,0 +1,51 @@ +--- +layout: simple +--- + +! content id="timestamp" + +! anchor timestamp go-up +## Timestamp + +Fetches the current system timestamp as microseconds since the UNIX epoch. + +### Address + +0x423Bb123D9d5143e662606Fd343b6766d7BCf721 + + +### Inputs + +None. + +### Output + +! table style1 +| Byte range | Name | Description | +| ------------------ | --------- | ---------------------------------------------------------- | +| [0; 31] (32 bytes) | timestamp | `uint128` microsecond timestamp, right aligned to 32 bytes | +! table end + +### Errors + +- Out of gas if provided gas is less than base cost. +- "time before unix epoch" if the system time is earlier than the UNIX epoch. + +### Gas Cost + +Static gas: 100 + +! content end + + +! content +! sticky + +### Example + +! codeblock title="solidity-sdk/src/Time.sol" +! codeblock import solidity "./src/Time.sol" lines="6-7,30-38" +! codeblock end + +! sticky end +! content end \ No newline at end of file diff --git a/docs/precompiles/txInfo.md b/docs/precompiles/txInfo.md new file mode 100644 index 00000000..977a6033 --- /dev/null +++ b/docs/precompiles/txInfo.md @@ -0,0 +1,55 @@ +--- +layout: simple +--- + +! content id="txInfo" + +! anchor txInfo go-up +## Transaction Information + +Fetches information about the current transaction. +Current implementation provides: +- nonce +- transaction hash + +### Address + +0x7687A3413739715807812b529f2d5f7Ef9057697 + +### Inputs + +None. + +### Output + +! table style1 +| Byte range | Name | Description | +| ------------------ | --------- | ------------------------------------------ | +| [0; 31] (32 bytes) | nonce | `uint64` transaction nonce, right aligned | +| [32; 63] (32 bytes)| txHash | transaction hash | +! table end + +> Note: If the precompile is used in a call, txHash is 0x00 + +### Errors + +- Out of gas if provided gas is less than base cost. + +### Gas Cost + +Static gas: 100 + +! content end + + +! content +! sticky + +### Example + +! codeblock title="solidity-sdk/src/Context.sol" +! codeblock import solidity "./src/Context.sol" lines="4-5,11-15,20-30" +! codeblock end + +! sticky end +! content end \ No newline at end of file diff --git a/examples/solidity/src/EthereumERC20Balance.sol b/examples/solidity/src/EthereumERC20Balance.sol new file mode 100644 index 00000000..9a21f7a0 --- /dev/null +++ b/examples/solidity/src/EthereumERC20Balance.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IERC20} from "openzeppelin-contracts/token/ERC20/IERC20.sol"; + +/// @dev Encodes an Ethereum transaction call used by the precompile. +struct Transaction { + address from; + address to; + uint256 gas; + uint256 gasPrice; + uint256 value; + bytes data; +} + +/// @dev Encapsulates call arguments and block selector for `eth_call`. +struct EthCallArgs { + Transaction transaction; + bytes blockNumber; +} + +/// @title EthereumERC20Balance +contract EthereumERC20Balance { + address internal constant EXTERNAL_CALL_PRECOMPILE = address(uint160(uint256(keccak256("POD_EXTERNAL_ETH_CALL")))); + uint256 internal constant ETH_CHAIN_ID = 1; + address internal constant ETH_USDC_CONTRACT = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + + /// @notice Returns the USDC balance for `account` on Ethereum mainnet. + /// @param account The account to query on Ethereum. + /// @return balance The USDC balance in base units. + /// @dev Builds ERC-20 calldata and delegates to the external-call precompile. Reverts on failure. + function getUSDCBalanceEthereum(address account) public view returns (uint256) { + bytes memory data = bytes.concat(IERC20.balanceOf.selector, abi.encode(account)); + + EthCallArgs memory callArgs = EthCallArgs({ + transaction: Transaction({ + from: address(0), + to: ETH_USDC_CONTRACT, + gas: 100000, + gasPrice: 1000, + value: 0, + data: data + }), + blockNumber: bytes("latest") + }); + + bytes memory inputData = abi.encode(ETH_CHAIN_ID, callArgs); + (bool success, bytes memory output) = EXTERNAL_CALL_PRECOMPILE.staticcall{gas: gasleft()}(inputData); + require(success, "Precompile call failed"); + return abi.decode(output, (uint256)); + } +} diff --git a/examples/solidity/src/QuorumRestrictedAction.sol b/examples/solidity/src/QuorumRestrictedAction.sol new file mode 100644 index 00000000..18bd00fa --- /dev/null +++ b/examples/solidity/src/QuorumRestrictedAction.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/// @title QuorumRestrictedAction +/// @notice Demonstrates gating function execution on quorum over a boolean predicate. +/// @dev Uses the `POD_REQUIRE_QUORUM` precompile to enforce quorum on pod. +contract QuorumRestrictedAction { + address constant REQUIRE_QUORUM = address(uint160(uint256(keccak256("POD_REQUIRE_QUORUM")))); + uint256 public etherThreshold = 1 ether; + + event ActionAllowed(address indexed account); + + /// @notice Reverts unless there is validator quorum over the predicate + /// `account.balance >= etherThreshold`. + /// @dev Calls the precompile via `staticcall` with the boolean predicate. + /// @param account The account whose Ether balance is checked. + modifier requireEnoughEther(address account) { + (bool success,) = REQUIRE_QUORUM.staticcall(abi.encode(account.balance >= etherThreshold)); + require(success, "Not enough Ether balance"); + _; + } + + /// @notice Example function restricted to callers whose Ether balance meets `etherThreshold`. + /// @dev Emits an `ActionAllowed` event on success. + function restrictedAction() public requireEnoughEther(msg.sender) { + emit ActionAllowed(msg.sender); + } +}