diff --git a/.gitignore b/.gitignore index e1c0300..1dd1203 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ node_modules ignition/deployments/chain-31337 /.idea +.devcontainer diff --git a/pod/IInbox.sol b/pod/IInbox.sol new file mode 100644 index 0000000..4846a14 --- /dev/null +++ b/pod/IInbox.sol @@ -0,0 +1,198 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +/// @title IInbox +/// @notice Cross-chain request/response inbox: send messages to remote chains, execute incoming calls, and query state. +/// @dev Fee-related fields on {Request} are **gas unit** budgets, not wei. See {InboxFeeManager}. +interface IInbox { + // --- Types --- + + /// @notice Encoded method call and optional MPC ABI metadata. + struct MpcMethodCall { + /// @notice Function selector to re-encode with MPC GTs; zero means `data` is raw calldata. + bytes4 selector; + /// @notice ABI-encoded arguments or raw calldata. + bytes data; + /// @notice MPC datatype descriptors used by {MpcAbiCodec}. + bytes8[] datatypes; + /// @notice MPC ciphertext length descriptors used by {MpcAbiCodec}. + bytes32[] datalens; + } + + /// @notice Stored outbound or incoming cross-chain request. + struct Request { + /// @notice Packed request id containing source chain id and nonce. + bytes32 requestId; + /// @notice Destination chain id for outbound requests, or source chain id for incoming requests. + uint256 targetChainId; + /// @notice Contract invoked on the destination/current chain. + address targetContract; + /// @notice Method call executed on the target. + MpcMethodCall methodCall; + /// @notice Immediate caller that submitted the request. + address callerContract; + /// @notice Application contract that should receive responses/errors. + address originalSender; + /// @notice Request creation or ingestion timestamp. + uint64 timestamp; + /// @notice Callback selector used for two-way success responses. + bytes4 callbackSelector; + /// @notice Error selector used for failed execution responses. + bytes4 errorSelector; + /// @notice True when a success response is expected. + bool isTwoWay; + /// @notice True after an incoming request or linked response has been processed. + bool executed; + /// @dev If this request is a one-way response or error delivery, links to the original two-way request ID. + bytes32 sourceRequestId; + /// @dev Gas unit budget for the remote execution leg (`call{gas: ...}` cap). Not wei. + uint256 targetFee; + /// @dev Gas unit budget for the callback leg on the source chain. Not wei. + uint256 callerFee; + } + + /// @notice Response metadata for an incoming request. + struct Response { + /// @notice Outbound request id that delivered the response. + bytes32 responseRequestId; + /// @notice Response payload returned by the target contract. + bytes response; + } + + /// @notice Stored execution or encoding error for a request. + struct Error { + /// @notice Request id that failed. + bytes32 requestId; + /// @notice Protocol-defined error code. + uint64 errorCode; + /// @notice Revert data or encoded error message. + bytes errorMessage; + } + + /// @notice Active incoming execution context exposed to target contracts. + struct ExecutionContext { + /// @notice Remote chain that sent the current request. + uint256 remoteChainId; + /// @notice Remote contract that sent the current request. + address remoteContract; + /// @notice Current incoming request id. + bytes32 requestId; + } + + // --- External: sends (payable) --- + + /// @notice Send a two-way message with callback and error handlers on the remote chain. + /// @param targetChainId Destination chain ID. + /// @param targetContract Contract to call on the destination chain. + /// @param methodCall Calldata and MPC metadata. + /// @param callbackSelector Selector invoked on the source chain when the remote call succeeds. + /// @param errorSelector Selector invoked on the source chain when the remote call fails. + /// @param callbackFeeLocalWei Wei from `msg.value` reserved for the callback leg (converted to gas units in fee logic). + /// @return requestId The new outbound request ID. + function sendTwoWayMessage( + uint256 targetChainId, + address targetContract, + MpcMethodCall calldata methodCall, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 callbackFeeLocalWei + ) external payable returns (bytes32); + + /// @notice Send a one-way message with an error handler only (no callback). + /// @param targetChainId Destination chain ID. + /// @param targetContract Contract to call on the destination chain. + /// @param methodCall Calldata and MPC metadata. + /// @param errorSelector Selector invoked on error. + /// @return requestId The new outbound request ID. + function sendOneWayMessage( + uint256 targetChainId, + address targetContract, + MpcMethodCall calldata methodCall, + bytes4 errorSelector + ) external payable returns (bytes32); + + // --- External: execution (non-payable) --- + + /// @notice Respond to the current incoming message (two-way flow). + /// @dev Gas for the return leg is **not** charged again: it uses the `callerFee` budget from the original two-way request. + /// @param data Payload routed to the original sender via `callbackSelector`. + function respond(bytes memory data) external; + + /// @notice Signal an application error for the current incoming two-way message (same routing constraints as {respond}). + /// @dev Gas for the return leg is **not** charged again: it uses the `callerFee` budget from the original two-way request. + /// @param data ABI-encoded argument for the remote `errorSelector(bytes)`. + function raise(bytes memory data) external; + + // --- External: views --- + + /// @notice Return error details for a failed outgoing request. + /// @param requestId Outbound request ID. + /// @return code Error code. + /// @return message Error message or revert data. + function getOutboxError(bytes32 requestId) external view returns (uint256 code, string memory message); + + /// @notice Return stored response bytes for a completed incoming flow. + /// @param requestId Incoming request ID. + /// @return response Response payload. + function getInboxResponse(bytes32 requestId) external view returns (bytes memory); + + /// @notice Return a slice of outbound requests sent to `targetChainId`, in per-target nonce order. + /// @param targetChainId Destination chain whose outbound requests to read. + /// @param from Start index (0-based) within that target's sequence. + /// @param len Maximum number of requests to return. + /// @return requestsList Request structs. + function getRequests(uint256 targetChainId, uint256 from, uint256 len) + external + view + returns (Request[] memory); + + /// @notice Total count of outbound requests issued from this inbox to `targetChainId`. + /// @param targetChainId Destination chain. + /// @return count Number of requests to that target. + function getRequestsLen(uint256 targetChainId) external view returns (uint256); + + /// @notice Look up a stored outbound request by its id (id encodes source+target+nonce). + /// @param requestId Outbound request id. + /// @return request The stored request (zeroed if unknown). + function getRequest(bytes32 requestId) external view returns (Request memory); + + /// @notice Look up a stored incoming request by its id (id encodes the source chain). + /// @param requestId Incoming request id. + /// @return request The stored incoming request (zeroed if unknown). + function getIncomingRequest(bytes32 requestId) external view returns (Request memory); + + /// @notice Remote chain ID and contract for the currently executing incoming message. + /// @return chainId Remote chain ID. + /// @return contractAddress Remote caller contract. + function inboxMsgSender() external view returns (uint256 chainId, address contractAddress); + + /// @notice Request ID for the currently executing incoming message. + /// @return requestId Active request ID. + function inboxRequestId() external view returns (bytes32); + + /// @notice Source request ID linked from the current incoming message (if any). + /// @return sourceRequestId Linked request ID. + function inboxSourceRequestId() external view returns (bytes32); + + // --- External: pure --- + + /// @notice Pack source chain id (64 bits), target chain id (64 bits) and nonce (128 bits) into a request id. + /// @param sourceChainId Originating chain id. + /// @param targetChainId Destination chain id. + /// @param nonce Per-target nonce. + /// @return requestId 256-bit packed id. + function getRequestId(uint256 sourceChainId, uint256 targetChainId, uint256 nonce) + external + pure + returns (bytes32); + + /// @notice Split a packed request id into source chain id, target chain id and nonce. + /// @param requestId Packed id. + /// @return sourceChainId Source chain id. + /// @return targetChainId Target chain id. + /// @return nonce Per-target nonce. + function unpackRequestId(bytes32 requestId) + external + pure + returns (uint256 sourceChainId, uint256 targetChainId, uint256 nonce); +} diff --git a/pod/IInboxMiner.sol b/pod/IInboxMiner.sol new file mode 100644 index 0000000..a4fe84e --- /dev/null +++ b/pod/IInboxMiner.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import "./IInbox.sol"; + +/// @title IInboxMiner +/// @notice Miner API: apply mined cross-chain payloads to this chain's inbox and withdraw fees. +interface IInboxMiner { + error RetryFailedRequestNotAFailedRequest(); + error RequestIdRequired(); + error RetryFailedRequestExecutionFailed(bytes returnData); + /// @notice The `sourceChainId` passed to {batchProcessRequests} is this chain's own id. + error SourceChainIsThisChain(uint256 chainId); + /// @notice A mined request's encoded source chain does not match the batch `sourceChainId`. + error RequestSourceChainMismatch(bytes32 requestId, uint256 expectedSourceChainId, uint256 actualSourceChainId); + /// @notice A mined request's encoded target chain is not this chain. + error RequestTargetChainMismatch(bytes32 requestId, uint256 expectedTargetChainId, uint256 actualTargetChainId); + /// @notice Inbound message processing is paused (circuit breaker). + error MessageProcessingPaused(); + + /// @notice Emitted when {retryFailedRequest} successfully re-executes a previously failed incoming request. + event RetryFailedRequestSuccess(bytes32 indexed requestId); + /// @notice Emitted when the owner toggles the message-processing circuit breaker. + event MessageProcessingPausedUpdated(bool paused); + + /// @notice Mined inbound request. `targetFee` and `callerFee` are gas unit budgets (see {IInbox.Request}). + struct MinedRequest { + bytes32 requestId; + address sourceContract; + address targetContract; + IInbox.MpcMethodCall methodCall; + bytes4 callbackSelector; + bytes4 errorSelector; + bool isTwoWay; + bytes32 sourceRequestId; + uint256 targetFee; + uint256 callerFee; + } + + /// @notice Validate and execute a batch of mined requests from `sourceChainId`. + /// @param sourceChainId Chain that produced the mined data. + /// @param mined Ordered requests to apply. + function batchProcessRequests(uint256 sourceChainId, MinedRequest[] memory mined) external; + + /// @notice Withdraw accumulated native token fees to `to` (owner-only in concrete implementations). + function collectFees(address payable to) external; + + /// @notice Pause or unpause inbound message processing (owner-only circuit breaker). + function setMessageProcessingPaused(bool paused) external; + + /// @notice Re-execute a mined incoming request whose target call failed (e.g. OOG). Open to any payer for gas. + function retryFailedRequest(bytes32 requestId) external; +} diff --git a/pod/InboxUser.sol b/pod/InboxUser.sol new file mode 100644 index 0000000..b50cc29 --- /dev/null +++ b/pod/InboxUser.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import "./IInbox.sol"; + +/// @title InboxUser +/// @notice Mixin that restricts selected functions to the configured {IInbox} and exposes `inbox`. +abstract contract InboxUser { + /// @notice Cross-chain inbox used for messaging. + IInbox public inbox; + + /// @notice Caller is not the configured inbox. + error OnlyInbox(address caller); + + /// @notice Restrict a function to the configured inbox. + /// @dev Reverts unless `msg.sender` is the configured inbox. + modifier onlyInbox() { + if (msg.sender != address(inbox)) { + revert OnlyInbox(msg.sender); + } + _; + } + + /// @notice Set the inbox contract (typically once from a constructor or initializer). + /// @param _inbox Inbox address. + function setInbox(address _inbox) internal { + inbox = IInbox(_inbox); + } +} diff --git a/pod/InboxUserCotiTestnet.sol b/pod/InboxUserCotiTestnet.sol new file mode 100644 index 0000000..6f312c6 --- /dev/null +++ b/pod/InboxUserCotiTestnet.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import "./InboxUser.sol"; +import "./PodNetworkConstants.sol"; + +/// @title InboxUserCotiTestnet +/// @notice Mixin that configures {InboxUser} for COTI testnet inbox address. +abstract contract InboxUserCotiTestnet is InboxUser { + constructor() { + setInbox(PodNetworkConstants.COTI_TESTNET_INBOX); + } +} diff --git a/pod/PodNetworkConstants.sol b/pod/PodNetworkConstants.sol new file mode 100644 index 0000000..81b4c2e --- /dev/null +++ b/pod/PodNetworkConstants.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +/// @title PodNetworkConstants +/// @notice Deployment constants used by chain-specific PoD dapp helper contracts. +library PodNetworkConstants { + /// @notice Deterministic inbox address shared by every chain (CreateX CREATE3 deploy). + /// @dev Same value on Sepolia, COTI testnet, and Avalanche Fuji because the CREATE3 address + /// depends only on the deployer EOA and salt (see scripts/createx.ts). + address internal constant INBOX = 0xAb625bE229F603f6BBF964474AFf6d5487e364De; + + /// @notice Source-chain inbox used by PoD dapps on Sepolia. + address internal constant SEPOLIA_INBOX = INBOX; + + /// @notice Avalanche Fuji chain id used as a source chain paired with COTI testnet. + uint256 internal constant AVALANCHE_FUJI_CHAIN_ID = 43113; + + /// @notice Source-chain inbox used by PoD dapps on Avalanche Fuji. + address internal constant AVALANCHE_FUJI_INBOX = INBOX; + + /// @notice COTI testnet chain id used for remote MPC execution. + uint256 internal constant COTI_TESTNET_CHAIN_ID = 7082400; + + /// @notice COTI-side MPC executor paired with source-chain PoD dapps. + /// @dev Redeploy and update this after the deterministic inbox redeploy on COTI. + address internal constant COTI_TESTNET_MPC_EXECUTOR = 0xC76aaE4F3810fBBd5d96b92DEFeBE0034405Ad9c; + + /// @notice COTI testnet inbox used by COTI-side dapps. + address internal constant COTI_TESTNET_INBOX = INBOX; +} diff --git a/pod/examples/MpcAdder.sol b/pod/examples/MpcAdder.sol new file mode 100644 index 0000000..8e80de1 --- /dev/null +++ b/pod/examples/MpcAdder.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import "../../utils/mpc/MpcCore.sol"; + +import "../mpc/PodLib.sol"; +import "../mpc/PodLibBase.sol"; + +/// @title MpcAdder +/// @notice Example dApp: 64-bit encrypted add over the inbox using {PodLib}. +contract MpcAdder is PodLib { + event AddRequest(bytes32 requestId); + + ctUint64 private _result; + + /// @notice Create an MPC adder bound to an inbox. + /// @param _inbox The inbox contract address. + constructor(address _inbox) PodLibBase(msg.sender) { + setInbox(_inbox); + } + + /// @notice Send an MPC add request using encrypted inputs. + /// @param a Encrypted input a (itUint64). + /// @param b Encrypted input b (itUint64). + /// @param callbackFeeLocalWei Wei slice for callback leg; total inbox payment is `msg.value`. + function add(itUint64 calldata a, itUint64 calldata b, uint256 callbackFeeLocalWei) external payable { + bytes32 requestId = add64( + a, + b, + msg.sender, + MpcAdder.receiveC.selector, + PodLibBase.onDefaultMpcError.selector, + msg.value, + callbackFeeLocalWei + ); + emit AddRequest(requestId); + } + + /// @notice Receive the response and store the ciphertext result. + /// @param data The response payload containing the ciphertext. + function receiveC(bytes memory data) external virtual onlyInbox { + _receiveResult(data); + } + + /// @dev Allows {MpcAdderPausable} to gate delivery without duplicating storage. + function _receiveResult(bytes memory data) internal virtual { + _result = abi.decode(data, (ctUint64)); + } + + /// @notice Return the last received ciphertext result. + function resultCiphertext() external view returns (ctUint64) { + return _result; + } +} + + + diff --git a/pod/examples/MpcAdderPausable.sol b/pod/examples/MpcAdderPausable.sol new file mode 100644 index 0000000..824c619 --- /dev/null +++ b/pod/examples/MpcAdderPausable.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts/utils/Pausable.sol"; + +import "./MpcAdder.sol"; + +/// @title MpcAdderPausable +/// @notice Same as {MpcAdder}, but `receiveC` reverts while paused (simulates callback failure until unpaused). +contract MpcAdderPausable is MpcAdder, Pausable { + constructor(address _inbox) MpcAdder(_inbox) {} + + /// @inheritdoc MpcAdder + function receiveC(bytes memory data) external override onlyInbox whenNotPaused { + _receiveResult(data); + } + + function pause() external onlyOwner { + _pause(); + } + + function unpause() external onlyOwner { + _unpause(); + } +} diff --git a/pod/examples/it128/PodAdder128.sol b/pod/examples/it128/PodAdder128.sol new file mode 100644 index 0000000..503747d --- /dev/null +++ b/pod/examples/it128/PodAdder128.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import "../../../utils/mpc/MpcCore.sol"; + +import "../../mpc/PodLib.sol"; +import "../../mpc/PodLibBase.sol"; + +/// @title PodAdder128 +/// @notice Example 128-bit MPC adder using {PodLib}. +contract PodAdder128 is PodLib { + event AddRequest(bytes32 requestId); + + ctUint128 private _result; + + /// @notice Create an MPC adder bound to an inbox. + /// @param _inbox The inbox contract address. + constructor(address _inbox) PodLibBase(msg.sender) { + setInbox(_inbox); + } + + /// @notice Send an MPC add request using encrypted inputs. + /// @param a Encrypted input a (itUint128). + /// @param b Encrypted input b (itUint128). + function add(itUint128 calldata a, itUint128 calldata b, uint256 callbackFeeLocalWei) external payable { + bytes32 requestId = add128( + a, + b, + msg.sender, + PodAdder128.receiveC.selector, + PodLibBase.onDefaultMpcError.selector, + msg.value, + callbackFeeLocalWei + ); + emit AddRequest(requestId); + } + + /// @notice Receive the response and store the ciphertext result. + /// @param data The response payload containing the ciphertext. + function receiveC(bytes memory data) external onlyInbox { + _result = abi.decode(data, (ctUint128)); + } + + /// @notice Return the last received ciphertext result. + function resultCiphertext() external view returns (ctUint128) { + return _result; + } +} diff --git a/pod/examples/it256/PodAdder256.sol b/pod/examples/it256/PodAdder256.sol new file mode 100644 index 0000000..57a0c00 --- /dev/null +++ b/pod/examples/it256/PodAdder256.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import "../../../utils/mpc/MpcCore.sol"; + +import "../../mpc/PodLib.sol"; +import "../../mpc/PodLibBase.sol"; + +/// @title PodAdder256 +/// @notice Example 256-bit MPC adder using {PodLib}. +contract PodAdder256 is PodLib { + event AddRequest(bytes32 requestId); + + ctUint256 private _result; + + /// @notice Create an MPC adder bound to an inbox. + /// @param _inbox The inbox contract address. + constructor(address _inbox) PodLibBase(msg.sender) { + setInbox(_inbox); + } + + /// @notice Send an MPC add request using encrypted inputs. + /// @param a Encrypted input a (itUint256). + /// @param b Encrypted input b (itUint256). + function add(itUint256 calldata a, itUint256 calldata b, uint256 callbackFeeLocalWei) external payable { + bytes32 requestId = add256( + a, + b, + msg.sender, + PodAdder256.receiveC.selector, + PodLibBase.onDefaultMpcError.selector, + msg.value, + callbackFeeLocalWei + ); + emit AddRequest(requestId); + } + + /// @notice Receive the response and store the ciphertext result. + /// @param data The response payload containing the ciphertext. + function receiveC(bytes memory data) external onlyInbox { + _result = abi.decode(data, (ctUint256)); + } + + /// @notice Return the last received ciphertext result. + function resultCiphertext() external view returns (ctUint256 memory) { + return _result; + } +} \ No newline at end of file diff --git a/pod/fee/IInboxFeeManager.sol b/pod/fee/IInboxFeeManager.sol new file mode 100644 index 0000000..5429d62 --- /dev/null +++ b/pod/fee/IInboxFeeManager.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +/// @title IInboxFeeManager +/// @notice Read-only fee estimation surface exposed by inbox contracts for PoD dapps. +interface IInboxFeeManager { + /// @notice Template for minimum fees in gas units. + struct FeeConfig { + uint256 constantFee; + uint256 gasPerByte; + uint256 callbackExecutionGas; + uint256 errorLength; + uint256 bufferRatioX10000; + } + + /// @notice Oracle used to convert gas budgets between local and remote fee tokens. + function priceOracle() external view returns (address); + + /// @notice Minimum fee template for the local callback leg. + function localMinFeeConfig() external view returns (FeeConfig memory); + + /// @notice Minimum fee template for the remote execution leg. + function remoteMinFeeConfig() external view returns (FeeConfig memory); + + /// @notice Estimate the local-token wei required for a two-way message. + /// @param remoteMethodCallSize Remote calldata size term. + /// @param callBackMethodCallSize Callback calldata size term. + /// @param remoteMethodExecutionGas Remote execution gas term. + /// @param callBackMethodExecutionGas Callback execution gas term. + /// @param gasPrice Wei per gas assumption. + /// @return targetFeeLocalWei Local-token wei estimated for the remote execution leg. + /// @return callerFeeLocalWei Local-token wei estimated for the callback leg. + function calculateTwoWayFeeRequiredInLocalToken( + uint256 remoteMethodCallSize, + uint256 callBackMethodCallSize, + uint256 remoteMethodExecutionGas, + uint256 callBackMethodExecutionGas, + uint256 gasPrice + ) external view returns (uint256 targetFeeLocalWei, uint256 callerFeeLocalWei); +} diff --git a/pod/manifest.json b/pod/manifest.json new file mode 100644 index 0000000..4c65488 --- /dev/null +++ b/pod/manifest.json @@ -0,0 +1,52 @@ +{ + "generatedAt": "2026-06-25T12:37:39Z", + "source": "/Users/ith/nk/pod-mpc-lib/contracts", + "target": "/Users/ith/nk/coti-contracts/pod", + "files": [ + "examples/it128/PodAdder128.sol", + "examples/it256/PodAdder256.sol", + "examples/MpcAdder.sol", + "examples/MpcAdderPausable.sol", + "fee/IInboxFeeManager.sol", + "IInbox.sol", + "IInboxMiner.sol", + "InboxUser.sol", + "InboxUserCotiTestnet.sol", + "mpc/coti-side/IPodExecutorOps.sol", + "mpc/PodLib.sol", + "mpc/PodLib128.sol", + "mpc/PodLib256.sol", + "mpc/PodLib64.sol", + "mpc/PodLibBase.sol", + "mpc/PodUser.sol", + "mpc/PodUserFuji.sol", + "mpc/PodUserSepolia.sol", + "mpccodec/MpcAbiCodec.sol", + "PodNetworkConstants.sol", + "privacy/IPrivacyPortal.sol", + "privacy/PrivacyPortal.sol", + "privacy/PrivacyPortalFactory.sol", + "token/erc7984/Erc7984Constants.sol", + "token/erc7984/Erc7984Pointers.sol", + "token/erc7984/IERC7984.sol", + "token/erc7984/IERC7984PortalWrapper.sol", + "token/erc7984/PodErc7984Mixin.sol", + "token/erc7984/README.md", + "token/perc20/cotiside/IPodErc20CotiSide.sol", + "token/perc20/cotiside/PodErc20CotiMother.sol", + "token/perc20/IPodERC20.sol", + "token/perc20/PodERC20.sol", + "token/perc20/PodErc20Mintable.sol", + "token/perc20/PodErc20MintableInitializable.sol", + "utils/IWrappedNative.sol" + ], + "externalImports": [ + "utils/mpc/MpcCore.sol", + "utils/mpc/MpcInterface.sol" + ], + "notes": [ + "MpcCore.sol and MpcInterface.sol live at contracts/utils/mpc/ in coti-contracts.", + "OpenZeppelin imports are unchanged (@openzeppelin/contracts).", + "Indexers/relayers: use getRequest(requestId) for full inbox payloads (compact MessageSent logs)." + ] +} diff --git a/pod/mpc/PodLib.sol b/pod/mpc/PodLib.sol new file mode 100644 index 0000000..9ed6250 --- /dev/null +++ b/pod/mpc/PodLib.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import "./PodLib64.sol"; +import "./PodLib128.sol"; +import "./PodLib256.sol"; + +/// @title PodLib +/// @notice Combined 64-, 128-, and 256-bit POD MPC helpers (linearized via {PodLib64}, {PodLib128}, {PodLib256}). +abstract contract PodLib is PodLib64, PodLib128, PodLib256 {} diff --git a/pod/mpc/PodLib128.sol b/pod/mpc/PodLib128.sol new file mode 100644 index 0000000..87d09e9 --- /dev/null +++ b/pod/mpc/PodLib128.sol @@ -0,0 +1,361 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import "../../utils/mpc/MpcCore.sol"; + +import "../IInbox.sol"; +import "../mpccodec/MpcAbiCodec.sol"; +import "./coti-side/IPodExecutorOps.sol"; +import "./PodLibBase.sol"; + +/// @title PodLib128 +/// @notice 128-bit POD MPC helpers (`itUint128` / `ctUint128`). +abstract contract PodLib128 is PodLibBase { + using MpcAbiCodec for MpcAbiCodec.MpcMethodCallContext; + + function add128( + itUint128 memory a, + itUint128 memory b, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _sendThree128(IPodExecutor128.add128.selector, a, b, cOwner, callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei); + } + + function sub128( + itUint128 memory a, + itUint128 memory b, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _sendThree128(IPodExecutor128.sub128.selector, a, b, cOwner, callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei); + } + + function mul128( + itUint128 memory a, + itUint128 memory b, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _sendThree128(IPodExecutor128.mul128.selector, a, b, cOwner, callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei); + } + + /// @notice Wrapping multiply modulo 2^128. Use only when downstream logic is defined over uint128 modular arithmetic. + function mulWrapping128( + itUint128 memory a, + itUint128 memory b, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _sendThree128(IPodExecutor128.mulWrapping128.selector, a, b, cOwner, callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei); + } + + function and128( + itUint128 memory a, + itUint128 memory b, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _sendThree128(IPodExecutor128.and128.selector, a, b, cOwner, callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei); + } + + function or128( + itUint128 memory a, + itUint128 memory b, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _sendThree128(IPodExecutor128.or128.selector, a, b, cOwner, callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei); + } + + function xor128( + itUint128 memory a, + itUint128 memory b, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _sendThree128(IPodExecutor128.xor128.selector, a, b, cOwner, callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei); + } + + function min128( + itUint128 memory a, + itUint128 memory b, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _sendThree128(IPodExecutor128.min128.selector, a, b, cOwner, callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei); + } + + function max128( + itUint128 memory a, + itUint128 memory b, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _sendThree128(IPodExecutor128.max128.selector, a, b, cOwner, callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei); + } + + function eq128( + itUint128 memory a, + itUint128 memory b, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _sendThree128(IPodExecutor128.eq128.selector, a, b, cOwner, callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei); + } + + function ne128( + itUint128 memory a, + itUint128 memory b, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _sendThree128(IPodExecutor128.ne128.selector, a, b, cOwner, callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei); + } + + function ge128( + itUint128 memory a, + itUint128 memory b, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _sendThree128(IPodExecutor128.ge128.selector, a, b, cOwner, callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei); + } + + function gt128( + itUint128 memory a, + itUint128 memory b, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _sendThree128(IPodExecutor128.gt128.selector, a, b, cOwner, callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei); + } + + function le128( + itUint128 memory a, + itUint128 memory b, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _sendThree128(IPodExecutor128.le128.selector, a, b, cOwner, callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei); + } + + function lt128( + itUint128 memory a, + itUint128 memory b, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _sendThree128(IPodExecutor128.lt128.selector, a, b, cOwner, callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei); + } + + function mux128( + itBool memory bit, + itUint128 memory a, + itUint128 memory b, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _forwardTwoWay( + _buildMpcMux128(bit, a, b, cOwner), + callbackSelector, + errorSelector, + totalValueWei, + callbackFeeLocalWei + ); + } + + function _buildMpcMux128( + itBool memory bit, + itUint128 memory a, + itUint128 memory b, + address cOwner + ) private pure returns (IInbox.MpcMethodCall memory) { + return MpcAbiCodec.create(IPodExecutor128.mux128.selector, 4) + .addArgument(bit) + .addArgument(a) + .addArgument(b) + .addArgument(cOwner) + .build(); + } + + function shl128( + itUint128 memory a, + uint8 s, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _sendShift128( + IPodExecutor128.shl128.selector, a, s, cOwner, callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei + ); + } + + function shr128( + itUint128 memory a, + uint8 s, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _sendShift128( + IPodExecutor128.shr128.selector, a, s, cOwner, callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei + ); + } + + /// @dev Callback `data` is `abi.encode(uint256)` plaintext (executor decrypts MPC rand on COTI). + function rand128( + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _forwardTwoWay( + _buildMpcRand128(cOwner), callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei + ); + } + + /// @dev Callback `data` is `abi.encode(uint256)` plaintext. + function randBoundedBits128( + uint8 numBits, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _forwardTwoWay( + _buildMpcRandBoundedBits128(numBits, cOwner), + callbackSelector, + errorSelector, + totalValueWei, + callbackFeeLocalWei + ); + } + + function _buildMpcRand128(address cOwner) private pure returns (IInbox.MpcMethodCall memory) { + return MpcAbiCodec.create(IPodExecutor128.rand128.selector, 1).addArgument(cOwner).build(); + } + + function _buildMpcRandBoundedBits128(uint8 numBits, address cOwner) + private + pure + returns (IInbox.MpcMethodCall memory) + { + return MpcAbiCodec.create(IPodExecutor128.randBoundedBits128.selector, 2) + .addArgument(uint256(uint8(numBits))) + .addArgument(cOwner) + .build(); + } + + function _buildMpcThree128(bytes4 selector, itUint128 memory a, itUint128 memory b, address cOwner) + private + pure + returns (IInbox.MpcMethodCall memory) + { + return MpcAbiCodec.create(selector, 3).addArgument(a).addArgument(b).addArgument(cOwner).build(); + } + + function _buildMpcShift128(bytes4 selector, itUint128 memory a, uint8 s, address cOwner) + private + pure + returns (IInbox.MpcMethodCall memory) + { + return MpcAbiCodec.create(selector, 3) + .addArgument(a) + .addArgument(uint256(uint8(s))) + .addArgument(cOwner) + .build(); + } + + function _sendThree128( + bytes4 selector, + itUint128 memory a, + itUint128 memory b, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) private returns (bytes32) { + return _forwardTwoWay( + _buildMpcThree128(selector, a, b, cOwner), + callbackSelector, + errorSelector, + totalValueWei, + callbackFeeLocalWei + ); + } + + function _sendShift128( + bytes4 selector, + itUint128 memory a, + uint8 s, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) private returns (bytes32) { + return _forwardTwoWay( + _buildMpcShift128(selector, a, s, cOwner), + callbackSelector, + errorSelector, + totalValueWei, + callbackFeeLocalWei + ); + } +} diff --git a/pod/mpc/PodLib256.sol b/pod/mpc/PodLib256.sol new file mode 100644 index 0000000..241de00 --- /dev/null +++ b/pod/mpc/PodLib256.sol @@ -0,0 +1,361 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import "../../utils/mpc/MpcCore.sol"; + +import "../IInbox.sol"; +import "../mpccodec/MpcAbiCodec.sol"; +import "./coti-side/IPodExecutorOps.sol"; +import "./PodLibBase.sol"; + +/// @title PodLib256 +/// @notice 256-bit POD MPC helpers (`itUint256` / `ctUint256`). +abstract contract PodLib256 is PodLibBase { + using MpcAbiCodec for MpcAbiCodec.MpcMethodCallContext; + + function add256( + itUint256 memory a, + itUint256 memory b, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _sendThree256(IPodExecutor256.add256.selector, a, b, cOwner, callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei); + } + + function sub256( + itUint256 memory a, + itUint256 memory b, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _sendThree256(IPodExecutor256.sub256.selector, a, b, cOwner, callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei); + } + + function mul256( + itUint256 memory a, + itUint256 memory b, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _sendThree256(IPodExecutor256.mul256.selector, a, b, cOwner, callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei); + } + + /// @notice Wrapping multiply modulo 2^256. Use only when downstream logic is defined over uint256 modular arithmetic. + function mulWrapping256( + itUint256 memory a, + itUint256 memory b, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _sendThree256(IPodExecutor256.mulWrapping256.selector, a, b, cOwner, callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei); + } + + function and256( + itUint256 memory a, + itUint256 memory b, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _sendThree256(IPodExecutor256.and256.selector, a, b, cOwner, callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei); + } + + function or256( + itUint256 memory a, + itUint256 memory b, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _sendThree256(IPodExecutor256.or256.selector, a, b, cOwner, callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei); + } + + function xor256( + itUint256 memory a, + itUint256 memory b, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _sendThree256(IPodExecutor256.xor256.selector, a, b, cOwner, callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei); + } + + function min256( + itUint256 memory a, + itUint256 memory b, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _sendThree256(IPodExecutor256.min256.selector, a, b, cOwner, callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei); + } + + function max256( + itUint256 memory a, + itUint256 memory b, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _sendThree256(IPodExecutor256.max256.selector, a, b, cOwner, callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei); + } + + function eq256( + itUint256 memory a, + itUint256 memory b, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _sendThree256(IPodExecutor256.eq256.selector, a, b, cOwner, callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei); + } + + function ne256( + itUint256 memory a, + itUint256 memory b, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _sendThree256(IPodExecutor256.ne256.selector, a, b, cOwner, callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei); + } + + function ge256( + itUint256 memory a, + itUint256 memory b, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _sendThree256(IPodExecutor256.ge256.selector, a, b, cOwner, callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei); + } + + function gt256( + itUint256 memory a, + itUint256 memory b, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _sendThree256(IPodExecutor256.gt256.selector, a, b, cOwner, callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei); + } + + function le256( + itUint256 memory a, + itUint256 memory b, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _sendThree256(IPodExecutor256.le256.selector, a, b, cOwner, callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei); + } + + function lt256( + itUint256 memory a, + itUint256 memory b, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _sendThree256(IPodExecutor256.lt256.selector, a, b, cOwner, callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei); + } + + function mux256( + itBool memory bit, + itUint256 memory a, + itUint256 memory b, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _forwardTwoWay( + _buildMpcMux256(bit, a, b, cOwner), + callbackSelector, + errorSelector, + totalValueWei, + callbackFeeLocalWei + ); + } + + function _buildMpcMux256( + itBool memory bit, + itUint256 memory a, + itUint256 memory b, + address cOwner + ) private pure returns (IInbox.MpcMethodCall memory) { + return MpcAbiCodec.create(IPodExecutor256.mux256.selector, 4) + .addArgument(bit) + .addArgument(a) + .addArgument(b) + .addArgument(cOwner) + .build(); + } + + function shl256( + itUint256 memory a, + uint8 s, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _sendShift256( + IPodExecutor256.shl256.selector, a, s, cOwner, callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei + ); + } + + function shr256( + itUint256 memory a, + uint8 s, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _sendShift256( + IPodExecutor256.shr256.selector, a, s, cOwner, callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei + ); + } + + /// @dev Callback `data` is `abi.encode(uint256)` plaintext (executor decrypts MPC rand on COTI). + function rand256( + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _forwardTwoWay( + _buildMpcRand256(cOwner), callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei + ); + } + + /// @dev Callback `data` is `abi.encode(uint256)` plaintext. + function randBoundedBits256( + uint8 numBits, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _forwardTwoWay( + _buildMpcRandBoundedBits256(numBits, cOwner), + callbackSelector, + errorSelector, + totalValueWei, + callbackFeeLocalWei + ); + } + + function _buildMpcRand256(address cOwner) private pure returns (IInbox.MpcMethodCall memory) { + return MpcAbiCodec.create(IPodExecutor256.rand256.selector, 1).addArgument(cOwner).build(); + } + + function _buildMpcRandBoundedBits256(uint8 numBits, address cOwner) + private + pure + returns (IInbox.MpcMethodCall memory) + { + return MpcAbiCodec.create(IPodExecutor256.randBoundedBits256.selector, 2) + .addArgument(uint256(uint8(numBits))) + .addArgument(cOwner) + .build(); + } + + function _buildMpcThree256(bytes4 selector, itUint256 memory a, itUint256 memory b, address cOwner) + private + pure + returns (IInbox.MpcMethodCall memory) + { + return MpcAbiCodec.create(selector, 3).addArgument(a).addArgument(b).addArgument(cOwner).build(); + } + + function _buildMpcShift256(bytes4 selector, itUint256 memory a, uint8 s, address cOwner) + private + pure + returns (IInbox.MpcMethodCall memory) + { + return MpcAbiCodec.create(selector, 3) + .addArgument(a) + .addArgument(uint256(uint8(s))) + .addArgument(cOwner) + .build(); + } + + function _sendThree256( + bytes4 selector, + itUint256 memory a, + itUint256 memory b, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) private returns (bytes32) { + return _forwardTwoWay( + _buildMpcThree256(selector, a, b, cOwner), + callbackSelector, + errorSelector, + totalValueWei, + callbackFeeLocalWei + ); + } + + function _sendShift256( + bytes4 selector, + itUint256 memory a, + uint8 s, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) private returns (bytes32) { + return _forwardTwoWay( + _buildMpcShift256(selector, a, s, cOwner), + callbackSelector, + errorSelector, + totalValueWei, + callbackFeeLocalWei + ); + } +} diff --git a/pod/mpc/PodLib64.sol b/pod/mpc/PodLib64.sol new file mode 100644 index 0000000..9142cfa --- /dev/null +++ b/pod/mpc/PodLib64.sol @@ -0,0 +1,385 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import "../../utils/mpc/MpcCore.sol"; + +import "../IInbox.sol"; +import "../mpccodec/MpcAbiCodec.sol"; +import "./coti-side/IPodExecutorOps.sol"; +import "./PodLibBase.sol"; + +/// @title PodLib64 +/// @notice 64-bit POD MPC helpers (`itUint64` / `ctUint64`) and comparisons to `ctBool`. +abstract contract PodLib64 is PodLibBase { + using MpcAbiCodec for MpcAbiCodec.MpcMethodCallContext; + + function add64( + itUint64 memory a, + itUint64 memory b, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _sendThree(IPodExecutor64.add64.selector, a, b, cOwner, callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei); + } + + function gt64( + itUint64 memory a, + itUint64 memory b, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _sendThree(IPodExecutor64.gt64.selector, a, b, cOwner, callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei); + } + + function sub64( + itUint64 memory a, + itUint64 memory b, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _sendThree(IPodExecutor64.sub64.selector, a, b, cOwner, callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei); + } + + function mul64( + itUint64 memory a, + itUint64 memory b, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _sendThree(IPodExecutor64.mul64.selector, a, b, cOwner, callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei); + } + + /// @notice Wrapping multiply modulo 2^64. Use only when downstream logic is defined over uint64 modular arithmetic. + function mulWrapping64( + itUint64 memory a, + itUint64 memory b, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _sendThree(IPodExecutor64.mulWrapping64.selector, a, b, cOwner, callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei); + } + + function div64( + itUint64 memory a, + itUint64 memory b, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _sendThree(IPodExecutor64.div64.selector, a, b, cOwner, callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei); + } + + function rem64( + itUint64 memory a, + itUint64 memory b, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _sendThree(IPodExecutor64.rem64.selector, a, b, cOwner, callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei); + } + + function and64( + itUint64 memory a, + itUint64 memory b, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _sendThree(IPodExecutor64.and64.selector, a, b, cOwner, callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei); + } + + function or64( + itUint64 memory a, + itUint64 memory b, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _sendThree(IPodExecutor64.or64.selector, a, b, cOwner, callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei); + } + + function xor64( + itUint64 memory a, + itUint64 memory b, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _sendThree(IPodExecutor64.xor64.selector, a, b, cOwner, callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei); + } + + function min64( + itUint64 memory a, + itUint64 memory b, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _sendThree(IPodExecutor64.min64.selector, a, b, cOwner, callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei); + } + + function max64( + itUint64 memory a, + itUint64 memory b, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _sendThree(IPodExecutor64.max64.selector, a, b, cOwner, callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei); + } + + function eq64( + itUint64 memory a, + itUint64 memory b, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _sendThree(IPodExecutor64.eq64.selector, a, b, cOwner, callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei); + } + + function ne64( + itUint64 memory a, + itUint64 memory b, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _sendThree(IPodExecutor64.ne64.selector, a, b, cOwner, callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei); + } + + function ge64( + itUint64 memory a, + itUint64 memory b, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _sendThree(IPodExecutor64.ge64.selector, a, b, cOwner, callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei); + } + + function le64( + itUint64 memory a, + itUint64 memory b, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _sendThree(IPodExecutor64.le64.selector, a, b, cOwner, callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei); + } + + function lt64( + itUint64 memory a, + itUint64 memory b, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _sendThree(IPodExecutor64.lt64.selector, a, b, cOwner, callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei); + } + + function mux64( + itBool memory bit, + itUint64 memory a, + itUint64 memory b, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _forwardTwoWay( + _buildMpcMux64(bit, a, b, cOwner), + callbackSelector, + errorSelector, + totalValueWei, + callbackFeeLocalWei + ); + } + + function _buildMpcMux64( + itBool memory bit, + itUint64 memory a, + itUint64 memory b, + address cOwner + ) private pure returns (IInbox.MpcMethodCall memory) { + return MpcAbiCodec.create(IPodExecutor64.mux64.selector, 4) + .addArgument(bit) + .addArgument(a) + .addArgument(b) + .addArgument(cOwner) + .build(); + } + + function shl64( + itUint64 memory a, + uint8 s, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _sendShift( + IPodExecutor64.shl64.selector, a, s, cOwner, callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei + ); + } + + function shr64( + itUint64 memory a, + uint8 s, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _sendShift( + IPodExecutor64.shr64.selector, a, s, cOwner, callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei + ); + } + + /// @dev Callback `data` is `abi.encode(uint256)` plaintext (executor decrypts MPC rand on COTI). + function rand64( + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _forwardTwoWay( + _buildMpcRand64(cOwner), callbackSelector, errorSelector, totalValueWei, callbackFeeLocalWei + ); + } + + function _buildMpcRand64(address cOwner) private pure returns (IInbox.MpcMethodCall memory) { + return MpcAbiCodec.create(IPodExecutor64.rand64.selector, 1).addArgument(cOwner).build(); + } + + /// @dev Callback `data` is `abi.encode(uint256)` plaintext. + function randBoundedBits64( + uint8 numBits, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _forwardTwoWay( + _buildMpcRandBoundedBits64(numBits, cOwner), + callbackSelector, + errorSelector, + totalValueWei, + callbackFeeLocalWei + ); + } + + function _buildMpcRandBoundedBits64(uint8 numBits, address cOwner) + private + pure + returns (IInbox.MpcMethodCall memory) + { + return MpcAbiCodec.create(IPodExecutor64.randBoundedBits64.selector, 2) + .addArgument(uint256(uint8(numBits))) + .addArgument(cOwner) + .build(); + } + + function _buildMpcThree64(bytes4 selector, itUint64 memory a, itUint64 memory b, address cOwner) + private + pure + returns (IInbox.MpcMethodCall memory) + { + return MpcAbiCodec.create(selector, 3).addArgument(a).addArgument(b).addArgument(cOwner).build(); + } + + function _buildMpcShift64(bytes4 selector, itUint64 memory a, uint8 s, address cOwner) + private + pure + returns (IInbox.MpcMethodCall memory) + { + return MpcAbiCodec.create(selector, 3) + .addArgument(a) + .addArgument(uint256(uint8(s))) + .addArgument(cOwner) + .build(); + } + + function _sendThree( + bytes4 selector, + itUint64 memory a, + itUint64 memory b, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) private returns (bytes32) { + return _forwardTwoWay( + _buildMpcThree64(selector, a, b, cOwner), + callbackSelector, + errorSelector, + totalValueWei, + callbackFeeLocalWei + ); + } + + function _sendShift( + bytes4 selector, + itUint64 memory a, + uint8 s, + address cOwner, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) private returns (bytes32) { + return _forwardTwoWay( + _buildMpcShift64(selector, a, s, cOwner), + callbackSelector, + errorSelector, + totalValueWei, + callbackFeeLocalWei + ); + } +} diff --git a/pod/mpc/PodLibBase.sol b/pod/mpc/PodLibBase.sol new file mode 100644 index 0000000..f161b3e --- /dev/null +++ b/pod/mpc/PodLibBase.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import "../../utils/mpc/MpcCore.sol"; + +import "../IInbox.sol"; +import "../mpccodec/MpcAbiCodec.sol"; +import "./PodUser.sol"; + +/// @title PodLibBase +/// @notice Shared POD helpers: fee-aware two-way sends and default outbox error surfacing. +/// @dev Callers supply `totalValueWei` and `callbackFeeLocalWei`; the inbox splits into gas units. Not responsible for `tx.gasprice`. +abstract contract PodLibBase is PodUser { + using MpcAbiCodec for MpcAbiCodec.MpcMethodCallContext; + + constructor(address initialOwner) PodUser(initialOwner) {} + + /// @dev Minimum callback slice in wei; inbox applies stricter policy from {InboxFeeManager}. + uint256 internal constant MIN_CALLBACK_FEE_WEI = 1; + + /// @notice Accept native funds that can subsidize future inbox fees paid by inherited Pod helper methods. + /// @dev `_sendTwoWayWithFee` checks contract balance, so callers may pre-fund a Pod app and later spend that balance. + receive() external payable {} + + /// @param totalValueWei Total native payment forwarded to `sendTwoWayMessage` (e.g. `msg.value`). + /// @param callbackFeeLocalWei Portion of that total reserved for the callback leg; caller-estimated. + function _sendTwoWayWithFee( + uint256 totalValueWei, + uint256 callbackFeeLocalWei, + uint256 targetChainId_, + address targetContract_, + IInbox.MpcMethodCall memory mpcMethodCall, + bytes4 callbackSelector_, + bytes4 errorSelector_ + ) internal returns (bytes32) { + require(callbackFeeLocalWei >= MIN_CALLBACK_FEE_WEI, "PodLib: callback fee min"); + require(callbackFeeLocalWei <= totalValueWei, "PodLib: callback exceeds total"); + require(address(this).balance >= totalValueWei, "PodLib: inbox fee"); + return IInbox(inbox).sendTwoWayMessage{value: totalValueWei}( + targetChainId_, + targetContract_, + mpcMethodCall, + callbackSelector_, + errorSelector_, + callbackFeeLocalWei + ); + } + + /// @dev Splits stack between building `mpc` and forwarding; avoids "stack too deep" in codec helpers. + function _forwardTwoWay( + IInbox.MpcMethodCall memory mpc, + bytes4 callbackSelector, + bytes4 errorSelector, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32) { + return _sendTwoWayWithFee( + totalValueWei, + callbackFeeLocalWei, + cotiChainId, + mpcExecutorAddress, + mpc, + callbackSelector, + errorSelector + ); + } + + function onDefaultMpcError(bytes32 requestId) external onlyInbox { + (uint256 code, string memory message) = inbox.getOutboxError(requestId); + emit ErrorRemoteCall(requestId, code, message); + } +} diff --git a/pod/mpc/PodUser.sol b/pod/mpc/PodUser.sol new file mode 100644 index 0000000..f834220 --- /dev/null +++ b/pod/mpc/PodUser.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import "@openzeppelin/contracts/access/Ownable.sol"; + +import "../IInbox.sol"; +import "../InboxUser.sol"; + +/// @title PodUser +/// @notice POD base: COTI chain ID, MPC executor address, and owner-gated {configure}. +abstract contract PodUser is InboxUser, Ownable { + /// @notice Default MPC error callback surfaced from the outbox. + event ErrorRemoteCall(bytes32 requestId, uint256 code, string message); + + /// @notice COTI-side MPC executor target. + address internal mpcExecutorAddress = 0x0000000000000000000000000000000000000000; + /// @notice COTI chain id used for MPC requests. + uint256 internal cotiChainId = 2632500; + + /// @param initialOwner Initial owner for configuration. + constructor(address initialOwner) Ownable(initialOwner) {} + + /// @dev Internal COTI routing; use {configure} from outside this contract. + /// @param _mpcExecutorAddress COTI-side MPC executor address. + /// @param _cotiChainId COTI chain id. + function configureCoti(address _mpcExecutorAddress, uint256 _cotiChainId) internal virtual { + mpcExecutorAddress = _mpcExecutorAddress; + cotiChainId = _cotiChainId; + } + + /// @notice Owner-only: set inbox when `inbox_ != address(0)`; always updates COTI executor and chain id. + /// @param inbox_ New inbox address, or zero to leave the existing inbox unchanged. + /// @param mpcExecutor_ New COTI-side MPC executor address. + /// @param cotiChainId_ New COTI chain id. + function configure(address inbox_, address mpcExecutor_, uint256 cotiChainId_) external onlyOwner { + if (inbox_ != address(0)) { + setInbox(inbox_); + } + configureCoti(mpcExecutor_, cotiChainId_); + } +} diff --git a/pod/mpc/PodUserFuji.sol b/pod/mpc/PodUserFuji.sol new file mode 100644 index 0000000..dc5c19c --- /dev/null +++ b/pod/mpc/PodUserFuji.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import "./PodUser.sol"; +import "../PodNetworkConstants.sol"; + +/// @title PodUserFuji +/// @notice PoD base for Avalanche Fuji source-chain dApps paired with COTI testnet. +abstract contract PodUserFuji is PodUser { + constructor() { + setInbox(PodNetworkConstants.AVALANCHE_FUJI_INBOX); + configureCoti(PodNetworkConstants.COTI_TESTNET_MPC_EXECUTOR, PodNetworkConstants.COTI_TESTNET_CHAIN_ID); + } +} diff --git a/pod/mpc/PodUserSepolia.sol b/pod/mpc/PodUserSepolia.sol new file mode 100644 index 0000000..7ca4c95 --- /dev/null +++ b/pod/mpc/PodUserSepolia.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import "./PodUser.sol"; +import "../PodNetworkConstants.sol"; + +/// @title PodUserSepolia +abstract contract PodUserSepolia is PodUser { + constructor() { + setInbox(PodNetworkConstants.SEPOLIA_INBOX); + configureCoti(PodNetworkConstants.COTI_TESTNET_MPC_EXECUTOR, PodNetworkConstants.COTI_TESTNET_CHAIN_ID); + } +} diff --git a/pod/mpc/coti-side/IPodExecutorOps.sol b/pod/mpc/coti-side/IPodExecutorOps.sol new file mode 100644 index 0000000..57fc971 --- /dev/null +++ b/pod/mpc/coti-side/IPodExecutorOps.sol @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import "../../../utils/mpc/MpcCore.sol"; + +/// @title IPodExecutorOps +/// @notice COTI-side MPC executor interfaces: 64-, 128-, and 256-bit operations with distinct selectors for {PodLib} dispatch. + +/// @notice 64-bit executor ops (distinct names for unambiguous `.selector` use in {PodLib64}). +interface IPodExecutor64 { + function add64(gtUint64 a, gtUint64 b, address cOwner) external; + function sub64(gtUint64 a, gtUint64 b, address cOwner) external; + /// @notice Checked multiplication. Reverts when the true product does not fit in uint64. + function mul64(gtUint64 a, gtUint64 b, address cOwner) external; + /// @notice Wrapping multiplication modulo 2^64. Use only when modulo arithmetic is an explicit invariant. + function mulWrapping64(gtUint64 a, gtUint64 b, address cOwner) external; + function div64(gtUint64 a, gtUint64 b, address cOwner) external; + function rem64(gtUint64 a, gtUint64 b, address cOwner) external; + function and64(gtUint64 a, gtUint64 b, address cOwner) external; + function or64(gtUint64 a, gtUint64 b, address cOwner) external; + function xor64(gtUint64 a, gtUint64 b, address cOwner) external; + function min64(gtUint64 a, gtUint64 b, address cOwner) external; + function max64(gtUint64 a, gtUint64 b, address cOwner) external; + function eq64(gtUint64 a, gtUint64 b, address cOwner) external; + function ne64(gtUint64 a, gtUint64 b, address cOwner) external; + function ge64(gtUint64 a, gtUint64 b, address cOwner) external; + function gt64(gtUint64 a, gtUint64 b, address cOwner) external; + function le64(gtUint64 a, gtUint64 b, address cOwner) external; + function lt64(gtUint64 a, gtUint64 b, address cOwner) external; + function mux64(gtBool bit, gtUint64 a, gtUint64 b, address cOwner) external; + function shl64(gtUint64 a, uint8 s, address cOwner) external; + function shr64(gtUint64 a, uint8 s, address cOwner) external; + + /// @notice Plaintext random; inbox payload is `abi.encode(uint256)` (not `ctUint64`). + function rand64(address cOwner) external; + + /// @notice Plaintext random; inbox payload is `abi.encode(uint256)` (not `ctUint64`). + function randBoundedBits64(uint8 numBits, address cOwner) external; +} + +/// @notice 128-bit executor ops. +interface IPodExecutor128 { + function add128(gtUint128 a, gtUint128 b, address cOwner) external; + function sub128(gtUint128 a, gtUint128 b, address cOwner) external; + /// @notice Checked multiplication. Reverts when the true product does not fit in uint128. + function mul128(gtUint128 a, gtUint128 b, address cOwner) external; + /// @notice Wrapping multiplication modulo 2^128. Use only when modulo arithmetic is an explicit invariant. + function mulWrapping128(gtUint128 a, gtUint128 b, address cOwner) external; + function and128(gtUint128 a, gtUint128 b, address cOwner) external; + function or128(gtUint128 a, gtUint128 b, address cOwner) external; + function xor128(gtUint128 a, gtUint128 b, address cOwner) external; + function min128(gtUint128 a, gtUint128 b, address cOwner) external; + function max128(gtUint128 a, gtUint128 b, address cOwner) external; + function eq128(gtUint128 a, gtUint128 b, address cOwner) external; + function ne128(gtUint128 a, gtUint128 b, address cOwner) external; + function ge128(gtUint128 a, gtUint128 b, address cOwner) external; + function gt128(gtUint128 a, gtUint128 b, address cOwner) external; + function le128(gtUint128 a, gtUint128 b, address cOwner) external; + function lt128(gtUint128 a, gtUint128 b, address cOwner) external; + function mux128(gtBool bit, gtUint128 a, gtUint128 b, address cOwner) external; + function shl128(gtUint128 a, uint8 s, address cOwner) external; + function shr128(gtUint128 a, uint8 s, address cOwner) external; + + /// @notice Plaintext random; inbox payload is `abi.encode(uint256)` (not `ctUint128`). + function rand128(address cOwner) external; + + /// @notice Plaintext random; inbox payload is `abi.encode(uint256)` (not `ctUint128`). + function randBoundedBits128(uint8 numBits, address cOwner) external; +} + +/// @notice 256-bit executor ops. +interface IPodExecutor256 { + function add256(gtUint256 a, gtUint256 b, address cOwner) external; + function sub256(gtUint256 a, gtUint256 b, address cOwner) external; + /// @notice Checked multiplication. Reverts when the true product does not fit in uint256. + function mul256(gtUint256 a, gtUint256 b, address cOwner) external; + /// @notice Wrapping multiplication modulo 2^256. Use only when modulo arithmetic is an explicit invariant. + function mulWrapping256(gtUint256 a, gtUint256 b, address cOwner) external; + function and256(gtUint256 a, gtUint256 b, address cOwner) external; + function or256(gtUint256 a, gtUint256 b, address cOwner) external; + function xor256(gtUint256 a, gtUint256 b, address cOwner) external; + function min256(gtUint256 a, gtUint256 b, address cOwner) external; + function max256(gtUint256 a, gtUint256 b, address cOwner) external; + function eq256(gtUint256 a, gtUint256 b, address cOwner) external; + function ne256(gtUint256 a, gtUint256 b, address cOwner) external; + function ge256(gtUint256 a, gtUint256 b, address cOwner) external; + function gt256(gtUint256 a, gtUint256 b, address cOwner) external; + function le256(gtUint256 a, gtUint256 b, address cOwner) external; + function lt256(gtUint256 a, gtUint256 b, address cOwner) external; + function mux256(gtBool bit, gtUint256 a, gtUint256 b, address cOwner) external; + function shl256(gtUint256 a, uint8 s, address cOwner) external; + function shr256(gtUint256 a, uint8 s, address cOwner) external; + + /// @notice Plaintext random; inbox payload is `abi.encode(uint256)` (not `ctUint256`). + function rand256(address cOwner) external; + + /// @notice Plaintext random; inbox payload is `abi.encode(uint256)` (not `ctUint256`). + function randBoundedBits256(uint8 numBits, address cOwner) external; +} diff --git a/pod/mpccodec/MpcAbiCodec.sol b/pod/mpccodec/MpcAbiCodec.sol new file mode 100644 index 0000000..44af14d --- /dev/null +++ b/pod/mpccodec/MpcAbiCodec.sol @@ -0,0 +1,482 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import "../../utils/mpc/MpcCore.sol"; + +import "../IInbox.sol"; + +/// @title MpcAbiCodec +/// @notice Library to build and re-encode {IInbox.MpcMethodCall} payloads for MPC and ABI dispatch. +/// @dev Maps it-* wire types to gt-* calldata expected by COTI-side executors. +library MpcAbiCodec { + enum MpcDataType { + UINT256, + ADDRESS, // include other system types for coded + BYTES32, + STRING, + BYTES, + UINT256_ARRAY, + ADDRESS_ARRAY, + BYTES32_ARRAY, + STRING_ARRAY, + BYTES_ARRAY, + IT_BOOL, + IT_UINT8, + IT_UINT16, + IT_UINT32, + IT_UINT64, + IT_UINT128, + IT_UINT256, + IT_STRING + } + + event ValidateCiphertextStart(uint8 dataType, uint256 argLen, bytes32 argHash); + event ValidateCiphertextSuccess(uint8 dataType); + + struct MpcMethodCallContext { + IInbox.MpcMethodCall mpcMethodCall; + bytes[] data; + uint256 dataSize; + uint256 argIndex; + } + + /// @notice Create a method call context with selector and argument count. + /// @param selector The method selector to call on the target contract. + /// @param argCount The number of arguments expected in the call. + /// @return context The initialized method call context. + function create(bytes4 selector, uint256 argCount) internal pure returns (MpcMethodCallContext memory) { + return MpcMethodCallContext({ + mpcMethodCall: IInbox.MpcMethodCall({ + selector: selector, + data: new bytes(0), + datatypes: new bytes8[](argCount), + datalens: new bytes32[](argCount) + }), + data: new bytes[](argCount), + dataSize: 0, + argIndex: 0 + }); + } + + /** + * @notice Add an argument to the method call context + * @param methodCall The method call + * @param arg the argument to add + * @return The updated method call + */ + function addArgument(MpcMethodCallContext memory methodCall, uint256 arg) + internal pure returns (MpcMethodCallContext memory) + { + return _appendArgument(methodCall, abi.encode(arg), MpcDataType.UINT256); + } + + /// @notice Add an address argument to the method call context. + /// @param methodCall The method call context. + /// @param arg The address argument to add. + /// @return The updated method call context. + function addArgument(MpcMethodCallContext memory methodCall, address arg) + internal pure returns (MpcMethodCallContext memory) { + return _appendArgument(methodCall, abi.encode(arg), MpcDataType.ADDRESS); + } + + /// @notice Add an itUint64 argument to the method call context. + /// @param methodCall The method call context. + /// @param arg The itUint64 argument to add. + /// @return The updated method call context. + function addArgument(MpcMethodCallContext memory methodCall, itUint64 memory arg) + internal pure returns (MpcMethodCallContext memory) { + return _appendArgument(methodCall, abi.encode(arg), MpcDataType.IT_UINT64); + } + + /// @notice Add an itBool argument to the method call context. + /// @param methodCall The method call context. + /// @param arg The itBool argument to add. + /// @return The updated method call context. + function addArgument(MpcMethodCallContext memory methodCall, itBool memory arg) + internal pure returns (MpcMethodCallContext memory) { + return _appendArgument(methodCall, abi.encode(arg), MpcDataType.IT_BOOL); + } + + /// @notice Add an itUint8 argument to the method call context. + /// @param methodCall The method call context. + /// @param arg The itUint8 argument to add. + /// @return The updated method call context. + function addArgument(MpcMethodCallContext memory methodCall, itUint8 memory arg) + internal pure returns (MpcMethodCallContext memory) { + return _appendArgument(methodCall, abi.encode(arg), MpcDataType.IT_UINT8); + } + + /// @notice Add an itUint16 argument to the method call context. + /// @param methodCall The method call context. + /// @param arg The itUint16 argument to add. + /// @return The updated method call context. + function addArgument(MpcMethodCallContext memory methodCall, itUint16 memory arg) + internal pure returns (MpcMethodCallContext memory) { + return _appendArgument(methodCall, abi.encode(arg), MpcDataType.IT_UINT16); + } + + /// @notice Add an itUint32 argument to the method call context. + /// @param methodCall The method call context. + /// @param arg The itUint32 argument to add. + /// @return The updated method call context. + function addArgument(MpcMethodCallContext memory methodCall, itUint32 memory arg) + internal pure returns (MpcMethodCallContext memory) { + return _appendArgument(methodCall, abi.encode(arg), MpcDataType.IT_UINT32); + } + + /// @notice Add an itUint128 argument to the method call context. + /// @param methodCall The method call context. + /// @param arg The itUint128 argument to add. + /// @return The updated method call context. + function addArgument(MpcMethodCallContext memory methodCall, itUint128 memory arg) + internal pure returns (MpcMethodCallContext memory) { + return _appendArgument(methodCall, abi.encode(arg), MpcDataType.IT_UINT128); + } + + /// @notice Add an itUint256 argument to the method call context. + /// @param methodCall The method call context. + /// @param arg The itUint256 argument to add. + /// @return The updated method call context. + function addArgument(MpcMethodCallContext memory methodCall, itUint256 memory arg) + internal pure returns (MpcMethodCallContext memory) { + return _appendArgument(methodCall, abi.encode(arg), MpcDataType.IT_UINT256); + } + + /// @notice Add an itString argument to the method call context. + /// @param methodCall The method call context. + /// @param arg The itString argument to add. + /// @return The updated method call context. + function addArgument(MpcMethodCallContext memory methodCall, itString memory arg) + internal pure returns (MpcMethodCallContext memory) { + return _appendArgument(methodCall, abi.encode(arg), MpcDataType.IT_STRING); + } + + /// @notice Add a bytes32 argument to the method call context. + /// @param methodCall The method call context. + /// @param arg The bytes32 argument to add. + /// @return The updated method call context. + function addArgument(MpcMethodCallContext memory methodCall, bytes32 arg) + internal pure returns (MpcMethodCallContext memory) { + return _appendArgument(methodCall, abi.encode(arg), MpcDataType.BYTES32); + } + + /// @notice Add a string argument to the method call context. + /// @param methodCall The method call context. + /// @param arg The string argument to add. + /// @return The updated method call context. + function addArgument(MpcMethodCallContext memory methodCall, string memory arg) + internal pure returns (MpcMethodCallContext memory) { + return _appendArgument(methodCall, abi.encode(arg), MpcDataType.STRING); + } + + /// @notice Add a bytes argument to the method call context. + /// @param methodCall The method call context. + /// @param arg The bytes argument to add. + /// @return The updated method call context. + function addArgument(MpcMethodCallContext memory methodCall, bytes memory arg) + internal pure returns (MpcMethodCallContext memory) { + return _appendArgument(methodCall, abi.encode(arg), MpcDataType.BYTES); + } + + /// @notice Add a uint256 array argument to the method call context. + /// @param methodCall The method call context. + /// @param arg The uint256[] argument to add. + /// @return The updated method call context. + function addArgument(MpcMethodCallContext memory methodCall, uint256[] memory arg) + internal pure returns (MpcMethodCallContext memory) { + return _appendArgument(methodCall, abi.encode(arg), MpcDataType.UINT256_ARRAY); + } + + /// @notice Add an address array argument to the method call context. + /// @param methodCall The method call context. + /// @param arg The address[] argument to add. + /// @return The updated method call context. + function addArgument(MpcMethodCallContext memory methodCall, address[] memory arg) + internal pure returns (MpcMethodCallContext memory) { + return _appendArgument(methodCall, abi.encode(arg), MpcDataType.ADDRESS_ARRAY); + } + + /// @notice Add a bytes32 array argument to the method call context. + /// @param methodCall The method call context. + /// @param arg The bytes32[] argument to add. + /// @return The updated method call context. + function addArgument(MpcMethodCallContext memory methodCall, bytes32[] memory arg) + internal pure returns (MpcMethodCallContext memory) { + return _appendArgument(methodCall, abi.encode(arg), MpcDataType.BYTES32_ARRAY); + } + + /// @notice Add a string array argument to the method call context. + /// @param methodCall The method call context. + /// @param arg The string[] argument to add. + /// @return The updated method call context. + function addArgument(MpcMethodCallContext memory methodCall, string[] memory arg) + internal pure returns (MpcMethodCallContext memory) { + return _appendArgument(methodCall, abi.encode(arg), MpcDataType.STRING_ARRAY); + } + + /// @notice Add a bytes array argument to the method call context. + /// @param methodCall The method call context. + /// @param arg The bytes[] argument to add. + /// @return The updated method call context. + function addArgument(MpcMethodCallContext memory methodCall, bytes[] memory arg) + internal pure returns (MpcMethodCallContext memory) { + return _appendArgument(methodCall, abi.encode(arg), MpcDataType.BYTES_ARRAY); + } + + /** + * @notice Build the method call from the context by resizing the data + * @param methodCall The method call context + * @return The method call + */ + function build(MpcMethodCallContext memory methodCall) internal pure returns (IInbox.MpcMethodCall memory) { + bytes memory resized = new bytes(methodCall.dataSize); + uint cursor = 0; + for (uint i = 0; i < methodCall.argIndex; i++) { + bytes memory chunk = methodCall.data[i]; + methodCall.mpcMethodCall.datalens[i] = bytes32(chunk.length); + for (uint j = 0; j < chunk.length; j++) { + resized[cursor + j] = chunk[j]; + } + cursor += chunk.length; + } + + methodCall.mpcMethodCall.data = resized; + return methodCall.mpcMethodCall; + } + + /// @notice Re-encode a method call, validating it-* types to gt-* and rebuilding calldata. + /// @param data The method call to re-encode. + /// @return calldataBytes The ABI-encoded calldata with selector prepended. + function reEncodeWithGt(IInbox.MpcMethodCall memory data) internal returns (bytes memory) { + uint argCount = data.datatypes.length; + require(data.datalens.length == argCount, "MpcAbiCodec: invalid datalens"); + bytes memory encodedArgs = data.data; + + bytes[] memory processed = new bytes[](argCount); + bool[] memory isDynamic = new bool[](argCount); + uint[] memory staticWords = new uint[](argCount); + uint totalTailSize = 0; + + uint cursor = 0; + for (uint i = 0; i < argCount; i++) { + uint argLen = uint(uint256(data.datalens[i])); + require(cursor + argLen <= encodedArgs.length, "MpcAbiCodec: arg out of bounds"); + + bytes memory argData = _slice(encodedArgs, cursor, argLen); + cursor += argLen; + + MpcDataType dataType = _decodeType(data.datatypes[i]); + (bytes memory encodedArg, bool dynamicType, uint words) = _normalizeArg(argData, dataType); + processed[i] = encodedArg; + isDynamic[i] = dynamicType; + staticWords[i] = words; + if (dynamicType) { + require(encodedArg.length >= 32, "MpcAbiCodec: invalid dynamic arg"); + totalTailSize += (encodedArg.length - 32); + } else { + require(encodedArg.length == words * 32, "MpcAbiCodec: invalid static arg"); + } + } + require(cursor == encodedArgs.length, "MpcAbiCodec: trailing data"); + + uint headSize = 0; + for (uint i = 0; i < argCount; i++) { + headSize += isDynamic[i] ? 32 : (staticWords[i] * 32); + } + bytes memory recoded = new bytes(4 + headSize + totalTailSize); + bytes4 selector = data.selector; + assembly { + mstore(add(recoded, 32), selector) + } + + uint tailCursor = 0; + uint headCursor = 0; + for (uint i = 0; i < argCount; i++) { + if (isDynamic[i]) { + uint offset = headSize + tailCursor; + _writeWord(recoded, 4 + headCursor, offset); + bytes memory tailData = processed[i]; + uint tailLen = tailData.length - 32; + _copyBytes(recoded, 4 + headSize + tailCursor, tailData, 32, tailLen); + tailCursor += tailLen; + headCursor += 32; + } else { + bytes memory staticData = processed[i]; + _copyBytes(recoded, 4 + headCursor, staticData, 0, staticData.length); + headCursor += staticData.length; + } + } + + return recoded; + } + + /// @dev Append an encoded argument and update type metadata and data size. + function _appendArgument( + MpcMethodCallContext memory methodCallContext, + bytes memory encodedArg, + MpcDataType dataType + ) internal pure returns (MpcMethodCallContext memory) { + require(methodCallContext.argIndex < methodCallContext.mpcMethodCall.datatypes.length, "MpcAbiCodec: too many args"); + + methodCallContext.mpcMethodCall.datatypes[methodCallContext.argIndex] = bytes8(uint64(uint8(dataType))); + methodCallContext.data[methodCallContext.argIndex] = encodedArg; + methodCallContext.dataSize += encodedArg.length; + methodCallContext.argIndex += 1; + return methodCallContext; + } + + /// @dev Read a 32-byte word from a bytes array at the given offset. + function _readUint256(bytes memory data, uint offset) internal pure returns (uint256 value) { + assembly { + value := mload(add(add(data, 32), offset)) + } + } + + /// @dev Write a 32-byte word into a bytes array at the given offset. + function _writeWord(bytes memory data, uint offset, uint256 value) internal pure { + assembly { + mstore(add(add(data, 32), offset), value) + } + } + + /// @dev Copy a slice of bytes into a new array. + function _slice(bytes memory data, uint offset, uint length) internal pure returns (bytes memory result) { + result = new bytes(length); + for (uint i = 0; i < length; i++) { + result[i] = data[offset + i]; + } + } + + /// @dev Decode an on-chain data type identifier into the enum. + function _decodeType(bytes8 dataType) internal pure returns (MpcDataType) { + return MpcDataType(uint8(uint64(dataType))); + } + + /// @dev Normalize an argument to ABI encoding and validate MPC ciphertexts. + function _normalizeArg(bytes memory argData, MpcDataType dataType) + internal returns (bytes memory encodedArg, bool dynamicType, uint staticWordCount) { + if (dataType == MpcDataType.UINT256) { + return (argData, false, 1); + } + + if (dataType == MpcDataType.ADDRESS) { + return (argData, false, 1); + } + + if (dataType == MpcDataType.BYTES32) { + return (argData, false, 1); + } + + if (dataType == MpcDataType.IT_UINT64) { + itUint64 memory itValue = abi.decode(argData, (itUint64)); + emit ValidateCiphertextStart(uint8(dataType), argData.length, keccak256(argData)); + gtUint64 gtValue = MpcCore.validateCiphertext(itValue); + emit ValidateCiphertextSuccess(uint8(dataType)); + return (abi.encode(gtUint64.unwrap(gtValue)), false, 1); + } + + if (dataType == MpcDataType.IT_BOOL) { + itBool memory itValue = abi.decode(argData, (itBool)); + emit ValidateCiphertextStart(uint8(dataType), argData.length, keccak256(argData)); + gtBool gtValue = MpcCore.validateCiphertext(itValue); + emit ValidateCiphertextSuccess(uint8(dataType)); + return (abi.encode(gtBool.unwrap(gtValue)), false, 1); + } + + if (dataType == MpcDataType.IT_UINT8) { + itUint8 memory itValue = abi.decode(argData, (itUint8)); + emit ValidateCiphertextStart(uint8(dataType), argData.length, keccak256(argData)); + gtUint8 gtValue = MpcCore.validateCiphertext(itValue); + emit ValidateCiphertextSuccess(uint8(dataType)); + return (abi.encode(gtUint8.unwrap(gtValue)), false, 1); + } + + if (dataType == MpcDataType.IT_UINT16) { + itUint16 memory itValue = abi.decode(argData, (itUint16)); + emit ValidateCiphertextStart(uint8(dataType), argData.length, keccak256(argData)); + gtUint16 gtValue = MpcCore.validateCiphertext(itValue); + emit ValidateCiphertextSuccess(uint8(dataType)); + return (abi.encode(gtUint16.unwrap(gtValue)), false, 1); + } + + if (dataType == MpcDataType.IT_UINT32) { + itUint32 memory itValue = abi.decode(argData, (itUint32)); + emit ValidateCiphertextStart(uint8(dataType), argData.length, keccak256(argData)); + gtUint32 gtValue = MpcCore.validateCiphertext(itValue); + emit ValidateCiphertextSuccess(uint8(dataType)); + return (abi.encode(gtUint32.unwrap(gtValue)), false, 1); + } + + if (dataType == MpcDataType.IT_UINT128) { + itUint128 memory itValue = abi.decode(argData, (itUint128)); + emit ValidateCiphertextStart(uint8(dataType), argData.length, keccak256(argData)); + gtUint128 gtValue = MpcCore.validateCiphertext(itValue); + emit ValidateCiphertextSuccess(uint8(dataType)); + bytes memory encoded = abi.encode(gtValue); + return (encoded, false, encoded.length / 32); + } + + if (dataType == MpcDataType.IT_UINT256) { + itUint256 memory itValue = abi.decode(argData, (itUint256)); + emit ValidateCiphertextStart(uint8(dataType), argData.length, keccak256(argData)); + gtUint256 gtValue = MpcCore.validateCiphertext(itValue); + emit ValidateCiphertextSuccess(uint8(dataType)); + bytes memory encoded = abi.encode(gtValue); + return (encoded, false, encoded.length / 32); + } + + if (dataType == MpcDataType.IT_STRING) { + itString memory itValue = abi.decode(argData, (itString)); + emit ValidateCiphertextStart(uint8(dataType), argData.length, keccak256(argData)); + gtString memory gtValue = MpcCore.validateCiphertext(itValue); + emit ValidateCiphertextSuccess(uint8(dataType)); + return (abi.encode(gtValue), true, 0); + } + + if ( + dataType == MpcDataType.STRING || + dataType == MpcDataType.BYTES || + dataType == MpcDataType.UINT256_ARRAY || + dataType == MpcDataType.ADDRESS_ARRAY || + dataType == MpcDataType.BYTES32_ARRAY || + dataType == MpcDataType.STRING_ARRAY || + dataType == MpcDataType.BYTES_ARRAY + ) { + return (argData, true, 0); + } + + revert("MpcAbiCodec: unknown type"); + } + + /// @dev Copy bytes from source to destination with word-aligned optimization. + function _copyBytes( + bytes memory dest, + uint destOffset, + bytes memory src, + uint srcOffset, + uint length + ) internal pure { + if (length == 0) { + return; + } + assembly { + let destPtr := add(add(dest, 32), destOffset) + let srcPtr := add(add(src, 32), srcOffset) + + let remaining := length + for { } gt(remaining, 31) { } { + mstore(destPtr, mload(srcPtr)) + destPtr := add(destPtr, 32) + srcPtr := add(srcPtr, 32) + remaining := sub(remaining, 32) + } + + if gt(remaining, 0) { + let mask := sub(shl(mul(remaining, 8), 1), 1) + let srcWord := and(mload(srcPtr), mask) + let destWord := and(mload(destPtr), not(mask)) + mstore(destPtr, or(destWord, srcWord)) + } + } + } +} \ No newline at end of file diff --git a/pod/privacy/IPrivacyPortal.sol b/pod/privacy/IPrivacyPortal.sol new file mode 100644 index 0000000..6fe841f --- /dev/null +++ b/pod/privacy/IPrivacyPortal.sol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +/// @title IPrivacyPortal +/// @notice Source-chain portal interface for locking public ERC20 collateral and minting/burning private pTokens. +/// @dev Public deposit/withdraw amounts are visible in calldata and events. Privacy-sensitive integrations should use encrypted pToken APIs where available after entering the private token system. +interface IPrivacyPortal { + /// @notice Withdrawal lifecycle tracked by the portal. + enum WithdrawalStatus { + /// @notice No withdrawal exists for the id. + None, + /// @notice pToken transfer to the portal has been requested but not yet released. + TransferPending, + /// @notice Underlying collateral was released to the recipient. + Released + } + + /// @notice Withdrawal request state stored by withdrawal id. + struct Withdrawal { + /// @notice User whose pTokens are transferred into portal custody. + address user; + /// @notice Recipient of the public underlying ERC20. + address recipient; + /// @notice Public withdrawal amount. + uint256 amount; + /// @notice Native fee reserved for the pToken burn request after release. + uint256 burnFee; + /// @notice Native fee reserved for the burn callback leg. + uint256 burnCallbackFee; + /// @notice Async pToken transfer request that must succeed before release. + bytes32 transferRequestId; + /// @notice Async pToken burn request submitted after release, if submission succeeded. + bytes32 burnRequestId; + /// @notice Current withdrawal lifecycle status. + WithdrawalStatus status; + } + + /// @notice Initialize a clone portal. + /// @param owner Owner for admin functions. + /// @param underlyingToken Public ERC20 locked and released by this portal (WETH/WAVAX when native-wrapped). + /// @param pToken PoD pToken minted and burned for the underlying token. + /// @param decimals Token decimals exposed for UI compatibility. + /// @param nativeWrappedUnderlying When true, use {depositNative} and unwrap withdrawals to native coin. + function initialize( + address owner, + address underlyingToken, + address pToken, + uint8 decimals, + bool nativeWrappedUnderlying + ) external; + + /// @notice Whether this portal wraps native coin on deposit and unwraps on withdraw. + function nativeWrappedUnderlying() external view returns (bool); + + /// @notice Lock public ERC20 and request a private pToken mint. + /// @param recipient Address receiving private pTokens on successful async mint. + /// @param amount Public amount to deposit; visible on-chain. + /// @param mintCallbackFee Native fee slice for the mint callback. + /// @return requestId Async pToken mint request id. + function deposit(address recipient, uint256 amount, uint256 mintCallbackFee) external payable returns (bytes32 requestId); + + /// @notice Wrap native coin into the underlying WETH/WAVAX, then mint pTokens (single tx). + /// @dev Requires {nativeWrappedUnderlying}. `msg.value` must equal `amount + mintFee` where `mintFee` is + /// forwarded to {IPodERC20.mint} (same as {deposit}'s `msg.value`). + /// @param recipient Address receiving private pTokens on successful async mint. + /// @param amount Native amount to wrap and lock (wei). + /// @param mintCallbackFee Native fee slice for the mint callback. + /// @return requestId Async pToken mint request id. + function depositNative( + address recipient, + uint256 amount, + uint256 mintCallbackFee + ) external payable returns (bytes32 requestId); + + /// @notice Request withdrawal by permitting and transferring pTokens into portal custody, then releasing after async success. + /// @dev `amount`, fees, recipient, and permit data are public. The pToken transfer must reach `RequestStatus.Success` before release. + /// @param recipient Public underlying recipient. + /// @param amount Public amount to withdraw. + /// @param transferFee Native fee paid for the pToken transfer request. + /// @param transferCallbackFee Native fee slice for the pToken transfer callback. + /// @param burnFee Native fee paid for the pToken burn after release. + /// @param burnCallbackFee Native fee slice for the burn callback. + /// @param permitDeadline Permit expiry timestamp. + /// @param v Permit signature v. + /// @param r Permit signature r. + /// @param s Permit signature s. + /// @return withdrawalId Portal withdrawal id. + /// @return transferRequestId Async pToken transfer request id. + function requestWithdrawWithPermit( + address recipient, + uint256 amount, + uint256 transferFee, + uint256 transferCallbackFee, + uint256 burnFee, + uint256 burnCallbackFee, + uint256 permitDeadline, + uint8 v, + bytes32 r, + bytes32 s + ) external payable returns (bytes32 withdrawalId, bytes32 transferRequestId); + + /// @notice Callback entry point used by the pToken after a transfer-to-portal succeeds. + /// @param withdrawalId Withdrawal id encoded into the pToken transfer callback data. + function onPTokenTransferred(bytes32 withdrawalId) external; + + /// @notice Manually release a pending withdrawal after its pToken transfer request is marked successful. + /// @param withdrawalId Withdrawal id to release. + function triggerWithdrawalRelease(bytes32 withdrawalId) external; + + /// @notice Update or disable the external pause controller for deposits and withdrawals. + /// @param pauseController New controller address, or zero to disable pause checks. + function setPauseController(address pauseController) external; + + /// @notice Sweep accidental native-token balance held by the portal. + /// @param recipient Recipient of swept native tokens. + /// @param amount Amount to sweep. + function sweepNative(address payable recipient, uint256 amount) external; +} diff --git a/pod/privacy/PrivacyPortal.sol b/pod/privacy/PrivacyPortal.sol new file mode 100644 index 0000000..909bcd3 --- /dev/null +++ b/pod/privacy/PrivacyPortal.sol @@ -0,0 +1,410 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; + +import "../token/perc20/IPodERC20.sol"; +import "../token/erc7984/IERC7984PortalWrapper.sol"; +import "../utils/IWrappedNative.sol"; +import "./IPrivacyPortal.sol"; + +/// @notice Optional external policy hook for pausing portal operations. +interface IPrivacyPortalPauseController { + /// @notice Whether new withdrawal requests should revert. + function withdrawalsPaused() external view returns (bool); + + /// @notice Whether new deposits / wraps should revert. + function depositsPaused() external view returns (bool); +} + +/// @title PrivacyPortal +/// @notice Locks a public ERC20 and mints/burns its PoD private pToken counterpart. +/// @dev The portal never reads private balances. It only reacts to successful pToken callbacks and records public bridge obligations. +contract PrivacyPortal is IPrivacyPortal, IERC7984PortalWrapper, Ownable, ReentrancyGuard { + using SafeERC20 for IERC20; + + /// @notice Public ERC20 collateral locked by this portal. + IERC20 public underlyingToken; + /// @notice Private pToken minted and burned against the underlying collateral. + IPodERC20 public pToken; + /// @notice Optional pause controller for deposits and withdrawals; zero disables pause checks. + address public pauseController; + /// @notice Token decimals mirrored from the underlying/pToken pair. + uint8 public decimals; + /// @notice When true, {depositNative} wraps native coin and withdrawals unwrap to native. + bool public nativeWrappedUnderlying; + + /// @notice Monotonic nonce used to derive withdrawal ids. + uint256 public withdrawalNonce; + /// @notice Total pToken amount held by the portal that still needs owner-submitted burn cleanup. + uint256 public burnDebtAmount; + + /// @notice Withdrawal state by withdrawal id. + mapping(bytes32 => Withdrawal) public withdrawals; + + /// @notice Public ERC20 was locked and an async private pToken mint was requested. + event DepositRequested( + address indexed user, + address indexed recipient, + uint256 amount, + bytes32 indexed mintRequestId + ); + /// @notice Public withdrawal was requested and a pToken transfer-to-portal request was submitted. + event WithdrawalRequested( + bytes32 indexed withdrawalId, + address indexed user, + address indexed recipient, + uint256 amount, + bytes32 transferRequestId + ); + /// @notice Underlying collateral was released to the withdrawal recipient. + event WithdrawalReleased(bytes32 indexed withdrawalId, address indexed recipient, uint256 amount); + /// @notice A burn request was submitted for pTokens in portal custody after release. + event BurnSubmitted(bytes32 indexed withdrawalId, uint256 amount, bytes32 indexed burnRequestId); + /// @notice Burn submission failed after release, leaving debt for owner cleanup. + event BurnDebtRecorded(bytes32 indexed withdrawalId, uint256 amount, bytes reason); + /// @notice Owner submitted a cleanup burn for accumulated pToken debt. + event BurnDebtSubmitted(address indexed caller, uint256 amount, bytes32 indexed burnRequestId); + /// @notice Pause controller was changed. + event PauseControllerUpdated(address indexed pauseController); + /// @notice Native token balance was swept by the owner. + event NativeSwept(address indexed recipient, uint256 amount); + + /// @notice A required address was zero. + error InvalidAddress(); + /// @notice Amount argument was zero. + error InvalidAmount(); + /// @notice `msg.value` did not match the expected fee. + error IncorrectFee(uint256 expected, uint256 actual); + /// @notice Caller was not the configured pToken. + error OnlyPToken(address caller); + /// @notice Withdrawal id is not known. + error UnknownWithdrawal(bytes32 withdrawalId); + /// @notice Withdrawal is not in the pending-transfer state. + error WithdrawalNotPending(bytes32 withdrawalId, WithdrawalStatus status); + /// @notice Requested burn cleanup exceeds accumulated debt. + error BurnDebtTooLow(uint256 debt, uint256 requested); + /// @notice Clone was already initialized. + error PortalAlreadyInitialized(); + /// @notice Pause controller reports withdrawals are paused. + error WithdrawalsPaused(); + /// @notice Pause controller reports deposits are paused. + error DepositsPaused(); + /// @notice Configured pause controller did not return a valid pause flag. + error PauseControllerFault(); + /// @notice pToken transfer request has not succeeded yet. + error PTokenTransferNotSuccessful(bytes32 requestId, IPodERC20.RequestStatus status); + /// @notice Portal underlying is not configured for native wrap/unwrap. + error NativeWrapDisabled(); + /// @notice Native transfer to the withdrawal recipient failed. + error NativeTransferFailed(); + + /// @notice Lock implementation instance by assigning a non-zero owner placeholder. + constructor() Ownable(address(1)) {} + + /// @notice Accept native funds used only for pToken burn-fee cleanup or accidental recovery. + /// @dev User-facing deposit and withdrawal fees should be passed as `msg.value`; arbitrary native donations can be swept by the owner. + receive() external payable {} + + function initialize( + address owner_, + address underlyingToken_, + address pToken_, + uint8 decimals_, + bool nativeWrappedUnderlying_ + ) external override { + if (address(underlyingToken) != address(0)) { + revert PortalAlreadyInitialized(); + } + if (owner_ == address(0)) { + revert OwnableInvalidOwner(owner_); + } + if (underlyingToken_ == address(0) || pToken_ == address(0)) { + revert InvalidAddress(); + } + _transferOwnership(owner_); + underlyingToken = IERC20(underlyingToken_); + pToken = IPodERC20(pToken_); + decimals = decimals_; + nativeWrappedUnderlying = nativeWrappedUnderlying_; + pauseController = msg.sender; + emit PauseControllerUpdated(msg.sender); + } + + /// @notice Update the external pause controller checked before deposits and withdrawal requests. + /// @dev Set to `address(0)` to disable pause checks. When non-zero, the controller must implement + /// {IPrivacyPortalPauseController}; failed staticcalls revert (fail-closed). + /// @param pauseController_ New controller address, or zero to disable. + function setPauseController(address pauseController_) external onlyOwner { + pauseController = pauseController_; + emit PauseControllerUpdated(pauseController_); + } + + /// @inheritdoc IPrivacyPortal + function deposit( + address recipient, + uint256 amount, + uint256 mintCallbackFee + ) external payable override nonReentrant returns (bytes32 requestId) { + return _deposit(recipient, amount, mintCallbackFee); + } + + function _deposit(address recipient, uint256 amount, uint256 mintCallbackFee) + private + returns (bytes32 requestId) + { + _checkDepositsNotPaused(); + if (recipient == address(0)) { + revert InvalidAddress(); + } + if (amount == 0) { + revert InvalidAmount(); + } + + underlyingToken.safeTransferFrom(msg.sender, address(this), amount); + requestId = pToken.mint{value: msg.value}(recipient, amount, mintCallbackFee); + emit DepositRequested(msg.sender, recipient, amount, requestId); + emit WrapRequested(msg.sender, recipient, amount, requestId); + } + + /// @inheritdoc IPrivacyPortal + function depositNative( + address recipient, + uint256 amount, + uint256 mintCallbackFee + ) external payable override nonReentrant returns (bytes32 requestId) { + if (!nativeWrappedUnderlying) { + revert NativeWrapDisabled(); + } + _checkDepositsNotPaused(); + if (recipient == address(0)) { + revert InvalidAddress(); + } + if (amount == 0) { + revert InvalidAmount(); + } + if (msg.value < amount) { + revert IncorrectFee(amount, msg.value); + } + uint256 mintFee = msg.value - amount; + + IWrappedNative(address(underlyingToken)).deposit{value: amount}(); + requestId = pToken.mint{value: mintFee}(recipient, amount, mintCallbackFee); + emit DepositRequested(msg.sender, recipient, amount, requestId); + emit WrapRequested(msg.sender, recipient, amount, requestId); + } + + /// @inheritdoc IERC7984PortalWrapper + function underlying() external view returns (address) { + return address(underlyingToken); + } + + /// @inheritdoc IERC7984PortalWrapper + function rate() external pure returns (uint256) { + return 1; + } + + /// @inheritdoc IERC7984PortalWrapper + function wrap(address to, uint256 amount, uint256 mintCallbackFee) + external + payable + nonReentrant + returns (bytes32 requestId) + { + return _deposit(to, amount, mintCallbackFee); + } + + /// @inheritdoc IPrivacyPortal + function requestWithdrawWithPermit( + address recipient, + uint256 amount, + uint256 transferFee, + uint256 transferCallbackFee, + uint256 burnFee, + uint256 burnCallbackFee, + uint256 permitDeadline, + uint8 v, + bytes32 r, + bytes32 s + ) external payable override nonReentrant returns (bytes32 withdrawalId, bytes32 transferRequestId) { + _checkWithdrawalsNotPaused(); + if (recipient == address(0)) { + revert InvalidAddress(); + } + if (amount == 0) { + revert InvalidAmount(); + } + uint256 expectedFee = transferFee + burnFee; + if (msg.value != expectedFee) { + revert IncorrectFee(expectedFee, msg.value); + } + + withdrawalId = keccak256(abi.encodePacked(address(this), msg.sender, recipient, amount, withdrawalNonce++)); + bytes memory callbackData = abi.encodeWithSelector(this.onPTokenTransferred.selector, withdrawalId); + + IPodERC20.PublicPermit memory permit = + IPodERC20.PublicPermit({deadline: permitDeadline, v: v, r: r, s: s}); + transferRequestId = pToken.transferFromAndCallWithPermit{value: transferFee}( + msg.sender, + address(this), + amount, + permit, + callbackData, + transferCallbackFee + ); + + withdrawals[withdrawalId] = Withdrawal({ + user: msg.sender, + recipient: recipient, + amount: amount, + burnFee: burnFee, + burnCallbackFee: burnCallbackFee, + transferRequestId: transferRequestId, + burnRequestId: bytes32(0), + status: WithdrawalStatus.TransferPending + }); + + emit WithdrawalRequested(withdrawalId, msg.sender, recipient, amount, transferRequestId); + emit UnwrapRequested( + recipient, + withdrawalId, + keccak256(abi.encode(withdrawalId, transferRequestId)) + ); + } + + /// @inheritdoc IPrivacyPortal + function onPTokenTransferred(bytes32 withdrawalId) external override nonReentrant { + if (msg.sender != address(pToken)) { + revert OnlyPToken(msg.sender); + } + + _releaseWithdrawal(withdrawalId); + } + + /// @inheritdoc IPrivacyPortal + function triggerWithdrawalRelease(bytes32 withdrawalId) external override nonReentrant { + _releaseWithdrawal(withdrawalId); + } + + /// @notice Release an eligible withdrawal exactly once and submit the follow-up burn request. + /// @dev Follows checks-effects-interactions by marking the withdrawal released before transferring underlying tokens. + function _releaseWithdrawal(bytes32 withdrawalId) private { + Withdrawal storage withdrawal = withdrawals[withdrawalId]; + if (withdrawal.user == address(0)) { + revert UnknownWithdrawal(withdrawalId); + } + if (withdrawal.status != WithdrawalStatus.TransferPending) { + revert WithdrawalNotPending(withdrawalId, withdrawal.status); + } + IPodERC20.RequestStatus requestStatus = pToken.requests(withdrawal.transferRequestId); + if (requestStatus != IPodERC20.RequestStatus.Success) { + revert PTokenTransferNotSuccessful(withdrawal.transferRequestId, requestStatus); + } + + withdrawal.status = WithdrawalStatus.Released; + if (nativeWrappedUnderlying) { + IWrappedNative(address(underlyingToken)).withdraw(withdrawal.amount); + (bool ok,) = payable(withdrawal.recipient).call{value: withdrawal.amount}(""); + if (!ok) { + revert NativeTransferFailed(); + } + } else { + underlyingToken.safeTransfer(withdrawal.recipient, withdrawal.amount); + } + emit WithdrawalReleased(withdrawalId, withdrawal.recipient, withdrawal.amount); + // Explorer correlation id for the confidential leg (not a ciphertext pointer; see pToken ConfidentialTransfer). + emit UnwrapFinalized( + withdrawal.recipient, + withdrawalId, + withdrawalId, + uint64(withdrawal.amount) + ); + + _trySubmitBurn(withdrawalId, withdrawal); + } + + /// @notice Keeper/admin cleanup for pTokens already in portal custody when a previous burn submission failed. + function burnAccumulatedDebt( + uint256 amount, + uint256 burnFee, + uint256 burnCallbackFee + ) external payable onlyOwner nonReentrant returns (bytes32 burnRequestId) { + if (amount == 0) { + revert InvalidAmount(); + } + if (amount > burnDebtAmount) { + revert BurnDebtTooLow(burnDebtAmount, amount); + } + if (msg.value != burnFee) { + revert IncorrectFee(burnFee, msg.value); + } + + burnRequestId = pToken.burn{value: burnFee}(amount, burnCallbackFee); + burnDebtAmount -= amount; + emit BurnDebtSubmitted(msg.sender, amount, burnRequestId); + } + + /// @notice Sweep accidental native-token balance to `recipient`. + /// @dev Does not touch locked ERC20 collateral. Keep enough balance for planned owner burn-debt retries before sweeping. + /// @param recipient Native-token recipient. + /// @param amount Amount to sweep. + function sweepNative(address payable recipient, uint256 amount) external onlyOwner nonReentrant { + if (recipient == address(0)) { + revert InvalidAddress(); + } + if (amount == 0) { + revert InvalidAmount(); + } + (bool ok,) = recipient.call{value: amount}(""); + require(ok, "PrivacyPortal: sweep failed"); + emit NativeSwept(recipient, amount); + } + + /// @notice Best-effort burn submission for pTokens already moved into portal custody. + /// @dev Release is final even if burn submission fails; failures increase {burnDebtAmount} for owner cleanup. + function _trySubmitBurn(bytes32 withdrawalId, Withdrawal storage withdrawal) private { + burnDebtAmount += withdrawal.amount; + try pToken.burn{value: withdrawal.burnFee}(withdrawal.amount, withdrawal.burnCallbackFee) returns ( + bytes32 burnRequestId + ) { + burnDebtAmount -= withdrawal.amount; + withdrawal.burnRequestId = burnRequestId; + emit BurnSubmitted(withdrawalId, withdrawal.amount, burnRequestId); + } catch (bytes memory reason) { + emit BurnDebtRecorded(withdrawalId, withdrawal.amount, reason); + } + } + + /// @notice Query the optional pause controller and revert when it reports withdrawals paused. + function _checkWithdrawalsNotPaused() private view { + if (_pauseFlag(IPrivacyPortalPauseController.withdrawalsPaused.selector)) { + revert WithdrawalsPaused(); + } + } + + /// @notice Query the optional pause controller and revert when it reports deposits paused. + function _checkDepositsNotPaused() private view { + if (_pauseFlag(IPrivacyPortalPauseController.depositsPaused.selector)) { + revert DepositsPaused(); + } + } + + /// @dev Returns the pause flag from {pauseController}. Disabled when zero; fail-closed for contracts. + function _pauseFlag(bytes4 selector) private view returns (bool) { + address controller = pauseController; + if (controller == address(0)) { + return false; + } + if (controller.code.length == 0) { + return false; + } + (bool success, bytes memory data) = controller.staticcall(abi.encodeWithSelector(selector)); + if (!success || data.length < 32) { + revert PauseControllerFault(); + } + return abi.decode(data, (bool)); + } +} diff --git a/pod/privacy/PrivacyPortalFactory.sol b/pod/privacy/PrivacyPortalFactory.sol new file mode 100644 index 0000000..051935f --- /dev/null +++ b/pod/privacy/PrivacyPortalFactory.sol @@ -0,0 +1,204 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/proxy/Clones.sol"; + +import "../IInbox.sol"; +import "../token/perc20/PodErc20MintableInitializable.sol"; +import "../token/perc20/cotiside/PodErc20CotiMother.sol"; +import "./IPrivacyPortal.sol"; + +/// @title PrivacyPortalFactory +/// @notice Deploys one-shot minimal-clone portals and pTokens for public ERC20 collateral. +contract PrivacyPortalFactory is Ownable { + /// @notice Source-chain inbox used by pToken clones and registration messages. + address public immutable inbox; + /// @notice COTI chain id used by pToken clones for remote MPC execution. + uint256 public immutable cotiChainId; + /// @notice Unified COTI-side pToken ledger all clones talk to. + address public immutable cotiMotherContract; + /// @notice Clone implementation for source-chain pTokens. + address public immutable podTokenImplementation; + /// @notice Clone implementation for portals. + address public immutable portalImplementation; + /// @notice Global flag exposed through the pause-controller interface for all portals created here. + bool public withdrawalsPaused; + /// @notice Global flag exposed through the pause-controller interface for deposits on factory-created portals. + bool public depositsPaused; + + /// @notice Addresses allowed to deploy portal/pToken pairs. + mapping(address => bool) public deployers; + /// @notice Portal address by underlying ERC20. + mapping(address => address) public portalForUnderlying; + /// @notice Source-chain pToken address by underlying ERC20. + mapping(address => address) public pTokenForUnderlying; + /// @notice Portal address by source-chain pToken. + mapping(address => address) public portalForPToken; + + /// @notice Deployer allowlist entry changed. + event DeployerUpdated(address indexed deployer, bool allowed); + /// @notice Global withdrawal pause flag changed. + event WithdrawalsPausedUpdated(bool paused); + /// @notice Global deposit pause flag changed. + event DepositsPausedUpdated(bool paused); + /// @notice Both deposit and withdrawal pause flags changed together (emergency circuit breaker). + event OperationsPausedUpdated(bool paused); + /// @notice A new portal and source-chain pToken clone pair was deployed. + event PortalCreated( + address indexed underlying, + address indexed portal, + address indexed pToken, + address cotiMotherContract, + uint8 decimals + ); + /// @notice One-way registration message submitted to the COTI mother contract. + event TokenRegistrationRequested(address indexed pToken, bytes32 indexed requestId); + + /// @notice Caller is not an allowlisted deployer. + error OnlyDeployer(address caller); + /// @notice A required address was zero. + error InvalidAddress(); + /// @notice A portal already exists for the underlying token. + error PortalAlreadyExists(address underlying, address portal); + + /// @notice Restrict a function to an allowlisted deployer. + modifier onlyDeployer() { + if (!deployers[msg.sender]) { + revert OnlyDeployer(msg.sender); + } + _; + } + + /// @param initialOwner Owner and initial deployer. + /// @param inbox_ Source-chain inbox used by pToken clones. + /// @param cotiChainId_ COTI chain id used by pToken clones. + /// @param cotiMotherContract_ Unified COTI-side pToken ledger. + /// @param podTokenImplementation_ Clone implementation for source-chain pTokens. + /// @param portalImplementation_ Clone implementation for portals. + constructor( + address initialOwner, + address inbox_, + uint256 cotiChainId_, + address cotiMotherContract_, + address podTokenImplementation_, + address portalImplementation_ + ) Ownable(initialOwner) { + if ( + initialOwner == address(0) || inbox_ == address(0) || cotiChainId_ == 0 + || cotiMotherContract_ == address(0) || podTokenImplementation_ == address(0) + || portalImplementation_ == address(0) + ) { + revert InvalidAddress(); + } + inbox = inbox_; + cotiChainId = cotiChainId_; + cotiMotherContract = cotiMotherContract_; + podTokenImplementation = podTokenImplementation_; + portalImplementation = portalImplementation_; + deployers[initialOwner] = true; + emit DeployerUpdated(initialOwner, true); + } + + /// @notice Add or remove a portal deployer. + /// @param deployer Address to update. + /// @param allowed Whether the address may create portals. + function setDeployer(address deployer, bool allowed) external onlyOwner { + if (deployer == address(0)) { + revert InvalidAddress(); + } + deployers[deployer] = allowed; + emit DeployerUpdated(deployer, allowed); + } + + /// @notice Set the global pause flag read by portals initialized from this factory. + /// @param paused True to make new withdrawal requests revert. + function setWithdrawalsPaused(bool paused) external onlyOwner { + withdrawalsPaused = paused; + emit WithdrawalsPausedUpdated(paused); + } + + /// @notice Set the global deposit pause flag read by portals initialized from this factory. + /// @param paused True to make new deposits / wraps revert. + function setDepositsPaused(bool paused) external onlyOwner { + depositsPaused = paused; + emit DepositsPausedUpdated(paused); + } + + /// @notice Pause or unpause both deposits and withdrawals (emergency circuit breaker). + /// @param paused True to halt new deposits and withdrawal requests on factory-created portals. + function setOperationsPaused(bool paused) external onlyOwner { + withdrawalsPaused = paused; + depositsPaused = paused; + emit OperationsPausedUpdated(paused); + emit WithdrawalsPausedUpdated(paused); + emit DepositsPausedUpdated(paused); + } + + /// @notice Deploy a portal and pToken clone for an underlying token and register on the COTI mother ledger. + /// @param underlying Public ERC20 collateral token. + /// @param name Source pToken name. + /// @param symbol Source pToken symbol. + /// @param decimals Token decimals. + /// @param nativeWrappedUnderlying True when underlying is WETH/WAVAX (native wrap deposit + unwrap withdraw). + /// @param portalOwner Owner assigned to the portal clone. + /// @return portal Deployed portal clone. + /// @return pToken Deployed source-chain pToken clone. + function createPortal( + address underlying, + string calldata name, + string calldata symbol, + uint8 decimals, + bool nativeWrappedUnderlying, + address portalOwner + ) external payable onlyDeployer returns (address portal, address pToken) { + if (underlying == address(0) || portalOwner == address(0)) { + revert InvalidAddress(); + } + if (portalForUnderlying[underlying] != address(0)) { + revert PortalAlreadyExists(underlying, portalForUnderlying[underlying]); + } + + portal = Clones.clone(portalImplementation); + pToken = Clones.clone(podTokenImplementation); + + PodErc20MintableInitializable(payable(pToken)).initialize( + portal, + cotiChainId, + inbox, + cotiMotherContract, + name, + symbol, + decimals + ); + IPrivacyPortal(portal).initialize(portalOwner, underlying, pToken, decimals, nativeWrappedUnderlying); + + portalForUnderlying[underlying] = portal; + pTokenForUnderlying[underlying] = pToken; + portalForPToken[pToken] = portal; + + bytes32 requestId = _requestMotherRegistration(pToken, name, symbol, decimals); + + emit PortalCreated(underlying, portal, pToken, cotiMotherContract, decimals); + emit TokenRegistrationRequested(pToken, requestId); + } + + /// @dev Submits a one-way inbox message to register `pToken` on the COTI mother contract. + function _requestMotherRegistration( + address pToken, + string calldata name, + string calldata symbol, + uint8 decimals + ) private returns (bytes32 requestId) { + IInbox.MpcMethodCall memory methodCall = IInbox.MpcMethodCall({ + selector: bytes4(0), + data: abi.encodeWithSelector(PodErc20CotiMother.registerToken.selector, pToken, name, symbol, decimals), + datatypes: new bytes8[](0), + datalens: new bytes32[](0) + }); + + requestId = IInbox(inbox).sendOneWayMessage{value: msg.value}( + cotiChainId, cotiMotherContract, methodCall, bytes4(0) + ); + } +} diff --git a/pod/token/erc7984/Erc7984Constants.sol b/pod/token/erc7984/Erc7984Constants.sol new file mode 100644 index 0000000..ace2dc8 --- /dev/null +++ b/pod/token/erc7984/Erc7984Constants.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +/// @title Erc7984Constants +/// @notice Shared constants for ERC-7984 explorer compatibility. +library Erc7984Constants { + /// @dev Interface id from EIP-7984; Blockscout uses this to classify confidential tokens. + bytes4 internal constant INTERFACE_ID = 0x4958f2a4; +} diff --git a/pod/token/erc7984/Erc7984Pointers.sol b/pod/token/erc7984/Erc7984Pointers.sol new file mode 100644 index 0000000..0fcacfd --- /dev/null +++ b/pod/token/erc7984/Erc7984Pointers.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "../../../utils/mpc/MpcCore.sol"; + +/// @title Erc7984Pointers +/// @notice Maps PoD ciphertext views to ERC-7984 `bytes32` confidential pointers for explorers. +library Erc7984Pointers { + /// @dev Deterministic handle from dual-limb PoD balance/transfer ciphertext. Does not reveal plaintext. + function toHandle(ctUint256 memory ct) internal pure returns (bytes32) { + return keccak256(abi.encode(ct.ciphertextHigh, ct.ciphertextLow)); + } + + /// @dev Pick the party-specific amount view for a completed transfer/mint/burn callback. + function transferAmountHandle( + address from, + address to, + ctUint256 memory senderValue, + ctUint256 memory receiverValue + ) internal pure returns (bytes32) { + if (from == address(0)) { + return toHandle(receiverValue); + } + if (to == address(0)) { + return toHandle(senderValue); + } + return toHandle(receiverValue); + } +} diff --git a/pod/token/erc7984/IERC7984.sol b/pod/token/erc7984/IERC7984.sol new file mode 100644 index 0000000..e6ac1fe --- /dev/null +++ b/pod/token/erc7984/IERC7984.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; + +/// @title IERC7984 +/// @notice Draft ERC-7984 confidential fungible token interface (pointer-based amounts). +/// @dev Technology-agnostic EIP-7984 surface for Blockscout and other indexers. PoD pTokens +/// implement this for explorer compatibility; execution remains async via inbox + COTI. +interface IERC7984 is IERC165 { + /// @dev Emitted when a confidential transfer completes (including mint/burn via zero address). + event ConfidentialTransfer(address indexed from, address indexed to, bytes32 indexed amount); + + /// @dev Emitted when an operator authorization is updated. + event OperatorSet(address indexed holder, address indexed operator, uint48 until); + + /// @dev Optional: emitted when a pointer amount is publicly disclosed. + event AmountDisclosed(bytes32 indexed handle, uint256 amount); + + function name() external view returns (string memory); + + function symbol() external view returns (string memory); + + function decimals() external view returns (uint8); + + function contractURI() external view returns (string memory); + + function confidentialTotalSupply() external view returns (bytes32); + + function confidentialBalanceOf(address account) external view returns (bytes32); + + function isOperator(address holder, address spender) external view returns (bool); + + function setOperator(address operator, uint48 until) external; + + function confidentialTransfer(address to, bytes32 amount) external payable returns (bytes32); + + function confidentialTransfer(address to, bytes32 amount, bytes calldata data) external payable returns (bytes32); + + function confidentialTransferFrom(address from, address to, bytes32 amount) external payable returns (bytes32); + + function confidentialTransferFrom(address from, address to, bytes32 amount, bytes calldata data) + external + payable + returns (bytes32); + + function confidentialTransferAndCall(address to, bytes32 amount, bytes calldata callData) + external + payable + returns (bytes32); + + function confidentialTransferAndCall(address to, bytes32 amount, bytes calldata data, bytes calldata callData) + external + payable + returns (bytes32); + + function confidentialTransferFromAndCall(address from, address to, bytes32 amount, bytes calldata callData) + external + payable + returns (bytes32); + + function confidentialTransferFromAndCall( + address from, + address to, + bytes32 amount, + bytes calldata data, + bytes calldata callData + ) external payable returns (bytes32); +} diff --git a/pod/token/erc7984/IERC7984PortalWrapper.sol b/pod/token/erc7984/IERC7984PortalWrapper.sol new file mode 100644 index 0000000..3dbd94d --- /dev/null +++ b/pod/token/erc7984/IERC7984PortalWrapper.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +/// @title IERC7984PortalWrapper +/// @notice Partial ERC-7984 wrapper surface for PrivacyPortal deposit/withdraw explorer visibility. +/// @dev Does not require full synchronous wrap/unwrap semantics; events mirror OpenZeppelin/Zama naming. +interface IERC7984PortalWrapper { + /// @dev Emitted when underlying is locked and a confidential mint is requested (portal deposit / wrap). + event WrapRequested(address indexed from, address indexed to, uint256 amount, bytes32 indexed mintRequestId); + + /// @dev Emitted when a confidential unwrap is requested (portal withdrawal). + event UnwrapRequested(address indexed receiver, bytes32 indexed unwrapRequestId, bytes32 amount); + + /// @dev Emitted when underlying is released after a confidential unwrap completes. + /// @param encryptedAmount PoD portals use `unwrapRequestId` as an explorer correlation id; the + /// real confidential amount pointer is on the pToken `ConfidentialTransfer` in the callback tx. + event UnwrapFinalized( + address indexed receiver, + bytes32 indexed unwrapRequestId, + bytes32 encryptedAmount, + uint64 cleartextAmount + ); + + function underlying() external view returns (address); + + /// @dev Conversion rate from underlying to confidential token (1:1 for PoD portals). + function rate() external view returns (uint256); + + /// @notice Lock underlying and request async confidential mint (alias for deposit with explicit fee). + function wrap(address to, uint256 amount, uint256 mintCallbackFee) external payable returns (bytes32); +} diff --git a/pod/token/erc7984/PodErc7984Mixin.sol b/pod/token/erc7984/PodErc7984Mixin.sol new file mode 100644 index 0000000..c7db760 --- /dev/null +++ b/pod/token/erc7984/PodErc7984Mixin.sol @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; +import "../../../utils/mpc/MpcCore.sol"; +import "./Erc7984Constants.sol"; +import "./Erc7984Pointers.sol"; +import "./IERC7984.sol"; + +/// @title PodErc7984Mixin +/// @notice ERC-7984 explorer compatibility for PoD pTokens. Emits `ConfidentialTransfer` on async callback success. +/// @dev Mixed into {PodERC20}. Use native {IPodERC20} async methods for transfers; ERC-7984 transfer entry points revert. +abstract contract PodErc7984Mixin is IERC7984, ERC165 { + /// @notice ERC-7572 metadata URI for explorers (optional). + string public contractURI; + + /// @dev Last confidential amount handle emitted on callback; portals may read for unwrap finalization events. + bytes32 public lastConfidentialTransferHandle; + + /// @dev ERC-7984 operator approvals (separate from async pToken allowance model). + mapping(address => mapping(address => uint48)) private _operatorUntil; + + /// @notice Thrown when an ERC-7984 transfer method is invoked; use {IPodERC20} async entry points instead. + error Erc7984UsePodTransferMethods(); + + /// @inheritdoc IERC165 + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(ERC165, IERC165) + returns (bool) + { + return interfaceId == Erc7984Constants.INTERFACE_ID || interfaceId == type(IERC7984).interfaceId + || super.supportsInterface(interfaceId); + } + + /// @inheritdoc IERC7984 + function confidentialTotalSupply() external pure returns (bytes32) { + return bytes32(0); + } + + /// @inheritdoc IERC7984 + function confidentialBalanceOf(address account) external view returns (bytes32) { + return Erc7984Pointers.toHandle(_erc7984BalanceOf(account)); + } + + /// @inheritdoc IERC7984 + function isOperator(address holder, address spender) public view returns (bool) { + return _operatorUntil[holder][spender] > block.timestamp; + } + + /// @inheritdoc IERC7984 + function setOperator(address operator, uint48 until) external { + _operatorUntil[msg.sender][operator] = until; + emit OperatorSet(msg.sender, operator, until); + } + + /// @inheritdoc IERC7984 + function confidentialTransfer(address, bytes32) external payable returns (bytes32) { + revert Erc7984UsePodTransferMethods(); + } + + /// @inheritdoc IERC7984 + function confidentialTransfer(address, bytes32, bytes calldata) external payable returns (bytes32) { + revert Erc7984UsePodTransferMethods(); + } + + /// @inheritdoc IERC7984 + function confidentialTransferFrom(address, address, bytes32) external payable returns (bytes32) { + revert Erc7984UsePodTransferMethods(); + } + + /// @inheritdoc IERC7984 + function confidentialTransferFrom(address, address, bytes32, bytes calldata) external payable returns (bytes32) { + revert Erc7984UsePodTransferMethods(); + } + + /// @inheritdoc IERC7984 + function confidentialTransferAndCall(address, bytes32, bytes calldata) external payable returns (bytes32) { + revert Erc7984UsePodTransferMethods(); + } + + /// @inheritdoc IERC7984 + function confidentialTransferAndCall(address, bytes32, bytes calldata, bytes calldata) + external + payable + returns (bytes32) + { + revert Erc7984UsePodTransferMethods(); + } + + /// @inheritdoc IERC7984 + function confidentialTransferFromAndCall(address, address, bytes32, bytes calldata) + external + payable + returns (bytes32) + { + revert Erc7984UsePodTransferMethods(); + } + + /// @inheritdoc IERC7984 + function confidentialTransferFromAndCall(address, address, bytes32, bytes calldata, bytes calldata) + external + payable + returns (bytes32) + { + revert Erc7984UsePodTransferMethods(); + } + + /// @dev Emit ERC-7984 transfer row for explorers after COTI callback applied balances. + function _emitConfidentialTransfer( + address from, + address to, + ctUint256 memory senderValue, + ctUint256 memory receiverValue + ) internal { + bytes32 handle = Erc7984Pointers.transferAmountHandle(from, to, senderValue, receiverValue); + lastConfidentialTransferHandle = handle; + emit ConfidentialTransfer(from, to, handle); + } + + /// @dev Child returns cached PoD balance ciphertext for `account`. + function _erc7984BalanceOf(address account) internal view virtual returns (ctUint256 memory); +} diff --git a/pod/token/erc7984/README.md b/pod/token/erc7984/README.md new file mode 100644 index 0000000..d3402ff --- /dev/null +++ b/pod/token/erc7984/README.md @@ -0,0 +1,60 @@ +# ERC-7984 explorer compatibility + +PoD pTokens expose a narrow [EIP-7984](https://eips.ethereum.org/EIPS/eip-7984) surface so Blockscout and similar explorers can classify confidential tokens and index completed confidential transfers. + +## Contracts + +| File | Role | +|------|------| +| `IERC7984.sol` | Draft ERC-7984 interface (`bytes32` confidential pointers) | +| `IERC7984PortalWrapper.sol` | Partial wrapper events/views for `PrivacyPortal` | +| `Erc7984Constants.sol` | Interface id `0x4958f2a4` | +| `Erc7984Pointers.sol` | Maps PoD `ctUint256` to explorer handles | +| `PodErc7984Mixin.sol` | ERC-165 + metadata + `ConfidentialTransfer` emission | + +`PodERC20` inherits `PodErc7984Mixin` and emits `ConfidentialTransfer` alongside the existing `Transfer` event when COTI callbacks succeed. + +`PrivacyPortal` emits `WrapRequested` on deposit/wrap and `UnwrapRequested` / `UnwrapFinalized` on withdraw/release. + +## Important notes + +- This is **explorer compatibility**, not Zama FHE semantic equivalence. +- ERC-7984 transfer entry points revert with `Erc7984UsePodTransferMethods`; use native async `IPodERC20` methods for actual moves. +- Explorer-visible confidential transfers are emitted **only after async callback success** on production `PodERC20`. +- **Live Sepolia pMTT/pWETH/pUSDC deployments do not include this mixin yet** — they emit `Transfer` only until upgraded. Use `DummyTestPERC20` at fresh addresses for explorer testing without touching production contracts. + +## Dummy test stack (no COTI) + +| File | Role | +|------|------| +| `contracts/mocks/DummyTestPERC20.sol` | Synchronous test pToken with `ConfidentialTransfer` on every completed move | +| `contracts/mocks/PodCallbackTestInbox.sol` | Optional inbox stub to complete pending dummy mints/transfers | + +Deploy and run on Sepolia (new addresses each run; saves `erc7984-dummy-deploy.json`): + +```bash +npm run demo:erc7984-dummy-sepolia +``` + +Then verify the pToken on Blockscout Sepolia so `supportsInterface(0x4958f2a4)` and `ConfidentialTransfer` appear in the UI. + +## Tests + +```bash +npm run test:erc7984 +``` + +Blockscout verification (requires deployed verified contracts): + +```bash +ERC7984_EXPLORER_TESTS=1 \ +ERC7984_PTOKEN=0x... \ +ERC7984_BLOCKSCOUT_API=https://eth.blockscout.com/api/v2 \ +npm run test:erc7984-explorer +``` + +Optional tx inspection: + +```bash +npx tsx scripts/erc7984/check-blockscout.ts --token 0x... --tx 0x... +``` diff --git a/pod/token/perc20/IPodERC20.sol b/pod/token/perc20/IPodERC20.sol new file mode 100644 index 0000000..feeefd3 --- /dev/null +++ b/pod/token/perc20/IPodERC20.sol @@ -0,0 +1,269 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "../../../utils/mpc/MpcCore.sol"; + +/// @title IPodERC20 +/// @notice Async private ERC-20: `ctUint256` balances/allowances; moves use `itUint256` and inbox + COTI settlement. +/// @dev Not IERC20-compatible: mutating calls return `requestId`; only the configured COTI peer may complete callbacks. +/// Plain `uint256` methods expose amounts in calldata and events; use encrypted `itUint256` methods for privacy-sensitive flows. +interface IPodERC20 { + // --- Types --- + + enum RequestStatus { + None, + Pending, + Success, + Failed + } + + /// @notice Allowance represented twice: re-encrypted for the owner and for the spender so each party can decrypt their view. + struct Allowance { + ctUint256 ownerCiphertext; + ctUint256 spenderCiphertext; + } + + /// @notice Off-chain helpers may track submitted transfer intents by `requestId`. + struct TransferRequested { + address from; + address to; + bytes32 requestId; + } + + /// @notice Off-chain helpers may track submitted approvals by `requestId`. + struct ApprovalRequested { + address owner; + address spender; + bytes32 requestId; + } + + /// @notice EIP-712 permit data used by public transferFrom flows that should not wait for async approve. + struct PublicPermit { + uint256 deadline; + uint8 v; + bytes32 r; + bytes32 s; + } + + // --- Events --- + + /** + * @notice Tokens moved from `from` to `to` after the COTI leg succeeded and this contract applied ciphertext updates. + * @dev `senderValue` / `receiverValue` are the same logical amount re-encrypted for each party; either may be zero in edge cases. + */ + event Transfer( + address indexed from, + address indexed to, + ctUint256 senderValue, + ctUint256 receiverValue + ); + + /// @notice The asynchronous transfer failed on the COTI side or was rejected before balances were updated. + event TransferFailed(address indexed from, address indexed to, bytes errorMsg); + + /** + * @notice Allowance for `spender` on `owner` was updated after a successful COTI `approve`. + * @dev `ownerValue` and `spenderValue` encrypt the same allowance amount for different AES keys. + */ + event Approval( + address indexed owner, + address indexed spender, + ctUint256 ownerValue, + ctUint256 spenderValue + ); + + /// @notice `transferAndCall` delivered tokens but the post-transfer `to.call(callbackData)` reverted or ran out of gas. + event RequestCallbackFailed(address from, address to, bytes32 requestId, bytes callbackData); + + /// @notice `syncBalances` refreshed `account` from the COTI ledger when the monotonic `nonce` allowed it. + event BalanceSynced(address account, ctUint256 amount); + + /// @notice Lifecycle transition for an async inbox request submitted by this token. + event RequestStatusUpdated(bytes32 indexed requestId, RequestStatus status); + + // --- Token metadata & supply --- + + /** + * @notice ERC-20-style total supply accessor. + * @dev Implementations may always return `0` to hide supply on-chain while the authoritative ledger lives on COTI. + */ + function totalSupply() external view returns (uint256); + + /// @notice Status of an async request submitted by this token. + function requests(bytes32 requestId) external view returns (RequestStatus); + + // --- Balances --- + + /** + * @notice Returns `account`'s balance as ciphertext encrypted for that account. + * @dev Stale reads are possible if a transfer is in flight; see {balanceOfWithStatus}. + */ + function balanceOf(address account) external view returns (ctUint256 memory); + + /** + * @notice Same as {balanceOf}, plus whether this account is currently locked by an in-flight transfer (or burn). + * @dev While `pending` is true, new transfers involving this address as `from` or `to` will revert. + */ + function balanceOfWithStatus(address account) external view returns (ctUint256 memory, bool pending); + + // --- Transfers --- + + /** + * @notice Starts an encrypted transfer of `value` from the caller to `to`. + * @return requestId Inbox request id; completion is asynchronous via {Transfer} or {TransferFailed}. + * @dev **Gotcha:** reverts if either the sender or `to` already has a pending transfer. **Gotcha:** concurrent approvals use a + * separate pending map and do not block transfers unless your deployment couples them elsewhere. + * @param callbackFeeLocalWei Caller-estimated wei slice for the callback leg; total payment is `msg.value`. + */ + function transfer(address to, itUint256 calldata value, uint256 callbackFeeLocalWei) external payable returns (bytes32 requestId); + + /** + * @notice Starts an encrypted transfer of `value` from the caller to `to`. + * @return requestId Inbox request id; completion is asynchronous via {Transfer} or {TransferFailed}. + * @dev The callback fee is calculated within the contract + */ + function transfer(address to, itUint256 calldata value) external payable returns (bytes32 requestId); + + /** + * @notice Starts a transfer from `from` to `to` using allowance granted to `msg.sender`. + * @dev **Gotcha:** allowance checks and consumption happen on COTI; this entry point only forwards the MPC call. + */ + function transferFrom(address from, address to, itUint256 calldata value, uint256 callbackFeeLocalWei) external payable returns (bytes32 requestId); + + /** + * @notice Starts a transfer from `from` to `to` using allowance granted to `msg.sender`. + * @dev The callback fee is calculated within the contract + */ + function transferFrom(address from, address to, itUint256 calldata value) external payable returns (bytes32 requestId); + + /** + * @notice Like {transfer}, then after success attempts `to.call(data)` with no gas stipend beyond the remaining tx gas. + * @dev **Gotcha:** callback failure does not undo the transfer; it only emits {RequestCallbackFailed}. Stored callback data is cleared on success path. + */ + function transferAndCall( + address to, + itUint256 calldata amount, + bytes calldata data, + uint256 callbackFeeLocalWei + ) external payable returns (bytes32 requestId); + + /** + * @notice Public-amount transferFrom followed by a PoD-side callback to `to` after the COTI transfer succeeds. + * @dev Uses the caller as spender and consumes allowance on COTI. + */ + function transferFromAndCall( + address from, + address to, + uint256 amount, + bytes calldata data, + uint256 callbackFeeLocalWei + ) external payable returns (bytes32 requestId); + + /** + * @notice Public-amount transferFrom authorized by a signature, followed by a callback to `to`. + * @dev Intended for portal withdrawals so users do not wait for a separate async approve. + */ + function transferFromAndCallWithPermit( + address from, + address to, + uint256 amount, + PublicPermit calldata permit, + bytes calldata data, + uint256 callbackFeeLocalWei + ) external payable returns (bytes32 requestId); + + /// @dev Reserved: re-encrypt the caller's balance for another account's key (not implemented in the reference token). + // function setAccountEncryptionAddress(address addr) external returns (bytes32 requestId); + + /** + * @notice Plain-amount transfer variant; the remote leg receives an un-encrypted `uint256` and garbles it on COTI. + * @dev **Gotcha:** exposes the transfer amount in calldata and events on PoD. Same pending-slot rules as the encrypted overload. + */ + function transfer(address to, uint256 amount, uint256 callbackFeeLocalWei) external payable returns (bytes32 requestId); + + /** + * @notice Plain-amount {transferFrom} variant; see {transfer(address,uint256,uint256)} gotchas. + */ + function transferFrom(address from, address to, uint256 amount, uint256 callbackFeeLocalWei) external payable returns (bytes32 requestId); + + // --- Allowances --- + + /** + * @notice Returns ciphertext views of the allowance; each party decrypts their half. + * @dev Default is empty/zero ciphertext until an {approve} succeeds. + */ + function allowance(address owner, address spender) external view returns (Allowance memory); + + /** + * @notice Same as {allowance}, plus whether an {approve} is already in flight for this pair. + * @dev While `pending` is true, another {approve} for the same owner/spender reverts. + */ + function allowanceWithStatus( + address owner, + address spender + ) external view returns (Allowance memory, bool pending); + + /** + * @notice Sets allowance of `spender` over the caller's tokens to `value` (encrypted input). + * @return requestId Asynchronous request id for this approval. + * @dev **Gotcha:** classic ERC-20 allowance front-running applies if you change from non-zero to non-zero in one step; + * consider setting to zero first. **Gotcha:** only one pending approval per `(owner, spender)` at a time. + */ + function approve(address spender, itUint256 calldata value, uint256 callbackFeeLocalWei) external payable returns (bytes32 requestId); + + /** + * @notice Sets allowance of `spender` over the caller's tokens to `value` (encrypted input). + * @return requestId Asynchronous request id for this approval. + * @dev The callback fee is calculated within the contract + */ + function approve(address spender, itUint256 calldata value) external payable returns (bytes32 requestId); + + /** + * @notice Plain-amount approval variant; the COTI leg garbles `amount` with `MpcCore.setPublic256`. + * @dev **Gotcha:** exposes the allowance in calldata and events on PoD. + */ + function approve(address spender, uint256 amount, uint256 callbackFeeLocalWei) external payable returns (bytes32 requestId); + + // --- Mint / burn --- + + /** + * @notice Destroys `amount` (encrypted) from the caller on the COTI ledger; PoD balances update on callback. + * @return requestId Asynchronous burn request. + * @dev **Gotcha:** uses the same pending-transfer slot as transfers; burns block other transfers for `msg.sender` until settled. + */ + function burn(itUint256 calldata amount, uint256 callbackFeeLocalWei) external payable returns (bytes32 requestId); + + /** + * @notice Plain-amount burn variant (non-encrypted input). + * @dev **Gotcha:** exposes burned amount in calldata; same pending-slot behavior as the encrypted variant. + */ + function burn(uint256 amount, uint256 callbackFeeLocalWei) external payable returns (bytes32 requestId); + + /** + * @notice Mints `amount` (encrypted) into `to` on the COTI ledger; PoD balance for `to` updates on callback. + * @return requestId Asynchronous mint request. + * @dev **Gotcha:** uses the same pending-transfer slot as transfers for the recipient; the `from` side of the callback is `address(0)`. + */ + function mint(address to, itUint256 calldata amount, uint256 callbackFeeLocalWei) external payable returns (bytes32 requestId); + + /** + * @notice Plain-amount mint variant; COTI garbles via `MpcCore.setPublic256`. + * @dev **Gotcha:** exposes minted amount in calldata. + */ + function mint(address to, uint256 amount, uint256 callbackFeeLocalWei) external payable returns (bytes32 requestId); + + /// @dev Reserved: burn garbled amount; not supported in reference flows. + // function burnGt(gtUint256 amount) external returns (gtBool); + + /// @dev Reserved: `transferFrom` with garbled amount; not supported. + // function transferFromGT(address from, address to, gtUint256 value) external returns (gtBool); + + // --- Sync --- + + /** + * @notice Pulls fresh garbled balances from COTI for `accounts` and applies them on success if the sync `nonce` is newer. + * @return requestId Two-way inbox request id. + * @dev **Gotcha:** large account lists mean heavy MPC work and gas on COTI; empty list may fail on the COTI side. + */ + function syncBalances(address[] calldata accounts, uint256 callbackFeeLocalWei) external payable returns (bytes32 requestId); +} diff --git a/pod/token/perc20/PodERC20.sol b/pod/token/perc20/PodERC20.sol new file mode 100644 index 0000000..1af80ea --- /dev/null +++ b/pod/token/perc20/PodERC20.sol @@ -0,0 +1,910 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "../../InboxUser.sol"; +import "../../fee/IInboxFeeManager.sol"; +import "../../mpccodec/MpcAbiCodec.sol"; +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "./IPodERC20.sol"; +import "./cotiside/IPodErc20CotiSide.sol"; +import "../erc7984/PodErc7984Mixin.sol"; + +/// @title PodERC20 +/// @notice PoD-side private ERC-20: ciphertext cache and inbox-mediated async moves; COTI holds authoritative garbled state via {IPodErc20CotiSide}. +/// @dev Callbacks only from `inbox` when the remote peer matches (`cotiChainId`, `cotiSideContract`). Public-amount methods expose amounts in calldata and logs; use encrypted `itUint256` entry points for privacy-sensitive flows. +contract PodERC20 is IPodERC20, InboxUser, PodErc7984Mixin { + using MpcAbiCodec for MpcAbiCodec.MpcMethodCallContext; + + // --- State variables --- + + uint256 public cotiChainId; + address public cotiSideContract; + string public name; + string public symbol; + uint8 public decimals; + uint256 public totalSupply; + bool private _podERC20Initialized; + /// @notice Nonces consumed by public transfer permits verified on PoD. + mapping(address => uint256) public nonces; + + /// @dev Rough calldata size for the remote MPC method (itUint256 + two addresses or similar). Matches the test harness default. + uint256 private constant FEE_ESTIMATE_REMOTE_CALL_SIZE = 512; + /// @dev Rough calldata size for the PoD callback payload (`transferCallback`/`approveCallback`). + uint256 private constant FEE_ESTIMATE_CALLBACK_CALL_SIZE = 512; + /// @dev Headroom for COTI-side MPC execution (onBoard/sub/add/offBoard round-trip). + uint256 private constant FEE_ESTIMATE_REMOTE_EXEC_GAS = 300_000; + /// @dev Headroom for PoD callback execution (decode + storage writes). + uint256 private constant FEE_ESTIMATE_CALLBACK_EXEC_GAS = 300_000; + bytes32 private constant PUBLIC_TRANSFER_PERMIT_TYPEHASH = + keccak256("TransferPermit(address owner,address spender,address to,uint256 value,uint256 nonce,uint256 deadline)"); + bytes32 private constant DOMAIN_TYPEHASH = + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + bytes32 private constant PERMIT_DOMAIN_VERSION_HASH = keccak256("1"); + mapping(address => ctUint256) private _balances; + mapping(address => mapping(address => IPodERC20.Allowance)) private _allowance; + /// @dev One in-flight transfer or burn per address (used as both sender and receiver lock for transfers). + mapping(address => bytes32) private _pendingTransferRequestIds; + mapping(address => mapping(address => bytes32)) private _pendingApprovalRequestIds; + /// @dev Optional `transferAndCall` payload keyed by inbox `sourceRequestId`, cleared after callback. + mapping(bytes32 => bytes) private _requestCallbacks; + mapping(bytes32 => IPodERC20.RequestStatus) public requests; + mapping(bytes32 => bytes) public failedRequests; + /// @dev Monotonic nonce from COTI; stale callbacks do not overwrite newer balances. + mapping(address => uint256) public balanceNonces; + + // --- Events (PoD-specific; {Transfer}, {Approval}, etc. are declared on {IPodERC20}) --- + + /// @notice Async transfer request was submitted to COTI. + event TransferRequestSubmitted(address indexed from, address indexed to, bytes32 requestId); + /// @notice Async approval request was submitted to COTI. + event ApprovalRequestSubmitted(address indexed owner, address indexed spender, bytes32 requestId); + /// @notice Async approval request failed on COTI. + event ApprovalFailed(address indexed owner, address indexed spender, bytes errorMsg); + /// @notice Async balance-sync request failed on COTI. + event SyncBalancesFailed(bytes32 requestId, bytes errorMsg); + /// @notice Async balance-sync request was submitted for accounts. + event SyncBalancesRequested(address[] accounts, bytes32 requestId); + + // --- Errors --- + + /// @notice A transfer/burn lock already exists for one of the affected accounts. + error TransferAlreadyPending(address from, address to, bytes32 requestId); + /// @notice An approval lock already exists for the owner/spender pair. + error ApprovalAlreadyPending(address owner, address spender, bytes32 requestId); + /// @notice Inbox caller was not the configured COTI-side peer. + error OnlyCotiSideContract(uint256 remoteChainId, address remoteContract); + /// @notice Thrown by the default {_checkMinter} hook; subclasses (e.g. {PodErc20Mintable}) can override to allow minting. + error MintNotAllowed(address caller); + /// @notice Clone storage was already initialized. + error PodERC20AlreadyInitialized(); + /// @notice Initializer received an invalid zero address or chain id. + error PodERC20InvalidInitialization(); + /// @notice Public transfer permit deadline has passed. + error PermitExpired(uint256 deadline); + /// @notice Public transfer permit signer did not match the owner. + error InvalidPermitSigner(address signer, address owner); + + // --- Constructor --- + + /** + * @param _cotiChainId Chain id of COTI; must match {IInbox.inboxMsgSender} when the peer calls back. + * @param _inbox Cross-chain inbox used for two-way messages (also sets {InboxUser.inbox}). + * @param _cotiSideContract Deployed {IPodErc20CotiSide} this token talks to on COTI. + * @param _name ERC-20 name string (public metadata on PoD). + * @param _symbol ERC-20 symbol string (public metadata on PoD). + */ + constructor( + uint256 _cotiChainId, + address _inbox, + address _cotiSideContract, + string memory _name, + string memory _symbol + ) { + _initializePodERC20(_cotiChainId, _inbox, _cotiSideContract, _name, _symbol); + } + + /// @notice Accept native funds used to pay inbox fees for async pToken operations. + /// @dev `_sendPodTwoWay` may spend existing contract balance, so operational tooling can pre-fund this token for auto-fee flows. + receive() external payable {} + + // --- External: mutating (user / admin) --- + + /** + * @inheritdoc IPodERC20 + * @dev **Gotcha:** reverts if either party already has a pending transfer. **Gotcha:** `TransferRequestSubmitted` indexes + * `msg.sender` as `from`, not the `from` argument of internal `_transfer` (same for direct `transfer`). + */ + function transfer(address to, itUint256 calldata value, uint256 callbackFeeLocalWei) external payable returns (bytes32 requestId) { + return _transfer(IPodErc20CotiSide.transfer.selector, msg.sender, to, value, msg.value, callbackFeeLocalWei); + } + + /// @inheritdoc IPodERC20 + function transfer(address to, itUint256 calldata value) external payable returns (bytes32 requestId) { + (, uint256 callbackFeeLocalWei) = _estimateTwoWayFeeInLocalToken(); + return _transfer(IPodErc20CotiSide.transfer.selector, msg.sender, to, value, msg.value, callbackFeeLocalWei); + } + + /// @inheritdoc IPodERC20 + function transferFrom(address from, address to, itUint256 calldata value, uint256 callbackFeeLocalWei) external payable returns (bytes32 requestId) { + return _transferFrom(IPodErc20CotiSide.transferFromAsSpender.selector, msg.sender, from, to, value, msg.value, callbackFeeLocalWei); + } + + /// @inheritdoc IPodERC20 + function transferFrom(address from, address to, itUint256 calldata value) external payable returns (bytes32 requestId) { + (, uint256 callbackFeeLocalWei) = _estimateTwoWayFeeInLocalToken(); + return _transferFrom(IPodErc20CotiSide.transferFromAsSpender.selector, msg.sender, from, to, value, msg.value, callbackFeeLocalWei); + } + + /** + * @inheritdoc IPodERC20 + * @dev Stores `data` under the new `requestId` until {transferCallback} runs successfully and forwards it to `to`. + */ + function transferAndCall( + address to, + itUint256 calldata amount, + bytes calldata data, + uint256 callbackFeeLocalWei + ) external payable returns (bytes32 requestId) { + requestId = _transfer(IPodErc20CotiSide.transfer.selector, msg.sender, to, amount, msg.value, callbackFeeLocalWei); + _requestCallbacks[requestId] = data; + return requestId; + } + + /// @inheritdoc IPodERC20 + function transferFromAndCall( + address from, + address to, + uint256 amount, + bytes calldata data, + uint256 callbackFeeLocalWei + ) external payable returns (bytes32 requestId) { + requestId = _transferPublicFrom( + IPodErc20CotiSide.transferFromPublicAsSpender.selector, + msg.sender, + from, + to, + amount, + msg.value, + callbackFeeLocalWei + ); + _requestCallbacks[requestId] = data; + } + + /// @inheritdoc IPodERC20 + function transferFromAndCallWithPermit( + address from, + address to, + uint256 amount, + PublicPermit calldata permit, + bytes calldata data, + uint256 callbackFeeLocalWei + ) external payable returns (bytes32 requestId) { + requestId = _transferPublicFromWithPermit( + msg.sender, + from, + to, + amount, + permit, + msg.value, + callbackFeeLocalWei + ); + _requestCallbacks[requestId] = data; + } + + /// @inheritdoc IPodERC20 + function approve(address spender, itUint256 calldata value, uint256 callbackFeeLocalWei) external payable returns (bytes32 requestId) { + return _approve(msg.sender, spender, value, msg.value, callbackFeeLocalWei); + } + + /// @inheritdoc IPodERC20 + function approve(address spender, itUint256 calldata value) external payable returns (bytes32 requestId) { + (, uint256 callbackFeeLocalWei) = _estimateTwoWayFeeInLocalToken(); + return _approve(msg.sender, spender, value, msg.value, callbackFeeLocalWei); + } + + /// @inheritdoc IPodERC20 + function burn(itUint256 calldata value, uint256 callbackFeeLocalWei) external payable returns (bytes32 requestId) { + return _burn(msg.sender, value, msg.value, callbackFeeLocalWei); + } + + /// @inheritdoc IPodERC20 + function mint(address to, itUint256 calldata amount, uint256 callbackFeeLocalWei) external payable returns (bytes32 requestId) { + _checkMinter(); + return _mint(to, amount, msg.value, callbackFeeLocalWei); + } + + // --- External: mutating (plain uint256 variants) --- + + /// @inheritdoc IPodERC20 + function transfer(address to, uint256 amount, uint256 callbackFeeLocalWei) external payable returns (bytes32 requestId) { + return _transferPublic(IPodErc20CotiSide.transferPublic.selector, msg.sender, to, amount, msg.value, callbackFeeLocalWei); + } + + /// @inheritdoc IPodERC20 + function transferFrom(address from, address to, uint256 amount, uint256 callbackFeeLocalWei) external payable returns (bytes32 requestId) { + return _transferPublicFrom(IPodErc20CotiSide.transferFromPublicAsSpender.selector, msg.sender, from, to, amount, msg.value, callbackFeeLocalWei); + } + + /// @inheritdoc IPodERC20 + function approve(address spender, uint256 amount, uint256 callbackFeeLocalWei) external payable returns (bytes32 requestId) { + return _approvePublic(msg.sender, spender, amount, msg.value, callbackFeeLocalWei); + } + + /// @inheritdoc IPodERC20 + function burn(uint256 amount, uint256 callbackFeeLocalWei) external payable returns (bytes32 requestId) { + return _burnPublic(msg.sender, amount, msg.value, callbackFeeLocalWei); + } + + /// @inheritdoc IPodERC20 + function mint(address to, uint256 amount, uint256 callbackFeeLocalWei) external payable returns (bytes32 requestId) { + _checkMinter(); + return _mintPublic(to, amount, msg.value, callbackFeeLocalWei); + } + + /** + * @inheritdoc IPodERC20 + * @dev Does not record a “pending” flag per account for sync; only transfers/burns use the pending-transfer map. + */ + function syncBalances(address[] calldata accounts, uint256 callbackFeeLocalWei) external payable returns (bytes32 requestId) { + IInbox.MpcMethodCall memory mpcMethodCall = MpcAbiCodec.create(IPodErc20CotiSide.syncBalances.selector, 1) + .addArgument(accounts) + .build(); + + requestId = _sendPodTwoWay( + msg.value, + callbackFeeLocalWei, + mpcMethodCall, + PodERC20.syncBalancesCallback.selector, + PodERC20.syncBalancesError.selector + ); + _setRequestStatus(requestId, IPodERC20.RequestStatus.Pending); + emit SyncBalancesRequested(accounts, requestId); + } + + // --- External: inbox callbacks (success) --- + + /** + * @notice Applies post-transfer ciphertext balances and optional `transferAndCall` hook. + * @dev **Gotcha:** balance updates apply only when `nonce` exceeds {balanceNonces}; replays with old nonces are ignored. + * COTI {PodErc20CotiMother} starts per-token callback nonces at 1 on registration so the first update applies. + * **Gotcha:** `to.call(callbackData)` uses all remaining gas; failures emit {RequestCallbackFailed} only. + */ + function transferCallback(bytes memory data) external onlyInbox { + (uint256 remoteChainId, address remoteContract) = inbox.inboxMsgSender(); + if (remoteChainId != cotiChainId || remoteContract != cotiSideContract) { + revert OnlyCotiSideContract(remoteChainId, remoteContract); + } + bytes32 sourceRequestId = inbox.inboxSourceRequestId(); + _setRequestStatus(sourceRequestId, IPodERC20.RequestStatus.Success); + ( + address from, + ctUint256 memory newBalanceFrom, + ctUint256 memory senderValue, + address to, + ctUint256 memory newBalanceTo, + ctUint256 memory receiverValue, + uint256 nonce + ) = abi.decode(data, (address, ctUint256, ctUint256, address, ctUint256, ctUint256, uint256)); + if (from != address(0)) { + _pendingTransferRequestIds[from] = bytes32(0); + if (balanceNonces[from] < nonce) { + _balances[from] = newBalanceFrom; + balanceNonces[from] = nonce; + } + } + if (to != address(0)) { + _pendingTransferRequestIds[to] = bytes32(0); + if (balanceNonces[to] < nonce) { + _balances[to] = newBalanceTo; + balanceNonces[to] = nonce; + } + } + bytes memory callbackData = _requestCallbacks[sourceRequestId]; + emit Transfer(from, to, senderValue, receiverValue); + _emitConfidentialTransfer(from, to, senderValue, receiverValue); + if (callbackData.length != 0) { + (bool success, ) = address(to).call(callbackData); + if (success) { + delete _requestCallbacks[sourceRequestId]; + } else { + emit RequestCallbackFailed(from, to, sourceRequestId, callbackData); + } + } + } + + /** + * @notice Writes new allowance ciphertext after COTI approved the request. + * @dev Clears the pending approval slot for `(owner, spender)`. + */ + function approveCallback(bytes memory data) external onlyInbox { + (uint256 remoteChainId, address remoteContract) = inbox.inboxMsgSender(); + if (remoteChainId != cotiChainId || remoteContract != cotiSideContract) { + revert OnlyCotiSideContract(remoteChainId, remoteContract); + } + bytes32 sourceRequestId = inbox.inboxSourceRequestId(); + _setRequestStatus(sourceRequestId, IPodERC20.RequestStatus.Success); + (address owner, ctUint256 memory ownerAmount, address spender, ctUint256 memory spenderAmount) = abi.decode( + data, + (address, ctUint256, address, ctUint256) + ); + _pendingApprovalRequestIds[owner][spender] = bytes32(0); + _allowance[owner][spender] = Allowance({spenderCiphertext: spenderAmount, ownerCiphertext: ownerAmount}); + emit Approval(owner, spender, ownerAmount, spenderAmount); + } + + /** + * @notice Applies batched balance ciphertexts from COTI after `syncBalances`. + * @dev Per-account update only if `nonce` is newer than {balanceNonces}; emits {BalanceSynced} for each update applied. + * COTI callback nonces start at 1 when the pToken namespace is registered on the mother. + */ + function syncBalancesCallback(bytes memory data) external onlyInbox { + (uint256 remoteChainId, address remoteContract) = inbox.inboxMsgSender(); + if (remoteChainId != cotiChainId || remoteContract != cotiSideContract) { + revert OnlyCotiSideContract(remoteChainId, remoteContract); + } + bytes32 sourceRequestId = inbox.inboxSourceRequestId(); + _setRequestStatus(sourceRequestId, IPodERC20.RequestStatus.Success); + (address[] memory addresses, ctUint256[] memory amounts, uint256 nonce) = abi.decode( + data, + (address[], ctUint256[], uint256) + ); + for (uint256 i = 0; i < addresses.length; i++) { + if (balanceNonces[addresses[i]] < nonce) { + _balances[addresses[i]] = amounts[i]; + balanceNonces[addresses[i]] = nonce; + emit BalanceSynced(addresses[i], amounts[i]); + } + } + } + + // --- External: inbox callbacks (errors) --- + + /** + * @notice Clears pending transfer state and records `failedRequests` for this `sourceRequestId`. + * @dev **Gotcha:** when both `from` and `to` were locked, both are cleared; `TransferFailed` carries decoded addresses. + */ + function transferError(bytes memory data) external onlyInbox { + (uint256 remoteChainId, address remoteContract) = inbox.inboxMsgSender(); + if (remoteChainId != cotiChainId || remoteContract != cotiSideContract) { + revert OnlyCotiSideContract(remoteChainId, remoteContract); + } + (address from, address to, bytes memory errorMsg) = abi.decode(data, (address, address, bytes)); + bytes32 sourceRequestId = inbox.inboxSourceRequestId(); + _setRequestStatus(sourceRequestId, IPodERC20.RequestStatus.Failed); + failedRequests[sourceRequestId] = errorMsg; + if (from != address(0)) { + _pendingTransferRequestIds[from] = bytes32(0); + } + _pendingTransferRequestIds[to] = bytes32(0); + emit TransferFailed(from, to, errorMsg); + } + + /// @notice Clears pending approval and surfaces COTI error bytes to listeners and {failedRequests}. + function approveError(bytes memory data) external onlyInbox { + (uint256 remoteChainId, address remoteContract) = inbox.inboxMsgSender(); + if (remoteChainId != cotiChainId || remoteContract != cotiSideContract) { + revert OnlyCotiSideContract(remoteChainId, remoteContract); + } + (address owner, address spender, bytes memory errorMsg) = abi.decode(data, (address, address, bytes)); + bytes32 sourceRequestId = inbox.inboxSourceRequestId(); + _setRequestStatus(sourceRequestId, IPodERC20.RequestStatus.Failed); + failedRequests[sourceRequestId] = errorMsg; + _pendingApprovalRequestIds[owner][spender] = bytes32(0); + emit ApprovalFailed(owner, spender, errorMsg); + } + + /// @notice `syncBalances` failed on COTI; `data` is forwarded into {SyncBalancesFailed} for debugging. + function syncBalancesError(bytes memory data) external onlyInbox { + (uint256 remoteChainId, address remoteContract) = inbox.inboxMsgSender(); + if (remoteChainId != cotiChainId || remoteContract != cotiSideContract) { + revert OnlyCotiSideContract(remoteChainId, remoteContract); + } + bytes32 sourceRequestId = inbox.inboxSourceRequestId(); + _setRequestStatus(sourceRequestId, IPodERC20.RequestStatus.Failed); + emit SyncBalancesFailed(sourceRequestId, data); + } + + // --- External: views --- + + /// @inheritdoc IPodERC20 + function balanceOf(address account) external view returns (ctUint256 memory) { + return _balances[account]; + } + + /// @inheritdoc IPodERC20 + function balanceOfWithStatus(address account) external view returns (ctUint256 memory, bool pending) { + return (_balances[account], _pendingTransferRequestIds[account] != bytes32(0)); + } + + /// @inheritdoc IPodERC20 + function allowance(address owner, address spender) external view returns (Allowance memory) { + return _allowance[owner][spender]; + } + + /// @inheritdoc IPodERC20 + function allowanceWithStatus( + address owner, + address spender + ) external view returns (Allowance memory, bool pending) { + return (_allowance[owner][spender], _pendingApprovalRequestIds[owner][spender] != bytes32(0)); + } + + /** + * @notice Estimate the native fee split used by auto-fee two-way token methods. + * @return totalFeeWei Sum of target and callback fee estimates. + * @return targetFeeWei Estimated local-token wei for the remote COTI execution leg. + * @return callbackFeeWei Estimated local-token wei for the PoD callback leg. + */ + function estimateFee() + external + view + returns (uint256 totalFeeWei, uint256 targetFeeWei, uint256 callbackFeeWei) + { + (targetFeeWei, callbackFeeWei) = _estimateTwoWayFeeInLocalToken(); + totalFeeWei = targetFeeWei + callbackFeeWei; + } + + /// @notice EIP-712 domain separator used by {transferFromAndCallWithPermit}. + function publicTransferPermitDomainSeparator() external view returns (bytes32) { + return _publicTransferPermitDomainSeparator(); + } + + // --- Internal --- + + function _initializePodERC20( + uint256 _cotiChainId, + address _inbox, + address _cotiSideContract, + string memory _name, + string memory _symbol + ) internal { + _initializePodERC20(_cotiChainId, _inbox, _cotiSideContract, _name, _symbol, 18); + } + + function _initializePodERC20( + uint256 _cotiChainId, + address _inbox, + address _cotiSideContract, + string memory _name, + string memory _symbol, + uint8 _decimals + ) internal { + if (_podERC20Initialized) { + revert PodERC20AlreadyInitialized(); + } + if (_cotiChainId == 0 || _inbox == address(0) || _cotiSideContract == address(0)) { + revert PodERC20InvalidInitialization(); + } + _podERC20Initialized = true; + setInbox(_inbox); + cotiChainId = _cotiChainId; + cotiSideContract = _cotiSideContract; + name = _name; + symbol = _symbol; + decimals = _decimals; + totalSupply = 0; + } + + /** + * @notice Access-control hook invoked by {mint} overloads; base implementation always reverts with {MintNotAllowed}. + * @dev Override in subclasses (e.g. {PodErc20Mintable}) to whitelist callers. The `msg.sender != address(0)` guard + * is always true at runtime but hides the unconditional revert from the compiler so callers don't trip the + * "unreachable code" warning in the base contract. + */ + function _checkMinter() internal view virtual { + if (msg.sender != address(0)) { + revert MintNotAllowed(msg.sender); + } + } + + /// @param totalValueWei Total native payment (e.g. `msg.value`); `callbackFeeLocalWei` is the caller-supplied callback slice. + function _sendPodTwoWay( + uint256 totalValueWei, + uint256 callbackFeeLocalWei, + IInbox.MpcMethodCall memory mpcMethodCall, + bytes4 callbackSelector_, + bytes4 errorSelector_ + ) internal returns (bytes32) { + require(callbackFeeLocalWei >= 1, "PodERC20: callback fee min"); + require(callbackFeeLocalWei <= totalValueWei, "PodERC20: callback exceeds total"); + require(address(this).balance >= totalValueWei, "PodERC20: inbox fee"); + return IInbox(inbox).sendTwoWayMessage{value: totalValueWei}( + cotiChainId, + cotiSideContract, + mpcMethodCall, + callbackSelector_, + errorSelector_, + callbackFeeLocalWei + ); + } + + function _setRequestStatus(bytes32 requestId, IPodERC20.RequestStatus status) internal { + requests[requestId] = status; + emit RequestStatusUpdated(requestId, status); + } + + function _approve(address owner, address spender, itUint256 calldata value, uint256 totalValueWei, uint256 callbackFeeLocalWei) internal returns (bytes32 requestId) { + if (_pendingApprovalRequestIds[owner][spender] != bytes32(0)) { + revert ApprovalAlreadyPending(owner, spender, _pendingApprovalRequestIds[owner][spender]); + } + IInbox.MpcMethodCall memory mpcMethodCall = MpcAbiCodec.create(IPodErc20CotiSide.approve.selector, 3) + .addArgument(owner) + .addArgument(spender) + .addArgument(value) + .build(); + + requestId = _sendPodTwoWay( + totalValueWei, + callbackFeeLocalWei, + mpcMethodCall, + PodERC20.approveCallback.selector, + PodERC20.approveError.selector + ); + _setRequestStatus(requestId, IPodERC20.RequestStatus.Pending); + _pendingApprovalRequestIds[owner][spender] = requestId; + emit ApprovalRequestSubmitted(owner, spender, requestId); + } + + function _burn(address from, itUint256 calldata value, uint256 totalValueWei, uint256 callbackFeeLocalWei) internal returns (bytes32 requestId) { + if (_pendingTransferRequestIds[from] != bytes32(0)) { + revert TransferAlreadyPending(from, address(0), _pendingTransferRequestIds[from]); + } + + IInbox.MpcMethodCall memory mpcMethodCall = MpcAbiCodec.create(IPodErc20CotiSide.burn.selector, 2) + .addArgument(from) + .addArgument(value) + .build(); + + requestId = _sendPodTwoWay( + totalValueWei, + callbackFeeLocalWei, + mpcMethodCall, + PodERC20.transferCallback.selector, + PodERC20.transferError.selector + ); + + _setRequestStatus(requestId, IPodERC20.RequestStatus.Pending); + _pendingTransferRequestIds[from] = requestId; + emit TransferRequestSubmitted(from, address(0), requestId); + } + + /** + * @dev **Gotcha:** `TransferAlreadyPending` carries `_pendingTransferRequestIds[from]` even when `to` was the party that + * was actually pending—inspect both sides off-chain when debugging reverts. + */ + function _transfer( + bytes4 remoteTransferSelector, + address from, + address to, + itUint256 calldata value, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32 requestId) { + if (_pendingTransferRequestIds[from] != bytes32(0) || _pendingTransferRequestIds[to] != bytes32(0)) { + revert TransferAlreadyPending(from, to, _pendingTransferRequestIds[from]); + } + + IInbox.MpcMethodCall memory mpcMethodCall = MpcAbiCodec.create(remoteTransferSelector, 3) + .addArgument(from) + .addArgument(to) + .addArgument(value) + .build(); + + requestId = _sendPodTwoWay( + totalValueWei, + callbackFeeLocalWei, + mpcMethodCall, + PodERC20.transferCallback.selector, + PodERC20.transferError.selector + ); + _setRequestStatus(requestId, IPodERC20.RequestStatus.Pending); + _pendingTransferRequestIds[from] = requestId; + _pendingTransferRequestIds[to] = requestId; + emit TransferRequestSubmitted(msg.sender, to, requestId); + } + + function _transferFrom( + bytes4 remoteTransferSelector, + address spender, + address from, + address to, + itUint256 calldata value, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32 requestId) { + if (_pendingTransferRequestIds[from] != bytes32(0) || _pendingTransferRequestIds[to] != bytes32(0)) { + revert TransferAlreadyPending(from, to, _pendingTransferRequestIds[from]); + } + + IInbox.MpcMethodCall memory mpcMethodCall = MpcAbiCodec.create(remoteTransferSelector, 4) + .addArgument(spender) + .addArgument(from) + .addArgument(to) + .addArgument(value) + .build(); + + requestId = _sendPodTwoWay( + totalValueWei, + callbackFeeLocalWei, + mpcMethodCall, + PodERC20.transferCallback.selector, + PodERC20.transferError.selector + ); + _setRequestStatus(requestId, IPodERC20.RequestStatus.Pending); + _pendingTransferRequestIds[from] = requestId; + _pendingTransferRequestIds[to] = requestId; + emit TransferRequestSubmitted(from, to, requestId); + } + + /** + * @notice Shared two-way-fee estimator used by the auto-fee overloads. + * @dev Uses {InboxFeeManager.calculateTwoWayFeeRequiredInLocalToken} with heuristic calldata/execution sizes. + * Amounts returned are in local (PoD) wei; the caller must send `msg.value >= targetFeeWei + callbackFeeWei`. + */ + function _estimateTwoWayFeeInLocalToken() + internal + view + returns (uint256 targetFeeWei, uint256 callbackFeeWei) + { + (targetFeeWei, callbackFeeWei) = IInboxFeeManager(address(inbox)).calculateTwoWayFeeRequiredInLocalToken( + FEE_ESTIMATE_REMOTE_CALL_SIZE, + FEE_ESTIMATE_CALLBACK_CALL_SIZE, + FEE_ESTIMATE_REMOTE_EXEC_GAS, + FEE_ESTIMATE_CALLBACK_EXEC_GAS, + tx.gasprice + ); + } + + function _consumePublicTransferPermit( + address owner, + address spender, + address to, + uint256 amount, + PublicPermit calldata permit + ) internal { + if (block.timestamp > permit.deadline) { + revert PermitExpired(permit.deadline); + } + + uint256 nonce = nonces[owner]; + bytes32 structHash = keccak256( + abi.encode(PUBLIC_TRANSFER_PERMIT_TYPEHASH, owner, spender, to, amount, nonce, permit.deadline) + ); + bytes32 digest = keccak256(abi.encodePacked("\x19\x01", _publicTransferPermitDomainSeparator(), structHash)); + address signer = ECDSA.recover(digest, permit.v, permit.r, permit.s); + if (signer != owner) { + revert InvalidPermitSigner(signer, owner); + } + nonces[owner] = nonce + 1; + } + + function _publicTransferPermitDomainSeparator() internal view returns (bytes32) { + return keccak256( + abi.encode( + DOMAIN_TYPEHASH, + keccak256(bytes(name)), + PERMIT_DOMAIN_VERSION_HASH, + block.chainid, + address(this) + ) + ); + } + + /// @dev Encrypted mint: sends `(to, amount)` to COTI's {IPodErc20CotiSide.mint} and locks `to`'s pending slot. + function _mint( + address to, + itUint256 calldata amount, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32 requestId) { + if (_pendingTransferRequestIds[to] != bytes32(0)) { + revert TransferAlreadyPending(address(0), to, _pendingTransferRequestIds[to]); + } + + IInbox.MpcMethodCall memory mpcMethodCall = MpcAbiCodec.create(IPodErc20CotiSide.mint.selector, 2) + .addArgument(to) + .addArgument(amount) + .build(); + + requestId = _sendPodTwoWay( + totalValueWei, + callbackFeeLocalWei, + mpcMethodCall, + PodERC20.transferCallback.selector, + PodERC20.transferError.selector + ); + _setRequestStatus(requestId, IPodERC20.RequestStatus.Pending); + _pendingTransferRequestIds[to] = requestId; + emit TransferRequestSubmitted(address(0), to, requestId); + } + + /// @dev Plain-uint256 transfer / transferFrom; sends to `IPodErc20CotiSide.transferPublic` / `transferFromPublic`. + function _transferPublic( + bytes4 remoteSelector, + address from, + address to, + uint256 amount, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32 requestId) { + if (_pendingTransferRequestIds[from] != bytes32(0) || _pendingTransferRequestIds[to] != bytes32(0)) { + revert TransferAlreadyPending(from, to, _pendingTransferRequestIds[from]); + } + + IInbox.MpcMethodCall memory mpcMethodCall = MpcAbiCodec.create(remoteSelector, 3) + .addArgument(from) + .addArgument(to) + .addArgument(amount) + .build(); + + requestId = _sendPodTwoWay( + totalValueWei, + callbackFeeLocalWei, + mpcMethodCall, + PodERC20.transferCallback.selector, + PodERC20.transferError.selector + ); + _setRequestStatus(requestId, IPodERC20.RequestStatus.Pending); + _pendingTransferRequestIds[from] = requestId; + _pendingTransferRequestIds[to] = requestId; + emit TransferRequestSubmitted(msg.sender, to, requestId); + } + + function _transferPublicFrom( + bytes4 remoteSelector, + address spender, + address from, + address to, + uint256 amount, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32 requestId) { + if (_pendingTransferRequestIds[from] != bytes32(0) || _pendingTransferRequestIds[to] != bytes32(0)) { + revert TransferAlreadyPending(from, to, _pendingTransferRequestIds[from]); + } + + IInbox.MpcMethodCall memory mpcMethodCall = MpcAbiCodec.create(remoteSelector, 4) + .addArgument(spender) + .addArgument(from) + .addArgument(to) + .addArgument(amount) + .build(); + + requestId = _sendPodTwoWay( + totalValueWei, + callbackFeeLocalWei, + mpcMethodCall, + PodERC20.transferCallback.selector, + PodERC20.transferError.selector + ); + _setRequestStatus(requestId, IPodERC20.RequestStatus.Pending); + _pendingTransferRequestIds[from] = requestId; + _pendingTransferRequestIds[to] = requestId; + emit TransferRequestSubmitted(from, to, requestId); + } + + function _transferPublicFromWithPermit( + address spender, + address from, + address to, + uint256 amount, + PublicPermit calldata permit, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32 requestId) { + if (_pendingTransferRequestIds[from] != bytes32(0) || _pendingTransferRequestIds[to] != bytes32(0)) { + revert TransferAlreadyPending(from, to, _pendingTransferRequestIds[from]); + } + _consumePublicTransferPermit(from, spender, to, amount, permit); + + IInbox.MpcMethodCall memory mpcMethodCall = MpcAbiCodec.create(IPodErc20CotiSide.transferFromPublic.selector, 3) + .addArgument(from) + .addArgument(to) + .addArgument(amount) + .build(); + + requestId = _sendPodTwoWay( + totalValueWei, + callbackFeeLocalWei, + mpcMethodCall, + PodERC20.transferCallback.selector, + PodERC20.transferError.selector + ); + _setRequestStatus(requestId, IPodERC20.RequestStatus.Pending); + _pendingTransferRequestIds[from] = requestId; + _pendingTransferRequestIds[to] = requestId; + emit TransferRequestSubmitted(from, to, requestId); + } + + /// @dev Plain-uint256 approve; sends to `IPodErc20CotiSide.approvePublic`. + function _approvePublic( + address owner, + address spender, + uint256 amount, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32 requestId) { + if (_pendingApprovalRequestIds[owner][spender] != bytes32(0)) { + revert ApprovalAlreadyPending(owner, spender, _pendingApprovalRequestIds[owner][spender]); + } + + IInbox.MpcMethodCall memory mpcMethodCall = MpcAbiCodec.create(IPodErc20CotiSide.approvePublic.selector, 3) + .addArgument(owner) + .addArgument(spender) + .addArgument(amount) + .build(); + + requestId = _sendPodTwoWay( + totalValueWei, + callbackFeeLocalWei, + mpcMethodCall, + PodERC20.approveCallback.selector, + PodERC20.approveError.selector + ); + _setRequestStatus(requestId, IPodERC20.RequestStatus.Pending); + _pendingApprovalRequestIds[owner][spender] = requestId; + emit ApprovalRequestSubmitted(owner, spender, requestId); + } + + /// @dev Plain-uint256 burn; sends to `IPodErc20CotiSide.burnPublic`. + function _burnPublic( + address from, + uint256 amount, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32 requestId) { + if (_pendingTransferRequestIds[from] != bytes32(0)) { + revert TransferAlreadyPending(from, address(0), _pendingTransferRequestIds[from]); + } + + IInbox.MpcMethodCall memory mpcMethodCall = MpcAbiCodec.create(IPodErc20CotiSide.burnPublic.selector, 2) + .addArgument(from) + .addArgument(amount) + .build(); + + requestId = _sendPodTwoWay( + totalValueWei, + callbackFeeLocalWei, + mpcMethodCall, + PodERC20.transferCallback.selector, + PodERC20.transferError.selector + ); + _setRequestStatus(requestId, IPodERC20.RequestStatus.Pending); + _pendingTransferRequestIds[from] = requestId; + emit TransferRequestSubmitted(from, address(0), requestId); + } + + /// @dev Plain-uint256 mint; sends to `IPodErc20CotiSide.mintPublic`. + function _mintPublic( + address to, + uint256 amount, + uint256 totalValueWei, + uint256 callbackFeeLocalWei + ) internal returns (bytes32 requestId) { + if (_pendingTransferRequestIds[to] != bytes32(0)) { + revert TransferAlreadyPending(address(0), to, _pendingTransferRequestIds[to]); + } + + IInbox.MpcMethodCall memory mpcMethodCall = MpcAbiCodec.create(IPodErc20CotiSide.mintPublic.selector, 2) + .addArgument(to) + .addArgument(amount) + .build(); + + requestId = _sendPodTwoWay( + totalValueWei, + callbackFeeLocalWei, + mpcMethodCall, + PodERC20.transferCallback.selector, + PodERC20.transferError.selector + ); + _setRequestStatus(requestId, IPodERC20.RequestStatus.Pending); + _pendingTransferRequestIds[to] = requestId; + emit TransferRequestSubmitted(address(0), to, requestId); + } + + // --- ERC-7984 mixin hooks --- + + function _erc7984BalanceOf(address account) internal view override returns (ctUint256 memory) { + return _balances[account]; + } +} diff --git a/pod/token/perc20/PodErc20Mintable.sol b/pod/token/perc20/PodErc20Mintable.sol new file mode 100644 index 0000000..cfd31a6 --- /dev/null +++ b/pod/token/perc20/PodErc20Mintable.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "./PodERC20.sol"; + +/// @title PodErc20Mintable +/// @notice {PodERC20} variant that permits `mint` calls from a single immutable `minter` address. +/// @dev The minter is set at construction and cannot change. To rotate minters, deploy a new token or subclass with +/// a mutable `minter` plus access control. All other behavior (transfers, approvals, burns, syncing) is inherited +/// verbatim from {PodERC20}. +contract PodErc20Mintable is PodERC20 { + /// @notice Sole address allowed to call {PodERC20.mint} (encrypted or plain variant). + address public minter; + bool private _mintableInitialized; + + /// @notice Thrown when a non-minter tries to mint; carries the caller for debugging. + error OnlyMinter(address caller); + /// @notice Mintable storage was already initialized. + error PodErc20MintableAlreadyInitialized(); + /// @notice Minter was the zero address. + error PodErc20MintableInvalidMinter(); + + /** + * @param _minter Address authorized to mint; must not be zero. + * @param _cotiChainId See {PodERC20}. + * @param _inbox See {PodERC20}. + * @param _cotiSideContract See {PodERC20}. + * @param _name See {PodERC20}. + * @param _symbol See {PodERC20}. + */ + constructor( + address _minter, + uint256 _cotiChainId, + address _inbox, + address _cotiSideContract, + string memory _name, + string memory _symbol + ) PodERC20(_cotiChainId, _inbox, _cotiSideContract, _name, _symbol) { + _initializeMintableMinter(_minter); + } + + /// @inheritdoc PodERC20 + /// @dev Allows the call only when `msg.sender == minter`; reverts with {OnlyMinter} otherwise. + function _checkMinter() internal view override { + if (msg.sender != minter) { + revert OnlyMinter(msg.sender); + } + } + + /// @notice Initialize mintable token storage with default 18 decimals. + function _initializePodErc20Mintable( + address _minter, + uint256 _cotiChainId, + address _inbox, + address _cotiSideContract, + string memory _name, + string memory _symbol + ) internal { + _initializePodErc20Mintable(_minter, _cotiChainId, _inbox, _cotiSideContract, _name, _symbol, 18); + } + + /// @notice Initialize mintable token storage with explicit decimals. + function _initializePodErc20Mintable( + address _minter, + uint256 _cotiChainId, + address _inbox, + address _cotiSideContract, + string memory _name, + string memory _symbol, + uint8 _decimals + ) internal { + _initializePodERC20(_cotiChainId, _inbox, _cotiSideContract, _name, _symbol, _decimals); + _initializeMintableMinter(_minter); + } + + /// @notice Initialize the immutable-style minter slot for constructors and clones. + /// @param _minter Address authorized to mint; must not be zero. + function _initializeMintableMinter(address _minter) internal { + if (_mintableInitialized) { + revert PodErc20MintableAlreadyInitialized(); + } + if (_minter == address(0)) { + revert PodErc20MintableInvalidMinter(); + } + _mintableInitialized = true; + minter = _minter; + } +} diff --git a/pod/token/perc20/PodErc20MintableInitializable.sol b/pod/token/perc20/PodErc20MintableInitializable.sol new file mode 100644 index 0000000..84a8b6a --- /dev/null +++ b/pod/token/perc20/PodErc20MintableInitializable.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "./PodErc20Mintable.sol"; + +/// @title PodErc20MintableInitializable +/// @notice Clone-friendly {PodErc20Mintable}; the implementation constructor only locks the implementation instance. +contract PodErc20MintableInitializable is PodErc20Mintable { + /// @notice Lock the implementation instance with placeholder values. + constructor() PodErc20Mintable(address(1), 1, address(1), address(1), "IMPLEMENTATION", "IMPL") {} + + /// @notice Initialize a mintable source-chain pToken clone. + /// @param _minter Address allowed to mint. + /// @param _cotiChainId COTI chain id for remote MPC execution. + /// @param _inbox Source-chain inbox. + /// @param _cotiSideContract COTI-side pToken ledger. + /// @param _name Token name. + /// @param _symbol Token symbol. + /// @param _decimals Token decimals. + function initialize( + address _minter, + uint256 _cotiChainId, + address _inbox, + address _cotiSideContract, + string memory _name, + string memory _symbol, + uint8 _decimals + ) external { + _initializePodErc20Mintable(_minter, _cotiChainId, _inbox, _cotiSideContract, _name, _symbol, _decimals); + } +} diff --git a/pod/token/perc20/cotiside/IPodErc20CotiSide.sol b/pod/token/perc20/cotiside/IPodErc20CotiSide.sol new file mode 100644 index 0000000..11fa914 --- /dev/null +++ b/pod/token/perc20/cotiside/IPodErc20CotiSide.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "../../../../utils/mpc/MpcCore.sol"; + +/** + * @title IPodErc20CotiSide + * @notice Entry points the COTI inbox invokes for a paired {PodERC20}: balance/allowance ciphertext on-chain, MPC garbling in memory, and `respond`/`raise` wiring. + * @dev Implementations must restrict callers to the inbox and to the configured remote `PodERC20`. `transferFrom` does not carry + * `msg.sender` as spender on this chain—allowance must be enforced before the cross-chain message is sent. + */ +interface IPodErc20CotiSide { + /** + * @notice Owner-only mint of plain `amount` into balance ciphertext storage for `to` (bridge / test setup). + * @dev Does not automatically update PoD ciphertext; use {syncBalances} on `PodERC20` after minting if mirrors must match. + * Kept distinct from the inbox-called {mint} / {mintPublic} so `.selector` access stays unambiguous. + */ + function ownerMint(address to, uint256 amount) external; + + /** + * @notice Inbox-only mint with garbled `value`; responds with a {PodERC20.transferCallback}-shaped tuple so PoD updates `to`'s ciphertext. + */ + function mint(address to, gtUint256 value) external; + + /** + * @notice Inbox-only mint using a plain `amount`; COTI garbles via `MpcCore.setPublic256` and responds as in {mint}. + */ + function mintPublic(address to, uint256 amount) external; + + /** + * @notice For each account, `onBoard`s stored balance ciphertext, `offBoardToUser`s to that address, and `respond`s with `(addresses, amounts, nonce)`. + * @dev **Gotcha:** empty `accounts` should be rejected by the implementation; otherwise PoD may receive useless callbacks. + */ + function syncBalances(address[] calldata accounts) external; + + /** + * @notice Moves `value` garbled tokens from `from` to `to` if balance suffices, then `respond`s with the PoD transfer tuple. + * @dev **Gotcha:** locks on PoD are tracked separately; this function assumes the inbox message is well-formed. + */ + function transfer(address from, address to, gtUint256 value) external; + + /** + * @notice Plain-amount variant of {transfer}; COTI garbles via `MpcCore.setPublic256`. + */ + function transferPublic(address from, address to, uint256 value) external; + + /** + * @notice Legacy same MPC move as {transfer}; kept for compatibility with older PoD tokens. + * @dev New allowance-based integrations should use {transferFromAsSpender}. + */ + function transferFrom(address from, address to, gtUint256 value) external; + + /** + * @notice Legacy plain-amount variant of {transferFrom}; kept for compatibility with older PoD tokens. + */ + function transferFromPublic(address from, address to, uint256 value) external; + + /** + * @notice Spender-aware transferFrom that consumes allowance on COTI before moving garbled `value`. + */ + function transferFromAsSpender(address spender, address from, address to, gtUint256 value) external; + + /** + * @notice Plain-amount variant of {transferFromAsSpender}. + */ + function transferFromPublicAsSpender(address spender, address from, address to, uint256 value) external; + + /** + * @notice Sets garbled allowance and `respond`s with owner- and spender-specific ciphertext of the same allowance amount. + * @dev On invalid addresses the implementation should `raise` rather than revert if you need PoD `approveError` symmetry. + */ + function approve(address owner, address spender, gtUint256 value) external; + + /** + * @notice Plain-amount variant of {approve}; COTI garbles via `MpcCore.setPublic256`. + */ + function approvePublic(address owner, address spender, uint256 value) external; + + /** + * @notice Subtracts `value` from `from` and responds with a burn-shaped tuple (`to == 0`, zero ciphertexts for receiver side). + */ + function burn(address from, gtUint256 value) external; + + /** + * @notice Plain-amount variant of {burn}; COTI garbles via `MpcCore.setPublic256`. + */ + function burnPublic(address from, uint256 value) external; +} diff --git a/pod/token/perc20/cotiside/PodErc20CotiMother.sol b/pod/token/perc20/cotiside/PodErc20CotiMother.sol new file mode 100644 index 0000000..447bd77 --- /dev/null +++ b/pod/token/perc20/cotiside/PodErc20CotiMother.sol @@ -0,0 +1,520 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.19; + +import "../../../../utils/mpc/MpcCore.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +import "../../../InboxUser.sol"; +import "./IPodErc20CotiSide.sol"; + +/// @title PodErc20CotiMother +/// @notice Unified COTI-side ledger for all paired {PodERC20} tokens. Balances are namespaced by `(sourceChainId, sourcePToken)`. +/// @dev Source-chain {PrivacyPortalFactory} registers tokens via one-way inbox messages. MPC ops use {inboxMsgSender} as token context. +contract PodErc20CotiMother is IPodErc20CotiSide, InboxUser, Ownable { + // --- Types --- + + struct TokenMeta { + string name; + string symbol; + uint8 decimals; + } + + // --- State variables --- + + /// @dev Per-token monotonic nonce included in PoD callbacks. Starts at 1 on registration so the first + /// callback satisfies PoD `balanceNonces[account] < nonce` (PoD nonces default to 0). + mapping(bytes32 => uint256) private _tokenNonce; + uint256 private constant INITIAL_TOKEN_NONCE = 1; + /// @dev Balance ciphertext per token and account. + mapping(bytes32 => mapping(address => ctUint256)) private _balanceCiphertext; + /// @dev Allowance ciphertext per token: `owner => spender => ctUint256`. + mapping(bytes32 => mapping(address => mapping(address => ctUint256))) private _allowanceCiphertext; + /// @dev Whether a `(sourceChainId, remotePToken)` namespace is registered. + mapping(bytes32 => bool) public registered; + /// @dev Public metadata recorded at registration. + mapping(bytes32 => TokenMeta) public tokenMeta; + /// @dev Allowlisted source-chain factories that may register tokens. + mapping(uint256 => mapping(address => bool)) public allowedFactories; + + // --- Events --- + + event CotiMotherInitialized(address indexed inbox, address indexed owner); + event AllowedFactoryUpdated(uint256 indexed sourceChainId, address indexed factory, bool allowed); + event TokenRegistered( + uint256 indexed sourceChainId, + address indexed remotePToken, + string name, + string symbol, + uint8 decimals + ); + event SyncBalancesResponded(bytes32 indexed tokenId, address[] accounts, uint256 nonce); + event TransferCompleted( + bytes32 indexed tokenId, + address indexed spender, + address indexed from, + address to, + bool allowanceSpent, + bool amountIsPublic, + uint256 publicAmount, + uint256 nonce + ); + event BurnCompleted( + bytes32 indexed tokenId, + address indexed from, + bool amountIsPublic, + uint256 publicAmount, + uint256 nonce + ); + event MintCompleted( + bytes32 indexed tokenId, + address indexed to, + bool amountIsPublic, + uint256 publicAmount, + uint256 nonce + ); + event ApprovalCompleted( + bytes32 indexed tokenId, + address indexed owner, + address indexed spender, + bool amountIsPublic, + uint256 publicAmount + ); + event TransferFailureRaised(bytes32 indexed tokenId, address indexed from, address indexed to, bytes reason); + event ApprovalFailureRaised(bytes32 indexed tokenId, address indexed owner, address indexed spender, bytes reason); + event SyncBalancesFailureRaised(bytes32 indexed tokenId, bytes reason); + + // --- Errors --- + + error InvalidAddress(); + error FactoryNotAllowed(uint256 sourceChainId, address factory); + error InvalidRemotePToken(); + error TokenAlreadyRegistered(bytes32 tokenId); + error TokenNotRegistered(bytes32 tokenId); + error MintToZeroAddress(); + error OwnerMintNotSupported(); + error ChainIdOverflow(uint256 sourceChainId); + + // --- Modifiers --- + + modifier onlyRegisteredFactoryMessage() { + if (msg.sender != address(inbox)) { + revert OnlyInbox(msg.sender); + } + (uint256 sourceChainId, address factory) = inbox.inboxMsgSender(); + if (!allowedFactories[sourceChainId][factory]) { + revert FactoryNotAllowed(sourceChainId, factory); + } + _; + } + + modifier onlyRegisteredPTokenMessage() { + if (msg.sender != address(inbox)) { + revert OnlyInbox(msg.sender); + } + (uint256 sourceChainId, address remotePToken) = inbox.inboxMsgSender(); + bytes32 id = _tokenId(sourceChainId, remotePToken); + if (!registered[id]) { + revert TokenNotRegistered(id); + } + _; + } + + // --- Constructor --- + + /// @param inboxAddress COTI-side inbox allowed to deliver remote pToken requests. + /// @param initialOwner Owner allowed to configure factories. + constructor(address inboxAddress, address initialOwner) Ownable(initialOwner) { + if (inboxAddress == address(0) || initialOwner == address(0)) { + revert InvalidAddress(); + } + setInbox(inboxAddress); + emit CotiMotherInitialized(inboxAddress, initialOwner); + } + + // --- External: views --- + + /// @notice Compute the namespace id for a source-chain pToken. + function tokenId(uint256 sourceChainId, address remotePToken) public pure returns (bytes32) { + return _tokenId(sourceChainId, remotePToken); + } + + /// @notice Whether a source-chain pToken namespace is registered. + function isRegistered(uint256 sourceChainId, address remotePToken) external view returns (bool) { + return registered[_tokenId(sourceChainId, remotePToken)]; + } + + // --- External: owner --- + + /// @notice Allow or disallow a source-chain factory to register tokens. + function setAllowedFactory(uint256 sourceChainId, address factory, bool allowed) external onlyOwner { + if (sourceChainId == 0 || factory == address(0)) { + revert InvalidAddress(); + } + allowedFactories[sourceChainId][factory] = allowed; + emit AllowedFactoryUpdated(sourceChainId, factory, allowed); + } + + /// @inheritdoc IPodErc20CotiSide + /// @dev Not supported on the unified mother ledger; mint via inbox {mintPublic} from the paired pToken minter. + function ownerMint(address, uint256) external pure override { + revert OwnerMintNotSupported(); + } + + // --- External: registration --- + + /// @notice Register a new source-chain pToken namespace. Callable only via inbox from an allowlisted factory. + function registerToken( + address remotePToken, + string calldata name, + string calldata symbol, + uint8 decimals + ) external onlyRegisteredFactoryMessage { + if (remotePToken == address(0)) { + revert InvalidRemotePToken(); + } + + (uint256 sourceChainId,) = inbox.inboxMsgSender(); + bytes32 id = _tokenId(sourceChainId, remotePToken); + if (registered[id]) { + revert TokenAlreadyRegistered(id); + } + + registered[id] = true; + tokenMeta[id] = TokenMeta(name, symbol, decimals); + _tokenNonce[id] = INITIAL_TOKEN_NONCE; + emit TokenRegistered(sourceChainId, remotePToken, name, symbol, decimals); + } + + // --- External: inbox + registered pToken --- + + /// @inheritdoc IPodErc20CotiSide + function syncBalances(address[] calldata accounts) external override onlyRegisteredPTokenMessage { + bytes32 id = _activeTokenId(); + if (accounts.length == 0) { + _sendSyncFailureToPod(id, bytes("PodErc20CotiMother: empty accounts")); + return; + } + + uint256 count = accounts.length; + address[] memory addresses = new address[](count); + ctUint256[] memory ciphertextAmounts = new ctUint256[](count); + + for (uint256 i = 0; i < count; ++i) { + address account = accounts[i]; + addresses[i] = account; + gtUint256 garbledBalance = _readGarbledBalance(id, account); + ciphertextAmounts[i] = MpcCore.offBoardToUser(garbledBalance, account); + } + + uint256 callbackNonce = _tokenNonce[id]; + inbox.respond(abi.encode(addresses, ciphertextAmounts, callbackNonce)); + emit SyncBalancesResponded(id, addresses, callbackNonce); + _tokenNonce[id] = callbackNonce + 1; + } + + /// @inheritdoc IPodErc20CotiSide + function transfer(address from, address to, gtUint256 value) external override onlyRegisteredPTokenMessage { + _moveOrBurn(_activeTokenId(), from, to, value, false, false, 0); + } + + /// @inheritdoc IPodErc20CotiSide + function transferPublic(address from, address to, uint256 value) external override onlyRegisteredPTokenMessage { + _moveOrBurn(_activeTokenId(), from, to, MpcCore.setPublic256(value), false, true, value); + } + + /// @inheritdoc IPodErc20CotiSide + function transferFrom( + address from, + address to, + gtUint256 value + ) external override onlyRegisteredPTokenMessage { + _moveOrBurn(_activeTokenId(), from, to, value, false, false, 0); + } + + /// @inheritdoc IPodErc20CotiSide + function transferFromPublic( + address from, + address to, + uint256 value + ) external override onlyRegisteredPTokenMessage { + _moveOrBurn(_activeTokenId(), from, to, MpcCore.setPublic256(value), false, true, value); + } + + /// @inheritdoc IPodErc20CotiSide + function transferFromAsSpender( + address spender, + address from, + address to, + gtUint256 value + ) external override onlyRegisteredPTokenMessage { + _moveWithOptionalAllowance( + _activeTokenId(), spender, from, to, value, true, false, false, 0 + ); + } + + /// @inheritdoc IPodErc20CotiSide + function transferFromPublicAsSpender( + address spender, + address from, + address to, + uint256 value + ) external override onlyRegisteredPTokenMessage { + _moveWithOptionalAllowance( + _activeTokenId(), spender, from, to, MpcCore.setPublic256(value), true, false, true, value + ); + } + + /// @inheritdoc IPodErc20CotiSide + function approve( + address owner, + address spender, + gtUint256 value + ) external override onlyRegisteredPTokenMessage { + _approveInternal(_activeTokenId(), owner, spender, value, false, 0); + } + + /// @inheritdoc IPodErc20CotiSide + function approvePublic( + address owner, + address spender, + uint256 value + ) external override onlyRegisteredPTokenMessage { + _approveInternal(_activeTokenId(), owner, spender, MpcCore.setPublic256(value), true, value); + } + + /// @inheritdoc IPodErc20CotiSide + function burn(address from, gtUint256 value) external override onlyRegisteredPTokenMessage { + _moveOrBurn(_activeTokenId(), from, address(0), value, true, false, 0); + } + + /// @inheritdoc IPodErc20CotiSide + function burnPublic(address from, uint256 value) external override onlyRegisteredPTokenMessage { + _moveOrBurn(_activeTokenId(), from, address(0), MpcCore.setPublic256(value), true, true, value); + } + + /// @inheritdoc IPodErc20CotiSide + function mint(address to, gtUint256 value) external override onlyRegisteredPTokenMessage { + _mintInternal(_activeTokenId(), to, value, false, 0); + } + + /// @inheritdoc IPodErc20CotiSide + function mintPublic(address to, uint256 value) external override onlyRegisteredPTokenMessage { + _mintInternal(_activeTokenId(), to, MpcCore.setPublic256(value), true, value); + } + + // --- Internal: token context --- + + /// @dev Packs `sourceChainId` (uint64) in the high 64 bits and `remotePToken` (uint160) in the low 160 bits. + function _tokenId(uint256 sourceChainId, address remotePToken) internal pure returns (bytes32) { + if (sourceChainId > type(uint64).max) { + revert ChainIdOverflow(sourceChainId); + } + return bytes32((uint256(uint64(sourceChainId)) << 160) | uint256(uint160(remotePToken))); + } + + function _activeTokenId() internal view returns (bytes32) { + (uint256 sourceChainId, address remotePToken) = inbox.inboxMsgSender(); + return _tokenId(sourceChainId, remotePToken); + } + + // --- Private: garbled balance helpers --- + + function _ciphertextPlainZero() private returns (ctUint256 memory) { + return MpcCore.offBoard(MpcCore.setPublic256(0)); + } + + function _isEmptyCtUint256(ctUint256 memory ct) private pure returns (bool) { + return ctUint128.unwrap(ct.ciphertextHigh) == 0 && ctUint128.unwrap(ct.ciphertextLow) == 0; + } + + function _readGarbledBalance(bytes32 id, address account) private returns (gtUint256) { + ctUint256 memory ct = _balanceCiphertext[id][account]; + if (_isEmptyCtUint256(ct)) { + return MpcCore.onBoard(_ciphertextPlainZero()); + } + return MpcCore.onBoard(ct); + } + + function _writeGarbledBalance(bytes32 id, address account, gtUint256 newBalance) private { + _balanceCiphertext[id][account] = MpcCore.offBoard(newBalance); + } + + function _moveOrBurn( + bytes32 id, + address from, + address to, + gtUint256 amount, + bool burning, + bool amountIsPublic, + uint256 publicAmount + ) private { + _moveWithOptionalAllowance(id, address(0), from, to, amount, false, burning, amountIsPublic, publicAmount); + } + + function _moveWithOptionalAllowance( + bytes32 id, + address spender, + address from, + address to, + gtUint256 amount, + bool spendAllowance, + bool burning, + bool amountIsPublic, + uint256 publicAmount + ) private { + if (from == address(0)) { + _sendTransferFailureToPod(id, from, to, bytes("PodErc20CotiMother: zero from")); + return; + } + if (!burning && to == address(0)) { + _sendTransferFailureToPod(id, from, to, bytes("PodErc20CotiMother: zero to")); + return; + } + + gtUint256 senderBalance = _readGarbledBalance(id, from); + + if (!MpcCore.decrypt(MpcCore.ge(senderBalance, amount))) { + _sendTransferFailureToPod(id, from, to, bytes("PodErc20CotiMother: insufficient balance")); + return; + } + + gtUint256 allowanceAfter; + if (spendAllowance && spender != from) { + gtUint256 currentAllowance = _readGarbledAllowance(id, from, spender); + if (!MpcCore.decrypt(MpcCore.ge(currentAllowance, amount))) { + _sendTransferFailureToPod(id, from, to, bytes("PodErc20CotiMother: insufficient allowance")); + return; + } + allowanceAfter = MpcCore.sub(currentAllowance, amount); + } + + gtUint256 senderAfter = MpcCore.sub(senderBalance, amount); + _writeGarbledBalance(id, from, senderAfter); + if (spendAllowance && spender != from) { + _allowanceCiphertext[id][from][spender] = MpcCore.offBoard(allowanceAfter); + } + + if (burning) { + ctUint256 memory zeroCiphertext = _ciphertextPlainZero(); + uint256 burnNonce = _tokenNonce[id]; + inbox.respond( + abi.encode( + from, + MpcCore.offBoardToUser(senderAfter, from), + MpcCore.offBoardToUser(amount, from), + address(0), + zeroCiphertext, + zeroCiphertext, + burnNonce + ) + ); + emit BurnCompleted(id, from, amountIsPublic, publicAmount, burnNonce); + _tokenNonce[id] = burnNonce + 1; + return; + } + + gtUint256 recipientBefore = _readGarbledBalance(id, to); + gtUint256 recipientAfter = MpcCore.add(recipientBefore, amount); + _writeGarbledBalance(id, to, recipientAfter); + + uint256 transferNonce = _tokenNonce[id]; + inbox.respond(_encodePodTransferCallback(from, to, amount, senderAfter, recipientAfter, transferNonce)); + emit TransferCompleted( + id, spender, from, to, spendAllowance && spender != from, amountIsPublic, publicAmount, transferNonce + ); + _tokenNonce[id] = transferNonce + 1; + } + + function _readGarbledAllowance(bytes32 id, address owner, address spender) private returns (gtUint256) { + ctUint256 memory ct = _allowanceCiphertext[id][owner][spender]; + if (_isEmptyCtUint256(ct)) { + return MpcCore.onBoard(_ciphertextPlainZero()); + } + return MpcCore.onBoard(ct); + } + + function _encodePodTransferCallback( + address from, + address to, + gtUint256 amount, + gtUint256 senderBalanceAfter, + gtUint256 recipientBalanceAfter, + uint256 callbackNonce + ) private returns (bytes memory) { + ctUint256 memory senderBalanceCt = MpcCore.offBoardToUser(senderBalanceAfter, from); + ctUint256 memory amountForSender = MpcCore.offBoardToUser(amount, from); + ctUint256 memory recipientBalanceCt = MpcCore.offBoardToUser(recipientBalanceAfter, to); + ctUint256 memory amountForRecipient = MpcCore.offBoardToUser(amount, to); + return abi.encode(from, senderBalanceCt, amountForSender, to, recipientBalanceCt, amountForRecipient, callbackNonce); + } + + function _approveInternal( + bytes32 id, + address owner, + address spender, + gtUint256 garbledAllowance, + bool amountIsPublic, + uint256 publicAmount + ) private { + if (owner == address(0) || spender == address(0)) { + _sendApproveFailureToPod(id, owner, spender, bytes("PodErc20CotiMother: zero owner or spender")); + return; + } + + _allowanceCiphertext[id][owner][spender] = MpcCore.offBoard(garbledAllowance); + + ctUint256 memory ciphertextForOwner = MpcCore.offBoardToUser(garbledAllowance, owner); + ctUint256 memory ciphertextForSpender = MpcCore.offBoardToUser(garbledAllowance, spender); + inbox.respond(abi.encode(owner, ciphertextForOwner, spender, ciphertextForSpender)); + emit ApprovalCompleted(id, owner, spender, amountIsPublic, publicAmount); + } + + function _mintInternal( + bytes32 id, + address to, + gtUint256 amount, + bool amountIsPublic, + uint256 publicAmount + ) private { + if (to == address(0)) { + _sendTransferFailureToPod(id, address(0), to, bytes("PodErc20CotiMother: mint zero to")); + return; + } + + gtUint256 recipientBefore = _readGarbledBalance(id, to); + gtUint256 recipientAfter = MpcCore.add(recipientBefore, amount); + _writeGarbledBalance(id, to, recipientAfter); + + ctUint256 memory zeroCiphertext = _ciphertextPlainZero(); + uint256 callbackNonce = _tokenNonce[id]; + inbox.respond( + abi.encode( + address(0), + zeroCiphertext, + zeroCiphertext, + to, + MpcCore.offBoardToUser(recipientAfter, to), + MpcCore.offBoardToUser(amount, to), + callbackNonce + ) + ); + emit MintCompleted(id, to, amountIsPublic, publicAmount, callbackNonce); + _tokenNonce[id] = callbackNonce + 1; + } + + function _sendTransferFailureToPod(bytes32 id, address from, address to, bytes memory reason) private { + emit TransferFailureRaised(id, from, to, reason); + inbox.raise(abi.encode(from, to, reason)); + } + + function _sendApproveFailureToPod(bytes32 id, address owner, address spender, bytes memory reason) private { + emit ApprovalFailureRaised(id, owner, spender, reason); + inbox.raise(abi.encode(owner, spender, reason)); + } + + function _sendSyncFailureToPod(bytes32 id, bytes memory reason) private { + emit SyncBalancesFailureRaised(id, reason); + inbox.raise(reason); + } +} diff --git a/pod/utils/IWrappedNative.sol b/pod/utils/IWrappedNative.sol new file mode 100644 index 0000000..4044cef --- /dev/null +++ b/pod/utils/IWrappedNative.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +/// @notice Minimal WETH9 / WAVAX interface for wrap-on-deposit and unwrap-on-withdraw. +interface IWrappedNative { + /// @notice Wrap native coin into the ERC-20 underlying (mints WETH/WAVAX to `msg.sender`). + function deposit() external payable; + + /// @notice Unwrap `wad` to native coin (burns caller balance, sends native to `msg.sender`). + function withdraw(uint256 wad) external; +}