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