diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 00000000..e5b6d8d6 --- /dev/null +++ b/.changeset/README.md @@ -0,0 +1,8 @@ +# 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 new file mode 100644 index 00000000..d88011f6 --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,11 @@ +{ + "$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 7e85472c..dcfc7f4c 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 deleted file mode 100644 index 0ecd2e5e..00000000 --- a/.github/workflows/auto-assign.yaml +++ /dev/null @@ -1,12 +0,0 @@ -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/ci.yaml b/.github/workflows/ci.yaml index 6915c959..c5f35baa 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -5,13 +5,11 @@ on: push: branches: [main] -permissions: - contents: write - pull-requests: write - jobs: test: name: Tests + permissions: + contents: read runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -21,31 +19,22 @@ jobs: - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 with: - version: nightly + version: nightly - name: Build run: | forge build - - name: Run tests - env: - UPGRADE_TEST_RPC_URL: ${{ secrets.UPGRADE_TEST_RPC_URL }} - UPGRADE_TEST_OLD_PROXY: ${{ secrets.UPGRADE_TEST_OLD_PROXY }} - UPGRADE_TEST_OLD_VERSION: ${{ secrets.UPGRADE_TEST_OLD_VERSION }} + - name: Check formatting run: | - forge test -vvv + forge fmt --check - - name: Format contracts and generate snapshots - if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository + - name: Run tests run: | - forge fmt - forge snapshot --isolate --match-contract Benchmark --via-ir + forge test --rerun -vvvvv - - name: Commit formatting and snapshot changes - if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository - uses: stefanzweifel/git-auto-commit-action@v5 - with: - commit_message: "chore: fmt contracts and update gas snapshots" - file_pattern: "*.sol snapshots/" - commit_user_name: github-actions[bot] - commit_user_email: github-actions[bot]@users.noreply.github.com + - name: Snapshot main branch + run: git fetch origin main && git worktree prune &&rm -rf .snapshot_worktree && git worktree add .snapshot_worktree origin/main && (cd .snapshot_worktree && forge snapshot --match-contract Benchmark --snap .temp-snapshot) && cp .snapshot_worktree/.temp-snapshot gas-snapshots/.gas-snapshot-main && git worktree remove --force .snapshot_worktree && git worktree prune + + - name: Compare gas snapshots + run: forge snapshot --match-contract Benchmark --diff gas-snapshots/.gas-snapshot-main diff --git a/.github/workflows/claude-code.yml b/.github/workflows/claude-code.yml index ac2e8dbb..e069019a 100644 --- a/.github/workflows/claude-code.yml +++ b/.github/workflows/claude-code.yml @@ -17,6 +17,8 @@ jobs: check-permissions: name: Check permissions runs-on: ubuntu-latest + permissions: + contents: read outputs: has-permission: ${{ steps.check.outputs.has-permission }} steps: diff --git a/.github/workflows/manual-deployment.yml b/.github/workflows/manual-deployment.yml index 6ce9859b..7a3a3f27 100644 --- a/.github/workflows/manual-deployment.yml +++ b/.github/workflows/manual-deployment.yml @@ -1,5 +1,8 @@ name: Manual Deployment Execution +permissions: + contents: read + on: workflow_dispatch: inputs: diff --git a/.github/workflows/test-infra.yml b/.github/workflows/test-infra.yml index 6ce9859b..8b69956d 100644 --- a/.github/workflows/test-infra.yml +++ b/.github/workflows/test-infra.yml @@ -1,4 +1,6 @@ name: Manual Deployment Execution +permissions: + contents: read on: workflow_dispatch: diff --git a/.github/workflows/version-check.yaml b/.github/workflows/version-check.yaml index f5946cb5..059fa1e4 100644 --- a/.github/workflows/version-check.yaml +++ b/.github/workflows/version-check.yaml @@ -57,22 +57,31 @@ 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 - - - 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 + + # 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: Create PR comment if: steps.check.outputs.needs_version_bump == 'true' diff --git a/.gitmodules b/.gitmodules index 28a7bc52..7aed494c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,6 +7,9 @@ [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/.gitignore b/.idea/.gitignore new file mode 100644 index 00000000..26d33521 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/account.iml b/.idea/account.iml new file mode 100644 index 00000000..d6ebd480 --- /dev/null +++ b/.idea/account.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/caches/deviceStreaming.xml b/.idea/caches/deviceStreaming.xml new file mode 100644 index 00000000..94e429a7 --- /dev/null +++ b/.idea/caches/deviceStreaming.xml @@ -0,0 +1,2007 @@ + + + + + + \ No newline at end of file diff --git a/.idea/markdown.xml b/.idea/markdown.xml new file mode 100644 index 00000000..c61ea334 --- /dev/null +++ b/.idea/markdown.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 00000000..862d09bd --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 00000000..4ae0bf91 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000..bb3b0703 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fd21859..dae9f612 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,24 +1,5 @@ # 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 @@ -113,7 +94,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, udpated to `OpDataError`, to enforce tighter validation of opdata. +- `OpDataTooShort` error, updated to `OpDataError`, to enforce tighter validation of opdata. - `checkAndIncrementNonce` function added to account. Can only be called by EP. - 6b3294a: Optimize `_isSuperAdmin` @@ -150,7 +131,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 atleast as `gExecute > (gCombined + 100_000) * 64/63)` + For the relay, gExecute now has to be set at least as `gExecute > (gCombined + 100_000) * 64/63)` ### Patch Changes diff --git a/README.md b/README.md index aa22490b..ea941768 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,10 @@ [![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, @@ -19,24 +23,17 @@ We believe that unstoppable crypto-powered accounts should be excellent througho # Features out of the box -- 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) +- [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. ## Getting Help diff --git a/deploy/ConfigureLayerZeroSettler.s.sol b/deploy/ConfigureLayerZeroSettler.s.sol index 63187a5f..4e505329 100644 --- a/deploy/ConfigureLayerZeroSettler.s.sol +++ b/deploy/ConfigureLayerZeroSettler.s.sol @@ -3,7 +3,6 @@ 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 @@ -37,7 +36,7 @@ import {LayerZeroSettler} from "../src/LayerZeroSettler.sol"; * --private-key $L0_SETTLER_OWNER_PK \ * "[84532,11155420]" */ -contract ConfigureLayerZeroSettler is Script, Config { +contract ConfigureLayerZeroSettler is Script { // Configuration type constants (matching ULN302) uint32 constant CONFIG_TYPE_EXECUTOR = 1; uint32 constant CONFIG_TYPE_ULN = 2; @@ -47,7 +46,6 @@ contract ConfigureLayerZeroSettler is Script, Config { string name; address layerZeroSettlerAddress; address layerZeroEndpoint; - address l0SettlerSigner; uint32 eid; address sendUln302; address receiveUln302; @@ -61,7 +59,6 @@ contract ConfigureLayerZeroSettler is Script, Config { // Fork ids for chain switching mapping(uint256 => uint256) public forkIds; - mapping(uint256 => bool) public isForkInitialized; mapping(uint256 => LayerZeroChainConfig) public chainConfigs; uint256[] public configuredChainIds; @@ -69,12 +66,8 @@ contract ConfigureLayerZeroSettler is Script, Config { * @notice Configure all chains with LayerZero configuration */ function run() external { - // 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(); + // Get all chain IDs from fork configuration + uint256[] memory chainIds = vm.readForkChainIds(); run(chainIds); } @@ -86,15 +79,12 @@ contract ConfigureLayerZeroSettler is Script, Config { 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]); @@ -110,22 +100,27 @@ contract ConfigureLayerZeroSettler is Script, Config { for (uint256 i = 0; i < chainIds.length; i++) { uint256 chainId = chainIds[i]; - // Switch to the fork for this chain (already created by _loadConfigAndForks) - vm.selectFork(forkOf[chainId]); + // Create fork to read configuration + string memory rpcUrl = vm.envString(string.concat("RPC_", vm.toString(chainId))); + uint256 forkId = vm.createFork(rpcUrl); + vm.selectFork(forkId); // Try to load LayerZero configuration - LayerZeroChainConfig memory chainConfig = loadChainConfig(chainId); - - // Only add chains that have LayerZero configuration - if (chainConfig.layerZeroSettlerAddress != address(0)) { - chainConfigs[chainId] = chainConfig; + LayerZeroChainConfig memory config = loadChainConfig(chainId); + + // Only store if chain has LayerZero configuration + if (config.sendUln302 != address(0)) { + chainConfigs[chainId] = config; configuredChainIds.push(chainId); - forkIds[chainId] = forkOf[chainId]; - isForkInitialized[forkOf[chainId]] = true; + forkIds[chainId] = forkId; console.log( string.concat( - " Loaded LayerZero config for ", chainConfig.name, " (", vm.toString(chainId), ")" + " Loaded LayerZero config for ", + config.name, + " (", + vm.toString(chainId), + ")" ) ); } @@ -135,68 +130,62 @@ contract ConfigureLayerZeroSettler is Script, Config { } /** - * @notice Load configuration for a single chain using StdConfig + * @notice Load configuration for a single chain from fork variables */ function loadChainConfig(uint256 chainId) internal view - returns (LayerZeroChainConfig memory chainConfig) + returns (LayerZeroChainConfig memory config) { - chainConfig.chainId = chainId; + config.chainId = chainId; // Load basic chain info - required - chainConfig.name = config.get(chainId, "name").toString(); + config.name = vm.readForkString("name"); - // 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)) { + // 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 { // No LayerZero settler configured for this chain - this is ok, return empty config - return chainConfig; + return config; } - chainConfig.layerZeroSettlerAddress = settlerAddr; // If we have a LayerZero settler, all other LayerZero fields are required - 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(); + 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"); // Load destination chain IDs - required for LayerZero configuration - chainConfig.destinationChainIds = config.get(chainId, "layerzero_destination_chain_ids").toUint256Array(); + config.destinationChainIds = vm.readForkUintArray("layerzero_destination_chain_ids"); // Load DVN configuration - required and optional DVN arrays - string[] memory requiredDVNNames = config.get(chainId, "layerzero_required_dvns").toStringArray(); - string[] memory optionalDVNNames = config.get(chainId, "layerzero_optional_dvns").toStringArray(); + string[] memory requiredDVNNames = vm.readForkStringArray("layerzero_required_dvns"); + string[] memory optionalDVNNames = vm.readForkStringArray("layerzero_optional_dvns"); // Resolve DVN names to addresses - chainConfig.requiredDVNs = resolveDVNAddresses(chainId, requiredDVNNames); - chainConfig.optionalDVNs = resolveDVNAddresses(chainId, optionalDVNNames); + config.requiredDVNs = resolveDVNAddresses(requiredDVNNames); + config.optionalDVNs = resolveDVNAddresses(optionalDVNNames); // Load optional DVN threshold - required field - chainConfig.optionalDVNThreshold = uint8(config.get(chainId, "layerzero_optional_dvn_threshold").toUint256()); + config.optionalDVNThreshold = uint8(vm.readForkUint("layerzero_optional_dvn_threshold")); // Load confirmations - required field - chainConfig.confirmations = uint64(config.get(chainId, "layerzero_confirmations").toUint256()); + config.confirmations = uint64(vm.readForkUint("layerzero_confirmations")); // Load max message size - required field - chainConfig.maxMessageSize = uint32(config.get(chainId, "layerzero_max_message_size").toUint256()); + config.maxMessageSize = uint32(vm.readForkUint("layerzero_max_message_size")); - return chainConfig; + return config; } /** - * @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 + * @notice Resolve DVN names to addresses by reading from fork variables + * @dev Takes DVN variable names and looks up their addresses using vm.readForkAddress * @param dvnNames Array of DVN variable names from config (e.g., "dvn_layerzero_labs") * @return addresses Array of resolved DVN addresses */ - function resolveDVNAddresses(uint256 chainId, string[] memory dvnNames) + function resolveDVNAddresses(string[] memory dvnNames) internal view returns (address[] memory) @@ -204,7 +193,7 @@ contract ConfigureLayerZeroSettler is Script, Config { address[] memory addresses = new address[](dvnNames.length); for (uint256 i = 0; i < dvnNames.length; i++) { - addresses[i] = config.get(chainId, dvnNames[i]).toAddress(); + addresses[i] = vm.readForkAddress(dvnNames[i]); require( addresses[i] != address(0), string.concat("DVN address not configured for: ", dvnNames[i]) @@ -214,8 +203,18 @@ contract ConfigureLayerZeroSettler is Script, Config { return addresses; } - function _selectFork(uint256 chainId) internal { - vm.selectFork(forkOf[chainId]); + /** + * @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]); + } } /** @@ -228,59 +227,31 @@ contract ConfigureLayerZeroSettler is Script, Config { 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 - _selectFork(chainId); + vm.selectFork(forkIds[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(config, settler, endpoint, destConfig.eid); + setExecutorConfig(settler, endpoint, destConfig.eid); // Set send ULN config setSendUlnConfig( @@ -295,31 +266,11 @@ contract ConfigureLayerZeroSettler is Script, Config { ); // Switch to destination chain to set receive config - _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); - } + vm.selectFork(forkIds[destChainId]); // Set receive ULN config on destination setReceiveUlnConfig( - destSettler, + LayerZeroSettler(payable(destConfig.layerZeroSettlerAddress)), ILayerZeroEndpointV2(destConfig.layerZeroEndpoint), config.eid, // Source EID destConfig.receiveUln302, @@ -330,7 +281,7 @@ contract ConfigureLayerZeroSettler is Script, Config { ); // Switch back to source chain - _selectFork(chainId); + vm.selectFork(forkIds[chainId]); } console.log("\n Configuration complete for", config.name); @@ -341,24 +292,12 @@ contract ConfigureLayerZeroSettler is Script, Config { // ============================================ function setExecutorConfig( - LayerZeroChainConfig memory config, LayerZeroSettler settler, ILayerZeroEndpointV2 endpoint, uint32 destEid ) internal { - 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"); + // LayerZeroSettler uses self-execution model, no executor config needed + console.log(" Using self-execution model (no executor config)"); } function setSendUlnConfig( @@ -398,7 +337,7 @@ contract ConfigureLayerZeroSettler is Script, Config { }); // Get the L0 settler owner who should be the delegate - address l0SettlerOwner = config.get(vm.getChainId(), "l0_settler_owner").toAddress(); + address l0SettlerOwner = vm.readForkAddress("l0_settler_owner"); console.log(" L0 Settler Owner (delegate):", l0SettlerOwner); vm.broadcast(); diff --git a/deploy/DeployMain.s.sol b/deploy/DeployMain.s.sol index ee513799..5ebbb109 100644 --- a/deploy/DeployMain.s.sol +++ b/deploy/DeployMain.s.sol @@ -3,8 +3,7 @@ pragma solidity ^0.8.23; import {Script, console} from "forge-std/Script.sol"; import {VmSafe} from "forge-std/Vm.sol"; -import {Config} from "forge-std/Config.sol"; -import {Variable, TypeKind} from "forge-std/LibVariable.sol"; +import {stdToml} from "forge-std/StdToml.sol"; import {SafeSingletonDeployer} from "./SafeSingletonDeployer.sol"; // Import contracts to deploy @@ -16,7 +15,6 @@ 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 @@ -51,26 +49,23 @@ import {ExperimentERC20} from "./mock/ExperimentalERC20.sol"; * --private-key $PRIVATE_KEY \ * "[1]" "/deploy/custom-config.toml" */ -contract DeployMain is Script, Config, SafeSingletonDeployer { +contract DeployMain is Script, SafeSingletonDeployer { + using stdToml for string; // 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 { @@ -82,8 +77,6 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { address layerZeroSettler; address simpleFunder; address simulator; - address expToken; // EXP token (testnet only) - address exp2Token; // EXP2 token (testnet only) } // State @@ -91,7 +84,9 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { mapping(uint256 => DeployedContracts) internal deployedContracts; uint256[] internal targetChainIds; - // Config path + // Paths and config + string internal registryPath; + string internal configContent; // For unified config string internal configPath = "/deploy/config.toml"; // Events for tracking @@ -109,17 +104,9 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { * @notice Deploy to all chains in config */ function run() external { - // 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(); + // Get all available chain IDs from fork configuration + uint256[] memory chainIds = vm.readForkChainIds(); + initializeDeployment(chainIds); executeDeployment(); } @@ -128,20 +115,11 @@ contract DeployMain is Script, Config, 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 = config.getChainIds(); + chainIds = vm.readForkChainIds(); } - targetChainIds = chainIds; - require(targetChainIds.length > 0, "No chains found in configuration"); - - // Load configuration for each chain - loadConfigurations(); - loadDeployedContracts(); + initializeDeployment(chainIds); executeDeployment(); } @@ -151,25 +129,47 @@ contract DeployMain is Script, Config, 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 = config.getChainIds(); + chainIds = vm.readForkChainIds(); } + 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,15 +178,20 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { for (uint256 i = 0; i < targetChainIds.length; i++) { uint256 chainId = targetChainIds[i]; - // Switch to the fork for this chain (already created by _loadConfigAndForks) - vm.selectFork(forkOf[chainId]); + // 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); // Verify we're on the correct chain require(block.chainid == chainId, "Chain ID mismatch"); - // Load configuration using new StdConfig pattern - ChainConfig memory chainConfig = loadChainConfigFromStdConfig(chainId); - chainConfigs[chainId] = chainConfig; + // Load configuration from fork variables + ChainConfig memory config = loadChainConfigFromFork(chainId); + chainConfigs[chainId] = config; } // Log the loaded configuration for verification @@ -194,66 +199,48 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { } /** - * @notice Load chain configuration using StdConfig + * @notice Load chain configuration from the currently active fork * @param chainId The chain ID we're loading config for */ - function loadChainConfigFromStdConfig(uint256 chainId) internal view returns (ChainConfig memory) { - ChainConfig memory chainConfig; + function loadChainConfigFromFork(uint256 chainId) internal view returns (ChainConfig memory) { + ChainConfig memory config; - chainConfig.chainId = chainId; + config.chainId = chainId; - // Use StdConfig to read variables - chainConfig.name = config.get(chainId, "name").toString(); - chainConfig.isTestnet = config.get(chainId, "is_testnet").toBool(); + // Use vm.readFork* functions to read variables from the active fork + config.name = vm.readForkString("name"); + config.isTestnet = vm.readForkBool("is_testnet"); // Load addresses - 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(); + 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"); // Load other configuration - 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(); - } + config.layerZeroEid = uint32(vm.readForkUint("layerzero_eid")); + config.salt = vm.readForkBytes32("salt"); // Load contracts list - required field, will revert if not present - string[] memory contractsList = config.get(chainId, "contracts").toStringArray(); + string[] memory contractsList = vm.readForkStringArray("contracts"); // Check if user specified "ALL" to deploy all contracts if ( contractsList.length == 1 && keccak256(bytes(contractsList[0])) == keccak256(bytes("ALL")) ) { - 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; - } + config.contracts = getAllContracts(); } else { - chainConfig.contracts = contractsList; + config.contracts = contractsList; } - return chainConfig; + return config; } /** - * @notice Get all available contracts (excluding ExpToken) + * @notice Get all available contracts */ function getAllContracts() internal pure returns (string[] memory) { string[] memory contracts = new string[](8); @@ -286,40 +273,31 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { } /** - * @notice Load deployed contracts from config + * @notice Load deployed contracts from registry */ function loadDeployedContracts() internal { for (uint256 i = 0; i < targetChainIds.length; i++) { uint256 chainId = targetChainIds[i]; - - 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); + 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 + } } - return variable.toAddress(); } /** @@ -337,8 +315,8 @@ contract DeployMain is Script, Config, 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:"); @@ -445,7 +423,7 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { } /** - * @notice Save deployed contract address to config + * @notice Save deployed contract address to registry */ function saveDeployedContract( uint256 chainId, @@ -469,38 +447,124 @@ contract DeployMain is Script, Config, 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 @@ -599,8 +663,6 @@ contract DeployMain is Script, Config, 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); } @@ -611,12 +673,17 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { ChainConfig memory config, DeployedContracts memory deployed ) internal { - bytes memory creationCode = type(Orchestrator).creationCode; - address orchestrator = - deployContractWithCreate2(chainId, creationCode, "", "Orchestrator"); - - saveDeployedContract(chainId, "Orchestrator", orchestrator); - deployed.orchestrator = orchestrator; + 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); + } } function deployIthacaAccount( @@ -625,15 +692,22 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { DeployedContracts memory deployed ) internal { // Ensure Orchestrator is deployed first (dependency) - require(deployed.orchestrator != address(0), "Orchestrator must be deployed before IthacaAccount"); + if (deployed.orchestrator == address(0)) { + console.log("Deploying Orchestrator first (dependency for IthacaAccount)..."); + deployOrchestrator(chainId, config, deployed); + } - bytes memory creationCode = type(IthacaAccount).creationCode; - bytes memory args = abi.encode(deployed.orchestrator); - address ithacaAccount = - deployContractWithCreate2(chainId, creationCode, args, "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"); - saveDeployedContract(chainId, "IthacaAccount", ithacaAccount); - deployed.ithacaAccount = ithacaAccount; + saveDeployedContract(chainId, "IthacaAccount", ithacaAccount); + deployed.ithacaAccount = ithacaAccount; + } else { + console.log("IthacaAccount already deployed:", deployed.ithacaAccount); + } } function deployAccountProxy( @@ -642,14 +716,21 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { DeployedContracts memory deployed ) internal { // Ensure IthacaAccount is deployed first (dependency) - require(deployed.ithacaAccount != address(0), "IthacaAccount must be deployed before AccountProxy"); + if (deployed.ithacaAccount == address(0)) { + console.log("Deploying IthacaAccount first (dependency for AccountProxy)..."); + deployIthacaAccount(chainId, config, deployed); + } - bytes memory proxyCode = LibEIP7702.proxyInitCode(deployed.ithacaAccount, address(0)); - address accountProxy = deployContractWithCreate2(chainId, proxyCode, "", "AccountProxy"); + if (deployed.accountProxy == address(0)) { + 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; + require(accountProxy != address(0), "Account proxy deployment failed"); + saveDeployedContract(chainId, "AccountProxy", accountProxy); + deployed.accountProxy = accountProxy; + } else { + console.log("AccountProxy already deployed:", deployed.accountProxy); + } } function deploySimulator( @@ -657,11 +738,15 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { ChainConfig memory config, DeployedContracts memory deployed ) internal { - bytes memory creationCode = type(Simulator).creationCode; - address simulator = deployContractWithCreate2(chainId, creationCode, "", "Simulator"); + if (deployed.simulator == address(0)) { + bytes memory creationCode = type(Simulator).creationCode; + address simulator = deployContractWithCreate2(chainId, creationCode, "", "Simulator"); - saveDeployedContract(chainId, "Simulator", simulator); - deployed.simulator = simulator; + saveDeployedContract(chainId, "Simulator", simulator); + deployed.simulator = simulator; + } else { + console.log("Simulator already deployed:", deployed.simulator); + } } function deploySimpleFunder( @@ -669,13 +754,23 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { ChainConfig memory config, DeployedContracts memory deployed ) internal { - bytes memory creationCode = type(SimpleFunder).creationCode; + // 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 args = abi.encode(config.funderSigner, config.funderOwner); - address funder = deployContractWithCreate2(chainId, creationCode, args, "SimpleFunder"); + 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"); - saveDeployedContract(chainId, "SimpleFunder", funder); - deployed.simpleFunder = funder; + saveDeployedContract(chainId, "SimpleFunder", funder); + deployed.simpleFunder = funder; + } else { + console.log("SimpleFunder already deployed:", deployed.simpleFunder); + } } function deployEscrow( @@ -683,11 +778,15 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { ChainConfig memory config, DeployedContracts memory deployed ) internal { - bytes memory creationCode = type(Escrow).creationCode; - address escrow = deployContractWithCreate2(chainId, creationCode, "", "Escrow"); + if (deployed.escrow == address(0)) { + bytes memory creationCode = type(Escrow).creationCode; + address escrow = deployContractWithCreate2(chainId, creationCode, "", "Escrow"); - saveDeployedContract(chainId, "Escrow", escrow); - deployed.escrow = escrow; + saveDeployedContract(chainId, "Escrow", escrow); + deployed.escrow = escrow; + } else { + console.log("Escrow already deployed:", deployed.escrow); + } } function deploySimpleSettler( @@ -695,14 +794,18 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { ChainConfig memory config, DeployedContracts memory deployed ) internal { - 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; + 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); + } } function deployLayerZeroSettler( @@ -710,62 +813,19 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { ChainConfig memory config, DeployedContracts memory deployed ) internal { - 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; + 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(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 42618c6e..02820e27 100644 --- a/deploy/FundSigners.s.sol +++ b/deploy/FundSigners.s.sol @@ -2,14 +2,12 @@ pragma solidity ^0.8.23; import {Script, console} from "forge-std/Script.sol"; -import {Config} from "forge-std/Config.sol"; +import {stdToml} from "forge-std/StdToml.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); } /** @@ -48,7 +46,9 @@ interface ISimpleFunder { * --private-key $PRIVATE_KEY \ * "[84532]" 5 */ -contract FundSigners is Script, Config { +contract FundSigners is Script { + using stdToml for string; + /** * @notice Configuration for funding on a specific chain */ @@ -59,7 +59,6 @@ contract FundSigners is Script, Config { uint256 targetBalance; address simpleFunderAddress; uint256 defaultNumSigners; - address[] supportedOrchestrators; } /** @@ -88,18 +87,18 @@ contract FundSigners is Script, Config { 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 { - // 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(); + // Default to common testnets + uint256[] memory chainIds = new uint256[](3); + chainIds[0] = 11155111; // Sepolia + chainIds[1] = 84532; // Base Sepolia + chainIds[2] = 11155420; // Optimism Sepolia uint256 numSigners = 10; // Default execute(chainIds, numSigners); } @@ -109,10 +108,6 @@ contract FundSigners is Script, Config { * @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); } @@ -123,10 +118,6 @@ contract FundSigners is Script, Config { * @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); } @@ -137,6 +128,9 @@ contract FundSigners is Script, Config { 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"); @@ -217,9 +211,6 @@ contract FundSigners is Script, Config { if (config.simpleFunderAddress != address(0) && config.simpleFunderAddress.code.length > 0) { setGasWalletsInSimpleFunder(config.simpleFunderAddress, signers); - setOrchestratorsInSimpleFunder( - config.simpleFunderAddress, config.supportedOrchestrators - ); } vm.stopBroadcast(); @@ -421,41 +412,6 @@ contract FundSigners is Script, Config { ); } - /** - * @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 */ @@ -475,29 +431,45 @@ contract FundSigners is Script, Config { 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) { - // 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(); + // 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); + } // Try to read default number of signers - 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(); + try vm.readForkUint("default_num_signers") returns (uint256 num) { + config.defaultNumSigners = num; + } catch { + config.defaultNumSigners = 10; // Default fallback + } - return chainConfig; + return config; } /** diff --git a/deploy/README.md b/deploy/README.md index b23c7606..cb27d1ef 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,90 +55,69 @@ optimism-sepolia = { key = "${ETHERSCAN_API_KEY}" } ## Configuration Structure -All configuration is in `deploy/config.toml` using the StdConfig format: +All configuration is in `deploy/config.toml`: ```toml -[base-sepolia] -endpoint_url = "${RPC_84532}" +[profile.deployment] +registry_path = "deploy/registry/" -[base-sepolia.bool] -is_testnet = true +[forks.base-sepolia] +rpc_url = "${RPC_84532}" -[base-sepolia.address] +[forks.base-sepolia.vars] # 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 -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_send_uln302 = "0x..." +layerzero_receive_uln302 = "0x..." layerzero_destination_chain_ids = [11155420] - -[base-sepolia.bytes32] -salt = "0x0000000000000000000000000000000000000000000000000000000000005678" # CREATE2 salt (SAVE THIS!) - -[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: +layerzero_optional_dvn_threshold = 0 +layerzero_confirmations = 1 +layerzero_max_message_size = 10000 -- ✅ **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 +dvn_layerzero_labs = "0x..." +dvn_google_cloud = "0x..." +``` ### 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) -- **ExpToken** - Test ERC20 tokens (Testnet only, automatically included with "ALL") -- **ALL** - Deploys all contracts (+ ExpToken on testnets) +- **ALL** - Deploys all contracts -**Dependencies**: -IthacaAccount requires Orchestrator; -AccountProxy requires IthacaAccount; +**Dependencies**: +IthacaAccount requires Orchestrator; +AccountProxy requires IthacaAccount; SimpleFunder requires Orchestrator. ## Quick Start - Complete Workflow @@ -170,7 +149,8 @@ forge script deploy/FundSigners.s.sol:FundSigners \ --private-key $PRIVATE_KEY \ "[84532,11155420]" -# 5. Fund SimpleFunder contract +# 5. Fund SimpleFunder contract +SIMPLE_FUNDER=$(cat deploy/registry/deployment_84532_*.json | jq -r .SimpleFunder) forge script deploy/FundSimpleFunder.s.sol:FundSimpleFunder \ --broadcast --multi --slow \ @@ -194,7 +174,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 \ @@ -227,8 +207,7 @@ 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 @@ -251,18 +230,15 @@ 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) @@ -305,7 +281,6 @@ 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 @@ -322,36 +297,37 @@ 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 -- **Deployment decisions based on on-chain state** - Scripts check actual deployed contracts, not config file data -## Adding New Chains +## Registry Files -1. Add configuration to `deploy/config.toml`: +Deployment addresses are saved in `deploy/registry/deployment_{chainId}_{salt}.json`: -```toml -[new-chain] -endpoint_url = "${RPC_CHAINID}" +```json +{ + "Orchestrator": "0xb33adF2c2257a94314d408255aC843fd53B1a7e1", + "IthacaAccount": "0x5a87ef243CDA70a855828d4989Fad61B56A467d3", + "AccountProxy": "0x4ACD713815fbb363a89D9Ff046C56cEdC7EF3ad7", + "SimpleFunder": "0xA47C5C472449979a2F37dF2971627cD6587bADb8" +} +``` -[new-chain.bool] -is_testnet = true +Registry files are for reference only - deployment decisions are based on on-chain state. -[new-chain.address] -funder_owner = "0x..." -# ... all required fields +## Adding New Chains -[new-chain.uint] -chain_id = CHAINID -# ... all required fields +1. Add configuration to `deploy/config.toml`: -[new-chain.bytes32] -salt = "0x0000000000000000000000000000000000000000000000000000000000005678" +```toml +[forks.new-chain] +rpc_url = "${RPC_CHAINID}" -[new-chain.string] +[forks.new-chain.vars] +chain_id = CHAINID name = "Chain Name" +# ... all required fields contracts = ["ALL"] ``` @@ -376,22 +352,18 @@ 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 @@ -404,145 +376,38 @@ 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 | -| `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 +| 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 | -**Purpose**: Deploy EXP and EXP2 test tokens automatically on testnet chains. +## Test Token Deployment -**Behavior**: +### DeployEXP - Test ERC20 Token -- **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 - -```bash -# 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 +**Purpose**: Deploy a simple test ERC20 token for testing. +**Usage**: ```bash -# Verify specific chains -./deploy/verify_config.sh 84532 11155420 - -# Verify all chains in config.toml -./deploy/verify_config.sh +# Deploy test token to a specific chain +forge script deploy/DeployEXP.s.sol:DeployEXP \ + --broadcast \ + --sig "run()" \ + --private-key $PRIVATE_KEY \ + --rpc-url $RPC_ ``` -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 +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 diff --git a/deploy/config.toml b/deploy/config.toml index 32d4fd67..0644abd6 100644 --- a/deploy/config.toml +++ b/deploy/config.toml @@ -1,155 +1,136 @@ -[11155111] -endpoint_url = "${RPC_11155111}" +# 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.bool] -is_testnet = true +# ============================================ +# GLOBAL DEPLOYMENT CONFIGURATION +# ============================================ + +[profile.deployment] +registry_path = "deploy/registry/" + +# ============================================ +# FORK CONFIGURATIONS (for vm.readFork* cheatcodes) +# ============================================ -[11155111.address] +# Sepolia +[forks.sepolia] +rpc_url = "${RPC_11155111}" + +[forks.sepolia.vars] +chain_id = 11155111 +name = "Sepolia" +is_testnet = true +pause_authority = "0x0000000000000000000000000000000000000001" 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" -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_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 -layerzero_optional_dvn_threshold = 0 -exp_mint_amount = "1000000000000000000000" -layerzero_destination_chain_ids = [ - 84532, - 11155420, -] - -[11155111.bytes32] -salt = "0xdf08a9fa957a9ac004f84f3a8ab7318a7be55d59948f46497bd3717865295b61" - -[11155111.string] -name = "Sepolia" -contracts = ["ALL"] -layerzero_required_dvns = ["dvn_layerzero_labs"] -layerzero_optional_dvns = [] +# DVN addresses +dvn_layerzero_labs = "0x8eebf8b423B73bFCa51a1Db4B7354AA0bFCA9193" +dvn_google_cloud = "0x8e5a7b5959C5A7C732b87dCE401E07F5819eEC2d" -[84532] -endpoint_url = "${RPC_84532}" +# Base Sepolia +[forks.base-sepolia] +rpc_url = "${RPC_84532}" -[84532.bool] +[forks.base-sepolia.vars] +orchestrator_address = "0x0000000000000000000000000000000000000000" +# Deployment Script +chain_id = 84532 +name = "Base Sepolia" is_testnet = true - -[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" +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" layerzero_send_uln302 = "0xC1868e054425D378095A003EcbA3823a5D0135C9" layerzero_receive_uln302 = "0x12523de19dc41c91F7d2093E0CFbB76b17012C8d" -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" - -[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] - -[84532.bytes32] -salt = "0xdf08a9fa957a9ac004f84f3a8ab7318a7be55d59948f46497bd3717865295b61" - -[84532.string] -name = "Base Sepolia" -contracts = ["ALL"] +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" -[11155420] -endpoint_url = "${RPC_11155420}" +# Optimism Sepolia +[forks.optimism-sepolia] +rpc_url = "${RPC_11155420}" -[11155420.bool] +[forks.optimism-sepolia.vars] +chain_id = 11155420 +name = "Optimism Sepolia" is_testnet = true - -[11155420.address] -funder_owner = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" -funder_signer = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" -settler_owner = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" +pause_authority = "0x0000000000000000000000000000000000000001" +funder_owner = "0x0000000000000000000000000000000000000003" +funder_signer = "0x0000000000000000000000000000000000000002" +settler_owner = "0x0000000000000000000000000000000000000004" l0_settler_owner = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" -l0_settler_signer = "0x0000000000000000000000000000000000000006" layerzero_endpoint = "0x6EDCE65403992e310A62460808c4b910D972f10f" -layerzero_settler_address = "0x9F610182BD096ab7734C1365A44f48dD0A574d0D" +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_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" -[11155420.string] -name = "Optimism Sepolia" -contracts = ["ALL"] -layerzero_required_dvns = ["dvn_layerzero_labs"] -layerzero_optional_dvns = [] +# 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 ) diff --git a/deploy/execute_config.sh b/deploy/execute_config.sh old mode 100755 new mode 100644 diff --git a/deploy/registry/deployment_11155420_0x0000000000000000000000000000000000000000000000000000000000000001.json b/deploy/registry/deployment_11155420_0x0000000000000000000000000000000000000000000000000000000000000001.json new file mode 100644 index 00000000..23cb5e8b --- /dev/null +++ b/deploy/registry/deployment_11155420_0x0000000000000000000000000000000000000000000000000000000000000001.json @@ -0,0 +1 @@ +{"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 new file mode 100644 index 00000000..c37d42b4 --- /dev/null +++ b/deploy/registry/deployment_84532_0x0000000000000000000000000000000000000000000000000000000000000001.json @@ -0,0 +1,10 @@ +{ + "Orchestrator": "0xb33adF2c2257a94314d408255aC843fd53B1a7e1", + "IthacaAccount": "0x5a87ef243CDA70a855828d4989Fad61B56A467d3", + "AccountProxy": "0x4ACD713815fbb363a89D9Ff046C56cEdC7EF3ad7", + "Simulator": "0x65Ae218EB1987b8bd0F9eeb38D1B344726D41dA5", + "SimpleFunder": "0xA47C5C472449979a2F37dF2971627cD6587bADb8", + "Escrow": "0x24F50280cE3B51Ab1967F048746FB7ba3C7B4067", + "SimpleSettler": "0xb934afBB50b8aBBe24959f9398fE024BEe9Bf716", + "LayerZeroSettler": "0xB89f4A85d38C3A2407854269527fabD3b61fd56a" +} diff --git a/deploy/verify_config.sh b/deploy/verify_config.sh old mode 100755 new mode 100644 diff --git a/docs/4337CallGraph.md b/docs/4337CallGraph.md deleted file mode 100644 index ec9e788d..00000000 --- a/docs/4337CallGraph.md +++ /dev/null @@ -1,32 +0,0 @@ -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 deleted file mode 100644 index 7c35733c..00000000 --- a/docs/IthacaCallGraph.md +++ /dev/null @@ -1,25 +0,0 @@ -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 deleted file mode 100644 index 43c3a861..00000000 Binary files a/docs/benchmarks.jpeg and /dev/null differ diff --git a/foundry.lock b/foundry.lock index 52f31524..fe1fb56d 100644 --- a/foundry.lock +++ b/foundry.lock @@ -5,7 +5,7 @@ "lib/forge-std": { "branch": { "name": "master", - "rev": "c2cf7017d27c1d20e74ace4dacb6c5ce4bbbe899" + "rev": "27ab851c7cde58b22cc4de426d59e23a31c5cb9c" } }, "lib/murky": { @@ -17,7 +17,7 @@ "lib/solady": { "branch": { "name": "main", - "rev": "acd959aa4bd04720d640bf4e6a5c71037510cc4b" + "rev": "73f13dd1483707ef6b4d16cb0543570b7e1715a8" } } } \ No newline at end of file diff --git a/foundry.toml b/foundry.toml index 9bc78c0c..b26c00b5 100644 --- a/foundry.toml +++ b/foundry.toml @@ -4,15 +4,17 @@ # The Default Profile [profile.default] -evm_version = "prague" +# 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. 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/config.toml" } + { access = "read-write", path = "deploy/registry" }, + { access = "read", path = "deploy/config.toml" } ] [fmt] @@ -32,3 +34,5 @@ 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 new file mode 100644 index 00000000..35524c7b --- /dev/null +++ b/gas-snapshots/.gas-snapshot-main @@ -0,0 +1,11 @@ +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 new file mode 100644 index 00000000..26d33521 --- /dev/null +++ b/lib/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/lib/.idea/caches/deviceStreaming.xml b/lib/.idea/caches/deviceStreaming.xml new file mode 100644 index 00000000..94e429a7 --- /dev/null +++ b/lib/.idea/caches/deviceStreaming.xml @@ -0,0 +1,2007 @@ + + + + + + \ No newline at end of file diff --git a/lib/.idea/lib.iml b/lib/.idea/lib.iml new file mode 100644 index 00000000..d6ebd480 --- /dev/null +++ b/lib/.idea/lib.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/lib/.idea/misc.xml b/lib/.idea/misc.xml new file mode 100644 index 00000000..862d09bd --- /dev/null +++ b/lib/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/lib/.idea/modules.xml b/lib/.idea/modules.xml new file mode 100644 index 00000000..b0c4ff69 --- /dev/null +++ b/lib/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/lib/.idea/vcs.xml b/lib/.idea/vcs.xml new file mode 100644 index 00000000..d3a8bd59 --- /dev/null +++ b/lib/.idea/vcs.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/lib/LayerZero-v2 b/lib/LayerZero-v2 index 88428755..ab9b0834 160000 --- a/lib/LayerZero-v2 +++ b/lib/LayerZero-v2 @@ -1 +1 @@ -Subproject commit 88428755be6caa71cb1d2926141d73c8989296b5 +Subproject commit ab9b083410b9359285a5756807e1b6145d4711a7 diff --git a/lib/forge-std b/lib/forge-std index c2cf7017..07853315 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit c2cf7017d27c1d20e74ace4dacb6c5ce4bbbe899 +Subproject commit 07853315f998f94dc724e464b1bab1270888ee64 diff --git a/lib/solady b/lib/solady index acd959aa..90db92ce 160000 --- a/lib/solady +++ b/lib/solady @@ -1 +1 @@ -Subproject commit acd959aa4bd04720d640bf4e6a5c71037510cc4b +Subproject commit 90db92ce173856605d24a554969f2c67cadbc7e9 diff --git a/prep/check-bytecode-changes.js b/prep/check-bytecode-changes.js old mode 100755 new mode 100644 diff --git a/snapshots/BenchmarkTest.json b/snapshots/BenchmarkTest.json index 82f7d2ae..dedb663c 100644 --- a/snapshots/BenchmarkTest.json +++ b/snapshots/BenchmarkTest.json @@ -1,53 +1,53 @@ { - "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" + "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" } \ No newline at end of file diff --git a/src/Escrow.sol b/src/Escrow.sol index a924bc59..6138cab9 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) { + if (block.timestamp > _escrow.refundTimestamp || msg.sender == _escrow.recipient) { + _refundDepositor(escrowIds[i], _escrow); + _refundRecipient(escrowIds[i], _escrow); + } else { revert RefundInvalid(); } - - _refundDepositor(escrowIds[i], _escrow); - _refundRecipient(escrowIds[i], _escrow); } } @@ -147,10 +147,11 @@ 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) { + if (block.timestamp > _escrow.refundTimestamp || msg.sender == _escrow.depositor) { + _refundDepositor(escrowIds[i], _escrow); + } else { revert RefundInvalid(); } - _refundDepositor(escrowIds[i], _escrow); } } @@ -181,11 +182,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) { + if (block.timestamp > _escrow.refundTimestamp || msg.sender == _escrow.recipient) { + _refundRecipient(escrowIds[i], _escrow); + } else { revert RefundInvalid(); } - - _refundRecipient(escrowIds[i], _escrow); } } diff --git a/src/GuardedExecutor.sol b/src/GuardedExecutor.sol index cf5eb502..4d655929 100644 --- a/src/GuardedExecutor.sol +++ b/src/GuardedExecutor.sol @@ -1,7 +1,6 @@ // 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"; @@ -13,6 +12,7 @@ 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 SuperAdminCanSpendAnything(); + if (_isSuperAdmin(keyHash)) revert SuperAdminCanExecuteEverything(); } // 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 overriden to return if `keyHash` corresponds to a super admin key. + /// @dev To be overridden to return if `keyHash` corresponds to a super admin key. function _isSuperAdmin(bytes32 keyHash) internal view virtual returns (bool); - /// @dev To be overriden to return the storage slot seed for a `keyHash`. + /// @dev To be overridden 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 0f0068ac..ddef33fe 100644 --- a/src/IthacaAccount.sol +++ b/src/IthacaAccount.sol @@ -274,8 +274,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. @@ -399,7 +399,12 @@ 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); @@ -573,8 +578,9 @@ 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); } @@ -622,10 +628,12 @@ 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(); } @@ -747,6 +755,6 @@ contract IthacaAccount is IIthacaAccount, EIP712, GuardedExecutor { returns (string memory name, string memory version) { name = "IthacaAccount"; - version = "0.5.10"; + version = "0.5.11"; } } diff --git a/src/MultiSigSigner.sol b/src/MultiSigSigner.sol index 73d82df9..6f6fb272 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. - /// - Calcualated as: bytes4(keccak256("isValidSignatureWithKeyHash(bytes32,bytes32,bytes)") + /// - Calculated 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 signaturs > threshold. + /// - Failing owner signatures are ignored, as long as valid signatures > threshold. function isValidSignatureWithKeyHash(bytes32 digest, bytes32 keyHash, bytes memory signature) public view diff --git a/src/Orchestrator.sol b/src/Orchestrator.sol index 06586bcc..72815450 100644 --- a/src/Orchestrator.sol +++ b/src/Orchestrator.sol @@ -20,6 +20,7 @@ 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. @@ -41,7 +42,13 @@ import {MerkleProofLib} from "solady/utils/MerkleProofLib.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 { +contract Orchestrator is + IOrchestrator, + EIP712, + CallContextChecker, + ReentrancyGuardTransient, + IntentHelpers +{ using LibERC7579 for bytes32[]; using EfficientHashLib for bytes32[]; using LibBitmap for LibBitmap.Bitmap; @@ -110,12 +117,12 @@ contract Orchestrator is IOrchestrator, EIP712, CallContextChecker, ReentrancyGu /// @dev For EIP712 signature digest calculation for the `execute` function. bytes32 public constant INTENT_TYPEHASH = keccak256( - "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)" + "Intent(Call[] calls,address eoa,uint256 nonce,address payer,address paymentToken,uint256 paymentMaxAmount,uint256 combinedGas,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(bool multichain,address eoa,Call[] calls,uint256 nonce)Call(address to,uint256 value,bytes data)" + "SignedCall(address eoa,Call[] calls,uint256 nonce)Call(address to,uint256 value,bytes data)" ); /// @dev For EIP712 signature digest calculation for the `execute` function. @@ -127,6 +134,10 @@ contract Orchestrator is IOrchestrator, EIP712, CallContextChecker, ReentrancyGu /// 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. @@ -195,7 +206,7 @@ contract Orchestrator is IOrchestrator, EIP712, CallContextChecker, ReentrancyGu /// 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) - public + external payable virtual nonReentrant @@ -211,7 +222,6 @@ contract Orchestrator is IOrchestrator, EIP712, CallContextChecker, ReentrancyGu public payable virtual - nonReentrant returns (bytes4[] memory errs) { // This allocation and loop was initially in assembly, but I've normified it for now. @@ -220,7 +230,7 @@ contract Orchestrator is IOrchestrator, EIP712, CallContextChecker, ReentrancyGu // 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]) = _execute(encodedIntents[i], 0, _NORMAL_MODE_FLAG); + errs[i] = this.execute(encodedIntents[i]); } } @@ -232,11 +242,16 @@ contract Orchestrator is IOrchestrator, EIP712, CallContextChecker, ReentrancyGu /// 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( - bool isStateOverride, - uint256 combinedGasOverride, - bytes calldata encodedIntent - ) external payable returns (uint256) { + 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) + } + // If Simulation Fails, then it will revert here. (uint256 gUsed, bytes4 err) = _execute(encodedIntent, combinedGasOverride, _SIMULATION_MODE_FLAG); @@ -260,33 +275,7 @@ contract Orchestrator is IOrchestrator, EIP712, CallContextChecker, ReentrancyGu } } - /// @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 @@ -303,7 +292,7 @@ contract Orchestrator is IOrchestrator, EIP712, CallContextChecker, ReentrancyGu /// @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 flexiblity in the future. + /// But we use a uint256 for efficient stack operations, and more flexibility 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) @@ -311,12 +300,11 @@ contract Orchestrator is IOrchestrator, EIP712, CallContextChecker, ReentrancyGu virtual returns (uint256 gUsed, bytes4 err) { - Intent calldata i = _extractIntent(encodedIntent); - - uint256 g = Math.coalesce(uint96(combinedGasOverride), i.combinedGas); + uint256 g = Math.coalesce(uint96(combinedGasOverride), _getCombinedGas()); uint256 gStart = gasleft(); + address eoa = _getEoa(); - if (i.paymentAmount > i.paymentMaxAmount) { + if (_getPaymentAmount() > _getPaymentMaxAmount()) { err = PaymentError.selector; if (flags == _SIMULATION_MODE_FLAG) { @@ -328,15 +316,20 @@ contract Orchestrator is IOrchestrator, EIP712, CallContextChecker, ReentrancyGu // 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. - if (((gasleft() * 63) >> 6) < Math.saturatingAdd(g, _INNER_GAS_OVERHEAD)) { + uint256 gasAvailable = (gasleft() * 63) >> 6; + uint256 gasRequired = Math.saturatingAdd(g, _INNER_GAS_OVERHEAD); + + if (gasAvailable < gasRequired) { if (flags != _SIMULATION_MODE_FLAG) { revert InsufficientGas(); } } } - if (i.supportedAccountImplementation != address(0)) { - if (accountImplementationOf(i.eoa) != i.supportedAccountImplementation) { + address accountImpl = _getSupportedAccountImplementation(); + if (accountImpl != address(0)) { + address currentImpl = accountImplementationOf(eoa); + if (currentImpl != accountImpl) { err = UnsupportedAccountImplementation.selector; if (flags == _SIMULATION_MODE_FLAG) { revert UnsupportedAccountImplementation(); @@ -344,18 +337,16 @@ contract Orchestrator is IOrchestrator, EIP712, CallContextChecker, ReentrancyGu } } - address payer = Math.coalesce(i.payer, i.eoa); + address payer = Math.coalesce(_getPayer(), 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 (!i.isMultichain && LibBit.and(i.paymentAmount != 0, err == 0)) { - if (TokenTransferLib.balanceOf(i.paymentToken, payer) < i.paymentAmount) { - err = PaymentError.selector; + if (TokenTransferLib.balanceOf(_getPaymentToken(), payer) < _getPaymentAmount()) { + err = PaymentError.selector; - if (flags == _SIMULATION_MODE_FLAG) { - revert PaymentError(); - } + if (flags == _SIMULATION_MODE_FLAG) { + revert PaymentError(); } } @@ -364,11 +355,14 @@ contract Orchestrator is IOrchestrator, EIP712, CallContextChecker, ReentrancyGu 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. @@ -380,26 +374,27 @@ contract Orchestrator is IOrchestrator, EIP712, CallContextChecker, ReentrancyGu address(), 0, add(m, 0x1c), - add(encodedIntent.length, 0x24), + add(encodedIntent.length, 0x44), 0x00, 0x20 ) err := mload(0x00) // The self call will do another self call to execute. - - 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()`. + } + } + 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()`. } } - emit IntentExecuted(i.eoa, i.nonce, selfCallSuccess, err); + emit IntentExecuted(_getEoa(), _getNonce(), selfCallSuccess, err); if (selfCallSuccess) { gUsed = Math.rawSub(gStart, gasleft()); } @@ -432,25 +427,36 @@ contract Orchestrator is IOrchestrator, EIP712, CallContextChecker, ReentrancyGu 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. - if (i.expiry != 0 && block.timestamp > i.expiry) { - revert IntentExpired(); + { + uint256 expiry = _getExpiry(); + if (expiry != 0 && block.timestamp > expiry) { + revert IntentExpired(); + } } - address eoa = i.eoa; - uint256 nonce = i.nonce; - bytes32 digest = _computeDigest(i); + 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); - _fund(eoa, i.funder, digest, i.encodedFundTransfers, i.funderSignature); + { + bytes calldata fundData = _getNextBytes(ptr); + 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. @@ -466,57 +472,82 @@ contract Orchestrator is IOrchestrator, EIP712, CallContextChecker, ReentrancyGu // simulation, and suggests banning users that intentionally grief the simulation. // Handle the sub Intents after initialize (if any), and before the `_verify`. - if (i.encodedPreCalls.length != 0) _handlePreCalls(eoa, i.payer, flags, i.encodedPreCalls); - + { + bytes calldata preCallsBytes = _getNextBytes(ptr); + if (preCallsBytes.length > 0) { + _handlePreCalls(eoa, _getPayer(), flags, preCallsBytes); + } + } // 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; - 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); + { + 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:]); + } } - } 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. - if (i.paymentAmount != 0) _pay(keyHash, digest, i); + { + uint256 paymentAmount = _getPaymentAmount(); + bytes calldata paymentSignature = _getNextBytes(ptr); + if (paymentAmount != 0) { + _pay( + paymentAmount, + keyHash, + digest, + eoa, + _getPayer(), + _getPaymentToken(), + _getPaymentRecipient(), + paymentSignature + ); + } + } // 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 data = LibERC7579.reencodeBatchAsExecuteCalldata( + bytes memory executeData = LibERC7579.reencodeBatchAsExecuteCalldata( hex"01000000000078210001", // ERC7821 batch execution mode. - i.executionData, + _getExecutionData(), abi.encode(keyHash) // `opData`. ); assembly ("memory-safe") { mstore(0x00, 0) // Zeroize the return slot. - if iszero(call(gas(), eoa, 0, add(0x20, data), mload(data), 0x00, 0x20)) { + if iszero(call(gas(), eoa, 0, add(0x20, executeData), mload(executeData), 0x00, 0x20)) { if eq(flags, _SIMULATION_MODE_FLAG) { returndatacopy(mload(0x40), 0x00, returndatasize()) revert(mload(0x40), returndatasize()) @@ -539,10 +570,15 @@ contract Orchestrator is IOrchestrator, EIP712, CallContextChecker, ReentrancyGu address parentEOA, address payer, uint256 flags, - bytes[] calldata encodedPreCalls + bytes calldata encodedPreCalls ) internal virtual { - for (uint256 j; j < encodedPreCalls.length; ++j) { - SignedCall calldata p = _extractPreCall(encodedPreCalls[j]); + 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]); address eoa = Math.coalesce(p.eoa, parentEOA); uint256 nonce = p.nonce; @@ -598,28 +634,6 @@ contract Orchestrator is IOrchestrator, EIP712, CallContextChecker, ReentrancyGu // 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 @@ -629,7 +643,7 @@ contract Orchestrator is IOrchestrator, EIP712, CallContextChecker, ReentrancyGu address eoa, address funder, bytes32 digest, - bytes[] memory encodedFundTransfers, + bytes[] calldata encodedFundTransfers, bytes memory funderSignature ) internal virtual { // Note: The fund function is mostly only used in the multi chain mode. @@ -664,51 +678,59 @@ contract Orchestrator is IOrchestrator, EIP712, CallContextChecker, ReentrancyGu /// @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(bytes32 keyHash, bytes32 digest, Intent calldata i) internal virtual { - uint256 paymentAmount = i.paymentAmount; - uint256 requiredBalanceAfter = Math.saturatingAdd( - TokenTransferLib.balanceOf(i.paymentToken, i.paymentRecipient), paymentAmount - ); + 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); - address payer = Math.coalesce(i.payer, i.eoa); + uint256 requiredBalanceAfter = Math.saturatingAdd(currentBalance, paymentAmount); // 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, 0xf81d87a7) // `pay(uint256,bytes32,bytes32,bytes)` - mstore(add(m, 0x20), paymentAmount) // Add payment amount as first param + mstore(m, 0x38e11b2a) // `pay(uint256,bytes32,bytes32,address,address,address,address,bytes)` + mstore(add(m, 0x20), paymentAmount) // Add paymentAmount as first param mstore(add(m, 0x40), keyHash) // Add keyHash as second 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) + 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, 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) + // Store paymentSignature length and data + mstore(add(m, 0x120), paymentSignature.length) + calldatacopy(add(m, 0x140), paymentSignature.offset, paymentSignature.length) // 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 - payer, // address + callee, // address 0, // value add(m, 0x1c), // input memory offset - add(0xc4, encodedSize), // input size + add(0x124, paymentSignature.length), // input size 0x00, // output memory offset 0x20 // output size ) ) { revert(0x00, 0x20) } } - if (TokenTransferLib.balanceOf(i.paymentToken, i.paymentRecipient) < requiredBalanceAfter) { + if (TokenTransferLib.balanceOf(paymentToken, paymentRecipient) < requiredBalanceAfter) { revert PaymentError(); } } @@ -752,42 +774,39 @@ contract Orchestrator is IOrchestrator, EIP712, CallContextChecker, ReentrancyGu /// @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(5); + bytes32[] memory f = EfficientHashLib.malloc(4); f.set(0, SIGNED_CALL_TYPEHASH); - f.set(1, LibBit.toUint(isMultichain)); - f.set(2, uint160(p.eoa)); - f.set(3, _executionDataHash(p.executionData)); - f.set(4, p.nonce); + f.set(1, uint160(p.eoa)); + f.set(2, _executionDataHash(p.executionData)); + f.set(3, p.nonce); - return isMultichain ? _hashTypedDataSansChainId(f.hash()) : _hashTypedData(f.hash()); + return p.nonce >> 240 == MULTICHAIN_NONCE_PREFIX + ? _hashTypedDataSansChainId(f.hash()) + : _hashTypedData(f.hash()); } - /// @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; + /// @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; - // 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()); + 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); } /// @dev Helper function to return the hash of the `execuctionData`. @@ -840,7 +859,7 @@ contract Orchestrator is IOrchestrator, EIP712, CallContextChecker, ReentrancyGu returns (string memory name, string memory version) { name = "Orchestrator"; - version = "0.5.5"; + version = "0.5.6"; } //////////////////////////////////////////////////////////////////////// diff --git a/src/SimpleFunder.sol b/src/SimpleFunder.sol index e74947fb..7109f028 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.8"; + version = "0.1.9"; } //////////////////////////////////////////////////////////////////////// @@ -133,11 +133,10 @@ 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(); } @@ -183,7 +182,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) // type(uint256).max + mstore(add(m, 0x40), 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) // 20-byte all-ones sentinel (2^160-1), not 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 1e468d6b..2a5e2a76 100644 --- a/src/Simulator.sol +++ b/src/Simulator.sol @@ -4,10 +4,12 @@ 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 { +contract Simulator is IntentHelpers { //////////////////////////////////////////////////////////////////////// // EIP-5267 Support //////////////////////////////////////////////////////////////////////// @@ -54,15 +56,23 @@ contract Simulator { /// @dev Updates the payment amounts for the Intent passed in. function _updatePaymentAmounts( - ICommon.Intent memory u, + bytes memory u, uint256 gas, uint8 paymentPerGasPrecision, uint256 paymentPerGas ) internal pure { uint256 paymentAmount = Math.fullMulDiv(gas, paymentPerGas, 10 ** paymentPerGasPrecision); - u.paymentAmount += paymentAmount; - u.paymentMaxAmount += paymentAmount; + // 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)) + } } /// @dev Performs a call to the Orchestrator, and returns the gas used by the Intent. @@ -101,10 +111,8 @@ contract Simulator { bytes calldata encodedIntent ) internal freeTempMemory returns (uint256) { bytes memory data = abi.encodeWithSignature( - "simulateExecute(bool,uint256,bytes)", - isStateOverride, - combinedGasOverride, - encodedIntent + "simulateExecute(bytes)", + abi.encodePacked(encodedIntent, isStateOverride, combinedGasOverride) ); return _callOrchestrator(oc, isStateOverride, data); } @@ -115,13 +123,10 @@ contract Simulator { address oc, bool isStateOverride, uint256 combinedGasOverride, - ICommon.Intent memory u + bytes memory u ) internal freeTempMemory returns (uint256) { bytes memory data = abi.encodeWithSignature( - "simulateExecute(bool,uint256,bytes)", - isStateOverride, - combinedGasOverride, - abi.encode(u) + "simulateExecute(bytes)", abi.encodePacked(u, isStateOverride, combinedGasOverride) ); return _callOrchestrator(oc, isStateOverride, data); } @@ -257,7 +262,7 @@ contract Simulator { /// @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 encodedIntent The encoded user operation + /// @param calldataIntent The encoded intent /// @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. @@ -268,10 +273,10 @@ contract Simulator { uint8 paymentPerGasPrecision, uint256 paymentPerGas, uint256 combinedGasIncrement, - bytes calldata encodedIntent + bytes calldata calldataIntent ) 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, encodedIntent); + gasUsed = _callOrchestratorCalldata(oc, false, type(uint256).max, calldataIntent); // If the simulation failed, bubble up the full revert. assembly ("memory-safe") { @@ -282,15 +287,21 @@ contract Simulator { } } - // Update payment amounts using the gasUsed value - ICommon.Intent memory u = abi.decode(encodedIntent, (ICommon.Intent)); + bytes memory memoryIntent = calldataIntent; - u.combinedGas += gasUsed; + // 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 + } - _updatePaymentAmounts(u, u.combinedGas, paymentPerGasPrecision, paymentPerGas); + _updatePaymentAmounts(memoryIntent, combinedGas, paymentPerGasPrecision, paymentPerGas); while (true) { - gasUsed = _callOrchestratorMemory(oc, false, 0, u); + gasUsed = _callOrchestratorMemory(oc, false, 0, memoryIntent); // If the simulation failed, bubble up the full revert. assembly ("memory-safe") { @@ -305,22 +316,34 @@ contract Simulator { } if (gasUsed != 0) { - return (gasUsed, u.combinedGas); + // Get current combinedGas from bytes + assembly ("memory-safe") { + combinedGas := mload(add(memoryIntent, offset)) + } + return (gasUsed, combinedGas); } - uint256 gasIncrement = Math.mulDiv(u.combinedGas, combinedGasIncrement, 10_000); + // Get current combinedGas and calculate increment + assembly ("memory-safe") { + combinedGas := mload(add(memoryIntent, offset)) + } + uint256 gasIncrement = Math.mulDiv(combinedGas, combinedGasIncrement, 10_000); - _updatePaymentAmounts(u, gasIncrement, paymentPerGasPrecision, paymentPerGas); + _updatePaymentAmounts(memoryIntent, gasIncrement, paymentPerGasPrecision, paymentPerGas); - // Step up the combined gas, until we see a simulation passing - u.combinedGas += gasIncrement; + // 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) + } } } /// @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 succesful combinedGas is found. + /// @param combinedGasVerificationOffset is a static value that is added after a successful 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) @@ -338,15 +361,18 @@ contract Simulator { combinedGas += combinedGasVerificationOffset; - ICommon.Intent memory u = abi.decode(encodedIntent, (ICommon.Intent)); + bytes memory encodedIntentCopy = encodedIntent; - _updatePaymentAmounts(u, combinedGas, paymentPerGasPrecision, paymentPerGas); + _updatePaymentAmounts(encodedIntentCopy, combinedGas, paymentPerGasPrecision, paymentPerGas); - u.combinedGas = combinedGas; + // 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) + } // Verification Run to generate the logs with the correct combinedGas and payment amounts. - gasUsed = _callOrchestratorMemory(oc, true, 0, u); - + gasUsed = _callOrchestratorMemory(oc, true, 0, encodedIntentCopy); // 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 54ccf388..9057b1f3 100644 --- a/src/interfaces/ICommon.sol +++ b/src/interfaces/ICommon.sol @@ -2,86 +2,6 @@ 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 90c2547f..ddbacc0f 100644 --- a/src/interfaces/IFunder.sol +++ b/src/interfaces/IFunder.sol @@ -16,9 +16,6 @@ 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 099f466d..9056db7c 100644 --- a/src/interfaces/IIthacaAccount.sol +++ b/src/interfaces/IIthacaAccount.sol @@ -7,14 +7,23 @@ 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, - bytes calldata encodedIntent + address eoa, + address payer, + address paymentToken, + address paymentRecipient, + bytes calldata paymentSignature ) external; /// @dev Returns if the signature is valid, along with its `keyHash`. diff --git a/src/interfaces/IMulticall3.sol b/src/interfaces/IMulticall3.sol deleted file mode 100644 index c0ab86f2..00000000 --- a/src/interfaces/IMulticall3.sol +++ /dev/null @@ -1,38 +0,0 @@ -// 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 ffb4de93..05dfabb4 100644 --- a/src/interfaces/IOrchestrator.sol +++ b/src/interfaces/IOrchestrator.sol @@ -27,11 +27,10 @@ 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( - bool isStateOverride, - uint256 combinedGasOverride, - bytes calldata encodedIntent - ) external payable returns (uint256 gasUsed); + function simulateExecute(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 new file mode 100644 index 00000000..5f726b42 --- /dev/null +++ b/src/libraries/ERC7821Ithaca.sol @@ -0,0 +1,347 @@ +// 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 new file mode 100644 index 00000000..fc4f2722 --- /dev/null +++ b/src/libraries/IntentHelpers.sol @@ -0,0 +1,158 @@ +// 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 deleted file mode 100644 index 4a87d7a2..00000000 --- a/src/vendor/layerzero/interfaces/IOAppCore.sol +++ /dev/null @@ -1,54 +0,0 @@ -// 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 deleted file mode 100644 index 4d3c82d6..00000000 --- a/src/vendor/layerzero/interfaces/IOAppMsgInspector.sol +++ /dev/null @@ -1,25 +0,0 @@ -// 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 deleted file mode 100644 index 0fe4317f..00000000 --- a/src/vendor/layerzero/interfaces/IOAppReceiver.sol +++ /dev/null @@ -1,27 +0,0 @@ -// 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 deleted file mode 100644 index 40042d7d..00000000 --- a/src/vendor/layerzero/mocks/EndpointV2Mock.sol +++ /dev/null @@ -1,418 +0,0 @@ -// 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 deleted file mode 100644 index a4d9576a..00000000 --- a/src/vendor/layerzero/oapp/OApp.sol +++ /dev/null @@ -1,38 +0,0 @@ -// 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 deleted file mode 100644 index 3eff1be5..00000000 --- a/src/vendor/layerzero/oapp/OAppCore.sol +++ /dev/null @@ -1,104 +0,0 @@ -// 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 deleted file mode 100644 index d3c34ad0..00000000 --- a/src/vendor/layerzero/oapp/OAppReceiver.sol +++ /dev/null @@ -1,143 +0,0 @@ -// 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 deleted file mode 100644 index 6fdf8ef4..00000000 --- a/src/vendor/layerzero/oapp/OAppSender.sol +++ /dev/null @@ -1,136 +0,0 @@ -// 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 d612ef5a..e4b8310b 100644 --- a/test/Account.t.sol +++ b/test/Account.t.sol @@ -354,49 +354,4 @@ 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 94749645..4e353a9a 100644 --- a/test/Base.t.sol +++ b/test/Base.t.sol @@ -4,7 +4,6 @@ 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"; @@ -24,6 +23,8 @@ 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; @@ -34,7 +35,6 @@ contract BaseTest is SoladyTest { EIP7702Proxy eip7702Proxy; TargetFunctionPayload[] targetFunctionPayloads; Simulator simulator; - bytes32 contextKeyHash; struct TargetFunctionPayload { address by; @@ -56,6 +56,9 @@ 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; @@ -95,10 +98,6 @@ 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 2ce04524..53219a9d 100644 --- a/test/Benchmark.t.sol +++ b/test/Benchmark.t.sol @@ -120,8 +120,9 @@ 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(""); @@ -1518,7 +1519,7 @@ contract BenchmarkTest is BaseTest { } } - function testERC20Transfer_IthacaAccount() public { + function testERC20Transfer_IthacaAccount1() public { DelegatedEOA[] memory delegatedEOAs = _createIthacaAccount(1); bytes memory payload = _transferExecutionData(address(paymentToken), address(0xbabe), 1 ether); @@ -1714,7 +1715,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++) { - Orchestrator.Intent memory u; + Intent memory u; u.eoa = delegatedEOAs[i].eoa; u.nonce = 0; u.combinedGas = 1000000; @@ -1750,14 +1751,14 @@ contract BenchmarkTest is BaseTest { _paymentType == PaymentType.APP_SPONSOR || _paymentType == PaymentType.APP_SPONSOR_ERC20 ) { - bytes32 digest = oc.computeDigest(u); + bytes32 digest = computeDigest(u); bytes32 signatureDigest = appSponsor.computeSignatureDigest(digest); u.paymentSignature = _eoaSig(paymasterPrivateKey, signatureDigest); } u.signature = _sig(delegatedEOAs[i], u); - encodedIntents[i] = abi.encodePacked(abi.encode(u), junk); + encodedIntents[i] = abi.encodePacked(encodeIntent(u), junk); } return encodedIntents; @@ -1785,7 +1786,7 @@ contract BenchmarkTest is BaseTest { d.d.setSpendLimit(k.keyHash, address(0), GuardedExecutor.SpendPeriod.Hour, 1 ether); vm.stopPrank(); - Orchestrator.Intent memory u; + Intent memory u; u.eoa = d.eoa; u.nonce = 0; u.combinedGas = 1000000; @@ -1797,7 +1798,7 @@ contract BenchmarkTest is BaseTest { u.signature = _sig(k, u); bytes[] memory encodedIntents = new bytes[](1); - encodedIntents[0] = abi.encode(u); + encodedIntents[0] = encodeIntent(u); oc.execute(encodedIntents); vm.snapshotGasLastCall("testERC20Transfer_IthacaAccountWithSpendLimits"); @@ -1805,6 +1806,52 @@ contract BenchmarkTest is BaseTest { assertEq(paymentToken.balanceOf(address(0xbabe)), 1 ether); } + function testERC20TransferViaPortoOrchestratorWithPasskey() public { + vm.pauseGasMetering(); + + PassKey memory k = _randomSecp256k1PassKey(); + + DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); + vm.deal(d.eoa, type(uint128).max); + _mint(address(paymentToken), d.eoa, type(uint128).max); + + vm.startPrank(d.eoa); + d.d.authorize(k.k); + d.d.setCanExecute( + k.keyHash, address(paymentToken), bytes4(keccak256("transfer(address,uint256)")), true + ); + d.d.setSpendLimit( + k.keyHash, address(paymentToken), GuardedExecutor.SpendPeriod.Hour, type(uint256).max + ); + d.d.setSpendLimit(k.keyHash, address(0), GuardedExecutor.SpendPeriod.Hour, type(uint256).max); + vm.stopPrank(); + + Orchestrator.Intent memory u; + u.eoa = d.eoa; + u.nonce = 0; + u.combinedGas = 1000000; + u.prePaymentAmount = 0 ether; + u.prePaymentMaxAmount = 0 ether; + u.totalPaymentAmount = 0.01 ether; + u.totalPaymentMaxAmount = 0.1 ether; + u.paymentToken = address(0); + // To maintain parity with the old benchmarks. + u.paymentRecipient = address(oc); + u.executionData = _transferExecutionData(address(paymentToken), address(0xbabe), 1 ether); + u.signature = _sig(k, u); + + bytes[] memory encodedIntents = new bytes[](1); + encodedIntents[0] = abi.encode(u); + + vm.resumeGasMetering(); + + oc.execute(encodedIntents); + + vm.pauseGasMetering(); + assertEq(paymentToken.balanceOf(address(0xbabe)), 1 ether); + vm.resumeGasMetering(); + } + function testERC20TransferDirect() public { DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); _giveAccountSomeTokens(d.eoa); diff --git a/test/Escrow.t.sol b/test/Escrow.t.sol index 57a6e8c0..ce105e8a 100644 --- a/test/Escrow.t.sol +++ b/test/Escrow.t.sol @@ -723,6 +723,80 @@ 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 a30c941e..d41e341f 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); - Orchestrator.Intent memory u; + Intent memory u; u.eoa = d.eoa; u.combinedGas = 10000000; @@ -38,7 +38,8 @@ contract GuardedExecutorTest is BaseTest { u.executionData = abi.encode(calls); u.signature = _sig(k, u); assertEq( - oc.execute(abi.encode(u)), bytes4(keccak256("UnauthorizedCall(bytes32,address,bytes)")) + oc.execute(encodeIntent(u)), + bytes4(keccak256("UnauthorizedCall(bytes32,address,bytes)")) ); // Set the call checker. bytes32 forKeyHash = _randomChance(2) ? k.keyHash : _ANY_KEYHASH; @@ -56,7 +57,8 @@ contract GuardedExecutorTest is BaseTest { u.executionData = abi.encode(calls); u.signature = _sig(k, u); assertEq( - oc.execute(abi.encode(u)), bytes4(keccak256("UnauthorizedCall(bytes32,address,bytes)")) + oc.execute(encodeIntent(u)), + bytes4(keccak256("UnauthorizedCall(bytes32,address,bytes)")) ); // Try, now with the checker configured to authorize the call.. @@ -66,7 +68,7 @@ contract GuardedExecutorTest is BaseTest { u.nonce = d.d.getNonce(0); u.executionData = abi.encode(calls); u.signature = _sig(k, u); - assertEq(oc.execute(abi.encode(u)), bytes4(0)); + assertEq(oc.execute(encodeIntent(u)), bytes4(0)); assertEq(counter.counter(), 1); // Try, now with the checker removed. @@ -79,7 +81,8 @@ contract GuardedExecutorTest is BaseTest { u.executionData = abi.encode(calls); u.signature = _sig(k, u); assertEq( - oc.execute(abi.encode(u)), bytes4(keccak256("UnauthorizedCall(bytes32,address,bytes)")) + oc.execute(encodeIntent(u)), + bytes4(keccak256("UnauthorizedCall(bytes32,address,bytes)")) ); } @@ -94,7 +97,7 @@ contract GuardedExecutorTest is BaseTest { d.d.setCanExecute(k.keyHash, address(paymentToken), _ANY_FN_SEL, true); vm.stopPrank(); - Orchestrator.Intent memory u; + Intent memory u; u.eoa = d.eoa; u.combinedGas = 10000000; @@ -112,7 +115,7 @@ contract GuardedExecutorTest is BaseTest { u.nonce = d.d.getNonce(0); u.executionData = abi.encode(calls); u.signature = _sig(k, u); - assertEq(oc.execute(abi.encode(u)), bytes4(keccak256("NoSpendPermissions()"))); + assertEq(oc.execute(encodeIntent(u)), bytes4(keccak256("NoSpendPermissions()"))); // Check that after the spend permission has been done, the token can be approved // and moved via `transferFrom`. @@ -127,7 +130,7 @@ contract GuardedExecutorTest is BaseTest { u.nonce = d.d.getNonce(0); u.executionData = abi.encode(calls); u.signature = _sig(k, u); - assertEq(oc.execute(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(u)), 0); assertEq(paymentToken.balanceOf(address(0xb0b)), 0.1 ether); // Check that the spent has been increased. @@ -232,7 +235,7 @@ contract GuardedExecutorTest is BaseTest { vm.prank(address(0xb0b)); paymentToken.approve(d.eoa, 1 ether); - Orchestrator.Intent memory u; + Intent memory u; u.eoa = d.eoa; u.combinedGas = 10000000; @@ -258,12 +261,12 @@ contract GuardedExecutorTest is BaseTest { emit LogBool("transferToSelf:", transferToSelf); if (transferToSelf) { - assertEq(oc.execute(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(u)), 0); assertEq(paymentToken.balanceOf(d.eoa), 0.1 ether); return; } - assertEq(oc.execute(abi.encode(u)), bytes4(keccak256("NoSpendPermissions()"))); + assertEq(oc.execute(encodeIntent(u)), bytes4(keccak256("NoSpendPermissions()"))); vm.startPrank(d.eoa); d.d @@ -274,7 +277,7 @@ contract GuardedExecutorTest is BaseTest { u.nonce = d.d.getNonce(0); u.signature = _sig(k, u); - assertEq(oc.execute(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(u)), 0); assertEq(paymentToken.balanceOf(address(0xdad)), 0.1 ether); assertEq(d.d.spendInfos(k.keyHash)[0].spent, 0.1 ether); } @@ -285,7 +288,7 @@ contract GuardedExecutorTest is BaseTest { } function testOnlySuperAdminAndEOACanSelfExecute() public { - Orchestrator.Intent memory u; + Intent memory u; DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); u.eoa = d.eoa; u.combinedGas = 10000000; @@ -327,14 +330,14 @@ contract GuardedExecutorTest is BaseTest { u.executionData = abi.encode(calls); u.signature = _eoaSig(d.privateKey, u); - assertEq(oc.execute(abi.encode(u)), 0, "2"); + assertEq(oc.execute(encodeIntent(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(abi.encode(u)), 0, "4"); + assertEq(oc.execute(encodeIntent(u)), 0, "4"); assertEq(d.d.x(), x, "5"); d.d.resetX(); @@ -342,7 +345,7 @@ contract GuardedExecutorTest is BaseTest { u.nonce = d.d.getNonce(0); u.signature = _sig(kRegular, u); assertEq( - oc.execute(abi.encode(u)), + oc.execute(encodeIntent(u)), bytes4(keccak256("UnauthorizedCall(bytes32,address,bytes)")), "6" ); @@ -353,7 +356,7 @@ contract GuardedExecutorTest is BaseTest { } function testSetAndRemoveSpendLimitRevertsForSuperAdmin() public { - Orchestrator.Intent memory u; + Intent memory u; DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); u.eoa = d.eoa; @@ -371,11 +374,11 @@ contract GuardedExecutorTest is BaseTest { calls[0].data = abi.encodeWithSelector(IthacaAccount.authorize.selector, k.k); u.executionData = abi.encode(calls); - u.nonce = 0xc1d0 << 240; + u.nonce = d.d.getNonce(0); u.signature = _sig(d, u); - assertEq(oc.execute(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(u)), 0); } // Set spend limits. { @@ -386,7 +389,7 @@ contract GuardedExecutorTest is BaseTest { u.executionData = abi.encode(calls); u.signature = _sig(d, u); - assertEq(oc.execute(abi.encode(u)), bytes4(keccak256("SuperAdminCanSpendAnything()"))); + assertEq(oc.execute(encodeIntent(u)), bytes4(keccak256("SuperAdminCanSpendAnything()"))); } // Remove spend limits. { @@ -397,14 +400,14 @@ contract GuardedExecutorTest is BaseTest { u.executionData = abi.encode(calls); u.signature = _sig(d, u); - assertEq(oc.execute(abi.encode(u)), bytes4(keccak256("SuperAdminCanSpendAnything()"))); + assertEq(oc.execute(encodeIntent(u)), bytes4(keccak256("SuperAdminCanSpendAnything()"))); } } function testSetAndRemoveSpendLimit(uint256 amount) public { vm.warp(86400 * 100); - Orchestrator.Intent memory u; + Intent memory u; DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); u.eoa = d.eoa; @@ -442,11 +445,11 @@ contract GuardedExecutorTest is BaseTest { calls[3] = _setSpendLimitCall(k, token, periods[1], 1 ether); u.executionData = abi.encode(calls); - u.nonce = 0xc1d0 << 240; + u.nonce = d.d.getNonce(0); u.signature = _eoaSig(d.privateKey, u); - assertEq(oc.execute(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(u)), 0); assertEq(d.d.spendInfos(k.keyHash).length, 2); } @@ -458,7 +461,7 @@ contract GuardedExecutorTest is BaseTest { u.nonce = d.d.getNonce(0); u.executionData = abi.encode(calls); u.signature = _sig(k, u); - assertEq(oc.execute(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(u)), 0); infos = d.d.spendInfos(k.keyHash); for (uint256 i; i < infos.length; ++i) { @@ -474,7 +477,7 @@ contract GuardedExecutorTest is BaseTest { u.nonce = d.d.getNonce(0); u.executionData = abi.encode(calls); u.signature = _sig(k, u); - assertEq(oc.execute(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(u)), 0); infos = d.d.spendInfos(k.keyHash); assertEq(infos.length, 1); @@ -490,7 +493,7 @@ contract GuardedExecutorTest is BaseTest { u.nonce = d.d.getNonce(0); u.executionData = abi.encode(calls); u.signature = _sig(k, u); - assertEq(oc.execute(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(u)), 0); infos = d.d.spendInfos(k.keyHash); for (uint256 i; i < infos.length; ++i) { if (infos[i].period == periods[0]) { @@ -513,7 +516,7 @@ contract GuardedExecutorTest is BaseTest { u.nonce = d.d.getNonce(0); u.executionData = abi.encode(calls); u.signature = _sig(k, u); - assertEq(oc.execute(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(u)), 0); infos = d.d.spendInfos(k.keyHash); for (uint256 i; i < infos.length; ++i) { @@ -534,7 +537,7 @@ contract GuardedExecutorTest is BaseTest { u.nonce = d.d.getNonce(0); u.executionData = abi.encode(calls); u.signature = _sig(k, u); - assertEq(oc.execute(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(u)), 0); assertEq(d.d.spendInfos(k.keyHash).length, 0); } @@ -555,9 +558,9 @@ contract GuardedExecutorTest is BaseTest { && calls[0].data[2] == bytes1(uint8(0x24)) && calls[0].data[3] == bytes1(uint8(0xd5))) || amount == 0 ) { - assertEq(oc.execute(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(u)), 0); } else { - assertEq(oc.execute(abi.encode(u)), bytes4(keccak256("NoSpendPermissions()"))); + assertEq(oc.execute(encodeIntent(u)), bytes4(keccak256("NoSpendPermissions()"))); } } @@ -570,7 +573,7 @@ contract GuardedExecutorTest is BaseTest { u.nonce = d.d.getNonce(0); u.executionData = abi.encode(calls); u.signature = _sig(k, u); - assertEq(oc.execute(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(u)), 0); infos = d.d.spendInfos(k.keyHash); for (uint256 i; i < infos.length; ++i) { @@ -587,7 +590,7 @@ contract GuardedExecutorTest is BaseTest { u.nonce = d.d.getNonce(0); u.executionData = abi.encode(calls); u.signature = _sig(k, u); - assertEq(oc.execute(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(u)), 0); infos = d.d.spendInfos(k.keyHash); for (uint256 i; i < infos.length; ++i) { @@ -597,7 +600,7 @@ contract GuardedExecutorTest is BaseTest { } function testSetSpendLimitWithTwoPeriods() public { - Orchestrator.Intent memory u; + Intent memory u; DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); u.eoa = d.eoa; @@ -628,11 +631,11 @@ contract GuardedExecutorTest is BaseTest { calls[5] = _setSpendLimitCall(k, token1, GuardedExecutor.SpendPeriod.Year, 1 ether); u.executionData = abi.encode(calls); - u.nonce = 0xc1d0 << 240; + u.nonce = d.d.getNonce(0); u.signature = _eoaSig(d.privateKey, u); - assertEq(oc.execute(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(u)), 0); assertEq(d.d.spendInfos(k.keyHash).length, 4); } @@ -642,11 +645,11 @@ contract GuardedExecutorTest is BaseTest { calls[0] = _transferCall2(token0, address(0xb0b), amount0); calls[1] = _transferCall2(token1, address(0xb0b), amount1); - u.nonce = 0; + u.nonce = d.d.getNonce(0); u.executionData = abi.encode(calls); u.signature = _sig(k, u); - assertEq(oc.execute(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(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); @@ -655,7 +658,7 @@ contract GuardedExecutorTest is BaseTest { } function testSpends(bytes32) public { - Orchestrator.Intent memory u; + Intent memory u; DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); u.eoa = d.eoa; @@ -690,17 +693,17 @@ contract GuardedExecutorTest is BaseTest { } u.executionData = abi.encode(calls); - u.nonce = 0xc1d0 << 240; + u.nonce = d.d.getNonce(0); u.signature = _eoaSig(d.privateKey, u); - assertEq(oc.execute(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(u)), 0); assertEq(d.d.spendInfos(k.keyHash).length, tokens.length); } // Test spends. { - u.nonce = 0; + u.nonce = d.d.getNonce(0); _deployPermit2(); if (_randomChance(2)) { @@ -756,7 +759,7 @@ contract GuardedExecutorTest is BaseTest { u.executionData = abi.encode(calls); u.signature = _sig(k, u); - assertEq(oc.execute(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(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]); @@ -806,7 +809,7 @@ contract GuardedExecutorTest is BaseTest { } function _testSpendWithPassKeyViaOrchestrator(PassKey memory k, address tokenToSpend) internal { - Orchestrator.Intent memory u; + Intent memory u; GuardedExecutor.SpendInfo memory info; uint256 gExecute; @@ -839,12 +842,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}(abi.encode(u)), 0); + assertEq(oc.execute{gas: gExecute}(encodeIntent(u)), 0); assertEq(d.d.spendInfos(k.keyHash).length, 2); assertEq(d.d.spendInfos(k.keyHash)[0].spent, 0); @@ -854,7 +857,7 @@ contract GuardedExecutorTest is BaseTest { // Prep Intent, and submit it. This Intent should pass. { - u.nonce = 0; + u.nonce++; ERC7821.Call[] memory calls = new ERC7821.Call[](1); calls[0] = _transferCall2(tokenToSpend, address(0xb0b), 0.6 ether); @@ -864,7 +867,7 @@ contract GuardedExecutorTest is BaseTest { u.signature = _sig(k, u); // Intent should pass. - assertEq(oc.execute{gas: gExecute}(abi.encode(u)), 0); + assertEq(oc.execute{gas: gExecute}(encodeIntent(u)), 0); assertEq(_balanceOf(tokenToSpend, address(0xb0b)), 0.6 ether); assertEq(d.d.spendInfos(k.keyHash)[0].spent, 0.6 ether); @@ -878,7 +881,7 @@ contract GuardedExecutorTest is BaseTest { u.signature = _sig(k, u); // Intent should fail. - assertEq(oc.execute(abi.encode(u)), GuardedExecutor.ExceededSpendLimit.selector); + assertEq(oc.execute(encodeIntent(u)), GuardedExecutor.ExceededSpendLimit.selector); } // Prep Intent to try to exactly hit daily spend limit. This Intent should pass. @@ -892,7 +895,7 @@ contract GuardedExecutorTest is BaseTest { (gExecute, u.combinedGas,) = _estimateGas(k, u); u.signature = _sig(k, u); - assertEq(oc.execute{gas: gExecute}(abi.encode(u)), 0); + assertEq(oc.execute{gas: gExecute}(encodeIntent(u)), 0); assertEq(_balanceOf(tokenToSpend, address(0xb0b)), 1 ether); assertEq(d.d.spendInfos(k.keyHash)[0].spent, 1 ether); } @@ -932,7 +935,7 @@ contract GuardedExecutorTest is BaseTest { u.signature = _sig(k, u); - assertEq(oc.execute{gas: gExecute}(abi.encode(u)), 0); + assertEq(oc.execute{gas: gExecute}(encodeIntent(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 941f2311..fe9be6ae 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 deleted file mode 100644 index 497e0618..00000000 --- a/test/MultiSigSigner.t.sol +++ /dev/null @@ -1,656 +0,0 @@ -// 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 d79c4cab..66d1bba7 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 { - Orchestrator.Intent[] intents; + 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 Orchestrator.Intent[](_random() & 3); + t.intents = new 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; - Orchestrator.Intent memory u = t.intents[i]; + 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] = abi.encode(u); + t.encodedIntents[i] = encodeIntent(u); } bytes4[] memory errors = oc.execute(t.encodedIntents); @@ -74,11 +74,12 @@ 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); - Orchestrator.Intent memory u; + Intent memory u; u.eoa = alice.eoa; u.nonce = 0; u.executionData = executionData; @@ -88,11 +89,9 @@ 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(abi.encode(u)), bytes4(keccak256("PaymentError()"))); + assertEq(oc.execute(encodeIntent(u)), bytes4(keccak256("Unauthorized()"))); } function testExecuteWithSecp256k1PassKey() public { @@ -107,7 +106,7 @@ contract OrchestratorTest is BaseTest { vm.prank(d.eoa); d.d.authorize(k.k); - Orchestrator.Intent memory u; + Intent memory u; u.eoa = d.eoa; u.nonce = 0; u.executionData = _transferExecutionData(address(paymentToken), address(0xabcd), 1 ether); @@ -131,7 +130,7 @@ contract OrchestratorTest is BaseTest { combinedGasVerificationOffset: 0 }) ); - assertEq(oc.execute(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(u)), 0); uint256 actualAmount = 0.1 ether; assertEq(paymentToken.balanceOf(address(oc)), actualAmount); assertEq(paymentToken.balanceOf(d.eoa), 50 ether - actualAmount - 1 ether); @@ -154,7 +153,7 @@ contract OrchestratorTest is BaseTest { calls[0].to = target; calls[0].data = abi.encodeWithSignature("revertWithData(bytes)", data); - Orchestrator.Intent memory u; + Intent memory u; u.eoa = d.eoa; u.nonce = 0; u.executionData = abi.encode(calls); @@ -163,7 +162,7 @@ contract OrchestratorTest is BaseTest { u.signature = _sig(k, u); (bool success, bytes memory result) = - address(oc).call(abi.encodeWithSignature("simulateFailed(bytes)", abi.encode(u))); + address(oc).call(abi.encodeWithSignature("simulateFailed(bytes)", encodeIntent(u))); assertFalse(success); assertEq(result, abi.encodeWithSignature("ErrorWithData(bytes)", data)); @@ -174,7 +173,7 @@ contract OrchestratorTest is BaseTest { paymentToken.mint(d.eoa, 500 ether); - Orchestrator.Intent memory u; + Intent memory u; u.eoa = d.eoa; u.nonce = 0; u.executionData = _transferExecutionData(address(paymentToken), address(0xabcd), 1 ether); @@ -195,7 +194,7 @@ contract OrchestratorTest is BaseTest { }) ); - assertEq(oc.execute(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(u)), 0); uint256 actualAmount = 10 ether; assertEq(paymentToken.balanceOf(address(this)), actualAmount); assertEq(paymentToken.balanceOf(d.eoa), 500 ether - actualAmount - 1 ether); @@ -212,7 +211,7 @@ contract OrchestratorTest is BaseTest { ds[i] = _randomEIP7702DelegatedEOA(); paymentToken.mint(ds[i].eoa, 1 ether); - Orchestrator.Intent memory u; + Intent memory u; u.eoa = ds[i].eoa; u.nonce = 0; u.executionData = @@ -224,7 +223,7 @@ contract OrchestratorTest is BaseTest { u.paymentMaxAmount = 0.5 ether; u.combinedGas = 10000000; u.signature = _sig(ds[i], u); - encodedIntents[i] = abi.encode(u); + encodedIntents[i] = encodeIntent(u); } bytes4[] memory errs = oc.execute(encodedIntents); @@ -247,7 +246,7 @@ contract OrchestratorTest is BaseTest { calls[i] = _transferCall(address(paymentToken), address(0xabcd), 0.5 ether); } - Orchestrator.Intent memory u; + Intent memory u; u.eoa = d.eoa; u.nonce = 0; u.executionData = abi.encode(calls); @@ -260,7 +259,7 @@ contract OrchestratorTest is BaseTest { (uint256 gExecute,,) = _estimateGas(u); - assertEq(oc.execute{gas: gExecute}(abi.encode(u)), 0); + assertEq(oc.execute{gas: gExecute}(encodeIntent(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); @@ -271,7 +270,7 @@ contract OrchestratorTest is BaseTest { paymentToken.mint(d.eoa, 500 ether); - Orchestrator.Intent memory u; + Intent memory u; u.eoa = d.eoa; u.nonce = 0; u.executionData = _transferExecutionData(address(paymentToken), address(0xabcd), 1 ether); @@ -313,7 +312,7 @@ contract OrchestratorTest is BaseTest { paymentToken.mint(d.eoa, 10 ether); // Create base intent with common fields - Orchestrator.Intent memory baseIntent; + Intent memory baseIntent; baseIntent.eoa = d.eoa; baseIntent.paymentToken = address(paymentToken); baseIntent.paymentAmount = 0.1 ether; @@ -322,50 +321,49 @@ contract OrchestratorTest is BaseTest { // Test case 1: Intent with no expiry (expiry = 0) should always be valid { - Orchestrator.Intent memory u = baseIntent; + 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(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(u)), 0); assertEq(paymentToken.balanceOf(address(0xabcd)), 1 ether); } // Test case 2: Intent with future expiry should be valid { - Orchestrator.Intent memory u = baseIntent; + 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(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(u)), 0); assertEq(paymentToken.balanceOf(address(0xbcde)), 1 ether); } // Test case 3: Intent with past expiry should fail { - Orchestrator.Intent memory u = baseIntent; + 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(abi.encode(u)); + bytes4 result = oc.execute(encodeIntent(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 - Orchestrator.Intent memory batchBase; + Intent memory batchBase; batchBase.eoa = d.eoa; batchBase.paymentToken = address(paymentToken); batchBase.paymentAmount = 0.05 ether; @@ -373,31 +371,31 @@ contract OrchestratorTest is BaseTest { batchBase.combinedGas = 10000000; // Valid intent with nonce 2 - Orchestrator.Intent memory u1 = batchBase; + 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] = abi.encode(u1); + encodedIntents[0] = encodeIntent(u1); // Expired intent with nonce 3 - Orchestrator.Intent memory u2 = batchBase; + 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] = abi.encode(u2); + encodedIntents[1] = encodeIntent(u2); // Another valid intent with nonce 3 (since nonce 3 wasn't consumed due to expiry) - Orchestrator.Intent memory u3 = batchBase; + 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] = abi.encode(u3); + encodedIntents[2] = encodeIntent(u3); bytes4[] memory errors = oc.execute(encodedIntents); assertEq(errors.length, 3); @@ -424,7 +422,7 @@ contract OrchestratorTest is BaseTest { paymentToken.mint(ds[i].eoa, 1 ether); vm.deal(ds[i].eoa, 1 ether); - Orchestrator.Intent memory u; + Intent memory u; u.eoa = ds[i].eoa; u.nonce = 0; u.executionData = @@ -437,7 +435,7 @@ contract OrchestratorTest is BaseTest { u.combinedGas = 10000000; u.signature = _sig(ds[i], u); - encodeIntents[i] = abi.encode(u); + encodeIntents[i] = encodeIntent(u); } bytes memory data = abi.encodeWithSignature("execute(bytes[])", encodeIntents); @@ -463,7 +461,7 @@ contract OrchestratorTest is BaseTest { vm.prank(d.eoa); d.d.authorize(k.k); - Orchestrator.Intent memory u; + Intent memory u; u.eoa = d.eoa; u.executionData = _executionData(address(0), 0, bytes("")); u.nonce = 0x2; @@ -473,12 +471,12 @@ contract OrchestratorTest is BaseTest { u.combinedGas = 20000000; u.signature = _sig(k, u); - oc.execute(abi.encode(u)); + oc.execute(encodeIntent(u)); } function testInvalidateNonce(uint96 seqKey, uint64 seq, uint64 seq2) public { uint256 nonce = (uint256(seqKey) << 64) | uint256(seq); - Orchestrator.Intent memory u; + Intent memory u; DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); u.eoa = d.eoa; @@ -518,9 +516,9 @@ contract OrchestratorTest is BaseTest { u.signature = _sig(d, u); if (seq > type(uint64).max - 2) { - assertEq(oc.execute(abi.encode(u)), bytes4(keccak256("InvalidNonce()"))); + assertEq(oc.execute(encodeIntent(u)), bytes4(keccak256("InvalidNonce()"))); } else { - assertEq(oc.execute(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(u)), 0); } } @@ -539,7 +537,7 @@ contract OrchestratorTest is BaseTest { p.paymentPerGas, p.combinedGasIncrement, p.combinedGasVerificationOffset, - abi.encode(p.u) + encodeIntent(p.u) ); vm.revertToStateAndDelete(snapshot); @@ -593,7 +591,7 @@ contract OrchestratorTest is BaseTest { preCall.signature = _eoaSig(payer.privateKey, oc.computeDigest(preCall)); // Create an Intent with the pre-call and fee payer - ICommon.Intent memory u; + Intent memory u; u.eoa = eoa; u.payer = address(payer.d); u.nonce = 0; @@ -615,7 +613,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 = oc.computeDigest(u); + bytes32 digest = computeDigest(u); u.signature = _eoaSig(ephemeralPK, digest); u.paymentSignature = _eoaSig(payer.privateKey, digest); @@ -623,7 +621,7 @@ contract OrchestratorTest is BaseTest { uint256 payerBalanceBefore = paymentToken.balanceOf(address(payer.d)); assertFalse(MockAccount(payable(payer.eoa)).keyCount() > 0); - assertEq(oc.execute(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(u)), 0); // Verify the session key was authorized in the payer's account assertTrue(MockAccount(payable(payer.eoa)).keyCount() > 0); @@ -656,7 +654,7 @@ contract OrchestratorTest is BaseTest { function testInitAndTransferInOneShot(bytes32) public { _TestAuthorizeWithPreCallsAndTransferTemps memory t; - Orchestrator.Intent memory u; + Intent memory u; uint256 ephemeralPK = _randomPrivateKey(); t.eoa = vm.addr(ephemeralPK); @@ -745,14 +743,14 @@ contract OrchestratorTest is BaseTest { // Test without gas estimation. u.combinedGas = 10000000; u.signature = _sig(kSession, u); - assertEq(oc.execute(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(u)), 0); assertEq(_balanceOf(tokenToTransfer, address(0xabcd)), 0.5 ether); } function testAuthorizeWithPreCallsAndTransfer(bytes32) public { _TestAuthorizeWithPreCallsAndTransferTemps memory t; - Orchestrator.Intent memory u; + Intent memory u; Orchestrator.SignedCall memory pInit; if (_randomChance(2)) { @@ -902,21 +900,21 @@ contract OrchestratorTest is BaseTest { if (t.testInvalidPreCallEOA) { u.combinedGas = 10000000; u.signature = _sig(kSession, u); - assertEq(oc.execute(abi.encode(u)), bytes4(keccak256("InvalidPreCallEOA()"))); + assertEq(oc.execute(encodeIntent(u)), bytes4(keccak256("InvalidPreCallEOA()"))); return; // Skip the rest. } if (t.testPreCallVerificationError) { u.combinedGas = 10000000; u.signature = _sig(kSession, u); - assertEq(oc.execute(abi.encode(u)), bytes4(keccak256("PreCallVerificationError()"))); + assertEq(oc.execute(encodeIntent(u)), bytes4(keccak256("PreCallVerificationError()"))); return; // Skip the rest. } if (t.testPreCallError) { u.combinedGas = 10000000; u.signature = _sig(kSession, u); - assertEq(oc.execute(abi.encode(u)), bytes4(keccak256("PreCallError()"))); + assertEq(oc.execute(encodeIntent(u)), bytes4(keccak256("PreCallError()"))); return; // Skip the rest. } @@ -924,20 +922,21 @@ contract OrchestratorTest is BaseTest { if (_randomChance(16)) { u.combinedGas += 10_000; // Fill with some junk signature, but with the session `keyHash`. - u.signature = - abi.encodePacked(keccak256("a"), keccak256("b"), kSession.keyHash, uint8(0)); + u.signature = abi.encodePacked( + keccak256("a"), keccak256("b"), kSession.keyHash, uint8(0), uint8(0) + ); (t.gExecute, t.gCombined, t.gUsed) = _estimateGas(u); u.combinedGas = t.gCombined; u.signature = _sig(kSession, u); - assertEq(oc.execute{gas: t.gExecute}(abi.encode(u)), 0); + assertEq(oc.execute{gas: t.gExecute}(encodeIntent(u)), 0); } else { // Otherwise, test without gas estimation. u.combinedGas = 10000000; u.signature = _sig(kSession, u); - assertEq(oc.execute(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(u)), 0); } assertEq(paymentToken.balanceOf(address(0xabcd)), 0.5 ether); @@ -967,7 +966,7 @@ contract OrchestratorTest is BaseTest { // 1 ether in the EOA for execution. vm.deal(address(d.d), 1 ether); - Orchestrator.Intent memory u; + Intent memory u; u.eoa = d.eoa; u.payer = address(payer.d); @@ -980,7 +979,7 @@ contract OrchestratorTest is BaseTest { u.executionData = _transferExecutionData(address(0), address(0xabcd), 1 ether); u.paymentRecipient = address(0x12345); - bytes32 digest = oc.computeDigest(u); + bytes32 digest = computeDigest(u); uint256 snapshot = vm.snapshotState(); // To allow paymasters to be used in simulation mode. @@ -989,12 +988,12 @@ contract OrchestratorTest is BaseTest { vm.revertToStateAndDelete(snapshot); u.combinedGas = gCombined; - digest = oc.computeDigest(u); + digest = 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}(abi.encode(u)), 0); + assertEq(oc.execute{gas: gExecute}(encodeIntent(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); @@ -1033,7 +1032,7 @@ contract OrchestratorTest is BaseTest { t.token = _randomChance(2) ? address(0) : address(paymentToken); t.isWithState = _randomChance(2); - Orchestrator.Intent memory u; + Intent memory u; t.d = _randomEIP7702DelegatedEOA(); vm.deal(t.d.eoa, type(uint192).max); @@ -1051,7 +1050,7 @@ contract OrchestratorTest is BaseTest { if (t.isWithState) { t.withState.increaseFunds(u.paymentToken, u.eoa, t.funds); } else { - bytes32 digest = oc.computeDigest(u); + bytes32 digest = computeDigest(u); digest = t.withSignature.computeSignatureDigest(digest); u.paymentSignature = _sig(t.withSignatureEOA, digest); t.corruptSignature = _randomChance(2); @@ -1069,7 +1068,7 @@ contract OrchestratorTest is BaseTest { t.withSignature.setApprovedOrchestrator(address(oc), false); } if ((t.unapprovedOrchestrator && u.paymentAmount != 0)) { - assertEq(oc.execute(abi.encode(u)), bytes4(keccak256("Unauthorized()"))); + assertEq(oc.execute(encodeIntent(u)), bytes4(keccak256("Unauthorized()"))); if (u.paymentAmount != 0) { assertEq(t.d.d.getNonce(0), u.nonce); @@ -1080,7 +1079,7 @@ contract OrchestratorTest is BaseTest { } else if (t.isWithState && u.paymentAmount > t.funds && u.paymentAmount != 0) { // Arithmetic underflow error assertEq( - oc.execute(abi.encode(u)), + oc.execute(encodeIntent(u)), 0x4e487b7100000000000000000000000000000000000000000000000000000000 ); @@ -1098,7 +1097,7 @@ contract OrchestratorTest is BaseTest { } } else if ((!t.isWithState && t.corruptSignature && u.paymentAmount != 0)) { // Pre payment will not happen - assertEq(oc.execute(abi.encode(u)), bytes4(keccak256("InvalidSignature()"))); + assertEq(oc.execute(encodeIntent(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); @@ -1108,7 +1107,7 @@ contract OrchestratorTest is BaseTest { assertEq(_balanceOf(t.token, u.payer), t.balanceBefore); assertEq(_balanceOf(address(0), address(0xabcd)), 0); } else { - assertEq(oc.execute(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(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); @@ -1127,7 +1126,7 @@ contract OrchestratorTest is BaseTest { t.testImplementationCheck = _randomChance(2); t.requireWrongImplementation = _randomChance(2); - Orchestrator.Intent memory u; + Intent memory u; vm.deal(t.d.eoa, type(uint192).max); u.eoa = t.d.eoa; @@ -1147,12 +1146,12 @@ contract OrchestratorTest is BaseTest { if (t.testImplementationCheck && t.requireWrongImplementation) { assertEq( - oc.execute(abi.encode(u)), bytes4(keccak256("UnsupportedAccountImplementation()")) + oc.execute(encodeIntent(u)), bytes4(keccak256("UnsupportedAccountImplementation()")) ); assertEq(t.d.d.getNonce(0), u.nonce); assertEq(_balanceOf(address(0), address(0xabcd)), 0); } else { - assertEq(oc.execute(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(u)), 0); assertEq(t.d.d.getNonce(0), u.nonce + 1); assertEq(_balanceOf(address(0), address(0xabcd)), 1 ether); } @@ -1239,7 +1238,7 @@ contract OrchestratorTest is BaseTest { vm.expectRevert(bytes4(keccak256("InvalidKeyHash()"))); t.d.d.execute(_ERC7821_BATCH_EXECUTION_MODE, abi.encode(calls)); - Orchestrator.Intent memory u; + Intent memory u; u.eoa = t.d.eoa; u.nonce = t.d.d.getNonce(0); u.executionData = abi.encode(calls); @@ -1249,14 +1248,13 @@ contract OrchestratorTest is BaseTest { u.signature = _sig(t.multiSigKey, u); // Test unwrapAndValidateSignature - bytes32 digest = oc.computeDigest(u); + bytes32 digest = 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}(abi.encode(u)), 0); + assertEq(oc.execute{gas: gExecute}(encodeIntent(u)), 0); (uint256 _threshold, bytes32[] memory o) = t.multiSigSigner.getConfig(address(t.d.d), _hash(t.multiSigKey.k)); @@ -1280,14 +1278,13 @@ 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}(abi.encode(u)), 0); + assertEq(oc.execute{gas: gExecute}(encodeIntent(u)), 0); (_threshold, o) = t.multiSigSigner.getConfig(address(t.d.d), _hash(t.multiSigKey.k)); assertEq(_threshold, newThreshold); @@ -1296,7 +1293,6 @@ contract OrchestratorTest is BaseTest { t.multiSigKey.threshold = newThreshold; } } - // Test removeOwner { uint256 removeIndex = _bound(_random(), 0, o.length - 1); @@ -1315,7 +1311,7 @@ contract OrchestratorTest is BaseTest { u.combinedGas = gCombined; u.signature = _sig(t.multiSigKey, u); - assertEq(oc.execute{gas: gExecute}(abi.encode(u)), 0); + assertEq(oc.execute{gas: gExecute}(encodeIntent(u)), 0); (_threshold, o) = t.multiSigSigner.getConfig(address(t.d.d), _hash(t.multiSigKey.k)); assertEq(o.length, t.multiSigKey.owners.length); @@ -1336,9 +1332,9 @@ contract OrchestratorTest is BaseTest { DelegatedEOA d; PassKey k; // Intent data - ICommon.Intent baseIntent; - ICommon.Intent arbIntent; - ICommon.Intent outputIntent; + Intent baseIntent; + Intent arbIntent; + Intent outputIntent; // Merkle data bytes32[] leafs; bytes32 root; @@ -1426,12 +1422,11 @@ 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(0); + t.outputIntent.nonce = t.d.d.getNonce(0x6D76 << 176); 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); @@ -1450,13 +1445,12 @@ contract OrchestratorTest is BaseTest { // Compute the output intent digest to use as settlementId vm.chainId(1); // Mainnet - t.settlementId = oc.computeDigest(t.outputIntent); + t.settlementId = computeDigest(t.outputIntent); // Base Intent with escrow execution data t.baseIntent.eoa = t.d.eoa; - t.baseIntent.nonce = t.d.d.getNonce(0); + t.baseIntent.nonce = t.d.d.getNonce(0x6D76 << 176); t.baseIntent.combinedGas = 1000000; - t.baseIntent.isMultichain = true; // Create Base escrow execution data { @@ -1496,9 +1490,8 @@ contract OrchestratorTest is BaseTest { // Arbitrum Intent with escrow execution data t.arbIntent.eoa = t.d.eoa; - t.arbIntent.nonce = t.d.d.getNonce(0); + t.arbIntent.nonce = t.d.d.getNonce(0x6D76 << 176); t.arbIntent.combinedGas = 1000000; - t.arbIntent.isMultichain = true; // Create Arbitrum escrow execution data { IEscrow.Escrow[] memory escrows = new IEscrow.Escrow[](1); @@ -1545,13 +1538,14 @@ contract OrchestratorTest is BaseTest { // User has 600 USDC on base t.usdcBase.mint(t.d.eoa, 600); - t.encodedIntents[0] = abi.encode(t.baseIntent); + t.encodedIntents[0] = encodeIntent(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); @@ -1562,7 +1556,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] = abi.encode(t.baseIntent); + t.encodedIntents[0] = encodeIntent(t.baseIntent); vm.prank(t.gasWallet); t.errs = oc.execute(t.encodedIntents); assertEq( @@ -1574,15 +1568,15 @@ contract OrchestratorTest is BaseTest { bytes32[] memory wrongLeafs = new bytes32[](3); // Some random leaf - wrongLeafs[0] = oc.computeDigest(t.arbIntent); - wrongLeafs[1] = oc.computeDigest(t.arbIntent); - wrongLeafs[2] = oc.computeDigest(t.outputIntent); + wrongLeafs[0] = computeDigest(t.arbIntent); + wrongLeafs[1] = computeDigest(t.arbIntent); + wrongLeafs[2] = 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] = abi.encode(t.arbIntent); + t.encodedIntents[0] = encodeIntent(t.arbIntent); vm.prank(t.gasWallet); t.errs = oc.execute(t.encodedIntents); assertEq( @@ -1595,7 +1589,7 @@ contract OrchestratorTest is BaseTest { } // User escrows funds on Arb - t.encodedIntents[0] = abi.encode(t.arbIntent); + t.encodedIntents[0] = encodeIntent(t.arbIntent); vm.expectEmit(true, false, false, false, address(t.escrowArb)); emit Escrow.EscrowCreated(t.escrowIdArb); vm.prank(t.gasWallet); @@ -1621,7 +1615,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] = abi.encode(t.outputIntent); + t.encodedIntents[0] = encodeIntent(t.outputIntent); vm.prank(t.gasWallet); t.errs = oc.execute(t.encodedIntents); assertEq(uint256(bytes32(t.errs[0])), 0); @@ -1656,7 +1650,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] = abi.encode(t.baseIntent); + t.encodedIntents[0] = encodeIntent(t.baseIntent); vm.expectEmit(true, false, false, false, address(t.escrowBase)); emit Escrow.EscrowCreated(t.escrowIdBase); vm.prank(t.gasWallet); @@ -1685,7 +1679,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] = abi.encode(t.arbIntent); + t.encodedIntents[0] = encodeIntent(t.arbIntent); vm.expectEmit(true, false, false, false, address(t.escrowArb)); emit Escrow.EscrowCreated(t.escrowIdArb); vm.prank(t.gasWallet); @@ -1732,7 +1726,7 @@ contract OrchestratorTest is BaseTest { t.outputIntent.funderSignature = _eoaSig(wrongPrivateKey, t.leafs[2]); - t.encodedIntents[0] = abi.encode(t.outputIntent); + t.encodedIntents[0] = encodeIntent(t.outputIntent); vm.prank(t.gasWallet); t.errs = oc.execute(t.encodedIntents); @@ -1755,11 +1749,11 @@ contract OrchestratorTest is BaseTest { function _computeMerkleData(_TestMultiChainIntentTemps memory t) internal { t.leafs = new bytes32[](3); vm.chainId(8453); - t.leafs[0] = oc.computeDigest(t.baseIntent); + t.leafs[0] = computeDigest(t.baseIntent); vm.chainId(42161); - t.leafs[1] = oc.computeDigest(t.arbIntent); + t.leafs[1] = computeDigest(t.arbIntent); vm.chainId(1); - t.leafs[2] = oc.computeDigest(t.outputIntent); + t.leafs[2] = computeDigest(t.outputIntent); t.root = merkleHelper.getRoot(t.leafs); @@ -1768,8 +1762,23 @@ contract OrchestratorTest is BaseTest { t.outputIntent.funderSignature = _eoaSig(t.funderPrivateKey, t.leafs[2]); - 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); + 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) + ); } } diff --git a/test/SimulateExecute.t.sol b/test/SimulateExecute.t.sol index 099d6acc..8e2d52b5 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) ); - Orchestrator.Intent memory i; + 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, abi.encode(i)); + simulator.simulateV1Logs(address(oc), 0, 1, 11_000, 10_000, encodeIntent(i)); vm.expectRevert(bytes4(keccak256("StateOverrideError()"))); - oc.simulateExecute(true, type(uint256).max, abi.encode(i)); + oc.simulateExecute(encodeIntent(i, true, type(uint256).max)); vm.expectPartialRevert(bytes4(keccak256("SimulationPassed(uint256)"))); - oc.simulateExecute(false, type(uint256).max, abi.encode(i)); + oc.simulateExecute(encodeIntent(i, false, type(uint256).max)); uint256 snapshot = vm.snapshotState(); vm.deal(_ORIGIN_ADDRESS, type(uint192).max); (t.gUsed, t.gCombined) = - simulator.simulateV1Logs(address(oc), 2, 1e11, 11_000, 0, abi.encode(i)); + simulator.simulateV1Logs(address(oc), 2, 1e11, 11_000, 0, encodeIntent(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}(abi.encode(i)); + oc.execute{gas: t.gExecute}(encodeIntent(i)); t.gExecute = Math.mulDiv(t.gCombined + 110_000, 64, 63); - assertEq(oc.execute{gas: t.gExecute}(abi.encode(i)), 0); + assertEq(oc.execute{gas: t.gExecute}(encodeIntent(i)), 0); } function testSimulateExecuteNoRevertUnderfundedReverts() public { @@ -127,7 +127,7 @@ contract SimulateExecuteTest is BaseTest { abi.encodeWithSignature("burnGas(uint256,uint256)", t.gasToBurn, t.randomness) ); - Orchestrator.Intent memory i; + 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, abi.encode(i)); + simulator.simulateV1Logs(address(oc), 0, 1, 11_000, 0, encodeIntent(i)); deal(i.paymentToken, address(i.eoa), 0x112233112233112233112233); vm.expectRevert(bytes4(keccak256("PaymentError()"))); - simulator.simulateCombinedGas(address(oc), 0, 1, 11_000, abi.encode(i)); + simulator.simulateCombinedGas(address(oc), 0, 1, 11_000, encodeIntent(i)); } function testSimulateExecuteNoRevert() public { @@ -173,7 +173,7 @@ contract SimulateExecuteTest is BaseTest { abi.encodeWithSignature("burnGas(uint256,uint256)", t.gasToBurn, t.randomness) ); - Orchestrator.Intent memory i; + 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, abi.encode(i)); + simulator.simulateV1Logs(address(oc), 2, 1e11, 11_000, 0, encodeIntent(i)); vm.revertToStateAndDelete(snapshot); @@ -205,7 +205,7 @@ contract SimulateExecuteTest is BaseTest { i.signature = _sig(d, i); - assertEq(oc.execute{gas: t.gExecute}(abi.encode(i)), 0); + assertEq(oc.execute{gas: t.gExecute}(encodeIntent(i)), 0); assertEq(gasBurner.randomness(), t.randomness); } @@ -229,7 +229,7 @@ contract SimulateExecuteTest is BaseTest { abi.encodeWithSignature("burnGas(uint256,uint256)", t.gasToBurn, t.randomness) ); - Orchestrator.Intent memory i; + 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, abi.encode(i)); + simulator.simulateV1Logs(address(oc), 2, 1e11, 10_800, 0, encodeIntent(i)); vm.revertToStateAndDelete(snapshot); @@ -261,7 +261,7 @@ contract SimulateExecuteTest is BaseTest { i.signature = _sig(d, i); - assertEq(oc.execute{gas: t.gExecute}(abi.encode(i)), 0); + assertEq(oc.execute{gas: t.gExecute}(encodeIntent(i)), 0); assertEq(gasBurner.randomness(), t.randomness); } @@ -289,7 +289,7 @@ contract SimulateExecuteTest is BaseTest { abi.encodeWithSignature("burnGas(uint256,uint256)", t.gasToBurn, t.randomness) ); - Orchestrator.Intent memory i; + Intent memory i; i.eoa = d.eoa; i.nonce = 0; i.executionData = t.executionData; @@ -305,13 +305,14 @@ contract SimulateExecuteTest is BaseTest { // it needs to add the variance for non-precompile P256 verification. // We need the `keyHash` in the signature so that the simulation is able // to hit all the gas for the GuardedExecutor stuff for the `keyHash`. - i.signature = abi.encodePacked(keccak256("a"), keccak256("b"), k.keyHash, uint8(0)); + i.signature = + abi.encodePacked(keccak256("a"), keccak256("b"), k.keyHash, uint8(0), uint8(0)); uint256 snapshot = vm.snapshotState(); vm.deal(_ORIGIN_ADDRESS, type(uint192).max); (t.gUsed, t.gCombined) = - simulator.simulateV1Logs(address(oc), 2, 1e11, 12_000, 10_000, abi.encode(i)); + simulator.simulateV1Logs(address(oc), 2, 1e11, 12_000, 10_000, encodeIntent(i)); vm.revertToStateAndDelete(snapshot); @@ -320,7 +321,7 @@ contract SimulateExecuteTest is BaseTest { i.signature = _sig(k, i); - assertEq(oc.execute{gas: t.gExecute}(abi.encode(i)), 0); + assertEq(oc.execute{gas: t.gExecute}(encodeIntent(i)), 0); assertEq(gasBurner.randomness(), t.randomness); } diff --git a/test/SubAccounts.t.sol b/test/SubAccounts.t.sol new file mode 100644 index 00000000..68417582 --- /dev/null +++ b/test/SubAccounts.t.sol @@ -0,0 +1,270 @@ +// 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 deleted file mode 100644 index d0d28d09..00000000 --- a/test/UpgradeTests.t.sol +++ /dev/null @@ -1,566 +0,0 @@ -// 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 1c6447ae..969c5f86 100644 --- a/test/utils/Brutalizer.sol +++ b/test/utils/Brutalizer.sol @@ -825,7 +825,9 @@ 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 2665aedc..d3d62e0b 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 singature. + /// @notice The length of the ERC-20 config without signature. uint8 constant ERC20_PAYMASTER_DATA_LENGTH = 117; - /// @notice The length of the verfiying config without singature. + /// @notice The length of the verifying config without signature. 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 deleted file mode 100644 index d9c3d3e1..00000000 --- a/test/utils/interfaces/ISafe.sol +++ /dev/null @@ -1,57 +0,0 @@ -// 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 e21a2360..a0c3226e 100644 --- a/test/utils/mocks/MockOrchestrator.sol +++ b/test/utils/mocks/MockOrchestrator.sol @@ -15,8 +15,13 @@ contract MockOrchestrator is Orchestrator, Brutalizer { return _computeDigest(preCall); } - function computeDigest(Intent calldata intent) public view returns (bytes32) { - return _computeDigest(intent); + // 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 simulateFailed(bytes calldata encodedIntent) public payable virtual { diff --git a/test/utils/mocks/MockPayerWithSignature.sol b/test/utils/mocks/MockPayerWithSignature.sol index 1de4c6d5..60678b52 100644 --- a/test/utils/mocks/MockPayerWithSignature.sol +++ b/test/utils/mocks/MockPayerWithSignature.sol @@ -51,38 +51,44 @@ contract MockPayerWithSignature is Ownable { } /// @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, - /// 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. + /// @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 function pay( uint256 paymentAmount, bytes32 keyHash, - bytes32 digest, - bytes calldata encodedIntent + bytes32 intentDigest, + address eoa, + address payer, + address paymentToken, + address paymentRecipient, + bytes calldata paymentSignature ) public virtual { if (!isApprovedOrchestrator[msg.sender]) revert Unauthorized(); // Check and set nonce to prevent replay attacks - if (paymasterNonces[digest]) { + if (paymasterNonces[intentDigest]) { revert PaymasterNonceError(); } - paymasterNonces[digest] = true; + paymasterNonces[intentDigest] = true; - ICommon.Intent memory u = abi.decode(encodedIntent, (ICommon.Intent)); + bytes32 signatureDigest = computeSignatureDigest(intentDigest); - bytes32 signatureDigest = computeSignatureDigest(digest); - - if (ECDSA.recover(signatureDigest, u.paymentSignature) != signer) { + if (ECDSA.recover(signatureDigest, paymentSignature) != signer) { revert InvalidSignature(); } - TokenTransferLib.safeTransfer(u.paymentToken, u.paymentRecipient, paymentAmount); + TokenTransferLib.safeTransfer(paymentToken, paymentRecipient, paymentAmount); + + emit Compensated(paymentToken, paymentRecipient, paymentAmount, eoa, keyHash); - emit Compensated(u.paymentToken, u.paymentRecipient, paymentAmount, u.eoa, keyHash); + // Unused parameters + payer; } function computeSignatureDigest(bytes32 intentDigest) public view returns (bytes32) { diff --git a/test/utils/mocks/MockPayerWithSignatureOptimized.sol b/test/utils/mocks/MockPayerWithSignatureOptimized.sol deleted file mode 100644 index 1d7701bf..00000000 --- a/test/utils/mocks/MockPayerWithSignatureOptimized.sol +++ /dev/null @@ -1,84 +0,0 @@ -// 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 365db440..e5e4c35d 100644 --- a/test/utils/mocks/MockPayerWithState.sol +++ b/test/utils/mocks/MockPayerWithState.sol @@ -54,35 +54,42 @@ contract MockPayerWithState is Ownable { } /// @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, - /// 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. + /// @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 function pay( uint256 paymentAmount, bytes32 keyHash, - bytes32 digest, - bytes calldata encodedIntent + bytes32 intentDigest, + address eoa, + address payer, + address paymentToken, + address paymentRecipient, + bytes calldata paymentSignature ) public virtual { if (!isApprovedOrchestrator[msg.sender]) revert Unauthorized(); // Check and set nonce to prevent replay attacks - if (paymasterNonces[digest]) { + if (paymasterNonces[intentDigest]) { revert PaymasterNonceError(); } - paymasterNonces[digest] = true; - - ICommon.Intent memory u = abi.decode(encodedIntent, (ICommon.Intent)); + paymasterNonces[intentDigest] = true; // We shall rely on arithmetic underflow error to revert if there's insufficient funds. - funds[u.paymentToken][u.eoa] -= paymentAmount; - TokenTransferLib.safeTransfer(u.paymentToken, u.paymentRecipient, paymentAmount); + funds[paymentToken][eoa] -= paymentAmount; + TokenTransferLib.safeTransfer(paymentToken, paymentRecipient, paymentAmount); // Emit the event for debugging. - emit Compensated(u.paymentToken, u.paymentRecipient, paymentAmount, u.eoa, keyHash); + emit Compensated(paymentToken, paymentRecipient, paymentAmount, eoa, keyHash); + + // Unused parameters + payer; + paymentSignature; } receive() external payable {} diff --git a/utils/JsonBindings.sol b/utils/JsonBindings.sol new file mode 100644 index 00000000..1a76c5b2 --- /dev/null +++ b/utils/JsonBindings.sol @@ -0,0 +1,767 @@ +// 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[])); + } +}