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..1d880964 100644 --- a/.env.example +++ b/.env.example @@ -2,28 +2,41 @@ # Copy this file to .env and fill in with actual values # ============================================ -# UPGRADE TESTS +# PRIVATE KEY (Required for deployment) # ============================================ -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 +# NEVER commit your actual private key! +PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 # ============================================ -# DEPLOYMENT SCRIPTS +# RPC ENDPOINTS (Required) # ============================================ -PRIVATE_KEY= - # Format: RPC_{chainId} # Mainnet chains RPC_1=https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY # Ethereum +RPC_42161=https://arb-mainnet.g.alchemy.com/v2/YOUR_API_KEY # Arbitrum +RPC_8453=https://base-mainnet.g.alchemy.com/v2/YOUR_API_KEY # Base # Testnet chains 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" - +# Local development +RPC_28405=https://porto-dev-paros.rpc.ithaca.xyz # Porto Devnet +RPC_28406=https://porto-dev-tinos.rpc.ithaca.xyz # Porto Devnet +RPC_28407=https://porto-dev-leros.rpc.ithaca.xyz # Porto Devnet +# ============================================ +# VERIFICATION API KEYS (Optional) +# ============================================ +# Format: VERIFICATION_KEY_{chainId} +# Get API keys from respective block explorers +VERIFICATION_KEY_1=YOUR_ETHERSCAN_API_KEY # https://etherscan.io +VERIFICATION_KEY_42161=YOUR_ARBISCAN_API_KEY # https://arbiscan.io +VERIFICATION_KEY_8453=YOUR_BASESCAN_API_KEY # https://basescan.org +# ============================================ +# CONFIGURATION +# ============================================ +# All deployment configuration is in deploy/deploy-config.json +# See deploy/README.md for detailed instructions \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..861f7131 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. macOS] + - Browser: [e.g. Chrome, Safari] + - Version: [e.g. 120] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser: [e.g. Safari, Chrome] + - Version: [e.g. 17] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/custom.md b/.github/ISSUE_TEMPLATE/custom.md new file mode 100644 index 00000000..5ec6ce41 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/custom.md @@ -0,0 +1,10 @@ +--- +name: Custom issue template +about: Describe this issue template's purpose here. +title: '' +labels: '' +assignees: '' + +--- + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..af184fda --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. E.g., I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. 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/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 00000000..428e29b8 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +clear && forge fmt && forge snapshot --isolate --match-contract Benchmark --via-ir && git add snapshots 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..1ba0d660 --- /dev/null +++ b/.idea/account.iml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/caches/deviceStreaming.xml b/.idea/caches/deviceStreaming.xml new file mode 100644 index 00000000..cc4b430a --- /dev/null +++ b/.idea/caches/deviceStreaming.xml @@ -0,0 +1,1698 @@ + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 00000000..4bec4ea8 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,117 @@ + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 00000000..a55e7a17 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/copilot.data.migration.ask2agent.xml b/.idea/copilot.data.migration.ask2agent.xml new file mode 100644 index 00000000..1f2ea11e --- /dev/null +++ b/.idea/copilot.data.migration.ask2agent.xml @@ -0,0 +1,6 @@ + + + + + \ 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/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..1995e14d --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.mergify.yml b/.mergify.yml new file mode 100644 index 00000000..1502935a --- /dev/null +++ b/.mergify.yml @@ -0,0 +1,8 @@ +queue_rules: + - name: account +merge_queue: + skip_intermediate_results: true + mode: parallel +scopes: + source: + files: {} diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fd21859..f8bdceaf 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 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 deleted file mode 100755 index 8e459b82..00000000 --- a/deploy/execute_config.sh +++ /dev/null @@ -1,486 +0,0 @@ -#!/bin/bash - -# Comprehensive deployment testing script for multiple chains -# This script deploys contracts, configures LayerZero, and funds signers -# -# Usage: bash deploy/test_deployment.sh [chain_id1] [chain_id2] ... -# -# Examples: -# bash deploy/test_deployment.sh # Deploy to all chains in config.toml -# bash deploy/test_deployment.sh 84532 # Deploy only to Base Sepolia -# bash deploy/test_deployment.sh 84532 11155420 # Deploy to Base Sepolia and Optimism Sepolia -# bash deploy/test_deployment.sh 11155111 84532 11155420 # Deploy to all three chains - -set -e # Exit on any error - -# Color codes for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -# Function to print colored output -log_info() { - echo -e "${GREEN}[INFO]${NC} $1" -} - -log_error() { - echo -e "${RED}[ERROR]${NC} $1" -} - -log_warning() { - echo -e "${YELLOW}[WARNING]${NC} $1" -} - -# Function to check if a command succeeded -check_success() { - if [ $? -eq 0 ]; then - log_info "$1 succeeded" - else - log_error "$1 failed" - exit 1 - fi -} - -# Function to get all chain IDs from config.toml -get_all_chains() { - grep '^\[' deploy/config.toml | grep -v '\.' | sed 's/\[//g' | sed 's/\]//g' -} - -# Parse command-line arguments -REQUESTED_CHAINS=() -if [ $# -gt 0 ]; then - # User provided specific chain IDs to deploy to - REQUESTED_CHAINS=("$@") - log_info "Requested deployment for chain IDs: ${REQUESTED_CHAINS[*]}" -else - # Get all chains from config.toml - REQUESTED_CHAINS=($(get_all_chains)) - log_info "No specific chains requested, will deploy to all chains in config.toml: ${REQUESTED_CHAINS[*]}" -fi - -# Start deployment process -log_info "Starting comprehensive deployment test" -if [ ${#REQUESTED_CHAINS[@]} -gt 0 ]; then - log_info "Deploying to chains: ${REQUESTED_CHAINS[*]}" -fi -echo "========================================================================" - -# Step 1: Load environment variables -log_info "Loading environment variables from .env" -if [ ! -f .env ]; then - log_error ".env file not found!" - exit 1 -fi - -source .env - -# Step 2: Validate required environment variables -log_info "Validating required environment variables" - -if [ -z "$PRIVATE_KEY" ]; then - log_error "PRIVATE_KEY is not set in .env" - exit 1 -fi - -# Check RPC URLs for requested chains -for chain_id in "${REQUESTED_CHAINS[@]}"; do - RPC_VAR="RPC_${chain_id}" - if [ -z "${!RPC_VAR}" ]; then - log_error "$RPC_VAR is not set in .env for chain $chain_id" - log_info "Please set $RPC_VAR in your .env file" - exit 1 - fi -done - -if [ -z "$GAS_SIGNER_MNEMONIC" ]; then - log_warning "GAS_SIGNER_MNEMONIC is not set - FundSigners may fail" -fi - -log_info "All required environment variables are set" -echo "" - -# Step 3: Generate random salt for new deployment addresses -log_info "Generating random salt for new contract addresses" - -# Generate a random 32-byte hex string for salt -RANDOM_SALT="0x$(openssl rand -hex 32)" -log_info "Generated salt: $RANDOM_SALT" - -# Update salt in config.toml for all requested chains -for chain_id in "${REQUESTED_CHAINS[@]}"; do - if grep -q "^\[${chain_id}\.bytes32\]" deploy/config.toml; then - # Get chain name if available - CHAIN_NAME=$(sed -n "/^\[${chain_id}\.string\]/,/^\[/p" deploy/config.toml | grep "^name" | cut -d'"' -f2) - if [ -z "$CHAIN_NAME" ]; then - CHAIN_NAME="Chain $chain_id" - fi - - log_info "Updating salt for $CHAIN_NAME ($chain_id) in config.toml" - sed -i.bak "/^\[${chain_id}\.bytes32\]/,/^\[/ s/^salt = .*/salt = \"$RANDOM_SALT\"/" deploy/config.toml - else - log_warning "No bytes32 section found for chain $chain_id, skipping salt update" - fi -done - -# Clean up backup files -rm -f deploy/config.toml.bak - -log_info "Salt updated in config.toml for all requested chains" -log_warning "⚠️ IMPORTANT: Save this salt value if you need to deploy to the same addresses on other chains: $RANDOM_SALT" -echo "" - -# Step 4: Deploy contracts to Base Sepolia and Optimism Sepolia -echo "========================================================================" -# Build chain array string for forge script -CHAIN_ARRAY="[" -for i in "${!REQUESTED_CHAINS[@]}"; do - if [ $i -gt 0 ]; then - CHAIN_ARRAY="${CHAIN_ARRAY}," - fi - CHAIN_ARRAY="${CHAIN_ARRAY}${REQUESTED_CHAINS[$i]}" -done -CHAIN_ARRAY="${CHAIN_ARRAY}]" - -log_info "STEP 1: Deploying contracts to chains: $CHAIN_ARRAY" -echo "========================================================================" - -forge script deploy/DeployMain.s.sol:DeployMain \ - --broadcast --multi --slow \ - --sig "run(uint256[])" "$CHAIN_ARRAY" \ - --private-key $PRIVATE_KEY \ - -vvv - -check_success "Contract deployment" -echo "" - -# Step 5: Configure LayerZero for cross-chain communication -echo "========================================================================" -log_info "STEP 2: Configuring LayerZero for cross-chain communication" -echo "========================================================================" - -# Note: Using the same PRIVATE_KEY as requested (instead of L0_SETTLER_OWNER_PK) -forge script deploy/ConfigureLayerZeroSettler.s.sol:ConfigureLayerZeroSettler \ - --broadcast --multi --slow \ - --sig "run(uint256[])" "$CHAIN_ARRAY" \ - --private-key $PRIVATE_KEY \ - -vvv - -check_success "LayerZero configuration" -echo "" - -# Base Sepolia LayerZero verification -if [ ! -z "$LAYERZERO_SETTLER_BASE" ]; then - log_info "Base Sepolia LayerZero configuration:" - - # Get LayerZero endpoint from config - LZ_ENDPOINT_BASE=$(sed -n "/^\[84532\.address\]/,/^\[/p" deploy/config.toml | grep "layerzero_endpoint" | cut -d'"' -f2) - LZ_EID_BASE=$(sed -n "/^\[84532\.uint\]/,/^\[/p" deploy/config.toml | grep "layerzero_eid" | awk -F' = ' '{print $2}') - LZ_SEND_ULN_BASE=$(sed -n "/^\[84532\.address\]/,/^\[/p" deploy/config.toml | grep "layerzero_send_uln302" | cut -d'"' -f2) - LZ_RECEIVE_ULN_BASE=$(sed -n "/^\[84532\.address\]/,/^\[/p" deploy/config.toml | grep "layerzero_receive_uln302" | cut -d'"' -f2) - - # Check if endpoint is set on LayerZeroSettler - CURRENT_ENDPOINT=$(cast call $LAYERZERO_SETTLER_BASE "endpoint()(address)" --rpc-url $RPC_84532 2>/dev/null || echo "0x0000000000000000000000000000000000000000") - - if [ "$CURRENT_ENDPOINT" == "$LZ_ENDPOINT_BASE" ]; then - log_info " ✓ Endpoint correctly set to $LZ_ENDPOINT_BASE" - else - log_error " ✗ Endpoint mismatch: Expected $LZ_ENDPOINT_BASE, got $CURRENT_ENDPOINT" - fi - - # Check L0SettlerSigner - LZ_SIGNER=$(cast call $LAYERZERO_SETTLER_BASE "l0SettlerSigner()(address)" --rpc-url $RPC_84532 2>/dev/null || echo "0x0000000000000000000000000000000000000000") - log_info " L0SettlerSigner: $LZ_SIGNER" -fi - -# Optimism Sepolia LayerZero verification -if [ ! -z "$LAYERZERO_SETTLER_OP" ]; then - log_info "Optimism Sepolia LayerZero configuration:" - - # Get LayerZero endpoint from config - these are in the address section - LZ_ENDPOINT_OP=$(sed -n "/^\[11155420\.address\]/,/^\[/p" deploy/config.toml | grep "layerzero_endpoint" | cut -d'"' -f2) - LZ_EID_OP=$(sed -n "/^\[11155420\.uint\]/,/^\[/p" deploy/config.toml | grep "layerzero_eid" | awk -F' = ' '{print $2}') - LZ_SEND_ULN_OP=$(sed -n "/^\[11155420\.address\]/,/^\[/p" deploy/config.toml | grep "layerzero_send_uln302" | cut -d'"' -f2) - LZ_RECEIVE_ULN_OP=$(sed -n "/^\[11155420\.address\]/,/^\[/p" deploy/config.toml | grep "layerzero_receive_uln302" | cut -d'"' -f2) - - # Check if endpoint is set on LayerZeroSettler - CURRENT_ENDPOINT=$(cast call $LAYERZERO_SETTLER_OP "endpoint()(address)" --rpc-url $RPC_11155420 2>/dev/null || echo "0x0000000000000000000000000000000000000000") - - if [ "$CURRENT_ENDPOINT" == "$LZ_ENDPOINT_OP" ]; then - log_info " ✓ Endpoint correctly set to $LZ_ENDPOINT_OP" - else - log_error " ✗ Endpoint mismatch: Expected $LZ_ENDPOINT_OP, got $CURRENT_ENDPOINT" - fi - - # Check L0SettlerSigner - LZ_SIGNER=$(cast call $LAYERZERO_SETTLER_OP "l0SettlerSigner()(address)" --rpc-url $RPC_11155420 2>/dev/null || echo "0x0000000000000000000000000000000000000000") - log_info " L0SettlerSigner: $LZ_SIGNER" -fi - -# Verify cross-chain pathway configuration -log_info "Cross-chain pathway verification:" - -# Get EIDs for both chains - they are in the uint sections -LZ_EID_BASE=$(sed -n "/^\[84532\.uint\]/,/^\[/p" deploy/config.toml | grep "layerzero_eid" | awk -F' = ' '{print $2}') -LZ_EID_OP=$(sed -n "/^\[11155420\.uint\]/,/^\[/p" deploy/config.toml | grep "layerzero_eid" | awk -F' = ' '{print $2}') - -# Check Base Sepolia -> Optimism Sepolia pathway -if [ ! -z "$LAYERZERO_SETTLER_BASE" ] && [ ! -z "$LZ_EID_OP" ] && [ ! -z "$LZ_ENDPOINT_BASE" ] && [ ! -z "$LZ_SEND_ULN_BASE" ]; then - log_info " Base Sepolia -> Optimism Sepolia pathway:" - - # Check executor configuration using endpoint.getConfig() - # CONFIG_TYPE_EXECUTOR = 1 - EXECUTOR_CONFIG_BYTES=$(cast call $LZ_ENDPOINT_BASE "getConfig(address,address,uint32,uint32)(bytes)" "$LAYERZERO_SETTLER_BASE" "$LZ_SEND_ULN_BASE" "$LZ_EID_OP" "1" --rpc-url $RPC_84532 2>/dev/null || echo "0x") - - if [ "$EXECUTOR_CONFIG_BYTES" != "0x" ] && [ ! -z "$EXECUTOR_CONFIG_BYTES" ]; then - # The executor config is encoded as (uint32 maxMessageSize, address executor) - # We need to decode the bytes - first 32 bytes is maxMessageSize, next 32 bytes is executor address - # Remove 0x prefix and get the executor address (last 40 hex chars of the second 32-byte word) - EXECUTOR_HEX=$(echo "$EXECUTOR_CONFIG_BYTES" | sed 's/0x//' | tail -c 41) - if [ ! -z "$EXECUTOR_HEX" ]; then - EXECUTOR_ADDR="0x$EXECUTOR_HEX" - - if [ "$(echo $EXECUTOR_ADDR | tr '[:upper:]' '[:lower:]')" == "$(echo $LAYERZERO_SETTLER_BASE | tr '[:upper:]' '[:lower:]')" ]; then - log_info " ✓ Executor correctly set to LayerZeroSettler" - else - log_warning " ⚠ Executor not set to LayerZeroSettler (self-execution model)" - fi - else - log_warning " ⚠ Could not parse executor configuration" - fi - else - log_warning " ⚠ Executor configuration not set" - fi - - # Check ULN configuration using endpoint.getConfig() - # CONFIG_TYPE_ULN = 2 - ULN_CONFIG_BYTES=$(cast call $LZ_ENDPOINT_BASE "getConfig(address,address,uint32,uint32)(bytes)" "$LAYERZERO_SETTLER_BASE" "$LZ_SEND_ULN_BASE" "$LZ_EID_OP" "2" --rpc-url $RPC_84532 2>/dev/null || echo "0x") - - if [ "$ULN_CONFIG_BYTES" != "0x" ] && [ ! -z "$ULN_CONFIG_BYTES" ] && [ ${#ULN_CONFIG_BYTES} -gt 10 ]; then - log_info " ✓ ULN configuration is set" - else - log_warning " ⚠ ULN configuration not set" - fi -fi - -# Check Optimism Sepolia -> Base Sepolia pathway -if [ ! -z "$LAYERZERO_SETTLER_OP" ] && [ ! -z "$LZ_EID_BASE" ] && [ ! -z "$LZ_ENDPOINT_OP" ] && [ ! -z "$LZ_SEND_ULN_OP" ]; then - log_info " Optimism Sepolia -> Base Sepolia pathway:" - - # Check executor configuration using endpoint.getConfig() - # CONFIG_TYPE_EXECUTOR = 1 - EXECUTOR_CONFIG_BYTES=$(cast call $LZ_ENDPOINT_OP "getConfig(address,address,uint32,uint32)(bytes)" "$LAYERZERO_SETTLER_OP" "$LZ_SEND_ULN_OP" "$LZ_EID_BASE" "1" --rpc-url $RPC_11155420 2>/dev/null || echo "0x") - - if [ "$EXECUTOR_CONFIG_BYTES" != "0x" ] && [ ! -z "$EXECUTOR_CONFIG_BYTES" ]; then - # The executor config is encoded as (uint32 maxMessageSize, address executor) - # Remove 0x prefix and get the executor address (last 40 hex chars of the second 32-byte word) - EXECUTOR_HEX=$(echo "$EXECUTOR_CONFIG_BYTES" | sed 's/0x//' | tail -c 41) - if [ ! -z "$EXECUTOR_HEX" ]; then - EXECUTOR_ADDR="0x$EXECUTOR_HEX" - - if [ "$(echo $EXECUTOR_ADDR | tr '[:upper:]' '[:lower:]')" == "$(echo $LAYERZERO_SETTLER_OP | tr '[:upper:]' '[:lower:]')" ]; then - log_info " ✓ Executor correctly set to LayerZeroSettler" - else - log_warning " ⚠ Executor not set to LayerZeroSettler (self-execution model)" - fi - else - log_warning " ⚠ Could not parse executor configuration" - fi - else - log_warning " ⚠ Executor configuration not set" - fi - - # Check ULN configuration using endpoint.getConfig() - # CONFIG_TYPE_ULN = 2 - ULN_CONFIG_BYTES=$(cast call $LZ_ENDPOINT_OP "getConfig(address,address,uint32,uint32)(bytes)" "$LAYERZERO_SETTLER_OP" "$LZ_SEND_ULN_OP" "$LZ_EID_BASE" "2" --rpc-url $RPC_11155420 2>/dev/null || echo "0x") - - if [ "$ULN_CONFIG_BYTES" != "0x" ] && [ ! -z "$ULN_CONFIG_BYTES" ] && [ ${#ULN_CONFIG_BYTES} -gt 10 ]; then - log_info " ✓ ULN configuration is set" - else - log_warning " ⚠ ULN configuration not set" - fi -fi - -echo "" - -# Step 3: Fund signers and set them as gas wallets -echo "========================================================================" -log_info "STEP 3: Funding signers and setting them as gas wallets" -echo "========================================================================" - -if [ -z "$GAS_SIGNER_MNEMONIC" ]; then - log_error "Cannot proceed with FundSigners - GAS_SIGNER_MNEMONIC not set" - exit 1 -fi - -forge script deploy/FundSigners.s.sol:FundSigners \ - --broadcast --multi --slow \ - --sig "run(uint256[])" "$CHAIN_ARRAY" \ - --private-key $PRIVATE_KEY \ - -vvv - -check_success "Signer funding" -echo "" - -# Final Step: Run comprehensive verification for all deployed contracts and configurations -echo "========================================================================" -log_info "FINAL VERIFICATION: Running comprehensive verification" -echo "========================================================================" - -log_info "Running verification script for all deployments and configurations..." -bash deploy/verify_config.sh "${REQUESTED_CHAINS[@]}" -check_success "Comprehensive verification" - -echo "" -echo "========================================================================" -log_info "DEPLOYMENT COMPLETED SUCCESSFULLY!" -echo "========================================================================" -log_info "✅ All contracts deployed to requested chains" -log_info "✅ LayerZero configured for cross-chain communication" -log_info "✅ Signers funded and set as gas wallets" -log_info "✅ All verifications passed" -echo "" -log_info "Deployment and configuration completed successfully for chains: ${REQUESTED_CHAINS[*]}" -exit 0 - -# The old verification code below is no longer needed since verify_config.sh handles everything -: ' -# Old verification code starts here - -# Derive signer addresses from mnemonic (first 3 for verification) -log_info "Checking signer balances..." - -# First signer address (derived from the mnemonic) -SIGNER_0="0x33097354Acf259e1fD19fB91159BAE6ccf912Fdb" -SIGNER_1="0x49e1f963ddb4122BD3ccC786eB8F9983dABa8658" -SIGNER_2="0x46C66f82B32f04bf04D05ED92e10b57188BF408A" - -# Get target balance from config -TARGET_BALANCE_BASE=$(sed -n "/^\[84532\.uint\]/,/^\[/p" deploy/config.toml | grep "target_balance" | awk -F' = ' '{print $2}' | tr -d '"') - -# Check balances on Base Sepolia -log_info "Base Sepolia (84532) signer balances (target: $TARGET_BALANCE_BASE wei):" -BALANCE_0_BASE=$(cast balance $SIGNER_0 --rpc-url $RPC_84532 2>/dev/null || echo "0") -BALANCE_1_BASE=$(cast balance $SIGNER_1 --rpc-url $RPC_84532 2>/dev/null || echo "0") -BALANCE_2_BASE=$(cast balance $SIGNER_2 --rpc-url $RPC_84532 2>/dev/null || echo "0") - -# Check if balances meet target -if [ "$BALANCE_0_BASE" -ge "$TARGET_BALANCE_BASE" ]; then - log_info " Signer 0 ($SIGNER_0): $BALANCE_0_BASE wei ✓" -else - log_warning " Signer 0 ($SIGNER_0): $BALANCE_0_BASE wei (below target)" -fi - -if [ "$BALANCE_1_BASE" -ge "$TARGET_BALANCE_BASE" ]; then - log_info " Signer 1 ($SIGNER_1): $BALANCE_1_BASE wei ✓" -else - log_warning " Signer 1 ($SIGNER_1): $BALANCE_1_BASE wei (below target)" -fi - -if [ "$BALANCE_2_BASE" -ge "$TARGET_BALANCE_BASE" ]; then - log_info " Signer 2 ($SIGNER_2): $BALANCE_2_BASE wei ✓" -else - log_warning " Signer 2 ($SIGNER_2): $BALANCE_2_BASE wei (below target)" -fi - -# Get target balance from config -TARGET_BALANCE_OP=$(sed -n "/^\[11155420\.uint\]/,/^\[/p" deploy/config.toml | grep "target_balance" | awk -F' = ' '{print $2}' | tr -d '"') - -# Check balances on Optimism Sepolia -log_info "Optimism Sepolia (11155420) signer balances (target: $TARGET_BALANCE_OP wei):" -BALANCE_0_OP=$(cast balance $SIGNER_0 --rpc-url $RPC_11155420 2>/dev/null || echo "0") -BALANCE_1_OP=$(cast balance $SIGNER_1 --rpc-url $RPC_11155420 2>/dev/null || echo "0") -BALANCE_2_OP=$(cast balance $SIGNER_2 --rpc-url $RPC_11155420 2>/dev/null || echo "0") - -# Check if balances meet target -if [ "$BALANCE_0_OP" -ge "$TARGET_BALANCE_OP" ]; then - log_info " Signer 0 ($SIGNER_0): $BALANCE_0_OP wei ✓" -else - log_warning " Signer 0 ($SIGNER_0): $BALANCE_0_OP wei (below target)" -fi - -if [ "$BALANCE_1_OP" -ge "$TARGET_BALANCE_OP" ]; then - log_info " Signer 1 ($SIGNER_1): $BALANCE_1_OP wei ✓" -else - log_warning " Signer 1 ($SIGNER_1): $BALANCE_1_OP wei (below target)" -fi - -if [ "$BALANCE_2_OP" -ge "$TARGET_BALANCE_OP" ]; then - log_info " Signer 2 ($SIGNER_2): $BALANCE_2_OP wei ✓" -else - log_warning " Signer 2 ($SIGNER_2): $BALANCE_2_OP wei (below target)" -fi - -# Verify gas wallets and orchestrators in SimpleFunder -log_info "Checking SimpleFunder configuration..." - -# Read orchestrator addresses from config.toml -# supported_orchestrators is an array like ["0xAddr1", "0xAddr2"] -# For simplicity, we'll extract the first orchestrator address -ORCHESTRATOR_BASE_CONFIG=$(sed -n "/^\[84532\.address\]/,/^\[/p" deploy/config.toml | grep "supported_orchestrators" | sed 's/.*\["\([^"]*\)".*/\1/') -ORCHESTRATOR_OP_CONFIG=$(sed -n "/^\[11155420\.address\]/,/^\[/p" deploy/config.toml | grep "supported_orchestrators" | sed 's/.*\["\([^"]*\)".*/\1/') - -# For Base Sepolia -if [ ! -z "$SIMPLE_FUNDER_BASE" ]; then - log_info "Base Sepolia SimpleFunder ($SIMPLE_FUNDER_BASE):" - - # Check if signers are gas wallets (using mapping gasWallets(address) => bool) - IS_GAS_WALLET_0=$(cast call $SIMPLE_FUNDER_BASE "gasWallets(address)(bool)" $SIGNER_0 --rpc-url $RPC_84532 2>/dev/null || echo "false") - - if [ "$IS_GAS_WALLET_0" == "true" ]; then - log_info " ✓ Signer 0 is registered as gas wallet" - else - log_warning " ✗ Signer 0 is NOT registered as gas wallet" - fi - - # Check orchestrator configuration (using mapping orchestrators(address) => bool) - if [ ! -z "$ORCHESTRATOR_BASE_CONFIG" ]; then - IS_SUPPORTED=$(cast call $SIMPLE_FUNDER_BASE "orchestrators(address)(bool)" $ORCHESTRATOR_BASE_CONFIG --rpc-url $RPC_84532 2>/dev/null || echo "false") - - if [ "$IS_SUPPORTED" == "true" ]; then - log_info " ✓ Orchestrator $ORCHESTRATOR_BASE_CONFIG is supported" - else - log_warning " ✗ Orchestrator $ORCHESTRATOR_BASE_CONFIG is NOT supported" - fi - fi -fi - -# For Optimism Sepolia -if [ ! -z "$SIMPLE_FUNDER_OP" ]; then - log_info "Optimism Sepolia SimpleFunder ($SIMPLE_FUNDER_OP):" - - # Check if signers are gas wallets - IS_GAS_WALLET_0=$(cast call $SIMPLE_FUNDER_OP "gasWallets(address)(bool)" $SIGNER_0 --rpc-url $RPC_11155420 2>/dev/null || echo "false") - - if [ "$IS_GAS_WALLET_0" == "true" ]; then - log_info " ✓ Signer 0 is registered as gas wallet" - else - log_warning " ✗ Signer 0 is NOT registered as gas wallet" - fi - - # Check orchestrator configuration - if [ ! -z "$ORCHESTRATOR_OP_CONFIG" ]; then - IS_SUPPORTED=$(cast call $SIMPLE_FUNDER_OP "orchestrators(address)(bool)" $ORCHESTRATOR_OP_CONFIG --rpc-url $RPC_11155420 2>/dev/null || echo "false") - - if [ "$IS_SUPPORTED" == "true" ]; then - log_info " ✓ Orchestrator $ORCHESTRATOR_OP_CONFIG is supported" - else - log_warning " ✗ Orchestrator $ORCHESTRATOR_OP_CONFIG is NOT supported" - fi - fi -fi - -echo "" - -# Step 7: Summary -echo "========================================================================" -log_info "DEPLOYMENT TEST COMPLETED SUCCESSFULLY!" -echo "========================================================================" -log_info "✅ Contracts deployed to Base Sepolia and Optimism Sepolia" -log_info "✅ LayerZero configured for cross-chain communication" -log_info "✅ Signers funded and set as gas wallets" -echo "" - -echo "" -log_info "All deployment steps completed successfully!" -' # End of commented old verification code \ No newline at end of file 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 deleted file mode 100755 index af3a0fcd..00000000 --- a/deploy/verify_config.sh +++ /dev/null @@ -1,553 +0,0 @@ -#!/bin/bash - -# Verification script for deployed contracts and configuration -# This script performs all verification checks from test_deployment.sh -# without running any deployments or modifications -# -# Usage: bash deploy/verify_config.sh [chain_id1] [chain_id2] ... -# -# Examples: -# bash deploy/verify_config.sh # Verify all chains in config.toml -# bash deploy/verify_config.sh 84532 # Verify only Base Sepolia -# bash deploy/verify_config.sh 84532 11155420 # Verify Base Sepolia and Optimism Sepolia -# bash deploy/verify_config.sh 11155111 # Verify only Sepolia - -set -e # Exit on any error - -# Color codes for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -# Function to print colored output -log_info() { - echo -e "${GREEN}[INFO]${NC} $1" -} - -log_error() { - echo -e "${RED}[ERROR]${NC} $1" -} - -log_warning() { - echo -e "${YELLOW}[WARNING]${NC} $1" -} - -# Parse command-line arguments -REQUESTED_CHAINS=() -if [ $# -gt 0 ]; then - # User provided specific chain IDs to check - REQUESTED_CHAINS=("$@") - log_info "Requested verification for chain IDs: ${REQUESTED_CHAINS[*]}" -else - log_info "No specific chains requested, will verify all chains in config.toml" -fi - -# Start verification process -echo "========================================================================" -log_info "Configuration and Deployment Verification Script" -log_info "This script verifies the current state without making any changes" -if [ ${#REQUESTED_CHAINS[@]} -gt 0 ]; then - log_info "Verifying chains: ${REQUESTED_CHAINS[*]}" -else - log_info "Verifying all chains in config.toml" -fi -echo "========================================================================" - -# Load environment variables -log_info "Loading environment variables from .env" -if [ ! -f .env ]; then - log_error ".env file not found!" - exit 1 -fi - -source .env - -# Validate required environment variables (just check core ones) -log_info "Validating required environment variables" -required_vars=("PRIVATE_KEY" "GAS_SIGNER_MNEMONIC") -missing_vars=() - -for var in "${required_vars[@]}"; do - if [ -z "${!var}" ]; then - missing_vars+=("$var") - fi -done - -if [ ${#missing_vars[@]} -gt 0 ]; then - log_error "Missing required environment variables: ${missing_vars[*]}" - exit 1 -fi - -log_info "Core environment variables are set" -echo "" - -# Function to extract value from TOML for a specific chain section -extract_value() { - local chain=$1 - local key=$2 - local config_file=$3 - - # Get the next section after the chain section - local next_section=$(awk "/^\[$chain\]/,0" "$config_file" | grep '^\[' | grep -v "^\[$chain" | head -1) - - if [ -z "$next_section" ]; then - # This is the last section, get everything after it - awk "/^\[$chain\]/,0" "$config_file" | grep "^$key" | head -1 | cut -d'"' -f2 - else - # Get content between this section and the next - awk "/^\[$chain\]/,/^${next_section//[/\\[}/ { if (/^$key/) print }" "$config_file" | head -1 | cut -d'"' -f2 - fi -} - -# Function to extract uint value from TOML for a specific chain section -extract_uint_value() { - local chain=$1 - local key=$2 - local config_file=$3 - - # Use sed to extract the section and grep for the key - sed -n "/^\[$chain\.uint\]/,/^\[/p" "$config_file" | grep "^$key" | head -1 | awk -F' = ' '{print $2}' | tr -d '"' -} - -# Get all chain sections from config.toml (excluding .uint, .bytes32, .address, .bool, .string subsections) -get_chains() { - grep '^\[' deploy/config.toml | grep -v '\.' | sed 's/\[//g' | sed 's/\]//g' -} - -# Function to extract address value from TOML for a specific chain section -extract_address_value() { - local chain=$1 - local key=$2 - local config_file=$3 - - # Use sed to extract the section and grep for the key - sed -n "/^\[$chain\.address\]/,/^\[/p" "$config_file" | grep "^$key" | head -1 | cut -d'"' -f2 -} - -# Function to extract string value from TOML for a specific chain section -extract_string_value() { - local chain=$1 - local key=$2 - local config_file=$3 - - # Use sed to extract the section and grep for the key - sed -n "/^\[$chain\.string\]/,/^\[/p" "$config_file" | grep "^$key" | head -1 | cut -d'"' -f2 -} - -# ======================================================================== -# SECTION 1: Verify Contract Deployments -# ======================================================================== - -echo "========================================================================" -log_info "SECTION 1: Verifying Contract Deployments" -echo "========================================================================" - -log_info "Reading deployed contract addresses from config.toml..." - -# Contract types to verify -CONTRACT_TYPES=("orchestrator_deployed:Orchestrator" "ithaca_account_deployed:IthacaAccount" "account_proxy_deployed:AccountProxy" "simulator_deployed:Simulator" "simple_funder_deployed:SimpleFunder" "escrow_deployed:Escrow" "simple_settler_deployed:SimpleSettler" "layerzero_settler_deployed:LayerZeroSettler" "exp_token_deployed:ExpToken" "exp2_token_deployed:Exp2Token") - -# Get chains to verify -if [ ${#REQUESTED_CHAINS[@]} -gt 0 ]; then - # Use the chains requested by the user - CHAINS=("${REQUESTED_CHAINS[@]}") - log_info "Verifying ${#CHAINS[@]} requested chain(s): ${CHAINS[*]}" -else - # Get all chains from config.toml - CHAINS=($(get_chains)) - log_info "Found ${#CHAINS[@]} chain(s) in config: ${CHAINS[*]}" -fi -echo "" - -TOTAL_FAILED=0 - -# Iterate over each chain -for chain in "${CHAINS[@]}"; do - # Verify this chain exists in config - if ! grep -q "^\[$chain\]" deploy/config.toml; then - log_error "Chain ID $chain not found in config.toml" - continue - fi - # The chain variable IS the chain ID now - CHAIN_ID=$chain - - # Get chain name for display purposes - CHAIN_NAME=$(extract_string_value "$chain" "name" "deploy/config.toml") - if [ -z "$CHAIN_NAME" ]; then - CHAIN_NAME="Chain $CHAIN_ID" - fi - - log_info "Checking deployed contracts on $CHAIN_NAME (ID: $CHAIN_ID)..." - - # Get RPC URL from environment variable - RPC_VAR="RPC_${CHAIN_ID}" - RPC_URL=${!RPC_VAR} - - if [ -z "$RPC_URL" ]; then - log_warning " RPC URL not set for chain $chain (ID: $CHAIN_ID), skipping..." - log_warning " Set environment variable $RPC_VAR to enable verification" - continue - fi - - log_info " Chain ID: $CHAIN_ID" - - CHAIN_FAILED=0 - - # Check each contract type - for contract_type in "${CONTRACT_TYPES[@]}"; do - IFS=":" read -r key name <<< "$contract_type" - - # Extract address for this contract on this chain from the .address section - addr=$(extract_address_value "$chain" "$key" "deploy/config.toml") - - if [ ! -z "$addr" ]; then - CODE=$(cast code $addr --rpc-url $RPC_URL 2>/dev/null || echo "0x") - if [ "$CODE" != "0x" ] && [ ! -z "$CODE" ]; then - log_info " ✓ $name deployed at $addr" - else - log_error " ✗ $name NOT found at $addr" - CHAIN_FAILED=$((CHAIN_FAILED + 1)) - fi - else - log_warning " ⚠ $name address not found in config.toml" - fi - done - - if [ $CHAIN_FAILED -eq 0 ]; then - log_info " ✅ All contracts verified on $CHAIN_NAME" - else - log_error " ❌ $CHAIN_FAILED contract(s) failed verification on $CHAIN_NAME" - TOTAL_FAILED=$((TOTAL_FAILED + CHAIN_FAILED)) - fi - echo "" -done - -if [ $TOTAL_FAILED -eq 0 ]; then - log_info "✅ All contracts verified successfully across all chains!" -else - log_error "❌ Total contract verification failures: $TOTAL_FAILED" -fi -echo "" - -# ======================================================================== -# SECTION 2: Verify LayerZero Configuration -# ======================================================================== - -echo "========================================================================" -log_info "SECTION 2: Verifying LayerZero Configuration" -echo "========================================================================" - -log_info "Checking LayerZero configuration on all chains..." - -# Store LayerZero info for each chain (using arrays instead of associative arrays for compatibility) -LZ_CHAINS=() -LZ_SETTLERS=() -LZ_ENDPOINTS=() -LZ_EIDS=() -LZ_SEND_ULNS=() -LZ_RECEIVE_ULNS=() -LZ_CHAIN_IDS=() -LZ_RPC_URLS=() - -# Collect LayerZero configuration for each chain -LZ_INDEX=0 -for chain in "${CHAINS[@]}"; do - # Skip if chain doesn't exist in config - if ! grep -q "^\[$chain\]" deploy/config.toml; then - continue - fi - # Chain variable IS the chain ID now - CHAIN_ID=$chain - - # Get LayerZero settler address - settler=$(extract_address_value "$chain" "layerzero_settler_deployed" "deploy/config.toml") - - if [ -z "$settler" ]; then - CHAIN_NAME=$(extract_string_value "$chain" "name" "deploy/config.toml") - log_info " No LayerZero settler deployed on $CHAIN_NAME (ID: $CHAIN_ID), skipping LayerZero checks..." - continue - fi - - LZ_CHAINS+=("$chain") - LZ_SETTLERS+=("$settler") - - # Get other LayerZero config from .address section - LZ_ENDPOINTS+=($(extract_address_value "$chain" "layerzero_endpoint" "deploy/config.toml")) - LZ_SEND_ULNS+=($(extract_address_value "$chain" "layerzero_send_uln302" "deploy/config.toml")) - LZ_RECEIVE_ULNS+=($(extract_address_value "$chain" "layerzero_receive_uln302" "deploy/config.toml")) - - # Get EID from .uint section - LZ_EIDS+=($(extract_uint_value "$chain" "layerzero_eid" "deploy/config.toml")) - - LZ_CHAIN_IDS+=("$CHAIN_ID") - - # Get RPC URL - RPC_VAR="RPC_${CHAIN_ID}" - LZ_RPC_URLS+=(${!RPC_VAR}) -done - -# Verify LayerZero configuration for each chain -for i in "${!LZ_CHAINS[@]}"; do - chain=${LZ_CHAINS[$i]} - CHAIN_NAME=$(extract_string_value "$chain" "name" "deploy/config.toml") - if [ -z "$CHAIN_NAME" ]; then - CHAIN_NAME="Chain $chain" - fi - log_info "$CHAIN_NAME LayerZero configuration:" - - settler=${LZ_SETTLERS[$i]} - endpoint=${LZ_ENDPOINTS[$i]} - eid=${LZ_EIDS[$i]} - rpc_url=${LZ_RPC_URLS[$i]} - - if [ -z "$rpc_url" ]; then - log_warning " RPC URL not available for $CHAIN_NAME, skipping checks..." - continue - fi - - log_info " Settler: $settler" - log_info " Expected Endpoint: $endpoint" - log_info " EID: $eid" - - # Check if endpoint is set on LayerZeroSettler - CURRENT_ENDPOINT=$(cast call $settler "endpoint()(address)" --rpc-url $rpc_url 2>/dev/null || echo "0x0000000000000000000000000000000000000000") - - if [ "$CURRENT_ENDPOINT" == "$endpoint" ]; then - log_info " ✓ Endpoint correctly set to $endpoint" - else - log_error " ✗ Endpoint mismatch: Expected $endpoint, got $CURRENT_ENDPOINT" - fi - - # Check L0SettlerSigner - LZ_SIGNER=$(cast call $settler "l0SettlerSigner()(address)" --rpc-url $rpc_url 2>/dev/null || echo "0x0000000000000000000000000000000000000000") - log_info " L0SettlerSigner: $LZ_SIGNER" - echo "" -done - -# Verify cross-chain pathway configuration -log_info "Cross-chain pathway verification:" - -# Check pathways between all pairs of chains -for i in "${!LZ_CHAINS[@]}"; do - for j in "${!LZ_CHAINS[@]}"; do - if [ "$i" == "$j" ]; then - continue - fi - - source_chain_id=${LZ_CHAINS[$i]} - dest_chain_id=${LZ_CHAINS[$j]} - - source_chain=$(extract_string_value "$source_chain_id" "name" "deploy/config.toml") - dest_chain=$(extract_string_value "$dest_chain_id" "name" "deploy/config.toml") - if [ -z "$source_chain" ]; then - source_chain="Chain $source_chain_id" - fi - if [ -z "$dest_chain" ]; then - dest_chain="Chain $dest_chain_id" - fi - - source_settler=${LZ_SETTLERS[$i]} - source_endpoint=${LZ_ENDPOINTS[$i]} - source_send_uln=${LZ_SEND_ULNS[$i]} - source_rpc=${LZ_RPC_URLS[$i]} - dest_eid=${LZ_EIDS[$j]} - - if [ -z "$source_rpc" ]; then - continue - fi - - log_info " $source_chain -> $dest_chain pathway:" - - # Check executor configuration using endpoint.getConfig() - # CONFIG_TYPE_EXECUTOR = 1 - EXECUTOR_CONFIG_BYTES=$(cast call $source_endpoint "getConfig(address,address,uint32,uint32)(bytes)" "$source_settler" "$source_send_uln" "$dest_eid" "1" --rpc-url $source_rpc 2>/dev/null || echo "0x") - - if [ "$EXECUTOR_CONFIG_BYTES" != "0x" ] && [ ! -z "$EXECUTOR_CONFIG_BYTES" ]; then - # Try to decode executor address using cast - DECODED=$(cast abi-decode "f()(uint32,address)" "$EXECUTOR_CONFIG_BYTES" 2>/dev/null || echo "") - if [ ! -z "$DECODED" ]; then - EXECUTOR_ADDR=$(echo "$DECODED" | tail -1) - if [ "$(echo $EXECUTOR_ADDR | tr '[:upper:]' '[:lower:]')" == "$(echo $source_settler | tr '[:upper:]' '[:lower:]')" ]; then - log_info " ✓ Executor correctly set to LayerZeroSettler" - else - log_error " ⚠ Executor not set to LayerZeroSettler (self-execution model)" - fi - else - log_error " ⚠ Could not parse executor configuration" - fi - else - log_error " ⚠ Executor configuration not set" - fi - - # Check ULN configuration using endpoint.getConfig() - # CONFIG_TYPE_ULN = 2 - ULN_CONFIG_BYTES=$(cast call $source_endpoint "getConfig(address,address,uint32,uint32)(bytes)" "$source_settler" "$source_send_uln" "$dest_eid" "2" --rpc-url $source_rpc 2>/dev/null || echo "0x") - - if [ "$ULN_CONFIG_BYTES" != "0x" ] && [ ! -z "$ULN_CONFIG_BYTES" ] && [ ${#ULN_CONFIG_BYTES} -gt 10 ]; then - log_info " ✓ ULN configuration is set" - else - log_error " ⚠ ULN configuration not set" - fi - done -done -echo "" - -# ======================================================================== -# SECTION 3: Verify Signer Funding and Gas Wallet Configuration -# ======================================================================== - -echo "========================================================================" -log_info "SECTION 3: Verifying Signer Funding and Gas Wallet Configuration" -echo "========================================================================" - -# Derive signer addresses from mnemonic (first 3 for verification) -log_info "Checking signer balances..." - -# Derive signer addresses from mnemonic -log_info "Deriving signer addresses from gas wallet mnemonic..." -SIGNER_0=$(cast wallet address --mnemonic "$GAS_SIGNER_MNEMONIC" --mnemonic-index 0) -SIGNER_1=$(cast wallet address --mnemonic "$GAS_SIGNER_MNEMONIC" --mnemonic-index 1) -SIGNER_2=$(cast wallet address --mnemonic "$GAS_SIGNER_MNEMONIC" --mnemonic-index 2) - -log_info "Signer addresses:" -log_info " Signer 0: $SIGNER_0" -log_info " Signer 1: $SIGNER_1" -log_info " Signer 2: $SIGNER_2" - -# Check balances on all chains -for chain in "${CHAINS[@]}"; do - # Skip if chain doesn't exist in config - if ! grep -q "^\[$chain\]" deploy/config.toml; then - continue - fi - # Chain variable IS the chain ID now - CHAIN_ID=$chain - RPC_VAR="RPC_${CHAIN_ID}" - RPC_URL=${!RPC_VAR} - - # Get chain name for display - CHAIN_NAME=$(extract_string_value "$chain" "name" "deploy/config.toml") - if [ -z "$CHAIN_NAME" ]; then - CHAIN_NAME="Chain $CHAIN_ID" - fi - - if [ -z "$RPC_URL" ]; then - log_warning " RPC URL not set for $CHAIN_NAME, skipping signer balance checks..." - continue - fi - - # Get target balance from config - TARGET_BALANCE=$(extract_uint_value "$chain" "target_balance" "deploy/config.toml") - - if [ -z "$TARGET_BALANCE" ]; then - log_error " Target balance not set for $CHAIN_NAME" - continue - fi - - log_info "$CHAIN_NAME (Chain ID: $CHAIN_ID) signer balances (target: $TARGET_BALANCE wei):" - - # Check each signer's balance - for i in 0 1 2; do - SIGNER_VAR="SIGNER_${i}" - SIGNER_ADDR=${!SIGNER_VAR} - - BALANCE=$(cast balance $SIGNER_ADDR --rpc-url $RPC_URL 2>/dev/null || echo "0") - - if [ "$BALANCE" -ge "$TARGET_BALANCE" ] 2>/dev/null; then - log_info " Signer $i ($SIGNER_ADDR): $BALANCE wei ✓" - else - log_error " Signer $i ($SIGNER_ADDR): $BALANCE wei (below target)" - fi - done - echo "" -done - -# Verify gas wallets and orchestrators in SimpleFunder -log_info "Checking SimpleFunder configuration..." - -for chain in "${CHAINS[@]}"; do - # Skip if chain doesn't exist in config - if ! grep -q "^\[$chain\]" deploy/config.toml; then - continue - fi - - # Get SimpleFunder address - SIMPLE_FUNDER=$(extract_address_value "$chain" "simple_funder_deployed" "deploy/config.toml") - - # Get chain name for display - CHAIN_NAME=$(extract_string_value "$chain" "name" "deploy/config.toml") - if [ -z "$CHAIN_NAME" ]; then - CHAIN_NAME="Chain $chain" - fi - - if [ -z "$SIMPLE_FUNDER" ]; then - log_info " No SimpleFunder deployed on $CHAIN_NAME, skipping..." - continue - fi - - # Chain variable IS the chain ID now - CHAIN_ID=$chain - RPC_VAR="RPC_${CHAIN_ID}" - RPC_URL=${!RPC_VAR} - - if [ -z "$RPC_URL" ]; then - log_warning " RPC URL not set for $CHAIN_NAME, skipping SimpleFunder checks..." - continue - fi - - log_info "$CHAIN_NAME SimpleFunder ($SIMPLE_FUNDER):" - - # Check if signers are gas wallets - GAS_WALLET_OK=0 - for i in 0 1 2; do - SIGNER_VAR="SIGNER_${i}" - SIGNER_ADDR=${!SIGNER_VAR} - IS_GAS_WALLET=$(cast call $SIMPLE_FUNDER "gasWallets(address)(bool)" $SIGNER_ADDR --rpc-url $RPC_URL 2>/dev/null || echo "false") - - if [ "$IS_GAS_WALLET" == "true" ]; then - log_info " ✓ Signer $i is registered as gas wallet" - GAS_WALLET_OK=$((GAS_WALLET_OK + 1)) - else - log_error " ✗ Signer $i is NOT registered as gas wallet" - fi - done - - # Check orchestrator configuration - # Extract orchestrator from supported_orchestrators array in .address section - ORCHESTRATOR_CONFIG=$(extract_address_value "$chain" "supported_orchestrators" "deploy/config.toml" | sed 's/.*\["\([^"]*\)".*/\1/') - - if [ ! -z "$ORCHESTRATOR_CONFIG" ]; then - IS_SUPPORTED=$(cast call $SIMPLE_FUNDER "orchestrators(address)(bool)" $ORCHESTRATOR_CONFIG --rpc-url $RPC_URL 2>/dev/null || echo "false") - - if [ "$IS_SUPPORTED" == "true" ]; then - log_info " ✓ Orchestrator $ORCHESTRATOR_CONFIG is supported" - else - log_error " ✗ Orchestrator $ORCHESTRATOR_CONFIG is NOT supported" - fi - fi - - # Check funder and owner - FUNDER_ADDR=$(cast call $SIMPLE_FUNDER "funder()(address)" --rpc-url $RPC_URL 2>/dev/null || echo "0x0") - OWNER_ADDR=$(cast call $SIMPLE_FUNDER "owner()(address)" --rpc-url $RPC_URL 2>/dev/null || echo "0x0") - log_info " Funder: $FUNDER_ADDR" - log_info " Owner: $OWNER_ADDR" - echo "" -done - -# ======================================================================== -# FINAL SUMMARY -# ======================================================================== - -echo "========================================================================" -log_info "VERIFICATION COMPLETE" -echo "========================================================================" - -log_info "Summary:" -log_info " • Contract Deployments: Checked" -log_info " • LayerZero Configuration: Checked" -log_info " • Signer Funding: Checked" -log_info " • Gas Wallet Configuration: Checked" -echo "" -log_info "Run 'bash deploy/test_deployment.sh' for full deployment and configuration" -log_info "This script only verifies the current state without making changes" \ No newline at end of file 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..fbc90982 100644 --- a/foundry.lock +++ b/foundry.lock @@ -2,11 +2,11 @@ "lib/LayerZero-v2": { "rev": "88428755be6caa71cb1d2926141d73c8989296b5" }, + "lib/devtools": { + "rev": "01c1eaeb123e4364fbe96cdddf875640edf2566c" + }, "lib/forge-std": { - "branch": { - "name": "master", - "rev": "c2cf7017d27c1d20e74ace4dacb6c5ce4bbbe899" - } + "rev": "a3dca253700f19f15b1837c57c67b9388f5cc3fb" }, "lib/murky": { "rev": "5feccd1253d7da820f7cccccdedf64471025455d" @@ -17,7 +17,7 @@ "lib/solady": { "branch": { "name": "main", - "rev": "acd959aa4bd04720d640bf4e6a5c71037510cc4b" + "rev": "834bbc4fd366ca8bce8c532a0e3b34eca6be709c" } } } \ 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/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/prep/check-bytecode-changes.js b/prep/check-bytecode-changes.js old mode 100755 new mode 100644 diff --git a/src/IthacaAccount.sol b/src/IthacaAccount.sol index 0f0068ac..c15368d6 100644 --- a/src/IthacaAccount.sol +++ b/src/IthacaAccount.sol @@ -23,6 +23,7 @@ import {LibNonce} from "./libraries/LibNonce.sol"; import {TokenTransferLib} from "./libraries/TokenTransferLib.sol"; import {LibTStack} from "./libraries/LibTStack.sol"; import {IIthacaAccount} from "./interfaces/IIthacaAccount.sol"; +import {MerkleProofLib} from "solady/utils/MerkleProofLib.sol"; /// @title Account /// @notice A account contract for EOAs with EIP7702. @@ -485,9 +486,43 @@ contract IthacaAccount is IIthacaAccount, EIP712, GuardedExecutor { return isMultichain ? _hashTypedDataSansChainId(structHash) : _hashTypedData(structHash); } + /// @dev Verifies the merkle sig + /// - Note: Each leaf of the merkle tree should be a standard digest. + /// - The signature for using merkle verification is encoded as: + /// - bytes signature = abi.encode(bytes32[] proof, bytes32 root, bytes rootSig) + function _verifyMerkleSig(bytes32 digest, bytes calldata signature) + internal + view + returns (bool isValid, bytes32 keyHash) + { + bytes32[] calldata proof; + bytes32 root; + bytes calldata rootSig; + + assembly ("memory-safe") { + let proofOffset := add(signature.offset, calldataload(signature.offset)) + proof.length := calldataload(proofOffset) + proof.offset := add(proofOffset, 0x20) + + root := calldataload(add(signature.offset, 0x20)) + + let rootSigOffset := add(signature.offset, calldataload(add(signature.offset, 0x40))) + rootSig.length := calldataload(rootSigOffset) + rootSig.offset := add(rootSigOffset, 0x20) + } + + if (MerkleProofLib.verifyCalldata(proof, root, digest)) { + (isValid, keyHash) = unwrapAndValidateSignature(root, rootSig); + + return (isValid, keyHash); + } + + return (false, bytes32(0)); + } + /// @dev Returns if the signature is valid, along with its `keyHash`. /// The `signature` is a wrapped signature, given by - /// `abi.encodePacked(bytes(innerSignature), bytes32(keyHash), bool(prehash))`. + /// `abi.encode(bytes(innerSignature), bytes32(keyHash), bool(prehash), bool(merkle))`. function unwrapAndValidateSignature(bytes32 digest, bytes calldata signature) public view @@ -502,14 +537,20 @@ contract IthacaAccount is IIthacaAccount, EIP712, GuardedExecutor { return (ECDSA.recoverCalldata(digest, signature) == address(this), 0); } + bool merkle; unchecked { - uint256 n = signature.length - 0x21; + uint256 n = signature.length - 0x22; keyHash = LibBytes.loadCalldata(signature, n); signature = LibBytes.truncatedCalldata(signature, n); // Do the prehash if last byte is non-zero. if (uint256(LibBytes.loadCalldata(signature, n + 1)) & 0xff != 0) { digest = EfficientHashLib.sha2(digest); // `sha256(abi.encode(digest))`. } + merkle = uint256(LibBytes.loadCalldata(signature, n + 2)) & 0xff != 0; + } + + if (merkle) { + return _verifyMerkleSig(digest, signature); } Key memory key = getKey(keyHash); @@ -573,8 +614,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); } @@ -747,6 +789,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/SimpleFunder.sol b/src/SimpleFunder.sol index e74947fb..959723d1 100644 --- a/src/SimpleFunder.sol +++ b/src/SimpleFunder.sol @@ -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(); } 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/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/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..083e6b3a 100644 --- a/test/Account.t.sol +++ b/test/Account.t.sol @@ -6,6 +6,8 @@ import "./Base.t.sol"; import {MockSampleDelegateCallTarget} from "./utils/mocks/MockSampleDelegateCallTarget.sol"; import {LibEIP7702} from "solady/accounts/LibEIP7702.sol"; +import {Merkle} from "murky/Merkle.sol"; + contract AccountTest is BaseTest { struct _TestExecuteWithSignatureTemps { TargetFunctionPayload[] targetFunctionPayloads; @@ -68,6 +70,70 @@ contract AccountTest is BaseTest { } } + function testMerkleSignature(uint256 seed) public { + DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); + PassKey memory k = _randomSecp256k1PassKey(); + k.k.isSuperAdmin = true; + + vm.prank(d.eoa); + d.d.authorize(k.k); + + // Fuzz number of leaves (2 to 256) + uint256 numLeaves = bound(seed, 2, 256); + bytes32[] memory leaves = new bytes32[](numLeaves); + + // Generate random leaves + for (uint256 i = 0; i < numLeaves; i++) { + leaves[i] = keccak256(abi.encodePacked("leaf", i, seed)); + } + + // Pick a random valid index + uint256 validIndex = seed % numLeaves; + bytes32 validDigest = leaves[validIndex]; + + Merkle merkle = new Merkle(); + bytes32 root = merkle.getRoot(leaves); + bytes32[] memory proof = merkle.getProof(leaves, validIndex); + + // Test valid merkle proof + { + bytes memory rootSig = abi.encode(proof, root, _sig(k, root)); + bytes memory sig = abi.encodePacked(rootSig, bytes32(0), uint8(0), uint8(1)); + + (bool isValid, bytes32 keyHash) = d.d.unwrapAndValidateSignature(validDigest, sig); + assertEq(isValid, true); + assertEq(keyHash, k.keyHash); + + // Test invalid digest not in tree + bytes32 invalidDigest = keccak256(abi.encodePacked("not_in_tree", seed)); + (isValid, keyHash) = d.d.unwrapAndValidateSignature(invalidDigest, sig); + assertEq(isValid, false); + assertEq(keyHash, bytes32(0)); + } + + // Test tampered proof (only if proof has elements to tamper) + if (proof.length > 0) { + bytes32[] memory tamperedProof = new bytes32[](proof.length); + for (uint256 i = 0; i < proof.length; i++) { + tamperedProof[i] = i == 0 ? bytes32(uint256(proof[i]) ^ 1) : proof[i]; + } + bytes memory tamperedRootSig = abi.encode(tamperedProof, root, _sig(k, root)); + bytes memory tamperedSig = + abi.encodePacked(tamperedRootSig, bytes32(0), uint8(0), uint8(1)); + (bool isValid,) = d.d.unwrapAndValidateSignature(validDigest, tamperedSig); + assertEq(isValid, false); + } + + // Test wrong root (tampered root should fail verification) + { + bytes32 wrongRoot = bytes32(uint256(root) ^ 1); + bytes memory wrongRootSig = abi.encode(proof, wrongRoot, _sig(k, wrongRoot)); + bytes memory wrongSig = abi.encodePacked(wrongRootSig, bytes32(0), uint8(0), uint8(1)); + (bool isValid,) = d.d.unwrapAndValidateSignature(validDigest, wrongSig); + assertEq(isValid, false); + } + } + function testSignatureCheckerApproval(bytes32) public { DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); PassKey memory k = _randomSecp256k1PassKey(); @@ -93,8 +159,7 @@ contract AccountTest is BaseTest { bytes32 replaySafeDigest = keccak256(abi.encode(d.d.SIGN_TYPEHASH(), digest)); - (, string memory name, string memory version,, address verifyingContract,,) = - d.d.eip712Domain(); + (,,,, address verifyingContract,,) = d.d.eip712Domain(); bytes32 domain = keccak256( abi.encode( 0x035aff83d86937d35b32e04f0ddc6ff469290eef2f1b692d8a815c89404d4749, // DOMAIN_TYPEHASH with only verifyingContract @@ -397,6 +462,10 @@ contract AccountTest is BaseTest { 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"); + 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..87e82660 100644 --- a/test/Base.t.sol +++ b/test/Base.t.sol @@ -220,7 +220,7 @@ contract BaseTest is SoladyTest { { (bytes32 r, bytes32 s) = vm.signP256(privateKey, digest); s = P256.normalized(s); - return abi.encodePacked(abi.encode(r, s), keyHash, uint8(prehash ? 1 : 0)); + return abi.encodePacked(abi.encode(r, s), keyHash, uint8(prehash ? 1 : 0), uint8(0)); } function _secp256k1Sig(uint256 privateKey, bytes32 keyHash, bytes32 digest) @@ -237,7 +237,8 @@ contract BaseTest is SoladyTest { returns (bytes memory) { (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest); - return abi.encodePacked(abi.encodePacked(r, s, v), keyHash, uint8(prehash ? 1 : 0)); + return + abi.encodePacked(abi.encodePacked(r, s, v), keyHash, uint8(prehash ? 1 : 0), uint8(0)); } function _multiSig(MultiSigKey memory k, bytes32 keyHash, bool preHash, bytes32 digest) @@ -250,7 +251,7 @@ contract BaseTest is SoladyTest { signatures[i] = _sig(k.owners[i], digest); } - return abi.encodePacked(abi.encode(signatures), keyHash, uint8(preHash ? 1 : 0)); + return abi.encodePacked(abi.encode(signatures), keyHash, uint8(preHash ? 1 : 0), uint8(0)); } function _estimateGasForEOAKey(Orchestrator.Intent memory i) @@ -282,7 +283,7 @@ contract BaseTest is SoladyTest { { (uint8 v, bytes32 r, bytes32 s) = vm.sign(uint128(_randomUniform()), bytes32(_randomUniform())); - i.signature = abi.encodePacked(abi.encodePacked(r, s, v), keyHash, uint8(0)); + i.signature = abi.encodePacked(abi.encodePacked(r, s, v), keyHash, uint8(0), uint8(0)); return _estimateGas(i); } @@ -290,7 +291,7 @@ contract BaseTest is SoladyTest { internal returns (uint256 gExecute, uint256 gCombined, uint256 gUsed) { - i.signature = abi.encodePacked(keccak256("a"), keccak256("b"), keyHash, uint8(0)); + i.signature = abi.encodePacked(keccak256("a"), keccak256("b"), keyHash, uint8(0), uint8(0)); return _estimateGas(i); } diff --git a/test/Benchmark.t.sol b/test/Benchmark.t.sol index 2ce04524..b2464a8e 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(""); @@ -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/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..9f2ada0c 100644 --- a/test/Orchestrator.t.sol +++ b/test/Orchestrator.t.sol @@ -924,8 +924,9 @@ 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); diff --git a/test/SimulateExecute.t.sol b/test/SimulateExecute.t.sol index 099d6acc..1e4a89a4 100644 --- a/test/SimulateExecute.t.sol +++ b/test/SimulateExecute.t.sol @@ -305,7 +305,8 @@ 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); 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/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/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 {} -}