diff --git a/Cargo.lock b/Cargo.lock index 3c3776a9f..3e29d521f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3089,6 +3089,16 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "ensip25" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3a7d61bf320a02b7aeac1e82e1c5acac7bf32a7afcd9605773c64ab09d2344a" +dependencies = [ + "alloy-primitives", + "thiserror 2.0.18", +] + [[package]] name = "enum-as-inner" version = "0.6.1" @@ -9373,20 +9383,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" -[[package]] -name = "tap-caip" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a00582facd6f675b335e83cea0152aadfc4fcf21d3daa9ccf4fc5de537492c61" -dependencies = [ - "hex", - "once_cell", - "regex", - "serde", - "serde_json", - "thiserror 1.0.69", -] - [[package]] name = "tempfile" version = "3.27.0" @@ -11493,6 +11489,7 @@ dependencies = [ "crossbeam", "dashmap", "duration-str", + "ensip25", "eth_trie", "ethabi", "foundry-compilers", @@ -11540,7 +11537,6 @@ dependencies = [ "sha2 0.10.9", "sha3", "sled", - "tap-caip", "tempfile", "thiserror 2.0.18", "tokio", diff --git a/bundler_tests/DummyBridge.sol b/bundler_tests/DummyBridge.sol index 383c19258..eafa79061 100644 --- a/bundler_tests/DummyBridge.sol +++ b/bundler_tests/DummyBridge.sol @@ -15,8 +15,9 @@ import { IERC7786GatewaySource, IERC7786Recipient } from "@openzeppelin/contracts/interfaces/draft-IERC7786.sol"; -import {CAIP2, CAIP10} from "@openzeppelin/contracts/utils/CAIP10.sol"; import {NoncesKeyed} from "@openzeppelin/contracts/utils/NoncesKeyed.sol"; +import {InteroperableAddress} from "@openzeppelin/contracts/utils/draft-InteroperableAddress.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; contract DummyBridge is Pausable, @@ -33,15 +34,17 @@ contract DummyBridge is bytes32 private immutable LOCAL_CHAIN_K256; address private EP_ADDRESS; - mapping(string => uint128[6]) private destinationFees; + mapping(uint64 => uint128[6]) private destinationFees; constructor(address _ep) payable { entryPoint = IEntryPoint(_ep); - LOCAL_CHAIN_K256 = keccak256(bytes(CAIP2.local())); + LOCAL_CHAIN_K256 = keccak256( + InteroperableAddress.formatEvmV1(block.chainid) + ); EP_ADDRESS = _ep; // pre-populate - destinationFees[CAIP2.local()] = [ + destinationFees[uint64(block.chainid)] = [ uint128(0x100001), uint128(0x100002), uint128(0x100003), @@ -60,28 +63,43 @@ contract DummyBridge is _; } - /// IAccountExecute::executeUserOp() + /// Execution calls + function execute( + address target, + uint256 value, + bytes calldata data + ) external onlyEntryPointOrOwner { + _execute(target, value, data); + } + /// Called in the execution phase of UserOp handling. + // function executeBatch( + // address[] calldata targets, + // uint256[] calldata values, + // bytes[] calldata datas + // ) external onlyEntryPointOrOwner { + // uint256 len = targets.length; + // require(len == values.length && len == datas.length); + // for (uint256 i; i < len; ++i) { + // _execute(targets[i], values[i], datas[i]); + // } + // } + + function _execute( + address target, + uint256 value, + bytes memory data + ) internal { + Address.functionCallWithValue(target, data, value); + } + + /// IAccountExecute::executeUserOp() + /// Configuration calls function executeUserOp( PackedUserOperation calldata userOp, bytes32 _userOpHash ) external onlyEntryPointOrOwner { - // 1. Validate the userOp - - // 2. Extract the call arguments - bytes32 sendId = keccak256(userOp.callData); - address gateway = address(uint160(userOp.nonce >> 96)); // byte20 prefix with gateway address - address relayer = address(bytes20(userOp.signature[:20])); // byte20 prefix with signer wallet - - // Call the gateway - require( - IERC7786Recipient(gateway).receiveMessage( - sendId, - abi.encodePacked(relayer), - userOp.callData - ) == IERC7786Recipient.receiveMessage.selector, - "Gateway.receiveMessage() failed" - ); + // return success } /// IAccount::validateUserOp() @@ -136,7 +154,7 @@ contract DummyBridge is } function getFees( - string calldata chain_id + uint64 chain_id ) public view virtual returns (uint128[6] memory) { return destinationFees[chain_id]; } @@ -167,19 +185,20 @@ contract DummyBridge is bytes memory recipient, bytes memory payload, uint256 _nonce - ) = abi.decode(_payload[4:], (bytes, bytes, bytes, uint256)); + ) = abi.decode(_payload, (bytes, bytes, bytes, uint256)); // 4. Nonce replay check // 5. Send to destination - (string memory dst_chain, string memory dst_addr) = CAIP10.parse( - string(recipient) + (uint256 dst_chain, address dst_addr) = InteroperableAddress.parseEvmV1( + recipient ); require( - keccak256(bytes(dst_chain)) == LOCAL_CHAIN_K256, + keccak256(InteroperableAddress.formatEvmV1(dst_chain)) == + LOCAL_CHAIN_K256, "Foreign destination" ); - (string memory src_chain, string memory src_addr) = CAIP10.parse( - string(sender) + (uint256 src_chain, address src_addr) = InteroperableAddress.parseEvmV1( + sender ); // require( @@ -196,26 +215,18 @@ contract DummyBridge is /// IERC7786GatewaySource::sendMessage() /// Constructs the cross-chain quad-tuple payload to be relayed. function sendMessage( - bytes calldata recipient, // CAIP10/EIP155 full address + bytes calldata recipient, // ERC7930 bytes calldata payload, - bytes[] calldata // Stick pricing in here? + bytes[] calldata attributes // Stick pricing in here? ) public payable virtual whenNotPaused returns (bytes32 sendId) { - require(msg.value == 0, "received value"); - - // retrieve destination fee structure - bytes[] memory attributes = new bytes[](1); - bytes memory feeAttribute = abi.encodeWithSignature( - "feeParams(uint128[6])", - destinationFees[CAIP2.local()] - ); - attributes[0] = feeAttribute; - // wrapping the payload - bytes memory sender = bytes(CAIP10.local(msg.sender)); + bytes memory sender = InteroperableAddress.formatEvmV1( + block.chainid, + msg.sender + ); uint256 nonce = _useNonce(address(this), uint192(0)); - bytes memory wrappedPayload = abi.encodeWithSelector( - IAccountExecute.executeUserOp.selector, // needed to trigger executeUserOp() later + bytes memory wrappedPayload = abi.encode( sender, recipient, payload, @@ -225,7 +236,10 @@ contract DummyBridge is // compute sendId sendId = keccak256(wrappedPayload); - bytes memory gateway = bytes(CAIP10.local(address(this))); + bytes memory gateway = InteroperableAddress.formatEvmV1( + block.chainid, + address(this) + ); emit MessageSent( sendId, diff --git a/bundler_tests/config-bundler-spec-tests.toml b/bundler_tests/config-bundler-spec-tests.toml index 22fc9e5d1..2108d7d75 100644 --- a/bundler_tests/config-bundler-spec-tests.toml +++ b/bundler_tests/config-bundler-spec-tests.toml @@ -19,8 +19,8 @@ consensus.genesis_accounts = [ ["2B5AD5c4795c026514f8317c7a215E218DcCD6cF", "5000000000000000000000"], ["6813Eb9362372EEF6200f3b1dbC3f819671cBA69", "5000000000000000000000"], ["1efF47bc3a10a45D4B230B5d10E37751FE6AA718", "5000000000000000000000"], - # geth account - # ["71562b71999873db5b286df957af199ec94617f7", "5000000000000000000000"], + # own account + ["99F7f7C00526426b8dCA99302e96d85A0e5fd400", "10000000000000000000000"], # hardhat addresses ["f39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "10000000000000000000000"], # 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 ["70997970c51812dc3a010c7d01b50e0d17dc79c8", "10000000000000000000000"], # 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d diff --git a/zilliqa/Cargo.toml b/zilliqa/Cargo.toml index a0494f74b..f5434480c 100644 --- a/zilliqa/Cargo.toml +++ b/zilliqa/Cargo.toml @@ -119,7 +119,7 @@ revm-inspectors = { version = "0.39.0", features = ["js-tracer"] } revm-precompile = "34.0.0" revm-inspector = "19.0.0" revm-context = "16.0.1" -tap-caip = "0.7.0" +ensip25 = "0.4.4" [dev-dependencies] alloy = { version = "2.0.4", default-features = false, features = ["consensus", "dyn-abi", "eips", "json-abi", "k256", "rlp", "rpc-types", "rpc-types-trace", "serde", "sol-types", "pubsub", "json-rpc", "contract", "signer-local", "providers", "provider-debug-api"] } diff --git a/zilliqa/src/api/bundler.rs b/zilliqa/src/api/bundler.rs index 78688dba7..0f503f5dd 100644 --- a/zilliqa/src/api/bundler.rs +++ b/zilliqa/src/api/bundler.rs @@ -1,14 +1,17 @@ use std::{sync::Arc, time::Duration}; use alloy::{ + consensus::TxLegacy, eips::BlockId, + network::TxSignerSync as _, + primitives::{Address, B256, TxKind}, rpc::types::{ BlockOverrides, TransactionRequest, state::{AccountOverride, StateOverride}, + trace::geth::{GethDebugTracingCallOptions, GethTrace}, }, }; -use alloy_rpc_types_trace::geth::{GethDebugTracingCallOptions, GethTrace}; -use anyhow::{Result, anyhow}; +use anyhow::{Context, Result, anyhow}; use eth_trie::{EthTrie, Trie as _}; use jsonrpsee::{ RpcModule, @@ -22,6 +25,7 @@ use crate::{ to_hex::ToHex as _, }, cfg::EnabledApi, + crypto::Hash, error::ensure_success, node::Node, state::Code, @@ -32,6 +36,12 @@ use crate::{ /// Provides bundler-specific API alternatives and implementations. pub fn rpc_module(node: Arc, enabled_apis: &[EnabledApi]) -> RpcModule> { let mut module = RpcModule::new(node.clone()); + module + .merge(super::web3::rpc_module(node.clone(), enabled_apis)) + .unwrap(); + module + .merge(super::net::rpc_module(node.clone(), enabled_apis)) + .unwrap(); module .merge(super::eth::rpc_module(node.clone(), enabled_apis)) .unwrap(); @@ -45,6 +55,12 @@ pub fn rpc_module(node: Arc, enabled_apis: &[EnabledApi]) -> RpcModule, enabled_apis: &[EnabledApi]) -> RpcModule) -> Result> { + let address = node.secret_key.to_evm_address(); + Ok(vec![address]) +} + +// FIXME: DO NOT EXPOSE THIS TO THE PUBLIC +#[cfg(not(debug_assertions))] +fn eth_send_transaction(_params: Params, _node: &Arc) -> Result { + Err(anyhow!("API method eth_sendTransaction is not available")) +} +// This is only for local development use. +#[cfg(debug_assertions)] +fn eth_send_transaction(params: Params, node: &Arc) -> Result { + let txn = params.one::()?; + + let address = node.secret_key.to_evm_address(); + let block = node.get_block(BlockId::latest())?.context("must exist")?; + let nonce = node.get_state(&block)?.get_account(address)?.nonce; + + let mut tx_legacy = TxLegacy { + chain_id: Some(node.chain_id.eth), + gas_price: node.config.consensus.gas_price.0, + gas_limit: txn.gas.unwrap_or_default(), + to: txn.to.unwrap_or(TxKind::Create), + value: txn.value.unwrap_or_default(), + input: txn.input.into_input().unwrap_or_default(), + nonce, + }; + + let signer = alloy::signers::local::PrivateKeySigner::from_bytes(&B256::from_slice( + node.secret_key.as_bytes().as_slice(), + ))?; + let sig = signer.sign_transaction_sync(&mut tx_legacy)?; + + let stx = crate::transaction::SignedTransaction::Legacy { tx: tx_legacy, sig }; + let vtx = stx.verify_bypass(Hash::ZERO)?; + + let (txn_hash, _result) = node.create_transaction(vtx)?; + Ok(txn_hash.0.to_hex()) +} + pub fn debug_trace_call(params: Params, node: &Arc) -> Result { let mut params = params.sequence(); let call_params: TransactionRequest = params.next()?; diff --git a/zilliqa/src/api/mod.rs b/zilliqa/src/api/mod.rs index 9e9beada4..7ab3175d7 100644 --- a/zilliqa/src/api/mod.rs +++ b/zilliqa/src/api/mod.rs @@ -68,7 +68,7 @@ pub fn all_enabled() -> Vec { } pub fn bundler_enabled() -> Vec { - ["debug", "eth"] + ["debug", "eth", "net", "web3"] .into_iter() .map(|ns| crate::cfg::EnabledApi::EnableAll(ns.to_owned())) .collect() diff --git a/zilliqa/src/contracts/uccb/UccbGateway.sol b/zilliqa/src/contracts/uccb/UccbGateway.sol new file mode 100644 index 000000000..4bbf1bcff --- /dev/null +++ b/zilliqa/src/contracts/uccb/UccbGateway.sol @@ -0,0 +1,294 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import { + IERC7786GatewaySource, + IERC7786Recipient +} from "@openzeppelin/contracts/interfaces/draft-IERC7786.sol"; +import {CrosschainLinkedUpgradeable} from "@openzeppelin/contracts-upgradeable/crosschain/CrosschainLinkedUpgradeable.sol"; +import {InteroperableAddress} from "@openzeppelin/contracts/utils/draft-InteroperableAddress.sol"; +import {Bytes} from "@openzeppelin/contracts/utils/Bytes.sol"; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; +import {EIP712Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol"; +import {ReentrancyGuardTransient} from "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol"; +import {NoncesKeyedUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/NoncesKeyedUpgradeable.sol"; +import {IAccountExecute} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; +import {ERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + +/** + * @title UccbGateway + * @notice ERC-7786 gateway contract that implements both + * {IERC7786GatewaySource} and {IERC7786Recipient}. + * + * OUTBOUND (source side): + * 1. Application calls sendMessage(recipient, payload, attributes). + * 2. Gateway validates, and assigns a sendId. + * 3. Emits MessageSent. Off-chain relayers pick this up and relay + * the message to the destination chain gateway. + * + * INBOUND (destination side / recipient side): + * 1. Sender calls receiveMessage() with the cross-chain payload. + * 2. Calls receiveMessage on the registered IERC7786Recipient. + * 3. Emits MessageReceived. + * + * @custom:oz-upgrades-unsafe-allow constructor + */ + +contract UccbGateway is + Initializable, + CrosschainLinkedUpgradeable, + UUPSUpgradeable, + AccessControlUpgradeable, + PausableUpgradeable, + ReentrancyGuardTransient, + NoncesKeyedUpgradeable, + EIP712Upgradeable, + IERC7786GatewaySource +{ + using Address for address payable; + // using SafeCast for uint256; + using InteroperableAddress for bytes; + using Bytes for bytes; + + // Roles + bytes32 public constant WITHDRAWER_ROLE = keccak256("WITHDRAWER_ROLE"); + bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); + + /// Emitted when an inbound message is successfully received. + event MessageReceived(bytes32 indexed receiveId, address gateway); + + /** + * @notice One-time proxy initializer. + * + * @param admin_ Address granted DEFAULT_ADMIN_ROLE (and all sub-roles). + * @param links_ Initial chain links (gateway ↔ counterpart pairs). + * Each entry is a {CrosschainLinkedUpgradeable.Link} struct. + */ + function initialize( + address admin_, + CrosschainLinkedUpgradeable.Link[] memory links_ + ) external initializer { + assert(admin_ != address(0)); + + __EIP712_init("UccbGateway", "1"); + __AccessControl_init(); + __Pausable_init(); + __CrosschainLinked_init(links_); + __ERC165_init(); + + _grantRole(DEFAULT_ADMIN_ROLE, admin_); + _grantRole(WITHDRAWER_ROLE, admin_); + _grantRole(PAUSER_ROLE, admin_); + } + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /// CoDec + uint8 internal constant MSG_VERSION = 1; + bytes4 internal constant MSG_CALL = 0x1b8b921d; // call(address,bytes) + bytes4 internal constant MSG_TRANSFER = 0xa9059cbb; // transfer(address,uint256) + + function _encode( + bytes4 msgType, + bytes memory body + ) internal pure returns (bytes memory) { + // TODO: Reduce size + return + abi.encodeWithSelector( + IAccountExecute.executeUserOp.selector, // needed to trigger executeUserOp() later + MSG_VERSION, + msgType, + body + ); + } + + function _decode( + bytes calldata payload + ) internal pure returns (bytes4, bytes memory) { + assert(payload.length > 32); + (uint8 version, bytes4 mType, bytes memory b) = abi.decode( + payload[4:], + (uint8, bytes4, bytes) + ); + assert(version == MSG_VERSION); + return (mType, b); + } + + // IERC7786GatewaySource + function supportsAttribute(bytes4) external pure override returns (bool) { + // TODO: Support some ERC7985 attributes + return false; + } + + // IERC7786GatewaySource + function sendMessage( + bytes calldata recipient, // ERC7930(recipient) + bytes calldata payload, + bytes[] calldata attributes + ) external payable override whenNotPaused nonReentrant returns (bytes32) { + assert(payload.length != 0); + assert(msg.value == 0); + assert(attributes.length == 0); + + // ERC7930(sender) + bytes memory sender = InteroperableAddress.formatEvmV1( + block.chainid, + msg.sender + ); + + uint256 nonce = _useNonce(address(this), uint192(0)); + + bytes memory wrappedPayload = _encode( + MSG_CALL, + abi.encode(sender, recipient, payload, nonce) + ); + + // TODO: deliver local messages directly? + + return + _sendMessageToCounterpart( + __extractChain(recipient), + wrappedPayload, + attributes + ); + } + + // CrosschainLinked + function _sendMessageToCounterpart( + bytes memory chain, + bytes memory payload, + bytes[] memory attributes + ) internal override(CrosschainLinkedUpgradeable) returns (bytes32) { + (, bytes memory counterpart) = getLink(chain); + + bytes memory originator = InteroperableAddress.formatEvmV1( + block.chainid, + address(this) + ); + + assert(!counterpart.equal(originator)); // prevent loop-back + + bytes32 sendId = keccak256(payload); + + emit MessageSent( + sendId, + originator, + counterpart, + payload, + 0, + attributes + ); + + return sendId; + } + + function setLink( + address sender, + bytes memory counterpart, + bool allowOverride + ) public onlyRole(DEFAULT_ADMIN_ROLE) { + _setLink(sender, counterpart, allowOverride); + } + + mapping(bytes32 => bool) private _usedIds; + + // ERC7786Recipient + function _processMessage( + address, // ERC4337(sender), + bytes32 receiveId, + bytes calldata relayer, + bytes calldata wrappedPayload + ) internal override { + require(receiveId == keccak256(wrappedPayload), "Invalid payload"); + + (, address senderAddr) = relayer.parseEvmV1(); + + // Deconstruct the quad-tuple payload + (bytes4 msgType, bytes memory quadtuple) = _decode(wrappedPayload); + require(msgType == MSG_CALL); + ( + bytes memory sender, + bytes memory recipient, + bytes memory payload, + + ) = abi.decode(quadtuple, (bytes, bytes, bytes, uint256)); + + // prevent replays + require(!_usedIds[receiveId], "Already processed"); + _usedIds[receiveId] = true; + + // signal received + emit MessageReceived(receiveId, senderAddr); + + // pass-thru to target + (, address target) = recipient.parseEvmV1(); + // TODO: allow failed execution + require( + IERC7786Recipient(target).receiveMessage( + receiveId, + sender, + payload + ) == IERC7786Recipient.receiveMessage.selector, + "Execution failure" + ); + } + + // Helper + function __extractChain( + bytes memory s + ) private pure returns (bytes memory) { + (bytes2 chainType, bytes memory chainReference, ) = s.parseV1(); + return InteroperableAddress.formatV1(chainType, chainReference, hex""); + } + + /** + * @notice Withdraw accumulated message fees. + * @param to Recipient of the fees. + */ + function withdrawTo( + address payable to, + uint256 amount + ) external onlyRole(WITHDRAWER_ROLE) nonReentrant { + require(to != address(0)); + to.sendValue(amount); + } + + // UUPSUpgradeable + + function _authorizeUpgrade( + address newImplementation + ) internal override onlyRole(DEFAULT_ADMIN_ROLE) { + // TODO: audit log + } + + // PausableUpgradeable – restricted entry points + + function pause() external onlyRole(PAUSER_ROLE) { + _pause(); + } + function unpause() external onlyRole(PAUSER_ROLE) { + _unpause(); + } + + /** + * @dev Advertises all interfaces implemented by this contract. + * AccessControlUpgradeable already registers IAccessControl. + */ + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(AccessControlUpgradeable) returns (bool) { + return + interfaceId == type(IERC7786GatewaySource).interfaceId || + interfaceId == type(IERC7786Recipient).interfaceId || + interfaceId == type(IERC165).interfaceId || + super.supportsInterface(interfaceId); + } +} diff --git a/zilliqa/src/contracts/uccb/UccbPaymaster.sol b/zilliqa/src/contracts/uccb/UccbPaymaster.sol new file mode 100644 index 000000000..3f0e78e82 --- /dev/null +++ b/zilliqa/src/contracts/uccb/UccbPaymaster.sol @@ -0,0 +1,218 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import { + IPaymaster, + PackedUserOperation +} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; +import {IEntryPoint} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; +import {ERC4337Utils} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol"; + +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; +import {ReentrancyGuardTransient} from "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol"; +import {ERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {EIP712Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol"; +import {MultiSignerERC7913WeightedUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/cryptography/signers/MultiSignerERC7913WeightedUpgradeable.sol"; + +/** + * @title Paymaster + * @notice ERC-4337 Paymaster skeleton built entirely on OpenZeppelin v5.6.x. + */ +contract UccbPaymaster is + Initializable, + UUPSUpgradeable, + AccessControlUpgradeable, + PausableUpgradeable, + EIP712Upgradeable, + ReentrancyGuardTransient, + // MultiSignerERC7913WeightedUpgradeable, + IPaymaster +{ + // using SafeERC20 for IERC20; + using Address for address payable; + using ERC4337Utils for PackedUserOperation; + + // Roles + bytes32 public constant SPONSORED_SENDER_ROLE = keccak256( + "SPONSORED_SENDER_ROLE" + ); + bytes32 public constant WITHDRAWER_ROLE = keccak256("WITHDRAWER_ROLE"); + bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); + + /** + * @dev Restricts a function to the trusted EntryPoint. + * validatePaymasterUserOp and postOp MUST only be called by it. + */ + modifier onlyEntryPoint() { + require(msg.sender == address(entryPoint())); + _; + } + + /// Use v0.9 entrypoint only + function entryPoint() private pure returns (IEntryPoint) { + return ERC4337Utils.ENTRYPOINT_V09; + } + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + // ── Initializer ────────────────────────────────────────── + + /** + * @notice One-time initializer called by the factory through the proxy. + */ + function initialize( + address admin_, + uint256 // _maxCostPerOp + ) external initializer { + assert(admin_ != address(0)); + + __EIP712_init("UccbPaymaster", "1"); + __AccessControl_init(); + __Pausable_init(); + __ERC165_init(); + + _grantRole(DEFAULT_ADMIN_ROLE, admin_); + _grantRole(WITHDRAWER_ROLE, admin_); + _grantRole(PAUSER_ROLE, admin_); + } + + /** + * @notice Called by the EntryPoint during the verification loop. + * Must decide whether to sponsor this UserOp and return: + */ + function validatePaymasterUserOp( + PackedUserOperation calldata userOp, + bytes32, // userOpHash, + uint256 // maxCost + ) + external + view + override + onlyEntryPoint + whenNotPaused + returns (bytes memory, uint256) + { + // allow all from SENDER + bool allowed = hasRole(SPONSORED_SENDER_ROLE, userOp.sender); + // extract validUntil/validAfter + // context = relayer + signers + return ("", ERC4337Utils.packValidationData(allowed, 0, 0)); // true, forever + } + + /** + * @notice Called by the EntryPoint after the UserOp executes (or after + * a failed execution attempt). + */ + function postOp( + PostOpMode, //mode, + bytes calldata context, + uint256, // actualGasCost, + uint256 // actualUserOpFeePerGas + ) external view override onlyEntryPoint { + // TODO: record the signer and co-signers + if (context.length == 0) return; + } + + /** + * @notice Deposit ETH into the EntryPoint so the paymaster can cover gas. + * Anyone can call; the EntryPoint credits the deposit to this contract. + */ + function depositTo() external payable nonReentrant { + entryPoint().depositTo{value: msg.value}(address(this)); + } + + /** + * @notice Withdraw ETH from the EntryPoint deposit back to this contract. + * @param amount Wei to withdraw. + */ + function withdrawTo( + uint256 amount + ) external onlyRole(WITHDRAWER_ROLE) nonReentrant { + entryPoint().withdrawTo(payable(address(this)), amount); + } + + /** + * @notice View the current EntryPoint deposit balance. + */ + function balanceOf() external view returns (uint256) { + return entryPoint().balanceOf(address(this)); + } + + /** + * @notice Add stake to the EntryPoint for this paymaster. + * + * @param unstakeDelaySec Delay (seconds) before stake can be withdrawn. + * Must meet the EntryPoint's minimum. + */ + function addStake( + uint32 unstakeDelaySec + ) external payable onlyRole(DEFAULT_ADMIN_ROLE) { + entryPoint().addStake{value: msg.value}(unstakeDelaySec); + } + + /** + * @notice Initiate the stake unlock process. After the unstake delay + * has elapsed, call {withdrawStake}. + */ + function unlockStake() external onlyRole(DEFAULT_ADMIN_ROLE) { + entryPoint().unlockStake(); + } + + /** + * @notice Withdraw previously unlocked stake. + * @param to Recipient of the returned ETH. + */ + function withdrawStake( + address payable to + ) external onlyRole(WITHDRAWER_ROLE) nonReentrant { + assert(to != address(0)); + entryPoint().withdrawStake(to); + } + + /** + * @notice Pause the paymaster. validatePaymasterUserOp will revert + * while paused, preventing new ops from being sponsored. + */ + function pause() external onlyRole(PAUSER_ROLE) { + _pause(); + } + + /** + * @notice Resume normal operation. + */ + function unpause() external onlyRole(PAUSER_ROLE) { + _unpause(); + } + + function _authorizeUpgrade( + address /*newImplementation*/ + ) internal view override onlyRole(DEFAULT_ADMIN_ROLE) { + // TODO: audit log + } + + /** + * @dev Advertises all interfaces implemented by this contract. + * AccessControlUpgradeable already registers IAccessControl. + */ + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(AccessControlUpgradeable) returns (bool) { + return + interfaceId == type(IPaymaster).interfaceId || + interfaceId == type(IERC165).interfaceId || + super.supportsInterface(interfaceId); + } + + /** + * @dev Accept ETH (refunds from EntryPoint, direct top-ups, etc.). + */ + receive() external payable {} +} diff --git a/zilliqa/src/contracts/uccb/UccbSender.sol b/zilliqa/src/contracts/uccb/UccbSender.sol new file mode 100644 index 000000000..f85809163 --- /dev/null +++ b/zilliqa/src/contracts/uccb/UccbSender.sol @@ -0,0 +1,190 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {Account} from "@openzeppelin/contracts/account/Account.sol"; +import {AbstractSigner} from "@openzeppelin/contracts/utils/cryptography/signers/AbstractSigner.sol"; +import {ERC4337Utils} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol"; +import { + IEntryPoint, + IAccount, + IAccountExecute, + PackedUserOperation +} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {ReentrancyGuardTransient} from "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {ERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import {EIP712Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol"; +import {MultiSignerERC7913Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/cryptography/signers/MultiSignerERC7913Upgradeable.sol"; + +// import {MultiSignerERC7913WeightedUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/cryptography/signers/MultiSignerERC7913WeightedUpgradeable.sol"; + +/** + * @title UccbSender + * @notice ERC-4337 Sender contract built entirely on OpenZeppelin v5.6.x. + * + * @custom:oz-upgrades-unsafe-allow constructor + */ +contract UccbSender is + Initializable, + MultiSignerERC7913Upgradeable, + ERC165Upgradeable, + UUPSUpgradeable, + ReentrancyGuardTransient, + EIP712Upgradeable, + IAccountExecute, + Account +{ + using Address for address; + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /** + * @notice One-time initializer called by the factory immediately after + * deploying the proxy. + * + * @param signers Owner / signing key for this account. + */ + function initialize(bytes[] memory signers) external initializer { + __EIP712_init("UccbSender", "1"); + __ERC165_init(); + + _addSigners(signers); + _setThreshold(uint64(1)); // one signer will pass + } + + /// ***** External execution ***** + + /** + * @notice Execute a single arbitrary call. + * Called by the EntryPoint after successful validateUserOp. + */ + function execute( + address target, + uint256 value, + bytes calldata data + ) external onlyEntryPointOrSelf nonReentrant { + _execute(target, value, data); + } + + /** + * @dev Low-level call with revert bubbling. + * Uses Address.functionCallWithValue so reverts propagate correctly + * even when returndata is empty. + */ + function _execute( + address target, + uint256 value, + bytes memory data + ) internal { + // Address.functionCallWithValue reverts with the upstream reason on failure. + // We catch it here to emit ExecutionFailure before re-reverting. + try this._callExternal(target, value, data) { + // emit ExecutionSuccess(target, value, data); + } catch (bytes memory reason) { + // emit ExecutionFailure(target, value, data, reason); + // Re-revert with the original reason. + assembly { + revert(add(reason, 32), mload(reason)) + } + } + } + + /** + * @dev External shim so try/catch can wrap a low-level call. + * Only callable by this contract itself (via _execute's try/catch). + */ + function _callExternal( + address target, + uint256 value, + bytes calldata data + ) external { + assert(msg.sender == address(this)); + Address.functionCallWithValue(target, data, value); + } + + /// ***** Internal execution ***** + function executeUserOp( + PackedUserOperation calldata userOp, + bytes32 userOpHash + ) external { + // TODO: Update stakers + } + + /// Called by entrypoint + function _rawSignatureValidation( + bytes32 hash, + bytes calldata signature + ) + internal + pure + override(AbstractSigner, MultiSignerERC7913Upgradeable) + returns (bool) + { + // TODO: verify all signatures signature + return hash != 0 && signature.length != 0; + } + + // ***** ENTRYPOINT ***** + + /** + * @notice Top-up this account's gas deposit in the EntryPoint. + */ + function depositTo() external payable { + entryPoint().depositTo{value: msg.value}(address(this)); + } + + /** + * @notice View current EntryPoint deposit balance. + */ + function balanceOf() external view returns (uint256) { + return entryPoint().balanceOf(address(this)); + } + + /** + * @notice Withdraw ETH from the EntryPoint deposit. + * @param to Recipient. + * @param amount Amount to withdraw (in wei). + */ + function withdrawTo( + address payable to, + uint256 amount + ) external onlyEntryPointOrSelf { + entryPoint().withdrawTo(to, amount); + } + + // ***** BOILER-PLATE ***** + + function _authorizeUpgrade( + address /*newImplementation*/ + ) internal view override onlyEntryPointOrSelf { + // TODO: audit log + } + + /** + * @dev Account.receive() already exists and emits nothing. + * Override to emit an event so indexers can track deposits. + */ + receive() external payable virtual override { + // emit Received(msg.sender, msg.value); + } + + /** + * @dev Advertises every interface this account satisfies. + * ERC165Upgradeable handles IERC165; all others are added here. + */ + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(ERC165Upgradeable) returns (bool) { + return + interfaceId == type(IAccountExecute).interfaceId || + interfaceId == type(IAccount).interfaceId || + interfaceId == type(IERC165).interfaceId || + super.supportsInterface(interfaceId); + } +} diff --git a/zilliqa/src/message.rs b/zilliqa/src/message.rs index 892deb68f..f683fdd16 100644 --- a/zilliqa/src/message.rs +++ b/zilliqa/src/message.rs @@ -246,6 +246,7 @@ pub struct UccbUserOp { pub public_key: NodePublicKey, pub userop: Option, pub signature: BlsSignature, + pub send_id: B256, } /// Used to convey proposal processing internally, to avoid blocking threads for too long. diff --git a/zilliqa/src/node.rs b/zilliqa/src/node.rs index 5cbbb0694..f27d2720d 100644 --- a/zilliqa/src/node.rs +++ b/zilliqa/src/node.rs @@ -175,6 +175,7 @@ pub struct Node { pub chain_id: ChainId, pub filters: Arc, swarm_peers: Arc>>, + pub secret_key: SecretKey, } #[derive(Debug, Copy, Clone)] @@ -224,6 +225,7 @@ impl Node { )?); let node = Node { + secret_key, config: config.clone(), peer_id, message_sender: message_sender.clone(), diff --git a/zilliqa/src/uccb/mod.rs b/zilliqa/src/uccb/mod.rs index b5e79b04d..9e8f9c44e 100644 --- a/zilliqa/src/uccb/mod.rs +++ b/zilliqa/src/uccb/mod.rs @@ -51,6 +51,11 @@ sol!( ); sol! { + function execute( + address target, + uint256 value, + bytes calldata data + ) external; interface IERC7786Attributes { function eip1559_fees(uint128 max_priority_gas_fee,uint128 max_base_gas_fee) external; } @@ -62,7 +67,7 @@ sol! { function feeParams(uint128[6]); event MessageReceived(bytes32 indexed receiveId, address relayer); function getFees( - string chain_id + uint64 chain_id ) external view returns (uint128[6]); } } @@ -120,10 +125,12 @@ pub struct SignUserOp { pub src_chain: Chain, pub blk_height: u64, pub uop_hash: Option, + pub send_id: B256, retry_s: u16, } impl SignUserOp { pub fn new( + send_id: B256, userop: AlloyUserOperation, dst_chain: Chain, src_chain: Chain, @@ -132,6 +139,7 @@ impl SignUserOp { blk_height: u64, ) -> Self { Self { + send_id, userop, dst_chain, src_chain, @@ -161,12 +169,12 @@ pub struct RelayUserOp { pub userop: AlloyUserOperation, pub chain: Chain, pub userop_hash: Hash, - pub send_id: Hash, + pub send_id: B256, retry_s: u16, } impl RelayUserOp { - pub fn new(userop: AlloyUserOperation, chain: Chain, userop_hash: Hash, send_id: Hash) -> Self { + pub fn new(userop: AlloyUserOperation, chain: Chain, userop_hash: Hash, send_id: B256) -> Self { Self { userop, chain, @@ -191,6 +199,7 @@ impl RelayUserOp { #[derive(Default)] pub struct BlsUserOp { pub userop: Option, + pub send_id: B256, pub signatures: Vec<(NodePublicKey, BlsSignature)>, pub threshold: u128, } @@ -380,9 +389,11 @@ impl Uccb { public_key, block_hash, chain, + send_id, }) => { // handle self.relayer.collect_userop( + send_id, from, chain, block_hash, diff --git a/zilliqa/src/uccb/relayer.rs b/zilliqa/src/uccb/relayer.rs index 10e82ec43..f4fdf6ebd 100644 --- a/zilliqa/src/uccb/relayer.rs +++ b/zilliqa/src/uccb/relayer.rs @@ -2,7 +2,7 @@ use std::{num::NonZeroUsize, sync::Arc}; use alloy::{ eips::BlockNumberOrTag, - primitives::{Address, B256, ChainId, U256, keccak256}, + primitives::{Address, B256, ChainId, U256}, providers::{Provider, utils::eip1559_default_estimator}, rpc::types::{Filter, PackedUserOperation as AlloyUserOperation, UserOperationGasEstimation}, sol_types::{SolEvent, SolValue}, @@ -327,12 +327,7 @@ impl Relayer { // queue processing Some(mut relay_uop) = relay_rx.recv() => { let dest = relay_uop.chain; - let send_id = keccak256(relay_uop.userop.call_data.iter().as_slice()); - // 0: Sanity check - if relay_uop.send_id.0 != send_id { - tracing::error!(%send_id, "Relayer({chain:?} => {dest:?}): mismatch"); - continue; - } else + let send_id = relay_uop.send_id; // TODO: 1. Check for sufficient gas // if let Err(err) = Self::check_gasfees(send_id, &mut relay_uop, providers.clone()).await // { @@ -377,6 +372,7 @@ impl Relayer { #[allow(clippy::too_many_arguments)] pub fn collect_userop( &self, + send_id: B256, from: PeerId, chain: Chain, block_hash: Hash, @@ -418,6 +414,7 @@ impl Relayer { .sum(); BlsUserOp { userop: None, + send_id: B256::ZERO, threshold: 2 * total_stake / 3 + 1, signatures: Vec::with_capacity(len), } @@ -430,8 +427,9 @@ impl Relayer { bop.threshold = bop.threshold.saturating_sub(stake.get()); bop.signatures.push((public_key, signature)); - // use only the UserOp we constructed ourselves. + // use only the (UserOp, send_id) we constructed ourselves. if from == self.peer_id { + bop.send_id = send_id; bop.userop = userop; } @@ -439,7 +437,8 @@ impl Relayer { if bop.userop.is_some() && bop.threshold == 0 { let bop = cache.pop(&userop_hash).unwrap(); let stakers = state.get_stakers(block.header).expect("must exist"); - self.relay_userop(userop_hash, chain, stakers, bop)?; + let send_id = bop.send_id; + self.relay_userop(send_id, userop_hash, chain, stakers, bop)?; } Ok(()) } @@ -449,19 +448,17 @@ impl Relayer { /// Multi-sign the UserOp; and queues it for sending to the Bundler. pub fn relay_userop( &self, + send_id: B256, userop_hash: Hash, chain: Chain, stakers: Vec, bop: BlsUserOp, ) -> Result<()> { anyhow::ensure!(!stakers.is_empty(), "stakers cannot be empty"); - let send_id = bop - .userop - .as_ref() - .map_or(alloy::primitives::KECCAK256_EMPTY, |uop| { - keccak256(uop.call_data.iter().as_slice()) - }); - + anyhow::ensure!( + send_id != alloy::primitives::KECCAK256_EMPTY, + "invalid send_id" + ); tracing::info!(%send_id, "Relayer({:?} => {chain:?}): promote", self.chain); let (signers, signatures): (Vec, Vec) = bop.signatures.into_iter().unzip(); @@ -516,7 +513,7 @@ impl Relayer { }, chain, userop_hash, - Hash(send_id.0), + send_id, ); // 4. Push UserOp to the sending queue diff --git a/zilliqa/src/uccb/signer.rs b/zilliqa/src/uccb/signer.rs index 70b84d5b6..c2f434e62 100644 --- a/zilliqa/src/uccb/signer.rs +++ b/zilliqa/src/uccb/signer.rs @@ -36,7 +36,7 @@ use crate::{ EndPoint, IERC7786GatewaySource::MessageSent, SignUserOp, - utils::{get_eip155_address, get_eip155_chain, get_user_op_hash}, + utils::{get_erc7930_address, get_erc7930_chain, get_user_op_hash}, }, }; @@ -219,22 +219,26 @@ impl Signer { tracing::warn!(%txn_hash, "MessageSent({chain:?}): invalid structure"); continue; // skip on failure }; + if sendId == alloy::primitives::KECCAK256_EMPTY { + tracing::debug!(send_id=%sendId, "MessageSent({chain:?}): skipped"); + continue; // skip local deliveries + } tracing::debug!(send_id=%sendId, "MessageSent({chain:?}): seen"); - // 5. Validate payload integrity + // 5. Validate payload integrity; prevent executeUserOp() calls. if sendId != keccak256(payload.iter().as_slice()) - && payload.starts_with(&super::IAccountExecute::executeUserOpCall::SELECTOR) + && !payload.starts_with(&super::IAccountExecute::executeUserOpCall::SELECTOR) { tracing::warn!(send_id=%sendId, "MessageSent({chain:?}): invalid payload"); continue; } // 6. Validate route - let Ok(dst_chain) = get_eip155_chain(std::str::from_utf8(&recipient)?) else { + let Ok(dst_chain) = get_erc7930_chain(recipient.iter().as_slice()) else { tracing::warn!(send_id=%sendId, "MessageSent({chain:?}): invalid destination"); continue; }; - let Ok(src_chain) = get_eip155_chain(std::str::from_utf8(&sender)?) else { + let Ok(src_chain) = get_erc7930_chain(sender.iter().as_slice()) else { tracing::warn!(send_id=%sendId, "MessageSent({chain:?}): invalid source"); continue; }; @@ -243,13 +247,13 @@ impl Signer { "MessageSent({chain:?}): invalid source" ); // MessageSent comes from source - let Ok(sender) = get_eip155_address(std::str::from_utf8(&sender)?) else { - tracing::warn!(send_id=%sendId, "MessageSent({chain:?}): invalid sender"); + let Ok(origin) = get_erc7930_address(sender.iter().as_slice()) else { + tracing::warn!(send_id=%sendId, "MessageSent({chain:?}): invalid origin"); continue; }; anyhow::ensure!( - sender == log.address(), - "MessageSent({chain:?}): invalid sender" + origin == log.address(), + "MessageSent({chain:?}): invalid origin" ); // Gateway contract is sender let is_src_test = src_chain @@ -268,10 +272,19 @@ impl Signer { // Warning: may dead-lock, if watchers is locked above if let Some(watcher) = watchers.get(&dst_chain.id()) { + // Encode a receiveMessage() call + let receive_message = super::IERC7786Recipient::receiveMessageCall { + receiveId: sendId, + sender, + payload, // quad-tuple + }; + let payload = receive_message.abi_encode(); + let EndPoint { allow_loopback, sender, paymaster, + gateway, .. } = watcher.value(); if !(dst_chain != src_chain || *allow_loopback) { @@ -280,9 +293,18 @@ impl Signer { } // 7. Construct partial UserOp; send for signing - let userop = Self::new_user_op(payload, sender, paymaster, value, block_height); + let userop = Self::new_user_op( + sendId, + payload.into(), + sender, + paymaster, + gateway, + value, + block_height, + ); tracing::trace!(send_id=%sendId, ?userop, "UserOp"); if let Err(err) = sign_tx.send(SignUserOp::new( + sendId, userop, dst_chain, src_chain, @@ -332,12 +354,12 @@ impl Signer { // exponential backoff queue let mut delayq: DelayQueue = DelayQueue::new(); // time-slot sending queue - let mut sendq: DelayQueue<(PeerId, UccbUserOp, B256)> = DelayQueue::new(); + let mut sendq: DelayQueue<(PeerId, UccbUserOp)> = DelayQueue::new(); loop { select! { Some(mut sign_uop) = sign_rx.recv() => { - let send_id = keccak256(sign_uop.userop.call_data.iter().as_slice()); + let send_id = sign_uop.send_id; // 1. Populate the nonce if let Err(err) = Self::populate_nonce(send_id, &mut sign_uop, providers.clone()).await { @@ -384,8 +406,7 @@ impl Signer { // retry Some(due) = delayq.next() => { let sign_uop = due.into_inner(); - let send_id = keccak256(sign_uop.userop.call_data.iter().as_slice()); - tracing::debug!(%send_id, "Signer({chain:?}): retry"); + tracing::debug!(send_id=%sign_uop.send_id, "Signer({chain:?}): retry"); if let Err(err) = sign_tx.send(sign_uop) { tracing::error!(%err, "sign_rx closed"); break Ok(()); @@ -393,8 +414,8 @@ impl Signer { } // delay send Some(due) = sendq.next() => { - let (peer, uccb_uop, send_id) = due.into_inner(); - tracing::debug!(%send_id, "Signer({chain:?}): relayed"); + let (peer, uccb_uop) = due.into_inner(); + tracing::debug!(send_id=%uccb_uop.send_id, "Signer({chain:?}): relayed"); if let Err(err) = message_sender.send_external_message(peer, ExternalMessage::UccbUserOp(uccb_uop)) { tracing::error!(%err, "message_sender closed"); break Ok(()); @@ -411,7 +432,7 @@ impl Signer { fn queue_userop( send_id: B256, sign_uop: &mut SignUserOp, - sendq: &mut DelayQueue<(PeerId, UccbUserOp, B256)>, + sendq: &mut DelayQueue<(PeerId, UccbUserOp)>, state: Arc, db: Arc, peer_id: PeerId, @@ -443,6 +464,7 @@ impl Signer { None }, signature, + send_id, }; // we use delay-slots to ensure that the first peer always has the first priority to submit the userop. // the two backup peers should only be able to submit it after a delay. the userop is lost if all fail. @@ -450,7 +472,7 @@ impl Signer { .average_blocktime_hint() .map_or_else(|| Duration::from_secs(60).mul(i), |d| d.mul(i)); - sendq.insert((peer, uccb_uop, send_id), delay_slot); + sendq.insert((peer, uccb_uop), delay_slot); } Ok(()) } @@ -532,9 +554,9 @@ impl Signer { // .get_or_insert() does not work in async *fees } else { - let caip2 = tap_caip::ChainId::new("eip155", &dst_chain.id().to_string())?; + let chain_id = dst_chain.id(); let fees = super::IERC4337Extra::new(*gateway, jsonrpc) - .getFees(caip2.to_string()) + .getFees(chain_id) .block(BlockId::number(*blk_height)) .call() .await?; @@ -680,18 +702,24 @@ impl Signer { /// Some dummy data is used to populate the UserOp initially. They *must* be replaced before submission. #[allow(clippy::too_many_arguments)] pub fn new_user_op( + _send_id: B256, payload: Bytes, sender: &Address, paymaster: &Address, - _value: U256, + gateway: &Address, + value: U256, block_height: u64, ) -> AlloyUserOperation { // we can encode some custom things in here - let paymaster_data = (block_height).abi_encode_packed(); - // FIXME: decode the values - // let [a, b, c, d] = value.into_limbs(); - // let max_fee_per_gas = (b as u128) << 64 | a as u128; - // let max_priority_fee_per_gas = (d as u128) << 64 | c as u128; + let paymaster_data = (block_height).abi_encode(); + + // Normal UserOps go to Sender::executeBatch() + let execute = super::executeCall { + value, + target: *gateway, + data: payload, + }; + let call_data = execute.abi_encode(); AlloyUserOperation { sender: *sender, nonce: U256::ZERO, // unpopulated nonce/sig @@ -699,7 +727,7 @@ impl Signer { // Some bundlers reject any initdata for existing senders e.g. // https://docs.candide.dev/wallet/technical-reference/aa10-sender-already-constructed/ factory_data: None, - call_data: payload, + call_data: call_data.into(), call_gas_limit: U256::ZERO, // estimateUserOpGas verification_gas_limit: U256::ZERO, // estimateUserOpGas pre_verification_gas: U256::ZERO, // estimateUserOpGas @@ -709,16 +737,16 @@ impl Signer { paymaster_verification_gas_limit: None, // estimateUserOpGas paymaster_post_op_gas_limit: None, // estimateUserOpGas paymaster_data: Some(Bytes::from(paymaster_data)), - signature: Bytes::new(), // unpopulated signature + signature: Bytes::new(), // blank signature } } pub fn default_user_op() -> AlloyUserOperation { Self::new_user_op( - // B256::ZERO, + B256::ZERO, Bytes::new(), &Address::ZERO, - // &Address::ZERO, + &Address::ZERO, &Address::ZERO, U256::ZERO, 0, diff --git a/zilliqa/src/uccb/utils.rs b/zilliqa/src/uccb/utils.rs index b563f5d40..bbd15ce33 100644 --- a/zilliqa/src/uccb/utils.rs +++ b/zilliqa/src/uccb/utils.rs @@ -1,6 +1,5 @@ use alloy::{ dyn_abi::Eip712Domain, - hex::FromHex, primitives::{Address, B256, U256, keccak256}, sol_types::SolValue, }; @@ -8,28 +7,31 @@ use alloy_chains::Chain; use anyhow::Result; use super::PackedUserOperation; +use crate::api::to_hex::ToHex; /// Retrieve the chain from a given CAIP-10 account -pub fn get_eip155_chain(account_id: &str) -> Result { - if let Ok(caip_id) = tap_caip::parse(account_id) - && let tap_caip::CaipId::AccountId(account_id) = caip_id - && account_id.chain_id().namespace() == "eip155" +pub fn get_erc7930_chain(account_id: &[u8]) -> Result { + if let Ok(erc7930) = ensip25::erc7930::InteropAddress::decode(account_id) + && erc7930.is_evm() { - return Ok(Chain::from_id( - account_id.chain_id().reference().parse::()?, - )); + return Ok(Chain::from_id(erc7930.evm_chain_id().expect("is evm"))); } - Err(anyhow::anyhow!("Invalid eip155 chain {account_id}")) + Err(anyhow::anyhow!( + "Invalid erc7930 chain {}", + account_id.to_hex() + )) } -pub fn get_eip155_address(account_id: &str) -> Result
{ - if let Ok(caip_id) = tap_caip::parse(account_id) - && let tap_caip::CaipId::AccountId(account_id) = caip_id - && account_id.chain_id().namespace() == "eip155" +pub fn get_erc7930_address(account_id: &[u8]) -> Result
{ + if let Ok(erc7930) = ensip25::erc7930::InteropAddress::decode(account_id) + && erc7930.is_evm() { - return Ok(Address::from_hex(account_id.address())?); + return Ok(erc7930.evm_address().expect("is evm")); } - Err(anyhow::anyhow!("Invalid eip155 account {account_id}")) + Err(anyhow::anyhow!( + "Invalid erc7930 account {}", + account_id.to_hex() + )) } /// keccak256("PackedUserOperation(address sender,uint256 nonce,bytes initCode,bytes callData,