From 0585af424fe4fc6546f5edef345089393b7d110c Mon Sep 17 00:00:00 2001 From: Shawn Date: Mon, 15 Jun 2026 15:01:31 +0800 Subject: [PATCH 01/13] switch from CAIP10 to ERC7930 addresses. --- Cargo.lock | 26 ++++++++++------------ bundler_tests/DummyBridge.sol | 41 ++++++++++++++++++++++------------- zilliqa/Cargo.toml | 2 +- zilliqa/src/uccb/mod.rs | 2 +- zilliqa/src/uccb/signer.rs | 12 +++++----- zilliqa/src/uccb/utils.rs | 33 +++++++++++++++------------- 6 files changed, 63 insertions(+), 53 deletions(-) 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..efcf50c7c 100644 --- a/bundler_tests/DummyBridge.sol +++ b/bundler_tests/DummyBridge.sol @@ -15,8 +15,8 @@ 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"; contract DummyBridge is Pausable, @@ -33,15 +33,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), @@ -136,7 +138,7 @@ contract DummyBridge is } function getFees( - string calldata chain_id + uint64 chain_id ) public view virtual returns (uint128[6] memory) { return destinationFees[chain_id]; } @@ -171,15 +173,16 @@ contract DummyBridge is // 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,22 +199,27 @@ 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? ) public payable virtual whenNotPaused returns (bytes32 sendId) { - require(msg.value == 0, "received value"); + (uint256 chainId, address addr) = InteroperableAddress.parseEvmV1( + recipient + ); // reverts if recipient is invalid // retrieve destination fee structure bytes[] memory attributes = new bytes[](1); bytes memory feeAttribute = abi.encodeWithSignature( "feeParams(uint128[6])", - destinationFees[CAIP2.local()] + destinationFees[block.chainid] ); 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( @@ -225,7 +233,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/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/uccb/mod.rs b/zilliqa/src/uccb/mod.rs index b5e79b04d..9b42bc4aa 100644 --- a/zilliqa/src/uccb/mod.rs +++ b/zilliqa/src/uccb/mod.rs @@ -62,7 +62,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]); } } diff --git a/zilliqa/src/uccb/signer.rs b/zilliqa/src/uccb/signer.rs index 70b84d5b6..9e1b5e565 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}, }, }; @@ -230,11 +230,11 @@ impl Signer { } // 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,7 +243,7 @@ impl Signer { "MessageSent({chain:?}): invalid source" ); // MessageSent comes from source - let Ok(sender) = get_eip155_address(std::str::from_utf8(&sender)?) else { + let Ok(sender) = get_erc7930_address(sender.iter().as_slice()) else { tracing::warn!(send_id=%sendId, "MessageSent({chain:?}): invalid sender"); continue; }; @@ -532,9 +532,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?; diff --git a/zilliqa/src/uccb/utils.rs b/zilliqa/src/uccb/utils.rs index b563f5d40..98ca9f468 100644 --- a/zilliqa/src/uccb/utils.rs +++ b/zilliqa/src/uccb/utils.rs @@ -1,35 +1,38 @@ use alloy::{ dyn_abi::Eip712Domain, - hex::FromHex, primitives::{Address, B256, U256, keccak256}, sol_types::SolValue, }; use alloy_chains::Chain; use anyhow::Result; +use crate::api::to_hex::ToHex; + use super::PackedUserOperation; /// 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, From 6aa096a3d45404b2e4de2e814de5595b35a4fd1a Mon Sep 17 00:00:00 2001 From: Shawn Date: Tue, 16 Jun 2026 10:48:18 +0800 Subject: [PATCH 02/13] skip local-deliveries. --- zilliqa/src/uccb/signer.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/zilliqa/src/uccb/signer.rs b/zilliqa/src/uccb/signer.rs index 9e1b5e565..6c2513790 100644 --- a/zilliqa/src/uccb/signer.rs +++ b/zilliqa/src/uccb/signer.rs @@ -219,6 +219,10 @@ 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 From c66a298011819d5bfe3f268155a89303327ecde9 Mon Sep 17 00:00:00 2001 From: Shawn Date: Tue, 16 Jun 2026 16:03:30 +0800 Subject: [PATCH 03/13] Initial UccbGateway.sol --- zilliqa/src/contracts/uccb/UccbGateway.sol | 264 +++++++++++++++++++++ 1 file changed, 264 insertions(+) create mode 100644 zilliqa/src/contracts/uccb/UccbGateway.sol diff --git a/zilliqa/src/contracts/uccb/UccbGateway.sol b/zilliqa/src/contracts/uccb/UccbGateway.sol new file mode 100644 index 000000000..ec5177119 --- /dev/null +++ b/zilliqa/src/contracts/uccb/UccbGateway.sol @@ -0,0 +1,264 @@ +// 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 {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.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"; + +/** + * @title PayloadCodec + * @notice Encode and decode the application-level message envelope that + * travels as the `payload` field of an ERC-7786 message. + * + * @dev Envelope format (abi.encode): + * + * ( uint8 version, - codec version (currently 1) + * bytes4 msgType, - application-defined message type selector + * bytes body ) - type-specific payload bytes + * + * Versioning the envelope at the codec level means the gateway can + * support rolling upgrades without breaking in-flight messages. + */ +library PayloadCodec { + bytes4 public constant MSG_CALL = 0x1b8b921d; // call(address,bytes) + bytes4 public constant MSG_TRANSFER = 0xa9059cbb; // transfer(address,uint256) + + uint8 internal constant VERSION = 1; + + function encode( + bytes4 msgType, + bytes memory body + ) internal pure returns (bytes memory) { + return abi.encode(VERSION, msgType, body); + } + + function decode( + bytes calldata payload + ) internal pure returns (bytes4 msgType, bytes memory body) { + require(payload.length >= 32, "Payload too short"); + (uint8 version, bytes4 mType, bytes memory b) = abi.decode( + payload, + (uint8, bytes4, bytes) + ); + require(version == VERSION, "Payload unsupported"); + return (mType, b); + } +} + +/** + * @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. Gateway verifies the relayer's RELAYER_ROLE attestation signature. + * 3. Calls receiveMessage on the registered IERC7786Recipient. + * 4. Emits MessageDelivered. + * + * @custom:oz-upgrades-unsafe-allow constructor + */ + +contract UccbGateway is + Initializable, + CrosschainLinkedUpgradeable, + UUPSUpgradeable, + OwnableUpgradeable, + PausableUpgradeable, + ReentrancyGuardTransient, + NoncesKeyedUpgradeable, + IERC7786GatewaySource +{ + // using Address for address payable; + // using SafeCast for uint256; + using InteroperableAddress for bytes; + using Bytes for bytes; + + /// Emitted when an inbound message is successfully received. + event MessageReceived(bytes32 indexed receiveId, address gateway); + + /** + * @notice One-time proxy initializer. + * + * @param owner 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 owner, + CrosschainLinkedUpgradeable.Link[] memory links + ) external initializer { + require(owner != address(0), "Owner == 0"); + __Pausable_init(); + __CrosschainLinked_init(links); + __Ownable_init(owner); + } + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + // 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 sendId) + { + require(payload.length != 0, "Payload == 0"); + require(msg.value == 0, "Value != 0"); + require(attributes.length == 0, "Attributes != 0"); + + // ERC7930(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 + PayloadCodec.encode( + PayloadCodec.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 returns (bytes32) { + (, bytes memory counterpart) = getLink(chain); + + bytes memory sender = InteroperableAddress.formatEvmV1( + block.chainid, + address(this) + ); + + bytes32 sendId = keccak256(payload); + + emit MessageSent(sendId, sender, counterpart, payload, 0, attributes); + + return sendId; + } + + function setLink( + address sender, + bytes memory counterpart, + bool allowOverride + ) public onlyOwner { + _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) = PayloadCodec.decode( + wrappedPayload[4:] + ); + require(msgType == PayloadCodec.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 self + ) private pure returns (bytes memory) { + (bytes2 chainType, bytes memory chainReference, ) = self.parseV1(); + return InteroperableAddress.formatV1(chainType, chainReference, hex""); + } + + // UUPSUpgradeable + + function _authorizeUpgrade( + address newImplementation + ) internal override onlyOwner {} + + // PausableUpgradeable – restricted entry points + + function pause() external onlyOwner { + _pause(); + } + function unpause() external onlyOwner { + _unpause(); + } +} From 78d5566d2e2929cc3074fbee5d41a94fc36ed507 Mon Sep 17 00:00:00 2001 From: Shawn Date: Tue, 16 Jun 2026 17:26:02 +0800 Subject: [PATCH 04/13] enable web3,net apis; and added eth_accounts() - for remix. --- zilliqa/src/api/bundler.rs | 20 ++++++++++++++++++++ zilliqa/src/api/mod.rs | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/zilliqa/src/api/bundler.rs b/zilliqa/src/api/bundler.rs index 78688dba7..9525b49c8 100644 --- a/zilliqa/src/api/bundler.rs +++ b/zilliqa/src/api/bundler.rs @@ -2,6 +2,7 @@ use std::{sync::Arc, time::Duration}; use alloy::{ eips::BlockId, + primitives::Address, rpc::types::{ BlockOverrides, TransactionRequest, state::{AccountOverride, StateOverride}, @@ -10,6 +11,7 @@ use alloy::{ use alloy_rpc_types_trace::geth::{GethDebugTracingCallOptions, GethTrace}; use anyhow::{Result, anyhow}; use eth_trie::{EthTrie, Trie as _}; +use itertools::Itertools; use jsonrpsee::{ RpcModule, types::{ErrorObjectOwned, Params}, @@ -32,6 +34,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 +53,7 @@ pub fn rpc_module(node: Arc, enabled_apis: &[EnabledApi]) -> RpcModule, enabled_apis: &[EnabledApi]) -> RpcModule) -> Result> { + let accounts = node + .config + .consensus + .genesis_accounts + .iter() + .map(|(addr, _amount)| *addr) + .collect_vec(); + Ok(accounts) +} + 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() From ab785b90d1ae252ecd2e94a3776c19485df7ba02 Mon Sep 17 00:00:00 2001 From: Shawn Date: Wed, 17 Jun 2026 09:17:35 +0800 Subject: [PATCH 05/13] added local/personal features. --- bundler_tests/DummyBridge.sol | 14 +-- zilliqa/src/api/bundler.rs | 61 +++++++++-- zilliqa/src/contracts/uccb/UccbGateway.sol | 122 +++++++++------------ zilliqa/src/node.rs | 2 + 4 files changed, 105 insertions(+), 94 deletions(-) diff --git a/bundler_tests/DummyBridge.sol b/bundler_tests/DummyBridge.sol index efcf50c7c..c4f6c29b1 100644 --- a/bundler_tests/DummyBridge.sol +++ b/bundler_tests/DummyBridge.sol @@ -201,20 +201,8 @@ contract DummyBridge is function sendMessage( 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) { - (uint256 chainId, address addr) = InteroperableAddress.parseEvmV1( - recipient - ); // reverts if recipient is invalid - - // retrieve destination fee structure - bytes[] memory attributes = new bytes[](1); - bytes memory feeAttribute = abi.encodeWithSignature( - "feeParams(uint128[6])", - destinationFees[block.chainid] - ); - attributes[0] = feeAttribute; - // wrapping the payload bytes memory sender = InteroperableAddress.formatEvmV1( block.chainid, diff --git a/zilliqa/src/api/bundler.rs b/zilliqa/src/api/bundler.rs index 9525b49c8..0f503f5dd 100644 --- a/zilliqa/src/api/bundler.rs +++ b/zilliqa/src/api/bundler.rs @@ -1,17 +1,18 @@ use std::{sync::Arc, time::Duration}; use alloy::{ + consensus::TxLegacy, eips::BlockId, - primitives::Address, + 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 itertools::Itertools; use jsonrpsee::{ RpcModule, types::{ErrorObjectOwned, Params}, @@ -24,6 +25,7 @@ use crate::{ to_hex::ToHex as _, }, cfg::EnabledApi, + crypto::Hash, error::ensure_success, node::Node, state::Code, @@ -54,6 +56,11 @@ pub fn rpc_module(node: Arc, enabled_apis: &[EnabledApi]) -> RpcModule, enabled_apis: &[EnabledApi]) -> RpcModule) -> Result> { - let accounts = node - .config - .consensus - .genesis_accounts - .iter() - .map(|(addr, _amount)| *addr) - .collect_vec(); - Ok(accounts) + 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 { diff --git a/zilliqa/src/contracts/uccb/UccbGateway.sol b/zilliqa/src/contracts/uccb/UccbGateway.sol index ec5177119..3641b1e3e 100644 --- a/zilliqa/src/contracts/uccb/UccbGateway.sol +++ b/zilliqa/src/contracts/uccb/UccbGateway.sol @@ -17,46 +17,6 @@ import {ReentrancyGuardTransient} from "@openzeppelin/contracts/utils/Reentrancy import {NoncesKeyedUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/NoncesKeyedUpgradeable.sol"; import {IAccountExecute} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; -/** - * @title PayloadCodec - * @notice Encode and decode the application-level message envelope that - * travels as the `payload` field of an ERC-7786 message. - * - * @dev Envelope format (abi.encode): - * - * ( uint8 version, - codec version (currently 1) - * bytes4 msgType, - application-defined message type selector - * bytes body ) - type-specific payload bytes - * - * Versioning the envelope at the codec level means the gateway can - * support rolling upgrades without breaking in-flight messages. - */ -library PayloadCodec { - bytes4 public constant MSG_CALL = 0x1b8b921d; // call(address,bytes) - bytes4 public constant MSG_TRANSFER = 0xa9059cbb; // transfer(address,uint256) - - uint8 internal constant VERSION = 1; - - function encode( - bytes4 msgType, - bytes memory body - ) internal pure returns (bytes memory) { - return abi.encode(VERSION, msgType, body); - } - - function decode( - bytes calldata payload - ) internal pure returns (bytes4 msgType, bytes memory body) { - require(payload.length >= 32, "Payload too short"); - (uint8 version, bytes4 mType, bytes memory b) = abi.decode( - payload, - (uint8, bytes4, bytes) - ); - require(version == VERSION, "Payload unsupported"); - return (mType, b); - } -} - /** * @title UccbGateway * @notice ERC-7786 gateway contract that implements both @@ -70,9 +30,8 @@ library PayloadCodec { * * INBOUND (destination side / recipient side): * 1. Sender calls receiveMessage() with the cross-chain payload. - * 2. Gateway verifies the relayer's RELAYER_ROLE attestation signature. - * 3. Calls receiveMessage on the registered IERC7786Recipient. - * 4. Emits MessageDelivered. + * 2. Calls receiveMessage on the registered IERC7786Recipient. + * 3. Emits MessageReceived. * * @custom:oz-upgrades-unsafe-allow constructor */ @@ -106,7 +65,7 @@ contract UccbGateway is address owner, CrosschainLinkedUpgradeable.Link[] memory links ) external initializer { - require(owner != address(0), "Owner == 0"); + assert(owner != address(0)); __Pausable_init(); __CrosschainLinked_init(links); __Ownable_init(owner); @@ -117,6 +76,36 @@ contract UccbGateway is _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) { + 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 @@ -128,17 +117,10 @@ contract UccbGateway is bytes calldata recipient, // ERC7930(recipient) bytes calldata payload, bytes[] calldata attributes - ) - external - payable - override - whenNotPaused - nonReentrant - returns (bytes32 sendId) - { - require(payload.length != 0, "Payload == 0"); - require(msg.value == 0, "Value != 0"); - require(attributes.length == 0, "Attributes != 0"); + ) 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( @@ -148,12 +130,9 @@ contract UccbGateway is uint256 nonce = _useNonce(address(this), uint192(0)); - bytes memory wrappedPayload = abi.encodeWithSelector( - IAccountExecute.executeUserOp.selector, // needed to trigger executeUserOp() later - PayloadCodec.encode( - PayloadCodec.MSG_CALL, - abi.encode(sender, recipient, payload, nonce) - ) + bytes memory wrappedPayload = _encode( + MSG_CALL, + abi.encode(sender, recipient, payload, nonce) ); // TODO: deliver local messages directly? @@ -174,14 +153,21 @@ contract UccbGateway is ) internal override returns (bytes32) { (, bytes memory counterpart) = getLink(chain); - bytes memory sender = InteroperableAddress.formatEvmV1( + bytes memory originator = InteroperableAddress.formatEvmV1( block.chainid, address(this) ); bytes32 sendId = keccak256(payload); - emit MessageSent(sendId, sender, counterpart, payload, 0, attributes); + emit MessageSent( + sendId, + originator, + counterpart, + payload, + 0, + attributes + ); return sendId; } @@ -208,10 +194,8 @@ contract UccbGateway is (, address senderAddr) = relayer.parseEvmV1(); // Deconstruct the quad-tuple payload - (bytes4 msgType, bytes memory quadtuple) = PayloadCodec.decode( - wrappedPayload[4:] - ); - require(msgType == PayloadCodec.MSG_CALL); + (bytes4 msgType, bytes memory quadtuple) = _decode(wrappedPayload); + require(msgType == MSG_CALL); ( bytes memory sender, bytes memory recipient, @@ -241,9 +225,9 @@ contract UccbGateway is // Helper function __extractChain( - bytes memory self + bytes memory s ) private pure returns (bytes memory) { - (bytes2 chainType, bytes memory chainReference, ) = self.parseV1(); + (bytes2 chainType, bytes memory chainReference, ) = s.parseV1(); return InteroperableAddress.formatV1(chainType, chainReference, hex""); } 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(), From 8fb9977f2b625b4a5e1ba4a0ed474f332a3f5c7e Mon Sep 17 00:00:00 2001 From: Shawn Date: Wed, 17 Jun 2026 10:34:35 +0800 Subject: [PATCH 06/13] Initial UccbSender.sol --- zilliqa/src/contracts/uccb/UccbGateway.sol | 1 + zilliqa/src/contracts/uccb/UccbSender.sol | 108 +++++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 zilliqa/src/contracts/uccb/UccbSender.sol diff --git a/zilliqa/src/contracts/uccb/UccbGateway.sol b/zilliqa/src/contracts/uccb/UccbGateway.sol index 3641b1e3e..b0e0dfb26 100644 --- a/zilliqa/src/contracts/uccb/UccbGateway.sol +++ b/zilliqa/src/contracts/uccb/UccbGateway.sol @@ -85,6 +85,7 @@ contract UccbGateway is bytes4 msgType, bytes memory body ) internal pure returns (bytes memory) { + // TODO: Reduce size return abi.encodeWithSelector( IAccountExecute.executeUserOp.selector, // needed to trigger executeUserOp() later diff --git a/zilliqa/src/contracts/uccb/UccbSender.sol b/zilliqa/src/contracts/uccb/UccbSender.sol new file mode 100644 index 000000000..5c46cec7d --- /dev/null +++ b/zilliqa/src/contracts/uccb/UccbSender.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {Account} from "@openzeppelin/contracts/account/Account.sol"; +import {ERC4337Utils} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol"; +import { + IEntryPoint, + 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"; + +/** + * @title UccbSmartAccount + * @notice ERC-4337 Sender contract built entirely on OpenZeppelin v5.6.x. + * + * @custom:oz-upgrades-unsafe-allow constructor + */ +contract UccbSmartAccount is + Initializable, + // SignerECDSAUpgradeable, + // ERC7739Upgradeable, + // ERC165Upgradeable, + UUPSUpgradeable, + ReentrancyGuardTransient, + // IERC721Receiver, + // IERC1155Receiver + 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 signerAddr Owner / signing key for this account. + * + * UUPSUpgradeable and ReentrancyGuardTransient have no state to init. + */ + function initialize(address signerAddr) external initializer { + assert(signerAddr != address(0)); + } + + /// Use v0.8 entrypoint only + function entryPoint() public pure override returns (IEntryPoint) { + return ERC4337Utils.ENTRYPOINT_V08; + } + + /// Called by validateUserOp() + function _rawSignatureValidation( + bytes32 hash, + bytes calldata // signature + ) internal pure override returns (bool) { + // TODO: Check signature + return hash != 0x0; + } + + /// Called by handleOps() + function executeUserOp( + PackedUserOperation calldata userOp, + bytes32 userOpHash + ) external {} + + + /** + * @notice Top-up this account's gas deposit in the EntryPoint. + */ + function addDeposit() external payable { + entryPoint().depositTo{value: msg.value}(address(this)); + } + + /** + * @notice View current EntryPoint deposit balance. + */ + function getDeposit() 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 withdrawDepositTo( + address payable to, + uint256 amount + ) external onlyEntryPointOrSelf { + entryPoint().withdrawTo(to, amount); + } + + /// UUPSUpgradeable + function _authorizeUpgrade( + address /*newImplementation*/ + ) internal view override onlyEntryPointOrSelf {} +} From f4afe575b7a42bef617b02deb8e150965e598e22 Mon Sep 17 00:00:00 2001 From: Shawn Date: Wed, 17 Jun 2026 15:31:52 +0800 Subject: [PATCH 07/13] Initial UccbPaymaster.sol --- bundler_tests/config-bundler-spec-tests.toml | 4 +- zilliqa/src/contracts/uccb/UccbPaymaster.sol | 190 +++++++++++++++++++ zilliqa/src/contracts/uccb/UccbSender.sol | 9 +- 3 files changed, 200 insertions(+), 3 deletions(-) create mode 100644 zilliqa/src/contracts/uccb/UccbPaymaster.sol 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/src/contracts/uccb/UccbPaymaster.sol b/zilliqa/src/contracts/uccb/UccbPaymaster.sol new file mode 100644 index 000000000..792f8e09c --- /dev/null +++ b/zilliqa/src/contracts/uccb/UccbPaymaster.sol @@ -0,0 +1,190 @@ +// 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 {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; + +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + +/** + * @title Paymaster + * @notice ERC-4337 Paymaster skeleton built entirely on OpenZeppelin v5.6.x. + */ +contract UccbPaymaster is + Initializable, + UUPSUpgradeable, + OwnableUpgradeable, + PausableUpgradeable, + // EIP712Upgradeable, + ReentrancyGuardTransient, + IPaymaster +{ + // using SafeERC20 for IERC20; + using Address for address payable; + using ERC4337Utils for PackedUserOperation; + + /** + * @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.8 entrypoint only + function entryPoint() private pure returns (IEntryPoint) { + return ERC4337Utils.ENTRYPOINT_V08; + } + + /** + * @dev Permanently disables initializers on the bare implementation + * so it cannot be hijacked. + * @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)); + __Ownable_init(_admin); + __Pausable_init(); + } + + /** + * @notice Called by the EntryPoint during the verification loop. + * Must decide whether to sponsor this UserOp and return: + * - context: arbitrary bytes forwarded to postOp (may be empty) + * - validationData: packed (sigFailure | validUntil | validAfter) + * via ERC4337Utils.packValidationData + */ + function validatePaymasterUserOp( + PackedUserOperation calldata, // userOp, + bytes32, // userOpHash, + uint256 // maxCost + ) + external + view + override + onlyEntryPoint + whenNotPaused + returns (bytes memory context, uint256 validationData) + { + // Context is for postOp bookkeeping. + context = ""; + validationData = ERC4337Utils.packValidationData(true, 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 { + // Decode the sponsor mode that was stored in context. + 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 depositToEntryPoint() 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 withdrawFromEntryPoint( + uint256 amount + ) external onlyOwner nonReentrant { + entryPoint().withdrawTo(payable(address(this)), amount); + } + + /** + * @notice View the current EntryPoint deposit balance. + */ + function getDeposit() external view returns (uint256) { + return entryPoint().balanceOf(address(this)); + } + + /** + * @notice Add stake to the EntryPoint for this paymaster. + * + * @dev Paymasters that access global / non-sender-associated storage + * in validatePaymasterUserOp MUST be staked to avoid bundler + * rejection under ERC-7562 reputation rules. + * + * @param unstakeDelaySec Delay (seconds) before stake can be withdrawn. + * Must meet the EntryPoint's minimum. + */ + function addStake(uint32 unstakeDelaySec) external payable onlyOwner { + entryPoint().addStake{value: msg.value}(unstakeDelaySec); + } + + /** + * @notice Initiate the stake unlock process. After the unstake delay + * has elapsed, call {withdrawStake}. + */ + function unlockStake() external onlyOwner { + entryPoint().unlockStake(); + } + + /** + * @notice Withdraw previously unlocked stake. + * @param to Recipient of the returned ETH. + */ + function withdrawStake(address payable to) external onlyOwner 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 onlyOwner { + _pause(); + } + + /** + * @notice Resume normal operation. + */ + function unpause() external onlyOwner { + _unpause(); + } + + function _authorizeUpgrade( + address /*newImplementation*/ + ) internal view override onlyOwner {} + + /** + * @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 index 5c46cec7d..42dc4cd5e 100644 --- a/zilliqa/src/contracts/uccb/UccbSender.sol +++ b/zilliqa/src/contracts/uccb/UccbSender.sol @@ -74,7 +74,6 @@ contract UccbSmartAccount is bytes32 userOpHash ) external {} - /** * @notice Top-up this account's gas deposit in the EntryPoint. */ @@ -105,4 +104,12 @@ contract UccbSmartAccount is function _authorizeUpgrade( address /*newImplementation*/ ) internal view override onlyEntryPointOrSelf {} + + /** + * @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); + // } } From 5f135da5f19044bf96587497f105a74f72f72c5f Mon Sep 17 00:00:00 2001 From: Shawn Date: Wed, 17 Jun 2026 17:20:21 +0800 Subject: [PATCH 08/13] moved from Owned to AccessControl. --- zilliqa/src/contracts/uccb/UccbGateway.sol | 39 +++++++++++---- zilliqa/src/contracts/uccb/UccbPaymaster.sol | 52 +++++++++++++------- zilliqa/src/contracts/uccb/UccbSender.sol | 12 ++--- 3 files changed, 67 insertions(+), 36 deletions(-) diff --git a/zilliqa/src/contracts/uccb/UccbGateway.sol b/zilliqa/src/contracts/uccb/UccbGateway.sol index b0e0dfb26..0bf68121c 100644 --- a/zilliqa/src/contracts/uccb/UccbGateway.sol +++ b/zilliqa/src/contracts/uccb/UccbGateway.sol @@ -10,12 +10,14 @@ import {InteroperableAddress} from "@openzeppelin/contracts/utils/draft-Interope 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 {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.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 {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; /** * @title UccbGateway @@ -40,7 +42,7 @@ contract UccbGateway is Initializable, CrosschainLinkedUpgradeable, UUPSUpgradeable, - OwnableUpgradeable, + AccessControlUpgradeable, PausableUpgradeable, ReentrancyGuardTransient, NoncesKeyedUpgradeable, @@ -57,18 +59,21 @@ contract UccbGateway is /** * @notice One-time proxy initializer. * - * @param owner Address granted DEFAULT_ADMIN_ROLE (and all sub-roles). + * @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 owner, + address admin_, CrosschainLinkedUpgradeable.Link[] memory links ) external initializer { - assert(owner != address(0)); + assert(admin_ != address(0)); + + __AccessControl_init(); __Pausable_init(); __CrosschainLinked_init(links); - __Ownable_init(owner); + + _grantRole(DEFAULT_ADMIN_ROLE, admin_); } /// @custom:oz-upgrades-unsafe-allow constructor @@ -177,7 +182,7 @@ contract UccbGateway is address sender, bytes memory counterpart, bool allowOverride - ) public onlyOwner { + ) public onlyRole(DEFAULT_ADMIN_ROLE) { _setLink(sender, counterpart, allowOverride); } @@ -236,14 +241,28 @@ contract UccbGateway is function _authorizeUpgrade( address newImplementation - ) internal override onlyOwner {} + ) internal override onlyRole(DEFAULT_ADMIN_ROLE) {} // PausableUpgradeable – restricted entry points - function pause() external onlyOwner { + function pause() external onlyRole(DEFAULT_ADMIN_ROLE) { _pause(); } - function unpause() external onlyOwner { + function unpause() external onlyRole(DEFAULT_ADMIN_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 index 792f8e09c..72ebfd8f1 100644 --- a/zilliqa/src/contracts/uccb/UccbPaymaster.sol +++ b/zilliqa/src/contracts/uccb/UccbPaymaster.sol @@ -12,8 +12,9 @@ import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Ini 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 {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; - +import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.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"; /** @@ -23,7 +24,7 @@ import {Address} from "@openzeppelin/contracts/utils/Address.sol"; contract UccbPaymaster is Initializable, UUPSUpgradeable, - OwnableUpgradeable, + AccessControlUpgradeable, PausableUpgradeable, // EIP712Upgradeable, ReentrancyGuardTransient, @@ -47,11 +48,7 @@ contract UccbPaymaster is return ERC4337Utils.ENTRYPOINT_V08; } - /** - * @dev Permanently disables initializers on the bare implementation - * so it cannot be hijacked. - * @custom:oz-upgrades-unsafe-allow constructor - */ + /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } @@ -62,12 +59,15 @@ contract UccbPaymaster is * @notice One-time initializer called by the factory through the proxy. */ function initialize( - address _admin, + address admin_, uint256 // _maxCostPerOp ) external initializer { - assert(_admin != address(0)); - __Ownable_init(_admin); + assert(admin_ != address(0)); + + __AccessControl_init(); __Pausable_init(); + + _grantRole(DEFAULT_ADMIN_ROLE, admin_); } /** @@ -122,7 +122,7 @@ contract UccbPaymaster is */ function withdrawFromEntryPoint( uint256 amount - ) external onlyOwner nonReentrant { + ) external onlyRole(DEFAULT_ADMIN_ROLE) nonReentrant { entryPoint().withdrawTo(payable(address(this)), amount); } @@ -143,7 +143,9 @@ contract UccbPaymaster is * @param unstakeDelaySec Delay (seconds) before stake can be withdrawn. * Must meet the EntryPoint's minimum. */ - function addStake(uint32 unstakeDelaySec) external payable onlyOwner { + function addStake( + uint32 unstakeDelaySec + ) external payable onlyRole(DEFAULT_ADMIN_ROLE) { entryPoint().addStake{value: msg.value}(unstakeDelaySec); } @@ -151,7 +153,7 @@ contract UccbPaymaster is * @notice Initiate the stake unlock process. After the unstake delay * has elapsed, call {withdrawStake}. */ - function unlockStake() external onlyOwner { + function unlockStake() external onlyRole(DEFAULT_ADMIN_ROLE) { entryPoint().unlockStake(); } @@ -159,7 +161,9 @@ contract UccbPaymaster is * @notice Withdraw previously unlocked stake. * @param to Recipient of the returned ETH. */ - function withdrawStake(address payable to) external onlyOwner nonReentrant { + function withdrawStake( + address payable to + ) external onlyRole(DEFAULT_ADMIN_ROLE) nonReentrant { assert(to != address(0)); entryPoint().withdrawStake(to); } @@ -168,20 +172,32 @@ contract UccbPaymaster is * @notice Pause the paymaster. validatePaymasterUserOp will revert * while paused, preventing new ops from being sponsored. */ - function pause() external onlyOwner { + function pause() external onlyRole(DEFAULT_ADMIN_ROLE) { _pause(); } /** * @notice Resume normal operation. */ - function unpause() external onlyOwner { + function unpause() external onlyRole(DEFAULT_ADMIN_ROLE) { _unpause(); } function _authorizeUpgrade( address /*newImplementation*/ - ) internal view override onlyOwner {} + ) internal view override onlyRole(DEFAULT_ADMIN_ROLE) {} + + /** + * @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 || + super.supportsInterface(interfaceId); + } /** * @dev Accept ETH (refunds from EntryPoint, direct top-ups, etc.). diff --git a/zilliqa/src/contracts/uccb/UccbSender.sol b/zilliqa/src/contracts/uccb/UccbSender.sol index 42dc4cd5e..8f9b3f297 100644 --- a/zilliqa/src/contracts/uccb/UccbSender.sol +++ b/zilliqa/src/contracts/uccb/UccbSender.sol @@ -8,10 +8,8 @@ import { 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"; @@ -35,9 +33,7 @@ contract UccbSmartAccount is { using Address for address; - /** - * @custom:oz-upgrades-unsafe-allow constructor - */ + /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } @@ -109,7 +105,7 @@ contract UccbSmartAccount is * @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); - // } + receive() external payable virtual override { + // emit Received(msg.sender, msg.value); + } } From 4ea83c9c6cacd01e8d1fe16b502969663d1a1785 Mon Sep 17 00:00:00 2001 From: Shawn Date: Wed, 17 Jun 2026 17:45:06 +0800 Subject: [PATCH 09/13] EIP712, ERC165. --- zilliqa/src/contracts/uccb/UccbGateway.sol | 9 +++- zilliqa/src/contracts/uccb/UccbPaymaster.sol | 29 +++++------ zilliqa/src/contracts/uccb/UccbSender.sol | 51 ++++++++++++++------ 3 files changed, 59 insertions(+), 30 deletions(-) diff --git a/zilliqa/src/contracts/uccb/UccbGateway.sol b/zilliqa/src/contracts/uccb/UccbGateway.sol index 0bf68121c..c8c4d3357 100644 --- a/zilliqa/src/contracts/uccb/UccbGateway.sol +++ b/zilliqa/src/contracts/uccb/UccbGateway.sol @@ -15,7 +15,7 @@ import {EIP712Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/crypt 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 {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.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"; @@ -46,6 +46,7 @@ contract UccbGateway is PausableUpgradeable, ReentrancyGuardTransient, NoncesKeyedUpgradeable, + EIP712Upgradeable, IERC7786GatewaySource { // using Address for address payable; @@ -69,9 +70,11 @@ contract UccbGateway is ) external initializer { assert(admin_ != address(0)); + __EIP712_init("UccbGateway", "1"); __AccessControl_init(); __Pausable_init(); __CrosschainLinked_init(links); + __ERC165_init(); _grantRole(DEFAULT_ADMIN_ROLE, admin_); } @@ -241,7 +244,9 @@ contract UccbGateway is function _authorizeUpgrade( address newImplementation - ) internal override onlyRole(DEFAULT_ADMIN_ROLE) {} + ) internal override onlyRole(DEFAULT_ADMIN_ROLE) { + // TODO: audit log + } // PausableUpgradeable – restricted entry points diff --git a/zilliqa/src/contracts/uccb/UccbPaymaster.sol b/zilliqa/src/contracts/uccb/UccbPaymaster.sol index 72ebfd8f1..06b58fc32 100644 --- a/zilliqa/src/contracts/uccb/UccbPaymaster.sol +++ b/zilliqa/src/contracts/uccb/UccbPaymaster.sol @@ -12,10 +12,11 @@ import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Ini 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 {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.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"; /** * @title Paymaster @@ -26,7 +27,7 @@ contract UccbPaymaster is UUPSUpgradeable, AccessControlUpgradeable, PausableUpgradeable, - // EIP712Upgradeable, + EIP712Upgradeable, ReentrancyGuardTransient, IPaymaster { @@ -64,8 +65,10 @@ contract UccbPaymaster is ) external initializer { assert(admin_ != address(0)); + __EIP712_init("UccbPaymaster", "1"); __AccessControl_init(); __Pausable_init(); + __ERC165_init(); _grantRole(DEFAULT_ADMIN_ROLE, admin_); } @@ -89,9 +92,8 @@ contract UccbPaymaster is whenNotPaused returns (bytes memory context, uint256 validationData) { - // Context is for postOp bookkeeping. - context = ""; - validationData = ERC4337Utils.packValidationData(true, 0, 0); // true, forever + // TODO: allow from SENDER + return ("", ERC4337Utils.packValidationData(true, 0, 0)); // true, forever } /** @@ -104,7 +106,7 @@ contract UccbPaymaster is uint256, // actualGasCost, uint256 // actualUserOpFeePerGas ) external view override onlyEntryPoint { - // Decode the sponsor mode that was stored in context. + // TODO: record the signer and co-signers if (context.length == 0) return; } @@ -112,7 +114,7 @@ contract UccbPaymaster is * @notice Deposit ETH into the EntryPoint so the paymaster can cover gas. * Anyone can call; the EntryPoint credits the deposit to this contract. */ - function depositToEntryPoint() external payable nonReentrant { + function depositTo() external payable nonReentrant { entryPoint().depositTo{value: msg.value}(address(this)); } @@ -120,7 +122,7 @@ contract UccbPaymaster is * @notice Withdraw ETH from the EntryPoint deposit back to this contract. * @param amount Wei to withdraw. */ - function withdrawFromEntryPoint( + function withdrawTo( uint256 amount ) external onlyRole(DEFAULT_ADMIN_ROLE) nonReentrant { entryPoint().withdrawTo(payable(address(this)), amount); @@ -129,17 +131,13 @@ contract UccbPaymaster is /** * @notice View the current EntryPoint deposit balance. */ - function getDeposit() external view returns (uint256) { + function balanceOf() external view returns (uint256) { return entryPoint().balanceOf(address(this)); } /** * @notice Add stake to the EntryPoint for this paymaster. * - * @dev Paymasters that access global / non-sender-associated storage - * in validatePaymasterUserOp MUST be staked to avoid bundler - * rejection under ERC-7562 reputation rules. - * * @param unstakeDelaySec Delay (seconds) before stake can be withdrawn. * Must meet the EntryPoint's minimum. */ @@ -185,7 +183,9 @@ contract UccbPaymaster is function _authorizeUpgrade( address /*newImplementation*/ - ) internal view override onlyRole(DEFAULT_ADMIN_ROLE) {} + ) internal view override onlyRole(DEFAULT_ADMIN_ROLE) { + // TODO: audit log + } /** * @dev Advertises all interfaces implemented by this contract. @@ -196,6 +196,7 @@ contract UccbPaymaster is ) public view virtual override(AccessControlUpgradeable) returns (bool) { return interfaceId == type(IPaymaster).interfaceId || + interfaceId == type(IERC165).interfaceId || super.supportsInterface(interfaceId); } diff --git a/zilliqa/src/contracts/uccb/UccbSender.sol b/zilliqa/src/contracts/uccb/UccbSender.sol index 8f9b3f297..9a8b33b3b 100644 --- a/zilliqa/src/contracts/uccb/UccbSender.sol +++ b/zilliqa/src/contracts/uccb/UccbSender.sol @@ -5,6 +5,7 @@ import {Account} from "@openzeppelin/contracts/account/Account.sol"; import {ERC4337Utils} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol"; import { IEntryPoint, + IAccount, IAccountExecute, PackedUserOperation } from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; @@ -12,22 +13,22 @@ import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/U 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"; /** - * @title UccbSmartAccount + * @title UccbSender * @notice ERC-4337 Sender contract built entirely on OpenZeppelin v5.6.x. * * @custom:oz-upgrades-unsafe-allow constructor */ -contract UccbSmartAccount is +contract UccbSender is Initializable, - // SignerECDSAUpgradeable, - // ERC7739Upgradeable, - // ERC165Upgradeable, + ERC165Upgradeable, UUPSUpgradeable, ReentrancyGuardTransient, - // IERC721Receiver, - // IERC1155Receiver + EIP712Upgradeable, IAccountExecute, Account { @@ -43,11 +44,12 @@ contract UccbSmartAccount is * deploying the proxy. * * @param signerAddr Owner / signing key for this account. - * - * UUPSUpgradeable and ReentrancyGuardTransient have no state to init. */ function initialize(address signerAddr) external initializer { assert(signerAddr != address(0)); + + __EIP712_init("UccbSender", "1"); + __ERC165_init(); } /// Use v0.8 entrypoint only @@ -68,19 +70,24 @@ contract UccbSmartAccount is function executeUserOp( PackedUserOperation calldata userOp, bytes32 userOpHash - ) external {} + ) external { + // TODO: + // 1. Determine if it is a CALL or CONFIG + // 2. On CONFIG, update the stakers; and + // 3. update the Paymaster stakers. + } /** * @notice Top-up this account's gas deposit in the EntryPoint. */ - function addDeposit() external payable { + function depositTo() external payable { entryPoint().depositTo{value: msg.value}(address(this)); } /** * @notice View current EntryPoint deposit balance. */ - function getDeposit() external view returns (uint256) { + function balanceOf() external view returns (uint256) { return entryPoint().balanceOf(address(this)); } @@ -89,7 +96,7 @@ contract UccbSmartAccount is * @param to Recipient. * @param amount Amount to withdraw (in wei). */ - function withdrawDepositTo( + function withdrawTo( address payable to, uint256 amount ) external onlyEntryPointOrSelf { @@ -99,7 +106,9 @@ contract UccbSmartAccount is /// UUPSUpgradeable function _authorizeUpgrade( address /*newImplementation*/ - ) internal view override onlyEntryPointOrSelf {} + ) internal view override onlyEntryPointOrSelf { + // TODO: audit log + } /** * @dev Account.receive() already exists and emits nothing. @@ -108,4 +117,18 @@ contract UccbSmartAccount is 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); + } } From 75c03b71b6b69c2ac19eafabd1f9fa49af4e6cb5 Mon Sep 17 00:00:00 2001 From: Shawn Date: Thu, 18 Jun 2026 15:44:41 +0800 Subject: [PATCH 10/13] minor cleanup on Uccb*.sol --- zilliqa/src/contracts/uccb/UccbGateway.sol | 25 +++++++++-- zilliqa/src/contracts/uccb/UccbPaymaster.sol | 37 ++++++++++------ zilliqa/src/contracts/uccb/UccbSender.sol | 44 ++++++++++++-------- 3 files changed, 72 insertions(+), 34 deletions(-) diff --git a/zilliqa/src/contracts/uccb/UccbGateway.sol b/zilliqa/src/contracts/uccb/UccbGateway.sol index c8c4d3357..65d917bce 100644 --- a/zilliqa/src/contracts/uccb/UccbGateway.sol +++ b/zilliqa/src/contracts/uccb/UccbGateway.sol @@ -18,6 +18,7 @@ import {IAccountExecute} from "@openzeppelin/contracts/interfaces/draft-IERC4337 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 @@ -49,11 +50,15 @@ contract UccbGateway is EIP712Upgradeable, IERC7786GatewaySource { - // using Address for address payable; + 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); @@ -77,6 +82,8 @@ contract UccbGateway is __ERC165_init(); _grantRole(DEFAULT_ADMIN_ROLE, admin_); + _grantRole(WITHDRAWER_ROLE, admin_); + _grantRole(PAUSER_ROLE, admin_); } /// @custom:oz-upgrades-unsafe-allow constructor @@ -240,6 +247,18 @@ contract UccbGateway is 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( @@ -250,10 +269,10 @@ contract UccbGateway is // PausableUpgradeable – restricted entry points - function pause() external onlyRole(DEFAULT_ADMIN_ROLE) { + function pause() external onlyRole(PAUSER_ROLE) { _pause(); } - function unpause() external onlyRole(DEFAULT_ADMIN_ROLE) { + function unpause() external onlyRole(PAUSER_ROLE) { _unpause(); } diff --git a/zilliqa/src/contracts/uccb/UccbPaymaster.sol b/zilliqa/src/contracts/uccb/UccbPaymaster.sol index 06b58fc32..3f0e78e82 100644 --- a/zilliqa/src/contracts/uccb/UccbPaymaster.sol +++ b/zilliqa/src/contracts/uccb/UccbPaymaster.sol @@ -17,6 +17,7 @@ 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 @@ -29,12 +30,20 @@ contract UccbPaymaster is 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. @@ -44,9 +53,9 @@ contract UccbPaymaster is _; } - /// Use v0.8 entrypoint only + /// Use v0.9 entrypoint only function entryPoint() private pure returns (IEntryPoint) { - return ERC4337Utils.ENTRYPOINT_V08; + return ERC4337Utils.ENTRYPOINT_V09; } /// @custom:oz-upgrades-unsafe-allow constructor @@ -71,17 +80,16 @@ contract UccbPaymaster is __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: - * - context: arbitrary bytes forwarded to postOp (may be empty) - * - validationData: packed (sigFailure | validUntil | validAfter) - * via ERC4337Utils.packValidationData */ function validatePaymasterUserOp( - PackedUserOperation calldata, // userOp, + PackedUserOperation calldata userOp, bytes32, // userOpHash, uint256 // maxCost ) @@ -90,10 +98,13 @@ contract UccbPaymaster is override onlyEntryPoint whenNotPaused - returns (bytes memory context, uint256 validationData) + returns (bytes memory, uint256) { - // TODO: allow from SENDER - return ("", ERC4337Utils.packValidationData(true, 0, 0)); // true, forever + // 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 } /** @@ -124,7 +135,7 @@ contract UccbPaymaster is */ function withdrawTo( uint256 amount - ) external onlyRole(DEFAULT_ADMIN_ROLE) nonReentrant { + ) external onlyRole(WITHDRAWER_ROLE) nonReentrant { entryPoint().withdrawTo(payable(address(this)), amount); } @@ -161,7 +172,7 @@ contract UccbPaymaster is */ function withdrawStake( address payable to - ) external onlyRole(DEFAULT_ADMIN_ROLE) nonReentrant { + ) external onlyRole(WITHDRAWER_ROLE) nonReentrant { assert(to != address(0)); entryPoint().withdrawStake(to); } @@ -170,14 +181,14 @@ contract UccbPaymaster is * @notice Pause the paymaster. validatePaymasterUserOp will revert * while paused, preventing new ops from being sponsored. */ - function pause() external onlyRole(DEFAULT_ADMIN_ROLE) { + function pause() external onlyRole(PAUSER_ROLE) { _pause(); } /** * @notice Resume normal operation. */ - function unpause() external onlyRole(DEFAULT_ADMIN_ROLE) { + function unpause() external onlyRole(PAUSER_ROLE) { _unpause(); } diff --git a/zilliqa/src/contracts/uccb/UccbSender.sol b/zilliqa/src/contracts/uccb/UccbSender.sol index 9a8b33b3b..7bffc36ff 100644 --- a/zilliqa/src/contracts/uccb/UccbSender.sol +++ b/zilliqa/src/contracts/uccb/UccbSender.sol @@ -2,6 +2,7 @@ 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, @@ -16,6 +17,8 @@ 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 @@ -25,6 +28,7 @@ import {EIP712Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/crypt */ contract UccbSender is Initializable, + MultiSignerERC7913Upgradeable, ERC165Upgradeable, UUPSUpgradeable, ReentrancyGuardTransient, @@ -43,27 +47,14 @@ contract UccbSender is * @notice One-time initializer called by the factory immediately after * deploying the proxy. * - * @param signerAddr Owner / signing key for this account. + * @param signers Owner / signing key for this account. */ - function initialize(address signerAddr) external initializer { - assert(signerAddr != address(0)); - + function initialize(bytes[] memory signers) external initializer { __EIP712_init("UccbSender", "1"); __ERC165_init(); - } - - /// Use v0.8 entrypoint only - function entryPoint() public pure override returns (IEntryPoint) { - return ERC4337Utils.ENTRYPOINT_V08; - } - /// Called by validateUserOp() - function _rawSignatureValidation( - bytes32 hash, - bytes calldata // signature - ) internal pure override returns (bool) { - // TODO: Check signature - return hash != 0x0; + _addSigners(signers); + _setThreshold(uint64(1)); // one signer will pass } /// Called by handleOps() @@ -77,6 +68,22 @@ contract UccbSender is // 3. update the Paymaster 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. */ @@ -103,7 +110,8 @@ contract UccbSender is entryPoint().withdrawTo(to, amount); } - /// UUPSUpgradeable + // ***** BOILER-PLATE ***** + function _authorizeUpgrade( address /*newImplementation*/ ) internal view override onlyEntryPointOrSelf { From d89fd4d4810a886804489e513b2934e1b684565f Mon Sep 17 00:00:00 2001 From: Shawn Date: Fri, 19 Jun 2026 13:13:35 +0800 Subject: [PATCH 11/13] incorporate Sender::execute() in the signer and contracts. --- bundler_tests/DummyBridge.sol | 86 ++++++++++++++-------- zilliqa/src/contracts/uccb/UccbGateway.sol | 12 +-- zilliqa/src/contracts/uccb/UccbSender.sol | 69 ++++++++++++++--- zilliqa/src/uccb/mod.rs | 5 ++ zilliqa/src/uccb/signer.rs | 49 ++++++++---- 5 files changed, 160 insertions(+), 61 deletions(-) diff --git a/bundler_tests/DummyBridge.sol b/bundler_tests/DummyBridge.sol index c4f6c29b1..048ea932f 100644 --- a/bundler_tests/DummyBridge.sol +++ b/bundler_tests/DummyBridge.sol @@ -3,20 +3,11 @@ pragma solidity ^0.8.28; import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol"; import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; -import { - IEntryPointNonces, - IPaymaster, - IEntryPoint, - PackedUserOperation, - IAccount, - IAccountExecute -} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; -import { - IERC7786GatewaySource, - IERC7786Recipient -} from "@openzeppelin/contracts/interfaces/draft-IERC7786.sol"; +import {IEntryPointNonces, IPaymaster, IEntryPoint, PackedUserOperation, IAccount, IAccountExecute} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; +import {IERC7786GatewaySource, IERC7786Recipient} from "@openzeppelin/contracts/interfaces/draft-IERC7786.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, @@ -62,28 +53,62 @@ contract DummyBridge is _; } + /// 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]); + } + } + + /** + * @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); + } + /// IAccountExecute::executeUserOp() /// Called in the execution phase of UserOp handling. 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() @@ -169,7 +194,7 @@ 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 @@ -210,8 +235,7 @@ contract DummyBridge is ); 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, diff --git a/zilliqa/src/contracts/uccb/UccbGateway.sol b/zilliqa/src/contracts/uccb/UccbGateway.sol index 65d917bce..4bbf1bcff 100644 --- a/zilliqa/src/contracts/uccb/UccbGateway.sol +++ b/zilliqa/src/contracts/uccb/UccbGateway.sol @@ -66,19 +66,19 @@ contract UccbGateway is * @notice One-time proxy initializer. * * @param admin_ Address granted DEFAULT_ADMIN_ROLE (and all sub-roles). - * @param links Initial chain links (gateway ↔ counterpart pairs). + * @param links_ Initial chain links (gateway ↔ counterpart pairs). * Each entry is a {CrosschainLinkedUpgradeable.Link} struct. */ function initialize( address admin_, - CrosschainLinkedUpgradeable.Link[] memory links + CrosschainLinkedUpgradeable.Link[] memory links_ ) external initializer { assert(admin_ != address(0)); __EIP712_init("UccbGateway", "1"); __AccessControl_init(); __Pausable_init(); - __CrosschainLinked_init(links); + __CrosschainLinked_init(links_); __ERC165_init(); _grantRole(DEFAULT_ADMIN_ROLE, admin_); @@ -113,7 +113,7 @@ contract UccbGateway is function _decode( bytes calldata payload ) internal pure returns (bytes4, bytes memory) { - assert(payload.length >= 32); + assert(payload.length > 32); (uint8 version, bytes4 mType, bytes memory b) = abi.decode( payload[4:], (uint8, bytes4, bytes) @@ -166,7 +166,7 @@ contract UccbGateway is bytes memory chain, bytes memory payload, bytes[] memory attributes - ) internal override returns (bytes32) { + ) internal override(CrosschainLinkedUpgradeable) returns (bytes32) { (, bytes memory counterpart) = getLink(chain); bytes memory originator = InteroperableAddress.formatEvmV1( @@ -174,6 +174,8 @@ contract UccbGateway is address(this) ); + assert(!counterpart.equal(originator)); // prevent loop-back + bytes32 sendId = keccak256(payload); emit MessageSent( diff --git a/zilliqa/src/contracts/uccb/UccbSender.sol b/zilliqa/src/contracts/uccb/UccbSender.sol index 7bffc36ff..712fa68a5 100644 --- a/zilliqa/src/contracts/uccb/UccbSender.sol +++ b/zilliqa/src/contracts/uccb/UccbSender.sol @@ -4,12 +4,7 @@ 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 {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"; @@ -18,6 +13,7 @@ import {ERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/intro 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"; /** @@ -57,15 +53,66 @@ contract UccbSender is _setThreshold(uint64(1)); // one signer will pass } - /// Called by handleOps() + /// ***** External execution ***** + + /** + * @notice Execute multiple calls atomically. + * Any single revert aborts the entire batch. + */ + function executeBatch( + address[] calldata targets, + uint256[] calldata values, + bytes[] calldata datas + ) external onlyEntryPointOrSelf nonReentrant { + uint256 len = targets.length; + require(len == values.length && len == datas.length); + for (uint256 i; i < len; ++i) { + _execute(targets[i], values[i], datas[i]); + } + } + + /** + * @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: - // 1. Determine if it is a CALL or CONFIG - // 2. On CONFIG, update the stakers; and - // 3. update the Paymaster stakers. + // TODO: Update stakers } /// Called by entrypoint diff --git a/zilliqa/src/uccb/mod.rs b/zilliqa/src/uccb/mod.rs index 9b42bc4aa..a2bae7c04 100644 --- a/zilliqa/src/uccb/mod.rs +++ b/zilliqa/src/uccb/mod.rs @@ -51,6 +51,11 @@ sol!( ); sol! { + function executeBatch( + address[] calldata targets, + uint256[] calldata values, + bytes[] calldata datas + ) external; interface IERC7786Attributes { function eip1559_fees(uint128 max_priority_gas_fee,uint128 max_base_gas_fee) external; } diff --git a/zilliqa/src/uccb/signer.rs b/zilliqa/src/uccb/signer.rs index 6c2513790..1206e50d1 100644 --- a/zilliqa/src/uccb/signer.rs +++ b/zilliqa/src/uccb/signer.rs @@ -225,9 +225,9 @@ impl Signer { } 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; @@ -247,13 +247,13 @@ impl Signer { "MessageSent({chain:?}): invalid source" ); // MessageSent comes from source - let Ok(sender) = get_erc7930_address(sender.iter().as_slice()) 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 @@ -272,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) { @@ -284,7 +293,14 @@ 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( + payload.into(), + sender, + paymaster, + gateway, + value, + block_height, + ); tracing::trace!(send_id=%sendId, ?userop, "UserOp"); if let Err(err) = sign_tx.send(SignUserOp::new( userop, @@ -687,15 +703,20 @@ impl Signer { 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; + + // Normal UserOps go to Sender::executeBatch() + let execute = super::executeBatchCall { + targets: vec![*gateway], + values: vec![value], + datas: vec![payload], + }; + let call_data = execute.abi_encode(); AlloyUserOperation { sender: *sender, nonce: U256::ZERO, // unpopulated nonce/sig @@ -703,7 +724,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 @@ -722,7 +743,7 @@ impl Signer { // B256::ZERO, Bytes::new(), &Address::ZERO, - // &Address::ZERO, + &Address::ZERO, &Address::ZERO, U256::ZERO, 0, From 1386c3fd2e906660d7dfb2da72e0dcc5d625e0cc Mon Sep 17 00:00:00 2001 From: Shawn Date: Fri, 19 Jun 2026 16:30:07 +0800 Subject: [PATCH 12/13] consistent send_id --- bundler_tests/DummyBridge.sol | 59 ++++++++--------------- zilliqa/src/contracts/uccb/UccbSender.sol | 18 +++---- zilliqa/src/message.rs | 1 + zilliqa/src/uccb/mod.rs | 18 ++++--- zilliqa/src/uccb/relayer.rs | 31 ++++++------ zilliqa/src/uccb/signer.rs | 33 +++++++------ 6 files changed, 72 insertions(+), 88 deletions(-) diff --git a/bundler_tests/DummyBridge.sol b/bundler_tests/DummyBridge.sol index 048ea932f..8731ba57d 100644 --- a/bundler_tests/DummyBridge.sol +++ b/bundler_tests/DummyBridge.sol @@ -53,57 +53,38 @@ contract DummyBridge is _; } - /// Called in the execution phase of UserOp handling. - function executeBatch( - address[] calldata targets, - uint256[] calldata values, - bytes[] calldata datas + /// Execution calls + function execute( + address target, + uint256 value, + bytes calldata data ) 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]); - } + _execute(target, value, data); } - /** - * @dev Low-level call with revert bubbling. - * Uses Address.functionCallWithValue so reverts propagate correctly - * even when returndata is empty. - */ + /// 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 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); } /// IAccountExecute::executeUserOp() - /// Called in the execution phase of UserOp handling. + /// Configuration calls function executeUserOp( PackedUserOperation calldata userOp, bytes32 _userOpHash diff --git a/zilliqa/src/contracts/uccb/UccbSender.sol b/zilliqa/src/contracts/uccb/UccbSender.sol index 712fa68a5..968149272 100644 --- a/zilliqa/src/contracts/uccb/UccbSender.sol +++ b/zilliqa/src/contracts/uccb/UccbSender.sol @@ -56,19 +56,15 @@ contract UccbSender is /// ***** External execution ***** /** - * @notice Execute multiple calls atomically. - * Any single revert aborts the entire batch. + * @notice Execute a single arbitrary call. + * Called by the EntryPoint after successful validateUserOp. */ - function executeBatch( - address[] calldata targets, - uint256[] calldata values, - bytes[] calldata datas + function execute( + address target, + uint256 value, + bytes calldata data ) external onlyEntryPointOrSelf nonReentrant { - uint256 len = targets.length; - require(len == values.length && len == datas.length); - for (uint256 i; i < len; ++i) { - _execute(targets[i], values[i], datas[i]); - } + _execute(target, value, data); } /** 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/uccb/mod.rs b/zilliqa/src/uccb/mod.rs index a2bae7c04..9e8f9c44e 100644 --- a/zilliqa/src/uccb/mod.rs +++ b/zilliqa/src/uccb/mod.rs @@ -51,10 +51,10 @@ sol!( ); sol! { - function executeBatch( - address[] calldata targets, - uint256[] calldata values, - bytes[] calldata datas + 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; @@ -125,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, @@ -137,6 +139,7 @@ impl SignUserOp { blk_height: u64, ) -> Self { Self { + send_id, userop, dst_chain, src_chain, @@ -166,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, @@ -196,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, } @@ -385,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 1206e50d1..c2f434e62 100644 --- a/zilliqa/src/uccb/signer.rs +++ b/zilliqa/src/uccb/signer.rs @@ -294,6 +294,7 @@ impl Signer { // 7. Construct partial UserOp; send for signing let userop = Self::new_user_op( + sendId, payload.into(), sender, paymaster, @@ -303,6 +304,7 @@ impl Signer { ); tracing::trace!(send_id=%sendId, ?userop, "UserOp"); if let Err(err) = sign_tx.send(SignUserOp::new( + sendId, userop, dst_chain, src_chain, @@ -352,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 { @@ -404,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(()); @@ -413,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(()); @@ -431,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, @@ -463,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. @@ -470,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(()) } @@ -700,6 +702,7 @@ 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, @@ -708,13 +711,13 @@ impl Signer { block_height: u64, ) -> AlloyUserOperation { // we can encode some custom things in here - let paymaster_data = (block_height).abi_encode_packed(); + let paymaster_data = (block_height).abi_encode(); // Normal UserOps go to Sender::executeBatch() - let execute = super::executeBatchCall { - targets: vec![*gateway], - values: vec![value], - datas: vec![payload], + let execute = super::executeCall { + value, + target: *gateway, + data: payload, }; let call_data = execute.abi_encode(); AlloyUserOperation { @@ -734,13 +737,13 @@ 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, From 417392440ee16ba04d14b3d01cd4336dd74b3aec Mon Sep 17 00:00:00 2001 From: Shawn Date: Fri, 19 Jun 2026 16:52:31 +0800 Subject: [PATCH 13/13] chore: formatting. --- bundler_tests/DummyBridge.sol | 14 ++++++++++++-- zilliqa/src/contracts/uccb/UccbSender.sol | 7 ++++++- zilliqa/src/uccb/utils.rs | 3 +-- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/bundler_tests/DummyBridge.sol b/bundler_tests/DummyBridge.sol index 8731ba57d..eafa79061 100644 --- a/bundler_tests/DummyBridge.sol +++ b/bundler_tests/DummyBridge.sol @@ -3,8 +3,18 @@ pragma solidity ^0.8.28; import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol"; import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; -import {IEntryPointNonces, IPaymaster, IEntryPoint, PackedUserOperation, IAccount, IAccountExecute} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; -import {IERC7786GatewaySource, IERC7786Recipient} from "@openzeppelin/contracts/interfaces/draft-IERC7786.sol"; +import { + IEntryPointNonces, + IPaymaster, + IEntryPoint, + PackedUserOperation, + IAccount, + IAccountExecute +} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; +import { + IERC7786GatewaySource, + IERC7786Recipient +} from "@openzeppelin/contracts/interfaces/draft-IERC7786.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"; diff --git a/zilliqa/src/contracts/uccb/UccbSender.sol b/zilliqa/src/contracts/uccb/UccbSender.sol index 968149272..f85809163 100644 --- a/zilliqa/src/contracts/uccb/UccbSender.sol +++ b/zilliqa/src/contracts/uccb/UccbSender.sol @@ -4,7 +4,12 @@ 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 { + 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"; diff --git a/zilliqa/src/uccb/utils.rs b/zilliqa/src/uccb/utils.rs index 98ca9f468..bbd15ce33 100644 --- a/zilliqa/src/uccb/utils.rs +++ b/zilliqa/src/uccb/utils.rs @@ -6,9 +6,8 @@ use alloy::{ use alloy_chains::Chain; use anyhow::Result; -use crate::api::to_hex::ToHex; - use super::PackedUserOperation; +use crate::api::to_hex::ToHex; /// Retrieve the chain from a given CAIP-10 account pub fn get_erc7930_chain(account_id: &[u8]) -> Result {