diff --git a/.changeset/README.md b/.changeset/README.md deleted file mode 100644 index e5b6d8d6..00000000 --- a/.changeset/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# Changesets - -Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works -with multi-package repos, or single-package repos to help you version and publish your code. You can -find the full documentation for it [in our repository](https://github.com/changesets/changesets) - -We have a quick list of common questions to get you started engaging with this project in -[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) diff --git a/.changeset/config.json b/.changeset/config.json deleted file mode 100644 index d88011f6..00000000 --- a/.changeset/config.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json", - "changelog": "@changesets/cli/changelog", - "commit": false, - "fixed": [], - "linked": [], - "access": "restricted", - "baseBranch": "main", - "updateInternalDependencies": "patch", - "ignore": [] -} diff --git a/.env.example b/.env.example index dcfc7f4c..7e85472c 100644 --- a/.env.example +++ b/.env.example @@ -4,7 +4,7 @@ # ============================================ # UPGRADE TESTS # ============================================ -UPGRADE_TEST_RPC_URL=https://base-mainnet.g.alchemy.com/v2/YOUR_API_KEY# Base +UPGRADE_TEST_RPC_URL=https://base-mainnet.g.alchemy.com/v2/YOUR_API_KEY # Base UPGRADE_TEST_OLD_PROXY=0x7C27e3AEcbF42879B64d76f604dC3430F4886462 UPGRADE_TEST_OLD_VERSION=0.5.10 @@ -16,10 +16,10 @@ PRIVATE_KEY= # Format: RPC_{chainId} # Mainnet chains -RPC_1=https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY# Ethereum +RPC_1=https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY # Ethereum # Testnet chains -RPC_11155111=https://eth-sepolia.g.alchemy.com/v2/YOUR_API_KEY# Sepolia +RPC_11155111=https://eth-sepolia.g.alchemy.com/v2/YOUR_API_KEY # Sepolia # Test mnemonic for funding script GAS_SIGNER_MNEMONIC="dash between kangaroo vacant gaze glass way sudden retire output retire evil" diff --git a/.github/workflows/auto-assign.yaml b/.github/workflows/auto-assign.yaml new file mode 100644 index 00000000..0ecd2e5e --- /dev/null +++ b/.github/workflows/auto-assign.yaml @@ -0,0 +1,12 @@ +name: Auto Assign PR to Author + +on: + pull_request: + types: [opened, reopened] + +jobs: + auto-assign: + permissions: + issues: write + pull-requests: write + uses: ithacaxyz/ci/.github/workflows/auto-assign-pr.yml@main diff --git a/.github/workflows/version-check.yaml b/.github/workflows/version-check.yaml index 059fa1e4..f5946cb5 100644 --- a/.github/workflows/version-check.yaml +++ b/.github/workflows/version-check.yaml @@ -57,31 +57,22 @@ jobs: - name: Bump version if needed if: steps.check.outputs.needs_version_bump == 'true' run: | - # Configure git - git config --local user.email "action@github.com" - git config --local user.name "GitHub Action" - # Get the contracts that need bumping CONTRACTS_TO_BUMP="${{ steps.check.outputs.contracts_to_bump }}" - + echo "Bumping versions for contracts: $CONTRACTS_TO_BUMP" - + # Update Solidity files using the upgrade script with specific contracts CONTRACTS_TO_BUMP="$CONTRACTS_TO_BUMP" node prep/update-version.js - - # Commit changes (only Solidity files, not package.json) - git add src/*.sol - git commit -m "chore: bump contract versions due to bytecode changes - Contracts updated: $CONTRACTS_TO_BUMP" - - # Pull latest changes and rebase - # Pull latest changes and rebase - if ! git pull origin ${{ github.event.pull_request.head.ref }} --rebase; then - echo "Failed to rebase version bump changes. Manual intervention required." - exit 1 - fi - - # Push to the PR branch - git push origin HEAD:${{ github.event.pull_request.head.ref }} + + - name: Commit version bump changes + if: steps.check.outputs.needs_version_bump == 'true' + uses: stefanzweifel/git-auto-commit-action@v5 + with: + commit_message: "chore: bump contract versions due to bytecode changes - Contracts updated: ${{ steps.check.outputs.contracts_to_bump }}" + file_pattern: "src/*.sol" + commit_user_name: github-actions[bot] + commit_user_email: github-actions[bot]@users.noreply.github.com - name: Create PR comment if: steps.check.outputs.needs_version_bump == 'true' diff --git a/.gitmodules b/.gitmodules index 7aed494c..28a7bc52 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,9 +7,6 @@ [submodule "lib/LayerZero-v2"] path = lib/LayerZero-v2 url = https://github.com/LayerZero-Labs/LayerZero-v2 -[submodule "lib/devtools"] - path = lib/devtools - url = https://github.com/LayerZero-Labs/devtools [submodule "lib/openzeppelin-contracts"] path = lib/openzeppelin-contracts url = https://github.com/OpenZeppelin/openzeppelin-contracts diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 862d09bd..00000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index dae9f612..2fd21859 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # porto-account +> Note: After v0.5.5, all changelogs will be published along with the release notes. +> From here on, this file is deprecated. + +## 0.5.5 + +### Patch Changes +- Update foundry config, to not include metadata in the bytecode. This ensures that the contract bytecode doesn't change because of some other change in the repository. + + +## 0.5.4 + +### Patch Changes + +- SimpleFunder supports multiple orchestrators instead of single immutable orchestrator + - Replaced immutable `ORCHESTRATOR` with `orchestrators` mapping and `setOrchestrators()` function + - Maintained backward compatibility with old `fund()` signature + - Added `supported_orchestrators` config field for deployment + - Version bumped to "0.1.5" + ## 0.5.0 ### Minor Changes @@ -94,7 +113,7 @@ - All fill related functions removed from EP. - EP is now completely stateless, also does not have a constructor. - PreCall with `nonce = type(uint256).max` is not replayable anymore. -- `OpDataTooShort` error, updated to `OpDataError`, to enforce tighter validation of opdata. +- `OpDataTooShort` error, udpated to `OpDataError`, to enforce tighter validation of opdata. - `checkAndIncrementNonce` function added to account. Can only be called by EP. - 6b3294a: Optimize `_isSuperAdmin` @@ -131,7 +150,7 @@ - Add back the INSUFFICIENT_GAS check, which prevents the relay from setting up the `execute` call on the account, in such a way causing it to intentionally fail. - For the relay, gExecute now has to be set at least as `gExecute > (gCombined + 100_000) * 64/63)` + For the relay, gExecute now has to be set atleast as `gExecute > (gCombined + 100_000) * 64/63)` ### Patch Changes diff --git a/README.md b/README.md index ea941768..aa22490b 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,6 @@ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/ithacaxyz/account) -> 🚧 **Work In Progress** -> This repository is under active development. Contracts are **unaudited**, and the codebase may have **breaking changes** without notice. -> A bug bounty is live on Base Mainnet — [details here](docs/bug-bounty.md). - **All-in-one EIP-7702 powered account contract, coupled with [Porto](https://github.com/ithacaxyz/porto)** Every app needs an account, traditionally requiring separate services for auth, payments, and recovery. Doing this in a way that empowers users with control over their funds and their data is the core challenge of the crypto space. While crypto wallets have made great strides, users still face a fragmented experience - juggling private keys, managing account balances across networks, @@ -23,17 +19,24 @@ We believe that unstoppable crypto-powered accounts should be excellent througho # Features out of the box -- [x] Secure Login: Using WebAuthN-compatible credentials like PassKeys. -- [x] Call Batching: Send multiple calls in 1. -- [x] Gas Sponsorship: Allow anyone to pay for your fees in any ERC20 or ETH. -- [x] Access Control: Whitelist receivers, function selectors and arguments. -- [x] Session Keys: Allow transactions without confirmations if they pass low-security access control policies. -- [x] Multi-sig Support: If a call is outside of a certain access control policy, require multiple signatures. -- [x] Interop: Transaction on any chain invisibly. -- [ ] Timelocks: Add a time delay between transaction verification and execution, for additional safety. -- [ ] Optimized for L2: Using BLS signatures. -- [ ] Privacy: Using stealth addresses, confidential ERC20 tokens, and privacy pool integrations. -- [ ] Account Recovery & Identity: Using ZK {Email, OAUth, Passport} and more. +- Secure Login: Using WebAuthN-compatible credentials like PassKeys. +- Call Batching: Send multiple calls in 1. +- Gas Sponsorship: Allow anyone to pay for your fees in any ERC20 or ETH. +- Access Control: Whitelist receivers, function selectors and arguments. +- Session Keys: Allow transactions without confirmations if they pass low-security access control policies. +- Multi-sig Support: If a call is outside of a certain access control policy, require multiple signatures. +- Interop: Transaction on any chain invisibly. + +## Benchmarks + +![Benchmarks](docs/benchmarks.jpeg) + +Gas benchmark implementations are in the [test repository](test/Benchmark.t.sol). We currently benchmark against leading ERC-4337 accounts. To generate the benchmarks, use `forge snapshot --isolate`. + +## Security +Contracts were audited in a 2 week engagement by @MiloTruck @rholterhus @kadenzipfel + +We also maintain an active bug bounty program, you can find more details about it [here](https://porto.sh/contracts/security-and-bug-bounty) ## Getting Help diff --git a/deploy/ConfigureLayerZeroSettler.s.sol b/deploy/ConfigureLayerZeroSettler.s.sol index 4e505329..63187a5f 100644 --- a/deploy/ConfigureLayerZeroSettler.s.sol +++ b/deploy/ConfigureLayerZeroSettler.s.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.23; import {Script} from "forge-std/Script.sol"; import {console} from "forge-std/Script.sol"; +import {Config} from "forge-std/Config.sol"; import {ILayerZeroEndpointV2} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; import {SetConfigParam} from @@ -36,7 +37,7 @@ import {LayerZeroSettler} from "../src/LayerZeroSettler.sol"; * --private-key $L0_SETTLER_OWNER_PK \ * "[84532,11155420]" */ -contract ConfigureLayerZeroSettler is Script { +contract ConfigureLayerZeroSettler is Script, Config { // Configuration type constants (matching ULN302) uint32 constant CONFIG_TYPE_EXECUTOR = 1; uint32 constant CONFIG_TYPE_ULN = 2; @@ -46,6 +47,7 @@ contract ConfigureLayerZeroSettler is Script { string name; address layerZeroSettlerAddress; address layerZeroEndpoint; + address l0SettlerSigner; uint32 eid; address sendUln302; address receiveUln302; @@ -59,6 +61,7 @@ contract ConfigureLayerZeroSettler is Script { // Fork ids for chain switching mapping(uint256 => uint256) public forkIds; + mapping(uint256 => bool) public isForkInitialized; mapping(uint256 => LayerZeroChainConfig) public chainConfigs; uint256[] public configuredChainIds; @@ -66,8 +69,12 @@ contract ConfigureLayerZeroSettler is Script { * @notice Configure all chains with LayerZero configuration */ function run() external { - // Get all chain IDs from fork configuration - uint256[] memory chainIds = vm.readForkChainIds(); + // Load configuration and setup forks + string memory configPath = string.concat(vm.projectRoot(), "/deploy/config.toml"); + _loadConfigAndForks(configPath, false); + + // Get all chain IDs from configuration + uint256[] memory chainIds = config.getChainIds(); run(chainIds); } @@ -79,12 +86,15 @@ contract ConfigureLayerZeroSettler is Script { console.log("Loading configuration from deploy/config.toml"); console.log("Configuring", chainIds.length, "chains"); + // If config not already loaded, load it + if (address(config) == address(0)) { + string memory configPath = string.concat(vm.projectRoot(), "/deploy/config.toml"); + _loadConfigAndForks(configPath, false); + } + // Load configurations for all chains loadConfigurations(chainIds); - // Create forks for all chains that have LayerZero config - createForks(); - // Configure each chain for (uint256 i = 0; i < configuredChainIds.length; i++) { configureChain(configuredChainIds[i]); @@ -100,27 +110,22 @@ contract ConfigureLayerZeroSettler is Script { for (uint256 i = 0; i < chainIds.length; i++) { uint256 chainId = chainIds[i]; - // Create fork to read configuration - string memory rpcUrl = vm.envString(string.concat("RPC_", vm.toString(chainId))); - uint256 forkId = vm.createFork(rpcUrl); - vm.selectFork(forkId); + // Switch to the fork for this chain (already created by _loadConfigAndForks) + vm.selectFork(forkOf[chainId]); // Try to load LayerZero configuration - LayerZeroChainConfig memory config = loadChainConfig(chainId); - - // Only store if chain has LayerZero configuration - if (config.sendUln302 != address(0)) { - chainConfigs[chainId] = config; + LayerZeroChainConfig memory chainConfig = loadChainConfig(chainId); + + // Only add chains that have LayerZero configuration + if (chainConfig.layerZeroSettlerAddress != address(0)) { + chainConfigs[chainId] = chainConfig; configuredChainIds.push(chainId); - forkIds[chainId] = forkId; + forkIds[chainId] = forkOf[chainId]; + isForkInitialized[forkOf[chainId]] = true; console.log( string.concat( - " Loaded LayerZero config for ", - config.name, - " (", - vm.toString(chainId), - ")" + " Loaded LayerZero config for ", chainConfig.name, " (", vm.toString(chainId), ")" ) ); } @@ -130,62 +135,68 @@ contract ConfigureLayerZeroSettler is Script { } /** - * @notice Load configuration for a single chain from fork variables + * @notice Load configuration for a single chain using StdConfig */ function loadChainConfig(uint256 chainId) internal view - returns (LayerZeroChainConfig memory config) + returns (LayerZeroChainConfig memory chainConfig) { - config.chainId = chainId; + chainConfig.chainId = chainId; // Load basic chain info - required - config.name = vm.readForkString("name"); + chainConfig.name = config.get(chainId, "name").toString(); - // Try to load LayerZero settler address - if not present, this chain doesn't have LayerZero config - try vm.readForkAddress("layerzero_settler_address") returns (address addr) { - config.layerZeroSettlerAddress = addr; - } catch { + // Try to load LayerZero settler deployed address first, then fall back to expected address + address settlerAddr = config.get(chainId, "layerzero_settler_deployed").toAddress(); + if (settlerAddr == address(0)) { + // Fall back to expected address from config + settlerAddr = config.get(chainId, "layerzero_settler_address").toAddress(); + } + if (settlerAddr == address(0)) { // No LayerZero settler configured for this chain - this is ok, return empty config - return config; + return chainConfig; } + chainConfig.layerZeroSettlerAddress = settlerAddr; // If we have a LayerZero settler, all other LayerZero fields are required - config.layerZeroEndpoint = vm.readForkAddress("layerzero_endpoint"); - config.eid = uint32(vm.readForkUint("layerzero_eid")); - config.sendUln302 = vm.readForkAddress("layerzero_send_uln302"); - config.receiveUln302 = vm.readForkAddress("layerzero_receive_uln302"); + chainConfig.layerZeroEndpoint = config.get(chainId, "layerzero_endpoint").toAddress(); + chainConfig.l0SettlerSigner = config.get(chainId, "l0_settler_signer").toAddress(); + chainConfig.eid = uint32(config.get(chainId, "layerzero_eid").toUint256()); + chainConfig.sendUln302 = config.get(chainId, "layerzero_send_uln302").toAddress(); + chainConfig.receiveUln302 = config.get(chainId, "layerzero_receive_uln302").toAddress(); // Load destination chain IDs - required for LayerZero configuration - config.destinationChainIds = vm.readForkUintArray("layerzero_destination_chain_ids"); + chainConfig.destinationChainIds = config.get(chainId, "layerzero_destination_chain_ids").toUint256Array(); // Load DVN configuration - required and optional DVN arrays - string[] memory requiredDVNNames = vm.readForkStringArray("layerzero_required_dvns"); - string[] memory optionalDVNNames = vm.readForkStringArray("layerzero_optional_dvns"); + string[] memory requiredDVNNames = config.get(chainId, "layerzero_required_dvns").toStringArray(); + string[] memory optionalDVNNames = config.get(chainId, "layerzero_optional_dvns").toStringArray(); // Resolve DVN names to addresses - config.requiredDVNs = resolveDVNAddresses(requiredDVNNames); - config.optionalDVNs = resolveDVNAddresses(optionalDVNNames); + chainConfig.requiredDVNs = resolveDVNAddresses(chainId, requiredDVNNames); + chainConfig.optionalDVNs = resolveDVNAddresses(chainId, optionalDVNNames); // Load optional DVN threshold - required field - config.optionalDVNThreshold = uint8(vm.readForkUint("layerzero_optional_dvn_threshold")); + chainConfig.optionalDVNThreshold = uint8(config.get(chainId, "layerzero_optional_dvn_threshold").toUint256()); // Load confirmations - required field - config.confirmations = uint64(vm.readForkUint("layerzero_confirmations")); + chainConfig.confirmations = uint64(config.get(chainId, "layerzero_confirmations").toUint256()); // Load max message size - required field - config.maxMessageSize = uint32(vm.readForkUint("layerzero_max_message_size")); + chainConfig.maxMessageSize = uint32(config.get(chainId, "layerzero_max_message_size").toUint256()); - return config; + return chainConfig; } /** - * @notice Resolve DVN names to addresses by reading from fork variables - * @dev Takes DVN variable names and looks up their addresses using vm.readForkAddress + * @notice Resolve DVN names to addresses using StdConfig + * @dev Takes DVN variable names and looks up their addresses using config.get + * @param chainId The chain ID to resolve DVN addresses for * @param dvnNames Array of DVN variable names from config (e.g., "dvn_layerzero_labs") * @return addresses Array of resolved DVN addresses */ - function resolveDVNAddresses(string[] memory dvnNames) + function resolveDVNAddresses(uint256 chainId, string[] memory dvnNames) internal view returns (address[] memory) @@ -193,7 +204,7 @@ contract ConfigureLayerZeroSettler is Script { address[] memory addresses = new address[](dvnNames.length); for (uint256 i = 0; i < dvnNames.length; i++) { - addresses[i] = vm.readForkAddress(dvnNames[i]); + addresses[i] = config.get(chainId, dvnNames[i]).toAddress(); require( addresses[i] != address(0), string.concat("DVN address not configured for: ", dvnNames[i]) @@ -203,18 +214,8 @@ contract ConfigureLayerZeroSettler is Script { return addresses; } - /** - * @notice Create forks for all configured chains - */ - function createForks() internal { - console.log("\n=== Creating forks for configured chains ==="); - - for (uint256 i = 0; i < configuredChainIds.length; i++) { - uint256 chainId = configuredChainIds[i]; - LayerZeroChainConfig memory config = chainConfigs[chainId]; - - console.log(" Fork created for", config.name, "fork ID:", forkIds[chainId]); - } + function _selectFork(uint256 chainId) internal { + vm.selectFork(forkOf[chainId]); } /** @@ -227,31 +228,59 @@ contract ConfigureLayerZeroSettler is Script { console.log(string.concat("Configuring ", config.name, " (", vm.toString(chainId), ")")); console.log(" LayerZero Settler:", config.layerZeroSettlerAddress); console.log(" Endpoint:", config.layerZeroEndpoint); + console.log(" L0SettlerSigner:", config.l0SettlerSigner); console.log(" EID:", config.eid); // Switch to the source chain - vm.selectFork(forkIds[chainId]); + _selectFork(chainId); LayerZeroSettler settler = LayerZeroSettler(payable(config.layerZeroSettlerAddress)); + + // Set or update the endpoint on the settler + address currentEndpoint = address(settler.endpoint()); + if (currentEndpoint != config.layerZeroEndpoint) { + if (currentEndpoint == address(0)) { + console.log(" Setting endpoint to:", config.layerZeroEndpoint); + } else { + console.log(" Updating endpoint from:", currentEndpoint); + console.log(" To:", config.layerZeroEndpoint); + } + vm.broadcast(); + settler.setEndpoint(config.layerZeroEndpoint); + console.log(" Endpoint configured successfully"); + } else { + console.log(" Endpoint already set to:", config.layerZeroEndpoint); + } + + // Set or update the L0SettlerSigner on the settler + address currentSigner = settler.l0SettlerSigner(); + if (currentSigner != config.l0SettlerSigner) { + if (currentSigner == address(0)) { + console.log(" Setting L0SettlerSigner to:", config.l0SettlerSigner); + } else { + console.log(" Updating L0SettlerSigner from:", currentSigner); + console.log(" To:", config.l0SettlerSigner); + } + vm.broadcast(); + settler.setL0SettlerSigner(config.l0SettlerSigner); + console.log(" L0SettlerSigner configured successfully"); + } else { + console.log(" L0SettlerSigner already set to:", config.l0SettlerSigner); + } + ILayerZeroEndpointV2 endpoint = ILayerZeroEndpointV2(config.layerZeroEndpoint); // Configure pathways to all destinations for (uint256 i = 0; i < config.destinationChainIds.length; i++) { uint256 destChainId = config.destinationChainIds[i]; - // Skip if destination not configured - if (forkIds[destChainId] == 0) { - console.log(" Skipping unconfigured destination:", destChainId); - continue; - } - LayerZeroChainConfig memory destConfig = chainConfigs[destChainId]; console.log(string.concat("\n Configuring pathway to ", destConfig.name)); console.log(" Destination EID:", destConfig.eid); // Set executor config (self-execution model) - setExecutorConfig(settler, endpoint, destConfig.eid); + setExecutorConfig(config, settler, endpoint, destConfig.eid); // Set send ULN config setSendUlnConfig( @@ -266,11 +295,31 @@ contract ConfigureLayerZeroSettler is Script { ); // Switch to destination chain to set receive config - vm.selectFork(forkIds[destChainId]); + _selectFork(destChainId); + + // Ensure destination settler has endpoint set before configuring + LayerZeroSettler destSettler = + LayerZeroSettler(payable(destConfig.layerZeroSettlerAddress)); + address currentDestEndpoint = address(destSettler.endpoint()); + if (currentDestEndpoint != destConfig.layerZeroEndpoint) { + if (currentDestEndpoint == address(0)) { + console.log( + " Setting endpoint on destination:", destConfig.layerZeroEndpoint + ); + } else { + console.log(" Updating endpoint on destination from:", currentDestEndpoint); + console.log(" To:", destConfig.layerZeroEndpoint); + } + vm.broadcast(); + destSettler.setEndpoint(destConfig.layerZeroEndpoint); + console.log(" Destination endpoint configured successfully"); + } else { + console.log(" Destination endpoint already set:", destConfig.layerZeroEndpoint); + } // Set receive ULN config on destination setReceiveUlnConfig( - LayerZeroSettler(payable(destConfig.layerZeroSettlerAddress)), + destSettler, ILayerZeroEndpointV2(destConfig.layerZeroEndpoint), config.eid, // Source EID destConfig.receiveUln302, @@ -281,7 +330,7 @@ contract ConfigureLayerZeroSettler is Script { ); // Switch back to source chain - vm.selectFork(forkIds[chainId]); + _selectFork(chainId); } console.log("\n Configuration complete for", config.name); @@ -292,12 +341,24 @@ contract ConfigureLayerZeroSettler is Script { // ============================================ function setExecutorConfig( + LayerZeroChainConfig memory config, LayerZeroSettler settler, ILayerZeroEndpointV2 endpoint, uint32 destEid ) internal { - // LayerZeroSettler uses self-execution model, no executor config needed - console.log(" Using self-execution model (no executor config)"); + console.log(" Setting executor config:"); + console.log(" Executor (self-execution):", address(settler)); + console.log(" Max message size:", config.maxMessageSize); + console.log(" Send ULN302:", config.sendUln302); + + bytes memory executorConfig = abi.encode(config.maxMessageSize, settler); + SetConfigParam[] memory params = new SetConfigParam[](1); + params[0] = + SetConfigParam({eid: destEid, configType: CONFIG_TYPE_EXECUTOR, config: executorConfig}); + + vm.broadcast(); + endpoint.setConfig(address(settler), config.sendUln302, params); + console.log(" Executor config set"); } function setSendUlnConfig( @@ -337,7 +398,7 @@ contract ConfigureLayerZeroSettler is Script { }); // Get the L0 settler owner who should be the delegate - address l0SettlerOwner = vm.readForkAddress("l0_settler_owner"); + address l0SettlerOwner = config.get(vm.getChainId(), "l0_settler_owner").toAddress(); console.log(" L0 Settler Owner (delegate):", l0SettlerOwner); vm.broadcast(); diff --git a/deploy/DeployMain.s.sol b/deploy/DeployMain.s.sol index 5ebbb109..ee513799 100644 --- a/deploy/DeployMain.s.sol +++ b/deploy/DeployMain.s.sol @@ -3,7 +3,8 @@ pragma solidity ^0.8.23; import {Script, console} from "forge-std/Script.sol"; import {VmSafe} from "forge-std/Vm.sol"; -import {stdToml} from "forge-std/StdToml.sol"; +import {Config} from "forge-std/Config.sol"; +import {Variable, TypeKind} from "forge-std/LibVariable.sol"; import {SafeSingletonDeployer} from "./SafeSingletonDeployer.sol"; // Import contracts to deploy @@ -15,6 +16,7 @@ import {SimpleFunder} from "../src/SimpleFunder.sol"; import {Escrow} from "../src/Escrow.sol"; import {SimpleSettler} from "../src/SimpleSettler.sol"; import {LayerZeroSettler} from "../src/LayerZeroSettler.sol"; +import {ExperimentERC20} from "./mock/ExperimentalERC20.sol"; /** * @title DeployMain @@ -49,23 +51,26 @@ import {LayerZeroSettler} from "../src/LayerZeroSettler.sol"; * --private-key $PRIVATE_KEY \ * "[1]" "/deploy/custom-config.toml" */ -contract DeployMain is Script, SafeSingletonDeployer { - using stdToml for string; +contract DeployMain is Script, Config, SafeSingletonDeployer { // Chain configuration struct struct ChainConfig { uint256 chainId; string name; bool isTestnet; - address pauseAuthority; address funderOwner; address funderSigner; address settlerOwner; address l0SettlerOwner; + address l0SettlerSigner; address layerZeroEndpoint; + address[] oldOrchestrators; uint32 layerZeroEid; bytes32 salt; string[] contracts; // Array of contract names to deploy + // EXP Token configuration (testnet only) + address expMinterAddress; + uint256 expMintAmount; } struct DeployedContracts { @@ -77,6 +82,8 @@ contract DeployMain is Script, SafeSingletonDeployer { address layerZeroSettler; address simpleFunder; address simulator; + address expToken; // EXP token (testnet only) + address exp2Token; // EXP2 token (testnet only) } // State @@ -84,9 +91,7 @@ contract DeployMain is Script, SafeSingletonDeployer { mapping(uint256 => DeployedContracts) internal deployedContracts; uint256[] internal targetChainIds; - // Paths and config - string internal registryPath; - string internal configContent; // For unified config + // Config path string internal configPath = "/deploy/config.toml"; // Events for tracking @@ -104,9 +109,17 @@ contract DeployMain is Script, SafeSingletonDeployer { * @notice Deploy to all chains in config */ function run() external { - // Get all available chain IDs from fork configuration - uint256[] memory chainIds = vm.readForkChainIds(); - initializeDeployment(chainIds); + // Load configuration and setup forks (enable write-back to save deployed addresses) + string memory fullConfigPath = string.concat(vm.projectRoot(), configPath); + _loadConfigAndForks(fullConfigPath, true); + + // Get all available chain IDs from configuration + targetChainIds = config.getChainIds(); + require(targetChainIds.length > 0, "No chains found in configuration"); + + // Load configuration for each chain + loadConfigurations(); + loadDeployedContracts(); executeDeployment(); } @@ -115,11 +128,20 @@ contract DeployMain is Script, SafeSingletonDeployer { * @param chainIds Array of chain IDs to deploy to (empty array = all chains) */ function run(uint256[] memory chainIds) external { + // Load configuration and setup forks (enable write-back to save deployed addresses) + string memory fullConfigPath = string.concat(vm.projectRoot(), configPath); + _loadConfigAndForks(fullConfigPath, true); + // If empty array, get all available chains if (chainIds.length == 0) { - chainIds = vm.readForkChainIds(); + chainIds = config.getChainIds(); } - initializeDeployment(chainIds); + targetChainIds = chainIds; + require(targetChainIds.length > 0, "No chains found in configuration"); + + // Load configuration for each chain + loadConfigurations(); + loadDeployedContracts(); executeDeployment(); } @@ -129,47 +151,25 @@ contract DeployMain is Script, SafeSingletonDeployer { * @param _configPath Path to custom TOML config file */ function run(uint256[] memory chainIds, string memory _configPath) external { + configPath = _configPath; + + // Load configuration and setup forks (enable write-back to save deployed addresses) + string memory fullConfigPath = string.concat(vm.projectRoot(), configPath); + _loadConfigAndForks(fullConfigPath, true); + // If empty array, get all available chains if (chainIds.length == 0) { - chainIds = vm.readForkChainIds(); + chainIds = config.getChainIds(); } - initializeDeployment(chainIds, _configPath); - executeDeployment(); - } - - /** - * @notice Initialize deployment with target chains using TOML config - * @param chainIds Array of chain IDs to deploy to - */ - function initializeDeployment(uint256[] memory chainIds) internal { - require(chainIds.length > 0, "No chains found in configuration"); - - // Load unified configuration - string memory fullConfigPath = string.concat(vm.projectRoot(), configPath); - configContent = vm.readFile(fullConfigPath); - - // Load registry path from config.toml - registryPath = configContent.readString(".profile.deployment.registry_path"); - - // Store target chain IDs targetChainIds = chainIds; - + require(targetChainIds.length > 0, "No chains found in configuration"); + // Load configuration for each chain loadConfigurations(); - - // Load existing deployed contracts from registry loadDeployedContracts(); + executeDeployment(); } - /** - * @notice Initialize deployment with custom config path - * @param chainIds Array of chain IDs to deploy to - * @param _configPath Path to the config file - */ - function initializeDeployment(uint256[] memory chainIds, string memory _configPath) internal { - configPath = _configPath; - initializeDeployment(chainIds); - } /** * @notice Load configurations for all target chains @@ -178,20 +178,15 @@ contract DeployMain is Script, SafeSingletonDeployer { for (uint256 i = 0; i < targetChainIds.length; i++) { uint256 chainId = targetChainIds[i]; - // Use the RPC_{chainId} environment variable directly - // This matches the naming convention in config.toml - string memory rpcUrl = vm.envString(string.concat("RPC_", vm.toString(chainId))); - - // Create fork using the RPC URL - uint256 forkId = vm.createFork(rpcUrl); - vm.selectFork(forkId); + // Switch to the fork for this chain (already created by _loadConfigAndForks) + vm.selectFork(forkOf[chainId]); // Verify we're on the correct chain require(block.chainid == chainId, "Chain ID mismatch"); - // Load configuration from fork variables - ChainConfig memory config = loadChainConfigFromFork(chainId); - chainConfigs[chainId] = config; + // Load configuration using new StdConfig pattern + ChainConfig memory chainConfig = loadChainConfigFromStdConfig(chainId); + chainConfigs[chainId] = chainConfig; } // Log the loaded configuration for verification @@ -199,48 +194,66 @@ contract DeployMain is Script, SafeSingletonDeployer { } /** - * @notice Load chain configuration from the currently active fork + * @notice Load chain configuration using StdConfig * @param chainId The chain ID we're loading config for */ - function loadChainConfigFromFork(uint256 chainId) internal view returns (ChainConfig memory) { - ChainConfig memory config; + function loadChainConfigFromStdConfig(uint256 chainId) internal view returns (ChainConfig memory) { + ChainConfig memory chainConfig; - config.chainId = chainId; + chainConfig.chainId = chainId; - // Use vm.readFork* functions to read variables from the active fork - config.name = vm.readForkString("name"); - config.isTestnet = vm.readForkBool("is_testnet"); + // Use StdConfig to read variables + chainConfig.name = config.get(chainId, "name").toString(); + chainConfig.isTestnet = config.get(chainId, "is_testnet").toBool(); // Load addresses - config.pauseAuthority = vm.readForkAddress("pause_authority"); - config.funderOwner = vm.readForkAddress("funder_owner"); - config.funderSigner = vm.readForkAddress("funder_signer"); - config.settlerOwner = vm.readForkAddress("settler_owner"); - config.l0SettlerOwner = vm.readForkAddress("l0_settler_owner"); - config.layerZeroEndpoint = vm.readForkAddress("layerzero_endpoint"); + chainConfig.funderOwner = config.get(chainId, "funder_owner").toAddress(); + chainConfig.funderSigner = config.get(chainId, "funder_signer").toAddress(); + chainConfig.settlerOwner = config.get(chainId, "settler_owner").toAddress(); + chainConfig.l0SettlerOwner = config.get(chainId, "l0_settler_owner").toAddress(); + chainConfig.l0SettlerSigner = config.get(chainId, "l0_settler_signer").toAddress(); + chainConfig.layerZeroEndpoint = config.get(chainId, "layerzero_endpoint").toAddress(); // Load other configuration - config.layerZeroEid = uint32(vm.readForkUint("layerzero_eid")); - config.salt = vm.readForkBytes32("salt"); + chainConfig.layerZeroEid = uint32(config.get(chainId, "layerzero_eid").toUint256()); + chainConfig.salt = config.get(chainId, "salt").toBytes32(); + + // Load EXP token configuration (testnet only) + if (chainConfig.isTestnet) { + chainConfig.expMinterAddress = config.get(chainId, "exp_minter_address").toAddress(); + chainConfig.expMintAmount = config.get(chainId, "exp_mint_amount").toUint256(); + } // Load contracts list - required field, will revert if not present - string[] memory contractsList = vm.readForkStringArray("contracts"); + string[] memory contractsList = config.get(chainId, "contracts").toStringArray(); // Check if user specified "ALL" to deploy all contracts if ( contractsList.length == 1 && keccak256(bytes(contractsList[0])) == keccak256(bytes("ALL")) ) { - config.contracts = getAllContracts(); + string[] memory baseContracts = getAllContracts(); + // For testnets with ALL specified, append ExpToken + if (chainConfig.isTestnet) { + string[] memory testnetContracts = new string[](baseContracts.length + 1); + for (uint256 i = 0; i < baseContracts.length; i++) { + testnetContracts[i] = baseContracts[i]; + } + testnetContracts[baseContracts.length] = "ExpToken"; + chainConfig.contracts = testnetContracts; + } else { + // For non-testnets, use base contracts (no ExpToken) + chainConfig.contracts = baseContracts; + } } else { - config.contracts = contractsList; + chainConfig.contracts = contractsList; } - return config; + return chainConfig; } /** - * @notice Get all available contracts + * @notice Get all available contracts (excluding ExpToken) */ function getAllContracts() internal pure returns (string[] memory) { string[] memory contracts = new string[](8); @@ -273,31 +286,40 @@ contract DeployMain is Script, SafeSingletonDeployer { } /** - * @notice Load deployed contracts from registry + * @notice Load deployed contracts from config */ function loadDeployedContracts() internal { for (uint256 i = 0; i < targetChainIds.length; i++) { uint256 chainId = targetChainIds[i]; - bytes32 salt = chainConfigs[chainId].salt; - string memory registryFile = getRegistryFilename(chainId, salt); - - try vm.readFile(registryFile) returns (string memory registryJson) { - // Use individual parsing for flexibility with missing fields - DeployedContracts memory deployed; - deployed.ithacaAccount = tryReadAddress(registryJson, ".IthacaAccount"); - deployed.accountProxy = tryReadAddress(registryJson, ".AccountProxy"); - deployed.escrow = tryReadAddress(registryJson, ".Escrow"); - deployed.orchestrator = tryReadAddress(registryJson, ".Orchestrator"); - deployed.simpleSettler = tryReadAddress(registryJson, ".SimpleSettler"); - deployed.layerZeroSettler = tryReadAddress(registryJson, ".LayerZeroSettler"); - deployed.simpleFunder = tryReadAddress(registryJson, ".SimpleFunder"); - deployed.simulator = tryReadAddress(registryJson, ".Simulator"); - - deployedContracts[chainId] = deployed; - } catch { - // No registry file exists yet - } + + DeployedContracts memory deployed; + + // Read deployed contract addresses from config, defaulting to address(0) if not set + deployed.orchestrator = tryGetAddress(chainId, "orchestrator_deployed"); + deployed.ithacaAccount = tryGetAddress(chainId, "ithaca_account_deployed"); + deployed.accountProxy = tryGetAddress(chainId, "account_proxy_deployed"); + deployed.escrow = tryGetAddress(chainId, "escrow_deployed"); + deployed.simpleSettler = tryGetAddress(chainId, "simple_settler_deployed"); + deployed.layerZeroSettler = tryGetAddress(chainId, "layerzero_settler_deployed"); + deployed.simpleFunder = tryGetAddress(chainId, "simple_funder_deployed"); + deployed.simulator = tryGetAddress(chainId, "simulator_deployed"); + deployed.expToken = tryGetAddress(chainId, "exp_token_deployed"); + deployed.exp2Token = tryGetAddress(chainId, "exp2_token_deployed"); + + deployedContracts[chainId] = deployed; + } + } + + /** + * @notice Try to get an address from config, return address(0) if not found + */ + function tryGetAddress(uint256 chainId, string memory key) internal view returns (address) { + Variable memory variable = config.get(chainId, key); + // Check if variable exists (TypeKind.None means missing key) + if (variable.ty.kind == TypeKind.None) { + return address(0); } + return variable.toAddress(); } /** @@ -315,8 +337,8 @@ contract DeployMain is Script, SafeSingletonDeployer { console.log("Funder Owner:", config.funderOwner); console.log("Funder Signer:", config.funderSigner); console.log("L0 Settler Owner:", config.l0SettlerOwner); + console.log("L0 Settler Signer:", config.l0SettlerSigner); console.log("Settler Owner:", config.settlerOwner); - console.log("Pause Authority:", config.pauseAuthority); console.log("LayerZero Endpoint:", config.layerZeroEndpoint); console.log("LayerZero EID:", config.layerZeroEid); console.log("Salt:"); @@ -423,7 +445,7 @@ contract DeployMain is Script, SafeSingletonDeployer { } /** - * @notice Save deployed contract address to registry + * @notice Save deployed contract address to config */ function saveDeployedContract( uint256 chainId, @@ -447,124 +469,38 @@ contract DeployMain is Script, SafeSingletonDeployer { deployedContracts[chainId].simpleSettler = contractAddress; } else if (keccak256(bytes(contractName)) == keccak256("LayerZeroSettler")) { deployedContracts[chainId].layerZeroSettler = contractAddress; + } else if (keccak256(bytes(contractName)) == keccak256("ExpToken")) { + deployedContracts[chainId].expToken = contractAddress; + } else if (keccak256(bytes(contractName)) == keccak256("Exp2Token")) { + deployedContracts[chainId].exp2Token = contractAddress; + } + + // Only write to config file during actual broadcasts, not simulations + if (vm.isContext(VmSafe.ForgeContext.ScriptBroadcast) || vm.isContext(VmSafe.ForgeContext.ScriptResume)) { + if (keccak256(bytes(contractName)) == keccak256("Orchestrator")) { + config.set(chainId, "orchestrator_deployed", contractAddress); + } else if (keccak256(bytes(contractName)) == keccak256("IthacaAccount")) { + config.set(chainId, "ithaca_account_deployed", contractAddress); + } else if (keccak256(bytes(contractName)) == keccak256("AccountProxy")) { + config.set(chainId, "account_proxy_deployed", contractAddress); + } else if (keccak256(bytes(contractName)) == keccak256("Simulator")) { + config.set(chainId, "simulator_deployed", contractAddress); + } else if (keccak256(bytes(contractName)) == keccak256("SimpleFunder")) { + config.set(chainId, "simple_funder_deployed", contractAddress); + } else if (keccak256(bytes(contractName)) == keccak256("Escrow")) { + config.set(chainId, "escrow_deployed", contractAddress); + } else if (keccak256(bytes(contractName)) == keccak256("SimpleSettler")) { + config.set(chainId, "simple_settler_deployed", contractAddress); + } else if (keccak256(bytes(contractName)) == keccak256("LayerZeroSettler")) { + config.set(chainId, "layerzero_settler_deployed", contractAddress); + } else if (keccak256(bytes(contractName)) == keccak256("ExpToken")) { + config.set(chainId, "exp_token_deployed", contractAddress); + } else if (keccak256(bytes(contractName)) == keccak256("Exp2Token")) { + config.set(chainId, "exp2_token_deployed", contractAddress); + } } - - // Write to registry file - writeToRegistry(chainId, contractName, contractAddress); - } - - /** - * @notice Write to registry file - */ - function writeToRegistry(uint256 chainId, string memory contractName, address contractAddress) - internal - { - // Only save registry during actual broadcasts, not dry runs - if ( - !vm.isContext(VmSafe.ForgeContext.ScriptBroadcast) - && !vm.isContext(VmSafe.ForgeContext.ScriptResume) - ) { - return; - } - - DeployedContracts memory deployed = deployedContracts[chainId]; - - string memory json = "{"; - bool first = true; - - if (deployed.orchestrator != address(0)) { - json = string.concat(json, '"Orchestrator": "', vm.toString(deployed.orchestrator), '"'); - first = false; - } - - if (deployed.ithacaAccount != address(0)) { - if (!first) json = string.concat(json, ","); - json = - string.concat(json, '"IthacaAccount": "', vm.toString(deployed.ithacaAccount), '"'); - first = false; - } - - if (deployed.accountProxy != address(0)) { - if (!first) json = string.concat(json, ","); - json = string.concat(json, '"AccountProxy": "', vm.toString(deployed.accountProxy), '"'); - first = false; - } - - if (deployed.simulator != address(0)) { - if (!first) json = string.concat(json, ","); - json = string.concat(json, '"Simulator": "', vm.toString(deployed.simulator), '"'); - first = false; - } - - if (deployed.simpleFunder != address(0)) { - if (!first) json = string.concat(json, ","); - json = string.concat(json, '"SimpleFunder": "', vm.toString(deployed.simpleFunder), '"'); - first = false; - } - - if (deployed.escrow != address(0)) { - if (!first) json = string.concat(json, ","); - json = string.concat(json, '"Escrow": "', vm.toString(deployed.escrow), '"'); - first = false; - } - - if (deployed.simpleSettler != address(0)) { - if (!first) json = string.concat(json, ","); - json = - string.concat(json, '"SimpleSettler": "', vm.toString(deployed.simpleSettler), '"'); - first = false; - } - - if (deployed.layerZeroSettler != address(0)) { - if (!first) json = string.concat(json, ","); - json = string.concat( - json, '"LayerZeroSettler": "', vm.toString(deployed.layerZeroSettler), '"' - ); - } - - json = string.concat(json, "}"); - - bytes32 salt = chainConfigs[chainId].salt; - string memory registryFile = getRegistryFilename(chainId, salt); - vm.writeFile(registryFile, json); - } - - /** - * @notice Get registry filename based on chainId and salt - */ - function getRegistryFilename(uint256 chainId, bytes32 salt) - internal - view - returns (string memory) - { - string memory filename = string.concat( - vm.projectRoot(), - "/", - registryPath, - "deployment_", - vm.toString(chainId), - "_", - vm.toString(salt), - ".json" - ); - return filename; } - /** - * @notice Try to read an address from JSON - */ - function tryReadAddress(string memory json, string memory key) - internal - pure - returns (address) - { - try vm.parseJson(json, key) returns (bytes memory data) { - if (data.length > 0) { - return abi.decode(data, (address)); - } - } catch {} - return address(0); - } /** * @notice Verify Safe Singleton Factory is deployed @@ -663,6 +599,8 @@ contract DeployMain is Script, SafeSingletonDeployer { deploySimpleSettler(chainId, config, deployed); } else if (nameHash == keccak256("LayerZeroSettler")) { deployLayerZeroSettler(chainId, config, deployed); + } else if (nameHash == keccak256("ExpToken")) { + deployExpToken(chainId, config, deployed); } else { console.log("Warning: Unknown contract name:", contractName); } @@ -673,17 +611,12 @@ contract DeployMain is Script, SafeSingletonDeployer { ChainConfig memory config, DeployedContracts memory deployed ) internal { - if (deployed.orchestrator == address(0)) { - bytes memory creationCode = type(Orchestrator).creationCode; - bytes memory args = abi.encode(config.pauseAuthority); - address orchestrator = - deployContractWithCreate2(chainId, creationCode, args, "Orchestrator"); - - saveDeployedContract(chainId, "Orchestrator", orchestrator); - deployed.orchestrator = orchestrator; - } else { - console.log("Orchestrator already deployed:", deployed.orchestrator); - } + bytes memory creationCode = type(Orchestrator).creationCode; + address orchestrator = + deployContractWithCreate2(chainId, creationCode, "", "Orchestrator"); + + saveDeployedContract(chainId, "Orchestrator", orchestrator); + deployed.orchestrator = orchestrator; } function deployIthacaAccount( @@ -692,22 +625,15 @@ contract DeployMain is Script, SafeSingletonDeployer { DeployedContracts memory deployed ) internal { // Ensure Orchestrator is deployed first (dependency) - if (deployed.orchestrator == address(0)) { - console.log("Deploying Orchestrator first (dependency for IthacaAccount)..."); - deployOrchestrator(chainId, config, deployed); - } + require(deployed.orchestrator != address(0), "Orchestrator must be deployed before IthacaAccount"); - if (deployed.ithacaAccount == address(0)) { - bytes memory creationCode = type(IthacaAccount).creationCode; - bytes memory args = abi.encode(deployed.orchestrator); - address ithacaAccount = - deployContractWithCreate2(chainId, creationCode, args, "IthacaAccount"); + bytes memory creationCode = type(IthacaAccount).creationCode; + bytes memory args = abi.encode(deployed.orchestrator); + address ithacaAccount = + deployContractWithCreate2(chainId, creationCode, args, "IthacaAccount"); - saveDeployedContract(chainId, "IthacaAccount", ithacaAccount); - deployed.ithacaAccount = ithacaAccount; - } else { - console.log("IthacaAccount already deployed:", deployed.ithacaAccount); - } + saveDeployedContract(chainId, "IthacaAccount", ithacaAccount); + deployed.ithacaAccount = ithacaAccount; } function deployAccountProxy( @@ -716,21 +642,14 @@ contract DeployMain is Script, SafeSingletonDeployer { DeployedContracts memory deployed ) internal { // Ensure IthacaAccount is deployed first (dependency) - if (deployed.ithacaAccount == address(0)) { - console.log("Deploying IthacaAccount first (dependency for AccountProxy)..."); - deployIthacaAccount(chainId, config, deployed); - } + require(deployed.ithacaAccount != address(0), "IthacaAccount must be deployed before AccountProxy"); - if (deployed.accountProxy == address(0)) { - bytes memory proxyCode = LibEIP7702.proxyInitCode(deployed.ithacaAccount, address(0)); - address accountProxy = deployContractWithCreate2(chainId, proxyCode, "", "AccountProxy"); + bytes memory proxyCode = LibEIP7702.proxyInitCode(deployed.ithacaAccount, address(0)); + address accountProxy = deployContractWithCreate2(chainId, proxyCode, "", "AccountProxy"); - require(accountProxy != address(0), "Account proxy deployment failed"); - saveDeployedContract(chainId, "AccountProxy", accountProxy); - deployed.accountProxy = accountProxy; - } else { - console.log("AccountProxy already deployed:", deployed.accountProxy); - } + require(accountProxy != address(0), "Account proxy deployment failed"); + saveDeployedContract(chainId, "AccountProxy", accountProxy); + deployed.accountProxy = accountProxy; } function deploySimulator( @@ -738,15 +657,11 @@ contract DeployMain is Script, SafeSingletonDeployer { ChainConfig memory config, DeployedContracts memory deployed ) internal { - if (deployed.simulator == address(0)) { - bytes memory creationCode = type(Simulator).creationCode; - address simulator = deployContractWithCreate2(chainId, creationCode, "", "Simulator"); + bytes memory creationCode = type(Simulator).creationCode; + address simulator = deployContractWithCreate2(chainId, creationCode, "", "Simulator"); - saveDeployedContract(chainId, "Simulator", simulator); - deployed.simulator = simulator; - } else { - console.log("Simulator already deployed:", deployed.simulator); - } + saveDeployedContract(chainId, "Simulator", simulator); + deployed.simulator = simulator; } function deploySimpleFunder( @@ -754,23 +669,13 @@ contract DeployMain is Script, SafeSingletonDeployer { ChainConfig memory config, DeployedContracts memory deployed ) internal { - // Ensure Orchestrator is deployed first (dependency) - if (deployed.orchestrator == address(0)) { - console.log("Deploying Orchestrator first (dependency for SimpleFunder)..."); - deployOrchestrator(chainId, config, deployed); - } + bytes memory creationCode = type(SimpleFunder).creationCode; - if (deployed.simpleFunder == address(0)) { - bytes memory creationCode = type(SimpleFunder).creationCode; - bytes memory args = - abi.encode(config.funderSigner, deployed.orchestrator, config.funderOwner); - address funder = deployContractWithCreate2(chainId, creationCode, args, "SimpleFunder"); + bytes memory args = abi.encode(config.funderSigner, config.funderOwner); + address funder = deployContractWithCreate2(chainId, creationCode, args, "SimpleFunder"); - saveDeployedContract(chainId, "SimpleFunder", funder); - deployed.simpleFunder = funder; - } else { - console.log("SimpleFunder already deployed:", deployed.simpleFunder); - } + saveDeployedContract(chainId, "SimpleFunder", funder); + deployed.simpleFunder = funder; } function deployEscrow( @@ -778,15 +683,11 @@ contract DeployMain is Script, SafeSingletonDeployer { ChainConfig memory config, DeployedContracts memory deployed ) internal { - if (deployed.escrow == address(0)) { - bytes memory creationCode = type(Escrow).creationCode; - address escrow = deployContractWithCreate2(chainId, creationCode, "", "Escrow"); + bytes memory creationCode = type(Escrow).creationCode; + address escrow = deployContractWithCreate2(chainId, creationCode, "", "Escrow"); - saveDeployedContract(chainId, "Escrow", escrow); - deployed.escrow = escrow; - } else { - console.log("Escrow already deployed:", deployed.escrow); - } + saveDeployedContract(chainId, "Escrow", escrow); + deployed.escrow = escrow; } function deploySimpleSettler( @@ -794,18 +695,14 @@ contract DeployMain is Script, SafeSingletonDeployer { ChainConfig memory config, DeployedContracts memory deployed ) internal { - if (deployed.simpleSettler == address(0)) { - bytes memory creationCode = type(SimpleSettler).creationCode; - bytes memory args = abi.encode(config.settlerOwner); - address settler = - deployContractWithCreate2(chainId, creationCode, args, "SimpleSettler"); - - console.log(" Owner:", config.settlerOwner); - saveDeployedContract(chainId, "SimpleSettler", settler); - deployed.simpleSettler = settler; - } else { - console.log("SimpleSettler already deployed:", deployed.simpleSettler); - } + bytes memory creationCode = type(SimpleSettler).creationCode; + bytes memory args = abi.encode(config.settlerOwner); + address settler = + deployContractWithCreate2(chainId, creationCode, args, "SimpleSettler"); + + console.log(" Owner:", config.settlerOwner); + saveDeployedContract(chainId, "SimpleSettler", settler); + deployed.simpleSettler = settler; } function deployLayerZeroSettler( @@ -813,19 +710,62 @@ contract DeployMain is Script, SafeSingletonDeployer { ChainConfig memory config, DeployedContracts memory deployed ) internal { - if (deployed.layerZeroSettler == address(0)) { - bytes memory creationCode = type(LayerZeroSettler).creationCode; - bytes memory args = abi.encode(config.layerZeroEndpoint, config.l0SettlerOwner); - address settler = - deployContractWithCreate2(chainId, creationCode, args, "LayerZeroSettler"); - - console.log(" Endpoint:", config.layerZeroEndpoint); - console.log(" Owner:", config.l0SettlerOwner); - console.log(" EID:", config.layerZeroEid); - saveDeployedContract(chainId, "LayerZeroSettler", settler); - deployed.layerZeroSettler = settler; - } else { - console.log("LayerZeroSettler already deployed:", deployed.layerZeroSettler); + bytes memory creationCode = type(LayerZeroSettler).creationCode; + bytes memory args = abi.encode(config.l0SettlerOwner, config.l0SettlerSigner); + address settler = + deployContractWithCreate2(chainId, creationCode, args, "LayerZeroSettler"); + + console.log(" Owner:", config.l0SettlerOwner); + console.log(" L0SettlerSigner:", config.l0SettlerSigner); + console.log(" Endpoint to be configured:", config.layerZeroEndpoint); + console.log(" EID:", config.layerZeroEid); + console.log( + " Note: Endpoint must be set by owner via ConfigureLayerZeroSettler script" + ); + + saveDeployedContract(chainId, "LayerZeroSettler", settler); + deployed.layerZeroSettler = settler; + } + + function deployExpToken( + uint256 chainId, + ChainConfig memory config, + DeployedContracts memory deployed + ) internal { + // Only deploy on testnets + if (!config.isTestnet) { + console.log("Skipping ExpToken deployment - not a testnet"); + return; } + + bytes memory creationCode = type(ExperimentERC20).creationCode; + + // Deploy EXP token + // Hardcode name and symbol to "EXP" + bytes memory args = abi.encode("EXP", "EXP", 1 ether); + address expToken = deployContractWithCreate2(chainId, creationCode, args, "ExpToken"); + + // Mint initial tokens to the configured minter address + ExperimentERC20(payable(expToken)).mint(config.expMinterAddress, config.expMintAmount); + + console.log(" EXP Name/Symbol: EXP"); + console.log(" EXP Address:", expToken); + saveDeployedContract(chainId, "ExpToken", expToken); + deployed.expToken = expToken; + + // Deploy EXP2 token + // Hardcode name and symbol to "EXP2" + bytes memory args2 = abi.encode("EXP2", "EXP2", 1 ether); + address exp2Token = deployContractWithCreate2(chainId, creationCode, args2, "Exp2Token"); + + // Mint initial tokens to the configured minter address (same as EXP) + ExperimentERC20(payable(exp2Token)).mint(config.expMinterAddress, config.expMintAmount); + + console.log(" EXP2 Name/Symbol: EXP2"); + console.log(" EXP2 Address:", exp2Token); + console.log(" Minter:", config.expMinterAddress); + console.log(" Mint Amount (each):", config.expMintAmount); + saveDeployedContract(chainId, "Exp2Token", exp2Token); + deployed.exp2Token = exp2Token; } } diff --git a/deploy/FundSigners.s.sol b/deploy/FundSigners.s.sol index 02820e27..42618c6e 100644 --- a/deploy/FundSigners.s.sol +++ b/deploy/FundSigners.s.sol @@ -2,12 +2,14 @@ pragma solidity ^0.8.23; import {Script, console} from "forge-std/Script.sol"; -import {stdToml} from "forge-std/StdToml.sol"; +import {Config} from "forge-std/Config.sol"; // SimpleFunder interface for setting gas wallets interface ISimpleFunder { function setGasWallet(address[] memory wallets, bool isGasWallet) external; function gasWallets(address) external view returns (bool); + function setOrchestrators(address[] memory ocs, bool val) external; + function orchestrators(address) external view returns (bool); } /** @@ -46,9 +48,7 @@ interface ISimpleFunder { * --private-key $PRIVATE_KEY \ * "[84532]" 5 */ -contract FundSigners is Script { - using stdToml for string; - +contract FundSigners is Script, Config { /** * @notice Configuration for funding on a specific chain */ @@ -59,6 +59,7 @@ contract FundSigners is Script { uint256 targetBalance; address simpleFunderAddress; uint256 defaultNumSigners; + address[] supportedOrchestrators; } /** @@ -87,18 +88,18 @@ contract FundSigners is Script { uint256 private totalEthDistributed; uint256 private chainsProcessed; - string internal configContent; string internal configPath = "/deploy/config.toml"; /** * @notice Fund all configured chains with default number of signers */ function run() external { - // Default to common testnets - uint256[] memory chainIds = new uint256[](3); - chainIds[0] = 11155111; // Sepolia - chainIds[1] = 84532; // Base Sepolia - chainIds[2] = 11155420; // Optimism Sepolia + // Load configuration and setup forks + string memory fullConfigPath = string.concat(vm.projectRoot(), configPath); + _loadConfigAndForks(fullConfigPath, false); + + // Get all chain IDs from configuration + uint256[] memory chainIds = config.getChainIds(); uint256 numSigners = 10; // Default execute(chainIds, numSigners); } @@ -108,6 +109,10 @@ contract FundSigners is Script { * @param chainIds Array of chain IDs to fund */ function run(uint256[] memory chainIds) external { + // Load configuration and setup forks + string memory fullConfigPath = string.concat(vm.projectRoot(), configPath); + _loadConfigAndForks(fullConfigPath, false); + uint256 numSigners = 10; // Default execute(chainIds, numSigners); } @@ -118,6 +123,10 @@ contract FundSigners is Script { * @param numSigners Number of signers to fund (starting from index 0) */ function run(uint256[] memory chainIds, uint256 numSigners) external { + // Load configuration and setup forks + string memory fullConfigPath = string.concat(vm.projectRoot(), configPath); + _loadConfigAndForks(fullConfigPath, false); + execute(chainIds, numSigners); } @@ -128,9 +137,6 @@ contract FundSigners is Script { console.log("=== Signer Funding Script (TOML Config) ==="); console.log("Number of signers to fund:", numSigners); - // Load configuration - loadConfig(); - // Get mnemonic from environment string memory mnemonic = vm.envString("GAS_SIGNER_MNEMONIC"); require(bytes(mnemonic).length > 0, "GAS_SIGNER_MNEMONIC not set"); @@ -211,6 +217,9 @@ contract FundSigners is Script { if (config.simpleFunderAddress != address(0) && config.simpleFunderAddress.code.length > 0) { setGasWalletsInSimpleFunder(config.simpleFunderAddress, signers); + setOrchestratorsInSimpleFunder( + config.simpleFunderAddress, config.supportedOrchestrators + ); } vm.stopBroadcast(); @@ -412,6 +421,41 @@ contract FundSigners is Script { ); } + /** + * @notice Set orchestrators in SimpleFunder contract + */ + function setOrchestratorsInSimpleFunder(address simpleFunder, address[] memory orchestrators) + internal + { + if (orchestrators.length == 0) { + console.log("No orchestrators configured, skipping orchestrator setup"); + return; + } + + ISimpleFunder funder = ISimpleFunder(simpleFunder); + + console.log("\nSetting orchestrators in SimpleFunder:"); + console.log(" SimpleFunder address:", simpleFunder); + console.log( + string.concat(" Setting ", vm.toString(orchestrators.length), " orchestrators...") + ); + + for (uint256 i = 0; i < orchestrators.length; i++) { + console.log( + string.concat( + " Orchestrator ", vm.toString(i), ": ", vm.toString(orchestrators[i]) + ) + ); + } + + funder.setOrchestrators(orchestrators, true); + console.log( + string.concat( + " Successfully set all ", vm.toString(orchestrators.length), " orchestrators" + ) + ); + } + /** * @notice Derive signer addresses from mnemonic */ @@ -431,45 +475,29 @@ contract FundSigners is Script { return signers; } - /** - * @notice Load configuration from TOML - */ - function loadConfig() internal { - string memory fullConfigPath = string.concat(vm.projectRoot(), configPath); - configContent = vm.readFile(fullConfigPath); - } - /** * @notice Get chain funding configuration */ function getChainFundingConfig(uint256 chainId) internal returns (ChainFundingConfig memory) { - // Create fork and read configuration from fork variables - string memory rpcUrl = vm.envString(string.concat("RPC_", vm.toString(chainId))); - uint256 forkId = vm.createFork(rpcUrl); - vm.selectFork(forkId); - - ChainFundingConfig memory config; - config.chainId = chainId; - config.name = vm.readForkString("name"); - config.isTestnet = vm.readForkBool("is_testnet"); - config.targetBalance = vm.readForkUint("target_balance"); - - // Try to read SimpleFunder address - may not be deployed on all chains - try vm.readForkAddress("simple_funder_address") returns (address addr) { - config.simpleFunderAddress = addr; - } catch { - // SimpleFunder not configured for this chain - config.simpleFunderAddress = address(0); - } + // Switch to the fork for this chain (already created by _loadConfigAndForks) + vm.selectFork(forkOf[chainId]); + + ChainFundingConfig memory chainConfig; + chainConfig.chainId = chainId; + chainConfig.name = config.get(chainId, "name").toString(); + chainConfig.isTestnet = config.get(chainId, "is_testnet").toBool(); + chainConfig.targetBalance = config.get(chainId, "target_balance").toUint256(); + chainConfig.simpleFunderAddress = config.get(chainId, "simple_funder_deployed").toAddress(); // Try to read default number of signers - try vm.readForkUint("default_num_signers") returns (uint256 num) { - config.defaultNumSigners = num; - } catch { - config.defaultNumSigners = 10; // Default fallback - } + uint256 numSigners = config.get(chainId, "default_num_signers").toUint256(); + chainConfig.defaultNumSigners = numSigners == 0 ? 10 : numSigners; // Default fallback + + // Read supported orchestrators - required field + chainConfig.supportedOrchestrators = + config.get(chainId, "supported_orchestrators").toAddressArray(); - return config; + return chainConfig; } /** diff --git a/deploy/README.md b/deploy/README.md index cb27d1ef..b23c7606 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -1,6 +1,6 @@ # Deployment System -Unified deployment and configuration system for the Ithaca Account Abstraction System. +Unified deployment and configuration system for the Ithaca Account Abstraction System. We use a single TOML config for fast and easy scripting. ## Available Scripts @@ -55,69 +55,90 @@ optimism-sepolia = { key = "${ETHERSCAN_API_KEY}" } ## Configuration Structure -All configuration is in `deploy/config.toml`: +All configuration is in `deploy/config.toml` using the StdConfig format: ```toml -[profile.deployment] -registry_path = "deploy/registry/" +[base-sepolia] +endpoint_url = "${RPC_84532}" -[forks.base-sepolia] -rpc_url = "${RPC_84532}" +[base-sepolia.bool] +is_testnet = true -[forks.base-sepolia.vars] +[base-sepolia.address] # Chain identification +funder_owner = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" +funder_signer = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" +settler_owner = "0x0000000000000000000000000000000000000004" +l0_settler_owner = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" +l0_settler_signer = "0x0000000000000000000000000000000000000006" +layerzero_endpoint = "0x6EDCE65403992e310A62460808c4b910D972f10f" +simple_funder_address = "0x09F6eF9525efAdb6167dFe71fFcfbE306De07988" +layerzero_settler_address = "0xd71d3c3ff2249A67cEa12030b20E66734fB1f833" +layerzero_send_uln302 = "0xC1868e054425D378095A003EcbA3823a5D0135C9" +layerzero_receive_uln302 = "0x12523de19dc41c91F7d2093E0CFbB76b17012C8d" +dvn_layerzero_labs = "0xe1a12515F9AB2764b887bF60B923Ca494EBbB2d6" +dvn_google_cloud = "0xFc9d8E5d3FaB22fB6E93E9E2C90916E9dCa83Ade" +exp_minter_address = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" +supported_orchestrators = ["0xEd7c1e839381c489Dcd1ED3CE1B0e79DaE714f77"] + +# Deployed contract addresses - automatically written during deployment +orchestrator_deployed = "0xC662Af195CD57bC330552f3E2Be5E03Ef69cB041" +ithaca_account_deployed = "0x49627C39cC7f39f95540C2100f18608f2365a59f" +account_proxy_deployed = "0xD2a48e4635fCB2437d2e482122137F06C8433706" +simulator_deployed = "0x332A5Cd675D9d26c4af3BF618A7175d0D623CABA" +simple_funder_deployed = "0x1ADE5D4CE3183D913791DEcaeaD42Fff193AeF8F" +escrow_deployed = "0xD1c7e21f2a50A2cDDCFaf554b998a800C3C35aD1" +simple_settler_deployed = "0x3291f7Ce832997920874d70d68A8186B388024F5" +layerzero_settler_deployed = "0xBDb45dA9e075a9fCbdf8fAa9d0c93A21b3D8671a" +exp_token_deployed = "0xaeB83430528fB0DeE5E15bF07A5056B6c0b37809" +exp2_token_deployed = "0x246c631Dac318a13023e98aB925499930c9801fB" + +[base-sepolia.uint] chain_id = 84532 -name = "Base Sepolia" -is_testnet = true - -# Contract ownership -pause_authority = "0x..." # Can pause contracts -funder_owner = "0x..." # Owns SimpleFunder -funder_signer = "0x..." # Signs funding operations -settler_owner = "0x..." # Owns SimpleSettler -l0_settler_owner = "0x..." # Owns LayerZeroSettler - -# Deployment configuration -salt = "0x0000..." # CREATE2 salt (SAVE THIS!) -contracts = ["ALL"] # Or specific: ["Orchestrator", "IthacaAccount"] - -# Funding configuration (only needed for Funding Script) -target_balance = 1000000000000000 # Target balance per signer (0.001 ETH) -simple_funder_address = "0x..." # SimpleFunder address -default_num_signers = 10 # Number of signers to fund - -# LayerZero configuration (only needed for ConfigureLayerZero) -layerzero_settler_address = "0x..." -layerzero_endpoint = "0x..." layerzero_eid = 40245 -layerzero_send_uln302 = "0x..." -layerzero_receive_uln302 = "0x..." -layerzero_destination_chain_ids = [11155420] -layerzero_required_dvns = ["dvn_layerzero_labs"] -layerzero_optional_dvns = [] -layerzero_optional_dvn_threshold = 0 +target_balance = "1000000000000000" # Target balance per signer (0.001 ETH) - must be quoted for large numbers +default_num_signers = 10 # Number of signers to fund layerzero_confirmations = 1 layerzero_max_message_size = 10000 +layerzero_optional_dvn_threshold = 0 +exp_mint_amount = "5000000000000000000000" # Amount to mint (in wei) - must be quoted +layerzero_destination_chain_ids = [11155420] + +[base-sepolia.bytes32] +salt = "0x0000000000000000000000000000000000000000000000000000000000005678" # CREATE2 salt (SAVE THIS!) -dvn_layerzero_labs = "0x..." -dvn_google_cloud = "0x..." +[base-sepolia.string] +name = "Base Sepolia" +contracts = ["ALL"] # Or specific: ["Orchestrator", "IthacaAccount"] +layerzero_required_dvns = ["dvn_layerzero_labs"] +layerzero_optional_dvns = [] ``` +### Deployed Address Management + +**Automatic Address Writing**: The deployment scripts automatically write deployed contract addresses to the config file during broadcast operations: + +- ✅ **During `--broadcast`**: Deployed addresses are written to config.toml as `contract_name_deployed = "0x..."` +- ❌ **During simulation** (no `--broadcast`): No config writes occur - safe for testing +- 📍 **Address detection**: Scripts always check actual on-chain state, not config file data +- 🔄 **State synchronization**: Config file stays in sync with actual deployments + ### Available Contracts - **Orchestrator** -- **IthacaAccount** -- **AccountProxy** -- **Simulator** -- **SimpleFunder** +- **IthacaAccount** +- **AccountProxy** +- **Simulator** +- **SimpleFunder** - **Escrow** (Only needed for Interop Chains) - **SimpleSettler** (Only needed for Interop testing) - **LayerZeroSettler** (Only needed for Interop Chains) -- **ALL** - Deploys all contracts +- **ExpToken** - Test ERC20 tokens (Testnet only, automatically included with "ALL") +- **ALL** - Deploys all contracts (+ ExpToken on testnets) -**Dependencies**: -IthacaAccount requires Orchestrator; -AccountProxy requires IthacaAccount; +**Dependencies**: +IthacaAccount requires Orchestrator; +AccountProxy requires IthacaAccount; SimpleFunder requires Orchestrator. ## Quick Start - Complete Workflow @@ -149,8 +170,7 @@ forge script deploy/FundSigners.s.sol:FundSigners \ --private-key $PRIVATE_KEY \ "[84532,11155420]" -# 5. Fund SimpleFunder contract -SIMPLE_FUNDER=$(cat deploy/registry/deployment_84532_*.json | jq -r .SimpleFunder) +# 5. Fund SimpleFunder contract forge script deploy/FundSimpleFunder.s.sol:FundSimpleFunder \ --broadcast --multi --slow \ @@ -174,7 +194,7 @@ forge script deploy/FundSimpleFunder.s.sol:FundSimpleFunder \ forge script deploy/DeployMain.s.sol:DeployMain \ --broadcast --verify --multi --slow \ --sig "run()" \ - --private-key $PRIVATE_KEY + --private-key $PRIVATE_KEY # Deploy to specific chains forge script deploy/DeployMain.s.sol:DeployMain \ @@ -207,7 +227,8 @@ forge script deploy/DeployMain.s.sol:DeployMain \ **Purpose**: Configure LayerZero messaging pathways between chains. -**Prerequisites**: +**Prerequisites**: + - LayerZeroSettler deployed on source and destination chains - Caller must be l0_settler_owner @@ -230,15 +251,18 @@ forge script deploy/ConfigureLayerZeroSettler.s.sol:ConfigureLayerZeroSettler \ **Purpose**: Fund signers and register them as gas wallets in SimpleFunder. -**Prerequisites**: +**Prerequisites**: + - SimpleFunder deployed - Caller must be funder_owner - GAS_SIGNER_MNEMONIC environment variable set **What it does**: + 1. Derives signer addresses from mnemonic 2. Tops up signers below target_balance 3. Registers signers as gas wallets in SimpleFunder +4. Sets configured orchestrators in SimpleFunder ```bash # Fund default number of signers (from config) @@ -281,6 +305,7 @@ forge script deploy/FundSimpleFunder.s.sol:FundSimpleFunder \ ``` **Parameters**: + - SimpleFunder address (same across chains if using CREATE2) - Array of (chainId, tokenAddress, amount) - Use `0x0000000000000000000000000000000000000000` for native ETH @@ -297,37 +322,36 @@ forge script deploy/FundSimpleFunder.s.sol:FundSimpleFunder \ All contracts deploy via Safe Singleton Factory (`0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7`) for deterministic addresses. **Key Points**: + - Same salt + same bytecode = same address on every chain - Addresses can be predicted before deployment - **⚠️ SAVE YOUR SALT VALUES** - Required for deploying to same addresses on new chains - -## Registry Files - -Deployment addresses are saved in `deploy/registry/deployment_{chainId}_{salt}.json`: - -```json -{ - "Orchestrator": "0xb33adF2c2257a94314d408255aC843fd53B1a7e1", - "IthacaAccount": "0x5a87ef243CDA70a855828d4989Fad61B56A467d3", - "AccountProxy": "0x4ACD713815fbb363a89D9Ff046C56cEdC7EF3ad7", - "SimpleFunder": "0xA47C5C472449979a2F37dF2971627cD6587bADb8" -} -``` - -Registry files are for reference only - deployment decisions are based on on-chain state. +- **Deployment decisions based on on-chain state** - Scripts check actual deployed contracts, not config file data ## Adding New Chains 1. Add configuration to `deploy/config.toml`: ```toml -[forks.new-chain] -rpc_url = "${RPC_CHAINID}" +[new-chain] +endpoint_url = "${RPC_CHAINID}" + +[new-chain.bool] +is_testnet = true + +[new-chain.address] +funder_owner = "0x..." +# ... all required fields -[forks.new-chain.vars] +[new-chain.uint] chain_id = CHAINID -name = "Chain Name" # ... all required fields + +[new-chain.bytes32] +salt = "0x0000000000000000000000000000000000000000000000000000000000005678" + +[new-chain.string] +name = "Chain Name" contracts = ["ALL"] ``` @@ -352,18 +376,22 @@ forge script deploy/DeployMain.s.sol:DeployMain \ ### Common Issues **"No chains found in configuration"** + - Verify config.toml has properly configured chains - Check RPC URLs are set for target chains **"Safe Singleton Factory not deployed"** + - Factory must exist at `0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7` - Most major chains have this deployed **Contract already deployed** + - Normal for CREATE2 - existing contracts are skipped - Change salt value to deploy to new addresses **RPC errors** + - Verify RPC URLs are correct and accessible - Check rate limits on public RPCs - Consider paid RPC services for production @@ -376,38 +404,145 @@ forge script deploy/DeployMain.s.sol:DeployMain \ 4. **Commit registry files** - Provides deployment history 5. **Use `--multi --slow`** - Ensures proper multi-chain ordering 6. **Verify while deploying** - Use `--verify` flag +7. **Large numbers must be quoted in TOML** - Use `"1000000000000000000"` not `1000000000000000000` ## Configuration Field Reference -| Field | Used By | Purpose | -|-------|---------|---------| -| `chain_id`, `name`, `is_testnet` | All scripts | Chain identification | -| `pause_authority` | DeployMain | Contract pause permissions | -| `funder_owner`, `funder_signer` | DeployMain, FundSigners | SimpleFunder control | -| `settler_owner` | DeployMain | SimpleSettler ownership | -| `l0_settler_owner` | DeployMain, ConfigureLayerZero | LayerZeroSettler ownership | -| `salt` | DeployMain | CREATE2 deployment salt | -| `contracts` | DeployMain | Which contracts to deploy | -| `target_balance` | FundSigners | Minimum signer balance | -| `simple_funder_address` | FundSigners, FundSimpleFunder | SimpleFunder location | -| `default_num_signers` | FundSigners | Number of signers | -| `layerzero_*` fields | ConfigureLayerZeroSettler | LayerZero configuration | +| Field | Used By | Purpose | +| --------------------------------------- | ------------------------------ | ------------------------------------------------ | +| `chain_id`, `name`, `is_testnet` | All scripts | Chain identification | +| `pause_authority` | DeployMain | Contract pause permissions | +| `funder_owner`, `funder_signer` | DeployMain, FundSigners | SimpleFunder control | +| `settler_owner` | DeployMain | SimpleSettler ownership | +| `l0_settler_owner` | DeployMain, ConfigureLayerZero | LayerZeroSettler ownership | +| `salt` | DeployMain | CREATE2 deployment salt | +| `contracts` | DeployMain | Which contracts to deploy | +| `target_balance` | FundSigners | Minimum signer balance | +| `simple_funder_address` | FundSigners, FundSimpleFunder | SimpleFunder location | +| `default_num_signers` | FundSigners | Number of signers | +| `supported_orchestrators` | FundSigners | Orchestrator addresses to enable in SimpleFunder | +| `layerzero_*` fields | ConfigureLayerZeroSettler | LayerZero configuration | +| `exp_minter_address`, `exp_mint_amount` | DeployMain (testnet) | ExpToken deployment | +| `*_deployed` fields | All scripts | Auto-written deployed contract addresses | + +## ExpToken Deployment (Testnets Only) + +### Automatic ExpToken Deployment -## Test Token Deployment +**Purpose**: Deploy EXP and EXP2 test tokens automatically on testnet chains. -### DeployEXP - Test ERC20 Token +**Behavior**: -**Purpose**: Deploy a simple test ERC20 token for testing. +- **Testnets** (`is_testnet = true`): ExpToken automatically included when using `["ALL"]` contracts +- **Production** (`is_testnet = false`): ExpToken never deployed, regardless of configuration +- **Two tokens deployed**: "EXP" and "EXP2" with hardcoded names +- **Same configuration**: Both tokens use the same minter address and mint amount + +### Configuration Requirements + +For testnet chains, **both fields are required** (deployment will fail if missing): + +```toml +[testnet-name.bool] +is_testnet = true + +[testnet-name.address] +exp_minter_address = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" # REQUIRED + +[testnet-name.uint] +exp_mint_amount = "5000000000000000000000" # REQUIRED (5000 tokens in wei) + +[testnet-name.string] +contracts = ["ALL"] # ExpToken automatically included for testnets +``` + +### Deployment Details + +**Two tokens are deployed**: + +1. **EXP Token**: Name and symbol "EXP" +2. **EXP2 Token**: Name and symbol "EXP2" + +**Both tokens**: + +- Use CREATE2 for deterministic addresses +- Mint `exp_mint_amount` tokens to `exp_minter_address` +- Are deployed at the end of the contract deployment sequence +- Are saved to registry files as "ExpToken" and "Exp2Token" + +### Examples + +**Base Sepolia (Testnet)**: + +```toml +[base-sepolia.bool] +is_testnet = true + +[base-sepolia.address] +exp_minter_address = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" + +[base-sepolia.uint] +exp_mint_amount = "5000000000000000000000" + +[base-sepolia.string] +contracts = ["ALL"] # Deploys 8 core contracts + ExpToken +``` + +## Supporting Bash Scripts + +### Overview + +There are two main scripts that handle multi-chain deployments and configuration verification: + +- **`deploy/execute_config.sh`** - Brings up the whole environment, by calling all scripts correctly. +- **`deploy/verify_config.sh`** - Verifies that the values in the config.toml are all set and configured correctly. + +### Usage + +#### Execute Deployment -**Usage**: ```bash -# Deploy test token to a specific chain -forge script deploy/DeployEXP.s.sol:DeployEXP \ - --broadcast \ - --sig "run()" \ - --private-key $PRIVATE_KEY \ - --rpc-url $RPC_ +# Deploy to specific chains +./deploy/execute_config.sh 84532 11155420 + +# Deploy to all chains in config.toml +./deploy/execute_config.sh +``` + +The script performs these steps: +1. Validates configuration for selected chains +2. Deploys core contracts (IthacaAccount, SimpleFunder, SimpleSettler, etc.) +3. Configures LayerZero cross-chain messaging +4. Funds signer accounts with gas tokens +5. Verifies all deployments match configuration + +#### Verify Configuration + +```bash +# Verify specific chains +./deploy/verify_config.sh 84532 11155420 + +# Verify all chains in config.toml +./deploy/verify_config.sh ``` -The test token includes basic ERC20 functionality and can be minted by anyone. -It should only be used for testing purposes. \ No newline at end of file +The script checks: +- Required environment variables (RPC URLs, private keys) +- Contract deployment addresses match config.toml +- Signer accounts have sufficient gas balances +- LayerZero endpoints and DVN configurations +- Cross-chain pathway configurations + +### Configuration + +Both scripts read from `deploy/config.toml` which defines per-chain: +- RPC endpoints and chain metadata +- Contract addresses and owners +- LayerZero endpoint configurations +- Cross-chain destination mappings +- Gas funding amounts + +Environment variables required: +- `PRIVATE_KEY` - Deployer private key +- `GAS_SIGNER_MNEMONIC` - Mnemonic for signer accounts +- `RPC_` - RPC URL for each chain (e.g., `RPC_84532`) \ No newline at end of file diff --git a/deploy/config.toml b/deploy/config.toml index 0644abd6..32d4fd67 100644 --- a/deploy/config.toml +++ b/deploy/config.toml @@ -1,136 +1,155 @@ -# Ithaca Account Deployment Configuration -# Single source of truth for all deployment and fork configurations -# This file extends foundry.toml and contains both Foundry fork configs and deployment-specific settings +[11155111] +endpoint_url = "${RPC_11155111}" -# ============================================ -# GLOBAL DEPLOYMENT CONFIGURATION -# ============================================ - -[profile.deployment] -registry_path = "deploy/registry/" - -# ============================================ -# FORK CONFIGURATIONS (for vm.readFork* cheatcodes) -# ============================================ - -# Sepolia -[forks.sepolia] -rpc_url = "${RPC_11155111}" - -[forks.sepolia.vars] -chain_id = 11155111 -name = "Sepolia" +[11155111.bool] is_testnet = true -pause_authority = "0x0000000000000000000000000000000000000001" + +[11155111.address] funder_owner = "0x0000000000000000000000000000000000000003" funder_signer = "0x0000000000000000000000000000000000000002" settler_owner = "0x0000000000000000000000000000000000000004" l0_settler_owner = "0x0000000000000000000000000000000000000005" +l0_settler_signer = "0x0000000000000000000000000000000000000006" layerzero_endpoint = "0x6EDCE65403992e310A62460808c4b910D972f10f" -layerzero_eid = 40161 -salt = "0x0000000000000000000000000000000000000000000000000000000000000000" -target_balance = 50000000000000000 # 0.05 ether -# Specify contracts to deploy. Use ["ALL"] to deploy all available contracts -contracts = ["LayerZeroSettler"] -# SimpleFunder configuration -simple_funder_address = "0x2AD8F6a3bB1126a777606eaFa9da9b95530d9597" # TODO: automate write when foundry upgrades -default_num_signers = 10 -# LayerZero configuration (copied from Base Sepolia for testing) -# TODO: Set correct addresses here. layerzero_settler_address = "0x4225041FF3DB1C7d7a1029406bB80C7298767aca" layerzero_send_uln302 = "0xC1868e054425D378095A003EcbA3823a5D0135C9" layerzero_receive_uln302 = "0x12523de19dc41c91F7d2093E0CFbB76b17012C8d" -layerzero_destination_chain_ids = [84532, 11155420] -layerzero_required_dvns = ["dvn_layerzero_labs"] # optimism sepolia -layerzero_optional_dvns = [] -layerzero_optional_dvn_threshold = 0 -layerzero_confirmations = 1 -layerzero_max_message_size = 10000 -# DVN addresses dvn_layerzero_labs = "0x8eebf8b423B73bFCa51a1Db4B7354AA0bFCA9193" dvn_google_cloud = "0x8e5a7b5959C5A7C732b87dCE401E07F5819eEC2d" +exp_minter_address = "0x0000000000000000000000000000000000000003" +supported_orchestrators = ["0xEd7c1e839381c489Dcd1ED3CE1B0e79DaE714f77"] +orchestrator_deployed = "0xCa58a1296c5AE231A2963A26E11f722bfe65acd4" +ithaca_account_deployed = "0xC203A827A6DB2d09fbF052f88e7aD794EEbf8928" +account_proxy_deployed = "0x0B5d691F3A98B705756ab196CFe4af97f35e06E4" +simulator_deployed = "0x3306F2DD87887885f4de81aF6BD88b648E467f4e" +simple_funder_deployed = "0xa0BF9a30fD55A45BF09309513734B8194fBE3934" +escrow_deployed = "0x669a3aB5816d1ADdbFdecE2F6B238D323F056248" +simple_settler_deployed = "0xFB0F29c658145DbDb1Bf974C8A342F4E78BF9801" +layerzero_settler_deployed = "0xC93D0142D960F7B33440F09B13917C195e09ffA0" +exp_token_deployed = "0x4D335fa317FB8d5493b83e06161134ec611Bc188" +exp2_token_deployed = "0x0348152303686f2197b24981571FCf8c36c66Cc5" + +[11155111.uint] +chain_id = 11155111 +layerzero_eid = 40161 +target_balance = "50000000000000000" +default_num_signers = 10 +layerzero_confirmations = 1 +layerzero_max_message_size = 10000 +layerzero_optional_dvn_threshold = 0 +exp_mint_amount = "1000000000000000000000" +layerzero_destination_chain_ids = [ + 84532, + 11155420, +] -# Base Sepolia -[forks.base-sepolia] -rpc_url = "${RPC_84532}" +[11155111.bytes32] +salt = "0xdf08a9fa957a9ac004f84f3a8ab7318a7be55d59948f46497bd3717865295b61" -[forks.base-sepolia.vars] -orchestrator_address = "0x0000000000000000000000000000000000000000" -# Deployment Script -chain_id = 84532 -name = "Base Sepolia" +[11155111.string] +name = "Sepolia" +contracts = ["ALL"] +layerzero_required_dvns = ["dvn_layerzero_labs"] +layerzero_optional_dvns = [] + +[84532] +endpoint_url = "${RPC_84532}" + +[84532.bool] is_testnet = true -pause_authority = "0x0000000000000000000000000000000000000001" # Set to separate PAUSE KEY -funder_owner = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" # Set to Master Key -funder_signer = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" # Set to Master Key -settler_owner = "0x0000000000000000000000000000000000000004" # Set to Master Key -l0_settler_owner = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" # Set to Master Key -layerzero_endpoint = "0x6EDCE65403992e310A62460808c4b910D972f10f" # Get from LayerZero docs -layerzero_eid = 40245 # Get from LayerZero docs -salt = "0x00000000000000000000000000000000000000000000000000000000000000c7" # Almost alwayse be bytes32(0) -# Specify contracts to deploy. Use ["ALL"] to deploy all available contracts -contracts = ["ALL"] -# Funding Script -target_balance = 1000000000000000 # 0.001 ether - -# SimpleFunder configuration -simple_funder_address = "0x5e6c8c24A53275e2C6C3DC5f1c5C027Ec13439Ba" -default_num_signers = 10 -# LayerZero configuration -layerzero_settler_address = "0x4225041FF3DB1C7d7a1029406bB80C7298767aca" + +[84532.address] +funder_owner = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" +funder_signer = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" +settler_owner = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" +l0_settler_owner = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" +l0_settler_signer = "0x0000000000000000000000000000000000000006" +layerzero_endpoint = "0x6EDCE65403992e310A62460808c4b910D972f10f" +layerzero_settler_address = "0xd71d3c3ff2249A67cEa12030b20E66734fB1f833" layerzero_send_uln302 = "0xC1868e054425D378095A003EcbA3823a5D0135C9" layerzero_receive_uln302 = "0x12523de19dc41c91F7d2093E0CFbB76b17012C8d" -layerzero_destination_chain_ids = [11155420] # optimism sepolia -layerzero_required_dvns = ["dvn_layerzero_labs"] -layerzero_optional_dvns = [] -layerzero_optional_dvn_threshold = 0 -layerzero_confirmations = 1 -layerzero_max_message_size = 10000 -# DVN addresses dvn_layerzero_labs = "0xe1a12515F9AB2764b887bF60B923Ca494EBbB2d6" dvn_google_cloud = "0xFc9d8E5d3FaB22fB6E93E9E2C90916E9dCa83Ade" +exp_minter_address = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" +supported_orchestrators = ["0xEd7c1e839381c489Dcd1ED3CE1B0e79DaE714f77"] +orchestrator_deployed = "0xcA91Dba3dab46478FC466ef76491AaB78dbC2d0f" +ithaca_account_deployed = "0x1E7a350c76CCd750470930B5026fEa26F44cf937" +account_proxy_deployed = "0xDe938C3642205782d52535d12eC9dFb4D1bC832b" +simulator_deployed = "0xb100Dc6e1BA9e54965fBB865F65Ac42b28BC25E8" +simple_funder_deployed = "0x57C3563C06CeDda15F7622A83a1BdBa84125351D" +escrow_deployed = "0xCd075ceb5Cd463a9233a8085fc915767139F655c" +simple_settler_deployed = "0x5aE547Dc236c31d30f19a5B9040992728Ac441A0" +layerzero_settler_deployed = "0x23D18C5b5Df22fDcf6dDE73b0f1a76ee5761d206" +exp_token_deployed = "0xa8071DA5e994cB8e3eB56CaD0FBB6ca424dD8dc0" +exp2_token_deployed = "0x61727778216127D0843A99A3e91e99C27e9f3BC7" -# Optimism Sepolia -[forks.optimism-sepolia] -rpc_url = "${RPC_11155420}" +[84532.uint] +chain_id = 84532 +layerzero_eid = 40245 +target_balance = "1000000000000000" +default_num_signers = 10 +layerzero_confirmations = 1 +layerzero_max_message_size = 10000 +layerzero_optional_dvn_threshold = 0 +exp_mint_amount = "5000000000000000000000" +layerzero_destination_chain_ids = [11155420] -[forks.optimism-sepolia.vars] -chain_id = 11155420 -name = "Optimism Sepolia" +[84532.bytes32] +salt = "0xdf08a9fa957a9ac004f84f3a8ab7318a7be55d59948f46497bd3717865295b61" + +[84532.string] +name = "Base Sepolia" +contracts = ["ALL"] +layerzero_required_dvns = ["dvn_layerzero_labs"] +layerzero_optional_dvns = [] + +[11155420] +endpoint_url = "${RPC_11155420}" + +[11155420.bool] is_testnet = true -pause_authority = "0x0000000000000000000000000000000000000001" -funder_owner = "0x0000000000000000000000000000000000000003" -funder_signer = "0x0000000000000000000000000000000000000002" -settler_owner = "0x0000000000000000000000000000000000000004" + +[11155420.address] +funder_owner = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" +funder_signer = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" +settler_owner = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" l0_settler_owner = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" +l0_settler_signer = "0x0000000000000000000000000000000000000006" layerzero_endpoint = "0x6EDCE65403992e310A62460808c4b910D972f10f" -layerzero_eid = 40232 -salt = "0x0000000000000000000000000000000000000000000000000000000000000001" -target_balance = 10000000000000000 # 0.01 ether -# Specify contracts to deploy. Use ["ALL"] to deploy all available contracts -contracts = ["ALL"] # Deploy all contracts -# SimpleFunder configuration -simple_funder_address = "0x2AD8F6a3bB1126a777606eaFa9da9b95530d9597" - -default_num_signers = 10 -# LayerZero configuration -layerzero_settler_address = "0x4225041FF3DB1C7d7a1029406bB80C7298767aca" +layerzero_settler_address = "0x9F610182BD096ab7734C1365A44f48dD0A574d0D" layerzero_send_uln302 = "0xB31D2cb502E25B30C651842C7C3293c51Fe6d16f" layerzero_receive_uln302 = "0x9284fd59B95b9143AF0b9795CAC16eb3C723C9Ca" -layerzero_destination_chain_ids = [84532, 11155111] -layerzero_required_dvns = ["dvn_layerzero_labs"] -layerzero_optional_dvns = [] -layerzero_optional_dvn_threshold = 0 -layerzero_confirmations = 1 -layerzero_max_message_size = 10000 -# DVN addresses dvn_layerzero_labs = "0x28B6140ead70cb2Fb669705b3598ffB4BEaA060b" dvn_google_cloud = "0x28b8B8Ea5C695EFa657B6b86a4a8E90ccbA93E9e" +exp_minter_address = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" +supported_orchestrators = ["0xEd7c1e839381c489Dcd1ED3CE1B0e79DaE714f77"] +orchestrator_deployed = "0xcA91Dba3dab46478FC466ef76491AaB78dbC2d0f" +ithaca_account_deployed = "0x1E7a350c76CCd750470930B5026fEa26F44cf937" +account_proxy_deployed = "0xDe938C3642205782d52535d12eC9dFb4D1bC832b" +simulator_deployed = "0xb100Dc6e1BA9e54965fBB865F65Ac42b28BC25E8" +simple_funder_deployed = "0x57C3563C06CeDda15F7622A83a1BdBa84125351D" +escrow_deployed = "0xCd075ceb5Cd463a9233a8085fc915767139F655c" +simple_settler_deployed = "0x5aE547Dc236c31d30f19a5B9040992728Ac441A0" +layerzero_settler_deployed = "0x23D18C5b5Df22fDcf6dDE73b0f1a76ee5761d206" +exp_token_deployed = "0xa8071DA5e994cB8e3eB56CaD0FBB6ca424dD8dc0" +exp2_token_deployed = "0x61727778216127D0843A99A3e91e99C27e9f3BC7" +[11155420.uint] +chain_id = 11155420 +layerzero_eid = 40232 +target_balance = "1000000000000000" +default_num_signers = 10 +layerzero_confirmations = 1 +layerzero_max_message_size = 10000 +layerzero_optional_dvn_threshold = 0 +exp_mint_amount = "2000000000000000000000" +layerzero_destination_chain_ids = [84532] +[11155420.bytes32] +salt = "0xdf08a9fa957a9ac004f84f3a8ab7318a7be55d59948f46497bd3717865295b61" -# Master Key has gas funds on all chains. -# 1. Deployment Key ( Master Key ) -# 2. Funder Owner Key ( Master Key ) -# 3. Gas Signers ( Funding is included in the fund signers script + setting them as gas wallets ) -# 4. Simple Funder Address ( Funds need to come from GK after bridging, no automation ) +[11155420.string] +name = "Optimism Sepolia" +contracts = ["ALL"] +layerzero_required_dvns = ["dvn_layerzero_labs"] +layerzero_optional_dvns = [] diff --git a/deploy/registry/deployment_11155420_0x0000000000000000000000000000000000000000000000000000000000000001.json b/deploy/registry/deployment_11155420_0x0000000000000000000000000000000000000000000000000000000000000001.json deleted file mode 100644 index 23cb5e8b..00000000 --- a/deploy/registry/deployment_11155420_0x0000000000000000000000000000000000000000000000000000000000000001.json +++ /dev/null @@ -1 +0,0 @@ -{"Orchestrator": "0xb33adF2c2257a94314d408255aC843fd53B1a7e1","IthacaAccount": "0x5a87ef243CDA70a855828d4989Fad61B56A467d3","AccountProxy": "0x4ACD713815fbb363a89D9Ff046C56cEdC7EF3ad7","Simulator": "0x65Ae218EB1987b8bd0F9eeb38D1B344726D41dA5","SimpleFunder": "0xA47C5C472449979a2F37dF2971627cD6587bADb8","Escrow": "0x24F50280cE3B51Ab1967F048746FB7ba3C7B4067","SimpleSettler": "0xb934afBB50b8aBBe24959f9398fE024BEe9Bf716","LayerZeroSettler": "0xB89f4A85d38C3A2407854269527fabD3b61fd56a"} \ No newline at end of file diff --git a/deploy/registry/deployment_84532_0x0000000000000000000000000000000000000000000000000000000000000001.json b/deploy/registry/deployment_84532_0x0000000000000000000000000000000000000000000000000000000000000001.json deleted file mode 100644 index c37d42b4..00000000 --- a/deploy/registry/deployment_84532_0x0000000000000000000000000000000000000000000000000000000000000001.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "Orchestrator": "0xb33adF2c2257a94314d408255aC843fd53B1a7e1", - "IthacaAccount": "0x5a87ef243CDA70a855828d4989Fad61B56A467d3", - "AccountProxy": "0x4ACD713815fbb363a89D9Ff046C56cEdC7EF3ad7", - "Simulator": "0x65Ae218EB1987b8bd0F9eeb38D1B344726D41dA5", - "SimpleFunder": "0xA47C5C472449979a2F37dF2971627cD6587bADb8", - "Escrow": "0x24F50280cE3B51Ab1967F048746FB7ba3C7B4067", - "SimpleSettler": "0xb934afBB50b8aBBe24959f9398fE024BEe9Bf716", - "LayerZeroSettler": "0xB89f4A85d38C3A2407854269527fabD3b61fd56a" -} diff --git a/docs/4337CallGraph.md b/docs/4337CallGraph.md new file mode 100644 index 00000000..ec9e788d --- /dev/null +++ b/docs/4337CallGraph.md @@ -0,0 +1,32 @@ +sequenceDiagram + participant User as User + participant Bundler as Bundler + participant EntryPoint as EntryPoint + participant Paymaster as Paymaster + participant Account as ERC4337 Account + + User->>Bundler: Submit signed UserOperation + paymasterAndData + Bundler->>EntryPoint: handleOps([userOp]) + + Note over EntryPoint: Validation Phase + EntryPoint->>Account: validateUserOp(userOp, userOpHash, missingAccountFunds) + Account-->>EntryPoint: validationData + + Note over EntryPoint: Paymaster validation & deposit check + EntryPoint->>Paymaster: validatePaymasterUserOp(userOp, userOpHash, maxCost) + Note over Paymaster: Validate user agreed to pay in USDC + Paymaster-->>EntryPoint: (context, validationData) + + + Note over EntryPoint: Execution Phase + EntryPoint->>Account: execute(userOp.callData) + + Note over EntryPoint: Payment Phase - ETH movement + EntryPoint-->>Bundler: ETH transfer (actualGasCost) + + EntryPoint->>Paymaster: postOp(mode, context, actualGasCost) + Note over Paymaster: Calculate USDC amount needed + Account-->>Paymaster: USDC transfer (calculated amount) + + EntryPoint-->>Bundler: Execution result + Bundler-->>User: UserOp result \ No newline at end of file diff --git a/docs/IthacaCallGraph.md b/docs/IthacaCallGraph.md new file mode 100644 index 00000000..7c35733c --- /dev/null +++ b/docs/IthacaCallGraph.md @@ -0,0 +1,25 @@ +sequenceDiagram + participant User as User + participant Relayer as Relayer + participant Orch as Orchestrator + participant Account as IthacaAccount + + User->>Relayer: Submit signed intent + Relayer->>Orch: execute(encodedIntent) + + Note over Orch: Verify signature + Orch->>Account: unwrapAndValidateSignature(digest, signature) + Account-->>Orch: (isValid, keyHash) + + Note over Orch: Increment nonce + Orch->>Account: checkAndIncrementNonce(nonce) + + Note over Orch: Process payment + Orch->>Account: pay(paymentAmount, keyHash, digest, intent) + Account-->>Relayer: ERC20 transfer (paymentAmount) + + Note over Orch: Execute intent + Orch->>Account: execute(executionData) + + Orch-->>Relayer: Return success/error code + Relayer-->>User: Execution result \ No newline at end of file diff --git a/docs/benchmarks.jpeg b/docs/benchmarks.jpeg new file mode 100644 index 00000000..43c3a861 Binary files /dev/null and b/docs/benchmarks.jpeg differ diff --git a/foundry.lock b/foundry.lock index fe1fb56d..52f31524 100644 --- a/foundry.lock +++ b/foundry.lock @@ -5,7 +5,7 @@ "lib/forge-std": { "branch": { "name": "master", - "rev": "27ab851c7cde58b22cc4de426d59e23a31c5cb9c" + "rev": "c2cf7017d27c1d20e74ace4dacb6c5ce4bbbe899" } }, "lib/murky": { @@ -17,7 +17,7 @@ "lib/solady": { "branch": { "name": "main", - "rev": "73f13dd1483707ef6b4d16cb0543570b7e1715a8" + "rev": "acd959aa4bd04720d640bf4e6a5c71037510cc4b" } } } \ No newline at end of file diff --git a/foundry.toml b/foundry.toml index b26c00b5..9bc78c0c 100644 --- a/foundry.toml +++ b/foundry.toml @@ -4,17 +4,15 @@ # The Default Profile [profile.default] -# Extend the deployment configuration from deploy/config.toml -# This inherits all [forks.*] configurations for multi-chain deployments -extends = "deploy/config.toml" -evm_version = "prague" # Cancun will be tested in the CI. +evm_version = "prague" auto_detect_solc = false +solc_version = "0.8.28" +bytecode_hash = "none" +cbor_metadata = false optimizer = true optimizer_runs = 200 -gas_limit = 100_000_000 # ETH is 30M, but we use a higher value. fs_permissions = [ - { access = "read-write", path = "deploy/registry" }, - { access = "read", path = "deploy/config.toml" } + { access = "read-write", path = "deploy/config.toml" } ] [fmt] @@ -34,5 +32,3 @@ runs = 10 # mainnet = { key = "${VERIFICATION_KEY_1}" } # arbitrum = { key = "${VERIFICATION_KEY_42161}" } # base = { key = "${VERIFICATION_KEY_8453}" } - - diff --git a/gas-snapshots/.gas-snapshot-main b/gas-snapshots/.gas-snapshot-main deleted file mode 100644 index 35524c7b..00000000 --- a/gas-snapshots/.gas-snapshot-main +++ /dev/null @@ -1,11 +0,0 @@ -BenchmarkTest:testERC20TransferDirect() (gas: 29222) -BenchmarkTest:testERC20TransferViaERC4337Orchestrator() (gas: 168797) -BenchmarkTest:testERC20TransferViaERC4337OrchestratorWithExtendedCalldata() (gas: 171723) -BenchmarkTest:testERC20TransferViaMinimals() (gas: 69717) -BenchmarkTest:testERC20TransferViaPortoOrchestrator() (gas: 97007) -BenchmarkTest:testERC20TransferViaPortoOrchestratorWithExtendedCalldata() (gas: 99523) -BenchmarkTest:testERC20TransferViaPortoOrchestratorWithSpendLimits() (gas: 117113) -BenchmarkTest:testNativeTransferViaPortoOrchestrator() (gas: 106399) -BenchmarkTest:testUniswapV2SwapViaERC4337Orchestrator() (gas: 216555) -BenchmarkTest:testUniswapV2SwapViaPortoOrchestrator() (gas: 144868) -BenchmarkTest:test__codesize() (gas: 124506) \ No newline at end of file diff --git a/lib/.idea/.gitignore b/lib/.idea/.gitignore deleted file mode 100644 index 26d33521..00000000 --- a/lib/.idea/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml diff --git a/lib/.idea/caches/deviceStreaming.xml b/lib/.idea/caches/deviceStreaming.xml deleted file mode 100644 index 94e429a7..00000000 --- a/lib/.idea/caches/deviceStreaming.xml +++ /dev/null @@ -1,2007 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/lib/.idea/lib.iml b/lib/.idea/lib.iml deleted file mode 100644 index d6ebd480..00000000 --- a/lib/.idea/lib.iml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/lib/.idea/misc.xml b/lib/.idea/misc.xml deleted file mode 100644 index 862d09bd..00000000 --- a/lib/.idea/misc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/lib/.idea/modules.xml b/lib/.idea/modules.xml deleted file mode 100644 index b0c4ff69..00000000 --- a/lib/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/lib/.idea/vcs.xml b/lib/.idea/vcs.xml deleted file mode 100644 index d3a8bd59..00000000 --- a/lib/.idea/vcs.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/lib/solady b/lib/solady index 90db92ce..acd959aa 160000 --- a/lib/solady +++ b/lib/solady @@ -1 +1 @@ -Subproject commit 90db92ce173856605d24a554969f2c67cadbc7e9 +Subproject commit acd959aa4bd04720d640bf4e6a5c71037510cc4b diff --git a/snapshots/BenchmarkTest.json b/snapshots/BenchmarkTest.json index dedb663c..82f7d2ae 100644 --- a/snapshots/BenchmarkTest.json +++ b/snapshots/BenchmarkTest.json @@ -1,53 +1,53 @@ { - "testERC20Transfer_AlchemyModularAccount": "159052", - "testERC20Transfer_AlchemyModularAccount_AppSponsor": "134278", - "testERC20Transfer_AlchemyModularAccount_ERC20SelfPay": "160169", - "testERC20Transfer_Batch100_AlchemyModularAccount_AppSponsor": "7053863", - "testERC20Transfer_Batch100_CoinbaseSmartWallet": "9801043", - "testERC20Transfer_Batch100_CoinbaseSmartWallet_AppSponsor": "6483523", - "testERC20Transfer_Batch100_CoinbaseSmartWallet_ERC20SelfPay": "8487729", - "testERC20Transfer_Batch100_Safe4337": "11165400", - "testERC20Transfer_Batch100_Safe4337_AppSponsor": "7525827", - "testERC20Transfer_Batch100_Safe4337_ERC20SelfPay": "9540939", - "testERC20Transfer_Batch100_ZerodevKernel_AppSponsor": "8956539", - "testERC20Transfer_CoinbaseSmartWallet": "150133", - "testERC20Transfer_CoinbaseSmartWallet_AppSponsor": "126009", - "testERC20Transfer_CoinbaseSmartWallet_ERC20SelfPay": "150241", - "testERC20Transfer_ERC4337MinimalAccount": "148602", - "testERC20Transfer_ERC4337MinimalAccount_AppSponsor": "123885", - "testERC20Transfer_ERC4337MinimalAccount_ERC20SelfPay": "149776", - "testERC20Transfer_IthacaAccount": "91666", - "testERC20Transfer_IthacaAccountWithSpendLimits": "113167", - "testERC20Transfer_IthacaAccount_AppSponsor": "99218", - "testERC20Transfer_IthacaAccount_AppSponsor_ERC20": "104319", - "testERC20Transfer_IthacaAccount_ERC20SelfPay": "91967", - "testERC20Transfer_Safe4337": "163675", - "testERC20Transfer_Safe4337_AppSponsor": "136323", - "testERC20Transfer_Safe4337_ERC20SelfPay": "160610", - "testERC20Transfer_ZerodevKernel": "175447", - "testERC20Transfer_ZerodevKernel_AppSponsor": "150734", - "testERC20Transfer_ZerodevKernel_ERC20SelfPay": "176663", - "testERC20Transfer_batch100_AlchemyModularAccount": "10438358", - "testERC20Transfer_batch100_AlchemyModularAccount_ERC20SelfPay": "9221814", - "testERC20Transfer_batch100_IthacaAccount": "6201928", - "testERC20Transfer_batch100_IthacaAccount_AppSponsor": "6758228", - "testERC20Transfer_batch100_IthacaAccount_AppSponsor_ERC20": "6565428", - "testERC20Transfer_batch100_IthacaAccount_ERC20SelfPay": "6004328", - "testERC20Transfer_batch100_ZerodevKernel": "12332690", - "testERC20Transfer_batch100_ZerodevKernel_ERC20SelfPay": "11134785", - "testNativeTransfer_AlchemyModularAccount": "168453", - "testNativeTransfer_CoinbaseSmartWallet": "159248", - "testNativeTransfer_IthacaAccount": "101064", - "testNativeTransfer_IthacaAccount_AppSponsor": "108635", - "testNativeTransfer_IthacaAccount_ERC20SelfPay": "101365", - "testNativeTransfer_Safe4337": "172763", - "testNativeTransfer_ZerodevKernel": "184855", - "testUniswapV2Swap_AlchemyModularAccount": "210111", - "testUniswapV2Swap_CoinbaseSmartWallet": "201623", - "testUniswapV2Swap_ERC4337MinimalAccount": "199690", - "testUniswapV2Swap_IthacaAccount": "142728", - "testUniswapV2Swap_IthacaAccount_AppSponsor": "150244", - "testUniswapV2Swap_IthacaAccount_ERC20SelfPay": "143029", - "testUniswapV2Swap_Safe4337": "215353", - "testUniswapV2Swap_ZerodevKernel": "226591" + "testERC20Transfer_AlchemyModularAccount": "179494", + "testERC20Transfer_AlchemyModularAccount_AppSponsor": "176436", + "testERC20Transfer_AlchemyModularAccount_ERC20SelfPay": "207779", + "testERC20Transfer_Batch100_AlchemyModularAccount_AppSponsor": "8897167", + "testERC20Transfer_Batch100_CoinbaseSmartWallet": "9952203", + "testERC20Transfer_Batch100_CoinbaseSmartWallet_AppSponsor": "8787327", + "testERC20Transfer_Batch100_CoinbaseSmartWallet_ERC20SelfPay": "11335605", + "testERC20Transfer_Batch100_Safe4337": "11685484", + "testERC20Transfer_Batch100_Safe4337_AppSponsor": "10198375", + "testERC20Transfer_Batch100_Safe4337_ERC20SelfPay": "12757523", + "testERC20Transfer_Batch100_ZerodevKernel_AppSponsor": "11427583", + "testERC20Transfer_CoinbaseSmartWallet": "177855", + "testERC20Transfer_CoinbaseSmartWallet_AppSponsor": "175259", + "testERC20Transfer_CoinbaseSmartWallet_ERC20SelfPay": "204919", + "testERC20Transfer_ERC4337MinimalAccount": "171509", + "testERC20Transfer_ERC4337MinimalAccount_AppSponsor": "168500", + "testERC20Transfer_ERC4337MinimalAccount_ERC20SelfPay": "199831", + "testERC20Transfer_IthacaAccount": "128195", + "testERC20Transfer_IthacaAccountWithSpendLimits": "193658", + "testERC20Transfer_IthacaAccount_AppSponsor": "138709", + "testERC20Transfer_IthacaAccount_AppSponsor_ERC20": "144010", + "testERC20Transfer_IthacaAccount_ERC20SelfPay": "128684", + "testERC20Transfer_Safe4337": "197561", + "testERC20Transfer_Safe4337_AppSponsor": "191725", + "testERC20Transfer_Safe4337_ERC20SelfPay": "221464", + "testERC20Transfer_ZerodevKernel": "207117", + "testERC20Transfer_ZerodevKernel_AppSponsor": "204120", + "testERC20Transfer_ZerodevKernel_ERC20SelfPay": "235489", + "testERC20Transfer_batch100_AlchemyModularAccount": "10109066", + "testERC20Transfer_batch100_AlchemyModularAccount_ERC20SelfPay": "11609298", + "testERC20Transfer_batch100_IthacaAccount": "7545392", + "testERC20Transfer_batch100_IthacaAccount_AppSponsor": "8151496", + "testERC20Transfer_batch100_IthacaAccount_AppSponsor_ERC20": "7977508", + "testERC20Transfer_batch100_IthacaAccount_ERC20SelfPay": "7366652", + "testERC20Transfer_batch100_ZerodevKernel": "12631318", + "testERC20Transfer_batch100_ZerodevKernel_ERC20SelfPay": "14149937", + "testNativeTransfer_AlchemyModularAccount": "180829", + "testNativeTransfer_CoinbaseSmartWallet": "178916", + "testNativeTransfer_IthacaAccount": "129551", + "testNativeTransfer_IthacaAccount_AppSponsor": "140096", + "testNativeTransfer_IthacaAccount_ERC20SelfPay": "137340", + "testNativeTransfer_Safe4337": "198595", + "testNativeTransfer_ZerodevKernel": "208635", + "testUniswapV2Swap_AlchemyModularAccount": "238647", + "testUniswapV2Swap_CoinbaseSmartWallet": "237451", + "testUniswapV2Swap_ERC4337MinimalAccount": "230691", + "testUniswapV2Swap_IthacaAccount": "187339", + "testUniswapV2Swap_IthacaAccount_AppSponsor": "197817", + "testUniswapV2Swap_IthacaAccount_ERC20SelfPay": "192628", + "testUniswapV2Swap_Safe4337": "257333", + "testUniswapV2Swap_ZerodevKernel": "266367" } \ No newline at end of file diff --git a/src/Escrow.sol b/src/Escrow.sol index 6138cab9..a924bc59 100644 --- a/src/Escrow.sol +++ b/src/Escrow.sol @@ -132,12 +132,12 @@ contract Escrow is IEscrow { for (uint256 i = 0; i < escrowIds.length; i++) { Escrow storage _escrow = escrows[escrowIds[i]]; // If refund timestamp hasn't passed yet, then the refund is invalid. - if (block.timestamp > _escrow.refundTimestamp || msg.sender == _escrow.recipient) { - _refundDepositor(escrowIds[i], _escrow); - _refundRecipient(escrowIds[i], _escrow); - } else { + if (block.timestamp <= _escrow.refundTimestamp) { revert RefundInvalid(); } + + _refundDepositor(escrowIds[i], _escrow); + _refundRecipient(escrowIds[i], _escrow); } } @@ -147,11 +147,10 @@ contract Escrow is IEscrow { for (uint256 i = 0; i < escrowIds.length; i++) { Escrow storage _escrow = escrows[escrowIds[i]]; // If refund timestamp hasn't passed yet, then the refund is invalid. - if (block.timestamp > _escrow.refundTimestamp || msg.sender == _escrow.depositor) { - _refundDepositor(escrowIds[i], _escrow); - } else { + if (block.timestamp <= _escrow.refundTimestamp) { revert RefundInvalid(); } + _refundDepositor(escrowIds[i], _escrow); } } @@ -182,11 +181,11 @@ contract Escrow is IEscrow { Escrow storage _escrow = escrows[escrowIds[i]]; // If settlement is still within the deadline, then refund is invalid. - if (block.timestamp > _escrow.refundTimestamp || msg.sender == _escrow.recipient) { - _refundRecipient(escrowIds[i], _escrow); - } else { + if (block.timestamp <= _escrow.refundTimestamp) { revert RefundInvalid(); } + + _refundRecipient(escrowIds[i], _escrow); } } diff --git a/src/GuardedExecutor.sol b/src/GuardedExecutor.sol index 4d655929..cf5eb502 100644 --- a/src/GuardedExecutor.sol +++ b/src/GuardedExecutor.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.23; +import {ERC7821} from "solady/accounts/ERC7821.sol"; import {LibSort} from "solady/utils/LibSort.sol"; import {LibBytes} from "solady/utils/LibBytes.sol"; import {LibZip} from "solady/utils/LibZip.sol"; @@ -12,7 +13,6 @@ import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; import {FixedPointMathLib as Math} from "solady/utils/FixedPointMathLib.sol"; import {DateTimeLib} from "solady/utils/DateTimeLib.sol"; import {ICallChecker} from "./interfaces/ICallChecker.sol"; -import {ERC7821Ithaca as ERC7821} from "./libraries/ERC7821Ithaca.sol"; /// @title GuardedExecutor /// @notice Mixin for spend limits and calldata execution guards. @@ -401,7 +401,7 @@ abstract contract GuardedExecutor is ERC7821 { checkKeyHashIsNonZero(keyHash) { if (keyHash != ANY_KEYHASH) { - if (_isSuperAdmin(keyHash)) revert SuperAdminCanExecuteEverything(); + if (_isSuperAdmin(keyHash)) revert SuperAdminCanSpendAnything(); } // It is ok even if we don't check for `_isSelfExecute` here, as we will still @@ -698,10 +698,10 @@ abstract contract GuardedExecutor is ERC7821 { // Configurables //////////////////////////////////////////////////////////////////////// - /// @dev To be overridden to return if `keyHash` corresponds to a super admin key. + /// @dev To be overriden to return if `keyHash` corresponds to a super admin key. function _isSuperAdmin(bytes32 keyHash) internal view virtual returns (bool); - /// @dev To be overridden to return the storage slot seed for a `keyHash`. + /// @dev To be overriden to return the storage slot seed for a `keyHash`. function _getGuardedExecutorKeyStorageSeed(bytes32 keyHash) internal view diff --git a/src/IthacaAccount.sol b/src/IthacaAccount.sol index a8ffdbca..f770cabc 100644 --- a/src/IthacaAccount.sol +++ b/src/IthacaAccount.sol @@ -275,8 +275,8 @@ contract IthacaAccount is IIthacaAccount, EIP712, GuardedExecutor { (bool isValid, bytes32 keyHash) = unwrapAndValidateSignature(digest, signature); if (LibBit.and(keyHash != 0, isValid)) { - isValid = - _isSuperAdmin(keyHash) || _getKeyExtraStorage(keyHash).checkers.contains(msg.sender); + isValid = _isSuperAdmin(keyHash) + || _getKeyExtraStorage(keyHash).checkers.contains(msg.sender); } // `bytes4(keccak256("isValidSignature(bytes32,bytes)")) = 0x1626ba7e`. // We use `0xffffffff` for invalid, in convention with the reference implementation. @@ -400,12 +400,7 @@ contract IthacaAccount is IIthacaAccount, EIP712, GuardedExecutor { } /// @dev Returns arrays of all (non-expired) authorized keys and their hashes. - function getKeys() - public - view - virtual - returns (Key[] memory keys, bytes32[] memory keyHashes) - { + function getKeys() public view virtual returns (Key[] memory keys, bytes32[] memory keyHashes) { uint256 totalCount = keyCount(); keys = new Key[](totalCount); @@ -619,9 +614,8 @@ contract IthacaAccount is IIthacaAccount, EIP712, GuardedExecutor { // `keccak256(abi.encode(key.keyType, keccak256(key.publicKey)))`. keyHash = hash(key); AccountStorage storage $ = _getAccountStorage(); - $.keyStorage[keyHash].set( - abi.encodePacked(key.publicKey, key.expiry, key.keyType, key.isSuperAdmin) - ); + $.keyStorage[keyHash] + .set(abi.encodePacked(key.publicKey, key.expiry, key.keyType, key.isSuperAdmin)); $.keyHashes.add(keyHash); } @@ -669,12 +663,10 @@ contract IthacaAccount is IIthacaAccount, EIP712, GuardedExecutor { if or(shr(64, t), lt(encodedIntent.length, 0x20)) { revert(0x00, 0x00) } } - if ( - !LibBit.and( + if (!LibBit.and( msg.sender == ORCHESTRATOR, LibBit.or(intent.eoa == address(this), intent.payer == address(this)) - ) - ) { + )) { revert Unauthorized(); } diff --git a/src/MultiSigSigner.sol b/src/MultiSigSigner.sol index 6f6fb272..73d82df9 100644 --- a/src/MultiSigSigner.sol +++ b/src/MultiSigSigner.sol @@ -12,7 +12,7 @@ contract MultiSigSigner is ISigner { //////////////////////////////////////////////////////////////////////// /// @dev The magic value returned by `isValidSignatureWithKeyHash` when the signature is valid. - /// - Calculated as: bytes4(keccak256("isValidSignatureWithKeyHash(bytes32,bytes32,bytes)") + /// - Calcualated as: bytes4(keccak256("isValidSignatureWithKeyHash(bytes32,bytes32,bytes)") bytes4 internal constant _MAGIC_VALUE = 0x8afc93b4; /// @dev The magic value returned by `isValidSignatureWithKeyHash` when the signature is invalid. @@ -175,7 +175,7 @@ contract MultiSigSigner is ISigner { /// for each owner key hash in the config. /// - Signature of a multi-sig should be encoded as abi.encode(bytes[] memory ownerSignatures) /// - For efficiency, place the signatures in the same order as the ownerKeyHashes in the config. - /// - Failing owner signatures are ignored, as long as valid signatures > threshold. + /// - Failing owner signatures are ignored, as long as valid signaturs > threshold. function isValidSignatureWithKeyHash(bytes32 digest, bytes32 keyHash, bytes memory signature) public view diff --git a/src/Orchestrator.sol b/src/Orchestrator.sol index 72815450..06586bcc 100644 --- a/src/Orchestrator.sol +++ b/src/Orchestrator.sol @@ -20,7 +20,6 @@ import {ICommon} from "./interfaces/ICommon.sol"; import {IFunder} from "./interfaces/IFunder.sol"; import {ISettler} from "./interfaces/ISettler.sol"; import {MerkleProofLib} from "solady/utils/MerkleProofLib.sol"; -import {IntentHelpers} from "./libraries/IntentHelpers.sol"; /// @title Orchestrator /// @notice Enables atomic verification, gas compensation and execution across eoas. @@ -42,13 +41,7 @@ import {IntentHelpers} from "./libraries/IntentHelpers.sol"; /// This means once an Intent is signed, it is infeasible to /// alter or rearrange it to force it to fail. -contract Orchestrator is - IOrchestrator, - EIP712, - CallContextChecker, - ReentrancyGuardTransient, - IntentHelpers -{ +contract Orchestrator is IOrchestrator, EIP712, CallContextChecker, ReentrancyGuardTransient { using LibERC7579 for bytes32[]; using EfficientHashLib for bytes32[]; using LibBitmap for LibBitmap.Bitmap; @@ -117,12 +110,12 @@ contract Orchestrator is /// @dev For EIP712 signature digest calculation for the `execute` function. bytes32 public constant INTENT_TYPEHASH = keccak256( - "Intent(Call[] calls,address eoa,uint256 nonce,address payer,address paymentToken,uint256 paymentMaxAmount,uint256 combinedGas,uint256 expiry)Call(address to,uint256 value,bytes data)" + "Intent(bool multichain,address eoa,Call[] calls,uint256 nonce,address payer,address paymentToken,uint256 paymentMaxAmount,uint256 combinedGas,bytes[] encodedPreCalls,bytes[] encodedFundTransfers,address settler,uint256 expiry)Call(address to,uint256 value,bytes data)" ); /// @dev For EIP712 signature digest calculation for SignedCalls bytes32 public constant SIGNED_CALL_TYPEHASH = keccak256( - "SignedCall(address eoa,Call[] calls,uint256 nonce)Call(address to,uint256 value,bytes data)" + "SignedCall(bool multichain,address eoa,Call[] calls,uint256 nonce)Call(address to,uint256 value,bytes data)" ); /// @dev For EIP712 signature digest calculation for the `execute` function. @@ -134,10 +127,6 @@ contract Orchestrator is /// This constant is a pun for "chain ID 0". uint16 public constant MULTICHAIN_NONCE_PREFIX = 0xc1d0; - /// @dev Nonce prefix to signal that the payload should use merkle verification. - /// This constant is "mv" in hex. - uint16 public constant MERKLE_VERIFICATION = 0x6D76; - /// @dev For ensuring that the remaining gas is sufficient for a self-call with /// overhead for cleaning up after the self-call. This also has an added benefit /// of preventing the censorship vector of calling `execute` in a very deep call-stack. @@ -206,7 +195,7 @@ contract Orchestrator is /// If sufficient gas is provided, returns an error selector that is non-zero /// if there is an error during the payment, verification, and call execution. function execute(bytes calldata encodedIntent) - external + public payable virtual nonReentrant @@ -222,6 +211,7 @@ contract Orchestrator is public payable virtual + nonReentrant returns (bytes4[] memory errs) { // This allocation and loop was initially in assembly, but I've normified it for now. @@ -230,7 +220,7 @@ contract Orchestrator is // We reluctantly use regular Solidity to access `encodedIntents[i]`. // This generates an unnecessary check for `i < encodedIntents.length`, but helps // generate all the implicit calldata bound checks on `encodedIntents[i]`. - errs[i] = this.execute(encodedIntents[i]); + (, errs[i]) = _execute(encodedIntents[i], 0, _NORMAL_MODE_FLAG); } } @@ -242,16 +232,11 @@ contract Orchestrator is /// But the balance of tx.origin has to be greater than or equal to type(uint192).max, to prove that a state override has been made offchain, /// and this is not an onchain call. This mode has been added so that receipt logs can be generated for `eth_simulateV1` /// @return gasUsed The amount of gas used by the execution. (Only returned if `isStateOverride` is true) - function simulateExecute(bytes calldata encodedIntent) external payable returns (uint256) { - bool isStateOverride; - uint256 combinedGasOverride; - assembly ("memory-safe") { - let endOfBytes := add(encodedIntent.offset, encodedIntent.length) - isStateOverride := calldataload(sub(endOfBytes, 0x40)) - combinedGasOverride := calldataload(sub(endOfBytes, 0x20)) - encodedIntent.length := sub(encodedIntent.length, 0x40) - } - + function simulateExecute( + bool isStateOverride, + uint256 combinedGasOverride, + bytes calldata encodedIntent + ) external payable returns (uint256) { // If Simulation Fails, then it will revert here. (uint256 gUsed, bytes4 err) = _execute(encodedIntent, combinedGasOverride, _SIMULATION_MODE_FLAG); @@ -275,7 +260,33 @@ contract Orchestrator is } } + /// @dev Extracts the Intent from the calldata bytes, with minimal checks. + function _extractIntent(bytes calldata encodedIntent) + internal + view + virtual + returns (Intent calldata i) + { + // This function does NOT allocate memory to avoid quadratic memory expansion costs. + // Otherwise, it will be unfair to the Intents at the back of the batch. + + // `dynamicStructInCalldata` internally performs out-of-bounds checks. + bytes calldata intentCalldata = LibBytes.dynamicStructInCalldata(encodedIntent, 0x00); + assembly ("memory-safe") { + i := intentCalldata.offset + } + // These checks are included for more safety: Swiss Cheese Model. + // Ensures that all the dynamic children in `encodedIntent` are contained. + LibBytes.checkInCalldata(i.executionData, intentCalldata); + LibBytes.checkInCalldata(i.encodedPreCalls, intentCalldata); + LibBytes.checkInCalldata(i.encodedFundTransfers, intentCalldata); + LibBytes.checkInCalldata(i.funderSignature, intentCalldata); + LibBytes.checkInCalldata(i.settlerContext, intentCalldata); + LibBytes.checkInCalldata(i.signature, intentCalldata); + LibBytes.checkInCalldata(i.paymentSignature, intentCalldata); + } /// @dev Extracts the PreCall from the calldata bytes, with minimal checks. + function _extractPreCall(bytes calldata encodedPreCall) internal virtual @@ -292,7 +303,7 @@ contract Orchestrator is /// @dev Executes a single encoded intent. /// @dev If flags is non-zero, then all errors are bubbled up. /// Currently there can only be 2 modes - simulation mode, and execution mode. - /// But we use a uint256 for efficient stack operations, and more flexibility in the future. + /// But we use a uint256 for efficient stack operations, and more flexiblity in the future. /// Note: We keep the flags in the stack/memory (TSTORE doesn't work) to make sure they are reset in each new call context, /// to provide protection against attacks which could spoof the execute function to believe it is in simulation mode. function _execute(bytes calldata encodedIntent, uint256 combinedGasOverride, uint256 flags) @@ -300,11 +311,12 @@ contract Orchestrator is virtual returns (uint256 gUsed, bytes4 err) { - uint256 g = Math.coalesce(uint96(combinedGasOverride), _getCombinedGas()); + Intent calldata i = _extractIntent(encodedIntent); + + uint256 g = Math.coalesce(uint96(combinedGasOverride), i.combinedGas); uint256 gStart = gasleft(); - address eoa = _getEoa(); - if (_getPaymentAmount() > _getPaymentMaxAmount()) { + if (i.paymentAmount > i.paymentMaxAmount) { err = PaymentError.selector; if (flags == _SIMULATION_MODE_FLAG) { @@ -316,20 +328,15 @@ contract Orchestrator is // Check if there's sufficient gas left for the gas-limited self calls // via the 63/64 rule. This is for gas estimation. If the total amount of gas // for the whole transaction is insufficient, revert. - uint256 gasAvailable = (gasleft() * 63) >> 6; - uint256 gasRequired = Math.saturatingAdd(g, _INNER_GAS_OVERHEAD); - - if (gasAvailable < gasRequired) { + if (((gasleft() * 63) >> 6) < Math.saturatingAdd(g, _INNER_GAS_OVERHEAD)) { if (flags != _SIMULATION_MODE_FLAG) { revert InsufficientGas(); } } } - address accountImpl = _getSupportedAccountImplementation(); - if (accountImpl != address(0)) { - address currentImpl = accountImplementationOf(eoa); - if (currentImpl != accountImpl) { + if (i.supportedAccountImplementation != address(0)) { + if (accountImplementationOf(i.eoa) != i.supportedAccountImplementation) { err = UnsupportedAccountImplementation.selector; if (flags == _SIMULATION_MODE_FLAG) { revert UnsupportedAccountImplementation(); @@ -337,16 +344,18 @@ contract Orchestrator is } } - address payer = Math.coalesce(_getPayer(), eoa); + address payer = Math.coalesce(i.payer, i.eoa); // Early skip the entire pay-verify-call workflow if the payer lacks tokens, // so that less gas is wasted when the Intent fails. // For multi chain mode, we skip this check, as the funding happens inside the self call. - if (TokenTransferLib.balanceOf(_getPaymentToken(), payer) < _getPaymentAmount()) { - err = PaymentError.selector; + if (!i.isMultichain && LibBit.and(i.paymentAmount != 0, err == 0)) { + if (TokenTransferLib.balanceOf(i.paymentToken, payer) < i.paymentAmount) { + err = PaymentError.selector; - if (flags == _SIMULATION_MODE_FLAG) { - revert PaymentError(); + if (flags == _SIMULATION_MODE_FLAG) { + revert PaymentError(); + } } } @@ -355,14 +364,11 @@ contract Orchestrator is assembly ("memory-safe") { let m := mload(0x40) // Grab the free memory pointer. if iszero(err) { + // Copy the encoded user op to the memory to be ready to pass to the self call. + calldatacopy(add(m, 0x40), encodedIntent.offset, encodedIntent.length) mstore(m, 0x00000000) // `selfCallPayVerifyCall537021665()`. // The word after the function selector contains the simulation flags. mstore(add(m, 0x20), flags) - // Copy the encoded user op to the memory to be ready to pass to the self call. - // We skip adding the calldata offset and length since we don't read that, but - // add `flags` in the first slot. - calldatacopy(add(m, 0x60), encodedIntent.offset, encodedIntent.length) - mstore(0x00, 0) // Zeroize the return slot. // To prevent griefing, we need to do a non-reverting gas-limited self call. @@ -374,27 +380,26 @@ contract Orchestrator is address(), 0, add(m, 0x1c), - add(encodedIntent.length, 0x44), + add(encodedIntent.length, 0x24), 0x00, 0x20 ) err := mload(0x00) // The self call will do another self call to execute. - } - } - assembly ("memory-safe") { - if iszero(selfCallSuccess) { - // If it is a simulation, we simply revert with the full error. - if eq(flags, _SIMULATION_MODE_FLAG) { - returndatacopy(mload(0x40), 0x00, returndatasize()) - revert(mload(0x40), returndatasize()) - } - // If we don't get an error selector, then we set this one. - if iszero(err) { err := shl(224, 0xad4db224) } // `VerifiedCallError()`. + if iszero(selfCallSuccess) { + // If it is a simulation, we simply revert with the full error. + if eq(flags, _SIMULATION_MODE_FLAG) { + returndatacopy(mload(0x40), 0x00, returndatasize()) + revert(mload(0x40), returndatasize()) + } + + // If we don't get an error selector, then we set this one. + if iszero(err) { err := shl(224, 0xad4db224) } // `VerifiedCallError()`. + } } } - emit IntentExecuted(_getEoa(), _getNonce(), selfCallSuccess, err); + emit IntentExecuted(i.eoa, i.nonce, selfCallSuccess, err); if (selfCallSuccess) { gUsed = Math.rawSub(gStart, gasleft()); } @@ -427,36 +432,25 @@ contract Orchestrator is function selfCallPayVerifyCall537021665() public payable { require(msg.sender == address(this)); + Intent calldata i; uint256 flags; assembly ("memory-safe") { + i := add(0x24, calldataload(0x24)) flags := calldataload(0x04) } // Check if intent has expired (only if expiry is set) // If expiry timestamp is set to 0, then expiry is considered to be infinite. - { - uint256 expiry = _getExpiry(); - if (expiry != 0 && block.timestamp > expiry) { - revert IntentExpired(); - } + if (i.expiry != 0 && block.timestamp > i.expiry) { + revert IntentExpired(); } - address eoa = _getEoa(); - - // Start a calldata pointer to traverse all the inner dynamic bytes within. Every time we get the next dynamic bytes, - // the pointer is advanced. A memory pointer gives some efficiency and will produce relatively clean top-level code. - CalldataPointer memory ptr; - bytes32 digest = _computeDigest(ptr); + address eoa = i.eoa; + uint256 nonce = i.nonce; + bytes32 digest = _computeDigest(i); - { - bytes calldata fundData = _getNextBytes(ptr); + _fund(eoa, i.funder, digest, i.encodedFundTransfers, i.funderSignature); - if (fundData.length > 0) { - (address funder, bytes calldata sig, bytes[] calldata transfers) = - _parseFundData(fundData); - _fund(_getEoa(), funder, digest, transfers, sig); - } - } // The chicken and egg problem: // A off-chain simulation of a successful Intent may not guarantee on-chain success. // The state may change in the window between simulation and actual on-chain execution. @@ -472,82 +466,57 @@ contract Orchestrator is // simulation, and suggests banning users that intentionally grief the simulation. // Handle the sub Intents after initialize (if any), and before the `_verify`. - { - bytes calldata preCallsBytes = _getNextBytes(ptr); - if (preCallsBytes.length > 0) { - _handlePreCalls(eoa, _getPayer(), flags, preCallsBytes); - } - } + if (i.encodedPreCalls.length != 0) _handlePreCalls(eoa, i.payer, flags, i.encodedPreCalls); + // If `_verify` is invalid, just revert. // The verification gas is determined by `executionData` and the account logic. // Off-chain simulation of `_verify` should suffice, provided that the eoa's // account is not changed, and the `keyHash` is not revoked // in the window between off-chain simulation and on-chain execution. + + bool isValid; bytes32 keyHash; - { - bool isValid; - bytes calldata signature = _getNextBytes(ptr); - - uint256 nonce = _getNonce(); - (isValid, keyHash) = _verify(digest, eoa, signature); - - if (nonce >> 240 == MERKLE_VERIFICATION) { - bytes calldata settlerData = _getNextBytes(ptr); - // If this is an output intent, then send the digest as the settlementId - // on all input chains. - if (settlerData.length > 0) { - // Output intent - first 32 bytes of settler data contains the settler address - // Then, it contains 2 offsets then the real data - ISettler(address(uint160(uint256(bytes32(settlerData[:32]))))) - .send(digest, settlerData[96:]); - } + if (i.isMultichain) { + // For multi chain intents, we have to verify using merkle sigs. + (isValid, keyHash) = _verifyMerkleSig(digest, eoa, i.signature); + + // If this is an output intent, then send the digest as the settlementId + // on all input chains. + if (i.encodedFundTransfers.length > 0) { + // Output intent + ISettler(i.settler).send(digest, i.settlerContext); } + } else { + (isValid, keyHash) = _verify(digest, eoa, i.signature); + } - if (flags == _SIMULATION_MODE_FLAG) { - isValid = true; - } + if (flags == _SIMULATION_MODE_FLAG) { + isValid = true; + } - if (!isValid) { - revert VerificationError(); - } + if (!isValid) revert VerificationError(); - _checkAndIncrementNonce(eoa, nonce); - } + _checkAndIncrementNonce(eoa, nonce); // Payment // If `_pay` fails, just revert. // Off-chain simulation of `_pay` should suffice, // provided that the token balance does not decrease in the window between // off-chain simulation and on-chain execution. - { - uint256 paymentAmount = _getPaymentAmount(); - bytes calldata paymentSignature = _getNextBytes(ptr); - if (paymentAmount != 0) { - _pay( - paymentAmount, - keyHash, - digest, - eoa, - _getPayer(), - _getPaymentToken(), - _getPaymentRecipient(), - paymentSignature - ); - } - } + if (i.paymentAmount != 0) _pay(keyHash, digest, i); // This re-encodes the ERC7579 `executionData` with the optional `opData`. // We expect that the account supports ERC7821 // (an extension of ERC7579 tailored for 7702 accounts). - bytes memory executeData = LibERC7579.reencodeBatchAsExecuteCalldata( + bytes memory data = LibERC7579.reencodeBatchAsExecuteCalldata( hex"01000000000078210001", // ERC7821 batch execution mode. - _getExecutionData(), + i.executionData, abi.encode(keyHash) // `opData`. ); assembly ("memory-safe") { mstore(0x00, 0) // Zeroize the return slot. - if iszero(call(gas(), eoa, 0, add(0x20, executeData), mload(executeData), 0x00, 0x20)) { + if iszero(call(gas(), eoa, 0, add(0x20, data), mload(data), 0x00, 0x20)) { if eq(flags, _SIMULATION_MODE_FLAG) { returndatacopy(mload(0x40), 0x00, returndatasize()) revert(mload(0x40), returndatasize()) @@ -570,15 +539,10 @@ contract Orchestrator is address parentEOA, address payer, uint256 flags, - bytes calldata encodedPreCalls + bytes[] calldata encodedPreCalls ) internal virtual { - bytes[] calldata calls; - assembly ("memory-safe") { - calls.length := calldataload(add(encodedPreCalls.offset, 0x20)) - calls.offset := add(encodedPreCalls.offset, 0x40) - } - for (uint256 j; j < calls.length; ++j) { - SignedCall calldata p = _extractPreCall(calls[j]); + for (uint256 j; j < encodedPreCalls.length; ++j) { + SignedCall calldata p = _extractPreCall(encodedPreCalls[j]); address eoa = Math.coalesce(p.eoa, parentEOA); uint256 nonce = p.nonce; @@ -634,6 +598,28 @@ contract Orchestrator is // Multi Chain Functions //////////////////////////////////////////////////////////////////////// + /// @dev Verifies the merkle sig for the multi chain intents. + /// - Note: Each leaf of the merkle tree should be a standard intent digest, computed with chainId. + /// - Leaf intents do NOT need to have the multichain nonce prefix. + /// - The signature for multi chain intents using merkle verification is encoded as: + /// - bytes signature = abi.encode(bytes32[] memory proof, bytes32 root, bytes memory rootSig) + function _verifyMerkleSig(bytes32 digest, address eoa, bytes memory signature) + internal + view + returns (bool isValid, bytes32 keyHash) + { + (bytes32[] memory proof, bytes32 root, bytes memory rootSig) = + abi.decode(signature, (bytes32[], bytes32, bytes)); + + if (MerkleProofLib.verify(proof, root, digest)) { + (isValid, keyHash) = IIthacaAccount(eoa).unwrapAndValidateSignature(root, rootSig); + + return (isValid, keyHash); + } + + return (false, bytes32(0)); + } + /// @dev Funds the eoa with with the encoded fund transfers, before executing the intent. /// - For ERC20 tokens, the funder needs to approve the orchestrator to pull funds. /// - For native assets like ETH, the funder needs to transfer the funds to the orchestrator @@ -643,7 +629,7 @@ contract Orchestrator is address eoa, address funder, bytes32 digest, - bytes[] calldata encodedFundTransfers, + bytes[] memory encodedFundTransfers, bytes memory funderSignature ) internal virtual { // Note: The fund function is mostly only used in the multi chain mode. @@ -678,59 +664,51 @@ contract Orchestrator is /// @dev Makes the `eoa` perform a payment to the `paymentRecipient` directly. /// This reverts if the payment is insufficient or fails. Otherwise returns nothing. - function _pay( - uint256 paymentAmount, - bytes32 keyHash, - bytes32 intentDigest, - address eoa, - address payer, - address paymentToken, - address paymentRecipient, - bytes calldata paymentSignature - ) internal virtual { - uint256 currentBalance = TokenTransferLib.balanceOf(paymentToken, paymentRecipient); + function _pay(bytes32 keyHash, bytes32 digest, Intent calldata i) internal virtual { + uint256 paymentAmount = i.paymentAmount; + uint256 requiredBalanceAfter = Math.saturatingAdd( + TokenTransferLib.balanceOf(i.paymentToken, i.paymentRecipient), paymentAmount + ); - uint256 requiredBalanceAfter = Math.saturatingAdd(currentBalance, paymentAmount); + address payer = Math.coalesce(i.payer, i.eoa); // Call the pay function on the account contract // Equivalent Solidity code: // IIthacaAccount(payer).pay(paymentAmount, keyHash, digest, abi.encode(i)); // Gas Savings: // Saves ~2k gas for normal use cases, by avoiding abi.encode and solidity external call overhead - address callee = Math.coalesce(payer, eoa); - assembly ("memory-safe") { let m := mload(0x40) // Load the free memory pointer - mstore(m, 0x38e11b2a) // `pay(uint256,bytes32,bytes32,address,address,address,address,bytes)` - mstore(add(m, 0x20), paymentAmount) // Add paymentAmount as first param + mstore(m, 0xf81d87a7) // `pay(uint256,bytes32,bytes32,bytes)` + mstore(add(m, 0x20), paymentAmount) // Add payment amount as first param mstore(add(m, 0x40), keyHash) // Add keyHash as second param - mstore(add(m, 0x60), intentDigest) // Add intentDigest as third param - mstore(add(m, 0x80), eoa) // Add eoa as fourth param - mstore(add(m, 0xa0), payer) // Add payer as fifth param - mstore(add(m, 0xc0), paymentToken) // Add paymentToken as sixth param - mstore(add(m, 0xe0), paymentRecipient) // Add paymentRecipient as seventh param - mstore(add(m, 0x100), 0x100) // Add offset for paymentSignature as eighth param + mstore(add(m, 0x60), digest) // Add digest as third param + mstore(add(m, 0x80), 0x80) // Add offset of encoded Intent as third param + + let encodedSize := sub(calldatasize(), i) - // Store paymentSignature length and data - mstore(add(m, 0x120), paymentSignature.length) - calldatacopy(add(m, 0x140), paymentSignature.offset, paymentSignature.length) + mstore(add(m, 0xa0), add(encodedSize, 0x20)) // Store length of encoded Intent at offset. + mstore(add(m, 0xc0), 0x20) // Offset at which the Intent struct starts in encoded Intent. + + // Copy the intent data to memory + calldatacopy(add(m, 0xe0), i, encodedSize) // We revert here, so that if the payment fails, the execution is also reverted. // The revert for payment is caught inside the selfCallPayVerify function. if iszero( call( gas(), // gas - callee, // address + payer, // address 0, // value add(m, 0x1c), // input memory offset - add(0x124, paymentSignature.length), // input size + add(0xc4, encodedSize), // input size 0x00, // output memory offset 0x20 // output size ) ) { revert(0x00, 0x20) } } - if (TokenTransferLib.balanceOf(paymentToken, paymentRecipient) < requiredBalanceAfter) { + if (TokenTransferLib.balanceOf(i.paymentToken, i.paymentRecipient) < requiredBalanceAfter) { revert PaymentError(); } } @@ -774,39 +752,42 @@ contract Orchestrator is /// @dev Computes the EIP712 digest for the PreCall. function _computeDigest(SignedCall calldata p) internal view virtual returns (bytes32) { + bool isMultichain = p.nonce >> 240 == MULTICHAIN_NONCE_PREFIX; // To avoid stack-too-deep. Faster than a regular Solidity array anyways. - bytes32[] memory f = EfficientHashLib.malloc(4); + bytes32[] memory f = EfficientHashLib.malloc(5); f.set(0, SIGNED_CALL_TYPEHASH); - f.set(1, uint160(p.eoa)); - f.set(2, _executionDataHash(p.executionData)); - f.set(3, p.nonce); + f.set(1, LibBit.toUint(isMultichain)); + f.set(2, uint160(p.eoa)); + f.set(3, _executionDataHash(p.executionData)); + f.set(4, p.nonce); - return p.nonce >> 240 == MULTICHAIN_NONCE_PREFIX - ? _hashTypedDataSansChainId(f.hash()) - : _hashTypedData(f.hash()); + return isMultichain ? _hashTypedDataSansChainId(f.hash()) : _hashTypedData(f.hash()); } - /// @dev Computes the EIP712 digest for the Intent - /// @dev Also updates the passed in CalldataPointer to point to the start of the next dynamic bytes. - function _computeDigest(CalldataPointer memory p) internal view virtual returns (bytes32) { - bytes32 digest = INTENT_TYPEHASH; + /// @dev Computes the EIP712 digest for the Intent. + /// If the the nonce starts with `MULTICHAIN_NONCE_PREFIX`, + /// the digest will be computed without the chain ID. + /// Otherwise, the digest will be computed with the chain ID. + function _computeDigest(Intent calldata i) internal view virtual returns (bytes32) { + bool isMultichain = i.nonce >> 240 == MULTICHAIN_NONCE_PREFIX; - assembly ("memory-safe") { - let fmp := mload(0x40) - mstore(fmp, digest) - let length := calldataload(_EXECUTION_DATA_OFFSET) - calldatacopy(add(fmp, 0x20), add(_EXECUTION_DATA_OFFSET, 0x20), length) - mstore(add(fmp, 0x20), keccak256(add(fmp, 0x20), length)) - calldatacopy(add(fmp, 0x40), _EOA_OFFSET, 224) // copy 7 words - digest := keccak256(fmp, 288) // hash 9 words, inc typehash - - // update the offset for the memory ptr - mstore(p, add(add(_EXECUTION_DATA_OFFSET, 0x20), length)) - } - - return _getNonce() >> 240 == MULTICHAIN_NONCE_PREFIX - ? _hashTypedDataSansChainId(digest) - : _hashTypedData(digest); + // To avoid stack-too-deep. Faster than a regular Solidity array anyways. + bytes32[] memory f = EfficientHashLib.malloc(13); + f.set(0, INTENT_TYPEHASH); + f.set(1, LibBit.toUint(isMultichain)); + f.set(2, uint160(i.eoa)); + f.set(3, _executionDataHash(i.executionData)); + f.set(4, i.nonce); + f.set(5, uint160(i.payer)); + f.set(6, uint160(i.paymentToken)); + f.set(7, i.paymentMaxAmount); + f.set(8, i.combinedGas); + f.set(9, _encodedArrHash(i.encodedPreCalls)); + f.set(10, _encodedArrHash(i.encodedFundTransfers)); + f.set(11, uint160(i.settler)); + f.set(12, i.expiry); + + return isMultichain ? _hashTypedDataSansChainId(f.hash()) : _hashTypedData(f.hash()); } /// @dev Helper function to return the hash of the `execuctionData`. @@ -859,7 +840,7 @@ contract Orchestrator is returns (string memory name, string memory version) { name = "Orchestrator"; - version = "0.5.6"; + version = "0.5.5"; } //////////////////////////////////////////////////////////////////////// diff --git a/src/SimpleFunder.sol b/src/SimpleFunder.sol index 7109f028..e74947fb 100644 --- a/src/SimpleFunder.sol +++ b/src/SimpleFunder.sol @@ -58,7 +58,7 @@ contract SimpleFunder is EIP712, Ownable, IFunder { returns (string memory name, string memory version) { name = "SimpleFunder"; - version = "0.1.9"; + version = "0.1.8"; } //////////////////////////////////////////////////////////////////////// @@ -133,10 +133,11 @@ contract SimpleFunder is EIP712, Ownable, IFunder { /// @dev Allows the orchestrator to fund an account. /// The `digest` includes the intent nonce and the transfers. - function fund(bytes32 digest, ICommon.Transfer[] memory transfers, bytes memory funderSignature) - public - override - { + function fund( + bytes32 digest, + ICommon.Transfer[] memory transfers, + bytes memory funderSignature + ) public override { if (!orchestrators[msg.sender]) { revert OnlyOrchestrator(); } @@ -182,7 +183,7 @@ contract SimpleFunder is EIP712, Ownable, IFunder { if gt(amount, allowance) { mstore(m, 0x095ea7b3) // `approve(address,uint256)`. mstore(add(m, 0x20), caller()) - mstore(add(m, 0x40), 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) // 20-byte all-ones sentinel (2^160-1), not uint256 max + mstore(add(m, 0x40), 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) // type(uint256).max // Orchestrator checks for token transfer success, so we don't need to check it here. pop(call(gas(), token, 0, add(m, 0x1c), 0x44, 0x00, 0x00)) } diff --git a/src/Simulator.sol b/src/Simulator.sol index 2a5e2a76..1e468d6b 100644 --- a/src/Simulator.sol +++ b/src/Simulator.sol @@ -4,12 +4,10 @@ pragma solidity ^0.8.23; import {ICommon} from "./interfaces/ICommon.sol"; import {IMulticall3} from "./interfaces/IMulticall3.sol"; import {FixedPointMathLib as Math} from "solady/utils/FixedPointMathLib.sol"; -import {MockOrchestrator} from "../test/utils/mocks/MockOrchestrator.sol"; -import {IntentHelpers} from "./libraries/IntentHelpers.sol"; /// @title Simulator /// @notice A separate contract for calling the Orchestrator contract solely for gas simulation. -contract Simulator is IntentHelpers { +contract Simulator { //////////////////////////////////////////////////////////////////////// // EIP-5267 Support //////////////////////////////////////////////////////////////////////// @@ -56,23 +54,15 @@ contract Simulator is IntentHelpers { /// @dev Updates the payment amounts for the Intent passed in. function _updatePaymentAmounts( - bytes memory u, + ICommon.Intent memory u, uint256 gas, uint8 paymentPerGasPrecision, uint256 paymentPerGas ) internal pure { uint256 paymentAmount = Math.fullMulDiv(gas, paymentPerGas, 10 ** paymentPerGasPrecision); - // 36 because we don't have the calldata offset, or the function selector - uint256 paymentOffset = _PAYMENT_AMOUNT_OFFSET - 36; - uint256 paymentMaxOffset = _PAYMENT_MAX_AMOUNT_OFFSET - 36; - assembly ("memory-safe") { - let currentPaymentAmount := mload(add(u, paymentOffset)) - mstore(add(u, paymentOffset), add(currentPaymentAmount, paymentAmount)) - - let currentPaymentMaxAmount := mload(add(u, paymentMaxOffset)) - mstore(add(u, paymentMaxOffset), add(currentPaymentMaxAmount, paymentAmount)) - } + u.paymentAmount += paymentAmount; + u.paymentMaxAmount += paymentAmount; } /// @dev Performs a call to the Orchestrator, and returns the gas used by the Intent. @@ -111,8 +101,10 @@ contract Simulator is IntentHelpers { bytes calldata encodedIntent ) internal freeTempMemory returns (uint256) { bytes memory data = abi.encodeWithSignature( - "simulateExecute(bytes)", - abi.encodePacked(encodedIntent, isStateOverride, combinedGasOverride) + "simulateExecute(bool,uint256,bytes)", + isStateOverride, + combinedGasOverride, + encodedIntent ); return _callOrchestrator(oc, isStateOverride, data); } @@ -123,10 +115,13 @@ contract Simulator is IntentHelpers { address oc, bool isStateOverride, uint256 combinedGasOverride, - bytes memory u + ICommon.Intent memory u ) internal freeTempMemory returns (uint256) { bytes memory data = abi.encodeWithSignature( - "simulateExecute(bytes)", abi.encodePacked(u, isStateOverride, combinedGasOverride) + "simulateExecute(bool,uint256,bytes)", + isStateOverride, + combinedGasOverride, + abi.encode(u) ); return _callOrchestrator(oc, isStateOverride, data); } @@ -262,7 +257,7 @@ contract Simulator is IntentHelpers { /// @dev The closer this number is to 10_000, the more precise combined gas will be. But more iterations will be needed. /// @dev This number should always be > 10_000, to get correct results. //// If the increment is too small, the function might run out of gas while finding the combined gas value. - /// @param calldataIntent The encoded intent + /// @param encodedIntent The encoded user operation /// @return gasUsed The gas used in the successful simulation /// @return combinedGas The first combined gas value that gives a successful simulation. /// This function reverts if the primary simulation run with max combinedGas fails. @@ -273,10 +268,10 @@ contract Simulator is IntentHelpers { uint8 paymentPerGasPrecision, uint256 paymentPerGas, uint256 combinedGasIncrement, - bytes calldata calldataIntent + bytes calldata encodedIntent ) public payable virtual returns (uint256 gasUsed, uint256 combinedGas) { // 1. Primary Simulation Run to get initial gasUsed value with combinedGasOverride - gasUsed = _callOrchestratorCalldata(oc, false, type(uint256).max, calldataIntent); + gasUsed = _callOrchestratorCalldata(oc, false, type(uint256).max, encodedIntent); // If the simulation failed, bubble up the full revert. assembly ("memory-safe") { @@ -287,21 +282,15 @@ contract Simulator is IntentHelpers { } } - bytes memory memoryIntent = calldataIntent; + // Update payment amounts using the gasUsed value + ICommon.Intent memory u = abi.decode(encodedIntent, (ICommon.Intent)); - // 36 because we don't have the calldata offset, or the function selector - uint256 offset = _COMBINED_GAS_OFFSET - 36; - assembly ("memory-safe") { - let currentCombinedGas := mload(add(memoryIntent, offset)) - let newCombinedGas := add(currentCombinedGas, gasUsed) - mstore(add(memoryIntent, offset), newCombinedGas) - combinedGas := newCombinedGas - } + u.combinedGas += gasUsed; - _updatePaymentAmounts(memoryIntent, combinedGas, paymentPerGasPrecision, paymentPerGas); + _updatePaymentAmounts(u, u.combinedGas, paymentPerGasPrecision, paymentPerGas); while (true) { - gasUsed = _callOrchestratorMemory(oc, false, 0, memoryIntent); + gasUsed = _callOrchestratorMemory(oc, false, 0, u); // If the simulation failed, bubble up the full revert. assembly ("memory-safe") { @@ -316,34 +305,22 @@ contract Simulator is IntentHelpers { } if (gasUsed != 0) { - // Get current combinedGas from bytes - assembly ("memory-safe") { - combinedGas := mload(add(memoryIntent, offset)) - } - return (gasUsed, combinedGas); + return (gasUsed, u.combinedGas); } - // Get current combinedGas and calculate increment - assembly ("memory-safe") { - combinedGas := mload(add(memoryIntent, offset)) - } - uint256 gasIncrement = Math.mulDiv(combinedGas, combinedGasIncrement, 10_000); + uint256 gasIncrement = Math.mulDiv(u.combinedGas, combinedGasIncrement, 10_000); - _updatePaymentAmounts(memoryIntent, gasIncrement, paymentPerGasPrecision, paymentPerGas); + _updatePaymentAmounts(u, gasIncrement, paymentPerGasPrecision, paymentPerGas); - // Update combinedGas with increment using assembly - assembly ("memory-safe") { - let currentCombinedGas := mload(add(memoryIntent, offset)) - let newCombinedGas := add(currentCombinedGas, gasIncrement) - mstore(add(memoryIntent, offset), newCombinedGas) - } + // Step up the combined gas, until we see a simulation passing + u.combinedGas += gasIncrement; } } /// @dev Same as simulateCombinedGas, but with an additional verification run /// that generates a successful non reverting state override simulation. /// Which can be used in eth_simulateV1 to get the trace.\ - /// @param combinedGasVerificationOffset is a static value that is added after a successful combinedGas is found. + /// @param combinedGasVerificationOffset is a static value that is added after a succesful combinedGas is found. /// This can be used to account for variations in sig verification gas, for keytypes like P256. /// @param paymentPerGasPrecision The precision of the payment per gas value. /// paymentAmount = gas * paymentPerGas / (10 ** paymentPerGasPrecision) @@ -361,18 +338,15 @@ contract Simulator is IntentHelpers { combinedGas += combinedGasVerificationOffset; - bytes memory encodedIntentCopy = encodedIntent; + ICommon.Intent memory u = abi.decode(encodedIntent, (ICommon.Intent)); - _updatePaymentAmounts(encodedIntentCopy, combinedGas, paymentPerGasPrecision, paymentPerGas); + _updatePaymentAmounts(u, combinedGas, paymentPerGasPrecision, paymentPerGas); - // 36 because we don't have the calldata offset, or the function selector - uint256 offset = _COMBINED_GAS_OFFSET - 36; - assembly ("memory-safe") { - mstore(add(encodedIntentCopy, offset), combinedGas) - } + u.combinedGas = combinedGas; // Verification Run to generate the logs with the correct combinedGas and payment amounts. - gasUsed = _callOrchestratorMemory(oc, true, 0, encodedIntentCopy); + gasUsed = _callOrchestratorMemory(oc, true, 0, u); + // If the simulation failed, bubble up full revert assembly ("memory-safe") { if iszero(gasUsed) { diff --git a/src/interfaces/ICommon.sol b/src/interfaces/ICommon.sol index 9057b1f3..54ccf388 100644 --- a/src/interfaces/ICommon.sol +++ b/src/interfaces/ICommon.sol @@ -2,6 +2,86 @@ pragma solidity ^0.8.23; interface ICommon { + //////////////////////////////////////////////////////////////////////// + // Data Structures + //////////////////////////////////////////////////////////////////////// + /// @dev A struct to hold the intent fields. + /// Since L2s already include calldata compression with savings forwarded to users, + /// we don't need to be too concerned about calldata overhead + struct Intent { + //////////////////////////////////////////////////////////////////////// + // EIP-712 Fields + //////////////////////////////////////////////////////////////////////// + /// @dev The user's address. + address eoa; + /// @dev An encoded array of calls, using ERC7579 batch execution encoding. + /// `abi.encode(calls)`, where `calls` is of type `Call[]`. + /// This allows for more efficient safe forwarding to the EOA. + bytes executionData; + /// @dev Per delegated EOA. + /// This nonce is a 4337-style 2D nonce with some specializations: + /// - Upper 192 bits are used for the `seqKey` (sequence key). + /// The upper 16 bits of the `seqKey` is `MULTICHAIN_NONCE_PREFIX`, + /// then the Intent EIP712 hash will exclude the chain ID. + /// - Lower 64 bits are used for the sequential nonce corresponding to the `seqKey`. + uint256 nonce; + /// @dev The account paying the payment token. + /// If this is `address(0)`, it defaults to the `eoa`. + address payer; + /// @dev The ERC20 or native token used to pay for gas. + address paymentToken; + /// @dev The maximum amount of the token to pay. + uint256 paymentMaxAmount; + /// @dev The combined gas limit for payment, verification, and calling the EOA. + uint256 combinedGas; + /// @dev Optional array of encoded SignedCalls that will be verified and executed + /// before the validation of the overall Intent. + /// A PreCall will NOT have its gas limit or payment applied. + /// The overall Intent's gas limit and payment will be applied, encompassing all its PreCalls. + /// The execution of a PreCall will check and increment the nonce in the PreCall. + /// If at any point, any PreCall cannot be verified to be correct, or fails in execution, + /// the overall Intent will revert before validation, and execute will return a non-zero error. + bytes[] encodedPreCalls; + /// @dev Only relevant for multi chain intents. + /// There should not be any duplicate token addresses. Use address(0) for native token. + /// If native token is used, the first transfer should be the native token transfer. + /// If encodedFundTransfers is not empty, then the intent is considered the output intent. + bytes[] encodedFundTransfers; + /// @dev The settler address. + address settler; + /// @dev The expiry timestamp for the intent. The intent is invalid after this timestamp. + /// If expiry timestamp is set to 0, then expiry is considered to be infinite. + uint256 expiry; + //////////////////////////////////////////////////////////////////////// + // Additional Fields (Not included in EIP-712) + //////////////////////////////////////////////////////////////////////// + /// @dev Whether the intent should use the multichain mode - i.e verify with merkle sigs + /// and send the cross chain message. + bool isMultichain; + /// @dev The funder address. + address funder; + /// @dev The funder signature. + bytes funderSignature; + /// @dev The settler context data to be passed to the settler. + bytes settlerContext; + /// @dev The actual payment amount, requested by the filler. MUST be less than or equal to `paymentMaxAmount` + uint256 paymentAmount; + /// @dev The payment recipient for the ERC20 token. + /// Excluded from signature. The filler can replace this with their own address. + /// This enables multiple fillers, allowing for competitive filling, better uptime. + address paymentRecipient; + /// @dev The wrapped signature. + /// `abi.encodePacked(innerSignature, keyHash, prehash)`. + bytes signature; + /// @dev Optional payment signature to be passed into the `compensate` function + /// on the `payer`. This signature is NOT included in the EIP712 signature. + bytes paymentSignature; + /// @dev Optional. If non-zero, the EOA must use `supportedAccountImplementation`. + /// Otherwise, if left as `address(0)`, any EOA implementation will be supported. + /// This field is NOT included in the EIP712 signature. + address supportedAccountImplementation; + } + /// @dev A struct to hold the fields for a SignedCall. /// A SignedCall is a struct that contains a signed execution batch along with the nonce // and address of the user. diff --git a/src/interfaces/IFunder.sol b/src/interfaces/IFunder.sol index ddbacc0f..90c2547f 100644 --- a/src/interfaces/IFunder.sol +++ b/src/interfaces/IFunder.sol @@ -16,6 +16,9 @@ interface IFunderV4 { interface IFunder is IFunderV4 { /// @dev Checks if fund transfers are valid given a funderSignature. /// @dev Funder implementations must revert if the signature is invalid. - function fund(bytes32 digest, ICommon.Transfer[] memory transfers, bytes memory funderSignature) - external; + function fund( + bytes32 digest, + ICommon.Transfer[] memory transfers, + bytes memory funderSignature + ) external; } diff --git a/src/interfaces/IIthacaAccount.sol b/src/interfaces/IIthacaAccount.sol index 9056db7c..099f466d 100644 --- a/src/interfaces/IIthacaAccount.sol +++ b/src/interfaces/IIthacaAccount.sol @@ -7,23 +7,14 @@ import {ICommon} from "../interfaces/ICommon.sol"; /// @notice Interface for the Account contract interface IIthacaAccount is ICommon { /// @dev Pays `paymentAmount` of `paymentToken` to the `paymentRecipient`. - /// @param paymentAmount The amount to pay /// @param keyHash The hash of the key used to authorize the operation + /// @param encodedIntent The encoded user operation /// @param intentDigest The digest of the user operation - /// @param eoa The EOA address - /// @param payer The payer address - /// @param paymentToken The token to pay with - /// @param paymentRecipient The recipient of the payment - /// @param paymentSignature The payment signature function pay( uint256 paymentAmount, bytes32 keyHash, bytes32 intentDigest, - address eoa, - address payer, - address paymentToken, - address paymentRecipient, - bytes calldata paymentSignature + bytes calldata encodedIntent ) external; /// @dev Returns if the signature is valid, along with its `keyHash`. diff --git a/src/interfaces/IMulticall3.sol b/src/interfaces/IMulticall3.sol new file mode 100644 index 00000000..c0ab86f2 --- /dev/null +++ b/src/interfaces/IMulticall3.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +/// @notice Interface for Multicall3 contract +interface IMulticall3 { + struct Call { + address target; + bytes callData; + } + + struct Call3 { + address target; + bool allowFailure; + bytes callData; + } + + struct Call3Value { + address target; + bool allowFailure; + uint256 value; + bytes callData; + } + + struct Result { + bool success; + bytes returnData; + } + + function aggregate(Call[] calldata calls) + external + payable + returns (uint256 blockNumber, bytes[] memory returnData); + + function aggregate3(Call3[] calldata calls) + external + payable + returns (Result[] memory returnData); +} diff --git a/src/interfaces/IOrchestrator.sol b/src/interfaces/IOrchestrator.sol index 05dfabb4..ffb4de93 100644 --- a/src/interfaces/IOrchestrator.sol +++ b/src/interfaces/IOrchestrator.sol @@ -27,10 +27,11 @@ interface IOrchestrator is ICommon { /// But the balance of tx.origin has to be greater than or equal to uint192.max, to prove that a state override has been made offchain, /// and this is not an onchain call. This mode has been added so that receipt logs can be generated for `eth_simulateV1` /// @return gasUsed The amount of gas used by the execution. (Only returned if `isStateOverride` is true) - function simulateExecute(bytes calldata encodedIntent) - external - payable - returns (uint256 gasUsed); + function simulateExecute( + bool isStateOverride, + uint256 combinedGasOverride, + bytes calldata encodedIntent + ) external payable returns (uint256 gasUsed); /// @dev Allows the orchestrator owner to withdraw tokens. /// @param token The token address (0 for native token) diff --git a/src/libraries/ERC7821Ithaca.sol b/src/libraries/ERC7821Ithaca.sol deleted file mode 100644 index 5f726b42..00000000 --- a/src/libraries/ERC7821Ithaca.sol +++ /dev/null @@ -1,347 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.4; - -import {Receiver} from "solady/accounts/Receiver.sol"; - -/// @notice Minimal batch executor mixin. -/// @author Solady (https://github.com/vectorized/solady/blob/main/src/accounts/ERC7821.sol) -/// -/// @dev This contract can be inherited to create fully-fledged smart accounts. -/// If you merely want to combine approve-swap transactions into a single transaction -/// using [EIP-7702](https://eips.ethereum.org/EIPS/eip-7702), you will need to implement basic -/// [ERC-1271](https://eips.ethereum.org/EIPS/eip-1271) `isValidSignature` functionality to -/// validate signatures with `ecrecover` against the EOA address. This is necessary because some -/// signature checks skip `ecrecover` if the signer has code. For a basic EOA batch executor, -/// please refer to [BEBE](https://github.com/vectorized/bebe), which inherits from this class. -contract ERC7821Ithaca is Receiver { - /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ - /* STRUCTS */ - /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ - - /// @dev Call struct for the `execute` function. - struct Call { - address to; // Replaced as `address(this)` if `address(0)`. Renamed to `to` for Ithaca Porto. - uint256 value; // Amount of native currency (i.e. Ether) to send. - bytes data; // Calldata to send with the call. - } - - struct CallSansTo { - uint256 value; // Amount of native currency (i.e. Ether) to send. - bytes data; // Calldata to send with the call. - } - - /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ - /* CUSTOM ERRORS */ - /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ - - /// @dev The execution mode is not supported. - error UnsupportedExecutionMode(); - - /// @dev Cannot decode `executionData` as a batch of batches `abi.encode(bytes[])`. - error BatchOfBatchesDecodingError(); - - /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ - /* EXECUTION OPERATIONS */ - /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ - - /// @dev Executes the calls in `executionData`. - /// Reverts and bubbles up error if any call fails. - /// - /// `executionData` encoding (single batch): - /// - If `opData` is empty, `executionData` is simply `abi.encode(calls)`. - /// - Else, `executionData` is `abi.encode(calls, opData)`. - /// See: https://eips.ethereum.org/EIPS/eip-7579 - /// - /// `executionData` encoding (batch of batches): - /// - `executionData` is `abi.encode(bytes[])`, where each element in `bytes[]` - /// is an `executionData` for a single batch. - /// - /// Supported modes: - /// - `0x01000000000000000000...`: Single batch. Does not support optional `opData`. - /// - `0x01000000000078210001...`: Single batch. Supports optional `opData`. - /// - `0x01000000000078210002...`: Batch of batches. - /// - `0x01000000000078210003...`: Single batch with common `to` address and optional `opData`. - /// - /// For the "batch of batches" mode, each batch will be recursively passed into - /// `execute` internally with mode `0x01000000000078210001...`. - /// Useful for passing in batches signed by different signers. - /// - /// Authorization checks: - /// - If `opData` is empty, the implementation SHOULD require that - /// `msg.sender == address(this)`. - /// - If `opData` is not empty, the implementation SHOULD use the signature - /// encoded in `opData` to determine if the caller can perform the execution. - /// - If `msg.sender` is an authorized entry point, then `execute` MAY accept - /// calls from the entry point, and MAY use `opData` for specialized logic. - /// - /// `opData` may be used to store additional data for authentication, - /// paymaster data, gas limits, etc. - function execute(bytes32 mode, bytes calldata executionData) public payable virtual { - uint256 id = _executionModeId(mode); - if (id == 3) return _executeBatchOfBatches(mode, executionData); - if (id == 4) return _executeBatchCommonTo(mode, executionData); - Call[] calldata calls; - bytes calldata opData; - - /// @solidity memory-safe-assembly - assembly { - if iszero(id) { - mstore(0x00, 0x7f181275) // `UnsupportedExecutionMode()`. - revert(0x1c, 0x04) - } - // Use inline assembly to extract the calls and optional `opData` efficiently. - opData.length := 0 - let o := add(executionData.offset, calldataload(executionData.offset)) - calls.offset := add(o, 0x20) - calls.length := calldataload(o) - // If the offset of `executionData` allows for `opData`, and the mode supports it. - if gt(eq(id, 2), gt(0x40, calldataload(executionData.offset))) { - let q := add(executionData.offset, calldataload(add(0x20, executionData.offset))) - opData.offset := add(q, 0x20) - opData.length := calldataload(q) - } - // Bounds checking for `executionData` is skipped here for efficiency. - // This is safe if it is only used as an argument to `execute` externally. - // If `executionData` used as an argument to other functions externally, - // please perform the bounds checks via `LibERC7579.decodeBatchAndOpData` - /// or `abi.decode` in the other functions for safety. - } - _execute(mode, executionData, calls, opData); - } - - /// @dev Provided for execution mode support detection. - function supportsExecutionMode(bytes32 mode) public view virtual returns (bool result) { - return _executionModeId(mode) != 0; - } - - /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ - /* INTERNAL HELPERS */ - /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ - - /// @dev 0: invalid mode, 1: no `opData` support, 2: with `opData` support, 3: batch of batches. - function _executionModeId(bytes32 mode) internal view virtual returns (uint256 id) { - // Only supports atomic batched executions. - // For the encoding scheme, see: https://eips.ethereum.org/EIPS/eip-7579 - // Bytes Layout: - // - [0] ( 1 byte ) `0x01` for batch call. - // - [1] ( 1 byte ) `0x00` for revert on any failure. - // - [2..5] ( 4 bytes) Reserved by ERC7579 for future standardization. - // - [6..9] ( 4 bytes) `0x00000000` or `0x78210001` or `0x78210002`. - // - [10..31] (22 bytes) Unused. Free for use. - /// @solidity memory-safe-assembly - assembly { - let m := and(shr(mul(22, 8), mode), 0xffff00000000ffffffff) - id := eq(m, 0x01000000000000000000) // 1. - id := or(shl(1, eq(m, 0x01000000000078210001)), id) // 2. - id := or(mul(3, eq(m, 0x01000000000078210002)), id) // 3. - id := or(mul(4, eq(m, 0x01000000000078210003)), id) // 4. - } - } - - /// @dev For execution of a batch of batches with a common `to` address. - /// @dev if to == address(0), it will be replaced with address(this) - /// Execution Data: abi.encode(address to, CallSansTo[] calls, bytes opData) - function _executeBatchCommonTo(bytes32 mode, bytes calldata executionData) internal virtual { - address to; - CallSansTo[] calldata calls; - bytes calldata opData; - - /// @solidity memory-safe-assembly - assembly { - to := calldataload(executionData.offset) - - let callOffset := - add(executionData.offset, calldataload(add(0x20, executionData.offset))) - calls.offset := add(callOffset, 0x20) - calls.length := calldataload(callOffset) - - // This line is needed to ensure that opdata is valid in all code paths. - // Otherwise the compiler complains. - opData.length := 0 - // If the offset of `executionData` allows for `opData`, and the mode supports it. - if gt(calldataload(add(0x20, executionData.offset)), 0x40) { - let opDataOffset := - add(executionData.offset, calldataload(add(0x40, executionData.offset))) - opData.offset := add(opDataOffset, 0x20) - opData.length := calldataload(opDataOffset) - } - } - - _execute(mode, executionData, to, calls, opData); - } - - /// @dev For execution of a batch of batches. - function _executeBatchOfBatches(bytes32 mode, bytes calldata executionData) internal virtual { - // Replace with `0x0100________78210001...` while preserving optional and reserved fields. - mode ^= bytes32(uint256(3 << (22 * 8))); // `2 XOR 3 = 1`. - (uint256 n, uint256 o, uint256 e) = (0, 0, 0); - /// @solidity memory-safe-assembly - assembly { - let j := calldataload(executionData.offset) - let t := add(executionData.offset, j) - n := calldataload(t) // `batches.length`. - o := add(0x20, t) // Offset of `batches[0]`. - e := add(executionData.offset, executionData.length) // End of `executionData`. - // Do the bounds check on `executionData` treating it as `abi.encode(bytes[])`. - // Not too expensive, so we will just do it right here right now. - if or(shr(64, j), or(lt(executionData.length, 0x20), gt(add(o, shl(5, n)), e))) { - mstore(0x00, 0x3995943b) // `BatchOfBatchesDecodingError()`. - revert(0x1c, 0x04) - } - } - unchecked { - for (uint256 i; i != n; ++i) { - bytes calldata batch; - /// @solidity memory-safe-assembly - assembly { - let j := calldataload(add(o, shl(5, i))) - let t := add(o, j) - batch.offset := add(t, 0x20) - batch.length := calldataload(t) - // Validate that `batches[i]` is not out-of-bounds. - if or(shr(64, j), gt(add(batch.offset, batch.length), e)) { - mstore(0x00, 0x3995943b) // `BatchOfBatchesDecodingError()`. - revert(0x1c, 0x04) - } - } - execute(mode, batch); - } - } - } - - /// @dev Executes the calls. - /// Reverts and bubbles up error if any call fails. - /// The `mode` and `executionData` are passed along in case there's a need to use them. - function _execute( - bytes32 mode, - bytes calldata executionData, - Call[] calldata calls, - bytes calldata opData - ) internal virtual { - // Silence compiler warning on unused variables. - mode = mode; - executionData = executionData; - // Very basic auth to only allow this contract to be called by itself. - // Override this function to perform more complex auth with `opData`. - if (opData.length == uint256(0)) { - require(msg.sender == address(this)); - // Remember to return `_execute(calls, extraData)` when you override this function. - return _execute(calls, bytes32(0)); - } - revert(); // In your override, replace this with logic to operate on `opData`. - } - - /// @dev Executes the calls. - /// Reverts and bubbles up error if any call fails. - /// The `mode` and `executionData` are passed along in case there's a need to use them. - function _execute( - bytes32 mode, - bytes calldata executionData, - address to, - CallSansTo[] calldata calls, - bytes calldata opData - ) internal virtual { - // Silence compiler warning on unused variables. - mode = mode; - executionData = executionData; - // Very basic auth to only allow this contract to be called by itself. - // Override this function to perform more complex auth with `opData`. - if (opData.length == uint256(0)) { - require(msg.sender == address(this)); - // Remember to return `_execute(calls, extraData)` when you override this function. - return _execute(calls, to, bytes32(0)); - } - revert(); // In your override, replace this with logic to operate on `opData`. - } - - /// @dev Executes the calls. - /// Reverts and bubbles up error if any call fails. - /// `extraData` can be any supplementary data (e.g. a memory pointer, some hash). - function _execute(Call[] calldata calls, bytes32 extraData) internal virtual { - unchecked { - uint256 i; - if (calls.length == uint256(0)) return; - do { - (address to, uint256 value, bytes calldata data) = _get(calls, i); - _execute(to, value, data, extraData); - } while (++i != calls.length); - } - } - - /// @dev Executes the calls. - /// Reverts and bubbles up error if any call fails. - /// `extraData` can be any supplementary data (e.g. a memory pointer, some hash). - function _execute(CallSansTo[] calldata calls, address to, bytes32 keyHash) internal virtual { - unchecked { - uint256 i; - // If `to` is address(0), it will be replaced with address(this) - /// @solidity memory-safe-assembly - assembly { - let t := shr(96, shl(96, to)) - to := or(mul(address(), iszero(t)), t) - } - if (calls.length == uint256(0)) return; - do { - (uint256 value, bytes calldata data) = _get(calls, i); - _execute(to, value, data, keyHash); - } while (++i != calls.length); - } - } - - /// @dev Executes the call. - /// Reverts and bubbles up error if any call fails. - /// `extraData` can be any supplementary data (e.g. a memory pointer, some hash). - function _execute(address to, uint256 value, bytes calldata data, bytes32 extraData) - internal - virtual - { - /// @solidity memory-safe-assembly - assembly { - extraData := extraData // Silence unused variable compiler warning. - let m := mload(0x40) // Grab the free memory pointer. - calldatacopy(m, data.offset, data.length) - if iszero(call(gas(), to, value, m, data.length, codesize(), 0x00)) { - // Bubble up the revert if the call reverts. - returndatacopy(m, 0x00, returndatasize()) - revert(m, returndatasize()) - } - } - } - - /// @dev Convenience function for getting `calls[i]`, without bounds checks. - function _get(CallSansTo[] calldata calls, uint256 i) - internal - view - virtual - returns (uint256 value, bytes calldata data) - { - /// @solidity memory-safe-assembly - assembly { - let c := add(calls.offset, calldataload(add(calls.offset, shl(5, i)))) - value := calldataload(c) - let o := add(c, calldataload(add(c, 0x20))) - data.offset := add(o, 0x20) - data.length := calldataload(o) - } - } - - /// @dev Convenience function for getting `calls[i]`, without bounds checks. - function _get(Call[] calldata calls, uint256 i) - internal - view - virtual - returns (address to, uint256 value, bytes calldata data) - { - /// @solidity memory-safe-assembly - assembly { - let c := add(calls.offset, calldataload(add(calls.offset, shl(5, i)))) - // Replaces `to` with `address(this)` if `address(0)` is provided. - let t := shr(96, shl(96, calldataload(c))) - to := or(mul(address(), iszero(t)), t) - value := calldataload(add(c, 0x20)) - let o := add(c, calldataload(add(c, 0x40))) - data.offset := add(o, 0x20) - data.length := calldataload(o) - } - } -} diff --git a/src/libraries/IntentHelpers.sol b/src/libraries/IntentHelpers.sol deleted file mode 100644 index fc4f2722..00000000 --- a/src/libraries/IntentHelpers.sol +++ /dev/null @@ -1,158 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.23; - -import {ICommon} from "../interfaces/ICommon.sol"; - -contract IntentHelpers { - /** - * The intent's layout in calldata: - * bytes4 fnSel // [0:4] - * uint256 offset; // [4:36] - * uint256 length; // [36:68] - * address supportedAccountImplementation; - * address eoa; - * uint256 nonce; - * address payer; - * address paymentToken; - * uint256 paymentMaxAmount; - * uint256 combinedGas; - * uint256 expiry; - * uint256 paymentAmount; - * address paymentRecipient; - * uint256 executionData.length - * bytes executionData; - * uint256 fundData.length - * bytes fundData; // abi.encode(funder, funderSignature, encodedFundTransfers) - * uint256 encodedPreCalls.length - * bytes encodedPreCalls; // abi.encode(bytes[]) - * uint256 signature.length - * bytes signature; - * uint256 settlerData.length - * bytes settlerData; // abi.encode(settler, settlerContext). To use settler, nonce needs to have `MERKLE_VERIFICATION` prefix - * uint256 paymentSignature.length - * bytes paymentSignature; - */ - uint256 internal constant _SUPPORTED_ACCOUNT_IMPLEMENTATION_OFFSET = 68; - uint256 internal constant _EOA_OFFSET = 88; - uint256 internal constant _NONCE_OFFSET = 120; - uint256 internal constant _PAYER_OFFSET = 152; - uint256 internal constant _PAYMENT_TOKEN_OFFSET = 184; - uint256 internal constant _PAYMENT_MAX_AMOUNT_OFFSET = 216; - uint256 internal constant _COMBINED_GAS_OFFSET = 248; - uint256 internal constant _EXPIRY_OFFSET = 280; - uint256 internal constant _PAYMENT_AMOUNT_OFFSET = 312; - uint256 internal constant _PAYMENT_RECIPIENT_OFFSET = 344; - uint256 internal constant _EXECUTION_DATA_OFFSET = 364; - - struct CalldataPointer { - uint256 offset; - } - - function _getSupportedAccountImplementation() internal pure returns (address) { - return address( - bytes20( - msg.data[ - _SUPPORTED_ACCOUNT_IMPLEMENTATION_OFFSET: - _SUPPORTED_ACCOUNT_IMPLEMENTATION_OFFSET + 20 - ] - ) - ); - } - - function _getEoa() internal pure returns (address a) { - assembly ("memory-safe") { - a := calldataload(_EOA_OFFSET) - } - } - - function _getNonce() internal pure returns (uint256 a) { - assembly ("memory-safe") { - a := calldataload(_NONCE_OFFSET) - } - } - - function _getPayer() internal pure returns (address a) { - assembly ("memory-safe") { - a := calldataload(_PAYER_OFFSET) - } - } - - function _getPaymentToken() internal pure returns (address a) { - assembly ("memory-safe") { - a := calldataload(_PAYMENT_TOKEN_OFFSET) - } - } - - function _getPaymentMaxAmount() internal pure returns (uint256 a) { - assembly ("memory-safe") { - a := calldataload(_PAYMENT_MAX_AMOUNT_OFFSET) - } - } - - function _getCombinedGas() internal pure returns (uint256 a) { - assembly ("memory-safe") { - a := calldataload(_COMBINED_GAS_OFFSET) - } - } - - function _getExpiry() internal pure returns (uint256 a) { - assembly ("memory-safe") { - a := calldataload(_EXPIRY_OFFSET) - } - } - - function _getPaymentAmount() internal pure returns (uint256 a) { - assembly ("memory-safe") { - a := calldataload(_PAYMENT_AMOUNT_OFFSET) - } - } - - function _getPaymentRecipient() internal pure returns (address a) { - return address(bytes20(msg.data[_PAYMENT_RECIPIENT_OFFSET:_PAYMENT_RECIPIENT_OFFSET + 20])); - } - - function _getExecutionData() internal pure returns (bytes calldata data) { - assembly ("memory-safe") { - data.length := calldataload(_EXECUTION_DATA_OFFSET) - data.offset := add(_EXECUTION_DATA_OFFSET, 0x20) - } - } - - /// @dev Splits data containing [uint256(len), bytes(data), ...] into [uint256(len), bytes(data)] and [...] - /// @dev Modifies the memory pointer to point to the next - function _getNextBytes(CalldataPointer memory p) - internal - pure - returns (bytes calldata returnData) - { - uint256 o = p.offset; - assembly ("memory-safe") { - returnData.length := calldataload(o) - returnData.offset := add(o, 0x20) - mstore(p, add(o, add(returnData.length, 0x20))) - } - } - - function _parseFundData(bytes calldata data) - internal - pure - returns (address funder, bytes calldata sig, bytes[] calldata transfers) - { - // fundData = abi.encode(funder, funderSignature, encodedFundTransfers) - // This gives a calldata layout of: - // 0x00: funder (20 bytes, left padded to 32 bytes) - // 0x20: offset to funderSignature (32 bytes) - we skip this and assume its starts at 0x60 - // 0x40: offset to encodedFundTransfers (32 bytes) - we skip this and assume it starts at 0x60+funderSignature.length+0x20 - // 0x60: funderSignature length (32 bytes) - // ... - assembly ("memory-safe") { - funder := calldataload(data.offset) - sig.offset := add(data.offset, 0x80) - sig.length := calldataload(add(data.offset, 0x60)) - // transfers array starts after sig, but needs to account for padding - let sigLenWords := mul(div(add(sig.length, 31), 32), 32) - transfers.length := calldataload(add(sig.offset, sigLenWords)) - transfers.offset := add(add(sig.offset, sigLenWords), 0x20) - } - } -} diff --git a/src/vendor/layerzero/interfaces/IOAppCore.sol b/src/vendor/layerzero/interfaces/IOAppCore.sol new file mode 100644 index 00000000..4a87d7a2 --- /dev/null +++ b/src/vendor/layerzero/interfaces/IOAppCore.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import { + ILayerZeroEndpointV2 +} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; + +/** + * @title IOAppCore + */ +interface IOAppCore { + // Custom error messages + error OnlyPeer(uint32 eid, bytes32 sender); + error NoPeer(uint32 eid); + error InvalidEndpointCall(); + error InvalidDelegate(); + + // Event emitted when a peer (OApp) is set for a corresponding endpoint + event PeerSet(uint32 eid, bytes32 peer); + + /** + * @notice Retrieves the OApp version information. + * @return senderVersion The version of the OAppSender.sol contract. + * @return receiverVersion The version of the OAppReceiver.sol contract. + */ + function oAppVersion() external view returns (uint64 senderVersion, uint64 receiverVersion); + + /** + * @notice Retrieves the LayerZero endpoint associated with the OApp. + * @return iEndpoint The LayerZero endpoint as an interface. + */ + function endpoint() external view returns (ILayerZeroEndpointV2 iEndpoint); + + /** + * @notice Retrieves the peer (OApp) associated with a corresponding endpoint. + * @param _eid The endpoint ID. + * @return peer The peer address (OApp instance) associated with the corresponding endpoint. + */ + function peers(uint32 _eid) external view returns (bytes32 peer); + + /** + * @notice Sets the peer address (OApp instance) for a corresponding endpoint. + * @param _eid The endpoint ID. + * @param _peer The address of the peer to be associated with the corresponding endpoint. + */ + function setPeer(uint32 _eid, bytes32 _peer) external; + + /** + * @notice Sets the delegate address for the OApp Core. + * @param _delegate The address of the delegate to be set. + */ + function setDelegate(address _delegate) external; +} diff --git a/src/vendor/layerzero/interfaces/IOAppMsgInspector.sol b/src/vendor/layerzero/interfaces/IOAppMsgInspector.sol new file mode 100644 index 00000000..4d3c82d6 --- /dev/null +++ b/src/vendor/layerzero/interfaces/IOAppMsgInspector.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +/** + * @title IOAppMsgInspector + * @dev Interface for the OApp Message Inspector, allowing examination of message and options contents. + */ +interface IOAppMsgInspector { + // Custom error message for inspection failure + error InspectionFailed(bytes message, bytes options); + + /** + * @notice Allows the inspector to examine LayerZero message contents and optionally throw a revert if invalid. + * @param _message The message payload to be inspected. + * @param _options Additional options or parameters for inspection. + * @return valid A boolean indicating whether the inspection passed (true) or failed (false). + * + * @dev Optionally done as a revert, OR use the boolean provided to handle the failure. + */ + function inspect(bytes calldata _message, bytes calldata _options) + external + view + returns (bool valid); +} diff --git a/src/vendor/layerzero/interfaces/IOAppReceiver.sol b/src/vendor/layerzero/interfaces/IOAppReceiver.sol new file mode 100644 index 00000000..0fe4317f --- /dev/null +++ b/src/vendor/layerzero/interfaces/IOAppReceiver.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import { + ILayerZeroReceiver, + Origin +} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroReceiver.sol"; + +interface IOAppReceiver is ILayerZeroReceiver { + /** + * @notice Indicates whether an address is an approved composeMsg sender to the Endpoint. + * @param _origin The origin information containing the source endpoint and sender address. + * - srcEid: The source chain endpoint ID. + * - sender: The sender address on the src chain. + * - nonce: The nonce of the message. + * @param _message The lzReceive payload. + * @param _sender The sender address. + * @return isSender Is a valid sender. + * + * @dev Applications can optionally choose to implement a separate composeMsg sender that is NOT the bridging layer. + * @dev The default sender IS the OAppReceiver implementer. + */ + function isComposeMsgSender(Origin calldata _origin, bytes calldata _message, address _sender) + external + view + returns (bool isSender); +} diff --git a/src/vendor/layerzero/mocks/EndpointV2Mock.sol b/src/vendor/layerzero/mocks/EndpointV2Mock.sol new file mode 100644 index 00000000..40042d7d --- /dev/null +++ b/src/vendor/layerzero/mocks/EndpointV2Mock.sol @@ -0,0 +1,418 @@ +// SPDX-License-Identifier: LZBL-1.2 + +pragma solidity ^0.8.20; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +// @dev oz4/5 breaking change... Ownable constructor +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + +import { + MessagingFee, + MessagingParams, + MessagingReceipt, + Origin, + ILayerZeroEndpointV2 +} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; +import { + ISendLib, + Packet +} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ISendLib.sol"; +import { + ILayerZeroReceiver +} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroReceiver.sol"; +import {Errors} from "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/Errors.sol"; +import {GUID} from "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/GUID.sol"; +import {Transfer} from "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/Transfer.sol"; +import {MessagingChannel} from "@layerzerolabs/lz-evm-protocol-v2/contracts/MessagingChannel.sol"; +import {MessagingComposer} from "@layerzerolabs/lz-evm-protocol-v2/contracts/MessagingComposer.sol"; +import {MessageLibManager} from "@layerzerolabs/lz-evm-protocol-v2/contracts/MessageLibManager.sol"; +import {MessagingContext} from "@layerzerolabs/lz-evm-protocol-v2/contracts/MessagingContext.sol"; + +// LayerZero EndpointV2 is fully backward compatible with LayerZero Endpoint(V1), but it also supports additional +// features that Endpoint(V1) does not support now and may not in the future. We have also changed some terminology +// to clarify pre-existing language that might have been confusing. +// +// The following is a list of terminology changes: +// -chainId -> eid +// - Rationale: chainId was a term we initially used to describe an endpoint on a specific chain. Since +// LayerZero supports non-EVMs we could not map the classic EVM chainIds to the LayerZero chainIds, making it +// confusing for developers. With the addition of EndpointV2 and its backward compatible nature, we would have +// two chainIds per chain that has Endpoint(V1), further confusing developers. We have decided to change the +// name to Endpoint Id, or eid, for simplicity and clarity. +// -adapterParams -> options +// -userApplication -> oapp. Omnichain Application +// -srcAddress -> sender +// -dstAddress -> receiver +// - Rationale: The sender/receiver on EVM is the address. However, on non-EVM chains, the sender/receiver could +// represented as a public key, or some other identifier. The term sender/receiver is more generic +// -payload -> message. +// - Rationale: The term payload is used in the context of a packet, which is a combination of the message and GUID +contract EndpointV2Mock is + ILayerZeroEndpointV2, + MessagingChannel, + MessageLibManager, + MessagingComposer, + MessagingContext +{ + address public lzToken; + + mapping(address oapp => address delegate) public delegates; + + /// @param _eid the unique Endpoint Id for this deploy that all other Endpoints can use to send to it + // @dev oz4/5 breaking change... Ownable constructor + constructor(uint32 _eid, address _owner) Ownable(_owner) MessagingChannel(_eid) {} + + /// @dev MESSAGING STEP 0 + /// @notice This view function gives the application built on top of LayerZero the ability to requests a quote + /// with the same parameters as they would to send their message. Since the quotes are given on chain there is a + /// race condition in which the prices could change between the time the user gets their quote and the time they + /// submit their message. If the price moves up and the user doesn't send enough funds the transaction will revert, + /// if the price goes down the _refundAddress provided by the app will be refunded the difference. + /// @param _params the messaging parameters + /// @param _sender the sender of the message + function quote(MessagingParams calldata _params, address _sender) + external + view + returns (MessagingFee memory) + { + // lzToken must be set to support payInLzToken + if (_params.payInLzToken && lzToken == address(0x0)) revert Errors.LZ_LzTokenUnavailable(); + + // get the correct outbound nonce + uint64 nonce = outboundNonce[_sender][_params.dstEid][_params.receiver] + 1; + + // construct the packet with a GUID + Packet memory packet = Packet({ + nonce: nonce, + srcEid: eid, + sender: _sender, + dstEid: _params.dstEid, + receiver: _params.receiver, + guid: GUID.generate(nonce, eid, _sender, _params.dstEid, _params.receiver), + message: _params.message + }); + + // get the send library by sender and dst eid + // use _ to avoid variable shadowing + address _sendLibrary = getSendLibrary(_sender, _params.dstEid); + + return ISendLib(_sendLibrary).quote(packet, _params.options, _params.payInLzToken); + } + + /// @dev MESSAGING STEP 1 - OApp need to transfer the fees to the endpoint before sending the message + /// @param _params the messaging parameters + /// @param _refundAddress the address to refund both the native and lzToken + function send(MessagingParams calldata _params, address _refundAddress) + external + payable + sendContext(_params.dstEid, msg.sender) + returns (MessagingReceipt memory) + { + if (_params.payInLzToken && lzToken == address(0x0)) { + revert Errors.LZ_LzTokenUnavailable(); + } + + // send message + (MessagingReceipt memory receipt, address _sendLibrary) = _send(msg.sender, _params); + + // OApp can simulate with 0 native value it will fail with error including the required fee, which can be provided in the actual call + // this trick can be used to avoid the need to write the quote() function + // however, without the quote view function it will be hard to compose an oapp on chain + uint256 suppliedNative = _suppliedNative(); + uint256 suppliedLzToken = _suppliedLzToken(_params.payInLzToken); + _assertMessagingFee(receipt.fee, suppliedNative, suppliedLzToken); + + // handle lz token fees + _payToken(lzToken, receipt.fee.lzTokenFee, suppliedLzToken, _sendLibrary, _refundAddress); + + // handle native fees + _payNative(receipt.fee.nativeFee, suppliedNative, _sendLibrary, _refundAddress); + + return receipt; + } + + /// @dev internal function for sending the messages used by all external send methods + /// @param _sender the address of the application sending the message to the destination chain + /// @param _params the messaging parameters + function _send(address _sender, MessagingParams calldata _params) + internal + returns (MessagingReceipt memory, address) + { + // get the correct outbound nonce + uint64 latestNonce = _outbound(_sender, _params.dstEid, _params.receiver); + + // construct the packet with a GUID + Packet memory packet = Packet({ + nonce: latestNonce, + srcEid: eid, + sender: _sender, + dstEid: _params.dstEid, + receiver: _params.receiver, + guid: GUID.generate(latestNonce, eid, _sender, _params.dstEid, _params.receiver), + message: _params.message + }); + + // get the send library by sender and dst eid + address _sendLibrary = getSendLibrary(_sender, _params.dstEid); + + // messageLib always returns encodedPacket with guid + (MessagingFee memory fee, bytes memory encodedPacket) = + ISendLib(_sendLibrary).send(packet, _params.options, _params.payInLzToken); + + // Emit packet information for DVNs, Executors, and any other offchain infrastructure to only listen + // for this one event to perform their actions. + emit PacketSent(encodedPacket, _params.options, _sendLibrary); + + return (MessagingReceipt(packet.guid, latestNonce, fee), _sendLibrary); + } + + /// @dev MESSAGING STEP 2 - on the destination chain + /// @dev configured receive library verifies a message + /// @param _origin a struct holding the srcEid, nonce, and sender of the message + /// @param _receiver the receiver of the message + /// @param _payloadHash the payload hash of the message + function verify(Origin calldata _origin, address _receiver, bytes32 _payloadHash) external { + if (!isValidReceiveLibrary(_receiver, _origin.srcEid, msg.sender)) { + revert Errors.LZ_InvalidReceiveLibrary(); + } + + uint64 lazyNonce = lazyInboundNonce[_receiver][_origin.srcEid][_origin.sender]; + if (!_initializable(_origin, _receiver, lazyNonce)) { + revert Errors.LZ_PathNotInitializable(); + } + if (!_verifiable(_origin, _receiver, lazyNonce)) revert Errors.LZ_PathNotVerifiable(); + + // insert the message into the message channel + _inbound(_receiver, _origin.srcEid, _origin.sender, _origin.nonce, _payloadHash); + emit PacketVerified(_origin, _receiver, _payloadHash); + } + + /// @dev MESSAGING STEP 3 - the last step + /// @dev execute a verified message to the designated receiver + /// @dev the execution provides the execution context (caller, extraData) to the receiver. the receiver can optionally assert the caller and validate the untrusted extraData + /// @dev cant reentrant because the payload is cleared before execution + /// @param _origin the origin of the message + /// @param _receiver the receiver of the message + /// @param _guid the guid of the message + /// @param _message the message + /// @param _extraData the extra data provided by the executor. this data is untrusted and should be validated. + function lzReceive( + Origin calldata _origin, + address _receiver, + bytes32 _guid, + bytes calldata _message, + bytes calldata _extraData + ) external payable { + // clear the payload first to prevent reentrancy, and then execute the message + _clearPayload( + _receiver, + _origin.srcEid, + _origin.sender, + _origin.nonce, + abi.encodePacked(_guid, _message) + ); + ILayerZeroReceiver(_receiver) + .lzReceive{value: msg.value}(_origin, _guid, _message, msg.sender, _extraData); + emit PacketDelivered(_origin, _receiver); + } + + /// @param _origin the origin of the message + /// @param _receiver the receiver of the message + /// @param _guid the guid of the message + /// @param _message the message + /// @param _extraData the extra data provided by the executor. + /// @param _reason the reason for failure + function lzReceiveAlert( + Origin calldata _origin, + address _receiver, + bytes32 _guid, + uint256 _gas, + uint256 _value, + bytes calldata _message, + bytes calldata _extraData, + bytes calldata _reason + ) external { + emit LzReceiveAlert( + _receiver, msg.sender, _origin, _guid, _gas, _value, _message, _extraData, _reason + ); + } + + /// @dev Oapp uses this interface to clear a message. + /// @dev this is a PULL mode versus the PUSH mode of lzReceive + /// @dev the cleared message can be ignored by the app (effectively burnt) + /// @dev authenticated by oapp + /// @param _origin the origin of the message + /// @param _guid the guid of the message + /// @param _message the message + function clear(address _oapp, Origin calldata _origin, bytes32 _guid, bytes calldata _message) + external + { + _assertAuthorized(_oapp); + + bytes memory payload = abi.encodePacked(_guid, _message); + _clearPayload(_oapp, _origin.srcEid, _origin.sender, _origin.nonce, payload); + emit PacketDelivered(_origin, _oapp); + } + + /// @dev allows reconfiguration to recover from wrong configurations + /// @dev users should never approve the EndpointV2 contract to spend their non-layerzero tokens + /// @dev override this function if the endpoint is charging ERC20 tokens as native + /// @dev only owner + /// @param _lzToken the new layer zero token address + function setLzToken(address _lzToken) public virtual onlyOwner { + lzToken = _lzToken; + emit LzTokenSet(_lzToken); + } + + /// @dev recover the token sent to this contract by mistake + /// @dev only owner + /// @param _token the token to recover. if 0x0 then it is native token + /// @param _to the address to send the token to + /// @param _amount the amount to send + function recoverToken(address _token, address _to, uint256 _amount) external onlyOwner { + Transfer.nativeOrToken(_token, _to, _amount); + } + + /// @dev handling token payments on endpoint. the sender must approve the endpoint to spend the token + /// @dev internal function + /// @param _token the token to pay + /// @param _required the amount required + /// @param _supplied the amount supplied + /// @param _receiver the receiver of the token + function _payToken( + address _token, + uint256 _required, + uint256 _supplied, + address _receiver, + address _refundAddress + ) internal { + if (_required > 0) { + Transfer.token(_token, _receiver, _required); + } + if (_required < _supplied) { + unchecked { + // refund the excess + Transfer.token(_token, _refundAddress, _supplied - _required); + } + } + } + + /// @dev handling native token payments on endpoint + /// @dev override this if the endpoint is charging ERC20 tokens as native + /// @dev internal function + /// @param _required the amount required + /// @param _supplied the amount supplied + /// @param _receiver the receiver of the native token + /// @param _refundAddress the address to refund the excess to + function _payNative( + uint256 _required, + uint256 _supplied, + address _receiver, + address _refundAddress + ) internal virtual { + if (_required > 0) { + Transfer.native(_receiver, _required); + } + if (_required < _supplied) { + unchecked { + // refund the excess + Transfer.native(_refundAddress, _supplied - _required); + } + } + } + + /// @dev get the balance of the lzToken as the supplied lzToken fee if payInLzToken is true + function _suppliedLzToken(bool _payInLzToken) internal view returns (uint256 supplied) { + if (_payInLzToken) { + supplied = IERC20(lzToken).balanceOf(address(this)); + + // if payInLzToken is true, the supplied fee must be greater than 0 to prevent a race condition + // in which an oapp sending a message with lz token and the lz token is set to a new token between the tx + // being sent and the tx being mined. if the required lz token fee is 0 and the old lz token would be + // locked in the contract instead of being refunded + if (supplied == 0) revert Errors.LZ_ZeroLzTokenFee(); + } + } + + /// @dev override this if the endpoint is charging ERC20 tokens as native + function _suppliedNative() internal view virtual returns (uint256) { + return msg.value; + } + + /// @dev Assert the required fees and the supplied fees are enough + function _assertMessagingFee( + MessagingFee memory _required, + uint256 _suppliedNativeFee, + uint256 _suppliedLzTokenFee + ) internal pure { + if (_required.nativeFee > _suppliedNativeFee || _required.lzTokenFee > _suppliedLzTokenFee) + { + revert Errors.LZ_InsufficientFee( + _required.nativeFee, _suppliedNativeFee, _required.lzTokenFee, _suppliedLzTokenFee + ); + } + } + + /// @dev override this if the endpoint is charging ERC20 tokens as native + /// @return 0x0 if using native. otherwise the address of the native ERC20 token + function nativeToken() external view virtual returns (address) { + return address(0x0); + } + + /// @notice delegate is authorized by the oapp to configure anything in layerzero + function setDelegate(address _delegate) external { + delegates[msg.sender] = _delegate; + emit DelegateSet(msg.sender, _delegate); + } + + // ========================= Internal ========================= + function _initializable(Origin calldata _origin, address _receiver, uint64 _lazyInboundNonce) + internal + view + returns (bool) + { + return _lazyInboundNonce > 0 // allowInitializePath already checked + || ILayerZeroReceiver(_receiver).allowInitializePath(_origin); + } + + /// @dev bytes(0) payloadHash can never be submitted + function _verifiable(Origin calldata _origin, address _receiver, uint64 _lazyInboundNonce) + internal + view + returns (bool) + { + return _origin.nonce > _lazyInboundNonce // either initializing an empty slot or reverifying + || inboundPayloadHash[_receiver][_origin.srcEid][_origin.sender][_origin.nonce] + != EMPTY_PAYLOAD_HASH; // only allow reverifying if it hasn't been executed + } + + /// @dev assert the caller to either be the oapp or the delegate + function _assertAuthorized(address _oapp) + internal + view + override(MessagingChannel, MessageLibManager) + { + if (msg.sender != _oapp && msg.sender != delegates[_oapp]) { + revert Errors.LZ_Unauthorized(); + } + } + + // ========================= VIEW FUNCTIONS FOR OFFCHAIN ONLY ========================= + // Not involved in any state transition function. + // ==================================================================================== + function initializable(Origin calldata _origin, address _receiver) + external + view + returns (bool) + { + return _initializable( + _origin, _receiver, lazyInboundNonce[_receiver][_origin.srcEid][_origin.sender] + ); + } + + function verifiable(Origin calldata _origin, address _receiver) external view returns (bool) { + return _verifiable( + _origin, _receiver, lazyInboundNonce[_receiver][_origin.srcEid][_origin.sender] + ); + } +} diff --git a/src/vendor/layerzero/oapp/OApp.sol b/src/vendor/layerzero/oapp/OApp.sol new file mode 100644 index 00000000..a4d9576a --- /dev/null +++ b/src/vendor/layerzero/oapp/OApp.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +// @dev Import the 'MessagingFee' and 'MessagingReceipt' so it's exposed to OApp implementers +// solhint-disable-next-line no-unused-import +import {OAppSender, MessagingFee, MessagingReceipt} from "./OAppSender.sol"; +// @dev Import the 'Origin' so it's exposed to OApp implementers +// solhint-disable-next-line no-unused-import +import {OAppReceiver, Origin} from "./OAppReceiver.sol"; +import {OAppCore} from "./OAppCore.sol"; + +/** + * @title OApp + * @dev Abstract contract serving as the base for OApp implementation, combining OAppSender and OAppReceiver functionality. + */ +abstract contract OApp is OAppSender, OAppReceiver { + /** + * @dev Constructor to initialize the OApp with the provided owner. + * @param _delegate The delegate capable of making OApp configurations inside of the endpoint. + */ + constructor(address _delegate) OAppCore(_delegate) {} + + /** + * @notice Retrieves the OApp version information. + * @return senderVersion The version of the OAppSender.sol implementation. + * @return receiverVersion The version of the OAppReceiver.sol implementation. + */ + function oAppVersion() + public + pure + virtual + override(OAppSender, OAppReceiver) + returns (uint64 senderVersion, uint64 receiverVersion) + { + return (SENDER_VERSION, RECEIVER_VERSION); + } +} diff --git a/src/vendor/layerzero/oapp/OAppCore.sol b/src/vendor/layerzero/oapp/OAppCore.sol new file mode 100644 index 00000000..3eff1be5 --- /dev/null +++ b/src/vendor/layerzero/oapp/OAppCore.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {IOAppCore, ILayerZeroEndpointV2} from "../interfaces/IOAppCore.sol"; + +/** + * @title OAppCore + * @dev Abstract contract implementing the IOAppCore interface with basic OApp configurations. + */ +abstract contract OAppCore is IOAppCore, Ownable { + // The LayerZero endpoint associated with the given OApp + ILayerZeroEndpointV2 public endpoint; + + // Mapping to store peers associated with corresponding endpoints + mapping(uint32 eid => bytes32 peer) internal _peers; + + /** + * @dev Constructor to initialize the OAppCore with the provided delegate. + * @param _delegate The delegate capable of making OApp configurations inside of the endpoint. + * + * @dev The delegate typically should be set as the owner of the contract. + */ + constructor(address _delegate) { + if (_delegate == address(0)) revert InvalidDelegate(); + _transferOwnership(_delegate); + } + + /** + * @notice Sets the LayerZero endpoint for this OApp. + * @param _endpoint The address of the LayerZero endpoint. + * @dev Can only be called by the owner. + * @dev Should be called immediately after deployment. + * Delegate is set to the owner of the OAppCore contract. + */ + function setEndpoint(address _endpoint) external onlyOwner { + if (_endpoint == address(0)) revert InvalidDelegate(); + endpoint = ILayerZeroEndpointV2(_endpoint); + endpoint.setDelegate(msg.sender); + } + + /** + * @notice Sets the peer address (OApp instance) for a corresponding endpoint. + * @param _eid The endpoint ID. + * @param _peer The address of the peer to be associated with the corresponding endpoint. + * + * @dev Only the owner/admin of the OApp can call this function. + * @dev Indicates that the peer is trusted to send LayerZero messages to this OApp. + * @dev Set this to bytes32(0) to remove the peer address. + * @dev Peer is a bytes32 to accommodate non-evm chains. + */ + function setPeer(uint32 _eid, bytes32 _peer) public virtual onlyOwner { + _setPeer(_eid, _peer); + } + + /** + * @notice Sets the peer address (OApp instance) for a corresponding endpoint. + * @param _eid The endpoint ID. + * @param _peer The address of the peer to be associated with the corresponding endpoint. + * + * @dev Indicates that the peer is trusted to send LayerZero messages to this OApp. + * @dev Set this to bytes32(0) to remove the peer address. + * @dev Peer is a bytes32 to accommodate non-evm chains. + */ + function _setPeer(uint32 _eid, bytes32 _peer) internal virtual { + _peers[_eid] = _peer; + emit PeerSet(_eid, _peer); + } + + /** + * @notice Internal function to get the peer address associated with a specific endpoint; reverts if NOT set. + * ie. the peer is set to bytes32(0). + * @param _eid The endpoint ID. + * @return peer The address of the peer associated with the specified endpoint. + */ + function _getPeerOrRevert(uint32 _eid) internal view virtual returns (bytes32) { + bytes32 peer = peers(_eid); + if (peer == bytes32(0)) revert NoPeer(_eid); + return peer; + } + + /** + * @notice Retrieves the peer (OApp instance) for a corresponding endpoint. + * @param _eid The endpoint ID. + * @return peer The address of the peer associated with the specified endpoint. + * @dev This function is virtual to allow overriding in derived contracts. + */ + function peers(uint32 _eid) public view virtual returns (bytes32 peer) { + return _peers[_eid]; + } + + /** + * @notice Sets the delegate address for the OApp. + * @param _delegate The address of the delegate to be set. + * + * @dev Only the owner/admin of the OApp can call this function. + * @dev Provides the ability for a delegate to set configs, on behalf of the OApp, directly on the Endpoint contract. + */ + function setDelegate(address _delegate) public onlyOwner { + if (address(endpoint) == address(0)) revert InvalidDelegate(); + endpoint.setDelegate(_delegate); + } +} diff --git a/src/vendor/layerzero/oapp/OAppReceiver.sol b/src/vendor/layerzero/oapp/OAppReceiver.sol new file mode 100644 index 00000000..d3c34ad0 --- /dev/null +++ b/src/vendor/layerzero/oapp/OAppReceiver.sol @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {IOAppReceiver, Origin} from "../interfaces/IOAppReceiver.sol"; +import {OAppCore} from "./OAppCore.sol"; + +/** + * @title OAppReceiver + * @dev Abstract contract implementing the ILayerZeroReceiver interface and extending OAppCore for OApp receivers. + */ +abstract contract OAppReceiver is IOAppReceiver, OAppCore { + // Custom error message for when the caller is not the registered endpoint/ + error OnlyEndpoint(address addr); + + // @dev The version of the OAppReceiver implementation. + // @dev Version is bumped when changes are made to this contract. + uint64 internal constant RECEIVER_VERSION = 2; + + /** + * @notice Retrieves the OApp version information. + * @return senderVersion The version of the OAppSender.sol contract. + * @return receiverVersion The version of the OAppReceiver.sol contract. + * + * @dev Providing 0 as the default for OAppSender version. Indicates that the OAppSender is not implemented. + * ie. this is a RECEIVE only OApp. + * @dev If the OApp uses both OAppSender and OAppReceiver, then this needs to be override returning the correct versions. + */ + function oAppVersion() + public + view + virtual + returns (uint64 senderVersion, uint64 receiverVersion) + { + return (0, RECEIVER_VERSION); + } + + /** + * @notice Indicates whether an address is an approved composeMsg sender to the Endpoint. + * @dev _origin The origin information containing the source endpoint and sender address. + * - srcEid: The source chain endpoint ID. + * - sender: The sender address on the src chain. + * - nonce: The nonce of the message. + * @dev _message The lzReceive payload. + * @param _sender The sender address. + * @return isSender Is a valid sender. + * + * @dev Applications can optionally choose to implement separate composeMsg senders that are NOT the bridging layer. + * @dev The default sender IS the OAppReceiver implementer. + */ + function isComposeMsgSender( + Origin calldata, /*_origin*/ + bytes calldata, /*_message*/ + address _sender + ) + public + view + virtual + returns (bool) + { + return _sender == address(this); + } + + /** + * @notice Checks if the path initialization is allowed based on the provided origin. + * @param origin The origin information containing the source endpoint and sender address. + * @return Whether the path has been initialized. + * + * @dev This indicates to the endpoint that the OApp has enabled msgs for this particular path to be received. + * @dev This defaults to assuming if a peer has been set, its initialized. + * Can be overridden by the OApp if there is other logic to determine this. + */ + function allowInitializePath(Origin calldata origin) public view virtual returns (bool) { + return peers(origin.srcEid) == origin.sender; + } + + /** + * @notice Retrieves the next nonce for a given source endpoint and sender address. + * @dev _srcEid The source endpoint ID. + * @dev _sender The sender address. + * @return nonce The next nonce. + * + * @dev The path nonce starts from 1. If 0 is returned it means that there is NO nonce ordered enforcement. + * @dev Is required by the off-chain executor to determine the OApp expects msg execution is ordered. + * @dev This is also enforced by the OApp. + * @dev By default this is NOT enabled. ie. nextNonce is hardcoded to return 0. + */ + function nextNonce( + uint32, + /*_srcEid*/ + bytes32 /*_sender*/ + ) + public + view + virtual + returns (uint64 nonce) + { + return 0; + } + + /** + * @dev Entry point for receiving messages or packets from the endpoint. + * @param _origin The origin information containing the source endpoint and sender address. + * - srcEid: The source chain endpoint ID. + * - sender: The sender address on the src chain. + * - nonce: The nonce of the message. + * @param _guid The unique identifier for the received LayerZero message. + * @param _message The payload of the received message. + * @param _executor The address of the executor for the received message. + * @param _extraData Additional arbitrary data provided by the corresponding executor. + * + * @dev Entry point for receiving msg/packet from the LayerZero endpoint. + */ + function lzReceive( + Origin calldata _origin, + bytes32 _guid, + bytes calldata _message, + address _executor, + bytes calldata _extraData + ) public payable virtual { + // Ensures that only the endpoint can attempt to lzReceive() messages to this OApp. + if (address(endpoint) != msg.sender) revert OnlyEndpoint(msg.sender); + + // Ensure that the sender matches the expected peer for the source endpoint. + if (_getPeerOrRevert(_origin.srcEid) != _origin.sender) { + revert OnlyPeer(_origin.srcEid, _origin.sender); + } + + // Call the internal OApp implementation of lzReceive. + _lzReceive(_origin, _guid, _message, _executor, _extraData); + } + + /** + * @dev Internal function to implement lzReceive logic without needing to copy the basic parameter validation. + */ + function _lzReceive( + Origin calldata _origin, + bytes32 _guid, + bytes calldata _message, + address _executor, + bytes calldata _extraData + ) internal virtual; +} diff --git a/src/vendor/layerzero/oapp/OAppSender.sol b/src/vendor/layerzero/oapp/OAppSender.sol new file mode 100644 index 00000000..6fdf8ef4 --- /dev/null +++ b/src/vendor/layerzero/oapp/OAppSender.sol @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { + MessagingParams, + MessagingFee, + MessagingReceipt +} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; +import {OAppCore} from "./OAppCore.sol"; + +/** + * @title OAppSender + * @dev Abstract contract implementing the OAppSender functionality for sending messages to a LayerZero endpoint. + */ +abstract contract OAppSender is OAppCore { + using SafeERC20 for IERC20; + + // Custom error messages + error NotEnoughNative(uint256 msgValue); + error LzTokenUnavailable(); + + // @dev The version of the OAppSender implementation. + // @dev Version is bumped when changes are made to this contract. + uint64 internal constant SENDER_VERSION = 1; + + /** + * @notice Retrieves the OApp version information. + * @return senderVersion The version of the OAppSender.sol contract. + * @return receiverVersion The version of the OAppReceiver.sol contract. + * + * @dev Providing 0 as the default for OAppReceiver version. Indicates that the OAppReceiver is not implemented. + * ie. this is a SEND only OApp. + * @dev If the OApp uses both OAppSender and OAppReceiver, then this needs to be override returning the correct versions + */ + function oAppVersion() + public + view + virtual + returns (uint64 senderVersion, uint64 receiverVersion) + { + return (SENDER_VERSION, 0); + } + + /** + * @dev Internal function to interact with the LayerZero EndpointV2.quote() for fee calculation. + * @param _dstEid The destination endpoint ID. + * @param _message The message payload. + * @param _options Additional options for the message. + * @param _payInLzToken Flag indicating whether to pay the fee in LZ tokens. + * @return fee The calculated MessagingFee for the message. + * - nativeFee: The native fee for the message. + * - lzTokenFee: The LZ token fee for the message. + */ + function _quote( + uint32 _dstEid, + bytes memory _message, + bytes memory _options, + bool _payInLzToken + ) internal view virtual returns (MessagingFee memory fee) { + return endpoint.quote( + MessagingParams(_dstEid, _getPeerOrRevert(_dstEid), _message, _options, _payInLzToken), + address(this) + ); + } + + /** + * @dev Internal function to interact with the LayerZero EndpointV2.send() for sending a message. + * @param _dstEid The destination endpoint ID. + * @param _message The message payload. + * @param _options Additional options for the message. + * @param _fee The calculated LayerZero fee for the message. + * - nativeFee: The native fee. + * - lzTokenFee: The lzToken fee. + * @param _refundAddress The address to receive any excess fee values sent to the endpoint. + * @return receipt The receipt for the sent message. + * - guid: The unique identifier for the sent message. + * - nonce: The nonce of the sent message. + * - fee: The LayerZero fee incurred for the message. + */ + function _lzSend( + uint32 _dstEid, + bytes memory _message, + bytes memory _options, + MessagingFee memory _fee, + address _refundAddress + ) internal virtual returns (MessagingReceipt memory receipt) { + // @dev Push corresponding fees to the endpoint, any excess is sent back to the _refundAddress from the endpoint. + uint256 messageValue = _payNative(_fee.nativeFee); + if (_fee.lzTokenFee > 0) _payLzToken(_fee.lzTokenFee); + + return endpoint. + // solhint-disable-next-line check-send-result + send{ + value: messageValue + }( + MessagingParams( + _dstEid, _getPeerOrRevert(_dstEid), _message, _options, _fee.lzTokenFee > 0 + ), + _refundAddress + ); + } + + /** + * @dev Internal function to pay the native fee associated with the message. + * @param _nativeFee The native fee to be paid. + * @return nativeFee The amount of native currency paid. + * + * @dev If the OApp needs to initiate MULTIPLE LayerZero messages in a single transaction, + * this will need to be overridden because msg.value would contain multiple lzFees. + * @dev Should be overridden in the event the LayerZero endpoint requires a different native currency. + * @dev Some EVMs use an ERC20 as a method for paying transactions/gasFees. + * @dev The endpoint is EITHER/OR, ie. it will NOT support both types of native payment at a time. + */ + function _payNative(uint256 _nativeFee) internal virtual returns (uint256 nativeFee) { + if (msg.value != _nativeFee) revert NotEnoughNative(msg.value); + return _nativeFee; + } + + /** + * @dev Internal function to pay the LZ token fee associated with the message. + * @param _lzTokenFee The LZ token fee to be paid. + * + * @dev If the caller is trying to pay in the specified lzToken, then the lzTokenFee is passed to the endpoint. + * @dev Any excess sent, is passed back to the specified _refundAddress in the _lzSend(). + */ + function _payLzToken(uint256 _lzTokenFee) internal virtual { + // @dev Cannot cache the token because it is not immutable in the endpoint. + address lzToken = endpoint.lzToken(); + if (lzToken == address(0)) revert LzTokenUnavailable(); + + // Pay LZ token fee by sending tokens to the endpoint. + IERC20(lzToken).safeTransferFrom(msg.sender, address(endpoint), _lzTokenFee); + } +} diff --git a/test/Account.t.sol b/test/Account.t.sol index 21c7c36e..87c007d9 100644 --- a/test/Account.t.sol +++ b/test/Account.t.sol @@ -419,4 +419,49 @@ contract AccountTest is BaseTest { uint256 keysCount137 = IthacaAccount(eoaAddress).keyCount(); assertEq(keysCount137, 2, "Keys should be added on chain 137"); } + + function testContextKeyHash() public { + DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); + + PassKey memory key = _randomPassKey(); + key.k.isSuperAdmin = true; + + vm.prank(d.eoa); + d.d.authorize(key.k); + + // Orchestrator workflow + vm.prank(d.d.ORCHESTRATOR()); + ERC7821.Call[] memory calls = new ERC7821.Call[](1); + calls[0].data = abi.encodeWithSelector(this.targetFunctionContextKeyHash.selector); + calls[0].to = address(this); + calls[0].value = 0; + bytes memory opData = abi.encode(key.keyHash); + bytes memory executionData = abi.encode(calls, opData); + d.d.execute(_ERC7821_BATCH_EXECUTION_MODE, executionData); + + assertEq(contextKeyHash, key.keyHash, "Context key hash mismatch orchestrator workflow"); + + // Reset context key hash + contextKeyHash = bytes32(0); + + // Workflow with opData + uint256 nonce = d.d.getNonce(0); + bytes memory signature = _sig(key, d.d.computeDigest(calls, nonce)); + opData = abi.encodePacked(nonce, signature); + executionData = abi.encode(calls, opData); + d.d.execute(_ERC7821_BATCH_EXECUTION_MODE, executionData); + + assertEq(contextKeyHash, key.keyHash, "Context key hash mismatch orchestrator workflow"); + + // Reset context key hash + contextKeyHash = bytes32(0); + + // Simple workflow without opData (self-execution) + bytes memory emptyOpData; + executionData = abi.encode(calls, emptyOpData); + vm.prank(address(d.d)); + d.d.execute(_ERC7821_BATCH_EXECUTION_MODE, executionData); + + assertEq(contextKeyHash, bytes32(0), "Context key hash should be zero for self-execution without opData"); + } } diff --git a/test/Base.t.sol b/test/Base.t.sol index aa0d05ad..87e82660 100644 --- a/test/Base.t.sol +++ b/test/Base.t.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.4; import "./utils/SoladyTest.sol"; import {EIP7702Proxy} from "solady/accounts/EIP7702Proxy.sol"; import {LibEIP7702} from "solady/accounts/LibEIP7702.sol"; +import {ERC7821} from "solady/accounts/ERC7821.sol"; import {LibERC7579} from "solady/accounts/LibERC7579.sol"; import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; import {EfficientHashLib} from "solady/utils/EfficientHashLib.sol"; @@ -23,8 +24,6 @@ import {IOrchestrator} from "../src/interfaces/IOrchestrator.sol"; import {Simulator} from "../src/Simulator.sol"; import {ICommon} from "../src/interfaces/ICommon.sol"; -import {ERC7821Ithaca as ERC7821} from "../src/libraries/ERC7821Ithaca.sol"; - contract BaseTest is SoladyTest { using LibRLP for LibRLP.List; @@ -35,6 +34,7 @@ contract BaseTest is SoladyTest { EIP7702Proxy eip7702Proxy; TargetFunctionPayload[] targetFunctionPayloads; Simulator simulator; + bytes32 contextKeyHash; struct TargetFunctionPayload { address by; @@ -56,9 +56,6 @@ contract BaseTest is SoladyTest { bytes32 internal constant _ERC7821_BATCH_EXECUTION_MODE = 0x0100000000007821000100000000000000000000000000000000000000000000; - bytes32 internal constant _ERC7821_BATCH_SANS_TO_EXECUTION_MODE = - 0x0100000000007821000300000000000000000000000000000000000000000000; - bytes32 internal constant _ERC7579_DELEGATE_CALL_MODE = 0xff00000000000000000000000000000000000000000000000000000000000000; @@ -98,6 +95,10 @@ contract BaseTest is SoladyTest { targetFunctionPayloads.push(TargetFunctionPayload(msg.sender, msg.value, data)); } + function targetFunctionContextKeyHash() public payable { + contextKeyHash = IthacaAccount(payable(msg.sender)).getContextKeyHash(); + } + function _setEIP7702Delegation(address eoa) internal { vm.etch(eoa, abi.encodePacked(hex"ef0100", address(account))); } diff --git a/test/Benchmark.t.sol b/test/Benchmark.t.sol index e146fa43..2ce04524 100644 --- a/test/Benchmark.t.sol +++ b/test/Benchmark.t.sol @@ -120,9 +120,8 @@ contract BenchmarkTest is BaseTest { token0, token1, 1 ether, 1 ether, 1, 1, address(this), block.timestamp + 999 ); - IStakeManager(_ERC4337_ENTRYPOINT_V06_ADDR).depositTo{value: 1 ether}( - _PIMLICO_PAYMASTER_V06 - ); + IStakeManager(_ERC4337_ENTRYPOINT_V06_ADDR) + .depositTo{value: 1 ether}(_PIMLICO_PAYMASTER_V06); IStakeManager(_ERC4337_ENTRYPOINT_ADDR).depositTo{value: 1 ether}(_PIMLICO_PAYMASTER_V07); (paymasterSigner, paymasterPrivateKey) = makeAddrAndKey(""); @@ -1519,7 +1518,7 @@ contract BenchmarkTest is BaseTest { } } - function testERC20Transfer_IthacaAccount1() public { + function testERC20Transfer_IthacaAccount() public { DelegatedEOA[] memory delegatedEOAs = _createIthacaAccount(1); bytes memory payload = _transferExecutionData(address(paymentToken), address(0xbabe), 1 ether); @@ -1715,7 +1714,7 @@ contract BenchmarkTest is BaseTest { ) internal view returns (bytes[] memory) { bytes[] memory encodedIntents = new bytes[](delegatedEOAs.length); for (uint256 i = 0; i < delegatedEOAs.length; i++) { - Intent memory u; + Orchestrator.Intent memory u; u.eoa = delegatedEOAs[i].eoa; u.nonce = 0; u.combinedGas = 1000000; @@ -1751,14 +1750,14 @@ contract BenchmarkTest is BaseTest { _paymentType == PaymentType.APP_SPONSOR || _paymentType == PaymentType.APP_SPONSOR_ERC20 ) { - bytes32 digest = computeDigest(u); + bytes32 digest = oc.computeDigest(u); bytes32 signatureDigest = appSponsor.computeSignatureDigest(digest); u.paymentSignature = _eoaSig(paymasterPrivateKey, signatureDigest); } u.signature = _sig(delegatedEOAs[i], u); - encodedIntents[i] = abi.encodePacked(encodeIntent(u), junk); + encodedIntents[i] = abi.encodePacked(abi.encode(u), junk); } return encodedIntents; @@ -1786,7 +1785,7 @@ contract BenchmarkTest is BaseTest { d.d.setSpendLimit(k.keyHash, address(0), GuardedExecutor.SpendPeriod.Hour, 1 ether); vm.stopPrank(); - Intent memory u; + Orchestrator.Intent memory u; u.eoa = d.eoa; u.nonce = 0; u.combinedGas = 1000000; @@ -1798,7 +1797,7 @@ contract BenchmarkTest is BaseTest { u.signature = _sig(k, u); bytes[] memory encodedIntents = new bytes[](1); - encodedIntents[0] = encodeIntent(u); + encodedIntents[0] = abi.encode(u); oc.execute(encodedIntents); vm.snapshotGasLastCall("testERC20Transfer_IthacaAccountWithSpendLimits"); diff --git a/test/Escrow.t.sol b/test/Escrow.t.sol index ce105e8a..57a6e8c0 100644 --- a/test/Escrow.t.sol +++ b/test/Escrow.t.sol @@ -723,80 +723,6 @@ contract EscrowTest is BaseTest { assertEq(uint8(escrow.statuses(escrowId3)), uint8(IEscrow.EscrowStatus.CREATED)); } - // ========== Early Refund Functionality Tests ========== - - function testEarlyRefundFunctionality() public { - // Test 1: Recipient can trigger early refund for both parties - IEscrow.Escrow memory escrowData = _createEscrowData(1000, 800); - bytes32 escrowId = _createAndFundEscrow(escrowData); - bytes32[] memory escrowIds = new bytes32[](1); - escrowIds[0] = escrowId; - - uint256 depositorBalanceBefore = token.balanceOf(depositor); - uint256 recipientBalanceBefore = token.balanceOf(recipient); - - vm.prank(recipient); - escrow.refund(escrowIds); - - assertEq(token.balanceOf(depositor) - depositorBalanceBefore, 800); // Depositor gets refundAmount - assertEq(token.balanceOf(recipient) - recipientBalanceBefore, 200); // Recipient gets escrowAmount - refundAmount - assertEq(uint256(escrow.statuses(escrowId)), uint256(IEscrow.EscrowStatus.FINALIZED)); - - // Test 2: Recipient can trigger early refund for themselves only - escrowData.salt = bytes12(uint96(2)); - escrowId = _createAndFundEscrow(escrowData); - escrowIds[0] = escrowId; - - recipientBalanceBefore = token.balanceOf(recipient); - - vm.prank(recipient); - escrow.refundRecipient(escrowIds); - - assertEq(token.balanceOf(recipient) - recipientBalanceBefore, 200); // Recipient gets escrowAmount - refundAmount - assertEq(uint256(escrow.statuses(escrowId)), uint256(IEscrow.EscrowStatus.REFUND_RECIPIENT)); - - // Test 3: Depositor can trigger early refund for themselves only - escrowData.salt = bytes12(uint96(3)); - escrowId = _createAndFundEscrow(escrowData); - escrowIds[0] = escrowId; - - depositorBalanceBefore = token.balanceOf(depositor); - - vm.prank(depositor); - escrow.refundDepositor(escrowIds); - - assertEq(token.balanceOf(depositor) - depositorBalanceBefore, 800); // Depositor gets refundAmount - assertEq(uint256(escrow.statuses(escrowId)), uint256(IEscrow.EscrowStatus.REFUND_DEPOSIT)); - - // Test 4: Unauthorized users cannot trigger early refunds - escrowData.salt = bytes12(uint96(4)); - escrowId = _createAndFundEscrow(escrowData); - escrowIds[0] = escrowId; - - vm.expectRevert(bytes4(keccak256("RefundInvalid()"))); - vm.prank(randomUser); - escrow.refund(escrowIds); - - vm.expectRevert(bytes4(keccak256("RefundInvalid()"))); - vm.prank(attacker); - escrow.refundDepositor(escrowIds); - - vm.expectRevert(bytes4(keccak256("RefundInvalid()"))); - vm.prank(randomUser); - escrow.refundRecipient(escrowIds); - - // Test 5: Normal timeout refunds still work after early refunds - depositorBalanceBefore = token.balanceOf(depositor); - recipientBalanceBefore = token.balanceOf(recipient); - - vm.warp(block.timestamp + 2 hours); - vm.prank(randomUser); - escrow.refund(escrowIds); - - assertEq(token.balanceOf(depositor) - depositorBalanceBefore, 800); // Depositor gets refundAmount - assertEq(token.balanceOf(recipient) - recipientBalanceBefore, 200); // Recipient gets escrowAmount - refundAmount - } - // ========== Helper Functions ========== function _createEscrowData(uint256 escrowAmount, uint256 refundAmount) diff --git a/test/GuardedExecutor.t.sol b/test/GuardedExecutor.t.sol index d41e341f..a30c941e 100644 --- a/test/GuardedExecutor.t.sol +++ b/test/GuardedExecutor.t.sol @@ -25,7 +25,7 @@ contract GuardedExecutorTest is BaseTest { vm.prank(d.eoa); d.d.authorize(k.k); - Intent memory u; + Orchestrator.Intent memory u; u.eoa = d.eoa; u.combinedGas = 10000000; @@ -38,8 +38,7 @@ contract GuardedExecutorTest is BaseTest { u.executionData = abi.encode(calls); u.signature = _sig(k, u); assertEq( - oc.execute(encodeIntent(u)), - bytes4(keccak256("UnauthorizedCall(bytes32,address,bytes)")) + oc.execute(abi.encode(u)), bytes4(keccak256("UnauthorizedCall(bytes32,address,bytes)")) ); // Set the call checker. bytes32 forKeyHash = _randomChance(2) ? k.keyHash : _ANY_KEYHASH; @@ -57,8 +56,7 @@ contract GuardedExecutorTest is BaseTest { u.executionData = abi.encode(calls); u.signature = _sig(k, u); assertEq( - oc.execute(encodeIntent(u)), - bytes4(keccak256("UnauthorizedCall(bytes32,address,bytes)")) + oc.execute(abi.encode(u)), bytes4(keccak256("UnauthorizedCall(bytes32,address,bytes)")) ); // Try, now with the checker configured to authorize the call.. @@ -68,7 +66,7 @@ contract GuardedExecutorTest is BaseTest { u.nonce = d.d.getNonce(0); u.executionData = abi.encode(calls); u.signature = _sig(k, u); - assertEq(oc.execute(encodeIntent(u)), bytes4(0)); + assertEq(oc.execute(abi.encode(u)), bytes4(0)); assertEq(counter.counter(), 1); // Try, now with the checker removed. @@ -81,8 +79,7 @@ contract GuardedExecutorTest is BaseTest { u.executionData = abi.encode(calls); u.signature = _sig(k, u); assertEq( - oc.execute(encodeIntent(u)), - bytes4(keccak256("UnauthorizedCall(bytes32,address,bytes)")) + oc.execute(abi.encode(u)), bytes4(keccak256("UnauthorizedCall(bytes32,address,bytes)")) ); } @@ -97,7 +94,7 @@ contract GuardedExecutorTest is BaseTest { d.d.setCanExecute(k.keyHash, address(paymentToken), _ANY_FN_SEL, true); vm.stopPrank(); - Intent memory u; + Orchestrator.Intent memory u; u.eoa = d.eoa; u.combinedGas = 10000000; @@ -115,7 +112,7 @@ contract GuardedExecutorTest is BaseTest { u.nonce = d.d.getNonce(0); u.executionData = abi.encode(calls); u.signature = _sig(k, u); - assertEq(oc.execute(encodeIntent(u)), bytes4(keccak256("NoSpendPermissions()"))); + assertEq(oc.execute(abi.encode(u)), bytes4(keccak256("NoSpendPermissions()"))); // Check that after the spend permission has been done, the token can be approved // and moved via `transferFrom`. @@ -130,7 +127,7 @@ contract GuardedExecutorTest is BaseTest { u.nonce = d.d.getNonce(0); u.executionData = abi.encode(calls); u.signature = _sig(k, u); - assertEq(oc.execute(encodeIntent(u)), 0); + assertEq(oc.execute(abi.encode(u)), 0); assertEq(paymentToken.balanceOf(address(0xb0b)), 0.1 ether); // Check that the spent has been increased. @@ -235,7 +232,7 @@ contract GuardedExecutorTest is BaseTest { vm.prank(address(0xb0b)); paymentToken.approve(d.eoa, 1 ether); - Intent memory u; + Orchestrator.Intent memory u; u.eoa = d.eoa; u.combinedGas = 10000000; @@ -261,12 +258,12 @@ contract GuardedExecutorTest is BaseTest { emit LogBool("transferToSelf:", transferToSelf); if (transferToSelf) { - assertEq(oc.execute(encodeIntent(u)), 0); + assertEq(oc.execute(abi.encode(u)), 0); assertEq(paymentToken.balanceOf(d.eoa), 0.1 ether); return; } - assertEq(oc.execute(encodeIntent(u)), bytes4(keccak256("NoSpendPermissions()"))); + assertEq(oc.execute(abi.encode(u)), bytes4(keccak256("NoSpendPermissions()"))); vm.startPrank(d.eoa); d.d @@ -277,7 +274,7 @@ contract GuardedExecutorTest is BaseTest { u.nonce = d.d.getNonce(0); u.signature = _sig(k, u); - assertEq(oc.execute(encodeIntent(u)), 0); + assertEq(oc.execute(abi.encode(u)), 0); assertEq(paymentToken.balanceOf(address(0xdad)), 0.1 ether); assertEq(d.d.spendInfos(k.keyHash)[0].spent, 0.1 ether); } @@ -288,7 +285,7 @@ contract GuardedExecutorTest is BaseTest { } function testOnlySuperAdminAndEOACanSelfExecute() public { - Intent memory u; + Orchestrator.Intent memory u; DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); u.eoa = d.eoa; u.combinedGas = 10000000; @@ -330,14 +327,14 @@ contract GuardedExecutorTest is BaseTest { u.executionData = abi.encode(calls); u.signature = _eoaSig(d.privateKey, u); - assertEq(oc.execute(encodeIntent(u)), 0, "2"); + assertEq(oc.execute(abi.encode(u)), 0, "2"); assertEq(d.d.x(), x, "3"); d.d.resetX(); u.nonce = d.d.getNonce(0); u.signature = _sig(kSuperAdmin, u); - assertEq(oc.execute(encodeIntent(u)), 0, "4"); + assertEq(oc.execute(abi.encode(u)), 0, "4"); assertEq(d.d.x(), x, "5"); d.d.resetX(); @@ -345,7 +342,7 @@ contract GuardedExecutorTest is BaseTest { u.nonce = d.d.getNonce(0); u.signature = _sig(kRegular, u); assertEq( - oc.execute(encodeIntent(u)), + oc.execute(abi.encode(u)), bytes4(keccak256("UnauthorizedCall(bytes32,address,bytes)")), "6" ); @@ -356,7 +353,7 @@ contract GuardedExecutorTest is BaseTest { } function testSetAndRemoveSpendLimitRevertsForSuperAdmin() public { - Intent memory u; + Orchestrator.Intent memory u; DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); u.eoa = d.eoa; @@ -374,11 +371,11 @@ contract GuardedExecutorTest is BaseTest { calls[0].data = abi.encodeWithSelector(IthacaAccount.authorize.selector, k.k); u.executionData = abi.encode(calls); - u.nonce = d.d.getNonce(0); + u.nonce = 0xc1d0 << 240; u.signature = _sig(d, u); - assertEq(oc.execute(encodeIntent(u)), 0); + assertEq(oc.execute(abi.encode(u)), 0); } // Set spend limits. { @@ -389,7 +386,7 @@ contract GuardedExecutorTest is BaseTest { u.executionData = abi.encode(calls); u.signature = _sig(d, u); - assertEq(oc.execute(encodeIntent(u)), bytes4(keccak256("SuperAdminCanSpendAnything()"))); + assertEq(oc.execute(abi.encode(u)), bytes4(keccak256("SuperAdminCanSpendAnything()"))); } // Remove spend limits. { @@ -400,14 +397,14 @@ contract GuardedExecutorTest is BaseTest { u.executionData = abi.encode(calls); u.signature = _sig(d, u); - assertEq(oc.execute(encodeIntent(u)), bytes4(keccak256("SuperAdminCanSpendAnything()"))); + assertEq(oc.execute(abi.encode(u)), bytes4(keccak256("SuperAdminCanSpendAnything()"))); } } function testSetAndRemoveSpendLimit(uint256 amount) public { vm.warp(86400 * 100); - Intent memory u; + Orchestrator.Intent memory u; DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); u.eoa = d.eoa; @@ -445,11 +442,11 @@ contract GuardedExecutorTest is BaseTest { calls[3] = _setSpendLimitCall(k, token, periods[1], 1 ether); u.executionData = abi.encode(calls); - u.nonce = d.d.getNonce(0); + u.nonce = 0xc1d0 << 240; u.signature = _eoaSig(d.privateKey, u); - assertEq(oc.execute(encodeIntent(u)), 0); + assertEq(oc.execute(abi.encode(u)), 0); assertEq(d.d.spendInfos(k.keyHash).length, 2); } @@ -461,7 +458,7 @@ contract GuardedExecutorTest is BaseTest { u.nonce = d.d.getNonce(0); u.executionData = abi.encode(calls); u.signature = _sig(k, u); - assertEq(oc.execute(encodeIntent(u)), 0); + assertEq(oc.execute(abi.encode(u)), 0); infos = d.d.spendInfos(k.keyHash); for (uint256 i; i < infos.length; ++i) { @@ -477,7 +474,7 @@ contract GuardedExecutorTest is BaseTest { u.nonce = d.d.getNonce(0); u.executionData = abi.encode(calls); u.signature = _sig(k, u); - assertEq(oc.execute(encodeIntent(u)), 0); + assertEq(oc.execute(abi.encode(u)), 0); infos = d.d.spendInfos(k.keyHash); assertEq(infos.length, 1); @@ -493,7 +490,7 @@ contract GuardedExecutorTest is BaseTest { u.nonce = d.d.getNonce(0); u.executionData = abi.encode(calls); u.signature = _sig(k, u); - assertEq(oc.execute(encodeIntent(u)), 0); + assertEq(oc.execute(abi.encode(u)), 0); infos = d.d.spendInfos(k.keyHash); for (uint256 i; i < infos.length; ++i) { if (infos[i].period == periods[0]) { @@ -516,7 +513,7 @@ contract GuardedExecutorTest is BaseTest { u.nonce = d.d.getNonce(0); u.executionData = abi.encode(calls); u.signature = _sig(k, u); - assertEq(oc.execute(encodeIntent(u)), 0); + assertEq(oc.execute(abi.encode(u)), 0); infos = d.d.spendInfos(k.keyHash); for (uint256 i; i < infos.length; ++i) { @@ -537,7 +534,7 @@ contract GuardedExecutorTest is BaseTest { u.nonce = d.d.getNonce(0); u.executionData = abi.encode(calls); u.signature = _sig(k, u); - assertEq(oc.execute(encodeIntent(u)), 0); + assertEq(oc.execute(abi.encode(u)), 0); assertEq(d.d.spendInfos(k.keyHash).length, 0); } @@ -558,9 +555,9 @@ contract GuardedExecutorTest is BaseTest { && calls[0].data[2] == bytes1(uint8(0x24)) && calls[0].data[3] == bytes1(uint8(0xd5))) || amount == 0 ) { - assertEq(oc.execute(encodeIntent(u)), 0); + assertEq(oc.execute(abi.encode(u)), 0); } else { - assertEq(oc.execute(encodeIntent(u)), bytes4(keccak256("NoSpendPermissions()"))); + assertEq(oc.execute(abi.encode(u)), bytes4(keccak256("NoSpendPermissions()"))); } } @@ -573,7 +570,7 @@ contract GuardedExecutorTest is BaseTest { u.nonce = d.d.getNonce(0); u.executionData = abi.encode(calls); u.signature = _sig(k, u); - assertEq(oc.execute(encodeIntent(u)), 0); + assertEq(oc.execute(abi.encode(u)), 0); infos = d.d.spendInfos(k.keyHash); for (uint256 i; i < infos.length; ++i) { @@ -590,7 +587,7 @@ contract GuardedExecutorTest is BaseTest { u.nonce = d.d.getNonce(0); u.executionData = abi.encode(calls); u.signature = _sig(k, u); - assertEq(oc.execute(encodeIntent(u)), 0); + assertEq(oc.execute(abi.encode(u)), 0); infos = d.d.spendInfos(k.keyHash); for (uint256 i; i < infos.length; ++i) { @@ -600,7 +597,7 @@ contract GuardedExecutorTest is BaseTest { } function testSetSpendLimitWithTwoPeriods() public { - Intent memory u; + Orchestrator.Intent memory u; DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); u.eoa = d.eoa; @@ -631,11 +628,11 @@ contract GuardedExecutorTest is BaseTest { calls[5] = _setSpendLimitCall(k, token1, GuardedExecutor.SpendPeriod.Year, 1 ether); u.executionData = abi.encode(calls); - u.nonce = d.d.getNonce(0); + u.nonce = 0xc1d0 << 240; u.signature = _eoaSig(d.privateKey, u); - assertEq(oc.execute(encodeIntent(u)), 0); + assertEq(oc.execute(abi.encode(u)), 0); assertEq(d.d.spendInfos(k.keyHash).length, 4); } @@ -645,11 +642,11 @@ contract GuardedExecutorTest is BaseTest { calls[0] = _transferCall2(token0, address(0xb0b), amount0); calls[1] = _transferCall2(token1, address(0xb0b), amount1); - u.nonce = d.d.getNonce(0); + u.nonce = 0; u.executionData = abi.encode(calls); u.signature = _sig(k, u); - assertEq(oc.execute(encodeIntent(u)), 0); + assertEq(oc.execute(abi.encode(u)), 0); GuardedExecutor.SpendInfo[] memory infos = d.d.spendInfos(k.keyHash); for (uint256 i; i < infos.length; ++i) { if (infos[i].token == token0) assertEq(infos[i].spent, amount0); @@ -658,7 +655,7 @@ contract GuardedExecutorTest is BaseTest { } function testSpends(bytes32) public { - Intent memory u; + Orchestrator.Intent memory u; DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); u.eoa = d.eoa; @@ -693,17 +690,17 @@ contract GuardedExecutorTest is BaseTest { } u.executionData = abi.encode(calls); - u.nonce = d.d.getNonce(0); + u.nonce = 0xc1d0 << 240; u.signature = _eoaSig(d.privateKey, u); - assertEq(oc.execute(encodeIntent(u)), 0); + assertEq(oc.execute(abi.encode(u)), 0); assertEq(d.d.spendInfos(k.keyHash).length, tokens.length); } // Test spends. { - u.nonce = d.d.getNonce(0); + u.nonce = 0; _deployPermit2(); if (_randomChance(2)) { @@ -759,7 +756,7 @@ contract GuardedExecutorTest is BaseTest { u.executionData = abi.encode(calls); u.signature = _sig(k, u); - assertEq(oc.execute(encodeIntent(u)), 0); + assertEq(oc.execute(abi.encode(u)), 0); GuardedExecutor.SpendInfo[] memory infos = d.d.spendInfos(k.keyHash); for (uint256 i; i < infos.length; ++i) { assertEq(infos[i].spent, expectedSpents[0][infos[i].token]); @@ -809,7 +806,7 @@ contract GuardedExecutorTest is BaseTest { } function _testSpendWithPassKeyViaOrchestrator(PassKey memory k, address tokenToSpend) internal { - Intent memory u; + Orchestrator.Intent memory u; GuardedExecutor.SpendInfo memory info; uint256 gExecute; @@ -842,12 +839,12 @@ contract GuardedExecutorTest is BaseTest { ); u.executionData = abi.encode(calls); + u.nonce = 0xc1d0 << 240; (gExecute, u.combinedGas,) = _estimateGasForEOAKey(u); - u.signature = _eoaSig(d.privateKey, u); - assertEq(oc.execute{gas: gExecute}(encodeIntent(u)), 0); + assertEq(oc.execute{gas: gExecute}(abi.encode(u)), 0); assertEq(d.d.spendInfos(k.keyHash).length, 2); assertEq(d.d.spendInfos(k.keyHash)[0].spent, 0); @@ -857,7 +854,7 @@ contract GuardedExecutorTest is BaseTest { // Prep Intent, and submit it. This Intent should pass. { - u.nonce++; + u.nonce = 0; ERC7821.Call[] memory calls = new ERC7821.Call[](1); calls[0] = _transferCall2(tokenToSpend, address(0xb0b), 0.6 ether); @@ -867,7 +864,7 @@ contract GuardedExecutorTest is BaseTest { u.signature = _sig(k, u); // Intent should pass. - assertEq(oc.execute{gas: gExecute}(encodeIntent(u)), 0); + assertEq(oc.execute{gas: gExecute}(abi.encode(u)), 0); assertEq(_balanceOf(tokenToSpend, address(0xb0b)), 0.6 ether); assertEq(d.d.spendInfos(k.keyHash)[0].spent, 0.6 ether); @@ -881,7 +878,7 @@ contract GuardedExecutorTest is BaseTest { u.signature = _sig(k, u); // Intent should fail. - assertEq(oc.execute(encodeIntent(u)), GuardedExecutor.ExceededSpendLimit.selector); + assertEq(oc.execute(abi.encode(u)), GuardedExecutor.ExceededSpendLimit.selector); } // Prep Intent to try to exactly hit daily spend limit. This Intent should pass. @@ -895,7 +892,7 @@ contract GuardedExecutorTest is BaseTest { (gExecute, u.combinedGas,) = _estimateGas(k, u); u.signature = _sig(k, u); - assertEq(oc.execute{gas: gExecute}(encodeIntent(u)), 0); + assertEq(oc.execute{gas: gExecute}(abi.encode(u)), 0); assertEq(_balanceOf(tokenToSpend, address(0xb0b)), 1 ether); assertEq(d.d.spendInfos(k.keyHash)[0].spent, 1 ether); } @@ -935,7 +932,7 @@ contract GuardedExecutorTest is BaseTest { u.signature = _sig(k, u); - assertEq(oc.execute{gas: gExecute}(encodeIntent(u)), 0); + assertEq(oc.execute{gas: gExecute}(abi.encode(u)), 0); assertEq(_balanceOf(tokenToSpend, address(0xb0b)), 1.5 ether); assertEq(d.d.spendInfos(k.keyHash)[0].spent, 0.5 ether); } diff --git a/test/LayerZeroSettler.t.sol b/test/LayerZeroSettler.t.sol index fe9be6ae..941f2311 100644 --- a/test/LayerZeroSettler.t.sol +++ b/test/LayerZeroSettler.t.sol @@ -716,9 +716,9 @@ contract LayerZeroSettlerTest is Test { // Should revert with InvalidL0SettlerSignature vm.expectRevert(abi.encodeWithSelector(LayerZeroSettler.InvalidL0SettlerSignature.selector)); - settlerA.executeSend{value: fee}( - orchestrator, settlementId, settlerContext, invalidSignature - ); + settlerA.executeSend{ + value: fee + }(orchestrator, settlementId, settlerContext, invalidSignature); } function test_executeSend_preventReplay() public { diff --git a/test/MultiSigSigner.t.sol b/test/MultiSigSigner.t.sol new file mode 100644 index 00000000..497e0618 --- /dev/null +++ b/test/MultiSigSigner.t.sol @@ -0,0 +1,656 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import "./Base.t.sol"; +import {MultiSigSigner} from "../src/MultiSigSigner.sol"; +import {IthacaAccount} from "../src/IthacaAccount.sol"; +import {ERC7821} from "solady/accounts/ERC7821.sol"; + +contract MultiSigSignerTest is BaseTest { + MultiSigSigner multiSigSigner; + DelegatedEOA delegatedAccount; + + struct MultiSigTestTemps { + PassKey[] owners; + bytes32[] ownerKeyHashes; + uint256 threshold; + MultiSigKey multiSigKey; + bytes32 digest; + } + + function setUp() public override { + super.setUp(); + multiSigSigner = new MultiSigSigner(); + delegatedAccount = _randomEIP7702DelegatedEOA(); + } + + function test_DuplicateOwnerSignatures() public { + MultiSigTestTemps memory t; + + // Setup: Create a multisig with threshold 2 but only 1 owner + t.threshold = 2; + t.owners = new PassKey[](2); + t.owners[0] = _randomPassKey(); + t.owners[1] = _randomPassKey(); + + // Create the multisig key configuration + t.multiSigKey.k = IthacaAccount.Key({ + expiry: 0, + keyType: IthacaAccount.KeyType.External, + isSuperAdmin: true, + publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) + }); + t.multiSigKey.threshold = t.threshold; + t.multiSigKey.owners = t.owners; + + // Authorize keys in the delegated account + vm.startPrank(delegatedAccount.eoa); + delegatedAccount.d.authorize(t.multiSigKey.k); + delegatedAccount.d.authorize(t.owners[0].k); + + // Initialize multisig config with threshold=2 but only 1 owner + t.ownerKeyHashes = new bytes32[](1); + t.ownerKeyHashes[0] = _hash(t.owners[0].k); + + // This should revert because threshold > number of owners + vm.expectRevert(MultiSigSigner.InvalidThreshold.selector); + multiSigSigner.initConfig(_hash(t.multiSigKey.k), t.threshold, t.ownerKeyHashes); + + // Now test with a valid config: threshold=2, 2 owners + t.ownerKeyHashes = new bytes32[](2); + t.ownerKeyHashes[0] = _hash(t.owners[0].k); + t.ownerKeyHashes[1] = _hash(t.owners[1].k); + + multiSigSigner.initConfig(_hash(t.multiSigKey.k), t.threshold, t.ownerKeyHashes); + + vm.stopPrank(); + + // Create a digest to sign + t.digest = keccak256("test message"); + + // Create signature array with the same signature duplicated + // Note: Each owner currently only has 1 signer power. + bytes[] memory signatures = new bytes[](2); + signatures[0] = _sig(t.owners[0], t.digest); + signatures[1] = signatures[0]; // Duplicate signature of the first owner + + // Call isValidSignatureWithKeyHash + vm.prank(address(delegatedAccount.d)); + bytes4 result = multiSigSigner.isValidSignatureWithKeyHash( + t.digest, _hash(t.multiSigKey.k), abi.encode(signatures) + ); + + // Same owner should not be able to sign multiple times + assertEq(result, bytes4(0xffffffff), "Duplicate signature should not be valid"); + + // Add the first owner twice in the owner key hash + vm.startPrank(address(delegatedAccount.eoa)); + delegatedAccount.d.setContextKeyHash(_hash(t.multiSigKey.k)); + multiSigSigner.addOwner(_hash(t.multiSigKey.k), _hash(t.owners[0].k)); + vm.stopPrank(); + + // Now it should be valid, because the first owner has 2 signer powers. + vm.prank(address(delegatedAccount.d)); + result = multiSigSigner.isValidSignatureWithKeyHash( + t.digest, _hash(t.multiSigKey.k), abi.encode(signatures) + ); + + assertEq(result, bytes4(0x8afc93b4), "Duplicate signature should be valid now"); + } + + function test_InitConfig() public { + MultiSigTestTemps memory t; + + // Setup owners + uint256 numOwners = 3; + t.owners = new PassKey[](numOwners); + t.ownerKeyHashes = new bytes32[](numOwners); + + for (uint256 i = 0; i < numOwners; i++) { + t.owners[i] = _randomPassKey(); + t.ownerKeyHashes[i] = _hash(t.owners[i].k); + } + + t.threshold = 2; + + // Create multisig key + t.multiSigKey.k = IthacaAccount.Key({ + expiry: 0, + keyType: IthacaAccount.KeyType.External, + isSuperAdmin: false, + publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) + }); + + vm.prank(address(delegatedAccount.d)); + multiSigSigner.initConfig(_hash(t.multiSigKey.k), t.threshold, t.ownerKeyHashes); + + // Verify config was set correctly + (uint256 storedThreshold, bytes32[] memory storedOwners) = + multiSigSigner.getConfig(address(delegatedAccount.d), _hash(t.multiSigKey.k)); + + assertEq(storedThreshold, t.threshold); + assertEq(storedOwners.length, t.ownerKeyHashes.length); + + for (uint256 i = 0; i < storedOwners.length; i++) { + assertEq(storedOwners[i], t.ownerKeyHashes[i]); + } + } + + function test_InitConfig_RevertsOnReinit() public { + MultiSigTestTemps memory t; + + t.owners = new PassKey[](1); + t.owners[0] = _randomPassKey(); + t.ownerKeyHashes = new bytes32[](1); + t.ownerKeyHashes[0] = _hash(t.owners[0].k); + t.threshold = 1; + + t.multiSigKey.k = IthacaAccount.Key({ + expiry: 0, + keyType: IthacaAccount.KeyType.External, + isSuperAdmin: false, + publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) + }); + + // First initialization should succeed + vm.startPrank(address(delegatedAccount.d)); + multiSigSigner.initConfig(_hash(t.multiSigKey.k), t.threshold, t.ownerKeyHashes); + + // Second initialization should revert + vm.expectRevert(MultiSigSigner.ConfigAlreadySet.selector); + multiSigSigner.initConfig(_hash(t.multiSigKey.k), t.threshold, t.ownerKeyHashes); + vm.stopPrank(); + } + + function test_InitConfig_InvalidThreshold() public { + MultiSigTestTemps memory t; + + t.owners = new PassKey[](2); + t.ownerKeyHashes = new bytes32[](2); + for (uint256 i = 0; i < 2; i++) { + t.owners[i] = _randomPassKey(); + t.ownerKeyHashes[i] = _hash(t.owners[i].k); + } + + t.multiSigKey.k = IthacaAccount.Key({ + expiry: 0, + keyType: IthacaAccount.KeyType.External, + isSuperAdmin: false, + publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) + }); + + vm.startPrank(address(delegatedAccount.d)); + + // Test threshold = 0 + vm.expectRevert(MultiSigSigner.InvalidThreshold.selector); + multiSigSigner.initConfig(_hash(t.multiSigKey.k), 0, t.ownerKeyHashes); + + // Test threshold > number of owners + vm.expectRevert(MultiSigSigner.InvalidThreshold.selector); + multiSigSigner.initConfig(_hash(t.multiSigKey.k), 3, t.ownerKeyHashes); + + vm.stopPrank(); + } + + function test_AddOwner() public { + MultiSigTestTemps memory t; + + // Initial setup with 2 owners + t.owners = new PassKey[](2); + t.ownerKeyHashes = new bytes32[](2); + for (uint256 i = 0; i < 2; i++) { + t.owners[i] = _randomPassKey(); + t.ownerKeyHashes[i] = _hash(t.owners[i].k); + } + t.threshold = 2; + + t.multiSigKey.k = IthacaAccount.Key({ + expiry: 0, + keyType: IthacaAccount.KeyType.External, + isSuperAdmin: true, + publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) + }); + + // Initialize config + vm.startPrank(address(delegatedAccount.d)); + delegatedAccount.d.authorize(t.multiSigKey.k); + multiSigSigner.initConfig(_hash(t.multiSigKey.k), t.threshold, t.ownerKeyHashes); + + // Add a new owner + PassKey memory newOwner = _randomPassKey(); + bytes32 newOwnerKeyHash = _hash(newOwner.k); + + // Set context key hash + delegatedAccount.d.setContextKeyHash(_hash(t.multiSigKey.k)); + multiSigSigner.addOwner(_hash(t.multiSigKey.k), newOwnerKeyHash); + + // Verify owner was added + (, bytes32[] memory storedOwners) = + multiSigSigner.getConfig(address(delegatedAccount.d), _hash(t.multiSigKey.k)); + + assertEq(storedOwners.length, 3); + assertEq(storedOwners[2], newOwnerKeyHash); + + vm.stopPrank(); + } + + function test_AddOwner_InvalidKeyHash() public { + MultiSigTestTemps memory t; + + t.owners = new PassKey[](1); + t.owners[0] = _randomPassKey(); + t.ownerKeyHashes = new bytes32[](1); + t.ownerKeyHashes[0] = _hash(t.owners[0].k); + + t.multiSigKey.k = IthacaAccount.Key({ + expiry: 0, + keyType: IthacaAccount.KeyType.External, + isSuperAdmin: true, + publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) + }); + + vm.startPrank(address(delegatedAccount.d)); + delegatedAccount.d.authorize(t.multiSigKey.k); + multiSigSigner.initConfig(_hash(t.multiSigKey.k), 1, t.ownerKeyHashes); + + // Try to add owner with wrong context key hash + PassKey memory newOwner = _randomPassKey(); + + // Set wrong context key hash + delegatedAccount.d.setContextKeyHash(bytes32(uint256(123))); + + vm.expectRevert(MultiSigSigner.InvalidKeyHash.selector); + multiSigSigner.addOwner(_hash(t.multiSigKey.k), _hash(newOwner.k)); + + vm.stopPrank(); + } + + function test_RemoveOwner() public { + MultiSigTestTemps memory t; + + // Setup with 3 owners + t.owners = new PassKey[](3); + t.ownerKeyHashes = new bytes32[](3); + for (uint256 i = 0; i < 3; i++) { + t.owners[i] = _randomPassKey(); + t.ownerKeyHashes[i] = _hash(t.owners[i].k); + } + t.threshold = 2; + + t.multiSigKey.k = IthacaAccount.Key({ + expiry: 0, + keyType: IthacaAccount.KeyType.External, + isSuperAdmin: true, + publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) + }); + + vm.startPrank(address(delegatedAccount.d)); + delegatedAccount.d.authorize(t.multiSigKey.k); + multiSigSigner.initConfig(_hash(t.multiSigKey.k), t.threshold, t.ownerKeyHashes); + + // Remove the second owner + delegatedAccount.d.setContextKeyHash(_hash(t.multiSigKey.k)); + multiSigSigner.removeOwner(_hash(t.multiSigKey.k), t.ownerKeyHashes[1]); + + // Verify owner was removed + (, bytes32[] memory storedOwners) = + multiSigSigner.getConfig(address(delegatedAccount.d), _hash(t.multiSigKey.k)); + + assertEq(storedOwners.length, 2); + // The last owner should have replaced the removed one + assertEq(storedOwners[1], t.ownerKeyHashes[2]); + + vm.stopPrank(); + } + + function test_RemoveOwner_OwnerNotFound() public { + MultiSigTestTemps memory t; + + t.owners = new PassKey[](2); + t.ownerKeyHashes = new bytes32[](2); + for (uint256 i = 0; i < 2; i++) { + t.owners[i] = _randomPassKey(); + t.ownerKeyHashes[i] = _hash(t.owners[i].k); + } + + t.multiSigKey.k = IthacaAccount.Key({ + expiry: 0, + keyType: IthacaAccount.KeyType.External, + isSuperAdmin: true, + publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) + }); + + vm.startPrank(address(delegatedAccount.d)); + delegatedAccount.d.authorize(t.multiSigKey.k); + multiSigSigner.initConfig(_hash(t.multiSigKey.k), 1, t.ownerKeyHashes); + + // Try to remove non-existent owner + delegatedAccount.d.setContextKeyHash(_hash(t.multiSigKey.k)); + vm.expectRevert(MultiSigSigner.OwnerNotFound.selector); + multiSigSigner.removeOwner(_hash(t.multiSigKey.k), bytes32(uint256(999))); + + vm.stopPrank(); + } + + function test_RemoveOwner_ThresholdViolation() public { + MultiSigTestTemps memory t; + + // Setup with 2 owners and threshold 2 + t.owners = new PassKey[](2); + t.ownerKeyHashes = new bytes32[](2); + for (uint256 i = 0; i < 2; i++) { + t.owners[i] = _randomPassKey(); + t.ownerKeyHashes[i] = _hash(t.owners[i].k); + } + t.threshold = 2; + + t.multiSigKey.k = IthacaAccount.Key({ + expiry: 0, + keyType: IthacaAccount.KeyType.External, + isSuperAdmin: true, + publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) + }); + + vm.startPrank(address(delegatedAccount.d)); + delegatedAccount.d.authorize(t.multiSigKey.k); + multiSigSigner.initConfig(_hash(t.multiSigKey.k), t.threshold, t.ownerKeyHashes); + + // Try to remove owner when it would violate threshold + delegatedAccount.d.setContextKeyHash(_hash(t.multiSigKey.k)); + vm.expectRevert(MultiSigSigner.InvalidThreshold.selector); + multiSigSigner.removeOwner(_hash(t.multiSigKey.k), t.ownerKeyHashes[0]); + + vm.stopPrank(); + } + + function test_SetThreshold() public { + MultiSigTestTemps memory t; + + // Setup with 3 owners + t.owners = new PassKey[](3); + t.ownerKeyHashes = new bytes32[](3); + for (uint256 i = 0; i < 3; i++) { + t.owners[i] = _randomPassKey(); + t.ownerKeyHashes[i] = _hash(t.owners[i].k); + } + t.threshold = 1; + + t.multiSigKey.k = IthacaAccount.Key({ + expiry: 0, + keyType: IthacaAccount.KeyType.External, + isSuperAdmin: true, + publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) + }); + + vm.startPrank(address(delegatedAccount.d)); + delegatedAccount.d.authorize(t.multiSigKey.k); + multiSigSigner.initConfig(_hash(t.multiSigKey.k), t.threshold, t.ownerKeyHashes); + + // Change threshold to 2 + delegatedAccount.d.setContextKeyHash(_hash(t.multiSigKey.k)); + multiSigSigner.setThreshold(_hash(t.multiSigKey.k), 2); + + // Verify threshold was changed + (uint256 storedThreshold,) = + multiSigSigner.getConfig(address(delegatedAccount.d), _hash(t.multiSigKey.k)); + + assertEq(storedThreshold, 2); + + // Change threshold to 3 + multiSigSigner.setThreshold(_hash(t.multiSigKey.k), 3); + + (storedThreshold,) = + multiSigSigner.getConfig(address(delegatedAccount.d), _hash(t.multiSigKey.k)); + + assertEq(storedThreshold, 3); + + vm.stopPrank(); + } + + function test_SetThreshold_Invalid() public { + MultiSigTestTemps memory t; + + t.owners = new PassKey[](2); + t.ownerKeyHashes = new bytes32[](2); + for (uint256 i = 0; i < 2; i++) { + t.owners[i] = _randomPassKey(); + t.ownerKeyHashes[i] = _hash(t.owners[i].k); + } + + t.multiSigKey.k = IthacaAccount.Key({ + expiry: 0, + keyType: IthacaAccount.KeyType.External, + isSuperAdmin: true, + publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) + }); + + vm.startPrank(address(delegatedAccount.d)); + delegatedAccount.d.authorize(t.multiSigKey.k); + multiSigSigner.initConfig(_hash(t.multiSigKey.k), 1, t.ownerKeyHashes); + delegatedAccount.d.setContextKeyHash(_hash(t.multiSigKey.k)); + + // Test threshold = 0 + vm.expectRevert(MultiSigSigner.InvalidThreshold.selector); + multiSigSigner.setThreshold(_hash(t.multiSigKey.k), 0); + + // Test threshold > number of owners + vm.expectRevert(MultiSigSigner.InvalidThreshold.selector); + multiSigSigner.setThreshold(_hash(t.multiSigKey.k), 3); + + vm.stopPrank(); + } + + function test_ValidSignature_MeetsThreshold() public { + MultiSigTestTemps memory t; + + // Setup with 3 owners, threshold 2 + t.owners = new PassKey[](3); + t.ownerKeyHashes = new bytes32[](3); + + vm.startPrank(delegatedAccount.eoa); + for (uint256 i = 0; i < 3; i++) { + t.owners[i] = _randomPassKey(); + t.ownerKeyHashes[i] = _hash(t.owners[i].k); + delegatedAccount.d.authorize(t.owners[i].k); + } + + t.threshold = 2; + + t.multiSigKey.k = IthacaAccount.Key({ + expiry: 0, + keyType: IthacaAccount.KeyType.External, + isSuperAdmin: true, + publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) + }); + + delegatedAccount.d.authorize(t.multiSigKey.k); + multiSigSigner.initConfig(_hash(t.multiSigKey.k), t.threshold, t.ownerKeyHashes); + vm.stopPrank(); + + // Create a digest to sign + t.digest = keccak256("test message"); + + // Create signatures from 2 owners (meets threshold) + bytes[] memory signatures = new bytes[](2); + signatures[0] = _sig(t.owners[0], t.digest); + signatures[1] = _sig(t.owners[1], t.digest); + + // Validate signature + vm.prank(address(delegatedAccount.d)); + bytes4 result = multiSigSigner.isValidSignatureWithKeyHash( + t.digest, _hash(t.multiSigKey.k), abi.encode(signatures) + ); + + assertEq(result, bytes4(0x8afc93b4), "Valid signature should return MAGIC_VALUE"); + } + + function test_InvalidSignature_BelowThreshold() public { + MultiSigTestTemps memory t; + + // Setup with 3 owners, threshold 2 + t.owners = new PassKey[](3); + t.ownerKeyHashes = new bytes32[](3); + + vm.startPrank(delegatedAccount.eoa); + for (uint256 i = 0; i < 3; i++) { + t.owners[i] = _randomPassKey(); + t.ownerKeyHashes[i] = _hash(t.owners[i].k); + delegatedAccount.d.authorize(t.owners[i].k); + } + + t.threshold = 2; + + t.multiSigKey.k = IthacaAccount.Key({ + expiry: 0, + keyType: IthacaAccount.KeyType.External, + isSuperAdmin: true, + publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) + }); + + delegatedAccount.d.authorize(t.multiSigKey.k); + multiSigSigner.initConfig(_hash(t.multiSigKey.k), t.threshold, t.ownerKeyHashes); + vm.stopPrank(); + + // Create a digest to sign + t.digest = keccak256("test message"); + + // Create signature from only 1 owner (below threshold) + bytes[] memory signatures = new bytes[](1); + signatures[0] = _sig(t.owners[0], t.digest); + + // Validate signature + vm.prank(address(delegatedAccount.d)); + bytes4 result = multiSigSigner.isValidSignatureWithKeyHash( + t.digest, _hash(t.multiSigKey.k), abi.encode(signatures) + ); + + assertEq(result, bytes4(0xffffffff), "Invalid signature should return FAIL_VALUE"); + } + + function test_InvalidSignature_NonOwner() public { + MultiSigTestTemps memory t; + + // Setup with 2 owners + t.owners = new PassKey[](2); + t.ownerKeyHashes = new bytes32[](2); + + vm.startPrank(delegatedAccount.eoa); + for (uint256 i = 0; i < 2; i++) { + t.owners[i] = _randomPassKey(); + t.ownerKeyHashes[i] = _hash(t.owners[i].k); + delegatedAccount.d.authorize(t.owners[i].k); + } + + t.threshold = 2; + + t.multiSigKey.k = IthacaAccount.Key({ + expiry: 0, + keyType: IthacaAccount.KeyType.External, + isSuperAdmin: true, + publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) + }); + + delegatedAccount.d.authorize(t.multiSigKey.k); + multiSigSigner.initConfig(_hash(t.multiSigKey.k), t.threshold, t.ownerKeyHashes); + + // Create a non-owner key + PassKey memory nonOwner = _randomPassKey(); + delegatedAccount.d.authorize(nonOwner.k); + + vm.stopPrank(); + + // Create a digest to sign + t.digest = keccak256("test message"); + + // Create signatures with one owner and one non-owner + bytes[] memory signatures = new bytes[](2); + signatures[0] = _sig(t.owners[0], t.digest); + signatures[1] = _sig(nonOwner, t.digest); + + // Validate signature + vm.prank(address(delegatedAccount.d)); + bytes4 result = multiSigSigner.isValidSignatureWithKeyHash( + t.digest, _hash(t.multiSigKey.k), abi.encode(signatures) + ); + + assertEq(result, bytes4(0xffffffff), "Non-owner signature should return FAIL_VALUE"); + } + + function testFuzz_InitConfig(uint256 numOwners, uint256 threshold) public { + numOwners = bound(numOwners, 1, 10); + threshold = bound(threshold, 1, numOwners); + + MultiSigTestTemps memory t; + t.owners = new PassKey[](numOwners); + t.ownerKeyHashes = new bytes32[](numOwners); + + for (uint256 i = 0; i < numOwners; i++) { + t.owners[i] = _randomPassKey(); + t.ownerKeyHashes[i] = _hash(t.owners[i].k); + } + + t.multiSigKey.k = IthacaAccount.Key({ + expiry: 0, + keyType: IthacaAccount.KeyType.External, + isSuperAdmin: false, + publicKey: abi.encodePacked(address(multiSigSigner), bytes12(uint96(_random()))) + }); + + vm.prank(address(delegatedAccount.d)); + multiSigSigner.initConfig(_hash(t.multiSigKey.k), threshold, t.ownerKeyHashes); + + (uint256 storedThreshold, bytes32[] memory storedOwners) = + multiSigSigner.getConfig(address(delegatedAccount.d), _hash(t.multiSigKey.k)); + + assertEq(storedThreshold, threshold); + assertEq(storedOwners.length, numOwners); + } + + function testFuzz_SignatureValidation(uint256 numOwners, uint256 threshold, uint256 numSigners) + public + { + numOwners = bound(numOwners, 1, 10); + threshold = bound(threshold, 1, numOwners); + numSigners = bound(numSigners, 0, numOwners); + + MultiSigTestTemps memory t; + t.owners = new PassKey[](numOwners); + t.ownerKeyHashes = new bytes32[](numOwners); + + vm.startPrank(delegatedAccount.eoa); + for (uint256 i = 0; i < numOwners; i++) { + t.owners[i] = _randomPassKey(); + t.ownerKeyHashes[i] = _hash(t.owners[i].k); + delegatedAccount.d.authorize(t.owners[i].k); + } + + t.multiSigKey.k = IthacaAccount.Key({ + expiry: 0, + keyType: IthacaAccount.KeyType.External, + isSuperAdmin: true, + publicKey: abi.encodePacked(address(multiSigSigner), bytes12(uint96(_random()))) + }); + + delegatedAccount.d.authorize(t.multiSigKey.k); + multiSigSigner.initConfig(_hash(t.multiSigKey.k), threshold, t.ownerKeyHashes); + vm.stopPrank(); + + t.digest = keccak256(abi.encode("test", _random())); + + bytes[] memory signatures = new bytes[](numSigners); + for (uint256 i = 0; i < numSigners; i++) { + signatures[i] = _sig(t.owners[i], t.digest); + } + + vm.prank(address(delegatedAccount.d)); + bytes4 result = multiSigSigner.isValidSignatureWithKeyHash( + t.digest, _hash(t.multiSigKey.k), abi.encode(signatures) + ); + + if (numSigners >= threshold) { + assertEq(result, bytes4(0x8afc93b4), "Should be valid when signatures >= threshold"); + } else { + assertEq(result, bytes4(0xffffffff), "Should be invalid when signatures < threshold"); + } + } +} diff --git a/test/Orchestrator.t.sol b/test/Orchestrator.t.sol index 66d1bba7..9f2ada0c 100644 --- a/test/Orchestrator.t.sol +++ b/test/Orchestrator.t.sol @@ -20,7 +20,7 @@ import {IEscrow} from "../src/interfaces/IEscrow.sol"; contract OrchestratorTest is BaseTest { struct _TestFullFlowTemps { - Intent[] intents; + Orchestrator.Intent[] intents; TargetFunctionPayload[] targetFunctionPayloads; DelegatedEOA[] delegatedEOAs; bytes[] encodedIntents; @@ -29,7 +29,7 @@ contract OrchestratorTest is BaseTest { function testFullFlow(uint256) public { _TestFullFlowTemps memory t; - t.intents = new Intent[](_random() & 3); + t.intents = new Orchestrator.Intent[](_random() & 3); t.targetFunctionPayloads = new TargetFunctionPayload[](t.intents.length); t.delegatedEOAs = new DelegatedEOA[](t.intents.length); t.encodedIntents = new bytes[](t.intents.length); @@ -38,7 +38,7 @@ contract OrchestratorTest is BaseTest { DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); t.delegatedEOAs[i] = d; - Intent memory u = t.intents[i]; + Orchestrator.Intent memory u = t.intents[i]; u.eoa = d.eoa; vm.deal(u.eoa, 2 ** 128 - 1); @@ -54,7 +54,7 @@ contract OrchestratorTest is BaseTest { u.combinedGas = 10000000; u.signature = _sig(d, u); - t.encodedIntents[i] = encodeIntent(u); + t.encodedIntents[i] = abi.encode(u); } bytes4[] memory errors = oc.execute(t.encodedIntents); @@ -74,12 +74,11 @@ contract OrchestratorTest is BaseTest { vm.deal(alice.eoa, 10 ether); vm.deal(bob.eoa, 10 ether); paymentToken.mint(alice.eoa, 50 ether); - paymentToken.mint(bob.eoa, 50 ether); bytes memory executionData = _transferExecutionData(address(paymentToken), address(0xabcd), 1 ether); - Intent memory u; + Orchestrator.Intent memory u; u.eoa = alice.eoa; u.nonce = 0; u.executionData = executionData; @@ -89,9 +88,11 @@ contract OrchestratorTest is BaseTest { u.paymentAmount = 0.1 ether; u.paymentMaxAmount = 0.5 ether; u.combinedGas = 10000000; + u.signature = ""; + u.signature = _sig(alice, u); - assertEq(oc.execute(encodeIntent(u)), bytes4(keccak256("Unauthorized()"))); + assertEq(oc.execute(abi.encode(u)), bytes4(keccak256("PaymentError()"))); } function testExecuteWithSecp256k1PassKey() public { @@ -106,7 +107,7 @@ contract OrchestratorTest is BaseTest { vm.prank(d.eoa); d.d.authorize(k.k); - Intent memory u; + Orchestrator.Intent memory u; u.eoa = d.eoa; u.nonce = 0; u.executionData = _transferExecutionData(address(paymentToken), address(0xabcd), 1 ether); @@ -130,7 +131,7 @@ contract OrchestratorTest is BaseTest { combinedGasVerificationOffset: 0 }) ); - assertEq(oc.execute(encodeIntent(u)), 0); + assertEq(oc.execute(abi.encode(u)), 0); uint256 actualAmount = 0.1 ether; assertEq(paymentToken.balanceOf(address(oc)), actualAmount); assertEq(paymentToken.balanceOf(d.eoa), 50 ether - actualAmount - 1 ether); @@ -153,7 +154,7 @@ contract OrchestratorTest is BaseTest { calls[0].to = target; calls[0].data = abi.encodeWithSignature("revertWithData(bytes)", data); - Intent memory u; + Orchestrator.Intent memory u; u.eoa = d.eoa; u.nonce = 0; u.executionData = abi.encode(calls); @@ -162,7 +163,7 @@ contract OrchestratorTest is BaseTest { u.signature = _sig(k, u); (bool success, bytes memory result) = - address(oc).call(abi.encodeWithSignature("simulateFailed(bytes)", encodeIntent(u))); + address(oc).call(abi.encodeWithSignature("simulateFailed(bytes)", abi.encode(u))); assertFalse(success); assertEq(result, abi.encodeWithSignature("ErrorWithData(bytes)", data)); @@ -173,7 +174,7 @@ contract OrchestratorTest is BaseTest { paymentToken.mint(d.eoa, 500 ether); - Intent memory u; + Orchestrator.Intent memory u; u.eoa = d.eoa; u.nonce = 0; u.executionData = _transferExecutionData(address(paymentToken), address(0xabcd), 1 ether); @@ -194,7 +195,7 @@ contract OrchestratorTest is BaseTest { }) ); - assertEq(oc.execute(encodeIntent(u)), 0); + assertEq(oc.execute(abi.encode(u)), 0); uint256 actualAmount = 10 ether; assertEq(paymentToken.balanceOf(address(this)), actualAmount); assertEq(paymentToken.balanceOf(d.eoa), 500 ether - actualAmount - 1 ether); @@ -211,7 +212,7 @@ contract OrchestratorTest is BaseTest { ds[i] = _randomEIP7702DelegatedEOA(); paymentToken.mint(ds[i].eoa, 1 ether); - Intent memory u; + Orchestrator.Intent memory u; u.eoa = ds[i].eoa; u.nonce = 0; u.executionData = @@ -223,7 +224,7 @@ contract OrchestratorTest is BaseTest { u.paymentMaxAmount = 0.5 ether; u.combinedGas = 10000000; u.signature = _sig(ds[i], u); - encodedIntents[i] = encodeIntent(u); + encodedIntents[i] = abi.encode(u); } bytes4[] memory errs = oc.execute(encodedIntents); @@ -246,7 +247,7 @@ contract OrchestratorTest is BaseTest { calls[i] = _transferCall(address(paymentToken), address(0xabcd), 0.5 ether); } - Intent memory u; + Orchestrator.Intent memory u; u.eoa = d.eoa; u.nonce = 0; u.executionData = abi.encode(calls); @@ -259,7 +260,7 @@ contract OrchestratorTest is BaseTest { (uint256 gExecute,,) = _estimateGas(u); - assertEq(oc.execute{gas: gExecute}(encodeIntent(u)), 0); + assertEq(oc.execute{gas: gExecute}(abi.encode(u)), 0); assertEq(paymentToken.balanceOf(address(0xabcd)), 0.5 ether * n); assertEq(paymentToken.balanceOf(d.eoa), 100 ether - (u.paymentAmount + 0.5 ether * n)); assertEq(d.d.getNonce(0), 1); @@ -270,7 +271,7 @@ contract OrchestratorTest is BaseTest { paymentToken.mint(d.eoa, 500 ether); - Intent memory u; + Orchestrator.Intent memory u; u.eoa = d.eoa; u.nonce = 0; u.executionData = _transferExecutionData(address(paymentToken), address(0xabcd), 1 ether); @@ -312,7 +313,7 @@ contract OrchestratorTest is BaseTest { paymentToken.mint(d.eoa, 10 ether); // Create base intent with common fields - Intent memory baseIntent; + Orchestrator.Intent memory baseIntent; baseIntent.eoa = d.eoa; baseIntent.paymentToken = address(paymentToken); baseIntent.paymentAmount = 0.1 ether; @@ -321,49 +322,50 @@ contract OrchestratorTest is BaseTest { // Test case 1: Intent with no expiry (expiry = 0) should always be valid { - Intent memory u = baseIntent; + Orchestrator.Intent memory u = baseIntent; u.nonce = d.d.getNonce(0); u.executionData = _transferExecutionData(address(paymentToken), address(0xabcd), 1 ether); u.expiry = 0; // No expiry u.signature = _sig(d, u); - assertEq(oc.execute(encodeIntent(u)), 0); + assertEq(oc.execute(abi.encode(u)), 0); assertEq(paymentToken.balanceOf(address(0xabcd)), 1 ether); } // Test case 2: Intent with future expiry should be valid { - Intent memory u = baseIntent; + Orchestrator.Intent memory u = baseIntent; u.nonce = d.d.getNonce(0); u.executionData = _transferExecutionData(address(paymentToken), address(0xbcde), 1 ether); u.expiry = block.timestamp + 1 hours; // Future expiry u.signature = _sig(d, u); - assertEq(oc.execute(encodeIntent(u)), 0); + assertEq(oc.execute(abi.encode(u)), 0); assertEq(paymentToken.balanceOf(address(0xbcde)), 1 ether); } // Test case 3: Intent with past expiry should fail { - Intent memory u = baseIntent; + Orchestrator.Intent memory u = baseIntent; u.nonce = d.d.getNonce(0); // This will be 2 after the previous two intents u.executionData = _transferExecutionData(address(paymentToken), address(0xcdef), 1 ether); u.expiry = block.timestamp - 1; // Past expiry u.signature = _sig(d, u); - bytes4 result = oc.execute(encodeIntent(u)); + bytes4 result = oc.execute(abi.encode(u)); assertEq(result, bytes4(keccak256("IntentExpired()"))); assertEq(paymentToken.balanceOf(address(0xcdef)), 0); // Transfer should not happen } + // Test case 4: Batch execution with mixed expired and valid intents { bytes[] memory encodedIntents = new bytes[](3); // Create base intent for batch with smaller amounts - Intent memory batchBase; + Orchestrator.Intent memory batchBase; batchBase.eoa = d.eoa; batchBase.paymentToken = address(paymentToken); batchBase.paymentAmount = 0.05 ether; @@ -371,31 +373,31 @@ contract OrchestratorTest is BaseTest { batchBase.combinedGas = 10000000; // Valid intent with nonce 2 - Intent memory u1 = batchBase; + Orchestrator.Intent memory u1 = batchBase; u1.nonce = 2; u1.executionData = _transferExecutionData(address(paymentToken), address(0x1111), 0.5 ether); u1.expiry = block.timestamp + 1 hours; u1.signature = _sig(d, u1); - encodedIntents[0] = encodeIntent(u1); + encodedIntents[0] = abi.encode(u1); // Expired intent with nonce 3 - Intent memory u2 = batchBase; + Orchestrator.Intent memory u2 = batchBase; u2.nonce = 3; u2.executionData = _transferExecutionData(address(paymentToken), address(0x2222), 0.5 ether); u2.expiry = block.timestamp - 1; u2.signature = _sig(d, u2); - encodedIntents[1] = encodeIntent(u2); + encodedIntents[1] = abi.encode(u2); // Another valid intent with nonce 3 (since nonce 3 wasn't consumed due to expiry) - Intent memory u3 = batchBase; + Orchestrator.Intent memory u3 = batchBase; u3.nonce = 3; u3.executionData = _transferExecutionData(address(paymentToken), address(0x3333), 0.5 ether); u3.expiry = 0; // No expiry u3.signature = _sig(d, u3); - encodedIntents[2] = encodeIntent(u3); + encodedIntents[2] = abi.encode(u3); bytes4[] memory errors = oc.execute(encodedIntents); assertEq(errors.length, 3); @@ -422,7 +424,7 @@ contract OrchestratorTest is BaseTest { paymentToken.mint(ds[i].eoa, 1 ether); vm.deal(ds[i].eoa, 1 ether); - Intent memory u; + Orchestrator.Intent memory u; u.eoa = ds[i].eoa; u.nonce = 0; u.executionData = @@ -435,7 +437,7 @@ contract OrchestratorTest is BaseTest { u.combinedGas = 10000000; u.signature = _sig(ds[i], u); - encodeIntents[i] = encodeIntent(u); + encodeIntents[i] = abi.encode(u); } bytes memory data = abi.encodeWithSignature("execute(bytes[])", encodeIntents); @@ -461,7 +463,7 @@ contract OrchestratorTest is BaseTest { vm.prank(d.eoa); d.d.authorize(k.k); - Intent memory u; + Orchestrator.Intent memory u; u.eoa = d.eoa; u.executionData = _executionData(address(0), 0, bytes("")); u.nonce = 0x2; @@ -471,12 +473,12 @@ contract OrchestratorTest is BaseTest { u.combinedGas = 20000000; u.signature = _sig(k, u); - oc.execute(encodeIntent(u)); + oc.execute(abi.encode(u)); } function testInvalidateNonce(uint96 seqKey, uint64 seq, uint64 seq2) public { uint256 nonce = (uint256(seqKey) << 64) | uint256(seq); - Intent memory u; + Orchestrator.Intent memory u; DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); u.eoa = d.eoa; @@ -516,9 +518,9 @@ contract OrchestratorTest is BaseTest { u.signature = _sig(d, u); if (seq > type(uint64).max - 2) { - assertEq(oc.execute(encodeIntent(u)), bytes4(keccak256("InvalidNonce()"))); + assertEq(oc.execute(abi.encode(u)), bytes4(keccak256("InvalidNonce()"))); } else { - assertEq(oc.execute(encodeIntent(u)), 0); + assertEq(oc.execute(abi.encode(u)), 0); } } @@ -537,7 +539,7 @@ contract OrchestratorTest is BaseTest { p.paymentPerGas, p.combinedGasIncrement, p.combinedGasVerificationOffset, - encodeIntent(p.u) + abi.encode(p.u) ); vm.revertToStateAndDelete(snapshot); @@ -591,7 +593,7 @@ contract OrchestratorTest is BaseTest { preCall.signature = _eoaSig(payer.privateKey, oc.computeDigest(preCall)); // Create an Intent with the pre-call and fee payer - Intent memory u; + ICommon.Intent memory u; u.eoa = eoa; u.payer = address(payer.d); u.nonce = 0; @@ -613,7 +615,7 @@ contract OrchestratorTest is BaseTest { u.encodedPreCalls[0] = abi.encode(preCall); // Sign the intent with the ephemeral key and the payment with the payer - bytes32 digest = computeDigest(u); + bytes32 digest = oc.computeDigest(u); u.signature = _eoaSig(ephemeralPK, digest); u.paymentSignature = _eoaSig(payer.privateKey, digest); @@ -621,7 +623,7 @@ contract OrchestratorTest is BaseTest { uint256 payerBalanceBefore = paymentToken.balanceOf(address(payer.d)); assertFalse(MockAccount(payable(payer.eoa)).keyCount() > 0); - assertEq(oc.execute(encodeIntent(u)), 0); + assertEq(oc.execute(abi.encode(u)), 0); // Verify the session key was authorized in the payer's account assertTrue(MockAccount(payable(payer.eoa)).keyCount() > 0); @@ -654,7 +656,7 @@ contract OrchestratorTest is BaseTest { function testInitAndTransferInOneShot(bytes32) public { _TestAuthorizeWithPreCallsAndTransferTemps memory t; - Intent memory u; + Orchestrator.Intent memory u; uint256 ephemeralPK = _randomPrivateKey(); t.eoa = vm.addr(ephemeralPK); @@ -743,14 +745,14 @@ contract OrchestratorTest is BaseTest { // Test without gas estimation. u.combinedGas = 10000000; u.signature = _sig(kSession, u); - assertEq(oc.execute(encodeIntent(u)), 0); + assertEq(oc.execute(abi.encode(u)), 0); assertEq(_balanceOf(tokenToTransfer, address(0xabcd)), 0.5 ether); } function testAuthorizeWithPreCallsAndTransfer(bytes32) public { _TestAuthorizeWithPreCallsAndTransferTemps memory t; - Intent memory u; + Orchestrator.Intent memory u; Orchestrator.SignedCall memory pInit; if (_randomChance(2)) { @@ -900,21 +902,21 @@ contract OrchestratorTest is BaseTest { if (t.testInvalidPreCallEOA) { u.combinedGas = 10000000; u.signature = _sig(kSession, u); - assertEq(oc.execute(encodeIntent(u)), bytes4(keccak256("InvalidPreCallEOA()"))); + assertEq(oc.execute(abi.encode(u)), bytes4(keccak256("InvalidPreCallEOA()"))); return; // Skip the rest. } if (t.testPreCallVerificationError) { u.combinedGas = 10000000; u.signature = _sig(kSession, u); - assertEq(oc.execute(encodeIntent(u)), bytes4(keccak256("PreCallVerificationError()"))); + assertEq(oc.execute(abi.encode(u)), bytes4(keccak256("PreCallVerificationError()"))); return; // Skip the rest. } if (t.testPreCallError) { u.combinedGas = 10000000; u.signature = _sig(kSession, u); - assertEq(oc.execute(encodeIntent(u)), bytes4(keccak256("PreCallError()"))); + assertEq(oc.execute(abi.encode(u)), bytes4(keccak256("PreCallError()"))); return; // Skip the rest. } @@ -931,12 +933,12 @@ contract OrchestratorTest is BaseTest { u.combinedGas = t.gCombined; u.signature = _sig(kSession, u); - assertEq(oc.execute{gas: t.gExecute}(encodeIntent(u)), 0); + assertEq(oc.execute{gas: t.gExecute}(abi.encode(u)), 0); } else { // Otherwise, test without gas estimation. u.combinedGas = 10000000; u.signature = _sig(kSession, u); - assertEq(oc.execute(encodeIntent(u)), 0); + assertEq(oc.execute(abi.encode(u)), 0); } assertEq(paymentToken.balanceOf(address(0xabcd)), 0.5 ether); @@ -966,7 +968,7 @@ contract OrchestratorTest is BaseTest { // 1 ether in the EOA for execution. vm.deal(address(d.d), 1 ether); - Intent memory u; + Orchestrator.Intent memory u; u.eoa = d.eoa; u.payer = address(payer.d); @@ -979,7 +981,7 @@ contract OrchestratorTest is BaseTest { u.executionData = _transferExecutionData(address(0), address(0xabcd), 1 ether); u.paymentRecipient = address(0x12345); - bytes32 digest = computeDigest(u); + bytes32 digest = oc.computeDigest(u); uint256 snapshot = vm.snapshotState(); // To allow paymasters to be used in simulation mode. @@ -988,12 +990,12 @@ contract OrchestratorTest is BaseTest { vm.revertToStateAndDelete(snapshot); u.combinedGas = gCombined; - digest = computeDigest(u); + digest = oc.computeDigest(u); u.signature = _eoaSig(d.privateKey, digest); u.paymentSignature = _eoaSig(payer.privateKey, digest); uint256 payerBalanceBefore = _balanceOf(u.paymentToken, address(payer.d)); - assertEq(oc.execute{gas: gExecute}(encodeIntent(u)), 0); + assertEq(oc.execute{gas: gExecute}(abi.encode(u)), 0); assertEq(d.d.getNonce(0), u.nonce + 1); assertEq(_balanceOf(u.paymentToken, u.paymentRecipient), u.paymentAmount); assertEq(_balanceOf(u.paymentToken, address(payer.d)), payerBalanceBefore - u.paymentAmount); @@ -1032,7 +1034,7 @@ contract OrchestratorTest is BaseTest { t.token = _randomChance(2) ? address(0) : address(paymentToken); t.isWithState = _randomChance(2); - Intent memory u; + Orchestrator.Intent memory u; t.d = _randomEIP7702DelegatedEOA(); vm.deal(t.d.eoa, type(uint192).max); @@ -1050,7 +1052,7 @@ contract OrchestratorTest is BaseTest { if (t.isWithState) { t.withState.increaseFunds(u.paymentToken, u.eoa, t.funds); } else { - bytes32 digest = computeDigest(u); + bytes32 digest = oc.computeDigest(u); digest = t.withSignature.computeSignatureDigest(digest); u.paymentSignature = _sig(t.withSignatureEOA, digest); t.corruptSignature = _randomChance(2); @@ -1068,7 +1070,7 @@ contract OrchestratorTest is BaseTest { t.withSignature.setApprovedOrchestrator(address(oc), false); } if ((t.unapprovedOrchestrator && u.paymentAmount != 0)) { - assertEq(oc.execute(encodeIntent(u)), bytes4(keccak256("Unauthorized()"))); + assertEq(oc.execute(abi.encode(u)), bytes4(keccak256("Unauthorized()"))); if (u.paymentAmount != 0) { assertEq(t.d.d.getNonce(0), u.nonce); @@ -1079,7 +1081,7 @@ contract OrchestratorTest is BaseTest { } else if (t.isWithState && u.paymentAmount > t.funds && u.paymentAmount != 0) { // Arithmetic underflow error assertEq( - oc.execute(encodeIntent(u)), + oc.execute(abi.encode(u)), 0x4e487b7100000000000000000000000000000000000000000000000000000000 ); @@ -1097,7 +1099,7 @@ contract OrchestratorTest is BaseTest { } } else if ((!t.isWithState && t.corruptSignature && u.paymentAmount != 0)) { // Pre payment will not happen - assertEq(oc.execute(encodeIntent(u)), bytes4(keccak256("InvalidSignature()"))); + assertEq(oc.execute(abi.encode(u)), bytes4(keccak256("InvalidSignature()"))); // If prePayment is 0, then nonce is incremented, because the prePayment doesn't fail. if (u.paymentAmount == 0) { assertEq(t.d.d.getNonce(0), u.nonce + 1); @@ -1107,7 +1109,7 @@ contract OrchestratorTest is BaseTest { assertEq(_balanceOf(t.token, u.payer), t.balanceBefore); assertEq(_balanceOf(address(0), address(0xabcd)), 0); } else { - assertEq(oc.execute(encodeIntent(u)), 0); + assertEq(oc.execute(abi.encode(u)), 0); assertEq(t.d.d.getNonce(0), u.nonce + 1); assertEq(_balanceOf(t.token, u.payer), t.balanceBefore - u.paymentAmount); assertEq(_balanceOf(address(0), address(0xabcd)), 1 ether); @@ -1126,7 +1128,7 @@ contract OrchestratorTest is BaseTest { t.testImplementationCheck = _randomChance(2); t.requireWrongImplementation = _randomChance(2); - Intent memory u; + Orchestrator.Intent memory u; vm.deal(t.d.eoa, type(uint192).max); u.eoa = t.d.eoa; @@ -1146,12 +1148,12 @@ contract OrchestratorTest is BaseTest { if (t.testImplementationCheck && t.requireWrongImplementation) { assertEq( - oc.execute(encodeIntent(u)), bytes4(keccak256("UnsupportedAccountImplementation()")) + oc.execute(abi.encode(u)), bytes4(keccak256("UnsupportedAccountImplementation()")) ); assertEq(t.d.d.getNonce(0), u.nonce); assertEq(_balanceOf(address(0), address(0xabcd)), 0); } else { - assertEq(oc.execute(encodeIntent(u)), 0); + assertEq(oc.execute(abi.encode(u)), 0); assertEq(t.d.d.getNonce(0), u.nonce + 1); assertEq(_balanceOf(address(0), address(0xabcd)), 1 ether); } @@ -1238,7 +1240,7 @@ contract OrchestratorTest is BaseTest { vm.expectRevert(bytes4(keccak256("InvalidKeyHash()"))); t.d.d.execute(_ERC7821_BATCH_EXECUTION_MODE, abi.encode(calls)); - Intent memory u; + Orchestrator.Intent memory u; u.eoa = t.d.eoa; u.nonce = t.d.d.getNonce(0); u.executionData = abi.encode(calls); @@ -1248,13 +1250,14 @@ contract OrchestratorTest is BaseTest { u.signature = _sig(t.multiSigKey, u); // Test unwrapAndValidateSignature - bytes32 digest = computeDigest(u); + bytes32 digest = oc.computeDigest(u); (bool isValid, bytes32 keyHash) = t.d.d.unwrapAndValidateSignature(digest, _sig(t.multiSigKey, digest)); assertEq(isValid, true); assertEq(keyHash, _hash(t.multiSigKey.k)); - assertEq(oc.execute{gas: gExecute}(encodeIntent(u)), 0); + + assertEq(oc.execute{gas: gExecute}(abi.encode(u)), 0); (uint256 _threshold, bytes32[] memory o) = t.multiSigSigner.getConfig(address(t.d.d), _hash(t.multiSigKey.k)); @@ -1278,13 +1281,14 @@ contract OrchestratorTest is BaseTest { if (newThreshold == 0) { vm.expectRevert(bytes4(keccak256("InvalidThreshold()"))); } + (gExecute, gCombined,) = _estimateGasForMultiSigKey(t.multiSigKey, u); u.combinedGas = gCombined; u.signature = _sig(t.multiSigKey, u); if (newThreshold > 0) { - assertEq(oc.execute{gas: gExecute}(encodeIntent(u)), 0); + assertEq(oc.execute{gas: gExecute}(abi.encode(u)), 0); (_threshold, o) = t.multiSigSigner.getConfig(address(t.d.d), _hash(t.multiSigKey.k)); assertEq(_threshold, newThreshold); @@ -1293,6 +1297,7 @@ contract OrchestratorTest is BaseTest { t.multiSigKey.threshold = newThreshold; } } + // Test removeOwner { uint256 removeIndex = _bound(_random(), 0, o.length - 1); @@ -1311,7 +1316,7 @@ contract OrchestratorTest is BaseTest { u.combinedGas = gCombined; u.signature = _sig(t.multiSigKey, u); - assertEq(oc.execute{gas: gExecute}(encodeIntent(u)), 0); + assertEq(oc.execute{gas: gExecute}(abi.encode(u)), 0); (_threshold, o) = t.multiSigSigner.getConfig(address(t.d.d), _hash(t.multiSigKey.k)); assertEq(o.length, t.multiSigKey.owners.length); @@ -1332,9 +1337,9 @@ contract OrchestratorTest is BaseTest { DelegatedEOA d; PassKey k; // Intent data - Intent baseIntent; - Intent arbIntent; - Intent outputIntent; + ICommon.Intent baseIntent; + ICommon.Intent arbIntent; + ICommon.Intent outputIntent; // Merkle data bytes32[] leafs; bytes32 root; @@ -1422,11 +1427,12 @@ contract OrchestratorTest is BaseTest { // 1. Prepare the output intent first to get its digest as settlementId t.outputIntent.eoa = t.d.eoa; - t.outputIntent.nonce = t.d.d.getNonce(0x6D76 << 176); + t.outputIntent.nonce = t.d.d.getNonce(0); t.outputIntent.executionData = _transferExecutionData(address(t.usdcMainnet), t.friend, 1000); t.outputIntent.combinedGas = 1000000; t.outputIntent.settler = address(t.settler); + t.outputIntent.isMultichain = true; { bytes[] memory encodedFundTransfers = new bytes[](1); @@ -1445,12 +1451,13 @@ contract OrchestratorTest is BaseTest { // Compute the output intent digest to use as settlementId vm.chainId(1); // Mainnet - t.settlementId = computeDigest(t.outputIntent); + t.settlementId = oc.computeDigest(t.outputIntent); // Base Intent with escrow execution data t.baseIntent.eoa = t.d.eoa; - t.baseIntent.nonce = t.d.d.getNonce(0x6D76 << 176); + t.baseIntent.nonce = t.d.d.getNonce(0); t.baseIntent.combinedGas = 1000000; + t.baseIntent.isMultichain = true; // Create Base escrow execution data { @@ -1490,8 +1497,9 @@ contract OrchestratorTest is BaseTest { // Arbitrum Intent with escrow execution data t.arbIntent.eoa = t.d.eoa; - t.arbIntent.nonce = t.d.d.getNonce(0x6D76 << 176); + t.arbIntent.nonce = t.d.d.getNonce(0); t.arbIntent.combinedGas = 1000000; + t.arbIntent.isMultichain = true; // Create Arbitrum escrow execution data { IEscrow.Escrow[] memory escrows = new IEscrow.Escrow[](1); @@ -1538,14 +1546,13 @@ contract OrchestratorTest is BaseTest { // User has 600 USDC on base t.usdcBase.mint(t.d.eoa, 600); - t.encodedIntents[0] = encodeIntent(t.baseIntent); + t.encodedIntents[0] = abi.encode(t.baseIntent); // User escrows funds on Base vm.expectEmit(true, false, false, false, address(t.escrowBase)); emit Escrow.EscrowCreated(t.escrowIdBase); vm.prank(t.gasWallet); t.errs = oc.execute(t.encodedIntents); assertEq(uint256(bytes32(t.errs[0])), 0); - // Verify funds are escrowed, not transferred yet vm.assertEq(t.usdcBase.balanceOf(address(t.escrowBase)), 600); vm.assertEq(t.usdcBase.balanceOf(t.relay), 0); @@ -1556,7 +1563,7 @@ contract OrchestratorTest is BaseTest { // User has 500 USDC on arb t.usdcArb.mint(t.d.eoa, 500); // Unhappy case, try to send base intent to arb - t.encodedIntents[0] = encodeIntent(t.baseIntent); + t.encodedIntents[0] = abi.encode(t.baseIntent); vm.prank(t.gasWallet); t.errs = oc.execute(t.encodedIntents); assertEq( @@ -1568,15 +1575,15 @@ contract OrchestratorTest is BaseTest { bytes32[] memory wrongLeafs = new bytes32[](3); // Some random leaf - wrongLeafs[0] = computeDigest(t.arbIntent); - wrongLeafs[1] = computeDigest(t.arbIntent); - wrongLeafs[2] = computeDigest(t.outputIntent); + wrongLeafs[0] = oc.computeDigest(t.arbIntent); + wrongLeafs[1] = oc.computeDigest(t.arbIntent); + wrongLeafs[2] = oc.computeDigest(t.outputIntent); bytes memory correctSig = t.arbIntent.signature; t.arbIntent.signature = abi.encode(merkleHelper.getProof(wrongLeafs, 1), t.root, t.rootSig); - t.encodedIntents[0] = encodeIntent(t.arbIntent); + t.encodedIntents[0] = abi.encode(t.arbIntent); vm.prank(t.gasWallet); t.errs = oc.execute(t.encodedIntents); assertEq( @@ -1589,7 +1596,7 @@ contract OrchestratorTest is BaseTest { } // User escrows funds on Arb - t.encodedIntents[0] = encodeIntent(t.arbIntent); + t.encodedIntents[0] = abi.encode(t.arbIntent); vm.expectEmit(true, false, false, false, address(t.escrowArb)); emit Escrow.EscrowCreated(t.escrowIdArb); vm.prank(t.gasWallet); @@ -1615,7 +1622,7 @@ contract OrchestratorTest is BaseTest { emit SimpleSettler.Sent(address(oc), t.settlementId, 42161); // Arbitrum // Relay funds the user account, and the intended execution happens. - t.encodedIntents[0] = encodeIntent(t.outputIntent); + t.encodedIntents[0] = abi.encode(t.outputIntent); vm.prank(t.gasWallet); t.errs = oc.execute(t.encodedIntents); assertEq(uint256(bytes32(t.errs[0])), 0); @@ -1650,7 +1657,7 @@ contract OrchestratorTest is BaseTest { // Re-execute the escrow on Base (to recreate the state) t.usdcBase.mint(t.d.eoa, 600); - t.encodedIntents[0] = encodeIntent(t.baseIntent); + t.encodedIntents[0] = abi.encode(t.baseIntent); vm.expectEmit(true, false, false, false, address(t.escrowBase)); emit Escrow.EscrowCreated(t.escrowIdBase); vm.prank(t.gasWallet); @@ -1679,7 +1686,7 @@ contract OrchestratorTest is BaseTest { // Re-execute the escrow on Arbitrum (to recreate the state) t.usdcArb.mint(t.d.eoa, 500); - t.encodedIntents[0] = encodeIntent(t.arbIntent); + t.encodedIntents[0] = abi.encode(t.arbIntent); vm.expectEmit(true, false, false, false, address(t.escrowArb)); emit Escrow.EscrowCreated(t.escrowIdArb); vm.prank(t.gasWallet); @@ -1726,7 +1733,7 @@ contract OrchestratorTest is BaseTest { t.outputIntent.funderSignature = _eoaSig(wrongPrivateKey, t.leafs[2]); - t.encodedIntents[0] = encodeIntent(t.outputIntent); + t.encodedIntents[0] = abi.encode(t.outputIntent); vm.prank(t.gasWallet); t.errs = oc.execute(t.encodedIntents); @@ -1749,11 +1756,11 @@ contract OrchestratorTest is BaseTest { function _computeMerkleData(_TestMultiChainIntentTemps memory t) internal { t.leafs = new bytes32[](3); vm.chainId(8453); - t.leafs[0] = computeDigest(t.baseIntent); + t.leafs[0] = oc.computeDigest(t.baseIntent); vm.chainId(42161); - t.leafs[1] = computeDigest(t.arbIntent); + t.leafs[1] = oc.computeDigest(t.arbIntent); vm.chainId(1); - t.leafs[2] = computeDigest(t.outputIntent); + t.leafs[2] = oc.computeDigest(t.outputIntent); t.root = merkleHelper.getRoot(t.leafs); @@ -1762,23 +1769,8 @@ contract OrchestratorTest is BaseTest { t.outputIntent.funderSignature = _eoaSig(t.funderPrivateKey, t.leafs[2]); - t.baseIntent.signature = abi.encodePacked( - abi.encode(merkleHelper.getProof(t.leafs, 0), t.root, t.rootSig), - bytes32(0), - uint8(0), - uint8(1) - ); - t.arbIntent.signature = abi.encodePacked( - abi.encode(merkleHelper.getProof(t.leafs, 1), t.root, t.rootSig), - bytes32(0), - uint8(0), - uint8(1) - ); - t.outputIntent.signature = abi.encodePacked( - abi.encode(merkleHelper.getProof(t.leafs, 2), t.root, t.rootSig), - bytes32(0), - uint8(0), - uint8(1) - ); + t.baseIntent.signature = abi.encode(merkleHelper.getProof(t.leafs, 0), t.root, t.rootSig); + t.arbIntent.signature = abi.encode(merkleHelper.getProof(t.leafs, 1), t.root, t.rootSig); + t.outputIntent.signature = abi.encode(merkleHelper.getProof(t.leafs, 2), t.root, t.rootSig); } } diff --git a/test/SimulateExecute.t.sol b/test/SimulateExecute.t.sol index 8e2d52b5..1e4a89a4 100644 --- a/test/SimulateExecute.t.sol +++ b/test/SimulateExecute.t.sol @@ -58,7 +58,7 @@ contract SimulateExecuteTest is BaseTest { abi.encodeWithSignature("burnGas(uint256,uint256)", t.gasToBurn, t.randomness) ); - Intent memory i; + Orchestrator.Intent memory i; i.eoa = d.eoa; i.nonce = 0; i.executionData = t.executionData; @@ -79,19 +79,19 @@ contract SimulateExecuteTest is BaseTest { // If the caller does not have max balance, then the simulation should revert. vm.expectRevert(bytes4(keccak256("StateOverrideError()"))); (t.gUsed, t.gCombined) = - simulator.simulateV1Logs(address(oc), 0, 1, 11_000, 10_000, encodeIntent(i)); + simulator.simulateV1Logs(address(oc), 0, 1, 11_000, 10_000, abi.encode(i)); vm.expectRevert(bytes4(keccak256("StateOverrideError()"))); - oc.simulateExecute(encodeIntent(i, true, type(uint256).max)); + oc.simulateExecute(true, type(uint256).max, abi.encode(i)); vm.expectPartialRevert(bytes4(keccak256("SimulationPassed(uint256)"))); - oc.simulateExecute(encodeIntent(i, false, type(uint256).max)); + oc.simulateExecute(false, type(uint256).max, abi.encode(i)); uint256 snapshot = vm.snapshotState(); vm.deal(_ORIGIN_ADDRESS, type(uint192).max); (t.gUsed, t.gCombined) = - simulator.simulateV1Logs(address(oc), 2, 1e11, 11_000, 0, encodeIntent(i)); + simulator.simulateV1Logs(address(oc), 2, 1e11, 11_000, 0, abi.encode(i)); vm.revertToStateAndDelete(snapshot); i.combinedGas = t.gCombined; @@ -101,11 +101,11 @@ contract SimulateExecuteTest is BaseTest { i.signature = _sig(d, i); vm.expectRevert(bytes4(keccak256("InsufficientGas()"))); - oc.execute{gas: t.gExecute}(encodeIntent(i)); + oc.execute{gas: t.gExecute}(abi.encode(i)); t.gExecute = Math.mulDiv(t.gCombined + 110_000, 64, 63); - assertEq(oc.execute{gas: t.gExecute}(encodeIntent(i)), 0); + assertEq(oc.execute{gas: t.gExecute}(abi.encode(i)), 0); } function testSimulateExecuteNoRevertUnderfundedReverts() public { @@ -127,7 +127,7 @@ contract SimulateExecuteTest is BaseTest { abi.encodeWithSignature("burnGas(uint256,uint256)", t.gasToBurn, t.randomness) ); - Intent memory i; + Orchestrator.Intent memory i; i.eoa = d.eoa; i.nonce = 0; i.executionData = t.executionData; @@ -146,11 +146,11 @@ contract SimulateExecuteTest is BaseTest { } vm.expectRevert(bytes4(keccak256("PaymentError()"))); - simulator.simulateV1Logs(address(oc), 0, 1, 11_000, 0, encodeIntent(i)); + simulator.simulateV1Logs(address(oc), 0, 1, 11_000, 0, abi.encode(i)); deal(i.paymentToken, address(i.eoa), 0x112233112233112233112233); vm.expectRevert(bytes4(keccak256("PaymentError()"))); - simulator.simulateCombinedGas(address(oc), 0, 1, 11_000, encodeIntent(i)); + simulator.simulateCombinedGas(address(oc), 0, 1, 11_000, abi.encode(i)); } function testSimulateExecuteNoRevert() public { @@ -173,7 +173,7 @@ contract SimulateExecuteTest is BaseTest { abi.encodeWithSignature("burnGas(uint256,uint256)", t.gasToBurn, t.randomness) ); - Intent memory i; + Orchestrator.Intent memory i; i.eoa = d.eoa; i.nonce = 0; i.executionData = t.executionData; @@ -195,7 +195,7 @@ contract SimulateExecuteTest is BaseTest { vm.deal(_ORIGIN_ADDRESS, type(uint192).max); (t.gUsed, t.gCombined) = - simulator.simulateV1Logs(address(oc), 2, 1e11, 11_000, 0, encodeIntent(i)); + simulator.simulateV1Logs(address(oc), 2, 1e11, 11_000, 0, abi.encode(i)); vm.revertToStateAndDelete(snapshot); @@ -205,7 +205,7 @@ contract SimulateExecuteTest is BaseTest { i.signature = _sig(d, i); - assertEq(oc.execute{gas: t.gExecute}(encodeIntent(i)), 0); + assertEq(oc.execute{gas: t.gExecute}(abi.encode(i)), 0); assertEq(gasBurner.randomness(), t.randomness); } @@ -229,7 +229,7 @@ contract SimulateExecuteTest is BaseTest { abi.encodeWithSignature("burnGas(uint256,uint256)", t.gasToBurn, t.randomness) ); - Intent memory i; + Orchestrator.Intent memory i; i.eoa = d.eoa; i.nonce = 0; i.executionData = t.executionData; @@ -251,7 +251,7 @@ contract SimulateExecuteTest is BaseTest { vm.deal(_ORIGIN_ADDRESS, type(uint192).max); (t.gUsed, t.gCombined) = - simulator.simulateV1Logs(address(oc), 2, 1e11, 10_800, 0, encodeIntent(i)); + simulator.simulateV1Logs(address(oc), 2, 1e11, 10_800, 0, abi.encode(i)); vm.revertToStateAndDelete(snapshot); @@ -261,7 +261,7 @@ contract SimulateExecuteTest is BaseTest { i.signature = _sig(d, i); - assertEq(oc.execute{gas: t.gExecute}(encodeIntent(i)), 0); + assertEq(oc.execute{gas: t.gExecute}(abi.encode(i)), 0); assertEq(gasBurner.randomness(), t.randomness); } @@ -289,7 +289,7 @@ contract SimulateExecuteTest is BaseTest { abi.encodeWithSignature("burnGas(uint256,uint256)", t.gasToBurn, t.randomness) ); - Intent memory i; + Orchestrator.Intent memory i; i.eoa = d.eoa; i.nonce = 0; i.executionData = t.executionData; @@ -312,7 +312,7 @@ contract SimulateExecuteTest is BaseTest { vm.deal(_ORIGIN_ADDRESS, type(uint192).max); (t.gUsed, t.gCombined) = - simulator.simulateV1Logs(address(oc), 2, 1e11, 12_000, 10_000, encodeIntent(i)); + simulator.simulateV1Logs(address(oc), 2, 1e11, 12_000, 10_000, abi.encode(i)); vm.revertToStateAndDelete(snapshot); @@ -321,7 +321,7 @@ contract SimulateExecuteTest is BaseTest { i.signature = _sig(k, i); - assertEq(oc.execute{gas: t.gExecute}(encodeIntent(i)), 0); + assertEq(oc.execute{gas: t.gExecute}(abi.encode(i)), 0); assertEq(gasBurner.randomness(), t.randomness); } diff --git a/test/SubAccounts.t.sol b/test/SubAccounts.t.sol deleted file mode 100644 index 68417582..00000000 --- a/test/SubAccounts.t.sol +++ /dev/null @@ -1,270 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.4; - -import "./Base.t.sol"; -import {ERC20} from "solady/tokens/ERC20.sol"; -import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; -import {ERC7821} from "solady/accounts/ERC7821.sol"; - -/// @title SubAccounts Test Suite -/// @notice Tests the parent-child account architecture with spending permissions -contract SubAccountsTest is BaseTest { - using SafeTransferLib for address; - - // Main account (parent) - controlled by user - DelegatedEOA mainAccount; - PassKey mainAccountKey; - - // Sub account (child) - controlled by DApp - DelegatedEOA subAccount; - PassKey dappKey; - - // Test tokens - MockPaymentToken usdc; - MockPaymentToken dai; - - // DApp that controls the sub account - address dappAddress; - address constant dappRecipient = address(0xDA99); - - // Spend IDs for tracking permissions - uint256 constant PARENT_SWEEP_SPEND_ID = 1; - uint256 constant CHILD_PULL_SPEND_ID = 2; - - function setUp() public override { - super.setUp(); - - // Setup tokens - usdc = new MockPaymentToken(); - dai = new MockPaymentToken(); - - // Setup main account (parent) - mainAccount = _randomEIP7702DelegatedEOA(); - mainAccountKey = _randomPassKey(); - mainAccountKey.k.isSuperAdmin = true; // Main account key needs to be super admin to call setSpend - vm.prank(address(mainAccount.d)); - mainAccount.d.authorize(mainAccountKey.k); - - // Setup sub account (controlled by DApp) - subAccount = _randomEIP7702DelegatedEOA(); - dappKey = _randomPassKey(); - dappKey.k.isSuperAdmin = true; // DApp has full control of sub account - vm.prank(address(subAccount.d)); - subAccount.d.authorize(dappKey.k); - - // Setup DApp address - dappAddress = vm.addr(uint256(keccak256("dapp"))); - } - - /// @notice Tests the complete subaccount flow with DApp integration - /// Flow: - /// 1. Create main account with funds - /// 2. Create sub account (controlled by DApp, no initial funds needed) - /// 3. Sub account grants parent sweep permission - /// 4. Main account grants sub account limited pull permission - /// 5. DApp executes bundle: pulls funds from main, sends to DApp - /// 6. Parent can sweep remaining funds back - function test_CompleteSubAccountFlowWithDApp() public { - // ============================================ - // STEP 1: Fund the main account - // ============================================ - uint256 mainAccountInitialBalance = 1000e6; // 1000 USDC - usdc.mint(address(mainAccount.eoa), mainAccountInitialBalance); - dai.mint(address(mainAccount.eoa), 500e18); // 500 DAI - - assertEq(usdc.balanceOf(address(mainAccount.eoa)), mainAccountInitialBalance); - assertEq(dai.balanceOf(address(mainAccount.eoa)), 500e18); - - // Sub account starts with 0 funds (DApp pays for gas) - assertEq(usdc.balanceOf(address(subAccount.eoa)), 0); - - // ============================================ - // STEP 2: Sub account grants parent sweep permission - // This allows parent to recover any funds from sub account - // ============================================ - { - // Create setSpend call to grant parent sweep permission - ERC7821.Call[] memory calls = new ERC7821.Call[](1); - - calls[0] = ERC7821.Call({ - to: address(subAccount.d), // Call to self - value: 0, - data: abi.encodeWithSelector( - IthacaAccount.setSpend.selector, - PARENT_SWEEP_SPEND_ID, - true, // isParent = true (can sweep everything) - uint32(0), // No expiry for parent - address(mainAccount.eoa), // Parent is the spender - new address[](0), // Tokens are ignored, because parent can sweep everything - new uint256[](0) // Limits are ignored, because parent can sweep everything - ) - }); - - // Execute as the subAccount EOA directly (no signature needed) - vm.prank(subAccount.eoa); - subAccount.d.execute(_ERC7821_BATCH_EXECUTION_MODE, abi.encode(calls)); - } - - // ============================================ - // STEP 3: Main account grants sub account limited pull permission - // This allows sub account to pull up to 100 USDC and 50 DAI - // ============================================ - { - // Create setSpend call to grant sub account limited permission - ERC7821.Call[] memory calls = new ERC7821.Call[](1); - address[] memory tokens = new address[](2); - tokens[0] = address(usdc); - tokens[1] = address(dai); - uint256[] memory limits = new uint256[](2); - limits[0] = 100e6; // 100 USDC limit - limits[1] = 50e18; // 50 DAI limit - - calls[0] = ERC7821.Call({ - to: address(mainAccount.d), // Call to self - value: 0, - data: abi.encodeWithSelector( - IthacaAccount.setSpend.selector, - CHILD_PULL_SPEND_ID, - false, // isParent = false (limited permissions) - uint32(block.timestamp + 30 days), // 30 day expiry - address(subAccount.eoa), // Sub account is the spender - tokens, - limits - ) - }); - - // Execute as the mainAccount EOA directly (no signature needed) - vm.prank(mainAccount.eoa); - mainAccount.d.execute(_ERC7821_BATCH_EXECUTION_MODE, abi.encode(calls)); - } - - // ============================================ - // STEP 4: Test direct spend call from subAccount to mainAccount - // ============================================ - uint256 amountToPull = 50e6; // 50 USDC - { - // SubAccount calls spend on mainAccount directly - address[] memory pullTokens = new address[](1); - pullTokens[0] = address(usdc); - uint256[] memory pullAmounts = new uint256[](1); - pullAmounts[0] = amountToPull; - - vm.prank(subAccount.eoa); - mainAccount.d.spend( - CHILD_PULL_SPEND_ID, pullTokens, pullAmounts, address(subAccount.eoa) - ); - - // Verify the transfer worked - assertEq( - usdc.balanceOf(address(mainAccount.eoa)), mainAccountInitialBalance - amountToPull - ); - assertEq(usdc.balanceOf(address(subAccount.eoa)), amountToPull); - } - - // ============================================ - // STEP 5: DApp executes bundle via sub account - // - Pulls more funds from main account - // - Then sends to DApp recipient - // ============================================ - uint256 secondPullAmount = 30e6; // 30 USDC - { - Orchestrator.Intent memory intent; - intent.eoa = address(subAccount.eoa); - intent.nonce = subAccount.d.getNonce(0); - intent.expiry = block.timestamp + 1 days; - intent.paymentToken = address(paymentToken); - intent.paymentAmount = 0 ether; - intent.paymentRecipient = address(0xfee); - intent.combinedGas = 1000000; - - // Create bundle: pull from main, then send to DApp - ERC7821.Call[] memory calls = new ERC7821.Call[](2); - - address[] memory pullTokens = new address[](1); - pullTokens[0] = address(usdc); - uint256[] memory pullAmounts = new uint256[](1); - pullAmounts[0] = secondPullAmount; - - calls[0] = ERC7821.Call({ - to: address(mainAccount.d), - value: 0, - data: abi.encodeWithSelector( - IthacaAccount.spend.selector, - CHILD_PULL_SPEND_ID, - pullTokens, - pullAmounts, - address(subAccount.eoa) - ) - }); - - // Send all funds to DApp (first pull + second pull) - calls[1] = ERC7821.Call({ - to: address(usdc), - value: 0, - data: abi.encodeWithSelector( - ERC20.transfer.selector, dappRecipient, amountToPull + secondPullAmount - ) - }); - - intent.executionData = abi.encode(calls); - intent.signature = _sig(dappKey, intent); - - // Execute the bundle - assertEq(oc.execute(abi.encode(intent)), 0); - - // Verify balances - assertEq( - usdc.balanceOf(address(mainAccount.eoa)), - mainAccountInitialBalance - amountToPull - secondPullAmount - ); - assertEq(usdc.balanceOf(address(subAccount.eoa)), 0); - assertEq(usdc.balanceOf(dappRecipient), amountToPull + secondPullAmount); - } - - // ============================================ - // STEP 6: Test that sub account cannot exceed limits (should fail) - // ============================================ - { - // Try to pull 21 more USDC (total would be 50+30+21 = 101, exceeding 100 limit) - address[] memory pullTokens = new address[](1); - pullTokens[0] = address(usdc); - uint256[] memory pullAmounts = new uint256[](1); - pullAmounts[0] = 21e6; // This would exceed the 100 USDC limit - - // Should revert due to exceeding limit - vm.prank(subAccount.eoa); - vm.expectRevert(); - mainAccount.d.spend( - CHILD_PULL_SPEND_ID, pullTokens, pullAmounts, address(subAccount.eoa) - ); - } - - // // ============================================ - // // STEP 7: Parent sweeps funds back from sub account - // // ============================================ - { - // Parent can sweep everything from sub account - address[] memory sweepTokens = new address[](1); - sweepTokens[0] = address(usdc); - uint256[] memory sweepAmounts = new uint256[](1); - sweepAmounts[0] = usdc.balanceOf(address(subAccount.eoa)); - - uint256 mainAccountPreBalance = usdc.balanceOf(address(mainAccount.eoa)); - - // Parent directly calls spend on sub account - vm.prank(mainAccount.eoa); - subAccount.d.spend( - PARENT_SWEEP_SPEND_ID, - sweepTokens, - sweepAmounts, - address(mainAccount.eoa) // Sweep back to parent - ); - - // Verify parent recovered all the funds - assertEq(usdc.balanceOf(address(subAccount.eoa)), 0); - assertEq( - usdc.balanceOf(address(mainAccount.eoa)), mainAccountPreBalance + sweepAmounts[0] - ); - } - } -} diff --git a/test/UpgradeTests.t.sol b/test/UpgradeTests.t.sol new file mode 100644 index 00000000..d0d28d09 --- /dev/null +++ b/test/UpgradeTests.t.sol @@ -0,0 +1,566 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import "./utils/SoladyTest.sol"; +import {IthacaAccount} from "./utils/mocks/MockAccount.sol"; +import {GuardedExecutor} from "../src/IthacaAccount.sol"; +import {BaseTest} from "./Base.t.sol"; +import {EIP7702Proxy} from "solady/accounts/EIP7702Proxy.sol"; +import {LibEIP7702} from "solady/accounts/LibEIP7702.sol"; +import {MockPaymentToken} from "./utils/mocks/MockPaymentToken.sol"; +import {LibClone} from "solady/utils/LibClone.sol"; +import {Orchestrator, MockOrchestrator} from "./utils/mocks/MockOrchestrator.sol"; +import {ERC7821} from "solady/accounts/ERC7821.sol"; + +contract UpgradeTests is BaseTest { + address payable public oldProxyAddress; + address public oldImplementation; + address public newImplementation; + + // Test EOA that will be delegated to the proxy + address public userEOA; + uint256 public userEOAKey; + IthacaAccount public userAccount; + + // Test keys + PassKey public p256Key; + PassKey public p256SuperAdminKey; + PassKey public secp256k1Key; + PassKey public secp256k1SuperAdminKey; + PassKey public webAuthnP256Key; + PassKey public externalKey; + + // Test tokens + MockPaymentToken public mockToken1; + MockPaymentToken public mockToken2; + + // Random addresses for testing transfers + address[] public randomRecipients; + + // State capture - using simple mappings to avoid memory-to-storage issues + // Pre-upgrade state + bytes32[] preKeyHashes; + mapping(bytes32 => IthacaAccount.Key) preKeys; + mapping(bytes32 => bool) preAuthorized; + uint256 preEthBalance; + uint256 preToken1Balance; + uint256 preToken2Balance; + uint256 preNonce; + // Get expected old version from environment variable + string public expectedOldVersion = vm.envString("UPGRADE_TEST_OLD_VERSION"); + + // Post-upgrade state + bytes32[] postKeyHashes; + mapping(bytes32 => IthacaAccount.Key) postKeys; + mapping(bytes32 => bool) postAuthorized; + uint256 postEthBalance; + uint256 postToken1Balance; + uint256 postToken2Balance; + uint256 postNonce; + + function setUp() public override { + super.setUp(); + + // Fork the network to get the proxy bytecode + string memory rpcUrl = vm.envString("UPGRADE_TEST_RPC_URL"); + vm.createSelectFork(rpcUrl); + + // Deploy test tokens + mockToken1 = new MockPaymentToken(); + mockToken2 = new MockPaymentToken(); + + // Setup random recipients + for (uint256 i = 0; i < 5; i++) { + randomRecipients.push(_randomAddress()); + } + + // Get old proxy address from environment + oldProxyAddress = payable(vm.envAddress("UPGRADE_TEST_OLD_PROXY")); + + // Setup delegated EOA + (userEOA, userEOAKey) = _randomUniqueSigner(); + + vm.etch(userEOA, abi.encodePacked(hex"ef0100", address(oldProxyAddress))); + + userAccount = IthacaAccount(payable(userEOA)); + + // Get the bytecode of the old proxy from the forked network + bytes memory proxyBytecode = oldProxyAddress.code; + require(proxyBytecode.length > 0, "No bytecode at old proxy address"); + + newImplementation = address(new IthacaAccount(address(oc))); + + // Generate test keys + p256Key = _randomSecp256r1PassKey(); + p256Key.k.isSuperAdmin = false; + p256Key.k.expiry = 0; // Never expires + + p256SuperAdminKey = _randomSecp256r1PassKey(); + p256SuperAdminKey.k.isSuperAdmin = true; + p256SuperAdminKey.k.expiry = uint40(block.timestamp + 365 days); // Expires in 1 year + + secp256k1Key = _randomSecp256k1PassKey(); + secp256k1Key.k.isSuperAdmin = false; + secp256k1Key.k.expiry = 0; + + secp256k1SuperAdminKey = _randomSecp256k1PassKey(); + secp256k1SuperAdminKey.k.isSuperAdmin = true; + secp256k1SuperAdminKey.k.expiry = uint40(block.timestamp + 30 days); // Expires in 30 days + + webAuthnP256Key = _randomSecp256r1PassKey(); + webAuthnP256Key.k.keyType = IthacaAccount.KeyType.WebAuthnP256; + webAuthnP256Key.k.isSuperAdmin = false; + webAuthnP256Key.k.expiry = 0; + + // Setup external key + address externalSigner = _randomAddress(); + bytes12 salt = bytes12(uint96(_randomUniform())); + externalKey.k.keyType = IthacaAccount.KeyType.External; + externalKey.k.publicKey = abi.encodePacked(externalSigner, salt); + externalKey.k.isSuperAdmin = false; + externalKey.k.expiry = uint40(block.timestamp + 7 days); + externalKey.keyHash = _hash(externalKey.k); + } + + function test_ComprehensiveUpgrade() public { + // Step 2: Setup the old account with various configurations + _setupOldAccountState(); + + // Step 3: Capture pre-upgrade state + _capturePreUpgradeState(); + + // Step 4: Perform upgrade + _performUpgrade(); + + // Step 5: Capture post-upgrade state + _capturePostUpgradeState(); + + // Step 6: Verify state preservation + _verifyStatePreservation(); + + // Step 7: Test post-upgrade functionality + _testPostUpgradeFunctionality(); + } + + function _performUpgrade() internal { + // Get the version from the new implementation for comparison + IthacaAccount newImpl = IthacaAccount(payable(newImplementation)); + (,, string memory expectedNewVersion,,,,) = newImpl.eip712Domain(); + + // Check version before upgrade matches expected old version + (,, string memory versionBefore,,,,) = userAccount.eip712Domain(); + assertEq( + keccak256(bytes(versionBefore)), + keccak256(bytes(expectedOldVersion)), + string.concat("Version before upgrade should be ", expectedOldVersion) + ); + + // Perform the upgrade + bytes memory upgradeCalldata = + abi.encodeWithSelector(IthacaAccount.upgradeProxyAccount.selector, newImplementation); + + vm.prank(userEOA); + (bool success,) = userEOA.call(upgradeCalldata); + require(success, "Upgrade failed"); + + // Check version after upgrade + (,, string memory versionAfter,,,,) = userAccount.eip712Domain(); + + // Verify the version matches the new implementation version after upgrade + assertEq( + keccak256(bytes(versionAfter)), + keccak256(bytes(expectedNewVersion)), + string.concat("Version after upgrade should be ", expectedNewVersion) + ); + } + + function _setupOldAccountState() internal { + // Authorize various keys + vm.startPrank(userEOA); + + userAccount.authorize(p256Key.k); + p256Key.keyHash = _hash(p256Key.k); + + userAccount.authorize(secp256k1Key.k); + secp256k1Key.keyHash = _hash(secp256k1Key.k); + + userAccount.authorize(secp256k1SuperAdminKey.k); + secp256k1SuperAdminKey.keyHash = _hash(secp256k1SuperAdminKey.k); + + userAccount.authorize(webAuthnP256Key.k); + webAuthnP256Key.keyHash = _hash(webAuthnP256Key.k); + + externalKey.keyHash = userAccount.authorize(externalKey.k); + + // Setup spending limits for different keys + _setupSpendingLimits(); + + // Setup execution permissions + _setupExecutionPermissions(); + + // Fund the account + _fundAccount(); + + vm.stopPrank(); + } + + function _setupSpendingLimits() internal { + // Only set spending limits for non-super admin keys + + // Daily ETH limit for secp256k1Key (not a super admin) + if (secp256k1Key.keyHash != bytes32(0)) { + userAccount.setSpendLimit( + secp256k1Key.keyHash, address(0), GuardedExecutor.SpendPeriod.Day, 1 ether + ); + + // Weekly ETH limit for secp256k1Key + userAccount.setSpendLimit( + secp256k1Key.keyHash, address(0), GuardedExecutor.SpendPeriod.Week, 5 ether + ); + + // Monthly token1 limit for secp256k1Key + userAccount.setSpendLimit( + secp256k1Key.keyHash, + address(mockToken1), + GuardedExecutor.SpendPeriod.Month, + 1000e18 + ); + } + + // Daily token2 limit for webAuthnP256Key (not a super admin) + if (webAuthnP256Key.keyHash != bytes32(0)) { + userAccount.setSpendLimit( + webAuthnP256Key.keyHash, + address(mockToken2), + GuardedExecutor.SpendPeriod.Day, + 100e18 + ); + } + + // Hour ETH limit for externalKey (not a super admin) + if (externalKey.keyHash != bytes32(0)) { + userAccount.setSpendLimit( + externalKey.keyHash, address(0), GuardedExecutor.SpendPeriod.Hour, 0.1 ether + ); + } + + // Forever limit for p256Key (if authorized and not a super admin) + if (p256Key.keyHash != bytes32(0) && !p256Key.k.isSuperAdmin) { + userAccount.setSpendLimit( + p256Key.keyHash, address(0), GuardedExecutor.SpendPeriod.Forever, 10 ether + ); + } + } + + function _setupExecutionPermissions() internal { + // Setup canExecute permissions (only for non-super admin keys) + address target1 = address(0x1234); + address target2 = address(0x5678); + bytes4 selector1 = bytes4(keccak256("transfer(address,uint256)")); + bytes4 selector2 = bytes4(keccak256("approve(address,uint256)")); + + // Only set for non-super admin secp256k1Key + if (secp256k1Key.keyHash != bytes32(0) && !secp256k1Key.k.isSuperAdmin) { + userAccount.setCanExecute(secp256k1Key.keyHash, target1, selector1, true); + userAccount.setCanExecute(secp256k1Key.keyHash, target2, selector2, true); + } + + // Only set for p256Key if it's not a super admin + if (p256Key.keyHash != bytes32(0) && !p256Key.k.isSuperAdmin) { + userAccount.setCanExecute(p256Key.keyHash, target1, selector2, true); + } + + // Only set for webAuthnP256Key if it's not a super admin + if (webAuthnP256Key.keyHash != bytes32(0) && !webAuthnP256Key.k.isSuperAdmin) { + userAccount.setCanExecute(webAuthnP256Key.keyHash, target2, selector1, true); + } + + // Setup call checkers (only for non-super admin keys) + address checker1 = address(0xAAAA); + address checker2 = address(0xBBBB); + + if (secp256k1Key.keyHash != bytes32(0) && !secp256k1Key.k.isSuperAdmin) { + userAccount.setCallChecker(secp256k1Key.keyHash, target1, checker1); + } + + if (webAuthnP256Key.keyHash != bytes32(0) && !webAuthnP256Key.k.isSuperAdmin) { + userAccount.setCallChecker(webAuthnP256Key.keyHash, target2, checker2); + } + } + + function _fundAccount() internal { + // Fund with ETH + vm.deal(address(userAccount), 10 ether); + + // Fund with tokens + mockToken1.mint(address(userAccount), 10000e18); + mockToken2.mint(address(userAccount), 5000e18); + } + + function _capturePreUpgradeState() internal { + // Capture authorized keys + (, bytes32[] memory keyHashes) = userAccount.getKeys(); + + // Clear and populate pre-upgrade key hashes + delete preKeyHashes; + for (uint256 i = 0; i < keyHashes.length; i++) { + preKeyHashes.push(keyHashes[i]); + bytes32 keyHash = keyHashes[i]; + preKeys[keyHash] = userAccount.getKey(keyHash); + preAuthorized[keyHash] = true; + } + + // Capture balances + preEthBalance = address(userAccount).balance; + preToken1Balance = mockToken1.balanceOf(address(userAccount)); + preToken2Balance = mockToken2.balanceOf(address(userAccount)); + + // Capture nonce + preNonce = userAccount.getNonce(0); + } + + function _capturePostUpgradeState() internal { + // Capture authorized keys + (, bytes32[] memory keyHashes) = userAccount.getKeys(); + + // Clear and populate post-upgrade key hashes + delete postKeyHashes; + for (uint256 i = 0; i < keyHashes.length; i++) { + postKeyHashes.push(keyHashes[i]); + bytes32 keyHash = keyHashes[i]; + postKeys[keyHash] = userAccount.getKey(keyHash); + postAuthorized[keyHash] = true; + } + + // Capture balances + postEthBalance = address(userAccount).balance; + postToken1Balance = mockToken1.balanceOf(address(userAccount)); + postToken2Balance = mockToken2.balanceOf(address(userAccount)); + + // Capture nonce + postNonce = userAccount.getNonce(0); + } + + function _verifyStatePreservation() internal view { + // Verify all keys are preserved + assertEq(preKeyHashes.length, postKeyHashes.length, "Number of authorized keys changed"); + + for (uint256 i = 0; i < preKeyHashes.length; i++) { + bytes32 keyHash = preKeyHashes[i]; + + assertTrue(postAuthorized[keyHash], "Key was deauthorized during upgrade"); + + IthacaAccount.Key memory preKey = preKeys[keyHash]; + IthacaAccount.Key memory postKey = postKeys[keyHash]; + + assertEq(preKey.expiry, postKey.expiry, "Key expiry changed"); + assertEq(uint8(preKey.keyType), uint8(postKey.keyType), "Key type changed"); + assertEq(preKey.isSuperAdmin, postKey.isSuperAdmin, "Key super admin status changed"); + assertEq(preKey.publicKey, postKey.publicKey, "Key public key changed"); + } + + // Verify balances preserved + assertEq(preEthBalance, postEthBalance, "ETH balance changed"); + assertEq(preToken1Balance, postToken1Balance, "Token1 balance changed"); + assertEq(preToken2Balance, postToken2Balance, "Token2 balance changed"); + + // Verify nonce preserved + assertEq(preNonce, postNonce, "Nonce changed"); + } + + function _testPostUpgradeFunctionality() internal { + vm.startPrank(userEOA); + + // Test 1: P256 keys can now be super admins (new in v0.5.7+) + PassKey memory newP256SuperAdmin = _randomSecp256r1PassKey(); + newP256SuperAdmin.k.isSuperAdmin = true; + newP256SuperAdmin.k.expiry = 0; + + // This should succeed in upgraded version + bytes32 newP256KeyHash = userAccount.authorize(newP256SuperAdmin.k); + IthacaAccount.Key memory retrievedKey = userAccount.getKey(newP256KeyHash); + assertEq( + uint8(retrievedKey.keyType), uint8(IthacaAccount.KeyType.P256), "Key type mismatch" + ); + assertTrue(retrievedKey.isSuperAdmin, "P256 should be super admin after upgrade"); + + // Test 2: Add a new non-super-admin key and set spending limit + PassKey memory newRegularKey = _randomSecp256k1PassKey(); + newRegularKey.k.isSuperAdmin = false; + newRegularKey.k.expiry = 0; + + bytes32 newRegularKeyHash = userAccount.authorize(newRegularKey.k); + + // Set spending limit for the regular key (not super admin) + userAccount.setSpendLimit( + newRegularKeyHash, address(0), GuardedExecutor.SpendPeriod.Week, 2 ether + ); + + GuardedExecutor.SpendInfo[] memory spendInfos = userAccount.spendInfos(newRegularKeyHash); + bool foundWeeklyLimit = false; + for (uint256 i = 0; i < spendInfos.length; i++) { + if ( + spendInfos[i].period == GuardedExecutor.SpendPeriod.Week + && spendInfos[i].token == address(0) + ) { + assertEq(spendInfos[i].limit, 2 ether, "Weekly limit not set correctly"); + foundWeeklyLimit = true; + break; + } + } + assertTrue(foundWeeklyLimit, "Weekly limit not found"); + + // Test 3: Verify keys can still be used (without actual execution) + // We verify the key is still authorized and has correct properties + IthacaAccount.Key memory existingKey = userAccount.getKey(secp256k1Key.keyHash); + assertEq( + uint8(existingKey.keyType), uint8(IthacaAccount.KeyType.Secp256k1), "Key type changed" + ); + assertFalse(existingKey.isSuperAdmin, "Key admin status changed"); + + // Test 4: Test revoke and re-authorize with a new key + // Create a new key to test revoke/re-authorize functionality + PassKey memory testRevokeKey = _randomSecp256k1PassKey(); + testRevokeKey.k.isSuperAdmin = false; + testRevokeKey.k.expiry = 0; + + bytes32 testRevokeKeyHash = userAccount.authorize(testRevokeKey.k); + + // Now revoke it + userAccount.revoke(testRevokeKeyHash); + + // Verify key is revoked by checking it no longer exists + // After revocation, getKey will revert with KeyDoesNotExist + vm.expectRevert(abi.encodeWithSelector(IthacaAccount.KeyDoesNotExist.selector)); + userAccount.getKey(testRevokeKeyHash); + + // Re-authorize + bytes32 reauthorizedHash = userAccount.authorize(testRevokeKey.k); + assertEq(reauthorizedHash, testRevokeKeyHash, "Key hash changed on re-authorization"); + + vm.stopPrank(); + } + + function test_UpgradeWithSpendLimitEnabledFlag() public { + // This test verifies the spend limit enabled flag feature added in newer versions + + vm.startPrank(userEOA); + + // Authorize a key with spending limits + PassKey memory testKey = _randomSecp256k1PassKey(); + bytes32 keyHash = userAccount.authorize(testKey.k); + + // Set spending limit + userAccount.setSpendLimit(keyHash, address(0), GuardedExecutor.SpendPeriod.Day, 0.5 ether); + + // Fund account + vm.deal(address(userAccount), 5 ether); + + vm.stopPrank(); + + // Perform upgrade + _performUpgrade(); + + // Verify spending limits still work after upgrade + GuardedExecutor.SpendInfo[] memory spendInfos = userAccount.spendInfos(keyHash); + assertEq(spendInfos.length, 1, "Spending limit not preserved"); + assertEq(spendInfos[0].limit, 0.5 ether, "Spending limit value changed"); + + vm.stopPrank(); + } + + function test_UpgradeWithMultipleKeyTypes() public { + // Test upgrade with all key types authorized + + vm.startPrank(userEOA); + + // Authorize all key types + PassKey[] memory keys = new PassKey[](4); + keys[0] = _randomSecp256r1PassKey(); + keys[1] = _randomSecp256k1PassKey(); + keys[2] = _randomSecp256r1PassKey(); + keys[2].k.keyType = IthacaAccount.KeyType.WebAuthnP256; + keys[3].k.keyType = IthacaAccount.KeyType.External; + keys[3].k.publicKey = abi.encodePacked(_randomAddress(), bytes12(uint96(_randomUniform()))); + keys[3].keyHash = _hash(keys[3].k); + + bytes32[] memory keyHashes = new bytes32[](4); + for (uint256 i = 0; i < keys.length; i++) { + // Some key types might fail in old versions, handle gracefully + try userAccount.authorize(keys[i].k) returns (bytes32 kh) { + keyHashes[i] = kh; + } catch { + // Skip if authorization fails + } + } + + // Capture authorized count before upgrade + (, bytes32[] memory keyHashesBefore) = userAccount.getKeys(); + uint256 authorizedCountBefore = keyHashesBefore.length; + + vm.stopPrank(); + + // Perform upgrade + _performUpgrade(); + + // Verify all keys preserved + (, bytes32[] memory keyHashesAfter) = userAccount.getKeys(); + uint256 authorizedCountAfter = keyHashesAfter.length; + assertEq(authorizedCountBefore, authorizedCountAfter, "Key count changed during upgrade"); + + vm.stopPrank(); + } + + function test_UpgradePreservesComplexSpendingState() public { + // Test that complex spending state with partially spent limits is preserved + + vm.startPrank(userEOA); + + // Setup key and limits + PassKey memory spendKey = _randomSecp256k1PassKey(); + bytes32 keyHash = userAccount.authorize(spendKey.k); + + // Set multiple spending limits + userAccount.setSpendLimit(keyHash, address(0), GuardedExecutor.SpendPeriod.Day, 1 ether); + userAccount.setSpendLimit(keyHash, address(0), GuardedExecutor.SpendPeriod.Week, 3 ether); + userAccount.setSpendLimit(keyHash, address(0), GuardedExecutor.SpendPeriod.Month, 10 ether); + + // Fund account + vm.deal(address(userAccount), 20 ether); + vm.stopPrank(); + + // Capture spending state before upgrade + GuardedExecutor.SpendInfo[] memory spendsBefore = userAccount.spendInfos(keyHash); + + // Verify spending limits are set + uint256 limitsCount = 0; + for (uint256 i = 0; i < spendsBefore.length; i++) { + if (spendsBefore[i].token == address(0)) { + limitsCount++; + } + } + assertEq(limitsCount, 3, "Should have 3 ETH spending limits"); + + // Perform upgrade + _performUpgrade(); + + // Verify spending state preserved + GuardedExecutor.SpendInfo[] memory spendsAfter = userAccount.spendInfos(keyHash); + + // Verify all limits still exist + uint256 limitsCountAfter = 0; + for (uint256 i = 0; i < spendsAfter.length; i++) { + if (spendsAfter[i].token == address(0)) { + limitsCountAfter++; + } + } + assertEq(limitsCountAfter, 3, "Spending limits not preserved after upgrade"); + + // Verify limits match + assertEq(spendsBefore.length, spendsAfter.length, "Number of spending limits changed"); + for (uint256 i = 0; i < spendsBefore.length; i++) { + assertEq(spendsBefore[i].limit, spendsAfter[i].limit, "Limit value changed"); + assertEq(uint8(spendsBefore[i].period), uint8(spendsAfter[i].period), "Period changed"); + } + } +} diff --git a/test/utils/Brutalizer.sol b/test/utils/Brutalizer.sol index 969c5f86..1c6447ae 100644 --- a/test/utils/Brutalizer.sol +++ b/test/utils/Brutalizer.sol @@ -825,9 +825,7 @@ contract Brutalizer { let remainder := and(length, 0x1f) if remainder { if shl(mul(8, remainder), lastWord) { notZeroRightPadded := 1 } } // Check if the memory allocated is sufficient. - if length { - if gt(add(add(s, 0x20), length), mload(0x40)) { insufficientMalloc := 1 } - } + if length { if gt(add(add(s, 0x20), length), mload(0x40)) { insufficientMalloc := 1 } } } if (notZeroRightPadded) revert("Not zero right padded!"); if (insufficientMalloc) revert("Insufficient memory allocation!"); diff --git a/test/utils/interfaces/IPimlicoPaymaster.sol b/test/utils/interfaces/IPimlicoPaymaster.sol index d3d62e0b..2665aedc 100644 --- a/test/utils/interfaces/IPimlicoPaymaster.sol +++ b/test/utils/interfaces/IPimlicoPaymaster.sol @@ -19,10 +19,10 @@ library PimlicoHelpers { /// @notice The length of the mode and allowAllBundlers bytes. uint8 constant MODE_AND_ALLOW_ALL_BUNDLERS_LENGTH = 1; - /// @notice The length of the ERC-20 config without signature. + /// @notice The length of the ERC-20 config without singature. uint8 constant ERC20_PAYMASTER_DATA_LENGTH = 117; - /// @notice The length of the verifying config without signature. + /// @notice The length of the verfiying config without singature. uint8 constant VERIFYING_PAYMASTER_DATA_LENGTH = 12; // 12 uint256 constant PAYMASTER_DATA_OFFSET = 52; diff --git a/test/utils/interfaces/ISafe.sol b/test/utils/interfaces/ISafe.sol new file mode 100644 index 00000000..d9c3d3e1 --- /dev/null +++ b/test/utils/interfaces/ISafe.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import "./IERC4337EntryPoint.sol"; + +interface ISafe { + function setup( + address[] calldata _owners, + uint256 _threshold, + address to, + bytes calldata data, + address fallbackHandler, + address paymentToken, + uint256 payment, + address paymentReceiver + ) external; + + function enableModule(address module) external; + + function execTransaction( + address to, + uint256 value, + bytes calldata data, + uint8 operation, + uint256 safeTxGas, + uint256 baseGas, + uint256 gasPrice, + address gasToken, + address payable refundReceiver, + bytes memory signatures + ) external payable returns (bool success); + + function getFallbackHandler() external view returns (address); +} + +interface ISafeProxyFactory { + function createProxyWithNonce(address _singleton, bytes memory initializer, uint256 saltNonce) + external + returns (address proxy); +} + +interface ISafe4337Module { + function SUPPORTED_ENTRYPOINT() external view returns (address); + + function getOperationHash(UserOperation calldata userOp) + external + view + returns (bytes32 operationHash); + + function executeUserOp(address to, uint256 value, bytes calldata data, uint8 operation) external; + + function domainSeparator() external view returns (bytes32); +} + +interface IAddModulesLib { + function enableModules(address[] memory modules) external; +} diff --git a/test/utils/mocks/MockOrchestrator.sol b/test/utils/mocks/MockOrchestrator.sol index a0c3226e..e21a2360 100644 --- a/test/utils/mocks/MockOrchestrator.sol +++ b/test/utils/mocks/MockOrchestrator.sol @@ -15,13 +15,8 @@ contract MockOrchestrator is Orchestrator, Brutalizer { return _computeDigest(preCall); } - // Expose internal functions for testing - function hashTypedData(bytes32 structHash) public view returns (bytes32) { - return _hashTypedData(structHash); - } - - function hashTypedDataSansChainId(bytes32 structHash) public view returns (bytes32) { - return _hashTypedDataSansChainId(structHash); + function computeDigest(Intent calldata intent) public view returns (bytes32) { + return _computeDigest(intent); } function simulateFailed(bytes calldata encodedIntent) public payable virtual { diff --git a/test/utils/mocks/MockPayerWithSignature.sol b/test/utils/mocks/MockPayerWithSignature.sol index 60678b52..1de4c6d5 100644 --- a/test/utils/mocks/MockPayerWithSignature.sol +++ b/test/utils/mocks/MockPayerWithSignature.sol @@ -51,44 +51,38 @@ contract MockPayerWithSignature is Ownable { } /// @dev Pays `paymentAmount` of `paymentToken` to the `paymentRecipient`. - /// @param paymentAmount The amount to pay - /// @param keyHash The hash of the key used to authorize the operation - /// @param intentDigest The digest of the user operation - /// @param eoa The EOA address - /// @param payer The payer address - /// @param paymentToken The token to pay with - /// @param paymentRecipient The recipient of the payment - /// @param paymentSignature The payment signature + /// The EOA and token details are extracted from the `encodedIntent`. + /// Reverts if the specified Orchestrator (`msg.sender`) is not approved, + /// if the signature is invalid, or if the nonce has already been used. + /// @param paymentAmount The amount to pay. + /// @param keyHash The key hash associated with the operation (not used in this mock's logic but kept for signature compatibility). + /// @param digest The digest of the intent (used for nonce tracking). + /// @param encodedIntent ABI encoded Intent struct. function pay( uint256 paymentAmount, bytes32 keyHash, - bytes32 intentDigest, - address eoa, - address payer, - address paymentToken, - address paymentRecipient, - bytes calldata paymentSignature + bytes32 digest, + bytes calldata encodedIntent ) public virtual { if (!isApprovedOrchestrator[msg.sender]) revert Unauthorized(); // Check and set nonce to prevent replay attacks - if (paymasterNonces[intentDigest]) { + if (paymasterNonces[digest]) { revert PaymasterNonceError(); } - paymasterNonces[intentDigest] = true; + paymasterNonces[digest] = true; - bytes32 signatureDigest = computeSignatureDigest(intentDigest); + ICommon.Intent memory u = abi.decode(encodedIntent, (ICommon.Intent)); - if (ECDSA.recover(signatureDigest, paymentSignature) != signer) { + bytes32 signatureDigest = computeSignatureDigest(digest); + + if (ECDSA.recover(signatureDigest, u.paymentSignature) != signer) { revert InvalidSignature(); } - TokenTransferLib.safeTransfer(paymentToken, paymentRecipient, paymentAmount); - - emit Compensated(paymentToken, paymentRecipient, paymentAmount, eoa, keyHash); + TokenTransferLib.safeTransfer(u.paymentToken, u.paymentRecipient, paymentAmount); - // Unused parameters - payer; + emit Compensated(u.paymentToken, u.paymentRecipient, paymentAmount, u.eoa, keyHash); } function computeSignatureDigest(bytes32 intentDigest) public view returns (bytes32) { diff --git a/test/utils/mocks/MockPayerWithSignatureOptimized.sol b/test/utils/mocks/MockPayerWithSignatureOptimized.sol new file mode 100644 index 00000000..1d7701bf --- /dev/null +++ b/test/utils/mocks/MockPayerWithSignatureOptimized.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import {TokenTransferLib} from "../../../src/libraries/TokenTransferLib.sol"; +import {Ownable} from "solady/auth/Ownable.sol"; +import {ECDSA} from "solady/utils/ECDSA.sol"; +import {ICommon} from "../../../src/interfaces/ICommon.sol"; +import {IOrchestrator} from "../../../src/interfaces/IOrchestrator.sol"; +/// @dev WARNING! This mock is strictly intended for testing purposes only. +/// Do NOT copy anything here into production code unless you really know what you are doing. + +contract MockPayerWithSignatureOptimized is Ownable { + error InvalidSignature(); + + address public signer; + + address public immutable APPROVED_ORCHESTRATOR; + + event Compensated( + address indexed paymentToken, + address indexed paymentRecipient, + uint256 paymentAmount, + address indexed eoa, + bytes32 keyHash + ); + + constructor(address orchestrator) { + APPROVED_ORCHESTRATOR = orchestrator; + _initializeOwner(msg.sender); + } + + function setSigner(address newSinger) public onlyOwner { + signer = newSinger; + } + + /// @dev `address(0)` denote native token (i.e. Ether). + function withdrawTokens(address token, address recipient, uint256 amount) + public + virtual + onlyOwner + { + TokenTransferLib.safeTransfer(token, recipient, amount); + } + + /// @dev Pays `paymentAmount` of `paymentToken` to the `paymentRecipient`. + /// The EOA and token details are extracted from the `encodedIntent`. + /// Reverts if the specified Orchestrator (`msg.sender`) is not approved. + /// NOTE: This mock no longer verifies signatures within the pay function itself, + /// aligning with the Account/Orchestrator pattern where verification happens before payment. + /// @param paymentAmount The amount to pay. + /// @param keyHash The key hash associated with the operation (not used in this mock's logic but kept for signature compatibility). + /// @param encodedIntent ABI encoded Intent struct. + function pay( + uint256 paymentAmount, + bytes32 keyHash, + bytes32 digest, + bytes calldata encodedIntent + ) public virtual { + if (msg.sender != APPROVED_ORCHESTRATOR) revert Unauthorized(); + + ICommon.Intent calldata u; + assembly { + let t := calldataload(encodedIntent.offset) + u := add(t, encodedIntent.offset) + } + + bytes32 signatureDigest = computeSignatureDigest(digest); + + if (ECDSA.recover(signatureDigest, u.paymentSignature) != signer) { + revert InvalidSignature(); + } + + TokenTransferLib.safeTransfer(u.paymentToken, u.paymentRecipient, paymentAmount); + + emit Compensated(u.paymentToken, u.paymentRecipient, paymentAmount, u.eoa, keyHash); + } + + function computeSignatureDigest(bytes32 intentDigest) public view returns (bytes32) { + // We shall just use this simplified hash instead of EIP712. + return keccak256(abi.encode(intentDigest, block.chainid, address(this))); + } + + receive() external payable {} +} diff --git a/test/utils/mocks/MockPayerWithState.sol b/test/utils/mocks/MockPayerWithState.sol index e5e4c35d..365db440 100644 --- a/test/utils/mocks/MockPayerWithState.sol +++ b/test/utils/mocks/MockPayerWithState.sol @@ -54,42 +54,35 @@ contract MockPayerWithState is Ownable { } /// @dev Pays `paymentAmount` of `paymentToken` to the `paymentRecipient`. - /// @param paymentAmount The amount to pay - /// @param keyHash The hash of the key used to authorize the operation - /// @param intentDigest The digest of the user operation - /// @param eoa The EOA address - /// @param payer The payer address - /// @param paymentToken The token to pay with - /// @param paymentRecipient The recipient of the payment - /// @param paymentSignature The payment signature + /// The EOA and token details are extracted from the `encodedIntent`. + /// Reverts if the specified Orchestrator (`msg.sender`) is not approved, + /// if the EOA lacks sufficient funds, or if the nonce has already been used. + /// @param paymentAmount The amount to pay. + /// @param keyHash The key hash associated with the operation (not used in this mock's logic but kept for signature compatibility). + /// @param digest The digest of the intent (used for nonce tracking). + /// @param encodedIntent ABI encoded Intent struct. function pay( uint256 paymentAmount, bytes32 keyHash, - bytes32 intentDigest, - address eoa, - address payer, - address paymentToken, - address paymentRecipient, - bytes calldata paymentSignature + bytes32 digest, + bytes calldata encodedIntent ) public virtual { if (!isApprovedOrchestrator[msg.sender]) revert Unauthorized(); // Check and set nonce to prevent replay attacks - if (paymasterNonces[intentDigest]) { + if (paymasterNonces[digest]) { revert PaymasterNonceError(); } - paymasterNonces[intentDigest] = true; + paymasterNonces[digest] = true; + + ICommon.Intent memory u = abi.decode(encodedIntent, (ICommon.Intent)); // We shall rely on arithmetic underflow error to revert if there's insufficient funds. - funds[paymentToken][eoa] -= paymentAmount; - TokenTransferLib.safeTransfer(paymentToken, paymentRecipient, paymentAmount); + funds[u.paymentToken][u.eoa] -= paymentAmount; + TokenTransferLib.safeTransfer(u.paymentToken, u.paymentRecipient, paymentAmount); // Emit the event for debugging. - emit Compensated(paymentToken, paymentRecipient, paymentAmount, eoa, keyHash); - - // Unused parameters - payer; - paymentSignature; + emit Compensated(u.paymentToken, u.paymentRecipient, paymentAmount, u.eoa, keyHash); } receive() external payable {} diff --git a/utils/JsonBindings.sol b/utils/JsonBindings.sol deleted file mode 100644 index 1a76c5b2..00000000 --- a/utils/JsonBindings.sol +++ /dev/null @@ -1,767 +0,0 @@ -// Automatically generated by forge bind-json. - -pragma solidity >=0.6.2 <0.9.0; -pragma experimental ABIEncoderV2; - -import {GuardedExecutor} from "src/GuardedExecutor.sol"; -import {IthacaAccount} from "src/IthacaAccount.sol"; -import {MultiSigSigner} from "src/MultiSigSigner.sol"; -import {ICommon} from "src/interfaces/ICommon.sol"; -import {IEscrow} from "src/interfaces/IEscrow.sol"; -import {LibTStack} from "src/libraries/LibTStack.sol"; -import {AccountTest} from "test/Account.t.sol"; -import {BaseTest} from "test/Base.t.sol"; -import {EnforcedOptionParam} from "test/LayerZeroSettler.t.sol"; -import {MultiSigSignerTest} from "test/MultiSigSigner.t.sol"; -import {OrchestratorTest} from "test/Orchestrator.t.sol"; -import {SimulateExecuteTest} from "test/SimulateExecute.t.sol"; -import {SignatureWrapper} from "test/utils/interfaces/ICoinbaseSmartWallet.sol"; -import {IERC4337EntryPoint, IStakeManager, PackedUserOperation, UserOperation} from "test/utils/interfaces/IERC4337EntryPoint.sol"; - -interface Vm { - function parseJsonTypeArray(string calldata json, string calldata key, string calldata typeDescription) external pure returns (bytes memory); - function parseJsonType(string calldata json, string calldata typeDescription) external pure returns (bytes memory); - function parseJsonType(string calldata json, string calldata key, string calldata typeDescription) external pure returns (bytes memory); - function serializeJsonType(string calldata typeDescription, bytes memory value) external pure returns (string memory json); - function serializeJsonType(string calldata objectKey, string calldata valueKey, string calldata typeDescription, bytes memory value) external returns (string memory json); -} - -library JsonBindings { - Vm constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); - - string constant schema_Intent = "Intent(address eoa,bytes executionData,uint256 nonce,address payer,address paymentToken,uint256 paymentMaxAmount,uint256 combinedGas,bytes[] encodedPreCalls,bytes[] encodedFundTransfers,address settler,uint256 expiry,bool isMultichain,address funder,bytes funderSignature,bytes settlerContext,uint256 paymentAmount,address paymentRecipient,bytes signature,bytes paymentSignature,address supportedAccountImplementation)"; - string constant schema_SignedCall = "SignedCall(address eoa,bytes executionData,uint256 nonce,bytes signature)"; - string constant schema_Transfer = "Transfer(address token,uint256 amount)"; - string constant schema_SpendInfo = "SpendInfo(address token,uint8 period,uint256 limit,uint256 spent,uint256 lastUpdated,uint256 currentSpent,uint256 current)"; - string constant schema_CallCheckerInfo = "CallCheckerInfo(address target,address checker)"; - string constant schema_TokenPeriodSpend = "TokenPeriodSpend(uint256 limit,uint256 spent,uint256 lastUpdated)"; - string constant schema__ExecuteTemps = "_ExecuteTemps(DynamicArray approvedERC20s,DynamicArray approvalSpenders,DynamicArray erc20s,DynamicArray transferAmounts,DynamicArray permit2ERC20s,DynamicArray permit2Spenders)DynamicArray(uint256[] data)"; - string constant schema_TStack = "TStack(uint256 slot)"; - string constant schema_Key = "Key(uint40 expiry,uint8 keyType,bool isSuperAdmin,bytes publicKey)"; - string constant schema_KeyExtraStorage = "KeyExtraStorage(AddressSet checkers)AddressSet(uint256 _spacer)"; - string constant schema_Escrow = "Escrow(bytes12 salt,address depositor,address recipient,address token,uint256 escrowAmount,uint256 refundAmount,uint256 refundTimestamp,address settler,address sender,bytes32 settlementId,uint256 senderChainId)"; - string constant schema_Config = "Config(uint256 threshold,bytes32[] ownerKeyHashes)"; - string constant schema_TargetFunctionPayload = "TargetFunctionPayload(address by,uint256 value,bytes data)"; - string constant schema_PassKey = "PassKey(Key k,uint256 privateKey,bytes32 keyHash)Key(uint40 expiry,uint8 keyType,bool isSuperAdmin,bytes publicKey)"; - string constant schema_MultiSigKey = "MultiSigKey(Key k,uint256 threshold,PassKey[] owners)Key(uint40 expiry,uint8 keyType,bool isSuperAdmin,bytes publicKey)PassKey(Key k,uint256 privateKey,bytes32 keyHash)"; - string constant schema_DelegatedEOA = "DelegatedEOA(address eoa,uint256 privateKey,address d)"; - string constant schema__EstimateGasParams = "_EstimateGasParams(Intent u,uint8 paymentPerGasPrecision,uint256 paymentPerGas,uint256 combinedGasIncrement,uint256 combinedGasVerificationOffset)Intent(address eoa,bytes executionData,uint256 nonce,address payer,address paymentToken,uint256 paymentMaxAmount,uint256 combinedGas,bytes[] encodedPreCalls,bytes[] encodedFundTransfers,address settler,uint256 expiry,bool isMultichain,address funder,bytes funderSignature,bytes settlerContext,uint256 paymentAmount,address paymentRecipient,bytes signature,bytes paymentSignature,address supportedAccountImplementation)"; - string constant schema__TestExecuteWithSignatureTemps = "_TestExecuteWithSignatureTemps(TargetFunctionPayload[] targetFunctionPayloads,Call[] calls,uint256 n,uint256 nonce,bytes opData,bytes executionData)Call(address to,uint256 value,bytes data)TargetFunctionPayload(address by,uint256 value,bytes data)"; - string constant schema__TestUpgradeAccountWithPassKeyTemps = "_TestUpgradeAccountWithPassKeyTemps(uint256 randomVersion,address implementation,Call[] calls,uint256 nonce,bytes opData,bytes executionData)Call(address to,uint256 value,bytes data)"; - string constant schema_DepositInfo = "DepositInfo(uint256 deposit,bool staked,uint112 stake,uint32 unstakeDelaySec,uint48 withdrawTime)"; - string constant schema_StakeInfo = "StakeInfo(uint256 stake,uint256 unstakeDelaySec)"; - string constant schema_PackedUserOperation = "PackedUserOperation(address sender,uint256 nonce,bytes initCode,bytes callData,bytes32 accountGasLimits,uint256 preVerificationGas,bytes32 gasFees,bytes paymasterAndData,bytes signature)"; - string constant schema_UserOperation = "UserOperation(address sender,uint256 nonce,bytes initCode,bytes callData,uint256 callGasLimit,uint256 verificationGasLimit,uint256 preVerificationGas,uint256 maxFeePerGas,uint256 maxPriorityFeePerGas,bytes paymasterAndData,bytes signature)"; - string constant schema_UserOpsPerAggregator = "UserOpsPerAggregator(PackedUserOperation[] userOps,address aggregator,bytes signature)PackedUserOperation(address sender,uint256 nonce,bytes initCode,bytes callData,bytes32 accountGasLimits,uint256 preVerificationGas,bytes32 gasFees,bytes paymasterAndData,bytes signature)"; - string constant schema_ReturnInfo = "ReturnInfo(uint256 preOpGas,uint256 prefund,uint256 accountValidationData,uint256 paymasterValidationData,bytes paymasterContext)"; - string constant schema_SignatureWrapper = "SignatureWrapper(uint8 ownerIndex,bytes signatureData)"; - string constant schema_EnforcedOptionParam = "EnforcedOptionParam(uint32 eid,uint16 msgType,bytes options)"; - string constant schema_MultiSigTestTemps = "MultiSigTestTemps(PassKey[] owners,bytes32[] ownerKeyHashes,uint256 threshold,MultiSigKey multiSigKey,bytes32 digest)Key(uint40 expiry,uint8 keyType,bool isSuperAdmin,bytes publicKey)MultiSigKey(Key k,uint256 threshold,PassKey[] owners)PassKey(Key k,uint256 privateKey,bytes32 keyHash)"; - string constant schema__TestFullFlowTemps = "_TestFullFlowTemps(Intent[] intents,TargetFunctionPayload[] targetFunctionPayloads,DelegatedEOA[] delegatedEOAs,bytes[] encodedIntents)DelegatedEOA(address eoa,uint256 privateKey,address d)Intent(address eoa,bytes executionData,uint256 nonce,address payer,address paymentToken,uint256 paymentMaxAmount,uint256 combinedGas,bytes[] encodedPreCalls,bytes[] encodedFundTransfers,address settler,uint256 expiry,bool isMultichain,address funder,bytes funderSignature,bytes settlerContext,uint256 paymentAmount,address paymentRecipient,bytes signature,bytes paymentSignature,address supportedAccountImplementation)TargetFunctionPayload(address by,uint256 value,bytes data)"; - string constant schema__TestAuthorizeWithPreCallsAndTransferTemps = "_TestAuthorizeWithPreCallsAndTransferTemps(uint256 gExecute,uint256 gCombined,uint256 gUsed,bool success,bytes result,bool testInvalidPreCallEOA,bool testPreCallVerificationError,bool testPreCallError,bool testInit,bool testEOACoalesce,bool testSkipNonce,uint192 superAdminNonceSeqKey,uint192 sessionNonceSeqKey,uint256 retrievedSuperAdminNonce,uint256 retrievedSessionNonce,PassKey kInit,DelegatedEOA d,address eoa)DelegatedEOA(address eoa,uint256 privateKey,address d)Key(uint40 expiry,uint8 keyType,bool isSuperAdmin,bytes publicKey)PassKey(Key k,uint256 privateKey,bytes32 keyHash)"; - string constant schema__TestPayViaAnotherPayerTemps = "_TestPayViaAnotherPayerTemps(address withState,address withSignature,DelegatedEOA withSignatureEOA,address token,uint256 funds,bool isWithState,bool corruptSignature,bool unapprovedOrchestrator,uint256 balanceBefore,DelegatedEOA d)DelegatedEOA(address eoa,uint256 privateKey,address d)"; - string constant schema__TestAccountImplementationVerificationTemps = "_TestAccountImplementationVerificationTemps(bool testImplementationCheck,bool requireWrongImplementation,DelegatedEOA d)DelegatedEOA(address eoa,uint256 privateKey,address d)"; - string constant schema__TestMultiSigTemps = "_TestMultiSigTemps(DelegatedEOA d,address multiSigSigner,uint256 numKeys,MultiSigKey multiSigKey)DelegatedEOA(address eoa,uint256 privateKey,address d)Key(uint40 expiry,uint8 keyType,bool isSuperAdmin,bytes publicKey)MultiSigKey(Key k,uint256 threshold,PassKey[] owners)PassKey(Key k,uint256 privateKey,bytes32 keyHash)"; - string constant schema__TestMultiChainIntentTemps = "_TestMultiChainIntentTemps(address funder,address settler,uint256 funderPrivateKey,address usdcMainnet,address usdcArb,address usdcBase,DelegatedEOA d,PassKey k,Intent baseIntent,Intent arbIntent,Intent outputIntent,bytes32[] leafs,bytes32 root,bytes rootSig,address gasWallet,address relay,address friend,address settlementOracle,address escrowBase,address escrowArb,bytes32 settlementId,bytes32 escrowIdBase,bytes32 escrowIdArb,bytes[] encodedIntents,bytes4[] errs,uint256 snapshot)DelegatedEOA(address eoa,uint256 privateKey,address d)Intent(address eoa,bytes executionData,uint256 nonce,address payer,address paymentToken,uint256 paymentMaxAmount,uint256 combinedGas,bytes[] encodedPreCalls,bytes[] encodedFundTransfers,address settler,uint256 expiry,bool isMultichain,address funder,bytes funderSignature,bytes settlerContext,uint256 paymentAmount,address paymentRecipient,bytes signature,bytes paymentSignature,address supportedAccountImplementation)Key(uint40 expiry,uint8 keyType,bool isSuperAdmin,bytes publicKey)PassKey(Key k,uint256 privateKey,bytes32 keyHash)"; - string constant schema__SimulateExecuteTemps = "_SimulateExecuteTemps(uint256 gasToBurn,uint256 randomness,uint256 gExecute,uint256 gCombined,uint256 gUsed,bytes executionData,bool success,bytes result)"; - - function serialize(ICommon.Intent memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema_Intent, abi.encode(value)); - } - - function serialize(ICommon.Intent memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema_Intent, abi.encode(value)); - } - - function deserializeIntent(string memory json) public pure returns (ICommon.Intent memory) { - return abi.decode(vm.parseJsonType(json, schema_Intent), (ICommon.Intent)); - } - - function deserializeIntent(string memory json, string memory path) public pure returns (ICommon.Intent memory) { - return abi.decode(vm.parseJsonType(json, path, schema_Intent), (ICommon.Intent)); - } - - function deserializeIntentArray(string memory json, string memory path) public pure returns (ICommon.Intent[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema_Intent), (ICommon.Intent[])); - } - - function serialize(ICommon.SignedCall memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema_SignedCall, abi.encode(value)); - } - - function serialize(ICommon.SignedCall memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema_SignedCall, abi.encode(value)); - } - - function deserializeSignedCall(string memory json) public pure returns (ICommon.SignedCall memory) { - return abi.decode(vm.parseJsonType(json, schema_SignedCall), (ICommon.SignedCall)); - } - - function deserializeSignedCall(string memory json, string memory path) public pure returns (ICommon.SignedCall memory) { - return abi.decode(vm.parseJsonType(json, path, schema_SignedCall), (ICommon.SignedCall)); - } - - function deserializeSignedCallArray(string memory json, string memory path) public pure returns (ICommon.SignedCall[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema_SignedCall), (ICommon.SignedCall[])); - } - - function serialize(ICommon.Transfer memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema_Transfer, abi.encode(value)); - } - - function serialize(ICommon.Transfer memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema_Transfer, abi.encode(value)); - } - - function deserializeTransfer(string memory json) public pure returns (ICommon.Transfer memory) { - return abi.decode(vm.parseJsonType(json, schema_Transfer), (ICommon.Transfer)); - } - - function deserializeTransfer(string memory json, string memory path) public pure returns (ICommon.Transfer memory) { - return abi.decode(vm.parseJsonType(json, path, schema_Transfer), (ICommon.Transfer)); - } - - function deserializeTransferArray(string memory json, string memory path) public pure returns (ICommon.Transfer[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema_Transfer), (ICommon.Transfer[])); - } - - function serialize(GuardedExecutor.SpendInfo memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema_SpendInfo, abi.encode(value)); - } - - function serialize(GuardedExecutor.SpendInfo memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema_SpendInfo, abi.encode(value)); - } - - function deserializeSpendInfo(string memory json) public pure returns (GuardedExecutor.SpendInfo memory) { - return abi.decode(vm.parseJsonType(json, schema_SpendInfo), (GuardedExecutor.SpendInfo)); - } - - function deserializeSpendInfo(string memory json, string memory path) public pure returns (GuardedExecutor.SpendInfo memory) { - return abi.decode(vm.parseJsonType(json, path, schema_SpendInfo), (GuardedExecutor.SpendInfo)); - } - - function deserializeSpendInfoArray(string memory json, string memory path) public pure returns (GuardedExecutor.SpendInfo[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema_SpendInfo), (GuardedExecutor.SpendInfo[])); - } - - function serialize(GuardedExecutor.CallCheckerInfo memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema_CallCheckerInfo, abi.encode(value)); - } - - function serialize(GuardedExecutor.CallCheckerInfo memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema_CallCheckerInfo, abi.encode(value)); - } - - function deserializeCallCheckerInfo(string memory json) public pure returns (GuardedExecutor.CallCheckerInfo memory) { - return abi.decode(vm.parseJsonType(json, schema_CallCheckerInfo), (GuardedExecutor.CallCheckerInfo)); - } - - function deserializeCallCheckerInfo(string memory json, string memory path) public pure returns (GuardedExecutor.CallCheckerInfo memory) { - return abi.decode(vm.parseJsonType(json, path, schema_CallCheckerInfo), (GuardedExecutor.CallCheckerInfo)); - } - - function deserializeCallCheckerInfoArray(string memory json, string memory path) public pure returns (GuardedExecutor.CallCheckerInfo[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema_CallCheckerInfo), (GuardedExecutor.CallCheckerInfo[])); - } - - function serialize(GuardedExecutor.TokenPeriodSpend memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema_TokenPeriodSpend, abi.encode(value)); - } - - function serialize(GuardedExecutor.TokenPeriodSpend memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema_TokenPeriodSpend, abi.encode(value)); - } - - function deserializeTokenPeriodSpend(string memory json) public pure returns (GuardedExecutor.TokenPeriodSpend memory) { - return abi.decode(vm.parseJsonType(json, schema_TokenPeriodSpend), (GuardedExecutor.TokenPeriodSpend)); - } - - function deserializeTokenPeriodSpend(string memory json, string memory path) public pure returns (GuardedExecutor.TokenPeriodSpend memory) { - return abi.decode(vm.parseJsonType(json, path, schema_TokenPeriodSpend), (GuardedExecutor.TokenPeriodSpend)); - } - - function deserializeTokenPeriodSpendArray(string memory json, string memory path) public pure returns (GuardedExecutor.TokenPeriodSpend[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema_TokenPeriodSpend), (GuardedExecutor.TokenPeriodSpend[])); - } - - function serialize(GuardedExecutor._ExecuteTemps memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema__ExecuteTemps, abi.encode(value)); - } - - function serialize(GuardedExecutor._ExecuteTemps memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema__ExecuteTemps, abi.encode(value)); - } - - function deserialize_ExecuteTemps(string memory json) public pure returns (GuardedExecutor._ExecuteTemps memory) { - return abi.decode(vm.parseJsonType(json, schema__ExecuteTemps), (GuardedExecutor._ExecuteTemps)); - } - - function deserialize_ExecuteTemps(string memory json, string memory path) public pure returns (GuardedExecutor._ExecuteTemps memory) { - return abi.decode(vm.parseJsonType(json, path, schema__ExecuteTemps), (GuardedExecutor._ExecuteTemps)); - } - - function deserialize_ExecuteTempsArray(string memory json, string memory path) public pure returns (GuardedExecutor._ExecuteTemps[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema__ExecuteTemps), (GuardedExecutor._ExecuteTemps[])); - } - - function serialize(LibTStack.TStack memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema_TStack, abi.encode(value)); - } - - function serialize(LibTStack.TStack memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema_TStack, abi.encode(value)); - } - - function deserializeTStack(string memory json) public pure returns (LibTStack.TStack memory) { - return abi.decode(vm.parseJsonType(json, schema_TStack), (LibTStack.TStack)); - } - - function deserializeTStack(string memory json, string memory path) public pure returns (LibTStack.TStack memory) { - return abi.decode(vm.parseJsonType(json, path, schema_TStack), (LibTStack.TStack)); - } - - function deserializeTStackArray(string memory json, string memory path) public pure returns (LibTStack.TStack[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema_TStack), (LibTStack.TStack[])); - } - - function serialize(IthacaAccount.Key memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema_Key, abi.encode(value)); - } - - function serialize(IthacaAccount.Key memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema_Key, abi.encode(value)); - } - - function deserializeKey(string memory json) public pure returns (IthacaAccount.Key memory) { - return abi.decode(vm.parseJsonType(json, schema_Key), (IthacaAccount.Key)); - } - - function deserializeKey(string memory json, string memory path) public pure returns (IthacaAccount.Key memory) { - return abi.decode(vm.parseJsonType(json, path, schema_Key), (IthacaAccount.Key)); - } - - function deserializeKeyArray(string memory json, string memory path) public pure returns (IthacaAccount.Key[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema_Key), (IthacaAccount.Key[])); - } - - function serialize(IthacaAccount.KeyExtraStorage memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema_KeyExtraStorage, abi.encode(value)); - } - - function serialize(IthacaAccount.KeyExtraStorage memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema_KeyExtraStorage, abi.encode(value)); - } - - function deserializeKeyExtraStorage(string memory json) public pure returns (IthacaAccount.KeyExtraStorage memory) { - return abi.decode(vm.parseJsonType(json, schema_KeyExtraStorage), (IthacaAccount.KeyExtraStorage)); - } - - function deserializeKeyExtraStorage(string memory json, string memory path) public pure returns (IthacaAccount.KeyExtraStorage memory) { - return abi.decode(vm.parseJsonType(json, path, schema_KeyExtraStorage), (IthacaAccount.KeyExtraStorage)); - } - - function deserializeKeyExtraStorageArray(string memory json, string memory path) public pure returns (IthacaAccount.KeyExtraStorage[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema_KeyExtraStorage), (IthacaAccount.KeyExtraStorage[])); - } - - function serialize(IEscrow.Escrow memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema_Escrow, abi.encode(value)); - } - - function serialize(IEscrow.Escrow memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema_Escrow, abi.encode(value)); - } - - function deserializeEscrow(string memory json) public pure returns (IEscrow.Escrow memory) { - return abi.decode(vm.parseJsonType(json, schema_Escrow), (IEscrow.Escrow)); - } - - function deserializeEscrow(string memory json, string memory path) public pure returns (IEscrow.Escrow memory) { - return abi.decode(vm.parseJsonType(json, path, schema_Escrow), (IEscrow.Escrow)); - } - - function deserializeEscrowArray(string memory json, string memory path) public pure returns (IEscrow.Escrow[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema_Escrow), (IEscrow.Escrow[])); - } - - function serialize(MultiSigSigner.Config memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema_Config, abi.encode(value)); - } - - function serialize(MultiSigSigner.Config memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema_Config, abi.encode(value)); - } - - function deserializeConfig(string memory json) public pure returns (MultiSigSigner.Config memory) { - return abi.decode(vm.parseJsonType(json, schema_Config), (MultiSigSigner.Config)); - } - - function deserializeConfig(string memory json, string memory path) public pure returns (MultiSigSigner.Config memory) { - return abi.decode(vm.parseJsonType(json, path, schema_Config), (MultiSigSigner.Config)); - } - - function deserializeConfigArray(string memory json, string memory path) public pure returns (MultiSigSigner.Config[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema_Config), (MultiSigSigner.Config[])); - } - - function serialize(BaseTest.TargetFunctionPayload memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema_TargetFunctionPayload, abi.encode(value)); - } - - function serialize(BaseTest.TargetFunctionPayload memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema_TargetFunctionPayload, abi.encode(value)); - } - - function deserializeTargetFunctionPayload(string memory json) public pure returns (BaseTest.TargetFunctionPayload memory) { - return abi.decode(vm.parseJsonType(json, schema_TargetFunctionPayload), (BaseTest.TargetFunctionPayload)); - } - - function deserializeTargetFunctionPayload(string memory json, string memory path) public pure returns (BaseTest.TargetFunctionPayload memory) { - return abi.decode(vm.parseJsonType(json, path, schema_TargetFunctionPayload), (BaseTest.TargetFunctionPayload)); - } - - function deserializeTargetFunctionPayloadArray(string memory json, string memory path) public pure returns (BaseTest.TargetFunctionPayload[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema_TargetFunctionPayload), (BaseTest.TargetFunctionPayload[])); - } - - function serialize(BaseTest.PassKey memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema_PassKey, abi.encode(value)); - } - - function serialize(BaseTest.PassKey memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema_PassKey, abi.encode(value)); - } - - function deserializePassKey(string memory json) public pure returns (BaseTest.PassKey memory) { - return abi.decode(vm.parseJsonType(json, schema_PassKey), (BaseTest.PassKey)); - } - - function deserializePassKey(string memory json, string memory path) public pure returns (BaseTest.PassKey memory) { - return abi.decode(vm.parseJsonType(json, path, schema_PassKey), (BaseTest.PassKey)); - } - - function deserializePassKeyArray(string memory json, string memory path) public pure returns (BaseTest.PassKey[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema_PassKey), (BaseTest.PassKey[])); - } - - function serialize(BaseTest.MultiSigKey memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema_MultiSigKey, abi.encode(value)); - } - - function serialize(BaseTest.MultiSigKey memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema_MultiSigKey, abi.encode(value)); - } - - function deserializeMultiSigKey(string memory json) public pure returns (BaseTest.MultiSigKey memory) { - return abi.decode(vm.parseJsonType(json, schema_MultiSigKey), (BaseTest.MultiSigKey)); - } - - function deserializeMultiSigKey(string memory json, string memory path) public pure returns (BaseTest.MultiSigKey memory) { - return abi.decode(vm.parseJsonType(json, path, schema_MultiSigKey), (BaseTest.MultiSigKey)); - } - - function deserializeMultiSigKeyArray(string memory json, string memory path) public pure returns (BaseTest.MultiSigKey[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema_MultiSigKey), (BaseTest.MultiSigKey[])); - } - - function serialize(BaseTest.DelegatedEOA memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema_DelegatedEOA, abi.encode(value)); - } - - function serialize(BaseTest.DelegatedEOA memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema_DelegatedEOA, abi.encode(value)); - } - - function deserializeDelegatedEOA(string memory json) public pure returns (BaseTest.DelegatedEOA memory) { - return abi.decode(vm.parseJsonType(json, schema_DelegatedEOA), (BaseTest.DelegatedEOA)); - } - - function deserializeDelegatedEOA(string memory json, string memory path) public pure returns (BaseTest.DelegatedEOA memory) { - return abi.decode(vm.parseJsonType(json, path, schema_DelegatedEOA), (BaseTest.DelegatedEOA)); - } - - function deserializeDelegatedEOAArray(string memory json, string memory path) public pure returns (BaseTest.DelegatedEOA[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema_DelegatedEOA), (BaseTest.DelegatedEOA[])); - } - - function serialize(BaseTest._EstimateGasParams memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema__EstimateGasParams, abi.encode(value)); - } - - function serialize(BaseTest._EstimateGasParams memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema__EstimateGasParams, abi.encode(value)); - } - - function deserialize_EstimateGasParams(string memory json) public pure returns (BaseTest._EstimateGasParams memory) { - return abi.decode(vm.parseJsonType(json, schema__EstimateGasParams), (BaseTest._EstimateGasParams)); - } - - function deserialize_EstimateGasParams(string memory json, string memory path) public pure returns (BaseTest._EstimateGasParams memory) { - return abi.decode(vm.parseJsonType(json, path, schema__EstimateGasParams), (BaseTest._EstimateGasParams)); - } - - function deserialize_EstimateGasParamsArray(string memory json, string memory path) public pure returns (BaseTest._EstimateGasParams[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema__EstimateGasParams), (BaseTest._EstimateGasParams[])); - } - - function serialize(AccountTest._TestExecuteWithSignatureTemps memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema__TestExecuteWithSignatureTemps, abi.encode(value)); - } - - function serialize(AccountTest._TestExecuteWithSignatureTemps memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema__TestExecuteWithSignatureTemps, abi.encode(value)); - } - - function deserialize_TestExecuteWithSignatureTemps(string memory json) public pure returns (AccountTest._TestExecuteWithSignatureTemps memory) { - return abi.decode(vm.parseJsonType(json, schema__TestExecuteWithSignatureTemps), (AccountTest._TestExecuteWithSignatureTemps)); - } - - function deserialize_TestExecuteWithSignatureTemps(string memory json, string memory path) public pure returns (AccountTest._TestExecuteWithSignatureTemps memory) { - return abi.decode(vm.parseJsonType(json, path, schema__TestExecuteWithSignatureTemps), (AccountTest._TestExecuteWithSignatureTemps)); - } - - function deserialize_TestExecuteWithSignatureTempsArray(string memory json, string memory path) public pure returns (AccountTest._TestExecuteWithSignatureTemps[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema__TestExecuteWithSignatureTemps), (AccountTest._TestExecuteWithSignatureTemps[])); - } - - function serialize(AccountTest._TestUpgradeAccountWithPassKeyTemps memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema__TestUpgradeAccountWithPassKeyTemps, abi.encode(value)); - } - - function serialize(AccountTest._TestUpgradeAccountWithPassKeyTemps memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema__TestUpgradeAccountWithPassKeyTemps, abi.encode(value)); - } - - function deserialize_TestUpgradeAccountWithPassKeyTemps(string memory json) public pure returns (AccountTest._TestUpgradeAccountWithPassKeyTemps memory) { - return abi.decode(vm.parseJsonType(json, schema__TestUpgradeAccountWithPassKeyTemps), (AccountTest._TestUpgradeAccountWithPassKeyTemps)); - } - - function deserialize_TestUpgradeAccountWithPassKeyTemps(string memory json, string memory path) public pure returns (AccountTest._TestUpgradeAccountWithPassKeyTemps memory) { - return abi.decode(vm.parseJsonType(json, path, schema__TestUpgradeAccountWithPassKeyTemps), (AccountTest._TestUpgradeAccountWithPassKeyTemps)); - } - - function deserialize_TestUpgradeAccountWithPassKeyTempsArray(string memory json, string memory path) public pure returns (AccountTest._TestUpgradeAccountWithPassKeyTemps[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema__TestUpgradeAccountWithPassKeyTemps), (AccountTest._TestUpgradeAccountWithPassKeyTemps[])); - } - - function serialize(IStakeManager.DepositInfo memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema_DepositInfo, abi.encode(value)); - } - - function serialize(IStakeManager.DepositInfo memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema_DepositInfo, abi.encode(value)); - } - - function deserializeDepositInfo(string memory json) public pure returns (IStakeManager.DepositInfo memory) { - return abi.decode(vm.parseJsonType(json, schema_DepositInfo), (IStakeManager.DepositInfo)); - } - - function deserializeDepositInfo(string memory json, string memory path) public pure returns (IStakeManager.DepositInfo memory) { - return abi.decode(vm.parseJsonType(json, path, schema_DepositInfo), (IStakeManager.DepositInfo)); - } - - function deserializeDepositInfoArray(string memory json, string memory path) public pure returns (IStakeManager.DepositInfo[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema_DepositInfo), (IStakeManager.DepositInfo[])); - } - - function serialize(IStakeManager.StakeInfo memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema_StakeInfo, abi.encode(value)); - } - - function serialize(IStakeManager.StakeInfo memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema_StakeInfo, abi.encode(value)); - } - - function deserializeStakeInfo(string memory json) public pure returns (IStakeManager.StakeInfo memory) { - return abi.decode(vm.parseJsonType(json, schema_StakeInfo), (IStakeManager.StakeInfo)); - } - - function deserializeStakeInfo(string memory json, string memory path) public pure returns (IStakeManager.StakeInfo memory) { - return abi.decode(vm.parseJsonType(json, path, schema_StakeInfo), (IStakeManager.StakeInfo)); - } - - function deserializeStakeInfoArray(string memory json, string memory path) public pure returns (IStakeManager.StakeInfo[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema_StakeInfo), (IStakeManager.StakeInfo[])); - } - - function serialize(PackedUserOperation memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema_PackedUserOperation, abi.encode(value)); - } - - function serialize(PackedUserOperation memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema_PackedUserOperation, abi.encode(value)); - } - - function deserializePackedUserOperation(string memory json) public pure returns (PackedUserOperation memory) { - return abi.decode(vm.parseJsonType(json, schema_PackedUserOperation), (PackedUserOperation)); - } - - function deserializePackedUserOperation(string memory json, string memory path) public pure returns (PackedUserOperation memory) { - return abi.decode(vm.parseJsonType(json, path, schema_PackedUserOperation), (PackedUserOperation)); - } - - function deserializePackedUserOperationArray(string memory json, string memory path) public pure returns (PackedUserOperation[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema_PackedUserOperation), (PackedUserOperation[])); - } - - function serialize(UserOperation memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema_UserOperation, abi.encode(value)); - } - - function serialize(UserOperation memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema_UserOperation, abi.encode(value)); - } - - function deserializeUserOperation(string memory json) public pure returns (UserOperation memory) { - return abi.decode(vm.parseJsonType(json, schema_UserOperation), (UserOperation)); - } - - function deserializeUserOperation(string memory json, string memory path) public pure returns (UserOperation memory) { - return abi.decode(vm.parseJsonType(json, path, schema_UserOperation), (UserOperation)); - } - - function deserializeUserOperationArray(string memory json, string memory path) public pure returns (UserOperation[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema_UserOperation), (UserOperation[])); - } - - function serialize(IERC4337EntryPoint.UserOpsPerAggregator memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema_UserOpsPerAggregator, abi.encode(value)); - } - - function serialize(IERC4337EntryPoint.UserOpsPerAggregator memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema_UserOpsPerAggregator, abi.encode(value)); - } - - function deserializeUserOpsPerAggregator(string memory json) public pure returns (IERC4337EntryPoint.UserOpsPerAggregator memory) { - return abi.decode(vm.parseJsonType(json, schema_UserOpsPerAggregator), (IERC4337EntryPoint.UserOpsPerAggregator)); - } - - function deserializeUserOpsPerAggregator(string memory json, string memory path) public pure returns (IERC4337EntryPoint.UserOpsPerAggregator memory) { - return abi.decode(vm.parseJsonType(json, path, schema_UserOpsPerAggregator), (IERC4337EntryPoint.UserOpsPerAggregator)); - } - - function deserializeUserOpsPerAggregatorArray(string memory json, string memory path) public pure returns (IERC4337EntryPoint.UserOpsPerAggregator[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema_UserOpsPerAggregator), (IERC4337EntryPoint.UserOpsPerAggregator[])); - } - - function serialize(IERC4337EntryPoint.ReturnInfo memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema_ReturnInfo, abi.encode(value)); - } - - function serialize(IERC4337EntryPoint.ReturnInfo memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema_ReturnInfo, abi.encode(value)); - } - - function deserializeReturnInfo(string memory json) public pure returns (IERC4337EntryPoint.ReturnInfo memory) { - return abi.decode(vm.parseJsonType(json, schema_ReturnInfo), (IERC4337EntryPoint.ReturnInfo)); - } - - function deserializeReturnInfo(string memory json, string memory path) public pure returns (IERC4337EntryPoint.ReturnInfo memory) { - return abi.decode(vm.parseJsonType(json, path, schema_ReturnInfo), (IERC4337EntryPoint.ReturnInfo)); - } - - function deserializeReturnInfoArray(string memory json, string memory path) public pure returns (IERC4337EntryPoint.ReturnInfo[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema_ReturnInfo), (IERC4337EntryPoint.ReturnInfo[])); - } - - function serialize(SignatureWrapper memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema_SignatureWrapper, abi.encode(value)); - } - - function serialize(SignatureWrapper memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema_SignatureWrapper, abi.encode(value)); - } - - function deserializeSignatureWrapper(string memory json) public pure returns (SignatureWrapper memory) { - return abi.decode(vm.parseJsonType(json, schema_SignatureWrapper), (SignatureWrapper)); - } - - function deserializeSignatureWrapper(string memory json, string memory path) public pure returns (SignatureWrapper memory) { - return abi.decode(vm.parseJsonType(json, path, schema_SignatureWrapper), (SignatureWrapper)); - } - - function deserializeSignatureWrapperArray(string memory json, string memory path) public pure returns (SignatureWrapper[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema_SignatureWrapper), (SignatureWrapper[])); - } - - function serialize(EnforcedOptionParam memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema_EnforcedOptionParam, abi.encode(value)); - } - - function serialize(EnforcedOptionParam memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema_EnforcedOptionParam, abi.encode(value)); - } - - function deserializeEnforcedOptionParam(string memory json) public pure returns (EnforcedOptionParam memory) { - return abi.decode(vm.parseJsonType(json, schema_EnforcedOptionParam), (EnforcedOptionParam)); - } - - function deserializeEnforcedOptionParam(string memory json, string memory path) public pure returns (EnforcedOptionParam memory) { - return abi.decode(vm.parseJsonType(json, path, schema_EnforcedOptionParam), (EnforcedOptionParam)); - } - - function deserializeEnforcedOptionParamArray(string memory json, string memory path) public pure returns (EnforcedOptionParam[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema_EnforcedOptionParam), (EnforcedOptionParam[])); - } - - function serialize(MultiSigSignerTest.MultiSigTestTemps memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema_MultiSigTestTemps, abi.encode(value)); - } - - function serialize(MultiSigSignerTest.MultiSigTestTemps memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema_MultiSigTestTemps, abi.encode(value)); - } - - function deserializeMultiSigTestTemps(string memory json) public pure returns (MultiSigSignerTest.MultiSigTestTemps memory) { - return abi.decode(vm.parseJsonType(json, schema_MultiSigTestTemps), (MultiSigSignerTest.MultiSigTestTemps)); - } - - function deserializeMultiSigTestTemps(string memory json, string memory path) public pure returns (MultiSigSignerTest.MultiSigTestTemps memory) { - return abi.decode(vm.parseJsonType(json, path, schema_MultiSigTestTemps), (MultiSigSignerTest.MultiSigTestTemps)); - } - - function deserializeMultiSigTestTempsArray(string memory json, string memory path) public pure returns (MultiSigSignerTest.MultiSigTestTemps[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema_MultiSigTestTemps), (MultiSigSignerTest.MultiSigTestTemps[])); - } - - function serialize(OrchestratorTest._TestFullFlowTemps memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema__TestFullFlowTemps, abi.encode(value)); - } - - function serialize(OrchestratorTest._TestFullFlowTemps memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema__TestFullFlowTemps, abi.encode(value)); - } - - function deserialize_TestFullFlowTemps(string memory json) public pure returns (OrchestratorTest._TestFullFlowTemps memory) { - return abi.decode(vm.parseJsonType(json, schema__TestFullFlowTemps), (OrchestratorTest._TestFullFlowTemps)); - } - - function deserialize_TestFullFlowTemps(string memory json, string memory path) public pure returns (OrchestratorTest._TestFullFlowTemps memory) { - return abi.decode(vm.parseJsonType(json, path, schema__TestFullFlowTemps), (OrchestratorTest._TestFullFlowTemps)); - } - - function deserialize_TestFullFlowTempsArray(string memory json, string memory path) public pure returns (OrchestratorTest._TestFullFlowTemps[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema__TestFullFlowTemps), (OrchestratorTest._TestFullFlowTemps[])); - } - - function serialize(OrchestratorTest._TestAuthorizeWithPreCallsAndTransferTemps memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema__TestAuthorizeWithPreCallsAndTransferTemps, abi.encode(value)); - } - - function serialize(OrchestratorTest._TestAuthorizeWithPreCallsAndTransferTemps memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema__TestAuthorizeWithPreCallsAndTransferTemps, abi.encode(value)); - } - - function deserialize_TestAuthorizeWithPreCallsAndTransferTemps(string memory json) public pure returns (OrchestratorTest._TestAuthorizeWithPreCallsAndTransferTemps memory) { - return abi.decode(vm.parseJsonType(json, schema__TestAuthorizeWithPreCallsAndTransferTemps), (OrchestratorTest._TestAuthorizeWithPreCallsAndTransferTemps)); - } - - function deserialize_TestAuthorizeWithPreCallsAndTransferTemps(string memory json, string memory path) public pure returns (OrchestratorTest._TestAuthorizeWithPreCallsAndTransferTemps memory) { - return abi.decode(vm.parseJsonType(json, path, schema__TestAuthorizeWithPreCallsAndTransferTemps), (OrchestratorTest._TestAuthorizeWithPreCallsAndTransferTemps)); - } - - function deserialize_TestAuthorizeWithPreCallsAndTransferTempsArray(string memory json, string memory path) public pure returns (OrchestratorTest._TestAuthorizeWithPreCallsAndTransferTemps[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema__TestAuthorizeWithPreCallsAndTransferTemps), (OrchestratorTest._TestAuthorizeWithPreCallsAndTransferTemps[])); - } - - function serialize(OrchestratorTest._TestPayViaAnotherPayerTemps memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema__TestPayViaAnotherPayerTemps, abi.encode(value)); - } - - function serialize(OrchestratorTest._TestPayViaAnotherPayerTemps memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema__TestPayViaAnotherPayerTemps, abi.encode(value)); - } - - function deserialize_TestPayViaAnotherPayerTemps(string memory json) public pure returns (OrchestratorTest._TestPayViaAnotherPayerTemps memory) { - return abi.decode(vm.parseJsonType(json, schema__TestPayViaAnotherPayerTemps), (OrchestratorTest._TestPayViaAnotherPayerTemps)); - } - - function deserialize_TestPayViaAnotherPayerTemps(string memory json, string memory path) public pure returns (OrchestratorTest._TestPayViaAnotherPayerTemps memory) { - return abi.decode(vm.parseJsonType(json, path, schema__TestPayViaAnotherPayerTemps), (OrchestratorTest._TestPayViaAnotherPayerTemps)); - } - - function deserialize_TestPayViaAnotherPayerTempsArray(string memory json, string memory path) public pure returns (OrchestratorTest._TestPayViaAnotherPayerTemps[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema__TestPayViaAnotherPayerTemps), (OrchestratorTest._TestPayViaAnotherPayerTemps[])); - } - - function serialize(OrchestratorTest._TestAccountImplementationVerificationTemps memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema__TestAccountImplementationVerificationTemps, abi.encode(value)); - } - - function serialize(OrchestratorTest._TestAccountImplementationVerificationTemps memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema__TestAccountImplementationVerificationTemps, abi.encode(value)); - } - - function deserialize_TestAccountImplementationVerificationTemps(string memory json) public pure returns (OrchestratorTest._TestAccountImplementationVerificationTemps memory) { - return abi.decode(vm.parseJsonType(json, schema__TestAccountImplementationVerificationTemps), (OrchestratorTest._TestAccountImplementationVerificationTemps)); - } - - function deserialize_TestAccountImplementationVerificationTemps(string memory json, string memory path) public pure returns (OrchestratorTest._TestAccountImplementationVerificationTemps memory) { - return abi.decode(vm.parseJsonType(json, path, schema__TestAccountImplementationVerificationTemps), (OrchestratorTest._TestAccountImplementationVerificationTemps)); - } - - function deserialize_TestAccountImplementationVerificationTempsArray(string memory json, string memory path) public pure returns (OrchestratorTest._TestAccountImplementationVerificationTemps[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema__TestAccountImplementationVerificationTemps), (OrchestratorTest._TestAccountImplementationVerificationTemps[])); - } - - function serialize(OrchestratorTest._TestMultiSigTemps memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema__TestMultiSigTemps, abi.encode(value)); - } - - function serialize(OrchestratorTest._TestMultiSigTemps memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema__TestMultiSigTemps, abi.encode(value)); - } - - function deserialize_TestMultiSigTemps(string memory json) public pure returns (OrchestratorTest._TestMultiSigTemps memory) { - return abi.decode(vm.parseJsonType(json, schema__TestMultiSigTemps), (OrchestratorTest._TestMultiSigTemps)); - } - - function deserialize_TestMultiSigTemps(string memory json, string memory path) public pure returns (OrchestratorTest._TestMultiSigTemps memory) { - return abi.decode(vm.parseJsonType(json, path, schema__TestMultiSigTemps), (OrchestratorTest._TestMultiSigTemps)); - } - - function deserialize_TestMultiSigTempsArray(string memory json, string memory path) public pure returns (OrchestratorTest._TestMultiSigTemps[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema__TestMultiSigTemps), (OrchestratorTest._TestMultiSigTemps[])); - } - - function serialize(OrchestratorTest._TestMultiChainIntentTemps memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema__TestMultiChainIntentTemps, abi.encode(value)); - } - - function serialize(OrchestratorTest._TestMultiChainIntentTemps memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema__TestMultiChainIntentTemps, abi.encode(value)); - } - - function deserialize_TestMultiChainIntentTemps(string memory json) public pure returns (OrchestratorTest._TestMultiChainIntentTemps memory) { - return abi.decode(vm.parseJsonType(json, schema__TestMultiChainIntentTemps), (OrchestratorTest._TestMultiChainIntentTemps)); - } - - function deserialize_TestMultiChainIntentTemps(string memory json, string memory path) public pure returns (OrchestratorTest._TestMultiChainIntentTemps memory) { - return abi.decode(vm.parseJsonType(json, path, schema__TestMultiChainIntentTemps), (OrchestratorTest._TestMultiChainIntentTemps)); - } - - function deserialize_TestMultiChainIntentTempsArray(string memory json, string memory path) public pure returns (OrchestratorTest._TestMultiChainIntentTemps[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema__TestMultiChainIntentTemps), (OrchestratorTest._TestMultiChainIntentTemps[])); - } - - function serialize(SimulateExecuteTest._SimulateExecuteTemps memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema__SimulateExecuteTemps, abi.encode(value)); - } - - function serialize(SimulateExecuteTest._SimulateExecuteTemps memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema__SimulateExecuteTemps, abi.encode(value)); - } - - function deserialize_SimulateExecuteTemps(string memory json) public pure returns (SimulateExecuteTest._SimulateExecuteTemps memory) { - return abi.decode(vm.parseJsonType(json, schema__SimulateExecuteTemps), (SimulateExecuteTest._SimulateExecuteTemps)); - } - - function deserialize_SimulateExecuteTemps(string memory json, string memory path) public pure returns (SimulateExecuteTest._SimulateExecuteTemps memory) { - return abi.decode(vm.parseJsonType(json, path, schema__SimulateExecuteTemps), (SimulateExecuteTest._SimulateExecuteTemps)); - } - - function deserialize_SimulateExecuteTempsArray(string memory json, string memory path) public pure returns (SimulateExecuteTest._SimulateExecuteTemps[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema__SimulateExecuteTemps), (SimulateExecuteTest._SimulateExecuteTemps[])); - } -}