From a5f7ead78c60d6bce369402101567ae18e07083f Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Mon, 28 Apr 2025 17:54:25 +0300 Subject: [PATCH 01/46] prevent stack to deep --- src/Periphery/Patcher.sol | 227 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 src/Periphery/Patcher.sol diff --git a/src/Periphery/Patcher.sol b/src/Periphery/Patcher.sol new file mode 100644 index 000000000..fbb5dcbc7 --- /dev/null +++ b/src/Periphery/Patcher.sol @@ -0,0 +1,227 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +/// @title Patcher +/// @notice A contract that patches calldata with dynamically retrieved values before execution +/// @dev Designed to be used with delegate calls +contract Patcher { + /// @notice Error when getting a dynamic value fails + error FailedToGetDynamicValue(); + + /// @notice Error when input arrays have mismatched lengths + error MismatchedArrayLengths(); + + /// @notice Error when a patch offset is invalid + error InvalidPatchOffset(); + + /// @notice Helper function to get a dynamic value from an external contract + /// @param valueSource The contract to query for the dynamic value + /// @param valueGetter The calldata to use to get the dynamic value + /// @return The uint256 value retrieved from the call + function _getDynamicValue( + address valueSource, + bytes calldata valueGetter + ) internal view returns (uint256) { + (bool valueSuccess, bytes memory valueData) = valueSource.staticcall( + valueGetter + ); + if (!valueSuccess) revert FailedToGetDynamicValue(); + + uint256 dynamicValue; + assembly { + // Load the value from the return data + dynamicValue := mload(add(valueData, 32)) + } + + return dynamicValue; + } + + /// @notice Helper function to apply a patch at a specific offset + /// @param patchedData The data to patch + /// @param offset The byte offset in the data + /// @param dynamicValue The value to write at the offset + function _applyPatch( + bytes memory patchedData, + uint256 offset, + uint256 dynamicValue + ) internal pure { + if (offset + 32 > patchedData.length) revert InvalidPatchOffset(); + + assembly { + // Calculate the position in memory where we need to write the new value + let position := add(add(patchedData, 32), offset) + + // Store the new value at the calculated position + mstore(position, dynamicValue) + } + } + + /// @notice Helper function to execute the final call + /// @param finalTarget The contract to call + /// @param value The ETH value to send + /// @param patchedData The patched calldata to use + /// @param delegateCall Whether to use delegatecall + /// @return success Whether the call was successful + /// @return returnData The data returned by the call + function _executeCall( + address finalTarget, + uint256 value, + bytes memory patchedData, + bool delegateCall + ) internal returns (bool success, bytes memory returnData) { + if (delegateCall) { + (success, returnData) = finalTarget.delegatecall(patchedData); + } else { + (success, returnData) = finalTarget.call{ value: value }( + patchedData + ); + } + } + + /// @notice Retrieves a value dynamically and uses it to patch calldata before execution + /// @param valueSource The contract to query for the dynamic value + /// @param valueGetter The calldata to use to get the dynamic value (e.g., balanceOf call) + /// @param finalTarget The contract to call with the patched data + /// @param value The ETH value to send with the final call + /// @param data The original calldata to patch and execute + /// @param offsets Array of byte offsets in the original calldata to patch with the dynamic value + /// @param delegateCall If true, executes a delegatecall instead of a regular call for the final call + /// @return success Whether the final call was successful + /// @return returnData The data returned by the final call + function executeWithDynamicPatches( + address valueSource, + bytes calldata valueGetter, + address finalTarget, + uint256 value, + bytes calldata data, + uint256[] calldata offsets, + bool delegateCall + ) external returns (bool success, bytes memory returnData) { + return + _executeWithDynamicPatches( + valueSource, + valueGetter, + finalTarget, + value, + data, + offsets, + delegateCall + ); + } + + /// @dev Internal implementation to avoid stack too deep errors + function _executeWithDynamicPatches( + address valueSource, + bytes calldata valueGetter, + address finalTarget, + uint256 value, + bytes calldata data, + uint256[] calldata offsets, + bool delegateCall + ) internal returns (bool success, bytes memory returnData) { + // Get the dynamic value + uint256 dynamicValue = _getDynamicValue(valueSource, valueGetter); + + // Create a mutable copy of the original calldata + bytes memory patchedData = bytes(data); + + // Apply the patches in-place + _applyPatches(patchedData, offsets, dynamicValue); + + // Execute the call with the patched data + return _executeCall(finalTarget, value, patchedData, delegateCall); + } + + /// @notice Retrieves multiple values dynamically and uses them to patch calldata at different offsets + /// @param valueSources Array of contracts to query for dynamic values + /// @param valueGetters Array of calldata to use to get each dynamic value + /// @param finalTarget The contract to call with the patched data + /// @param value The ETH value to send with the final call + /// @param data The original calldata to patch and execute + /// @param offsetGroups Array of offset arrays, each corresponding to a value source/getter pair + /// @param delegateCall If true, executes a delegatecall instead of a regular call for the final call + /// @return success Whether the final call was successful + /// @return returnData The data returned by the final call + function executeWithMultiplePatches( + address[] calldata valueSources, + bytes[] calldata valueGetters, + address finalTarget, + uint256 value, + bytes calldata data, + uint256[][] calldata offsetGroups, + bool delegateCall + ) external returns (bool success, bytes memory returnData) { + return + _executeWithMultiplePatches( + valueSources, + valueGetters, + finalTarget, + value, + data, + offsetGroups, + delegateCall + ); + } + + /// @dev Internal implementation to avoid stack too deep errors + function _executeWithMultiplePatches( + address[] calldata valueSources, + bytes[] calldata valueGetters, + address finalTarget, + uint256 value, + bytes calldata data, + uint256[][] calldata offsetGroups, + bool delegateCall + ) internal returns (bool success, bytes memory returnData) { + // Validation + if ( + valueSources.length != valueGetters.length || + valueSources.length != offsetGroups.length + ) { + revert MismatchedArrayLengths(); + } + + // Create a mutable copy of the original calldata + bytes memory patchedData = bytes(data); + + // Process patches in batches to avoid stack too deep + _processPatches(valueSources, valueGetters, offsetGroups, patchedData); + + // Execute the call with the patched data + return _executeCall(finalTarget, value, patchedData, delegateCall); + } + + /// @dev Helper function to process patches in batches + function _processPatches( + address[] calldata valueSources, + bytes[] calldata valueGetters, + uint256[][] calldata offsetGroups, + bytes memory patchedData + ) internal view { + for (uint256 i = 0; i < valueSources.length; i++) { + // Get the dynamic value for this patch + uint256 dynamicValue = _getDynamicValue( + valueSources[i], + valueGetters[i] + ); + + // Apply the patches for this value + uint256[] calldata offsets = offsetGroups[i]; + _applyPatches(patchedData, offsets, dynamicValue); + } + } + + /// @notice Helper function to apply multiple patches with the same value + /// @param patchedData The data to patch + /// @param offsets Array of offsets where to apply the patches + /// @param dynamicValue The value to write at each offset + function _applyPatches( + bytes memory patchedData, + uint256[] calldata offsets, + uint256 dynamicValue + ) internal pure { + for (uint256 j = 0; j < offsets.length; j++) { + _applyPatch(patchedData, offsets[j], dynamicValue); + } + } +} From 5bdbb687681d316f906efdf6c84bb82b113892ad Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Thu, 8 May 2025 18:33:35 +0300 Subject: [PATCH 02/46] add demo script --- bun.lock | 235 ++++++ deployments/_deployments_log_file.json | 16 + deployments/arbitrum.diamond.staging.json | 3 +- deployments/arbitrum.staging.json | 3 +- package.json | 1 + script/demoScripts/demoPatcher.ts | 928 ++++++++++++++++++++++ script/deploy/facets/DeployPatcher.s.sol | 13 + src/Periphery/Patcher.sol | 4 +- 8 files changed, 1200 insertions(+), 3 deletions(-) create mode 100644 script/demoScripts/demoPatcher.ts create mode 100644 script/deploy/facets/DeployPatcher.s.sol diff --git a/bun.lock b/bun.lock index a79d6bd88..18cb98509 100644 --- a/bun.lock +++ b/bun.lock @@ -5,6 +5,7 @@ "name": "lifi-contracts", "dependencies": { "@arbitrum/sdk": "^3.0.0", + "@cowprotocol/cow-sdk": "^5.10.3", "@hop-protocol/sdk": "0.0.1-beta.310", "@layerzerolabs/lz-v2-utilities": "^2.3.21", "@octokit/rest": "^21.0.1", @@ -66,6 +67,8 @@ "@arbitrum/sdk": ["@arbitrum/sdk@3.7.2", "", { "dependencies": { "@ethersproject/address": "^5.0.8", "@ethersproject/bignumber": "^5.1.1", "@ethersproject/bytes": "^5.0.8", "async-mutex": "^0.4.0", "ethers": "^5.1.0" } }, "sha512-oW2sg/a2MUc8HfvVFkasl6TY539xJ8HBjVnVEC9v31Sxnz+5Kpqpauct7zFTE4HepXUy5S7GPX8K9lq52du+Fw=="], + "@assemblyscript/loader": ["@assemblyscript/loader@0.9.4", "", {}, "sha512-HazVq9zwTVwGmqdwYzu7WyQ6FQVZ7SwET0KKQuKm55jD0IfUpZgN0OPIiZG3zV1iSrVYcN0bdwLRXI/VNCYsUA=="], + "@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@1.2.2", "", { "dependencies": { "@aws-crypto/util": "^1.2.2", "@aws-sdk/types": "^3.1.0", "tslib": "^1.11.1" } }, "sha512-Nr1QJIbW/afYYGzYvrF70LtaHrIRtd4TNAglX8BvlfxJLZ45SAmueIKYl5tWoNBPzp65ymXGFK0Bb1vZUpuc9g=="], "@aws-crypto/util": ["@aws-crypto/util@1.2.2", "", { "dependencies": { "@aws-sdk/types": "^3.1.0", "@aws-sdk/util-utf8-browser": "^3.0.0", "tslib": "^1.11.1" } }, "sha512-H8PjG5WJ4wz0UXAFXeJjWCW1vkvIJ3qUUD+rGRwJ2/hj+xT58Qle2MTql/2MGzkU+1JLAFuR6aJpLAjHwhmwwg=="], @@ -80,6 +83,12 @@ "@babel/runtime": ["@babel/runtime@7.26.9", "", { "dependencies": { "regenerator-runtime": "^0.14.0" } }, "sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg=="], + "@cowprotocol/app-data": ["@cowprotocol/app-data@2.5.1", "", { "dependencies": { "ajv": "^8.11.0", "cross-fetch": "^3.1.5", "ipfs-only-hash": "^4.0.0", "json-stringify-deterministic": "^1.0.8", "multiformats": "^9.6.4" }, "peerDependencies": { "ethers": "^5.0.0" } }, "sha512-GvcLQ44ZUHKdSAXiZ+YCaIehtme/OIvbvL/4go1UqNLTj3jDTDkagPh7CH/z2IPwFWTADyNZO/RSzKrW5UDNaA=="], + + "@cowprotocol/contracts": ["@cowprotocol/contracts@1.7.0", "", { "peerDependencies": { "ethers": "^5.4.0" } }, "sha512-tog/0igLaWZv6kK30+aFJ267+yNyMINx6qDHaJQFKZs051XUXnzNFlZgCEfICwDz/821XYjZxOnNbe449tgD5w=="], + + "@cowprotocol/cow-sdk": ["@cowprotocol/cow-sdk@5.10.3", "", { "dependencies": { "@cowprotocol/app-data": "^2.4.0", "@cowprotocol/contracts": "^1.6.0", "@ethersproject/abstract-signer": "^5.7.0", "@openzeppelin/merkle-tree": "^1.0.5", "cross-fetch": "^3.1.5", "exponential-backoff": "^3.1.1", "graphql": "^16.3.0", "graphql-request": "^4.3.0", "limiter": "^3.0.0" }, "peerDependencies": { "ethers": "^5.7.2" } }, "sha512-whnfufOmJqJWw7u2HqLt6k+rw8eMH+ywmB++oxG6U6zLBdQihk18mIMz1wrNwfTUk0FFtn8g8nhrQL14oV27rg=="], + "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.4.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA=="], @@ -94,8 +103,12 @@ "@eth-optimism/core-utils": ["@eth-optimism/core-utils@0.12.0", "", { "dependencies": { "@ethersproject/abi": "^5.7.0", "@ethersproject/abstract-provider": "^5.7.0", "@ethersproject/address": "^5.7.0", "@ethersproject/bignumber": "^5.7.0", "@ethersproject/bytes": "^5.7.0", "@ethersproject/constants": "^5.7.0", "@ethersproject/contracts": "^5.7.0", "@ethersproject/hash": "^5.7.0", "@ethersproject/keccak256": "^5.7.0", "@ethersproject/properties": "^5.7.0", "@ethersproject/providers": "^5.7.0", "@ethersproject/rlp": "^5.7.0", "@ethersproject/transactions": "^5.7.0", "@ethersproject/web": "^5.7.0", "bufio": "^1.0.7", "chai": "^4.3.4" } }, "sha512-qW+7LZYCz7i8dRa7SRlUKIo1VBU8lvN0HeXCxJR+z+xtMzMQpPds20XJNCMclszxYQHkXY00fOT6GvFw9ZL6nw=="], + "@ethereumjs/common": ["@ethereumjs/common@3.2.0", "", { "dependencies": { "@ethereumjs/util": "^8.1.0", "crc-32": "^1.2.0" } }, "sha512-pksvzI0VyLgmuEF2FA/JR/4/y6hcPq8OUail3/AvycBaW1d5VSauOZzqGvJ3RTmR4MU35lWE8KseKOsEhrFRBA=="], + "@ethereumjs/rlp": ["@ethereumjs/rlp@4.0.1", "", { "bin": { "rlp": "bin/rlp" } }, "sha512-tqsQiBQDQdmPWE1xkkBq4rlSW5QZpLOUJ5RJh2/9fug+q9tnUhuZoVLk7s0scUIKTOzEtR72DFBXI4WiZcMpvw=="], + "@ethereumjs/tx": ["@ethereumjs/tx@4.2.0", "", { "dependencies": { "@ethereumjs/common": "^3.2.0", "@ethereumjs/rlp": "^4.0.1", "@ethereumjs/util": "^8.1.0", "ethereum-cryptography": "^2.0.0" } }, "sha512-1nc6VO4jtFd172BbSnTnDQVr9IYBFl1y4xPzZdtkrkKIncBCkdbgfdRV+MiTkJYAtTxvV12GRZLqBFT1PNK6Yw=="], + "@ethereumjs/util": ["@ethereumjs/util@8.1.0", "", { "dependencies": { "@ethereumjs/rlp": "^4.0.1", "ethereum-cryptography": "^2.0.0", "micro-ftch": "^0.3.1" } }, "sha512-zQ0IqbdX8FZ9aw11vP+dZkKDkS+kgIvQPHnSAXzP9pLu+Rfu3D3XEeLbicvoXJTYnhZiPmsZUxgdzXwNKxRPbA=="], "@ethersproject/abi": ["@ethersproject/abi@5.7.0", "", { "dependencies": { "@ethersproject/address": "^5.7.0", "@ethersproject/bignumber": "^5.7.0", "@ethersproject/bytes": "^5.7.0", "@ethersproject/constants": "^5.7.0", "@ethersproject/hash": "^5.7.0", "@ethersproject/keccak256": "^5.7.0", "@ethersproject/logger": "^5.7.0", "@ethersproject/properties": "^5.7.0", "@ethersproject/strings": "^5.7.0" } }, "sha512-351ktp42TiRcYB3H1OP8yajPeAQstMW/yCFokj/AthP9bLHzQFPlOrxOcwYEDkUAICmOHljvN4K39OMTMUa9RA=="], @@ -194,10 +207,20 @@ "@mayanfinance/swap-sdk": ["@mayanfinance/swap-sdk@8.6.0", "", { "dependencies": { "@solana/buffer-layout": "^4 || ^3", "@solana/web3.js": "^1.87.6", "cross-fetch": "^3.1.5", "ethers": "^6", "js-sha256": "^0.9.0", "js-sha3": "^0.8.0" } }, "sha512-Flk8+9BkMlmu2HI+cwEohlmwnNViUapXORsYZEbDzy1xhLdJQOb2V0ykw9TqXKcfp9BAdD17oqEyCzZgkXGC4g=="], + "@metamask/abi-utils": ["@metamask/abi-utils@2.0.4", "", { "dependencies": { "@metamask/superstruct": "^3.1.0", "@metamask/utils": "^9.0.0" } }, "sha512-StnIgUB75x7a7AgUhiaUZDpCsqGp7VkNnZh2XivXkJ6mPkE83U8ARGQj5MbRis7VJY8BC5V1AbB1fjdh0hupPQ=="], + "@metamask/eth-sig-util": ["@metamask/eth-sig-util@4.0.1", "", { "dependencies": { "ethereumjs-abi": "^0.6.8", "ethereumjs-util": "^6.2.1", "ethjs-util": "^0.1.6", "tweetnacl": "^1.0.3", "tweetnacl-util": "^0.15.1" } }, "sha512-tghyZKLHZjcdlDqCA3gNZmLeR0XvOE9U1qoQO9ohyAZT6Pya+H9vkBPcsyXytmYLNgVoin7CKCmweo/R43V+tQ=="], + "@metamask/superstruct": ["@metamask/superstruct@3.2.1", "", {}, "sha512-fLgJnDOXFmuVlB38rUN5SmU7hAFQcCjrg3Vrxz67KTY7YHFnSNEKvX4avmEBdOI0yTCxZjwMCFEqsC8k2+Wd3g=="], + + "@metamask/utils": ["@metamask/utils@9.3.0", "", { "dependencies": { "@ethereumjs/tx": "^4.2.0", "@metamask/superstruct": "^3.1.0", "@noble/hashes": "^1.3.1", "@scure/base": "^1.1.3", "@types/debug": "^4.1.7", "debug": "^4.3.4", "pony-cause": "^2.1.10", "semver": "^7.5.4", "uuid": "^9.0.1" } }, "sha512-w8CVbdkDrVXFJbfBSlDfafDR6BAkpDmv1bC1UJVCoVny5tW2RKAdn9i68Xf7asYT4TnUhl/hN4zfUiKQq9II4g=="], + "@mongodb-js/saslprep": ["@mongodb-js/saslprep@1.2.0", "", { "dependencies": { "sparse-bitfield": "^3.0.3" } }, "sha512-+ywrb0AqkfaYuhHs6LxKWgqbh3I72EpEgESCw37o+9qPx9WTCkgDm2B+eMrwehGtHBWHFU4GXvnSCNiFhhausg=="], + "@multiformats/base-x": ["@multiformats/base-x@4.0.1", "", {}, "sha512-eMk0b9ReBbV23xXU693TAIrLyeO5iTgBZGSJfpqriG8UkYvr/hC9u9pyMlAakDNHWmbhMZCDs6KQO0jzKD8OTw=="], + + "@noble/ciphers": ["@noble/ciphers@1.3.0", "", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="], + "@noble/curves": ["@noble/curves@1.8.1", "", { "dependencies": { "@noble/hashes": "1.7.1" } }, "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ=="], "@noble/hashes": ["@noble/hashes@1.7.1", "", {}, "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ=="], @@ -278,6 +301,8 @@ "@octokit/types": ["@octokit/types@13.8.0", "", { "dependencies": { "@octokit/openapi-types": "^23.0.1" } }, "sha512-x7DjTIbEpEWXK99DMd01QfWy0hd5h4EN+Q7shkdKds3otGQP+oWE/y0A76i1OvH9fygo4ddvNf7ZvF0t78P98A=="], + "@openzeppelin/merkle-tree": ["@openzeppelin/merkle-tree@1.0.8", "", { "dependencies": { "@metamask/abi-utils": "^2.0.4", "ethereum-cryptography": "^3.0.0" } }, "sha512-E2c9/Y3vjZXwVvPZKqCKUn7upnvam1P1ZhowJyZVQSkzZm5WhumtaRr+wkUXrZVfkIc7Gfrl7xzabElqDL09ow=="], + "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], "@pnpm/config.env-replace": ["@pnpm/config.env-replace@1.1.0", "", {}, "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w=="], @@ -356,6 +381,8 @@ "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], + "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], + "@types/fined": ["@types/fined@1.1.5", "", {}, "sha512-2N93vadEGDFhASTIRbizbl4bNqpMOId5zZfj6hHqYZfEzEfO9onnU4Im8xvzo8uudySDveDHBOOSlTWf38ErfQ=="], "@types/fs-extra": ["@types/fs-extra@11.0.4", "", { "dependencies": { "@types/jsonfile": "*", "@types/node": "*" } }, "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ=="], @@ -384,10 +411,16 @@ "@types/minimatch": ["@types/minimatch@5.1.2", "", {}, "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA=="], + "@types/minimist": ["@types/minimist@1.2.5", "", {}, "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag=="], + "@types/mkdirp": ["@types/mkdirp@0.5.2", "", { "dependencies": { "@types/node": "*" } }, "sha512-U5icWpv7YnZYGsN4/cmh3WD2onMY0aJIiTE6+51TwJCttdHvtCYmkBNOobHlXwrJRL0nkH9jH4kD+1FAdMN4Tg=="], + "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], + "@types/node": ["@types/node@17.0.45", "", {}, "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw=="], + "@types/normalize-package-data": ["@types/normalize-package-data@2.4.4", "", {}, "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA=="], + "@types/pbkdf2": ["@types/pbkdf2@3.1.2", "", { "dependencies": { "@types/node": "*" } }, "sha512-uRwJqmiXmh9++aSu1VNEn3iIxWOhd8AHXNSdlaLfdAAdSTY9jYVeGWnzejM3dvrkbqE3/hyQkQQ29IFATEGlew=="], "@types/pino": ["@types/pino@7.0.5", "", { "dependencies": { "pino": "*" } }, "sha512-wKoab31pknvILkxAF8ss+v9iNyhw5Iu/0jLtRkUD74cNfOOLJNnqfFKAv0r7wVaTQxRZtWrMpGfShwwBjOcgcg=="], @@ -530,6 +563,8 @@ "async-retry": ["async-retry@1.3.3", "", { "dependencies": { "retry": "0.13.1" } }, "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw=="], + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + "at-least-node": ["at-least-node@1.0.0", "", {}, "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg=="], "atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="], @@ -618,6 +653,8 @@ "camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="], + "camelcase-keys": ["camelcase-keys@6.2.2", "", { "dependencies": { "camelcase": "^5.3.1", "map-obj": "^4.0.0", "quick-lru": "^4.0.1" } }, "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg=="], + "capital-case": ["capital-case@1.0.4", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3", "upper-case-first": "^2.0.2" } }, "sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A=="], "chai": ["chai@4.5.0", "", { "dependencies": { "assertion-error": "^1.1.0", "check-error": "^1.0.3", "deep-eql": "^4.1.3", "get-func-name": "^2.0.2", "loupe": "^2.3.6", "pathval": "^1.1.1", "type-detect": "^4.1.0" } }, "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw=="], @@ -636,6 +673,8 @@ "ci-info": ["ci-info@2.0.0", "", {}, "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ=="], + "cids": ["cids@1.1.9", "", { "dependencies": { "multibase": "^4.0.1", "multicodec": "^3.0.1", "multihashes": "^4.0.1", "uint8arrays": "^3.0.0" } }, "sha512-l11hWRfugIcbGuTZwAM5PwpjPPjyb6UZOGwlHSnOBV5o07XhQ4gNpBN67FbODvpjyHtd+0Xs6KNvUcGBiDRsdg=="], + "cipher-base": ["cipher-base@1.0.6", "", { "dependencies": { "inherits": "^2.0.4", "safe-buffer": "^5.2.1" } }, "sha512-3Ek9H3X6pj5TgenXYtNWdaBon1tgYCaebd+XPg0keyjEbEfkD4KkmAxkQ/i1vYvxdcT5nscLBfq9VJRmCBcFSw=="], "citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="], @@ -662,6 +701,8 @@ "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + "command-exists": ["command-exists@1.2.9", "", {}, "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w=="], "command-line-args": ["command-line-args@5.2.1", "", { "dependencies": { "array-back": "^3.1.0", "find-replace": "^3.0.0", "lodash.camelcase": "^4.3.0", "typical": "^4.0.0" } }, "sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg=="], @@ -716,6 +757,8 @@ "decamelize": ["decamelize@4.0.0", "", {}, "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ=="], + "decamelize-keys": ["decamelize-keys@1.1.1", "", { "dependencies": { "decamelize": "^1.1.0", "map-obj": "^1.0.0" } }, "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg=="], + "decimal.js": ["decimal.js@10.5.0", "", {}, "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw=="], "decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="], @@ -748,6 +791,8 @@ "delay": ["delay@5.0.0", "", {}, "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw=="], + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], "detect-file": ["detect-file@1.0.0", "", {}, "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q=="], @@ -896,6 +941,8 @@ "external-editor": ["external-editor@3.1.0", "", { "dependencies": { "chardet": "^0.7.0", "iconv-lite": "^0.4.24", "tmp": "^0.0.33" } }, "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew=="], + "extract-files": ["extract-files@9.0.0", "", {}, "sha512-CvdFfHkC95B4bBBk36hcEmvdR2awOdhhVUYH6S/zrVj3477zven/fJMYg7121h4T1xHZC+tetUpubpAhxwI7hQ=="], + "eyes": ["eyes@0.1.8", "", {}, "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ=="], "fast-base64-decode": ["fast-base64-decode@1.0.0", "", {}, "sha512-qwaScUgUGBYeDNRnbc/KyllVU88Jk1pRHPStuF/lO7B0/RTRLj7U0lkdTAutlBblY08rwZDff6tNU9cjv6j//Q=="], @@ -962,6 +1009,8 @@ "foreground-child": ["foreground-child@3.3.0", "", { "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" } }, "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg=="], + "form-data": ["form-data@3.0.3", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "mime-types": "^2.1.35" } }, "sha512-q5YBMeWy6E2Un0nMGWMgI65MAKtaylxfNJGJxpGh45YDciZB4epbWpaAfImil6CPAPTYB4sh0URQNDRIZG5F2w=="], + "form-data-encoder": ["form-data-encoder@2.1.4", "", {}, "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw=="], "fp-ts": ["fp-ts@1.19.3", "", {}, "sha512-H5KQDspykdHuztLTg+ajGN0Z2qUjcEf3Ybxc6hLt0k7/zPkn29XnKnxlBPyW2XIddWrGaJBzBl4VLYOtk39yZg=="], @@ -1028,10 +1077,18 @@ "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="], + "graphql": ["graphql@16.11.0", "", {}, "sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw=="], + + "graphql-request": ["graphql-request@4.3.0", "", { "dependencies": { "cross-fetch": "^3.1.5", "extract-files": "^9.0.0", "form-data": "^3.0.0" }, "peerDependencies": { "graphql": "14 - 16" } }, "sha512-2v6hQViJvSsifK606AliqiNiijb1uwWp6Re7o0RTyH+uRTv/u7Uqm2g4Fjq/LgZIzARB38RZEvVBFOQOVdlBow=="], + "gtoken": ["gtoken@5.3.2", "", { "dependencies": { "gaxios": "^4.0.0", "google-p12-pem": "^3.1.3", "jws": "^4.0.0" } }, "sha512-gkvEKREW7dXWF8NV8pVrKfW7WqReAmjjkMBh6lNCCGOM4ucS0r0YyXXl0r/9Yj8wcW/32ISkfc8h5mPTDbtifQ=="], + "hamt-sharding": ["hamt-sharding@2.0.1", "", { "dependencies": { "sparse-array": "^1.3.1", "uint8arrays": "^3.0.0" } }, "sha512-vnjrmdXG9dDs1m/H4iJ6z0JFI2NtgsW5keRkTcM85NGak69Mkf5PHUqBz+Xs0T4sg0ppvj9O5EGAJo40FTxmmA=="], + "handlebars": ["handlebars@4.7.8", "", { "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", "source-map": "^0.6.1", "wordwrap": "^1.0.0" }, "optionalDependencies": { "uglify-js": "^3.1.4" }, "bin": { "handlebars": "bin/handlebars" } }, "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ=="], + "hard-rejection": ["hard-rejection@2.1.0", "", {}, "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA=="], + "hardhat": ["hardhat@2.22.18", "", { "dependencies": { "@ethersproject/abi": "^5.1.2", "@metamask/eth-sig-util": "^4.0.0", "@nomicfoundation/edr": "^0.7.0", "@nomicfoundation/ethereumjs-common": "4.0.4", "@nomicfoundation/ethereumjs-tx": "5.0.4", "@nomicfoundation/ethereumjs-util": "9.0.4", "@nomicfoundation/solidity-analyzer": "^0.1.0", "@sentry/node": "^5.18.1", "@types/bn.js": "^5.1.0", "@types/lru-cache": "^5.1.0", "adm-zip": "^0.4.16", "aggregate-error": "^3.0.0", "ansi-escapes": "^4.3.0", "boxen": "^5.1.2", "chokidar": "^4.0.0", "ci-info": "^2.0.0", "debug": "^4.1.1", "enquirer": "^2.3.0", "env-paths": "^2.2.0", "ethereum-cryptography": "^1.0.3", "ethereumjs-abi": "^0.6.8", "find-up": "^5.0.0", "fp-ts": "1.19.3", "fs-extra": "^7.0.1", "immutable": "^4.0.0-rc.12", "io-ts": "1.10.4", "json-stream-stringify": "^3.1.4", "keccak": "^3.0.2", "lodash": "^4.17.11", "mnemonist": "^0.38.0", "mocha": "^10.0.0", "p-map": "^4.0.0", "picocolors": "^1.1.0", "raw-body": "^2.4.1", "resolve": "1.17.0", "semver": "^6.3.0", "solc": "0.8.26", "source-map-support": "^0.5.13", "stacktrace-parser": "^0.1.10", "tinyglobby": "^0.2.6", "tsort": "0.0.1", "undici": "^5.14.0", "uuid": "^8.3.2", "ws": "^7.4.6" }, "peerDependencies": { "ts-node": "*", "typescript": "*" }, "optionalPeers": ["ts-node", "typescript"], "bin": { "hardhat": "internal/cli/bootstrap.js" } }, "sha512-2+kUz39gvMo56s75cfLBhiFedkQf+gXdrwCcz4R/5wW0oBdwiyfj2q9BIkMoaA0WIGYYMU2I1Cc4ucTunhfjzw=="], "has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="], @@ -1064,6 +1121,8 @@ "homedir-polyfill": ["homedir-polyfill@1.0.3", "", { "dependencies": { "parse-passwd": "^1.0.0" } }, "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA=="], + "hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="], + "http-cache-semantics": ["http-cache-semantics@4.1.1", "", {}, "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ=="], "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], @@ -1108,6 +1167,8 @@ "inquirer": ["inquirer@8.2.6", "", { "dependencies": { "ansi-escapes": "^4.2.1", "chalk": "^4.1.1", "cli-cursor": "^3.1.0", "cli-width": "^3.0.0", "external-editor": "^3.0.3", "figures": "^3.0.0", "lodash": "^4.17.21", "mute-stream": "0.0.8", "ora": "^5.4.1", "run-async": "^2.4.0", "rxjs": "^7.5.5", "string-width": "^4.1.0", "strip-ansi": "^6.0.0", "through": "^2.3.6", "wrap-ansi": "^6.0.1" } }, "sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg=="], + "interface-ipld-format": ["interface-ipld-format@1.0.1", "", { "dependencies": { "cids": "^1.1.6", "multicodec": "^3.0.1", "multihashes": "^4.0.2" } }, "sha512-WV/ar+KQJVoQpqRDYdo7YPGYIUHJxCuOEhdvsRpzLqoOIVCqPKdMMYmsLL1nCRsF3yYNio+PAJbCKiv6drrEAg=="], + "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], "interpret": ["interpret@2.2.0", "", {}, "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw=="], @@ -1116,6 +1177,14 @@ "ip-address": ["ip-address@9.0.5", "", { "dependencies": { "jsbn": "1.1.0", "sprintf-js": "^1.1.3" } }, "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g=="], + "ipfs-only-hash": ["ipfs-only-hash@4.0.0", "", { "dependencies": { "ipfs-unixfs-importer": "^7.0.1", "meow": "^9.0.0" }, "bin": { "ipfs-only-hash": "cli.js" } }, "sha512-TE1DZCvfw8i3gcsTq3P4TFx3cKFJ3sluu/J3XINkJhIN9OwJgNMqKA+WnKx6ByCb1IoPXsTp1KM7tupElb6SyA=="], + + "ipfs-unixfs": ["ipfs-unixfs@4.0.3", "", { "dependencies": { "err-code": "^3.0.1", "protobufjs": "^6.10.2" } }, "sha512-hzJ3X4vlKT8FQ3Xc4M1szaFVjsc1ZydN+E4VQ91aXxfpjFn9G2wsMo1EFdAXNq/BUnN5dgqIOMP5zRYr3DTsAw=="], + + "ipfs-unixfs-importer": ["ipfs-unixfs-importer@7.0.3", "", { "dependencies": { "bl": "^5.0.0", "cids": "^1.1.5", "err-code": "^3.0.1", "hamt-sharding": "^2.0.0", "ipfs-unixfs": "^4.0.3", "ipld-dag-pb": "^0.22.2", "it-all": "^1.0.5", "it-batch": "^1.0.8", "it-first": "^1.0.6", "it-parallel-batch": "^1.0.9", "merge-options": "^3.0.4", "multihashing-async": "^2.1.0", "rabin-wasm": "^0.1.4", "uint8arrays": "^2.1.2" } }, "sha512-qeFOlD3AQtGzr90sr5Tq1Bi8pT5Nr2tSI8z310m7R4JDYgZc6J1PEZO3XZQ8l1kuGoqlAppBZuOYmPEqaHcVQQ=="], + + "ipld-dag-pb": ["ipld-dag-pb@0.22.3", "", { "dependencies": { "cids": "^1.0.0", "interface-ipld-format": "^1.0.0", "multicodec": "^3.0.1", "multihashing-async": "^2.0.0", "protobufjs": "^6.10.2", "stable": "^0.1.8", "uint8arrays": "^2.0.5" } }, "sha512-dfG5C5OVAR4FEP7Al2CrHWvAyIM7UhAQrjnOYOIxXGQz5NlEj6wGX0XQf6Ru6or1na6upvV3NQfstapQG8X2rg=="], + "is": ["is@3.3.0", "", {}, "sha512-nW24QBoPcFGGHJGUwnfpI7Yc5CdqWNdsyHQszVE/z2pKHXzh7FZ5GWhJqSyaQ9wMkQnsTx+kAI8bHlCX4tKdbg=="], "is-absolute": ["is-absolute@1.0.0", "", { "dependencies": { "is-relative": "^1.0.0", "is-windows": "^1.0.1" } }, "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA=="], @@ -1226,6 +1295,14 @@ "isows": ["isows@1.0.6", "", { "peerDependencies": { "ws": "*" } }, "sha512-lPHCayd40oW98/I0uvgaHKWCSvkzY27LjWLbtzOm64yQ+G3Q5npjjbdppU65iZXkK1Zt+kH9pfegli0AYfwYYw=="], + "it-all": ["it-all@1.0.6", "", {}, "sha512-3cmCc6Heqe3uWi3CVM/k51fa/XbMFpQVzFoDsV0IZNHSQDyAXl3c4MjHkFX5kF3922OGj7Myv1nSEUgRtcuM1A=="], + + "it-batch": ["it-batch@1.0.9", "", {}, "sha512-7Q7HXewMhNFltTsAMdSz6luNhyhkhEtGGbYek/8Xb/GiqYMtwUmopE1ocPSiJKKp3rM4Dt045sNFoUu+KZGNyA=="], + + "it-first": ["it-first@1.0.7", "", {}, "sha512-nvJKZoBpZD/6Rtde6FXqwDqDZGF1sCADmr2Zoc0hZsIvnE449gRFnGctxDf09Bzc/FWnHXAdaHVIetY6lrE0/g=="], + + "it-parallel-batch": ["it-parallel-batch@1.0.11", "", { "dependencies": { "it-batch": "^1.0.9" } }, "sha512-UWsWHv/kqBpMRmyZJzlmZeoAMA0F3SZr08FBdbhtbe+MtoEBgr/ZUAKrnenhXCBrsopy76QjRH2K/V8kNdupbQ=="], + "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], "jayson": ["jayson@4.1.3", "", { "dependencies": { "@types/connect": "^3.4.33", "@types/node": "^12.12.54", "@types/ws": "^7.4.4", "JSONStream": "^1.3.5", "commander": "^2.20.3", "delay": "^5.0.0", "es6-promisify": "^5.0.0", "eyes": "^0.1.8", "isomorphic-ws": "^4.0.1", "json-stringify-safe": "^5.0.1", "uuid": "^8.3.2", "ws": "^7.5.10" }, "bin": { "jayson": "bin/jayson.js" } }, "sha512-LtXh5aYZodBZ9Fc3j6f2w+MTNcnxteMOrb+QgIouguGOulWi0lieEkOUg+HkjjFs0DGoWDds6bi4E9hpNFLulQ=="], @@ -1256,6 +1333,8 @@ "json-stream-stringify": ["json-stream-stringify@3.1.6", "", {}, "sha512-x7fpwxOkbhFCaJDJ8vb1fBY3DdSa4AlITaz+HHILQJzdPMnHEFjxPwVUi1ALIbcIxDE0PNe/0i7frnY8QnBQog=="], + "json-stringify-deterministic": ["json-stringify-deterministic@1.0.12", "", {}, "sha512-q3PN0lbUdv0pmurkBNdJH3pfFvOTL/Zp0lquqpvcjfKzt6Y0j49EPHAmVHCAS4Ceq/Y+PejWTzyiVpoY71+D6g=="], + "json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="], "json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="], @@ -1298,6 +1377,8 @@ "lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="], + "limiter": ["limiter@3.0.0", "", {}, "sha512-hev7DuXojsTFl2YwyzUJMDnZ/qBDd3yZQLSH3aD4tdL1cqfc3TMnoecEJtWFaQFdErZsKoFMBTxF/FBSkgDbEg=="], + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], "lint-staged": ["lint-staged@13.3.0", "", { "dependencies": { "chalk": "5.3.0", "commander": "11.0.0", "debug": "4.3.4", "execa": "7.2.0", "lilconfig": "2.1.0", "listr2": "6.6.1", "micromatch": "4.0.5", "pidtree": "0.6.0", "string-argv": "0.3.2", "yaml": "2.3.1" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-mPRtrYnipYYv1FEE134ufbWpeggNTo+O/UPzngoaKzbzHAthvR55am+8GfHTnqNRQVRRrYQLGW9ZyUoD7DsBHQ=="], @@ -1346,6 +1427,8 @@ "map-cache": ["map-cache@0.2.2", "", {}, "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg=="], + "map-obj": ["map-obj@4.3.0", "", {}, "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], "md5.js": ["md5.js@1.3.5", "", { "dependencies": { "hash-base": "^3.0.0", "inherits": "^2.0.1", "safe-buffer": "^5.1.2" } }, "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg=="], @@ -1356,6 +1439,10 @@ "memorystream": ["memorystream@0.3.1", "", {}, "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw=="], + "meow": ["meow@9.0.0", "", { "dependencies": { "@types/minimist": "^1.2.0", "camelcase-keys": "^6.2.2", "decamelize": "^1.2.0", "decamelize-keys": "^1.1.0", "hard-rejection": "^2.1.0", "minimist-options": "4.1.0", "normalize-package-data": "^3.0.0", "read-pkg-up": "^7.0.1", "redent": "^3.0.0", "trim-newlines": "^3.0.0", "type-fest": "^0.18.0", "yargs-parser": "^20.2.3" } }, "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ=="], + + "merge-options": ["merge-options@3.0.4", "", { "dependencies": { "is-plain-obj": "^2.1.0" } }, "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ=="], + "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], @@ -1368,10 +1455,16 @@ "micromatch": ["micromatch@4.0.5", "", { "dependencies": { "braces": "^3.0.2", "picomatch": "^2.3.1" } }, "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA=="], + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "mimic-fn": ["mimic-fn@4.0.0", "", {}, "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="], "mimic-response": ["mimic-response@4.0.0", "", {}, "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg=="], + "min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="], + "minimalistic-assert": ["minimalistic-assert@1.0.1", "", {}, "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="], "minimalistic-crypto-utils": ["minimalistic-crypto-utils@1.0.1", "", {}, "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg=="], @@ -1380,6 +1473,8 @@ "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + "minimist-options": ["minimist-options@4.1.0", "", { "dependencies": { "arrify": "^1.0.1", "is-plain-obj": "^1.1.0", "kind-of": "^6.0.3" } }, "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A=="], + "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], "minipass-collect": ["minipass-collect@2.0.1", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw=="], @@ -1408,6 +1503,18 @@ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "multibase": ["multibase@4.0.6", "", { "dependencies": { "@multiformats/base-x": "^4.0.1" } }, "sha512-x23pDe5+svdLz/k5JPGCVdfn7Q5mZVMBETiC+ORfO+sor9Sgs0smJzAjfTbM5tckeCqnaUuMYoz+k3RXMmJClQ=="], + + "multicodec": ["multicodec@3.2.1", "", { "dependencies": { "uint8arrays": "^3.0.0", "varint": "^6.0.0" } }, "sha512-+expTPftro8VAW8kfvcuNNNBgb9gPeNYV9dn+z1kJRWF2vih+/S79f2RVeIwmrJBUJ6NT9IUPWnZDQvegEh5pw=="], + + "multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], + + "multihashes": ["multihashes@4.0.3", "", { "dependencies": { "multibase": "^4.0.1", "uint8arrays": "^3.0.0", "varint": "^5.0.2" } }, "sha512-0AhMH7Iu95XjDLxIeuCOOE4t9+vQZsACyKZ9Fxw2pcsRmlX4iCn1mby0hS0bb+nQOVpdQYWPpnyusw4da5RPhA=="], + + "multihashing-async": ["multihashing-async@2.1.4", "", { "dependencies": { "blakejs": "^1.1.0", "err-code": "^3.0.0", "js-sha3": "^0.8.0", "multihashes": "^4.0.1", "murmurhash3js-revisited": "^3.0.0", "uint8arrays": "^3.0.0" } }, "sha512-sB1MiQXPSBTNRVSJc2zM157PXgDtud2nMFUEIvBrsq5Wv96sUclMRK/ecjoP1T/W61UJBqt4tCTwMkUpt2Gbzg=="], + + "murmurhash3js-revisited": ["murmurhash3js-revisited@3.0.0", "", {}, "sha512-/sF3ee6zvScXMb1XFJ8gDsSnY+X8PbOyjIuBhtgis10W2Jx4ZjIhikUCIF9c4gpJxVnQIsPAFrSwTCuAjicP6g=="], + "mute-stream": ["mute-stream@0.0.8", "", {}, "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA=="], "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], @@ -1440,6 +1547,8 @@ "nopt": ["nopt@7.2.1", "", { "dependencies": { "abbrev": "^2.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w=="], + "normalize-package-data": ["normalize-package-data@3.0.3", "", { "dependencies": { "hosted-git-info": "^4.0.1", "is-core-module": "^2.5.0", "semver": "^7.3.4", "validate-npm-package-license": "^3.0.1" } }, "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA=="], + "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], "normalize-url": ["normalize-url@8.0.1", "", {}, "sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w=="], @@ -1500,6 +1609,8 @@ "p-map": ["p-map@4.0.0", "", { "dependencies": { "aggregate-error": "^3.0.0" } }, "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ=="], + "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], + "package-json": ["package-json@8.1.1", "", { "dependencies": { "got": "^12.1.0", "registry-auth-token": "^5.0.1", "registry-url": "^6.0.0", "semver": "^7.3.7" } }, "sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA=="], "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], @@ -1558,6 +1669,8 @@ "pluralize": ["pluralize@8.0.0", "", {}, "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA=="], + "pony-cause": ["pony-cause@2.1.11", "", {}, "sha512-M7LhCsdNbNgiLYiP4WjsfLUuFmCfnjdF6jKe2R9NKl4WFN+HZPGHJZ9lnLP7f9ZnKe3U9nuWD0szirmj+migUg=="], + "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], "postinstall-postinstall": ["postinstall-postinstall@2.1.0", "", {}, "sha512-7hQX6ZlZXIoRiWNrbMQaLzUUfH+sSx39u8EJ9HYuDc1kLo9IXKWjM5RSquZN1ad5GnH8CGFM78fsAAQi3OKEEQ=="], @@ -1596,12 +1709,18 @@ "quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="], + "rabin-wasm": ["rabin-wasm@0.1.5", "", { "dependencies": { "@assemblyscript/loader": "^0.9.4", "bl": "^5.0.0", "debug": "^4.3.1", "minimist": "^1.2.5", "node-fetch": "^2.6.1", "readable-stream": "^3.6.0" }, "bin": { "rabin-wasm": "cli/bin.js" } }, "sha512-uWgQTo7pim1Rnj5TuWcCewRDTf0PEFTSlaUjWP4eY9EbLV9em08v89oCz/WO+wRxpYuO36XEHp4wgYQnAgOHzA=="], + "randombytes": ["randombytes@2.1.0", "", { "dependencies": { "safe-buffer": "^5.1.0" } }, "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ=="], "raw-body": ["raw-body@2.5.2", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } }, "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA=="], "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], + "read-pkg": ["read-pkg@5.2.0", "", { "dependencies": { "@types/normalize-package-data": "^2.4.0", "normalize-package-data": "^2.5.0", "parse-json": "^5.0.0", "type-fest": "^0.6.0" } }, "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg=="], + + "read-pkg-up": ["read-pkg-up@7.0.1", "", { "dependencies": { "find-up": "^4.1.0", "read-pkg": "^5.2.0", "type-fest": "^0.8.1" } }, "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg=="], + "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], @@ -1612,6 +1731,8 @@ "recursive-readdir": ["recursive-readdir@2.2.3", "", { "dependencies": { "minimatch": "^3.0.5" } }, "sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA=="], + "redent": ["redent@3.0.0", "", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg=="], + "reduce-flatten": ["reduce-flatten@2.0.0", "", {}, "sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w=="], "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], @@ -1742,8 +1863,18 @@ "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + "sparse-array": ["sparse-array@1.3.2", "", {}, "sha512-ZT711fePGn3+kQyLuv1fpd3rNSkNF8vd5Kv2D+qnOANeyKs3fx6bUMGWRPvgTTcYV64QMqZKZwcuaQSP3AZ0tg=="], + "sparse-bitfield": ["sparse-bitfield@3.0.3", "", { "dependencies": { "memory-pager": "^1.0.2" } }, "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ=="], + "spdx-correct": ["spdx-correct@3.2.0", "", { "dependencies": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" } }, "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA=="], + + "spdx-exceptions": ["spdx-exceptions@2.5.0", "", {}, "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w=="], + + "spdx-expression-parse": ["spdx-expression-parse@3.0.1", "", { "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q=="], + + "spdx-license-ids": ["spdx-license-ids@3.0.21", "", {}, "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg=="], + "split-array-stream": ["split-array-stream@2.0.0", "", { "dependencies": { "is-stream-ended": "^0.1.4" } }, "sha512-hmMswlVY91WvGMxs0k8MRgq8zb2mSen4FmDNc5AFiTWtrBpdZN6nwD6kROVe4vNL+ywrvbCKsWVCnEd4riELIg=="], "split-on-first": ["split-on-first@1.1.0", "", {}, "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw=="], @@ -1754,6 +1885,8 @@ "ssri": ["ssri@10.0.6", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ=="], + "stable": ["stable@0.1.8", "", {}, "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w=="], + "stacktrace-parser": ["stacktrace-parser@0.1.11", "", { "dependencies": { "type-fest": "^0.7.1" } }, "sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg=="], "statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], @@ -1792,6 +1925,8 @@ "strip-hex-prefix": ["strip-hex-prefix@1.0.0", "", { "dependencies": { "is-hex-prefixed": "1.0.0" } }, "sha512-q8d4ue7JGEiVcypji1bALTos+0pWtyGlivAWyPuTkHzuTCJqrK9sWxYQZUq6Nq3cuyv3bm734IhHvHtGGURU6A=="], + "strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="], + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], "stubs": ["stubs@3.0.0", "", {}, "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw=="], @@ -1836,6 +1971,8 @@ "treeify": ["treeify@1.1.0", "", {}, "sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A=="], + "trim-newlines": ["trim-newlines@3.0.1", "", {}, "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw=="], + "ts-api-utils": ["ts-api-utils@1.4.3", "", { "peerDependencies": { "typescript": ">=4.2.0" } }, "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw=="], "ts-command-line-args": ["ts-command-line-args@2.5.1", "", { "dependencies": { "chalk": "^4.1.0", "command-line-args": "^5.1.1", "command-line-usage": "^6.1.0", "string-format": "^2.0.0" }, "bin": { "write-markdown": "dist/write-markdown.js" } }, "sha512-H69ZwTw3rFHb5WYpQya40YAX2/w7Ut75uUECbgBIsLmM+BNuYnxsltfyyLMxy6sEeKxgijLTnQtLd0nKd6+IYw=="], @@ -1886,6 +2023,8 @@ "uglify-js": ["uglify-js@3.19.3", "", { "bin": { "uglifyjs": "bin/uglifyjs" } }, "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ=="], + "uint8arrays": ["uint8arrays@2.1.10", "", { "dependencies": { "multiformats": "^9.4.2" } }, "sha512-Q9/hhJa2836nQfEJSZTmr+pg9+cDJS9XEAp7N2Vg5MzL3bK/mkMVfjscRGYruP9jNda6MAdf4QD/y78gSzkp6A=="], + "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], "unc-path-regex": ["unc-path-regex@0.1.2", "", {}, "sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg=="], @@ -1926,6 +2065,10 @@ "v8flags": ["v8flags@4.0.1", "", {}, "sha512-fcRLaS4H/hrZk9hYwbdRM35D0U8IYMfEClhXxCivOojl+yTRAZH3Zy2sSy6qVCiGbV9YAtPssP6jaChqC9vPCg=="], + "validate-npm-package-license": ["validate-npm-package-license@3.0.4", "", { "dependencies": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" } }, "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew=="], + + "varint": ["varint@6.0.0", "", {}, "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg=="], + "viem": ["viem@2.23.5", "", { "dependencies": { "@noble/curves": "1.8.1", "@noble/hashes": "1.7.1", "@scure/bip32": "1.6.2", "@scure/bip39": "1.5.4", "abitype": "1.0.8", "isows": "1.0.6", "ox": "0.6.7", "ws": "8.18.0" }, "peerDependencies": { "typescript": ">=5.0.4" }, "optionalPeers": ["typescript"] }, "sha512-cUfBHdFQHmBlPW0loFXda0uZcoU+uJw3NRYQRwYgkrpH6PgovH8iuVqDn6t1jZk82zny4wQL54c9dCX2W9kLMg=="], "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="], @@ -2048,6 +2191,10 @@ "@aws-sdk/util-utf8-browser/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@cowprotocol/app-data/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + + "@ethereumjs/tx/ethereum-cryptography": ["ethereum-cryptography@2.2.1", "", { "dependencies": { "@noble/curves": "1.4.2", "@noble/hashes": "1.4.0", "@scure/bip32": "1.4.0", "@scure/bip39": "1.3.0" } }, "sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg=="], + "@ethereumjs/util/ethereum-cryptography": ["ethereum-cryptography@2.2.1", "", { "dependencies": { "@noble/curves": "1.4.2", "@noble/hashes": "1.4.0", "@scure/bip32": "1.4.0", "@scure/bip39": "1.3.0" } }, "sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg=="], "@ethersproject/json-wallets/aes-js": ["aes-js@3.0.0", "", {}, "sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw=="], @@ -2074,10 +2221,18 @@ "@metamask/eth-sig-util/ethereumjs-util": ["ethereumjs-util@6.2.1", "", { "dependencies": { "@types/bn.js": "^4.11.3", "bn.js": "^4.11.0", "create-hash": "^1.1.2", "elliptic": "^6.5.2", "ethereum-cryptography": "^0.1.3", "ethjs-util": "0.1.6", "rlp": "^2.2.3" } }, "sha512-W2Ktez4L01Vexijrm5EB6w7dg4n/TgpoYU4avuT5T3Vmnw/eCRtiBrJfQYS/DCSvDIOLn2k57GcHdeBcgVxAqw=="], + "@metamask/utils/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + + "@metamask/utils/@scure/base": ["@scure/base@1.2.5", "", {}, "sha512-9rE6EOVeIQzt5TSu4v+K523F8u6DhBsoZWPGKlnCshhlDhy0kJzUX4V+tr2dWmzF1GdekvThABoEQBGBQI7xZw=="], + + "@metamask/utils/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], + "@nomicfoundation/ethereumjs-tx/ethereum-cryptography": ["ethereum-cryptography@0.1.3", "", { "dependencies": { "@types/pbkdf2": "^3.0.0", "@types/secp256k1": "^4.0.1", "blakejs": "^1.1.0", "browserify-aes": "^1.2.0", "bs58check": "^2.1.2", "create-hash": "^1.2.0", "create-hmac": "^1.1.7", "hash.js": "^1.1.7", "keccak": "^3.0.0", "pbkdf2": "^3.0.17", "randombytes": "^2.1.0", "safe-buffer": "^5.1.2", "scrypt-js": "^3.0.0", "secp256k1": "^4.0.1", "setimmediate": "^1.0.5" } }, "sha512-w8/4x1SGGzc+tO97TASLja6SLd3fRIK2tLVcV2Gx4IB21hE19atll5Cq9o3d0ZmAYC/8aw0ipieTSiekAea4SQ=="], "@nomicfoundation/ethereumjs-util/ethereum-cryptography": ["ethereum-cryptography@0.1.3", "", { "dependencies": { "@types/pbkdf2": "^3.0.0", "@types/secp256k1": "^4.0.1", "blakejs": "^1.1.0", "browserify-aes": "^1.2.0", "bs58check": "^2.1.2", "create-hash": "^1.2.0", "create-hmac": "^1.1.7", "hash.js": "^1.1.7", "keccak": "^3.0.0", "pbkdf2": "^3.0.17", "randombytes": "^2.1.0", "safe-buffer": "^5.1.2", "scrypt-js": "^3.0.0", "secp256k1": "^4.0.1", "setimmediate": "^1.0.5" } }, "sha512-w8/4x1SGGzc+tO97TASLja6SLd3fRIK2tLVcV2Gx4IB21hE19atll5Cq9o3d0ZmAYC/8aw0ipieTSiekAea4SQ=="], + "@openzeppelin/merkle-tree/ethereum-cryptography": ["ethereum-cryptography@3.2.0", "", { "dependencies": { "@noble/ciphers": "1.3.0", "@noble/curves": "1.9.0", "@noble/hashes": "1.8.0", "@scure/bip32": "1.7.0", "@scure/bip39": "1.6.0" } }, "sha512-Urr5YVsalH+Jo0sYkTkv1MyI9bLYZwW8BENZCeE1QYaTHETEYx0Nv/SVsWkSqpYrzweg6d8KMY1wTjH/1m/BIg=="], + "@pnpm/network.ca-file/graceful-fs": ["graceful-fs@4.2.10", "", {}, "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="], "@sentry/core/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], @@ -2156,8 +2311,14 @@ "camel-case/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "camelcase-keys/camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="], + + "camelcase-keys/quick-lru": ["quick-lru@4.0.1", "", {}, "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g=="], + "capital-case/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "cids/uint8arrays": ["uint8arrays@3.1.1", "", { "dependencies": { "multiformats": "^9.4.2" } }, "sha512-+QJa8QRnbdXVpHYjLoTpJIdCTiw9Ir62nocClWuXIq2JIh4Uta0cQsTSpFL678p2CN8B+XSApwcU+pQEqVpKWg=="], + "cli-truncate/slice-ansi": ["slice-ansi@5.0.0", "", { "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ=="], "cli-truncate/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], @@ -2174,6 +2335,10 @@ "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "decamelize-keys/decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="], + + "decamelize-keys/map-obj": ["map-obj@1.0.1", "", {}, "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg=="], + "decompress-response/mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], "defender-base-client/axios": ["axios@0.21.4", "", { "dependencies": { "follow-redirects": "^1.14.0" } }, "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg=="], @@ -2262,6 +2427,8 @@ "google-auth-library/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], + "hamt-sharding/uint8arrays": ["uint8arrays@3.1.1", "", { "dependencies": { "multiformats": "^9.4.2" } }, "sha512-+QJa8QRnbdXVpHYjLoTpJIdCTiw9Ir62nocClWuXIq2JIh4Uta0cQsTSpFL678p2CN8B+XSApwcU+pQEqVpKWg=="], + "hardhat/fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="], "hardhat/resolve": ["resolve@1.17.0", "", { "dependencies": { "path-parse": "^1.0.6" } }, "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w=="], @@ -2272,12 +2439,18 @@ "header-case/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "hosted-git-info/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], + "inquirer/cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="], "inquirer/ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="], "inquirer/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], + "ipfs-unixfs/err-code": ["err-code@3.0.1", "", {}, "sha512-GiaH0KJUewYok+eeY05IIgjtAe4Yltygk9Wqp1V5yVWLdhf0hYZchRjNIT9bb0mSwRcIusT3cx7PJUf3zEIfUA=="], + + "ipfs-unixfs-importer/err-code": ["err-code@3.0.1", "", {}, "sha512-GiaH0KJUewYok+eeY05IIgjtAe4Yltygk9Wqp1V5yVWLdhf0hYZchRjNIT9bb0mSwRcIusT3cx7PJUf3zEIfUA=="], + "jayson/@types/node": ["@types/node@12.20.55", "", {}, "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="], "jayson/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], @@ -2310,8 +2483,16 @@ "memdown/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + "meow/decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="], + + "meow/type-fest": ["type-fest@0.18.1", "", {}, "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw=="], + "merkle-patricia-tree/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], + "minimist-options/arrify": ["arrify@1.0.1", "", {}, "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA=="], + + "minimist-options/is-plain-obj": ["is-plain-obj@1.1.0", "", {}, "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg=="], + "minipass-flush/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], "minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], @@ -2332,6 +2513,16 @@ "mongodb-connection-string-url/whatwg-url": ["whatwg-url@14.1.1", "", { "dependencies": { "tr46": "^5.0.0", "webidl-conversions": "^7.0.0" } }, "sha512-mDGf9diDad/giZ/Sm9Xi2YcyzaFpbdLpJPr+E9fSkyQ7KpQD4SdFcugkRQYzhmfI4KeV4Qpnn2sKPdo+kmsgRQ=="], + "multicodec/uint8arrays": ["uint8arrays@3.1.1", "", { "dependencies": { "multiformats": "^9.4.2" } }, "sha512-+QJa8QRnbdXVpHYjLoTpJIdCTiw9Ir62nocClWuXIq2JIh4Uta0cQsTSpFL678p2CN8B+XSApwcU+pQEqVpKWg=="], + + "multihashes/uint8arrays": ["uint8arrays@3.1.1", "", { "dependencies": { "multiformats": "^9.4.2" } }, "sha512-+QJa8QRnbdXVpHYjLoTpJIdCTiw9Ir62nocClWuXIq2JIh4Uta0cQsTSpFL678p2CN8B+XSApwcU+pQEqVpKWg=="], + + "multihashes/varint": ["varint@5.0.2", "", {}, "sha512-lKxKYG6H03yCZUpAGOPOsMcGxd1RHCu1iKvEHYDPmTyq2HueGhD73ssNBqqQWfvYs04G9iUFRvmAVLW20Jw6ow=="], + + "multihashing-async/err-code": ["err-code@3.0.1", "", {}, "sha512-GiaH0KJUewYok+eeY05IIgjtAe4Yltygk9Wqp1V5yVWLdhf0hYZchRjNIT9bb0mSwRcIusT3cx7PJUf3zEIfUA=="], + + "multihashing-async/uint8arrays": ["uint8arrays@3.1.1", "", { "dependencies": { "multiformats": "^9.4.2" } }, "sha512-+QJa8QRnbdXVpHYjLoTpJIdCTiw9Ir62nocClWuXIq2JIh4Uta0cQsTSpFL678p2CN8B+XSApwcU+pQEqVpKWg=="], + "no-case/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "node-plop/globby": ["globby@13.2.2", "", { "dependencies": { "dir-glob": "^3.0.1", "fast-glob": "^3.3.0", "ignore": "^5.2.4", "merge2": "^1.4.1", "slash": "^4.0.0" } }, "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w=="], @@ -2370,6 +2561,14 @@ "rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], + "read-pkg/normalize-package-data": ["normalize-package-data@2.5.0", "", { "dependencies": { "hosted-git-info": "^2.1.4", "resolve": "^1.10.0", "semver": "2 || 3 || 4 || 5", "validate-npm-package-license": "^3.0.1" } }, "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA=="], + + "read-pkg/type-fest": ["type-fest@0.6.0", "", {}, "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg=="], + + "read-pkg-up/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + + "read-pkg-up/type-fest": ["type-fest@0.8.1", "", {}, "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA=="], + "resolve-dir/global-modules": ["global-modules@1.0.0", "", { "dependencies": { "global-prefix": "^1.0.1", "is-windows": "^1.0.1", "resolve-dir": "^1.0.0" } }, "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg=="], "restore-cursor/onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], @@ -2548,6 +2747,16 @@ "zx/@types/node": ["@types/node@22.7.5", "", { "dependencies": { "undici-types": "~6.19.2" } }, "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ=="], + "@cowprotocol/app-data/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "@ethereumjs/tx/ethereum-cryptography/@noble/curves": ["@noble/curves@1.4.2", "", { "dependencies": { "@noble/hashes": "1.4.0" } }, "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw=="], + + "@ethereumjs/tx/ethereum-cryptography/@noble/hashes": ["@noble/hashes@1.4.0", "", {}, "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg=="], + + "@ethereumjs/tx/ethereum-cryptography/@scure/bip32": ["@scure/bip32@1.4.0", "", { "dependencies": { "@noble/curves": "~1.4.0", "@noble/hashes": "~1.4.0", "@scure/base": "~1.1.6" } }, "sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg=="], + + "@ethereumjs/tx/ethereum-cryptography/@scure/bip39": ["@scure/bip39@1.3.0", "", { "dependencies": { "@noble/hashes": "~1.4.0", "@scure/base": "~1.1.6" } }, "sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ=="], + "@ethereumjs/util/ethereum-cryptography/@noble/curves": ["@noble/curves@1.4.2", "", { "dependencies": { "@noble/hashes": "1.4.0" } }, "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw=="], "@ethereumjs/util/ethereum-cryptography/@noble/hashes": ["@noble/hashes@1.4.0", "", {}, "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg=="], @@ -2582,6 +2791,14 @@ "@metamask/eth-sig-util/ethereumjs-util/ethereum-cryptography": ["ethereum-cryptography@0.1.3", "", { "dependencies": { "@types/pbkdf2": "^3.0.0", "@types/secp256k1": "^4.0.1", "blakejs": "^1.1.0", "browserify-aes": "^1.2.0", "bs58check": "^2.1.2", "create-hash": "^1.2.0", "create-hmac": "^1.1.7", "hash.js": "^1.1.7", "keccak": "^3.0.0", "pbkdf2": "^3.0.17", "randombytes": "^2.1.0", "safe-buffer": "^5.1.2", "scrypt-js": "^3.0.0", "secp256k1": "^4.0.1", "setimmediate": "^1.0.5" } }, "sha512-w8/4x1SGGzc+tO97TASLja6SLd3fRIK2tLVcV2Gx4IB21hE19atll5Cq9o3d0ZmAYC/8aw0ipieTSiekAea4SQ=="], + "@openzeppelin/merkle-tree/ethereum-cryptography/@noble/curves": ["@noble/curves@1.9.0", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-7YDlXiNMdO1YZeH6t/kvopHHbIZzlxrCV9WLqCY6QhcXOoXiNCMDqJIglZ9Yjx5+w7Dz30TITFrlTjnRg7sKEg=="], + + "@openzeppelin/merkle-tree/ethereum-cryptography/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + + "@openzeppelin/merkle-tree/ethereum-cryptography/@scure/bip32": ["@scure/bip32@1.7.0", "", { "dependencies": { "@noble/curves": "~1.9.0", "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw=="], + + "@openzeppelin/merkle-tree/ethereum-cryptography/@scure/bip39": ["@scure/bip39@1.6.0", "", { "dependencies": { "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A=="], + "@sentry/node/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], "@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/types@5.62.0", "", {}, "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ=="], @@ -2708,6 +2925,12 @@ "patch-package/cross-spawn/which": ["which@1.3.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "which": "./bin/which" } }, "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ=="], + "read-pkg-up/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + + "read-pkg/normalize-package-data/hosted-git-info": ["hosted-git-info@2.8.9", "", {}, "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw=="], + + "read-pkg/normalize-package-data/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], + "resolve-dir/global-modules/global-prefix": ["global-prefix@1.0.2", "", { "dependencies": { "expand-tilde": "^2.0.2", "homedir-polyfill": "^1.0.1", "ini": "^1.3.4", "is-windows": "^1.0.1", "which": "^1.2.14" } }, "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg=="], "restore-cursor/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], @@ -2836,6 +3059,10 @@ "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + "@ethereumjs/tx/ethereum-cryptography/@scure/bip32/@scure/base": ["@scure/base@1.1.9", "", {}, "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg=="], + + "@ethereumjs/tx/ethereum-cryptography/@scure/bip39/@scure/base": ["@scure/base@1.1.9", "", {}, "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg=="], + "@ethereumjs/util/ethereum-cryptography/@scure/bip32/@scure/base": ["@scure/base@1.1.9", "", {}, "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg=="], "@ethereumjs/util/ethereum-cryptography/@scure/bip39/@scure/base": ["@scure/base@1.1.9", "", {}, "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg=="], @@ -2846,6 +3073,10 @@ "@metamask/eth-sig-util/ethereumjs-util/@types/bn.js/@types/node": ["@types/node@22.7.5", "", { "dependencies": { "undici-types": "~6.19.2" } }, "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ=="], + "@openzeppelin/merkle-tree/ethereum-cryptography/@scure/bip32/@scure/base": ["@scure/base@1.2.5", "", {}, "sha512-9rE6EOVeIQzt5TSu4v+K523F8u6DhBsoZWPGKlnCshhlDhy0kJzUX4V+tr2dWmzF1GdekvThABoEQBGBQI7xZw=="], + + "@openzeppelin/merkle-tree/ethereum-cryptography/@scure/bip39/@scure/base": ["@scure/base@1.2.5", "", {}, "sha512-9rE6EOVeIQzt5TSu4v+K523F8u6DhBsoZWPGKlnCshhlDhy0kJzUX4V+tr2dWmzF1GdekvThABoEQBGBQI7xZw=="], + "@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/globby/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], "@typescript-eslint/utils/@typescript-eslint/typescript-estree/globby/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], @@ -2870,6 +3101,8 @@ "patch-package/cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "read-pkg-up/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + "resolve-dir/global-modules/global-prefix/which": ["which@1.3.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "which": "./bin/which" } }, "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ=="], "sc-istanbul/js-yaml/argparse/sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], @@ -3004,6 +3237,8 @@ "inquirer/cli-cursor/restore-cursor/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + "read-pkg-up/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + "resolve-dir/global-modules/global-prefix/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "solidity-coverage/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], diff --git a/deployments/_deployments_log_file.json b/deployments/_deployments_log_file.json index 6eb05a54b..2e87d1046 100644 --- a/deployments/_deployments_log_file.json +++ b/deployments/_deployments_log_file.json @@ -31326,5 +31326,21 @@ ] } } + }, + "Patcher": { + "arbitrum": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0xE65b50EcF482f97f53557f0E02946aa27f8839EC", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-05-08 17:01:23", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "", + "VERIFIED": "true" + } + ] + } + } } } diff --git a/deployments/arbitrum.diamond.staging.json b/deployments/arbitrum.diamond.staging.json index fe40cd4b9..bbcb2af56 100644 --- a/deployments/arbitrum.diamond.staging.json +++ b/deployments/arbitrum.diamond.staging.json @@ -173,7 +173,8 @@ "ReceiverChainflip": "", "ReceiverStargateV2": "", "RelayerCelerIM": "0xa1Ed8783AC96385482092b82eb952153998e9b70", - "TokenWrapper": "0xF63b27AE2Dc887b88f82E2Cc597d07fBB2E78E70" + "TokenWrapper": "0xF63b27AE2Dc887b88f82E2Cc597d07fBB2E78E70", + "Patcher": "0x3971A968c03cd9640239C937F8d30D024840E691" } } } \ No newline at end of file diff --git a/deployments/arbitrum.staging.json b/deployments/arbitrum.staging.json index 32c1e5ba5..54d03c55c 100644 --- a/deployments/arbitrum.staging.json +++ b/deployments/arbitrum.staging.json @@ -53,5 +53,6 @@ "RelayFacet": "0x3cf7dE0e31e13C93c8Aada774ADF1C7eD58157f5", "GlacisFacet": "0xF82830B952Bc60b93206FA22f1cD4770cedb2840", "GasZipFacet": "0x37f3F3E9d909fB1163448C511193b8481e541C62", - "ChainflipFacet": "0xDd661337B48BEA5194F6d26F2C59fF0855E15289" + "ChainflipFacet": "0xDd661337B48BEA5194F6d26F2C59fF0855E15289", + "Patcher": "0xE65b50EcF482f97f53557f0E02946aa27f8839EC" } \ No newline at end of file diff --git a/package.json b/package.json index 3754bfe11..ca4988112 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ }, "dependencies": { "@arbitrum/sdk": "^3.0.0", + "@cowprotocol/cow-sdk": "^5.10.3", "@hop-protocol/sdk": "0.0.1-beta.310", "@layerzerolabs/lz-v2-utilities": "^2.3.21", "@octokit/rest": "^21.0.1", diff --git a/script/demoScripts/demoPatcher.ts b/script/demoScripts/demoPatcher.ts new file mode 100644 index 000000000..3e934a0f2 --- /dev/null +++ b/script/demoScripts/demoPatcher.ts @@ -0,0 +1,928 @@ +import { + parseAbi, + encodeFunctionData, + formatUnits, + parseUnits, + zeroAddress, + getCreate2Address, + keccak256, + concat, + encodeAbiParameters, + parseAbiParameters, + TypedDataEncoder, + SignTypedDataParameters, + hashTypedData, +} from 'viem' +import { privateKeyToAccount } from 'viem/accounts' +import { arbitrum } from 'viem/chains' +import { createPublicClient, createWalletClient, http } from 'viem' +import { + SupportedChainId, + OrderKind, + TradingSdk, + SwapAdvancedSettings, + TradeParameters, + TraderParameters, +} from '@cowprotocol/cow-sdk' +import { getEnvVar } from './utils/demoScriptHelpers' +import deploymentsArbitrum from '../../deployments/arbitrum.staging.json' + +// Import necessary types and utilities +import { + logKeyValue, + logSectionHeader, + logSuccess, +} from '../../dump/src/lib/utils/logging' +import { truncateCalldata } from '../../dump/src/lib/utils/formatting' +import { left, Result, right } from '../../dump/src/lib/result' + +// Define CoW Protocol constants +const COW_SHED_FACTORY = '0x00E989b87700514118Fa55326CD1cCE82faebEF6' +const COW_SHED_IMPLEMENTATION = '0x2CFFA8cf11B90C9F437567b86352169dF4009F73' + +// Define interfaces and types +interface Token { + readonly address: string + readonly decimals: number +} + +interface ICall { + target: string + callData: string + value: bigint + allowFailure: boolean + isDelegateCall: boolean +} + +interface PatchOperation { + valueSource: string + valueGetter: string + valueToReplace: string | number | bigint +} + +type TransactionIntent = + | { + readonly type: 'regular' + readonly targetAddress: string + readonly callData: string + readonly nativeValue?: bigint + readonly allowFailure?: boolean + readonly isDelegateCall?: boolean + } + | { + readonly type: 'dynamicValue' + readonly patcherAddress: string + readonly valueSource: string + readonly valueGetter: string + readonly targetAddress: string + readonly callDataToPatch: string + readonly valueToReplace: string | number | bigint + readonly nativeValue?: bigint + readonly delegateCall?: boolean + } + | { + readonly type: 'multiPatch' + readonly patcherAddress: string + readonly targetAddress: string + readonly callDataToPatch: string + readonly patchOperations: readonly PatchOperation[] + readonly nativeValue?: bigint + readonly delegateCall?: boolean + } + +// ABIs +const ERC20_ABI = parseAbi([ + 'function balanceOf(address owner) view returns (uint256)', + 'function transfer(address to, uint256 amount) returns (bool)', + 'function approve(address spender, uint256 amount) returns (bool)', +]) + +const DUMMY_TARGET_ABI = parseAbi([ + 'function deposit(address token, uint256 amount, bool flag) returns (bool)', + 'function multiDeposit(address token, uint256 amount1, uint256 amount2, bool flag) returns (bool)', + 'function getDoubledBalance(address token, address account) view returns (uint256)', +]) + +const PATCHER_ABI = parseAbi([ + 'function dynamicValuePatch(address valueSource, bytes valueGetter, address targetAddress, bytes callDataToPatch, bytes32 valueToReplace, uint256 offset, bool delegateCall) payable returns (bytes memory)', + 'function multiPatch(address targetAddress, bytes callDataToPatch, (address valueSource, bytes valueGetter, bytes32 valueToReplace, uint256 offset)[] patchOperations, bool delegateCall) payable returns (bytes memory)', +]) + +// CoW Protocol constants and ABIs +console.log('COW_SHED_FACTORY:', COW_SHED_FACTORY) +console.log('COW_SHED_IMPLEMENTATION:', COW_SHED_IMPLEMENTATION) +const PROXY_CREATION_CODE = + '0x60a034608e57601f61037138819003918201601f19168301916001600160401b038311848410176093578084926040948552833981010312608e57604b602060458360a9565b920160a9565b6080527f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc556040516102b490816100bd8239608051818181608f01526101720152f35b600080fd5b634e487b7160e01b600052604160045260246000fd5b51906001600160a01b0382168203608e5756fe60806040526004361015610018575b3661019457610194565b6000803560e01c908163025b22bc1461003b575063f851a4400361000e5761010d565b3461010a5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261010a5773ffffffffffffffffffffffffffffffffffffffff60043581811691828203610106577f0000000000000000000000000000000000000000000000000000000000000000163314600014610101577f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc557fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b8280a280f35b61023d565b8380fd5b80fd5b346101645760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610164576020610146610169565b73ffffffffffffffffffffffffffffffffffffffff60405191168152f35b600080fd5b333003610101577f000000000000000000000000000000000000000000000000000000000000000090565b60ff7f68df44b1011761f481358c0f49a711192727fb02c377d697bcb0ea8ff8393ac0541615806101ef575b1561023d5760046040517ff92ee8a9000000000000000000000000000000000000000000000000000000008152fd5b507f400ada75000000000000000000000000000000000000000000000000000000007fffffffff000000000000000000000000000000000000000000000000000000006000351614156101c0565b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc546000808092368280378136915af43d82803e1561027a573d90f35b3d90fdfea2646970667358221220c7c26ff3040b96a28e96d6d27b743972943aeaef81cc821544c5fe1e24f9b17264736f6c63430008190033' + +const FACTORY_ABI = parseAbi([ + 'function executeHooks((address target, uint256 value, bytes callData, bool allowFailure, bool isDelegateCall)[] calls, bytes32 nonce, uint256 deadline, address user, bytes signature) external', +]) + +const SHED_ABI = parseAbi([ + 'function executeHooks((address target, uint256 value, bytes callData, bool allowFailure, bool isDelegateCall)[] calls, bytes32 nonce, uint256 deadline, bytes signature) external', +]) + +// Helper functions +const encodeBalanceOfCall = (account: string): string => { + return encodeFunctionData({ + abi: ERC20_ABI, + functionName: 'balanceOf', + args: [account], + }) +} + +const encodeTransferCall = (to: string, amount: string): string => { + return encodeFunctionData({ + abi: ERC20_ABI, + functionName: 'transfer', + args: [to, BigInt(amount)], + }) +} + +const encodeGetDoubledBalanceCall = ( + token: string, + account: string +): string => { + return encodeFunctionData({ + abi: DUMMY_TARGET_ABI, + functionName: 'getDoubledBalance', + args: [token, account], + }) +} + +// Process transaction intent +const processTransactionIntent = ( + intent: TransactionIntent, + verbose: boolean +): Result => { + switch (intent.type) { + case 'regular': { + const call: ICall = { + target: intent.targetAddress, + callData: intent.callData, + value: intent.nativeValue ?? BigInt(0), + allowFailure: intent.allowFailure ?? false, + isDelegateCall: intent.isDelegateCall ?? false, + } + + if (verbose) { + logKeyValue('Call data', truncateCalldata(intent.callData, 10)) + logSuccess('Regular call created') + } + + return right(call) + } + + case 'dynamicValue': { + try { + if (!intent.patcherAddress || intent.patcherAddress === zeroAddress) { + return left(new Error('patcherAddress must be provided')) + } + + if (verbose) { + logSectionHeader(`Creating dynamic value patch call`) + logKeyValue('Call', truncateCalldata(intent.callDataToPatch, 10)) + logKeyValue('Value Source', intent.valueSource) + logKeyValue('Value Getter', truncateCalldata(intent.valueGetter, 10)) + logKeyValue('Target', intent.targetAddress) + logKeyValue('Value to replace', intent.valueToReplace.toString()) + logKeyValue( + 'Native Value', + (intent.nativeValue || BigInt(0)).toString() + ) + } + + // Create dynamic patch call + const callData = encodeFunctionData({ + abi: PATCHER_ABI, + functionName: 'dynamicValuePatch', + args: [ + intent.valueSource, + intent.valueGetter, + intent.targetAddress, + intent.callDataToPatch, + `0x${intent.valueToReplace + .toString() + .padStart(64, '0')}` as `0x${string}`, + BigInt(0), // offset + intent.delegateCall ?? false, + ], + }) + + const call: ICall = { + target: intent.patcherAddress, + callData, + value: intent.nativeValue ?? BigInt(0), + allowFailure: false, + isDelegateCall: false, + } + + if (verbose) { + logSuccess('Dynamic value patch call created') + } + + return right(call) + } catch (error) { + return left( + new Error(`Failed to create dynamic value patch: ${error.message}`) + ) + } + } + + case 'multiPatch': { + try { + if (verbose) { + logSectionHeader( + `Creating multi-patch call with ${intent.patchOperations.length} operations` + ) + logKeyValue( + 'Call data to patch', + truncateCalldata(intent.callDataToPatch, 10) + ) + logKeyValue('Target', intent.targetAddress) + } + + // Format patch operations for the contract call + const patchOperations = intent.patchOperations.map((op) => { + return { + valueSource: op.valueSource, + valueGetter: op.valueGetter, + valueToReplace: `0x${op.valueToReplace + .toString() + .padStart(64, '0')}` as `0x${string}`, + offset: BigInt(0), + } + }) + + // Create multi-patch call + const callData = encodeFunctionData({ + abi: PATCHER_ABI, + functionName: 'multiPatch', + args: [ + intent.targetAddress, + intent.callDataToPatch, + patchOperations, + intent.delegateCall ?? false, + ], + }) + + const call: ICall = { + target: intent.patcherAddress, + callData, + value: intent.nativeValue ?? BigInt(0), + allowFailure: false, + isDelegateCall: false, + } + + if (verbose) { + logSuccess('Multi-patch call created') + } + + return right(call) + } catch (error) { + return left(new Error(`Failed to create multi-patch: ${error.message}`)) + } + } + } +} + +// CoW Shed SDK implementation with Viem +class CowShedSdk { + private factoryAddress: string + private implementationAddress: string + private proxyCreationCode: string + private chainId: number + + constructor(options: { + factoryAddress: string + implementationAddress: string + proxyCreationCode?: string + chainId: number + }) { + this.factoryAddress = options.factoryAddress + this.implementationAddress = options.implementationAddress + this.proxyCreationCode = options.proxyCreationCode || PROXY_CREATION_CODE + this.chainId = options.chainId + } + + computeProxyAddress(user: string): string { + console.log('Computing proxy address for user:', user) + + if (!user || user === 'undefined' || !user.startsWith('0x')) { + throw new Error(`Invalid user address: ${user}`) + } + + // Ensure addresses are properly formatted + if (!this.factoryAddress || !this.factoryAddress.startsWith('0x')) { + throw new Error(`Invalid factory address: ${this.factoryAddress}`) + } + + if ( + !this.implementationAddress || + !this.implementationAddress.startsWith('0x') + ) { + throw new Error( + `Invalid implementation address: ${this.implementationAddress}` + ) + } + + const factoryAddress = this.factoryAddress as `0x${string}` + const implementationAddress = this.implementationAddress as `0x${string}` + const userAddress = user as `0x${string}` + + console.log('Using addresses:', { + factoryAddress, + implementationAddress, + userAddress, + }) + + try { + const salt = encodeAbiParameters(parseAbiParameters('address'), [ + userAddress, + ]) + console.log('Salt:', salt) + + const initCode = concat([ + this.proxyCreationCode as `0x${string}`, + encodeAbiParameters(parseAbiParameters('address, address'), [ + implementationAddress, + userAddress, + ]), + ]) + console.log( + 'InitCode (first 66 chars):', + initCode.substring(0, 66) + '...' + ) + + const initCodeHash = keccak256(initCode) + console.log('InitCodeHash:', initCodeHash) + + // Log each parameter individually to identify which one might be undefined + console.log('getCreate2Address parameters:', { + factoryAddress: factoryAddress, + factoryAddressType: typeof factoryAddress, + salt: salt, + saltType: typeof salt, + bytecodeHash: initCodeHash, + bytecodeHashType: typeof initCodeHash, + }) + + // Validate each parameter + if (!factoryAddress || factoryAddress === 'undefined') { + throw new Error('factoryAddress is undefined') + } + if (!salt || salt === 'undefined') { + throw new Error('salt is undefined') + } + if (!initCodeHash || initCodeHash === 'undefined') { + throw new Error('initCodeHash is undefined') + } + + // Ensure all parameters are properly formatted as hex strings + const formattedFactoryAddress = factoryAddress.startsWith('0x') + ? factoryAddress + : (`0x${factoryAddress}` as `0x${string}`) + const formattedSalt = salt.startsWith('0x') + ? salt + : (`0x${salt}` as `0x${string}`) + const formattedInitCodeHash = initCodeHash.startsWith('0x') + ? initCodeHash + : (`0x${initCodeHash}` as `0x${string}`) + + const proxyAddress = getCreate2Address({ + factoryAddress: formattedFactoryAddress, + salt: formattedSalt, + bytecodeHash: formattedInitCodeHash, + }) + + console.log('Computed proxy address:', proxyAddress) + return proxyAddress + } catch (error) { + console.error('Error in computeProxyAddress:', error) + throw error // Re-throw the error to propagate it + } + } + + hashToSignWithUser( + calls: readonly ICall[], + nonce: `0x${string}`, + deadline: bigint, + user: string + ): `0x${string}` { + const proxy = this.computeProxyAddress(user) + return this.hashToSign(calls, nonce, deadline, proxy) + } + + private hashToSign( + calls: readonly ICall[], + nonce: `0x${string}`, + deadline: bigint, + proxy: string + ): `0x${string}` { + const domain = { + name: 'COWShed', + version: '1.0.0', + chainId: this.chainId, + verifyingContract: proxy as `0x${string}`, + } + + const types = { + ExecuteHooks: [ + { name: 'calls', type: 'Call[]' }, + { name: 'nonce', type: 'bytes32' }, + { name: 'deadline', type: 'uint256' }, + ], + Call: [ + { name: 'target', type: 'address' }, + { name: 'value', type: 'uint256' }, + { name: 'callData', type: 'bytes' }, + { name: 'allowFailure', type: 'bool' }, + { name: 'isDelegateCall', type: 'bool' }, + ], + } + + const message = { + calls: calls.map((call) => ({ + target: call.target as `0x${string}`, + value: call.value, + callData: call.callData as `0x${string}`, + allowFailure: call.allowFailure, + isDelegateCall: call.isDelegateCall, + })), + nonce, + deadline, + } + + return hashTypedData({ + domain, + types, + primaryType: 'ExecuteHooks', + message, + }) + } + + static encodeExecuteHooksForFactory( + calls: readonly ICall[], + nonce: `0x${string}`, + deadline: bigint, + user: string, + signature: `0x${string}` + ): string { + return encodeFunctionData({ + abi: FACTORY_ABI, + functionName: 'executeHooks', + args: [ + calls.map((call) => ({ + target: call.target as `0x${string}`, + value: call.value, + callData: call.callData as `0x${string}`, + allowFailure: call.allowFailure, + isDelegateCall: call.isDelegateCall, + })), + nonce, + deadline, + user as `0x${string}`, + signature, + ], + }) + } +} + +// Real implementation of CoW Protocol functions +const calculateCowShedProxyAddress = async ( + chainId: number, + owner: string +): Promise => { + console.log('calculateCowShedProxyAddress called with:', { chainId, owner }) + + if (!owner || owner === 'undefined') { + throw new Error('Owner address is undefined or empty') + } + + console.log('Using COW_SHED_FACTORY:', COW_SHED_FACTORY) + console.log('Using COW_SHED_IMPLEMENTATION:', COW_SHED_IMPLEMENTATION) + + // Validate inputs before creating the SDK + if ( + !COW_SHED_FACTORY || + COW_SHED_FACTORY === 'undefined' || + COW_SHED_FACTORY === zeroAddress + ) { + throw new Error('COW_SHED_FACTORY is undefined or invalid') + } + + if ( + !COW_SHED_IMPLEMENTATION || + COW_SHED_IMPLEMENTATION === 'undefined' || + COW_SHED_IMPLEMENTATION === zeroAddress + ) { + throw new Error('COW_SHED_IMPLEMENTATION is undefined or invalid') + } + + const shedSDK = new CowShedSdk({ + factoryAddress: COW_SHED_FACTORY, + implementationAddress: COW_SHED_IMPLEMENTATION, + chainId, + }) + + const proxyAddress = shedSDK.computeProxyAddress(owner) + console.log('Computed proxy address:', proxyAddress) + + return proxyAddress +} + +const setupCowShedPostHooks = async ( + chainId: number, + walletClient: any, + calls: ICall[], + verbose: boolean +): Promise<{ shedDeterministicAddress: string; postHooks: any[] }> => { + const account = walletClient.account + const signerAddress = account.address + + console.log('Using COW_SHED_FACTORY:', COW_SHED_FACTORY) + console.log('Using COW_SHED_IMPLEMENTATION:', COW_SHED_IMPLEMENTATION) + + const shedSDK = new CowShedSdk({ + factoryAddress: COW_SHED_FACTORY, + implementationAddress: COW_SHED_IMPLEMENTATION, + chainId, + }) + + // Generate nonce and deadline for CoW Shed + const nonce = `0x${Array.from({ length: 64 }, () => + Math.floor(Math.random() * 16).toString(16) + ).join('')}` as `0x${string}` + const deadline = BigInt(Math.floor(Date.now() / 1000) + 7200) // now + 2 hours + const shedDeterministicAddress = shedSDK.computeProxyAddress(signerAddress) + + // Sign the post hooks + const hashToSign = shedSDK.hashToSignWithUser( + calls, + nonce, + deadline, + signerAddress + ) + + // Sign with the wallet + const signature = await walletClient.signMessage({ + account, + message: { raw: hashToSign }, + }) + + // Encode the post hooks call data + const shedEncodedPostHooksCallData = CowShedSdk.encodeExecuteHooksForFactory( + calls, + nonce, + deadline, + signerAddress, + signature + ) + + // Create the post hooks + const postHooks = [ + { + target: COW_SHED_FACTORY, + callData: shedEncodedPostHooksCallData, + gasLimit: '2000000', + }, + ] + + // Log information if verbose is true + if (verbose) { + logKeyValue('CoW-Shed deterministic address', shedDeterministicAddress) + logSuccess('Post hook ready for execution through CoW Protocol') + logKeyValue( + 'CoW-Shed encoded post-hook', + truncateCalldata(shedEncodedPostHooksCallData, 25) + ) + } + + return { + shedDeterministicAddress, + postHooks, + } +} + +const cowFlow = async ( + chainId: number, + walletClient: any, + fromToken: Token, + toToken: Token, + fromAmount: bigint, + postReceiver: string, + preHooks: readonly any[], + postHooks: readonly any[] +): Promise> => { + try { + // Create trader parameters with a signer adapter for Viem wallet + const traderParams: TraderParameters = { + chainId, + signer: { + signMessage: async (message: string) => { + return walletClient.signMessage({ + account: walletClient.account, + message: { raw: message as `0x${string}` }, + }) + }, + getAddress: async () => walletClient.account.address, + }, + appCode: 'LI.FI Patcher Demo', + } + + // Create trade parameters + const params: TradeParameters = { + kind: OrderKind.SELL, // SELL order (specify amount in) + sellToken: fromToken.address, + sellTokenDecimals: fromToken.decimals, + buyToken: toToken.address, + buyTokenDecimals: toToken.decimals, + amount: fromAmount.toString(), + receiver: postReceiver, + } + + // Create advanced settings with hooks + const advancedSettings: SwapAdvancedSettings = { + appData: { + metadata: { + hooks: { + version: '1', + pre: [...preHooks], + post: [...postHooks], + }, + }, + }, + } + + // Initialize the SDK and post the order + const sdk = new TradingSdk(traderParams) + const hash = await sdk.postSwapOrder(params, advancedSettings) + + return right(hash) + } catch (error) { + return left(new Error(`Failed to execute CoW flow: ${error.message}`)) + } +} + +// Main demo function +const demoPatcher = async (): Promise> => { + try { + logSectionHeader('Running Patcher Example with Post-Swap Flow') + + // Setup environment + console.log('Setting up environment...') + + // Get private key and ensure it has the correct format + const privateKeyRaw = process.env.PRIVATE_KEY + if (!privateKeyRaw) { + throw new Error('PRIVATE_KEY environment variable is not set') + } + + // Ensure the private key has the correct format (0x prefix) + const privateKey = privateKeyRaw.startsWith('0x') + ? (privateKeyRaw as `0x${string}`) + : (`0x${privateKeyRaw}` as `0x${string}`) + + console.log('Creating account...') + const account = privateKeyToAccount(privateKey) + console.log('Account address:', account.address) + + console.log('Creating clients...') + const rpcUrl = process.env.ETH_NODE_URI_ARBITRUM + if (!rpcUrl) { + throw new Error('ETH_NODE_URI_ARBITRUM environment variable is not set') + } + + const publicClient = createPublicClient({ + chain: arbitrum, + transport: http(rpcUrl), + }) + + const walletClient = createWalletClient({ + chain: arbitrum, + transport: http(rpcUrl), + account, + }) + + console.log('Getting contract addresses...') + // Get the Patcher address from deployments + const patcherAddress = deploymentsArbitrum.Patcher + if ( + !patcherAddress || + patcherAddress === '0x0000000000000000000000000000000000000000' + ) { + throw new Error( + 'Patcher address not found in deployments or is zero address' + ) + } + console.log('Patcher address:', patcherAddress) + + // Use a mock address for the dummy target + const dummyTargetAddress = '0x0000000000000000000000000000000000000001' + + const baseValuePlaceholder = '1000000000000000000' + const doubledValuePlaceholder = '2000000000000000000' + + const originalTokenOwner = account.address + // Use the correct Arbitrum chain ID + const chainId = 42161 + + // Define token information + console.log('Setting up token information...') + const fromToken = { + address: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1', // WETH on Arbitrum + decimals: 18, + } + + const toToken = { + address: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', // USDC on Arbitrum + decimals: 6, + } + + // Calculate CoW Shed proxy address + console.log('Calculating CoW Shed proxy address...') + const shedProxyAddress = await calculateCowShedProxyAddress( + chainId, + originalTokenOwner + ) + console.log('Shed proxy address:', shedProxyAddress) + + // Define transaction intents + console.log('Defining transaction intents...') + const transactionIntents: readonly TransactionIntent[] = [ + { + type: 'dynamicValue', + patcherAddress: patcherAddress, + valueSource: toToken.address, + valueGetter: encodeBalanceOfCall(shedProxyAddress), + targetAddress: dummyTargetAddress, + callDataToPatch: encodeFunctionData({ + abi: DUMMY_TARGET_ABI, + functionName: 'deposit', + args: [toToken.address, BigInt(baseValuePlaceholder), true], + }), + valueToReplace: baseValuePlaceholder, + nativeValue: BigInt(0), + }, + { + type: 'multiPatch', + patcherAddress: patcherAddress, + targetAddress: dummyTargetAddress, + callDataToPatch: encodeFunctionData({ + abi: DUMMY_TARGET_ABI, + functionName: 'multiDeposit', + args: [ + toToken.address, + BigInt(baseValuePlaceholder), + BigInt(doubledValuePlaceholder), + true, + ], + }), + patchOperations: [ + { + valueSource: toToken.address, + valueGetter: encodeBalanceOfCall(shedProxyAddress), + valueToReplace: baseValuePlaceholder, + }, + { + valueSource: dummyTargetAddress, + valueGetter: encodeGetDoubledBalanceCall( + toToken.address, + shedProxyAddress + ), + valueToReplace: doubledValuePlaceholder, + }, + ], + nativeValue: BigInt(0), + }, + { + type: 'dynamicValue', + patcherAddress: patcherAddress, + valueSource: toToken.address, + valueGetter: encodeBalanceOfCall(shedProxyAddress), + targetAddress: toToken.address, + callDataToPatch: encodeTransferCall( + originalTokenOwner, + baseValuePlaceholder + ), + valueToReplace: baseValuePlaceholder, + nativeValue: BigInt(0), + }, + ] + + logKeyValue('Patcher Address', patcherAddress) + logKeyValue('From Token', fromToken.address) + logKeyValue('To Token', toToken.address) + + // Process transaction intents + console.log('Processing transaction intents...') + const processedIntents = transactionIntents.map((intent, index) => { + logSectionHeader(`Processing intent ${index + 1} (type: ${intent.type})`) + return processTransactionIntent(intent, true) + }) + + // Check for any errors in the processed intents + console.log('Checking for errors in processed intents...') + const errorResult = processedIntents.find( + (result) => result._type === 'Left' + ) + if (errorResult && errorResult._type === 'Left') { + return errorResult + } + + // Extract the successful calls + console.log('Extracting successful calls...') + const calls = processedIntents + .filter( + (result): result is { readonly _type: 'Right'; readonly data: ICall } => + result._type === 'Right' + ) + .map((result) => result.data) + + // Setup post hooks for CoW Shed + console.log('Setting up post hooks for CoW Shed...') + const setupResult = await setupCowShedPostHooks( + chainId, + walletClient, + calls, + true + ) + + const { shedDeterministicAddress, postHooks } = setupResult + + // Amount to swap (for example, 0.1 token) + console.log('Calculating swap amount...') + const fromAmount = parseUnits('0.1', fromToken.decimals) + + logSectionHeader('Creating CoW Swap Order') + logKeyValue('From Token', fromToken.address) + logKeyValue('To Token', toToken.address) + logKeyValue('Amount', fromAmount.toString()) + logKeyValue('Token Receiver', shedProxyAddress) + logKeyValue('Post-hook receiver', shedDeterministicAddress) + + // Execute the CoW Protocol flow + console.log('Executing CoW Protocol flow...') + const cowHashResult = await cowFlow( + chainId, + walletClient, + fromToken, + toToken, + fromAmount, + shedProxyAddress, + [], + [...postHooks] + ) + + if (cowHashResult._type === 'Left') { + return cowHashResult + } + + const orderHash = cowHashResult.data + + logSectionHeader('CoW Swap Order Created') + logSuccess('Order submitted successfully') + logKeyValue('Order hash', orderHash) + logKeyValue( + 'Explorer URL', + `https://explorer.cow.fi/orders/${orderHash}?chainId=${chainId}` + ) + + return right( + `Patcher integrated with CoW Protocol - Order created with hash: ${orderHash}` + ) + } catch (error) { + console.error('Error in demoPatcher:', error) + return left( + new Error( + `Failed to execute patcher demo: ${ + error instanceof Error ? error.message : String(error) + }` + ) + ) + } +} + +async function main() { + try { + const result = await demoPatcher() + if (result && result._type === 'Left') { + console.error( + `Error: ${result.error ? result.error.message : 'Unknown error'}` + ) + process.exit(1) + } else if (result && result._type === 'Right') { + console.log(result.data) + process.exit(0) + } else { + console.error('Unexpected result format') + process.exit(1) + } + } catch (error) { + console.error('Unexpected error:', error) + process.exit(1) + } +} + +// Run the script +main() diff --git a/script/deploy/facets/DeployPatcher.s.sol b/script/deploy/facets/DeployPatcher.s.sol new file mode 100644 index 000000000..24e53a4a4 --- /dev/null +++ b/script/deploy/facets/DeployPatcher.s.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import { DeployScriptBase } from "./utils/DeployScriptBase.sol"; +import { Patcher } from "lifi/Periphery/Patcher.sol"; + +contract DeployScript is DeployScriptBase { + constructor() DeployScriptBase("Patcher") {} + + function run() public returns (Patcher deployed) { + deployed = Patcher(deploy(type(Patcher).creationCode)); + } +} diff --git a/src/Periphery/Patcher.sol b/src/Periphery/Patcher.sol index fbb5dcbc7..8c967003a 100644 --- a/src/Periphery/Patcher.sol +++ b/src/Periphery/Patcher.sol @@ -1,9 +1,11 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: MIT pragma solidity ^0.8.17; /// @title Patcher +/// @author LI.FI (https://li.fi) /// @notice A contract that patches calldata with dynamically retrieved values before execution /// @dev Designed to be used with delegate calls +/// @custom:version 1.0.0 contract Patcher { /// @notice Error when getting a dynamic value fails error FailedToGetDynamicValue(); From 6428887f198d404177f273df0e75064a011a7746 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Fri, 16 May 2025 12:55:17 +0300 Subject: [PATCH 03/46] refactor --- script/demoScripts/demoPatcher.ts | 329 ++++-------------- script/demoScripts/utils/lib/address.ts | 19 + .../demoScripts/utils/lib/cowShedConstants.ts | 12 + script/demoScripts/utils/lib/cowShedSdk.ts | 168 +++++++++ script/demoScripts/utils/lib/formatting.ts | 9 + script/demoScripts/utils/lib/logging.ts | 22 ++ script/demoScripts/utils/lib/result.ts | 34 ++ 7 files changed, 328 insertions(+), 265 deletions(-) create mode 100644 script/demoScripts/utils/lib/address.ts create mode 100644 script/demoScripts/utils/lib/cowShedConstants.ts create mode 100644 script/demoScripts/utils/lib/cowShedSdk.ts create mode 100644 script/demoScripts/utils/lib/formatting.ts create mode 100644 script/demoScripts/utils/lib/logging.ts create mode 100644 script/demoScripts/utils/lib/result.ts diff --git a/script/demoScripts/demoPatcher.ts b/script/demoScripts/demoPatcher.ts index 3e934a0f2..bf5d30d83 100644 --- a/script/demoScripts/demoPatcher.ts +++ b/script/demoScripts/demoPatcher.ts @@ -4,18 +4,13 @@ import { formatUnits, parseUnits, zeroAddress, - getCreate2Address, - keccak256, - concat, - encodeAbiParameters, - parseAbiParameters, - TypedDataEncoder, - SignTypedDataParameters, hashTypedData, + getAddress, } from 'viem' import { privateKeyToAccount } from 'viem/accounts' import { arbitrum } from 'viem/chains' import { createPublicClient, createWalletClient, http } from 'viem' +import { ethers } from 'ethers' import { SupportedChainId, OrderKind, @@ -23,18 +18,18 @@ import { SwapAdvancedSettings, TradeParameters, TraderParameters, + OrderBookApi, + OrderSigningUtils, } from '@cowprotocol/cow-sdk' +import { formatAddress } from './utils/lib/address' import { getEnvVar } from './utils/demoScriptHelpers' +import { CowShedSdk } from './utils/lib/cowShedSdk' import deploymentsArbitrum from '../../deployments/arbitrum.staging.json' // Import necessary types and utilities -import { - logKeyValue, - logSectionHeader, - logSuccess, -} from '../../dump/src/lib/utils/logging' -import { truncateCalldata } from '../../dump/src/lib/utils/formatting' -import { left, Result, right } from '../../dump/src/lib/result' +import { logKeyValue, logSectionHeader, logSuccess } from './utils/lib/logging' +import { truncateCalldata } from './utils/lib/formatting' +import { left, Result, right } from './utils/lib/result' // Define CoW Protocol constants const COW_SHED_FACTORY = '0x00E989b87700514118Fa55326CD1cCE82faebEF6' @@ -108,19 +103,9 @@ const PATCHER_ABI = parseAbi([ 'function multiPatch(address targetAddress, bytes callDataToPatch, (address valueSource, bytes valueGetter, bytes32 valueToReplace, uint256 offset)[] patchOperations, bool delegateCall) payable returns (bytes memory)', ]) -// CoW Protocol constants and ABIs -console.log('COW_SHED_FACTORY:', COW_SHED_FACTORY) -console.log('COW_SHED_IMPLEMENTATION:', COW_SHED_IMPLEMENTATION) -const PROXY_CREATION_CODE = - '0x60a034608e57601f61037138819003918201601f19168301916001600160401b038311848410176093578084926040948552833981010312608e57604b602060458360a9565b920160a9565b6080527f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc556040516102b490816100bd8239608051818181608f01526101720152f35b600080fd5b634e487b7160e01b600052604160045260246000fd5b51906001600160a01b0382168203608e5756fe60806040526004361015610018575b3661019457610194565b6000803560e01c908163025b22bc1461003b575063f851a4400361000e5761010d565b3461010a5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261010a5773ffffffffffffffffffffffffffffffffffffffff60043581811691828203610106577f0000000000000000000000000000000000000000000000000000000000000000163314600014610101577f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc557fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b8280a280f35b61023d565b8380fd5b80fd5b346101645760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610164576020610146610169565b73ffffffffffffffffffffffffffffffffffffffff60405191168152f35b600080fd5b333003610101577f000000000000000000000000000000000000000000000000000000000000000090565b60ff7f68df44b1011761f481358c0f49a711192727fb02c377d697bcb0ea8ff8393ac0541615806101ef575b1561023d5760046040517ff92ee8a9000000000000000000000000000000000000000000000000000000008152fd5b507f400ada75000000000000000000000000000000000000000000000000000000007fffffffff000000000000000000000000000000000000000000000000000000006000351614156101c0565b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc546000808092368280378136915af43d82803e1561027a573d90f35b3d90fdfea2646970667358221220c7c26ff3040b96a28e96d6d27b743972943aeaef81cc821544c5fe1e24f9b17264736f6c63430008190033' - -const FACTORY_ABI = parseAbi([ - 'function executeHooks((address target, uint256 value, bytes callData, bool allowFailure, bool isDelegateCall)[] calls, bytes32 nonce, uint256 deadline, address user, bytes signature) external', -]) - -const SHED_ABI = parseAbi([ - 'function executeHooks((address target, uint256 value, bytes callData, bool allowFailure, bool isDelegateCall)[] calls, bytes32 nonce, uint256 deadline, bytes signature) external', -]) +// Log CoW Shed constants +console.log('Using COW_SHED_FACTORY:', COW_SHED_FACTORY) +console.log('Using COW_SHED_IMPLEMENTATION:', COW_SHED_IMPLEMENTATION) // Helper functions const encodeBalanceOfCall = (account: string): string => { @@ -286,207 +271,7 @@ const processTransactionIntent = ( } } -// CoW Shed SDK implementation with Viem -class CowShedSdk { - private factoryAddress: string - private implementationAddress: string - private proxyCreationCode: string - private chainId: number - - constructor(options: { - factoryAddress: string - implementationAddress: string - proxyCreationCode?: string - chainId: number - }) { - this.factoryAddress = options.factoryAddress - this.implementationAddress = options.implementationAddress - this.proxyCreationCode = options.proxyCreationCode || PROXY_CREATION_CODE - this.chainId = options.chainId - } - - computeProxyAddress(user: string): string { - console.log('Computing proxy address for user:', user) - - if (!user || user === 'undefined' || !user.startsWith('0x')) { - throw new Error(`Invalid user address: ${user}`) - } - - // Ensure addresses are properly formatted - if (!this.factoryAddress || !this.factoryAddress.startsWith('0x')) { - throw new Error(`Invalid factory address: ${this.factoryAddress}`) - } - - if ( - !this.implementationAddress || - !this.implementationAddress.startsWith('0x') - ) { - throw new Error( - `Invalid implementation address: ${this.implementationAddress}` - ) - } - - const factoryAddress = this.factoryAddress as `0x${string}` - const implementationAddress = this.implementationAddress as `0x${string}` - const userAddress = user as `0x${string}` - - console.log('Using addresses:', { - factoryAddress, - implementationAddress, - userAddress, - }) - - try { - const salt = encodeAbiParameters(parseAbiParameters('address'), [ - userAddress, - ]) - console.log('Salt:', salt) - - const initCode = concat([ - this.proxyCreationCode as `0x${string}`, - encodeAbiParameters(parseAbiParameters('address, address'), [ - implementationAddress, - userAddress, - ]), - ]) - console.log( - 'InitCode (first 66 chars):', - initCode.substring(0, 66) + '...' - ) - - const initCodeHash = keccak256(initCode) - console.log('InitCodeHash:', initCodeHash) - - // Log each parameter individually to identify which one might be undefined - console.log('getCreate2Address parameters:', { - factoryAddress: factoryAddress, - factoryAddressType: typeof factoryAddress, - salt: salt, - saltType: typeof salt, - bytecodeHash: initCodeHash, - bytecodeHashType: typeof initCodeHash, - }) - - // Validate each parameter - if (!factoryAddress || factoryAddress === 'undefined') { - throw new Error('factoryAddress is undefined') - } - if (!salt || salt === 'undefined') { - throw new Error('salt is undefined') - } - if (!initCodeHash || initCodeHash === 'undefined') { - throw new Error('initCodeHash is undefined') - } - - // Ensure all parameters are properly formatted as hex strings - const formattedFactoryAddress = factoryAddress.startsWith('0x') - ? factoryAddress - : (`0x${factoryAddress}` as `0x${string}`) - const formattedSalt = salt.startsWith('0x') - ? salt - : (`0x${salt}` as `0x${string}`) - const formattedInitCodeHash = initCodeHash.startsWith('0x') - ? initCodeHash - : (`0x${initCodeHash}` as `0x${string}`) - - const proxyAddress = getCreate2Address({ - factoryAddress: formattedFactoryAddress, - salt: formattedSalt, - bytecodeHash: formattedInitCodeHash, - }) - - console.log('Computed proxy address:', proxyAddress) - return proxyAddress - } catch (error) { - console.error('Error in computeProxyAddress:', error) - throw error // Re-throw the error to propagate it - } - } - - hashToSignWithUser( - calls: readonly ICall[], - nonce: `0x${string}`, - deadline: bigint, - user: string - ): `0x${string}` { - const proxy = this.computeProxyAddress(user) - return this.hashToSign(calls, nonce, deadline, proxy) - } - - private hashToSign( - calls: readonly ICall[], - nonce: `0x${string}`, - deadline: bigint, - proxy: string - ): `0x${string}` { - const domain = { - name: 'COWShed', - version: '1.0.0', - chainId: this.chainId, - verifyingContract: proxy as `0x${string}`, - } - - const types = { - ExecuteHooks: [ - { name: 'calls', type: 'Call[]' }, - { name: 'nonce', type: 'bytes32' }, - { name: 'deadline', type: 'uint256' }, - ], - Call: [ - { name: 'target', type: 'address' }, - { name: 'value', type: 'uint256' }, - { name: 'callData', type: 'bytes' }, - { name: 'allowFailure', type: 'bool' }, - { name: 'isDelegateCall', type: 'bool' }, - ], - } - - const message = { - calls: calls.map((call) => ({ - target: call.target as `0x${string}`, - value: call.value, - callData: call.callData as `0x${string}`, - allowFailure: call.allowFailure, - isDelegateCall: call.isDelegateCall, - })), - nonce, - deadline, - } - - return hashTypedData({ - domain, - types, - primaryType: 'ExecuteHooks', - message, - }) - } - - static encodeExecuteHooksForFactory( - calls: readonly ICall[], - nonce: `0x${string}`, - deadline: bigint, - user: string, - signature: `0x${string}` - ): string { - return encodeFunctionData({ - abi: FACTORY_ABI, - functionName: 'executeHooks', - args: [ - calls.map((call) => ({ - target: call.target as `0x${string}`, - value: call.value, - callData: call.callData as `0x${string}`, - allowFailure: call.allowFailure, - isDelegateCall: call.isDelegateCall, - })), - nonce, - deadline, - user as `0x${string}`, - signature, - ], - }) - } -} +// Using the CowShedSdk implementation from utils/lib/cowShedSdk.ts // Real implementation of CoW Protocol functions const calculateCowShedProxyAddress = async ( @@ -519,6 +304,7 @@ const calculateCowShedProxyAddress = async ( throw new Error('COW_SHED_IMPLEMENTATION is undefined or invalid') } + // Use our new CowShedSdk implementation const shedSDK = new CowShedSdk({ factoryAddress: COW_SHED_FACTORY, implementationAddress: COW_SHED_IMPLEMENTATION, @@ -527,7 +313,6 @@ const calculateCowShedProxyAddress = async ( const proxyAddress = shedSDK.computeProxyAddress(owner) console.log('Computed proxy address:', proxyAddress) - return proxyAddress } @@ -554,6 +339,8 @@ const setupCowShedPostHooks = async ( Math.floor(Math.random() * 16).toString(16) ).join('')}` as `0x${string}` const deadline = BigInt(Math.floor(Date.now() / 1000) + 7200) // now + 2 hours + + // Get the proxy address const shedDeterministicAddress = shedSDK.computeProxyAddress(signerAddress) // Sign the post hooks @@ -615,50 +402,62 @@ const cowFlow = async ( postHooks: readonly any[] ): Promise> => { try { - // Create trader parameters with a signer adapter for Viem wallet - const traderParams: TraderParameters = { - chainId, - signer: { - signMessage: async (message: string) => { - return walletClient.signMessage({ - account: walletClient.account, - message: { raw: message as `0x${string}` }, - }) - }, - getAddress: async () => walletClient.account.address, - }, - appCode: 'LI.FI Patcher Demo', + console.log('Executing CoW Protocol flow...') + console.log('From token:', fromToken.address) + console.log('To token:', toToken.address) + console.log('Amount:', fromAmount.toString()) + console.log('Receiver:', postReceiver) + console.log('Post hooks count:', postHooks.length) + + // Get the private key from the environment + const privateKeyRaw = process.env.PRIVATE_KEY + if (!privateKeyRaw) { + throw new Error('PRIVATE_KEY environment variable is not set') } - // Create trade parameters - const params: TradeParameters = { - kind: OrderKind.SELL, // SELL order (specify amount in) + // Ensure the private key has the correct format + const privateKey = privateKeyRaw.startsWith('0x') + ? privateKeyRaw + : `0x${privateKeyRaw}` + + // Create an ethers.js wallet from the private key + const provider = new ethers.providers.JsonRpcProvider( + process.env.ETH_NODE_URI_ARBITRUM + ) + const ethersSigner = new ethers.Wallet(privateKey, provider) + + // Initialize the CoW Protocol SDK with the ethers.js signer + const cowSdk = new TradingSdk({ + chainId: chainId as SupportedChainId, + signer: ethersSigner, + appCode: 'LiFi', + }) + + // Create the order parameters + const parameters: TradeParameters = { + kind: OrderKind.SELL, sellToken: fromToken.address, - sellTokenDecimals: fromToken.decimals, buyToken: toToken.address, - buyTokenDecimals: toToken.decimals, - amount: fromAmount.toString(), + // Convert bigint to string for the API + sellAmountBeforeFee: fromAmount.toString(), receiver: postReceiver, } - // Create advanced settings with hooks - const advancedSettings: SwapAdvancedSettings = { - appData: { - metadata: { - hooks: { - version: '1', - pre: [...preHooks], - post: [...postHooks], - }, - }, - }, - } - - // Initialize the SDK and post the order - const sdk = new TradingSdk(traderParams) - const hash = await sdk.postSwapOrder(params, advancedSettings) + // Add post hooks if provided + if (postHooks && postHooks.length > 0) { + // Add post hooks to the parameters + const traderParams: TraderParameters = { + postHooks, + } - return right(hash) + // Submit the order with post hooks + const orderId = await cowSdk.postSwapOrder(parameters, traderParams) + return right(orderId) + } else { + // Submit the order without post hooks + const orderId = await cowSdk.postSwapOrder(parameters) + return right(orderId) + } } catch (error) { return left(new Error(`Failed to execute CoW flow: ${error.message}`)) } diff --git a/script/demoScripts/utils/lib/address.ts b/script/demoScripts/utils/lib/address.ts new file mode 100644 index 000000000..5c417747a --- /dev/null +++ b/script/demoScripts/utils/lib/address.ts @@ -0,0 +1,19 @@ +/** + * Utility functions for working with Ethereum addresses + */ + +/** + * Formats an address string to ensure it has the 0x prefix and is properly formatted + * @param address The address to format + * @returns The formatted address with 0x prefix + */ +export function formatAddress(address: string): `0x${string}` { + // Ensure the address has the 0x prefix + const prefixedAddress = address.startsWith('0x') ? address : `0x${address}` + + // Ensure the address is lowercase for consistency + const formattedAddress = prefixedAddress.toLowerCase() + + // Return the address with the correct type + return formattedAddress as `0x${string}` +} \ No newline at end of file diff --git a/script/demoScripts/utils/lib/cowShedConstants.ts b/script/demoScripts/utils/lib/cowShedConstants.ts new file mode 100644 index 000000000..cc16c6c7c --- /dev/null +++ b/script/demoScripts/utils/lib/cowShedConstants.ts @@ -0,0 +1,12 @@ +// Constants for CoW Shed SDK +import { parseAbi } from 'viem' + +export const FACTORY_ABI = parseAbi([ + 'function executeHooks((address,uint256,bytes,bool,bool)[] calls, bytes32 nonce, uint256 deadline, address user, bytes signature) external', +]) + +export const SHED_ABI = parseAbi([ + 'function executeHooks((address,uint256,bytes,bool,bool)[] calls, bytes32 nonce, uint256 deadline, bytes signature) external', +]) + +export const PROXY_CREATION_CODE = '0x60a034608e57601f61037138819003918201601f19168301916001600160401b038311848410176093578084926040948552833981010312608e57604b602060458360a9565b920160a9565b6080527f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc556040516102b490816100bd8239608051818181608f01526101720152f35b600080fd5b634e487b7160e01b600052604160045260246000fd5b51906001600160a01b0382168203608e5756fe60806040526004361015610018575b3661019457610194565b6000803560e01c908163025b22bc1461003b575063f851a4400361000e5761010d565b3461010a5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261010a5773ffffffffffffffffffffffffffffffffffffffff60043581811691828203610106577f0000000000000000000000000000000000000000000000000000000000000000163314600014610101577f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc557fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b8280a280f35b61023d565b8380fd5b80fd5b346101645760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610164576020610146610169565b73ffffffffffffffffffffffffffffffffffffffff60405191168152f35b600080fd5b333003610101577f000000000000000000000000000000000000000000000000000000000000000090565b60ff7f68df44b1011761f481358c0f49a711192727fb02c377d697bcb0ea8ff8393ac0541615806101ef575b1561023d5760046040517ff92ee8a9000000000000000000000000000000000000000000000000000000008152fd5b507f400ada75000000000000000000000000000000000000000000000000000000007fffffffff000000000000000000000000000000000000000000000000000000006000351614156101c0565b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc546000808092368280378136915af43d82803e1561027a573d90f35b3d90fdfea2646970667358221220c7c26ff3040b96a28e96d6d27b743972943aeaef81cc821544c5fe1e24f9b17264736f6c63430008190033' \ No newline at end of file diff --git a/script/demoScripts/utils/lib/cowShedSdk.ts b/script/demoScripts/utils/lib/cowShedSdk.ts new file mode 100644 index 000000000..5739ef994 --- /dev/null +++ b/script/demoScripts/utils/lib/cowShedSdk.ts @@ -0,0 +1,168 @@ +/** + * CoW Shed SDK implementation using viem + */ +import { + getCreate2Address, + keccak256, + concat, + encodeAbiParameters, + parseAbiParameters, + hashTypedData, + encodeFunctionData, + Address, +} from 'viem' +import { FACTORY_ABI, PROXY_CREATION_CODE, SHED_ABI } from './cowShedConstants' +import { formatAddress } from './address' + +export interface ISdkOptions { + factoryAddress: string + implementationAddress: string + proxyCreationCode?: string + chainId: number +} + +export interface ICall { + target: string + value: bigint + callData: string + allowFailure: boolean + isDelegateCall: boolean +} + +export class CowShedSdk { + private factoryAddress: string + private implementationAddress: string + private proxyCreationCode: string + private chainId: number + + constructor(options: ISdkOptions) { + this.factoryAddress = options.factoryAddress + this.implementationAddress = options.implementationAddress + this.proxyCreationCode = options.proxyCreationCode || PROXY_CREATION_CODE + this.chainId = options.chainId + } + + computeProxyAddress(user: string): string { + // Format addresses to ensure they're valid + const formattedFactoryAddress = formatAddress(this.factoryAddress) + const formattedImplementationAddress = formatAddress(this.implementationAddress) + const formattedUserAddress = formatAddress(user) + + // Create the salt from the user address + const salt = encodeAbiParameters( + parseAbiParameters('address'), + [formattedUserAddress] + ) + + // Create the init code by concatenating the proxy creation code and the encoded constructor arguments + const initCode = concat([ + this.proxyCreationCode as `0x${string}`, + encodeAbiParameters( + parseAbiParameters('address, address'), + [formattedImplementationAddress, formattedUserAddress] + ) + ]) + + // Calculate the init code hash + const initCodeHash = keccak256(initCode) + + // Calculate the CREATE2 address + const proxyAddress = getCreate2Address({ + from: formattedFactoryAddress, + salt: salt, + bytecodeHash: initCodeHash, + }) + + return proxyAddress + } + + hashToSignWithUser( + calls: ICall[], + nonce: `0x${string}`, + deadline: bigint, + user: string + ): `0x${string}` { + const proxy = this.computeProxyAddress(user) + return this.hashToSign(calls, nonce, deadline, proxy) + } + + private hashToSign( + calls: ICall[], + nonce: `0x${string}`, + deadline: bigint, + proxy: string + ): `0x${string}` { + const domain = { + name: 'COWShed', + version: '1.0.0', + chainId: this.chainId, + verifyingContract: proxy as `0x${string}`, + } + + const types = { + ExecuteHooks: [ + { name: 'calls', type: 'Call[]' }, + { name: 'nonce', type: 'bytes32' }, + { name: 'deadline', type: 'uint256' }, + ], + Call: [ + { name: 'target', type: 'address' }, + { name: 'value', type: 'uint256' }, + { name: 'callData', type: 'bytes' }, + { name: 'allowFailure', type: 'bool' }, + { name: 'isDelegateCall', type: 'bool' }, + ], + } + + const message = { + calls: calls.map((call) => ({ + target: call.target as `0x${string}`, + value: call.value, + callData: call.callData as `0x${string}`, + allowFailure: call.allowFailure, + isDelegateCall: call.isDelegateCall, + })), + nonce, + deadline, + } + + return hashTypedData({ + domain, + types, + primaryType: 'ExecuteHooks', + message, + }) + } + + static encodeExecuteHooksForFactory( + calls: ICall[], + nonce: `0x${string}`, + deadline: bigint, + user: string, + signature: `0x${string}` + ): string { + // Format the user address + const formattedUser = formatAddress(user) + + // Convert calls to the expected format for the ABI + const formattedCalls = calls.map(call => [ + formatAddress(call.target), + call.value, + call.callData as `0x${string}`, + call.allowFailure, + call.isDelegateCall + ] as const) + + return encodeFunctionData({ + abi: FACTORY_ABI, + functionName: 'executeHooks', + args: [ + formattedCalls, + nonce, + deadline, + formattedUser, + signature, + ], + }) + } +} \ No newline at end of file diff --git a/script/demoScripts/utils/lib/formatting.ts b/script/demoScripts/utils/lib/formatting.ts new file mode 100644 index 000000000..3f4af7cc5 --- /dev/null +++ b/script/demoScripts/utils/lib/formatting.ts @@ -0,0 +1,9 @@ +/** + * Truncate calldata for display purposes + */ +export function truncateCalldata(calldata: string, length: number = 10): string { + if (!calldata || calldata.length <= length * 2) return calldata + + const prefix = calldata.substring(0, length * 2) + return `${prefix}...` +} \ No newline at end of file diff --git a/script/demoScripts/utils/lib/logging.ts b/script/demoScripts/utils/lib/logging.ts new file mode 100644 index 000000000..025f24a0d --- /dev/null +++ b/script/demoScripts/utils/lib/logging.ts @@ -0,0 +1,22 @@ +// Simple logging utilities for demo scripts + +/** + * Log a key-value pair with formatting + */ +export function logKeyValue(key: string, value: string | number | bigint): void { + console.log(`${key}: ${value}`) +} + +/** + * Log a section header with formatting + */ +export function logSectionHeader(text: string): void { + console.log(`\n=== ${text} ===`) +} + +/** + * Log a success message with formatting + */ +export function logSuccess(text: string): void { + console.log(`✅ ${text}`) +} \ No newline at end of file diff --git a/script/demoScripts/utils/lib/result.ts b/script/demoScripts/utils/lib/result.ts new file mode 100644 index 000000000..072059a38 --- /dev/null +++ b/script/demoScripts/utils/lib/result.ts @@ -0,0 +1,34 @@ +/** + * Result type for handling success and error cases + */ +export type Result = Left | Right + +/** + * Left type for error cases + */ +export interface Left { + readonly _type: 'Left' + readonly error: E +} + +/** + * Right type for success cases + */ +export interface Right { + readonly _type: 'Right' + readonly data: T +} + +/** + * Create a Left result (error case) + */ +export function left(error: E): Result { + return { _type: 'Left', error } +} + +/** + * Create a Right result (success case) + */ +export function right(data: T): Result { + return { _type: 'Right', data } +} \ No newline at end of file From 5e1234ecd6031b9ca5d4aec9c412efd37c83f4c8 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Mon, 19 May 2025 18:00:58 +0300 Subject: [PATCH 04/46] TX is successful now --- script/demoScripts/demoPatcher.ts | 183 +++++++++++++++++++++--------- 1 file changed, 128 insertions(+), 55 deletions(-) diff --git a/script/demoScripts/demoPatcher.ts b/script/demoScripts/demoPatcher.ts index bf5d30d83..5e8de4e3c 100644 --- a/script/demoScripts/demoPatcher.ts +++ b/script/demoScripts/demoPatcher.ts @@ -17,11 +17,7 @@ import { TradingSdk, SwapAdvancedSettings, TradeParameters, - TraderParameters, - OrderBookApi, - OrderSigningUtils, } from '@cowprotocol/cow-sdk' -import { formatAddress } from './utils/lib/address' import { getEnvVar } from './utils/demoScriptHelpers' import { CowShedSdk } from './utils/lib/cowShedSdk' import deploymentsArbitrum from '../../deployments/arbitrum.staging.json' @@ -32,54 +28,56 @@ import { truncateCalldata } from './utils/lib/formatting' import { left, Result, right } from './utils/lib/result' // Define CoW Protocol constants -const COW_SHED_FACTORY = '0x00E989b87700514118Fa55326CD1cCE82faebEF6' -const COW_SHED_IMPLEMENTATION = '0x2CFFA8cf11B90C9F437567b86352169dF4009F73' +const COW_SHED_FACTORY = + '0x00E989b87700514118Fa55326CD1cCE82faebEF6' as `0x${string}` +const COW_SHED_IMPLEMENTATION = + '0x2CFFA8cf11B90C9F437567b86352169dF4009F73' as `0x${string}` // Define interfaces and types interface Token { - readonly address: string + readonly address: `0x${string}` readonly decimals: number } interface ICall { - target: string - callData: string + target: `0x${string}` + callData: `0x${string}` value: bigint allowFailure: boolean isDelegateCall: boolean } interface PatchOperation { - valueSource: string - valueGetter: string + valueSource: `0x${string}` + valueGetter: `0x${string}` valueToReplace: string | number | bigint } type TransactionIntent = | { readonly type: 'regular' - readonly targetAddress: string - readonly callData: string + readonly targetAddress: `0x${string}` + readonly callData: `0x${string}` readonly nativeValue?: bigint readonly allowFailure?: boolean readonly isDelegateCall?: boolean } | { readonly type: 'dynamicValue' - readonly patcherAddress: string - readonly valueSource: string - readonly valueGetter: string - readonly targetAddress: string - readonly callDataToPatch: string + readonly patcherAddress: `0x${string}` + readonly valueSource: `0x${string}` + readonly valueGetter: `0x${string}` + readonly targetAddress: `0x${string}` + readonly callDataToPatch: `0x${string}` readonly valueToReplace: string | number | bigint readonly nativeValue?: bigint readonly delegateCall?: boolean } | { readonly type: 'multiPatch' - readonly patcherAddress: string - readonly targetAddress: string - readonly callDataToPatch: string + readonly patcherAddress: `0x${string}` + readonly targetAddress: `0x${string}` + readonly callDataToPatch: `0x${string}` readonly patchOperations: readonly PatchOperation[] readonly nativeValue?: bigint readonly delegateCall?: boolean @@ -108,30 +106,30 @@ console.log('Using COW_SHED_FACTORY:', COW_SHED_FACTORY) console.log('Using COW_SHED_IMPLEMENTATION:', COW_SHED_IMPLEMENTATION) // Helper functions -const encodeBalanceOfCall = (account: string): string => { +const encodeBalanceOfCall = (account: string): `0x${string}` => { return encodeFunctionData({ abi: ERC20_ABI, functionName: 'balanceOf', - args: [account], + args: [account as `0x${string}`], }) } -const encodeTransferCall = (to: string, amount: string): string => { +const encodeTransferCall = (to: string, amount: string): `0x${string}` => { return encodeFunctionData({ abi: ERC20_ABI, functionName: 'transfer', - args: [to, BigInt(amount)], + args: [to as `0x${string}`, BigInt(amount)], }) } const encodeGetDoubledBalanceCall = ( token: string, account: string -): string => { +): `0x${string}` => { return encodeFunctionData({ abi: DUMMY_TARGET_ABI, functionName: 'getDoubledBalance', - args: [token, account], + args: [token as `0x${string}`, account as `0x${string}`], }) } @@ -209,7 +207,11 @@ const processTransactionIntent = ( return right(call) } catch (error) { return left( - new Error(`Failed to create dynamic value patch: ${error.message}`) + new Error( + `Failed to create dynamic value patch: ${ + error instanceof Error ? error.message : String(error) + }` + ) ) } } @@ -265,7 +267,13 @@ const processTransactionIntent = ( return right(call) } catch (error) { - return left(new Error(`Failed to create multi-patch: ${error.message}`)) + return left( + new Error( + `Failed to create multi-patch: ${ + error instanceof Error ? error.message : String(error) + }` + ) + ) } } } @@ -280,7 +288,7 @@ const calculateCowShedProxyAddress = async ( ): Promise => { console.log('calculateCowShedProxyAddress called with:', { chainId, owner }) - if (!owner || owner === 'undefined') { + if (!owner || owner === '0x0000000000000000000000000000000000000000') { throw new Error('Owner address is undefined or empty') } @@ -290,16 +298,14 @@ const calculateCowShedProxyAddress = async ( // Validate inputs before creating the SDK if ( !COW_SHED_FACTORY || - COW_SHED_FACTORY === 'undefined' || - COW_SHED_FACTORY === zeroAddress + COW_SHED_FACTORY === '0x0000000000000000000000000000000000000000' ) { throw new Error('COW_SHED_FACTORY is undefined or invalid') } if ( !COW_SHED_IMPLEMENTATION || - COW_SHED_IMPLEMENTATION === 'undefined' || - COW_SHED_IMPLEMENTATION === zeroAddress + COW_SHED_IMPLEMENTATION === '0x0000000000000000000000000000000000000000' ) { throw new Error('COW_SHED_IMPLEMENTATION is undefined or invalid') } @@ -433,25 +439,88 @@ const cowFlow = async ( appCode: 'LiFi', }) + // Get the VaultRelayer address for the current chain + // Hardcoded VaultRelayer addresses for different chains + const vaultRelayerAddresses = { + [SupportedChainId.ARBITRUM_ONE]: + '0xC92E8bdf79f0507f65a392b0ab4667716BFE0110', + } + + const vaultRelayerAddress = + vaultRelayerAddresses[chainId as SupportedChainId] + if (!vaultRelayerAddress) { + throw new Error(`VaultRelayer address not found for chain ID ${chainId}`) + } + console.log(`VaultRelayer address: ${vaultRelayerAddress}`) + + // Create ERC20 contract instance for the sell token + const sellTokenContract = new ethers.Contract( + fromToken.address, + [ + 'function approve(address spender, uint256 amount) public returns (bool)', + 'function allowance(address owner, address spender) public view returns (uint256)', + ], + ethersSigner + ) + + // Check current allowance + const currentAllowance = await sellTokenContract.allowance( + ethersSigner.address, + vaultRelayerAddress + ) + console.log(`Current allowance: ${currentAllowance.toString()}`) + + // If allowance is insufficient, approve the token + if (currentAllowance.lt(fromAmount)) { + console.log('Approving token for CoW Protocol VaultRelayer...') + const maxApproval = ethers.constants.MaxUint256 + const approveTx = await sellTokenContract.approve( + vaultRelayerAddress, + maxApproval + ) + console.log(`Approval transaction hash: ${approveTx.hash}`) + + // Wait for the approval transaction to be confirmed + console.log('Waiting for approval transaction to be confirmed...') + await approveTx.wait() + console.log('Token approved successfully') + } else { + console.log('Token already has sufficient allowance') + } + // Create the order parameters const parameters: TradeParameters = { kind: OrderKind.SELL, sellToken: fromToken.address, + sellTokenDecimals: fromToken.decimals, buyToken: toToken.address, - // Convert bigint to string for the API - sellAmountBeforeFee: fromAmount.toString(), + buyTokenDecimals: toToken.decimals, + amount: fromAmount.toString(), receiver: postReceiver, + // Add a reasonable validity period (30 minutes) + validFor: 30 * 60, // 30 minutes in seconds + // Add a reasonable slippage (0.5%) + slippageBps: 50, } // Add post hooks if provided if (postHooks && postHooks.length > 0) { - // Add post hooks to the parameters - const traderParams: TraderParameters = { - postHooks, - } + // Simplify post hooks to reduce payload size + const simplifiedPostHooks = postHooks.map((hook) => ({ + target: hook.target, + callData: hook.callData.substring(0, 200), // Truncate calldata to reduce payload size + gasLimit: '1000000', + })) // Submit the order with post hooks - const orderId = await cowSdk.postSwapOrder(parameters, traderParams) + const orderId = await cowSdk.postSwapOrder(parameters, { + // Pass post hooks via appData + appData: JSON.stringify({ + hooks: { + post: simplifiedPostHooks, + }, + }), + }) return right(orderId) } else { // Submit the order without post hooks @@ -459,7 +528,14 @@ const cowFlow = async ( return right(orderId) } } catch (error) { - return left(new Error(`Failed to execute CoW flow: ${error.message}`)) + console.error('CoW Protocol error details:', error) + return left( + new Error( + `Failed to execute CoW flow: ${ + error instanceof Error ? error.message : String(error) + }` + ) + ) } } @@ -516,9 +592,6 @@ const demoPatcher = async (): Promise> => { } console.log('Patcher address:', patcherAddress) - // Use a mock address for the dummy target - const dummyTargetAddress = '0x0000000000000000000000000000000000000001' - const baseValuePlaceholder = '1000000000000000000' const doubledValuePlaceholder = '2000000000000000000' @@ -529,12 +602,12 @@ const demoPatcher = async (): Promise> => { // Define token information console.log('Setting up token information...') const fromToken = { - address: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1', // WETH on Arbitrum + address: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1' as `0x${string}`, // WETH on Arbitrum decimals: 18, } const toToken = { - address: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', // USDC on Arbitrum + address: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831' as `0x${string}`, // USDC on Arbitrum decimals: 6, } @@ -551,10 +624,10 @@ const demoPatcher = async (): Promise> => { const transactionIntents: readonly TransactionIntent[] = [ { type: 'dynamicValue', - patcherAddress: patcherAddress, + patcherAddress: patcherAddress as `0x${string}`, valueSource: toToken.address, valueGetter: encodeBalanceOfCall(shedProxyAddress), - targetAddress: dummyTargetAddress, + targetAddress: patcherAddress as `0x${string}`, callDataToPatch: encodeFunctionData({ abi: DUMMY_TARGET_ABI, functionName: 'deposit', @@ -565,8 +638,8 @@ const demoPatcher = async (): Promise> => { }, { type: 'multiPatch', - patcherAddress: patcherAddress, - targetAddress: dummyTargetAddress, + patcherAddress: patcherAddress as `0x${string}`, + targetAddress: patcherAddress as `0x${string}`, callDataToPatch: encodeFunctionData({ abi: DUMMY_TARGET_ABI, functionName: 'multiDeposit', @@ -584,9 +657,9 @@ const demoPatcher = async (): Promise> => { valueToReplace: baseValuePlaceholder, }, { - valueSource: dummyTargetAddress, + valueSource: patcherAddress as `0x${string}`, valueGetter: encodeGetDoubledBalanceCall( - toToken.address, + toToken.address as string, shedProxyAddress ), valueToReplace: doubledValuePlaceholder, @@ -596,7 +669,7 @@ const demoPatcher = async (): Promise> => { }, { type: 'dynamicValue', - patcherAddress: patcherAddress, + patcherAddress: patcherAddress as `0x${string}`, valueSource: toToken.address, valueGetter: encodeBalanceOfCall(shedProxyAddress), targetAddress: toToken.address, @@ -651,7 +724,7 @@ const demoPatcher = async (): Promise> => { // Amount to swap (for example, 0.1 token) console.log('Calculating swap amount...') - const fromAmount = parseUnits('0.1', fromToken.decimals) + const fromAmount = parseUnits('0.001', fromToken.decimals) logSectionHeader('Creating CoW Swap Order') logKeyValue('From Token', fromToken.address) From 79a6357c09a841c74a05d5defa8d47588147d544 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Mon, 19 May 2025 18:11:25 +0300 Subject: [PATCH 05/46] Cleanup --- script/demoScripts/demoPatcher.ts | 38 ++++++++----------------------- 1 file changed, 9 insertions(+), 29 deletions(-) diff --git a/script/demoScripts/demoPatcher.ts b/script/demoScripts/demoPatcher.ts index 5e8de4e3c..2529461cb 100644 --- a/script/demoScripts/demoPatcher.ts +++ b/script/demoScripts/demoPatcher.ts @@ -1,12 +1,4 @@ -import { - parseAbi, - encodeFunctionData, - formatUnits, - parseUnits, - zeroAddress, - hashTypedData, - getAddress, -} from 'viem' +import { parseAbi, encodeFunctionData, parseUnits, zeroAddress } from 'viem' import { privateKeyToAccount } from 'viem/accounts' import { arbitrum } from 'viem/chains' import { createPublicClient, createWalletClient, http } from 'viem' @@ -15,10 +7,8 @@ import { SupportedChainId, OrderKind, TradingSdk, - SwapAdvancedSettings, TradeParameters, } from '@cowprotocol/cow-sdk' -import { getEnvVar } from './utils/demoScriptHelpers' import { CowShedSdk } from './utils/lib/cowShedSdk' import deploymentsArbitrum from '../../deployments/arbitrum.staging.json' @@ -432,25 +422,15 @@ const cowFlow = async ( ) const ethersSigner = new ethers.Wallet(privateKey, provider) - // Initialize the CoW Protocol SDK with the ethers.js signer + // Initialize the CoW Protocol SDK with the ethers.js signer for Arbitrum const cowSdk = new TradingSdk({ - chainId: chainId as SupportedChainId, + chainId: SupportedChainId.ARBITRUM_ONE, signer: ethersSigner, appCode: 'LiFi', }) - // Get the VaultRelayer address for the current chain - // Hardcoded VaultRelayer addresses for different chains - const vaultRelayerAddresses = { - [SupportedChainId.ARBITRUM_ONE]: - '0xC92E8bdf79f0507f65a392b0ab4667716BFE0110', - } - - const vaultRelayerAddress = - vaultRelayerAddresses[chainId as SupportedChainId] - if (!vaultRelayerAddress) { - throw new Error(`VaultRelayer address not found for chain ID ${chainId}`) - } + // Get the VaultRelayer address for Arbitrum + const vaultRelayerAddress = '0xC92E8bdf79f0507f65a392b0ab4667716BFE0110' console.log(`VaultRelayer address: ${vaultRelayerAddress}`) // Create ERC20 contract instance for the sell token @@ -562,7 +542,7 @@ const demoPatcher = async (): Promise> => { const account = privateKeyToAccount(privateKey) console.log('Account address:', account.address) - console.log('Creating clients...') + console.log('Creating clients for Arbitrum...') const rpcUrl = process.env.ETH_NODE_URI_ARBITRUM if (!rpcUrl) { throw new Error('ETH_NODE_URI_ARBITRUM environment variable is not set') @@ -597,10 +577,10 @@ const demoPatcher = async (): Promise> => { const originalTokenOwner = account.address // Use the correct Arbitrum chain ID - const chainId = 42161 + const chainId = 42161 // Arbitrum One - // Define token information - console.log('Setting up token information...') + // Define token information for Arbitrum + console.log('Setting up token information for Arbitrum...') const fromToken = { address: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1' as `0x${string}`, // WETH on Arbitrum decimals: 18, From 08c23e3e489fc540be91ed36c2b032b6b80f03df Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Tue, 20 May 2025 10:17:08 +0300 Subject: [PATCH 06/46] Fix cowswap call --- script/demoScripts/demoPatcher.ts | 76 ++++++++++++++++++++++--------- 1 file changed, 55 insertions(+), 21 deletions(-) diff --git a/script/demoScripts/demoPatcher.ts b/script/demoScripts/demoPatcher.ts index 2529461cb..c75f7475a 100644 --- a/script/demoScripts/demoPatcher.ts +++ b/script/demoScripts/demoPatcher.ts @@ -8,6 +8,7 @@ import { OrderKind, TradingSdk, TradeParameters, + SwapAdvancedSettings, } from '@cowprotocol/cow-sdk' import { CowShedSdk } from './utils/lib/cowShedSdk' import deploymentsArbitrum from '../../deployments/arbitrum.staging.json' @@ -362,12 +363,17 @@ const setupCowShedPostHooks = async ( signature ) + // Log the encoded post hooks calldata + console.log('Encoded post hooks calldata:') + console.log(`Calldata: ${shedEncodedPostHooksCallData}`) + console.log(`Calldata length: ${shedEncodedPostHooksCallData.length} bytes`) + // Create the post hooks const postHooks = [ { target: COW_SHED_FACTORY, callData: shedEncodedPostHooksCallData, - gasLimit: '2000000', + gasLimit: '3000000', // Increased gas limit for full calldata }, ] @@ -483,30 +489,46 @@ const cowFlow = async ( slippageBps: 50, } - // Add post hooks if provided - if (postHooks && postHooks.length > 0) { - // Simplify post hooks to reduce payload size - const simplifiedPostHooks = postHooks.map((hook) => ({ - target: hook.target, - callData: hook.callData.substring(0, 200), // Truncate calldata to reduce payload size - gasLimit: '1000000', - })) - - // Submit the order with post hooks - const orderId = await cowSdk.postSwapOrder(parameters, { - // Pass post hooks via appData - appData: JSON.stringify({ + // Add post hooks - this script requires post hooks + if (!postHooks || postHooks.length === 0) { + return left( + new Error('Post hooks are required for this script to function') + ) + } + + // Create post hooks with full calldata + const simplifiedPostHooks = postHooks.map((hook) => ({ + target: hook.target, + callData: hook.callData, // Use the full calldata without truncation + gasLimit: '3000000', + })) + + // Log the full calldata for debugging + console.log('Full post hook calldata:') + simplifiedPostHooks.forEach((hook, index) => { + console.log(`Hook ${index + 1} target: ${hook.target}`) + console.log(`Hook ${index + 1} calldata: ${hook.callData}`) + console.log( + `Hook ${index + 1} calldata length: ${hook.callData.length} bytes` + ) + }) + + // Create advanced settings with the correct format + const advancedSettings: SwapAdvancedSettings = { + appData: { + metadata: { hooks: { + version: '1', + pre: [], post: simplifiedPostHooks, }, - }), - }) - return right(orderId) - } else { - // Submit the order without post hooks - const orderId = await cowSdk.postSwapOrder(parameters) - return right(orderId) + }, + }, } + + // Submit the order with post hooks + const orderId = await cowSdk.postSwapOrder(parameters, advancedSettings) + return right(orderId) } catch (error) { console.error('CoW Protocol error details:', error) return left( @@ -713,6 +735,18 @@ const demoPatcher = async (): Promise> => { logKeyValue('Token Receiver', shedProxyAddress) logKeyValue('Post-hook receiver', shedDeterministicAddress) + // Log the original post hooks + console.log('Original post hooks:') + postHooks.forEach((hook, index) => { + console.log(`Original hook ${index + 1} target: ${hook.target}`) + console.log(`Original hook ${index + 1} calldata: ${hook.callData}`) + console.log( + `Original hook ${index + 1} calldata length: ${ + hook.callData.length + } bytes` + ) + }) + // Execute the CoW Protocol flow console.log('Executing CoW Protocol flow...') const cowHashResult = await cowFlow( From b649bf900d3d5f618b1319eb7ca194810ce09320 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Tue, 20 May 2025 12:09:52 +0300 Subject: [PATCH 07/46] Fix patch calls --- script/demoScripts/demoPatcher.ts | 112 +++++++++++++++++++----------- 1 file changed, 71 insertions(+), 41 deletions(-) diff --git a/script/demoScripts/demoPatcher.ts b/script/demoScripts/demoPatcher.ts index c75f7475a..3a13eb174 100644 --- a/script/demoScripts/demoPatcher.ts +++ b/script/demoScripts/demoPatcher.ts @@ -23,6 +23,8 @@ const COW_SHED_FACTORY = '0x00E989b87700514118Fa55326CD1cCE82faebEF6' as `0x${string}` const COW_SHED_IMPLEMENTATION = '0x2CFFA8cf11B90C9F437567b86352169dF4009F73' as `0x${string}` +const DUMMY_TARGET = + '0x00c8Bd044Be424E48FDCC2C171eF5adebE631C09' as `0x${string}` // Define interfaces and types interface Token { @@ -88,8 +90,8 @@ const DUMMY_TARGET_ABI = parseAbi([ ]) const PATCHER_ABI = parseAbi([ - 'function dynamicValuePatch(address valueSource, bytes valueGetter, address targetAddress, bytes callDataToPatch, bytes32 valueToReplace, uint256 offset, bool delegateCall) payable returns (bytes memory)', - 'function multiPatch(address targetAddress, bytes callDataToPatch, (address valueSource, bytes valueGetter, bytes32 valueToReplace, uint256 offset)[] patchOperations, bool delegateCall) payable returns (bytes memory)', + 'function executeWithDynamicPatches(address valueSource, bytes valueGetter, address finalTarget, uint256 value, bytes callDataToPatch, uint256[] offsets, bool delegateCall) returns (bool success, bytes memory returnData)', + 'function executeWithMultiplePatches(address[] valueSources, bytes[] valueGetters, address finalTarget, uint256 value, bytes callDataToPatch, uint256[][] offsetGroups, bool delegateCall) returns (bool success, bytes memory returnData)', ]) // Log CoW Shed constants @@ -169,19 +171,17 @@ const processTransactionIntent = ( // Create dynamic patch call const callData = encodeFunctionData({ abi: PATCHER_ABI, - functionName: 'dynamicValuePatch', + functionName: 'executeWithDynamicPatches', args: [ intent.valueSource, intent.valueGetter, intent.targetAddress, + BigInt(0), // value intent.callDataToPatch, - `0x${intent.valueToReplace - .toString() - .padStart(64, '0')}` as `0x${string}`, - BigInt(0), // offset + [BigInt(36)], // array of offsets - amount parameter is at offset 36 (4 bytes function selector + 32 bytes address) intent.delegateCall ?? false, ], - }) + }) as `0x${string}` const call: ICall = { target: intent.patcherAddress, @@ -213,36 +213,40 @@ const processTransactionIntent = ( logSectionHeader( `Creating multi-patch call with ${intent.patchOperations.length} operations` ) - logKeyValue( - 'Call data to patch', - truncateCalldata(intent.callDataToPatch, 10) - ) + logKeyValue('Call data to patch', intent.callDataToPatch) logKeyValue('Target', intent.targetAddress) } - // Format patch operations for the contract call - const patchOperations = intent.patchOperations.map((op) => { - return { - valueSource: op.valueSource, - valueGetter: op.valueGetter, - valueToReplace: `0x${op.valueToReplace - .toString() - .padStart(64, '0')}` as `0x${string}`, - offset: BigInt(0), - } - }) + // Prepare arrays for executeWithMultiplePatches + const valueSources = intent.patchOperations.map( + (op) => op.valueSource + ) as `0x${string}`[] + const valueGetters = intent.patchOperations.map( + (op) => op.valueGetter + ) as `0x${string}`[] + + // For multiDeposit function: + // baseAmount parameter is at offset 36 (4 bytes function selector + 32 bytes address) + // doubledAmount parameter is at offset 68 (4 bytes function selector + 32 bytes address + 32 bytes baseAmount) + const offsetGroups = [ + [BigInt(36)], // First operation patches at offset 36 + [BigInt(68)], // Second operation patches at offset 68 + ] as readonly (readonly bigint[])[] // Create multi-patch call const callData = encodeFunctionData({ abi: PATCHER_ABI, - functionName: 'multiPatch', + functionName: 'executeWithMultiplePatches', args: [ + valueSources, + valueGetters, intent.targetAddress, + BigInt(0), // value intent.callDataToPatch, - patchOperations, + offsetGroups, intent.delegateCall ?? false, ], - }) + }) as `0x${string}` const call: ICall = { target: intent.patcherAddress, @@ -332,26 +336,52 @@ const setupCowShedPostHooks = async ( }) // Generate nonce and deadline for CoW Shed + // Use a 32-byte nonce (bytes32) const nonce = `0x${Array.from({ length: 64 }, () => Math.floor(Math.random() * 16).toString(16) ).join('')}` as `0x${string}` - const deadline = BigInt(Math.floor(Date.now() / 1000) + 7200) // now + 2 hours + + // Use a longer deadline to ensure it doesn't expire during testing + const deadline = BigInt(Math.floor(Date.now() / 1000) + 24 * 60 * 60) // now + 24 hours // Get the proxy address const shedDeterministicAddress = shedSDK.computeProxyAddress(signerAddress) - // Sign the post hooks - const hashToSign = shedSDK.hashToSignWithUser( - calls, - nonce, - deadline, - signerAddress - ) - - // Sign with the wallet - const signature = await walletClient.signMessage({ + // Sign with the wallet - use signTypedData with the exact same structure as the reference implementation + const signature = await walletClient.signTypedData({ account, - message: { raw: hashToSign }, + domain: { + name: 'COWShed', + version: '1.0.0', + chainId: BigInt(chainId), + verifyingContract: shedDeterministicAddress as `0x${string}`, + }, + types: { + ExecuteHooks: [ + { name: 'calls', type: 'Call[]' }, + { name: 'nonce', type: 'bytes32' }, + { name: 'deadline', type: 'uint256' }, + ], + Call: [ + { name: 'target', type: 'address' }, + { name: 'value', type: 'uint256' }, + { name: 'callData', type: 'bytes' }, + { name: 'allowFailure', type: 'bool' }, + { name: 'isDelegateCall', type: 'bool' }, + ], + }, + primaryType: 'ExecuteHooks', + message: { + calls: calls.map((call) => ({ + target: call.target, + value: call.value, + callData: call.callData, + allowFailure: call.allowFailure, + isDelegateCall: call.isDelegateCall, + })), + nonce, + deadline, + }, }) // Encode the post hooks call data @@ -629,7 +659,7 @@ const demoPatcher = async (): Promise> => { patcherAddress: patcherAddress as `0x${string}`, valueSource: toToken.address, valueGetter: encodeBalanceOfCall(shedProxyAddress), - targetAddress: patcherAddress as `0x${string}`, + targetAddress: DUMMY_TARGET, callDataToPatch: encodeFunctionData({ abi: DUMMY_TARGET_ABI, functionName: 'deposit', @@ -641,7 +671,7 @@ const demoPatcher = async (): Promise> => { { type: 'multiPatch', patcherAddress: patcherAddress as `0x${string}`, - targetAddress: patcherAddress as `0x${string}`, + targetAddress: DUMMY_TARGET, callDataToPatch: encodeFunctionData({ abi: DUMMY_TARGET_ABI, functionName: 'multiDeposit', @@ -659,7 +689,7 @@ const demoPatcher = async (): Promise> => { valueToReplace: baseValuePlaceholder, }, { - valueSource: patcherAddress as `0x${string}`, + valueSource: DUMMY_TARGET, valueGetter: encodeGetDoubledBalanceCall( toToken.address as string, shedProxyAddress From 7aa9fd5bf0568a5f17fc162aa4cb13970d3f56f9 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Fri, 23 May 2025 14:00:44 +0300 Subject: [PATCH 08/46] Simplify --- script/demoScripts/demoPatcher.ts | 1066 ++++++----------- script/demoScripts/utils/lib/address.ts | 19 - .../demoScripts/utils/lib/cowShedConstants.ts | 12 - script/demoScripts/utils/lib/cowShedSdk.ts | 168 --- script/demoScripts/utils/lib/formatting.ts | 9 - script/demoScripts/utils/lib/logging.ts | 22 - script/demoScripts/utils/lib/result.ts | 34 - 7 files changed, 345 insertions(+), 985 deletions(-) delete mode 100644 script/demoScripts/utils/lib/address.ts delete mode 100644 script/demoScripts/utils/lib/cowShedConstants.ts delete mode 100644 script/demoScripts/utils/lib/cowShedSdk.ts delete mode 100644 script/demoScripts/utils/lib/formatting.ts delete mode 100644 script/demoScripts/utils/lib/logging.ts delete mode 100644 script/demoScripts/utils/lib/result.ts diff --git a/script/demoScripts/demoPatcher.ts b/script/demoScripts/demoPatcher.ts index 3a13eb174..21a5da3af 100644 --- a/script/demoScripts/demoPatcher.ts +++ b/script/demoScripts/demoPatcher.ts @@ -1,360 +1,251 @@ -import { parseAbi, encodeFunctionData, parseUnits, zeroAddress } from 'viem' +#!/usr/bin/env bun + +import { + parseAbi, + parseUnits, + encodeFunctionData, + createWalletClient, + http, + getContract, + Hex, +} from 'viem' import { privateKeyToAccount } from 'viem/accounts' -import { arbitrum } from 'viem/chains' -import { createPublicClient, createWalletClient, http } from 'viem' +import { arbitrum, base } from 'viem/chains' +import { randomBytes } from 'crypto' import { ethers } from 'ethers' -import { - SupportedChainId, - OrderKind, - TradingSdk, - TradeParameters, - SwapAdvancedSettings, -} from '@cowprotocol/cow-sdk' -import { CowShedSdk } from './utils/lib/cowShedSdk' -import deploymentsArbitrum from '../../deployments/arbitrum.staging.json' - -// Import necessary types and utilities -import { logKeyValue, logSectionHeader, logSuccess } from './utils/lib/logging' -import { truncateCalldata } from './utils/lib/formatting' -import { left, Result, right } from './utils/lib/result' - -// Define CoW Protocol constants -const COW_SHED_FACTORY = - '0x00E989b87700514118Fa55326CD1cCE82faebEF6' as `0x${string}` -const COW_SHED_IMPLEMENTATION = - '0x2CFFA8cf11B90C9F437567b86352169dF4009F73' as `0x${string}` -const DUMMY_TARGET = - '0x00c8Bd044Be424E48FDCC2C171eF5adebE631C09' as `0x${string}` - -// Define interfaces and types -interface Token { - readonly address: `0x${string}` - readonly decimals: number -} - -interface ICall { - target: `0x${string}` - callData: `0x${string}` - value: bigint - allowFailure: boolean - isDelegateCall: boolean -} - -interface PatchOperation { - valueSource: `0x${string}` - valueGetter: `0x${string}` - valueToReplace: string | number | bigint -} - -type TransactionIntent = - | { - readonly type: 'regular' - readonly targetAddress: `0x${string}` - readonly callData: `0x${string}` - readonly nativeValue?: bigint - readonly allowFailure?: boolean - readonly isDelegateCall?: boolean - } - | { - readonly type: 'dynamicValue' - readonly patcherAddress: `0x${string}` - readonly valueSource: `0x${string}` - readonly valueGetter: `0x${string}` - readonly targetAddress: `0x${string}` - readonly callDataToPatch: `0x${string}` - readonly valueToReplace: string | number | bigint - readonly nativeValue?: bigint - readonly delegateCall?: boolean - } - | { - readonly type: 'multiPatch' - readonly patcherAddress: `0x${string}` - readonly targetAddress: `0x${string}` - readonly callDataToPatch: `0x${string}` - readonly patchOperations: readonly PatchOperation[] - readonly nativeValue?: bigint - readonly delegateCall?: boolean - } +import { SupportedChainId, OrderKind, TradingSdk } from '@cowprotocol/cow-sdk' +import { defineCommand, runMain } from 'citty' +import { consola } from 'consola' +import patcherArtifact from '../../out/Patcher.sol/Patcher.json' +import erc20Artifact from '../../out/ERC20/ERC20.sol/ERC20.json' + +// Constants +const ARBITRUM_WETH = '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1' +const ARBITRUM_USDC = '0xaf88d065e77c8cC2239327C5EDb3A432268e5831' +const BASE_USDC = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' +const LIFI_DIAMOND_ARBITRUM = '0xD3b2b0aC0AFdd0d166a495f5E9fca4eCc715a782' +const PATCHER_ARBITRUM = '0xE65b50EcF482f97f53557f0E02946aa27f8839EC' +const COW_SHED_FACTORY = '0x00E989b87700514118Fa55326CD1cCE82faebEF6' +const COW_SHED_IMPLEMENTATION = '0x2CFFA8cf11B90C9F437567b86352169dF4009F73' +const VAULT_RELAYER_ARBITRUM = '0xC92E8bdf79f0507f65a392b0ab4667716BFE0110' // ABIs -const ERC20_ABI = parseAbi([ - 'function balanceOf(address owner) view returns (uint256)', - 'function transfer(address to, uint256 amount) returns (bool)', - 'function approve(address spender, uint256 amount) returns (bool)', -]) - -const DUMMY_TARGET_ABI = parseAbi([ - 'function deposit(address token, uint256 amount, bool flag) returns (bool)', - 'function multiDeposit(address token, uint256 amount1, uint256 amount2, bool flag) returns (bool)', - 'function getDoubledBalance(address token, address account) view returns (uint256)', -]) - -const PATCHER_ABI = parseAbi([ - 'function executeWithDynamicPatches(address valueSource, bytes valueGetter, address finalTarget, uint256 value, bytes callDataToPatch, uint256[] offsets, bool delegateCall) returns (bool success, bytes memory returnData)', - 'function executeWithMultiplePatches(address[] valueSources, bytes[] valueGetters, address finalTarget, uint256 value, bytes callDataToPatch, uint256[][] offsetGroups, bool delegateCall) returns (bool success, bytes memory returnData)', -]) - -// Log CoW Shed constants -console.log('Using COW_SHED_FACTORY:', COW_SHED_FACTORY) -console.log('Using COW_SHED_IMPLEMENTATION:', COW_SHED_IMPLEMENTATION) - -// Helper functions -const encodeBalanceOfCall = (account: string): `0x${string}` => { - return encodeFunctionData({ - abi: ERC20_ABI, - functionName: 'balanceOf', - args: [account as `0x${string}`], - }) -} - -const encodeTransferCall = (to: string, amount: string): `0x${string}` => { - return encodeFunctionData({ - abi: ERC20_ABI, - functionName: 'transfer', - args: [to as `0x${string}`, BigInt(amount)], - }) -} - -const encodeGetDoubledBalanceCall = ( - token: string, - account: string -): `0x${string}` => { - return encodeFunctionData({ - abi: DUMMY_TARGET_ABI, - functionName: 'getDoubledBalance', - args: [token as `0x${string}`, account as `0x${string}`], - }) -} - -// Process transaction intent -const processTransactionIntent = ( - intent: TransactionIntent, - verbose: boolean -): Result => { - switch (intent.type) { - case 'regular': { - const call: ICall = { - target: intent.targetAddress, - callData: intent.callData, - value: intent.nativeValue ?? BigInt(0), - allowFailure: intent.allowFailure ?? false, - isDelegateCall: intent.isDelegateCall ?? false, - } - - if (verbose) { - logKeyValue('Call data', truncateCalldata(intent.callData, 10)) - logSuccess('Regular call created') - } - - return right(call) - } - - case 'dynamicValue': { - try { - if (!intent.patcherAddress || intent.patcherAddress === zeroAddress) { - return left(new Error('patcherAddress must be provided')) - } - - if (verbose) { - logSectionHeader(`Creating dynamic value patch call`) - logKeyValue('Call', truncateCalldata(intent.callDataToPatch, 10)) - logKeyValue('Value Source', intent.valueSource) - logKeyValue('Value Getter', truncateCalldata(intent.valueGetter, 10)) - logKeyValue('Target', intent.targetAddress) - logKeyValue('Value to replace', intent.valueToReplace.toString()) - logKeyValue( - 'Native Value', - (intent.nativeValue || BigInt(0)).toString() - ) - } - - // Create dynamic patch call - const callData = encodeFunctionData({ - abi: PATCHER_ABI, - functionName: 'executeWithDynamicPatches', - args: [ - intent.valueSource, - intent.valueGetter, - intent.targetAddress, - BigInt(0), // value - intent.callDataToPatch, - [BigInt(36)], // array of offsets - amount parameter is at offset 36 (4 bytes function selector + 32 bytes address) - intent.delegateCall ?? false, - ], - }) as `0x${string}` - - const call: ICall = { - target: intent.patcherAddress, - callData, - value: intent.nativeValue ?? BigInt(0), - allowFailure: false, - isDelegateCall: false, - } - - if (verbose) { - logSuccess('Dynamic value patch call created') - } - - return right(call) - } catch (error) { - return left( - new Error( - `Failed to create dynamic value patch: ${ - error instanceof Error ? error.message : String(error) - }` - ) - ) - } - } - - case 'multiPatch': { - try { - if (verbose) { - logSectionHeader( - `Creating multi-patch call with ${intent.patchOperations.length} operations` - ) - logKeyValue('Call data to patch', intent.callDataToPatch) - logKeyValue('Target', intent.targetAddress) - } - - // Prepare arrays for executeWithMultiplePatches - const valueSources = intent.patchOperations.map( - (op) => op.valueSource - ) as `0x${string}`[] - const valueGetters = intent.patchOperations.map( - (op) => op.valueGetter - ) as `0x${string}`[] - - // For multiDeposit function: - // baseAmount parameter is at offset 36 (4 bytes function selector + 32 bytes address) - // doubledAmount parameter is at offset 68 (4 bytes function selector + 32 bytes address + 32 bytes baseAmount) - const offsetGroups = [ - [BigInt(36)], // First operation patches at offset 36 - [BigInt(68)], // Second operation patches at offset 68 - ] as readonly (readonly bigint[])[] - - // Create multi-patch call - const callData = encodeFunctionData({ - abi: PATCHER_ABI, - functionName: 'executeWithMultiplePatches', - args: [ - valueSources, - valueGetters, - intent.targetAddress, - BigInt(0), // value - intent.callDataToPatch, - offsetGroups, - intent.delegateCall ?? false, - ], - }) as `0x${string}` - - const call: ICall = { - target: intent.patcherAddress, - callData, - value: intent.nativeValue ?? BigInt(0), - allowFailure: false, - isDelegateCall: false, - } - - if (verbose) { - logSuccess('Multi-patch call created') - } - - return right(call) - } catch (error) { - return left( - new Error( - `Failed to create multi-patch: ${ - error instanceof Error ? error.message : String(error) - }` - ) - ) - } - } +const ERC20_ABI = erc20Artifact.abi +const PATCHER_ABI = patcherArtifact.abi + +/** + * CowShed SDK for computing deterministic proxy addresses and encoding hook calls + */ +class CowShedSdk { + factoryAddress: string + implementationAddress: string + chainId: number + + constructor({ + factoryAddress, + implementationAddress, + chainId, + }: { + factoryAddress: string + implementationAddress: string + chainId: number + }) { + this.factoryAddress = factoryAddress + this.implementationAddress = implementationAddress + this.chainId = chainId } -} -// Using the CowShedSdk implementation from utils/lib/cowShedSdk.ts - -// Real implementation of CoW Protocol functions -const calculateCowShedProxyAddress = async ( - chainId: number, - owner: string -): Promise => { - console.log('calculateCowShedProxyAddress called with:', { chainId, owner }) + // Compute the deterministic proxy address for a user + computeProxyAddress(owner: string): string { + // This uses CREATE2 to compute the deterministic address + const salt = ethers.utils.solidityKeccak256( + ['address', 'address'], + [owner, this.implementationAddress] + ) - if (!owner || owner === '0x0000000000000000000000000000000000000000') { - throw new Error('Owner address is undefined or empty') - } + const proxyBytecode = + '0x' + + ethers.utils + .solidityPack( + ['bytes', 'address'], + [ + '0x3d602d80600a3d3981f3363d3d373d3d3d363d73', + this.implementationAddress, + ] + ) + .slice(2) + + '5af43d82803e903d91602b57fd5bf3' - console.log('Using COW_SHED_FACTORY:', COW_SHED_FACTORY) - console.log('Using COW_SHED_IMPLEMENTATION:', COW_SHED_IMPLEMENTATION) + const create2Input = ethers.utils.solidityKeccak256( + ['bytes'], + [ + ethers.utils.solidityPack( + ['bytes1', 'address', 'bytes32', 'bytes32'], + [ + '0xff', + this.factoryAddress, + salt, + ethers.utils.keccak256(proxyBytecode), + ] + ), + ] + ) - // Validate inputs before creating the SDK - if ( - !COW_SHED_FACTORY || - COW_SHED_FACTORY === '0x0000000000000000000000000000000000000000' - ) { - throw new Error('COW_SHED_FACTORY is undefined or invalid') + return ethers.utils.getAddress('0x' + create2Input.slice(26)) } - if ( - !COW_SHED_IMPLEMENTATION || - COW_SHED_IMPLEMENTATION === '0x0000000000000000000000000000000000000000' - ) { - throw new Error('COW_SHED_IMPLEMENTATION is undefined or invalid') + // Encode the executeHooks call for the factory + static encodeExecuteHooksForFactory( + calls: any[], + nonce: string, + deadline: bigint, + owner: string, + signature: string + ): string { + const cowShedFactoryAbi = parseAbi([ + 'function deployProxyAndExecuteHooks(address owner, address implementation, (address target, uint256 value, bytes callData, bool allowFailure, bool isDelegateCall)[] calls, bytes32 nonce, uint256 deadline, bytes signature) returns (address proxy)', + ]) + + return encodeFunctionData({ + abi: cowShedFactoryAbi, + functionName: 'deployProxyAndExecuteHooks', + args: [ + owner, + COW_SHED_IMPLEMENTATION, + calls.map((call) => ({ + target: call.target, + value: call.value, + callData: call.callData, + allowFailure: call.allowFailure, + isDelegateCall: call.isDelegateCall, + })), + nonce, + deadline, + signature, + ], + }) } - - // Use our new CowShedSdk implementation - const shedSDK = new CowShedSdk({ - factoryAddress: COW_SHED_FACTORY, - implementationAddress: COW_SHED_IMPLEMENTATION, - chainId, - }) - - const proxyAddress = shedSDK.computeProxyAddress(owner) - console.log('Computed proxy address:', proxyAddress) - return proxyAddress } -const setupCowShedPostHooks = async ( +/** + * Setup CowShed post hooks for bridging USDC to BASE + */ +async function setupCowShedPostHooks( chainId: number, walletClient: any, - calls: ICall[], - verbose: boolean -): Promise<{ shedDeterministicAddress: string; postHooks: any[] }> => { + usdcAddress: string, + receivedAmount: bigint +) { const account = walletClient.account const signerAddress = account.address - console.log('Using COW_SHED_FACTORY:', COW_SHED_FACTORY) - console.log('Using COW_SHED_IMPLEMENTATION:', COW_SHED_IMPLEMENTATION) - const shedSDK = new CowShedSdk({ factoryAddress: COW_SHED_FACTORY, implementationAddress: COW_SHED_IMPLEMENTATION, chainId, }) - // Generate nonce and deadline for CoW Shed - // Use a 32-byte nonce (bytes32) + // Generate a random nonce const nonce = `0x${Array.from({ length: 64 }, () => Math.floor(Math.random() * 16).toString(16) - ).join('')}` as `0x${string}` + ).join('')}` - // Use a longer deadline to ensure it doesn't expire during testing - const deadline = BigInt(Math.floor(Date.now() / 1000) + 24 * 60 * 60) // now + 24 hours + // Set a deadline 24 hours from now + const deadline = BigInt(Math.floor(Date.now() / 1000) + 24 * 60 * 60) // Get the proxy address const shedDeterministicAddress = shedSDK.computeProxyAddress(signerAddress) + consola.info(`CowShed proxy address: ${shedDeterministicAddress}`) + + // Create the bridge data for LiFi + const bridgeData = { + transactionId: `0x${randomBytes(32).toString('hex')}`, + bridge: 'across', + integrator: 'lifi-demo', + referrer: '0x0000000000000000000000000000000000000000', + sendingAssetId: usdcAddress, + receiver: signerAddress, + destinationChainId: 8453, // BASE chain ID + minAmount: receivedAmount, + hasSourceSwaps: false, + hasDestinationCall: false, + } + + // Create AcrossV3Data + const acrossData = { + receiverAddress: signerAddress, + refundAddress: signerAddress, + receivingAssetId: BASE_USDC, // USDC on BASE + outputAmount: 0n, // This will be patched dynamically + outputAmountPercent: parseUnits('0.995', 18), // 0.5% fee (example) + exclusiveRelayer: '0x0000000000000000000000000000000000000000', + quoteTimestamp: Math.floor(Date.now() / 1000), + fillDeadline: Math.floor(Date.now() / 1000) + 3600, // 1 hour + exclusivityDeadline: Math.floor(Date.now() / 1000), + message: '0x', // No message + } - // Sign with the wallet - use signTypedData with the exact same structure as the reference implementation + // Encode the AcrossFacetV3 call + const acrossFacetAbi = parseAbi([ + 'function startBridgeTokensViaAcrossV3((bytes32 transactionId, string bridge, string integrator, address referrer, address sendingAssetId, address receiver, uint256 destinationChainId, uint256 minAmount, bool hasSourceSwaps, bool hasDestinationCall) _bridgeData, (address receiverAddress, address refundAddress, address receivingAssetId, uint256 outputAmount, uint64 outputAmountPercent, address exclusiveRelayer, uint32 quoteTimestamp, uint32 fillDeadline, uint32 exclusivityDeadline, bytes message) _acrossData) payable', + ]) + + const acrossCalldata = encodeFunctionData({ + abi: acrossFacetAbi, + functionName: 'startBridgeTokensViaAcrossV3', + args: [bridgeData, acrossData], + }) + + // Calculate the offset for the outputAmount field in the AcrossV3Data struct + // This is a fixed offset in the calldata where the outputAmount value needs to be patched + // The offset is calculated based on the ABI encoding of the function call + const outputAmountOffset = 644 // This offset needs to be calculated correctly + + // Encode the balanceOf call to get the USDC balance + const valueGetter = encodeFunctionData({ + abi: parseAbi([ + 'function balanceOf(address account) view returns (uint256)', + ]), + functionName: 'balanceOf', + args: [shedDeterministicAddress], + }) + + // Encode the patcher call + const patcherCalldata = encodeFunctionData({ + abi: parseAbi([ + 'function executeWithDynamicPatches(address valueSource, bytes valueGetter, address finalTarget, uint256 value, bytes data, uint256[] offsets, bool delegateCall) returns (bool success, bytes returnData)', + ]), + functionName: 'executeWithDynamicPatches', + args: [ + usdcAddress, // valueSource - USDC contract + valueGetter, // valueGetter - balanceOf call + LIFI_DIAMOND_ARBITRUM, // finalTarget - LiFiDiamond contract + 0n, // value - no ETH being sent + acrossCalldata, // data - the encoded AcrossFacetV3 call + [outputAmountOffset], // offsets - position of outputAmount in the calldata + false, // delegateCall - regular call, not delegateCall + ], + }) + + // Define the post-swap call to the patcher + const postSwapCalls = [ + { + target: PATCHER_ARBITRUM, + callData: patcherCalldata, + value: 0n, + allowFailure: false, + isDelegateCall: false, + }, + ] + + // Sign the typed data for the hooks const signature = await walletClient.signTypedData({ account, domain: { name: 'COWShed', version: '1.0.0', chainId: BigInt(chainId), - verifyingContract: shedDeterministicAddress as `0x${string}`, + verifyingContract: shedDeterministicAddress, }, types: { ExecuteHooks: [ @@ -372,7 +263,7 @@ const setupCowShedPostHooks = async ( }, primaryType: 'ExecuteHooks', message: { - calls: calls.map((call) => ({ + calls: postSwapCalls.map((call) => ({ target: call.target, value: call.value, callData: call.callData, @@ -386,459 +277,192 @@ const setupCowShedPostHooks = async ( // Encode the post hooks call data const shedEncodedPostHooksCallData = CowShedSdk.encodeExecuteHooksForFactory( - calls, + postSwapCalls, nonce, deadline, signerAddress, signature ) - // Log the encoded post hooks calldata - console.log('Encoded post hooks calldata:') - console.log(`Calldata: ${shedEncodedPostHooksCallData}`) - console.log(`Calldata length: ${shedEncodedPostHooksCallData.length} bytes`) - // Create the post hooks const postHooks = [ { target: COW_SHED_FACTORY, callData: shedEncodedPostHooksCallData, - gasLimit: '3000000', // Increased gas limit for full calldata + gasLimit: '3000000', }, ] - // Log information if verbose is true - if (verbose) { - logKeyValue('CoW-Shed deterministic address', shedDeterministicAddress) - logSuccess('Post hook ready for execution through CoW Protocol') - logKeyValue( - 'CoW-Shed encoded post-hook', - truncateCalldata(shedEncodedPostHooksCallData, 25) - ) - } - return { shedDeterministicAddress, postHooks, } } -const cowFlow = async ( - chainId: number, - walletClient: any, - fromToken: Token, - toToken: Token, - fromAmount: bigint, - postReceiver: string, - preHooks: readonly any[], - postHooks: readonly any[] -): Promise> => { +/** + * Main function to execute the demo + */ +async function main(options: { privateKey: string; dryRun: boolean }) { try { - console.log('Executing CoW Protocol flow...') - console.log('From token:', fromToken.address) - console.log('To token:', toToken.address) - console.log('Amount:', fromAmount.toString()) - console.log('Receiver:', postReceiver) - console.log('Post hooks count:', postHooks.length) - - // Get the private key from the environment - const privateKeyRaw = process.env.PRIVATE_KEY - if (!privateKeyRaw) { - throw new Error('PRIVATE_KEY environment variable is not set') - } + consola.start('Starting CowSwap with Patcher demo') - // Ensure the private key has the correct format - const privateKey = privateKeyRaw.startsWith('0x') - ? privateKeyRaw - : `0x${privateKeyRaw}` + // Set up wallet client + const account = privateKeyToAccount(options.privateKey as Hex) + const walletClient = createWalletClient({ + chain: arbitrum, + transport: http(), + account, + }) - // Create an ethers.js wallet from the private key - const provider = new ethers.providers.JsonRpcProvider( - process.env.ETH_NODE_URI_ARBITRUM - ) - const ethersSigner = new ethers.Wallet(privateKey, provider) + const walletAddress = account.address + consola.info(`Connected wallet: ${walletAddress}`) - // Initialize the CoW Protocol SDK with the ethers.js signer for Arbitrum - const cowSdk = new TradingSdk({ - chainId: SupportedChainId.ARBITRUM_ONE, - signer: ethersSigner, - appCode: 'LiFi', + // Amount to swap: 0.001 WETH + const swapAmount = parseUnits('0.001', 18) + consola.info(`Swap amount: 0.001 WETH`) + + // Check WETH balance and approve if needed + const wethContract = getContract({ + address: ARBITRUM_WETH as Hex, + abi: ERC20_ABI, + client: { public: walletClient, wallet: walletClient }, }) - // Get the VaultRelayer address for Arbitrum - const vaultRelayerAddress = '0xC92E8bdf79f0507f65a392b0ab4667716BFE0110' - console.log(`VaultRelayer address: ${vaultRelayerAddress}`) + const wethBalance = await wethContract.read.balanceOf([walletAddress]) + consola.info(`WETH balance: ${wethBalance}`) - // Create ERC20 contract instance for the sell token - const sellTokenContract = new ethers.Contract( - fromToken.address, - [ - 'function approve(address spender, uint256 amount) public returns (bool)', - 'function allowance(address owner, address spender) public view returns (uint256)', - ], - ethersSigner + if (wethBalance < swapAmount) { + consola.error(`Insufficient WETH balance. Need at least 0.001 WETH.`) + process.exit(1) + } + + // Check allowance + const allowance = await wethContract.read.allowance([ + walletAddress, + VAULT_RELAYER_ARBITRUM, + ]) + consola.info(`Current allowance: ${allowance}`) + + if (allowance < swapAmount) { + consola.info('Approving WETH for CoW Protocol VaultRelayer...') + if (!options.dryRun) { + const approveTx = await wethContract.write.approve([ + VAULT_RELAYER_ARBITRUM, + BigInt( + '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' + ), // Max uint256 + ]) + consola.success(`Approval transaction sent: ${approveTx}`) + } else { + consola.info(`[DRY RUN] Would approve WETH for VaultRelayer`) + } + } + + // Set up CowShed post hooks + const { shedDeterministicAddress, postHooks } = await setupCowShedPostHooks( + 42161, // Arbitrum chain ID + walletClient, + ARBITRUM_USDC, + parseUnits('0', 6) // This will be dynamically patched ) - // Check current allowance - const currentAllowance = await sellTokenContract.allowance( - ethersSigner.address, - vaultRelayerAddress + // Create ethers provider and signer for CoW SDK + const provider = new ethers.providers.JsonRpcProvider( + arbitrum.rpcUrls.default.http[0] ) - console.log(`Current allowance: ${currentAllowance.toString()}`) - - // If allowance is insufficient, approve the token - if (currentAllowance.lt(fromAmount)) { - console.log('Approving token for CoW Protocol VaultRelayer...') - const maxApproval = ethers.constants.MaxUint256 - const approveTx = await sellTokenContract.approve( - vaultRelayerAddress, - maxApproval - ) - console.log(`Approval transaction hash: ${approveTx.hash}`) - - // Wait for the approval transaction to be confirmed - console.log('Waiting for approval transaction to be confirmed...') - await approveTx.wait() - console.log('Token approved successfully') - } else { - console.log('Token already has sufficient allowance') - } + const ethersSigner = new ethers.Wallet(options.privateKey, provider) + + // Initialize CoW SDK + const cowSdk = new TradingSdk({ + chainId: SupportedChainId.ARBITRUM, + signer: ethersSigner, + appCode: 'lifi-demo', + }) // Create the order parameters - const parameters: TradeParameters = { + const parameters = { kind: OrderKind.SELL, - sellToken: fromToken.address, - sellTokenDecimals: fromToken.decimals, - buyToken: toToken.address, - buyTokenDecimals: toToken.decimals, - amount: fromAmount.toString(), - receiver: postReceiver, - // Add a reasonable validity period (30 minutes) + sellToken: ARBITRUM_WETH, + sellTokenDecimals: 18, + buyToken: ARBITRUM_USDC, + buyTokenDecimals: 6, + amount: swapAmount.toString(), + receiver: shedDeterministicAddress, // Important: Set the receiver to the CowShed proxy validFor: 30 * 60, // 30 minutes in seconds - // Add a reasonable slippage (0.5%) - slippageBps: 50, - } - - // Add post hooks - this script requires post hooks - if (!postHooks || postHooks.length === 0) { - return left( - new Error('Post hooks are required for this script to function') - ) + slippageBps: 50, // 0.5% slippage } - // Create post hooks with full calldata - const simplifiedPostHooks = postHooks.map((hook) => ({ - target: hook.target, - callData: hook.callData, // Use the full calldata without truncation - gasLimit: '3000000', - })) - - // Log the full calldata for debugging - console.log('Full post hook calldata:') - simplifiedPostHooks.forEach((hook, index) => { - console.log(`Hook ${index + 1} target: ${hook.target}`) - console.log(`Hook ${index + 1} calldata: ${hook.callData}`) - console.log( - `Hook ${index + 1} calldata length: ${hook.callData.length} bytes` - ) - }) - - // Create advanced settings with the correct format - const advancedSettings: SwapAdvancedSettings = { + // Create advanced settings with post hooks + const advancedSettings = { appData: { metadata: { hooks: { version: '1', pre: [], - post: simplifiedPostHooks, + post: postHooks, }, }, }, } // Submit the order with post hooks - const orderId = await cowSdk.postSwapOrder(parameters, advancedSettings) - return right(orderId) - } catch (error) { - console.error('CoW Protocol error details:', error) - return left( - new Error( - `Failed to execute CoW flow: ${ - error instanceof Error ? error.message : String(error) - }` - ) - ) - } -} - -// Main demo function -const demoPatcher = async (): Promise> => { - try { - logSectionHeader('Running Patcher Example with Post-Swap Flow') - - // Setup environment - console.log('Setting up environment...') - - // Get private key and ensure it has the correct format - const privateKeyRaw = process.env.PRIVATE_KEY - if (!privateKeyRaw) { - throw new Error('PRIVATE_KEY environment variable is not set') - } - - // Ensure the private key has the correct format (0x prefix) - const privateKey = privateKeyRaw.startsWith('0x') - ? (privateKeyRaw as `0x${string}`) - : (`0x${privateKeyRaw}` as `0x${string}`) - - console.log('Creating account...') - const account = privateKeyToAccount(privateKey) - console.log('Account address:', account.address) - - console.log('Creating clients for Arbitrum...') - const rpcUrl = process.env.ETH_NODE_URI_ARBITRUM - if (!rpcUrl) { - throw new Error('ETH_NODE_URI_ARBITRUM environment variable is not set') - } - - const publicClient = createPublicClient({ - chain: arbitrum, - transport: http(rpcUrl), - }) - - const walletClient = createWalletClient({ - chain: arbitrum, - transport: http(rpcUrl), - account, - }) - - console.log('Getting contract addresses...') - // Get the Patcher address from deployments - const patcherAddress = deploymentsArbitrum.Patcher - if ( - !patcherAddress || - patcherAddress === '0x0000000000000000000000000000000000000000' - ) { - throw new Error( - 'Patcher address not found in deployments or is zero address' - ) - } - console.log('Patcher address:', patcherAddress) - - const baseValuePlaceholder = '1000000000000000000' - const doubledValuePlaceholder = '2000000000000000000' - - const originalTokenOwner = account.address - // Use the correct Arbitrum chain ID - const chainId = 42161 // Arbitrum One - - // Define token information for Arbitrum - console.log('Setting up token information for Arbitrum...') - const fromToken = { - address: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1' as `0x${string}`, // WETH on Arbitrum - decimals: 18, - } - - const toToken = { - address: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831' as `0x${string}`, // USDC on Arbitrum - decimals: 6, - } - - // Calculate CoW Shed proxy address - console.log('Calculating CoW Shed proxy address...') - const shedProxyAddress = await calculateCowShedProxyAddress( - chainId, - originalTokenOwner - ) - console.log('Shed proxy address:', shedProxyAddress) - - // Define transaction intents - console.log('Defining transaction intents...') - const transactionIntents: readonly TransactionIntent[] = [ - { - type: 'dynamicValue', - patcherAddress: patcherAddress as `0x${string}`, - valueSource: toToken.address, - valueGetter: encodeBalanceOfCall(shedProxyAddress), - targetAddress: DUMMY_TARGET, - callDataToPatch: encodeFunctionData({ - abi: DUMMY_TARGET_ABI, - functionName: 'deposit', - args: [toToken.address, BigInt(baseValuePlaceholder), true], - }), - valueToReplace: baseValuePlaceholder, - nativeValue: BigInt(0), - }, - { - type: 'multiPatch', - patcherAddress: patcherAddress as `0x${string}`, - targetAddress: DUMMY_TARGET, - callDataToPatch: encodeFunctionData({ - abi: DUMMY_TARGET_ABI, - functionName: 'multiDeposit', - args: [ - toToken.address, - BigInt(baseValuePlaceholder), - BigInt(doubledValuePlaceholder), - true, - ], - }), - patchOperations: [ - { - valueSource: toToken.address, - valueGetter: encodeBalanceOfCall(shedProxyAddress), - valueToReplace: baseValuePlaceholder, - }, - { - valueSource: DUMMY_TARGET, - valueGetter: encodeGetDoubledBalanceCall( - toToken.address as string, - shedProxyAddress - ), - valueToReplace: doubledValuePlaceholder, - }, - ], - nativeValue: BigInt(0), - }, - { - type: 'dynamicValue', - patcherAddress: patcherAddress as `0x${string}`, - valueSource: toToken.address, - valueGetter: encodeBalanceOfCall(shedProxyAddress), - targetAddress: toToken.address, - callDataToPatch: encodeTransferCall( - originalTokenOwner, - baseValuePlaceholder - ), - valueToReplace: baseValuePlaceholder, - nativeValue: BigInt(0), - }, - ] - - logKeyValue('Patcher Address', patcherAddress) - logKeyValue('From Token', fromToken.address) - logKeyValue('To Token', toToken.address) - - // Process transaction intents - console.log('Processing transaction intents...') - const processedIntents = transactionIntents.map((intent, index) => { - logSectionHeader(`Processing intent ${index + 1} (type: ${intent.type})`) - return processTransactionIntent(intent, true) - }) - - // Check for any errors in the processed intents - console.log('Checking for errors in processed intents...') - const errorResult = processedIntents.find( - (result) => result._type === 'Left' - ) - if (errorResult && errorResult._type === 'Left') { - return errorResult - } - - // Extract the successful calls - console.log('Extracting successful calls...') - const calls = processedIntents - .filter( - (result): result is { readonly _type: 'Right'; readonly data: ICall } => - result._type === 'Right' - ) - .map((result) => result.data) - - // Setup post hooks for CoW Shed - console.log('Setting up post hooks for CoW Shed...') - const setupResult = await setupCowShedPostHooks( - chainId, - walletClient, - calls, - true - ) - - const { shedDeterministicAddress, postHooks } = setupResult - - // Amount to swap (for example, 0.1 token) - console.log('Calculating swap amount...') - const fromAmount = parseUnits('0.001', fromToken.decimals) - - logSectionHeader('Creating CoW Swap Order') - logKeyValue('From Token', fromToken.address) - logKeyValue('To Token', toToken.address) - logKeyValue('Amount', fromAmount.toString()) - logKeyValue('Token Receiver', shedProxyAddress) - logKeyValue('Post-hook receiver', shedDeterministicAddress) - - // Log the original post hooks - console.log('Original post hooks:') - postHooks.forEach((hook, index) => { - console.log(`Original hook ${index + 1} target: ${hook.target}`) - console.log(`Original hook ${index + 1} calldata: ${hook.callData}`) - console.log( - `Original hook ${index + 1} calldata length: ${ - hook.callData.length - } bytes` - ) - }) - - // Execute the CoW Protocol flow - console.log('Executing CoW Protocol flow...') - const cowHashResult = await cowFlow( - chainId, - walletClient, - fromToken, - toToken, - fromAmount, - shedProxyAddress, - [], - [...postHooks] - ) - - if (cowHashResult._type === 'Left') { - return cowHashResult - } - - const orderHash = cowHashResult.data - - logSectionHeader('CoW Swap Order Created') - logSuccess('Order submitted successfully') - logKeyValue('Order hash', orderHash) - logKeyValue( - 'Explorer URL', - `https://explorer.cow.fi/orders/${orderHash}?chainId=${chainId}` - ) - - return right( - `Patcher integrated with CoW Protocol - Order created with hash: ${orderHash}` - ) - } catch (error) { - console.error('Error in demoPatcher:', error) - return left( - new Error( - `Failed to execute patcher demo: ${ - error instanceof Error ? error.message : String(error) - }` - ) - ) - } -} + if (!options.dryRun) { + consola.info('Submitting order to CowSwap...') + try { + // Add a timeout to the order submission + const orderPromise = cowSdk.postSwapOrder(parameters, advancedSettings) + const timeoutPromise = new Promise((_, reject) => { + setTimeout( + () => + reject(new Error('Order submission timed out after 30 seconds')), + 30000 + ) + }) -async function main() { - try { - const result = await demoPatcher() - if (result && result._type === 'Left') { - console.error( - `Error: ${result.error ? result.error.message : 'Unknown error'}` - ) - process.exit(1) - } else if (result && result._type === 'Right') { - console.log(result.data) - process.exit(0) + const orderId = await Promise.race([orderPromise, timeoutPromise]) + consola.success(`Order created with hash: ${orderId}`) + consola.info( + `Explorer URL: https://explorer.cow.fi/orders/${orderId}?chainId=42161` + ) + } catch (error) { + consola.error('Error submitting order to CowSwap:', error) + throw error + } } else { - console.error('Unexpected result format') - process.exit(1) + consola.info(`[DRY RUN] Would submit order to CowSwap with post hooks`) + consola.info(`Parameters: ${JSON.stringify(parameters, null, 2)}`) + consola.info(`Post hooks: ${JSON.stringify(postHooks, null, 2)}`) } + + consola.success('Demo completed successfully') } catch (error) { - console.error('Unexpected error:', error) + consola.error('Error executing demo:', error) process.exit(1) } } -// Run the script -main() +// CLI command definition +const cmd = defineCommand({ + meta: { + name: 'demoPatcher', + description: 'Demo script for CowSwap with Patcher contract', + }, + args: { + privateKey: { + type: 'string', + description: 'Private key for the wallet', + required: true, + }, + dryRun: { + type: 'boolean', + description: 'Run in dry-run mode without submitting transactions', + default: false, + }, + }, + run: async ({ args }) => { + await main(args) + }, +}) + +// Run the command +runMain(cmd) diff --git a/script/demoScripts/utils/lib/address.ts b/script/demoScripts/utils/lib/address.ts deleted file mode 100644 index 5c417747a..000000000 --- a/script/demoScripts/utils/lib/address.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Utility functions for working with Ethereum addresses - */ - -/** - * Formats an address string to ensure it has the 0x prefix and is properly formatted - * @param address The address to format - * @returns The formatted address with 0x prefix - */ -export function formatAddress(address: string): `0x${string}` { - // Ensure the address has the 0x prefix - const prefixedAddress = address.startsWith('0x') ? address : `0x${address}` - - // Ensure the address is lowercase for consistency - const formattedAddress = prefixedAddress.toLowerCase() - - // Return the address with the correct type - return formattedAddress as `0x${string}` -} \ No newline at end of file diff --git a/script/demoScripts/utils/lib/cowShedConstants.ts b/script/demoScripts/utils/lib/cowShedConstants.ts deleted file mode 100644 index cc16c6c7c..000000000 --- a/script/demoScripts/utils/lib/cowShedConstants.ts +++ /dev/null @@ -1,12 +0,0 @@ -// Constants for CoW Shed SDK -import { parseAbi } from 'viem' - -export const FACTORY_ABI = parseAbi([ - 'function executeHooks((address,uint256,bytes,bool,bool)[] calls, bytes32 nonce, uint256 deadline, address user, bytes signature) external', -]) - -export const SHED_ABI = parseAbi([ - 'function executeHooks((address,uint256,bytes,bool,bool)[] calls, bytes32 nonce, uint256 deadline, bytes signature) external', -]) - -export const PROXY_CREATION_CODE = '0x60a034608e57601f61037138819003918201601f19168301916001600160401b038311848410176093578084926040948552833981010312608e57604b602060458360a9565b920160a9565b6080527f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc556040516102b490816100bd8239608051818181608f01526101720152f35b600080fd5b634e487b7160e01b600052604160045260246000fd5b51906001600160a01b0382168203608e5756fe60806040526004361015610018575b3661019457610194565b6000803560e01c908163025b22bc1461003b575063f851a4400361000e5761010d565b3461010a5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261010a5773ffffffffffffffffffffffffffffffffffffffff60043581811691828203610106577f0000000000000000000000000000000000000000000000000000000000000000163314600014610101577f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc557fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b8280a280f35b61023d565b8380fd5b80fd5b346101645760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610164576020610146610169565b73ffffffffffffffffffffffffffffffffffffffff60405191168152f35b600080fd5b333003610101577f000000000000000000000000000000000000000000000000000000000000000090565b60ff7f68df44b1011761f481358c0f49a711192727fb02c377d697bcb0ea8ff8393ac0541615806101ef575b1561023d5760046040517ff92ee8a9000000000000000000000000000000000000000000000000000000008152fd5b507f400ada75000000000000000000000000000000000000000000000000000000007fffffffff000000000000000000000000000000000000000000000000000000006000351614156101c0565b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc546000808092368280378136915af43d82803e1561027a573d90f35b3d90fdfea2646970667358221220c7c26ff3040b96a28e96d6d27b743972943aeaef81cc821544c5fe1e24f9b17264736f6c63430008190033' \ No newline at end of file diff --git a/script/demoScripts/utils/lib/cowShedSdk.ts b/script/demoScripts/utils/lib/cowShedSdk.ts deleted file mode 100644 index 5739ef994..000000000 --- a/script/demoScripts/utils/lib/cowShedSdk.ts +++ /dev/null @@ -1,168 +0,0 @@ -/** - * CoW Shed SDK implementation using viem - */ -import { - getCreate2Address, - keccak256, - concat, - encodeAbiParameters, - parseAbiParameters, - hashTypedData, - encodeFunctionData, - Address, -} from 'viem' -import { FACTORY_ABI, PROXY_CREATION_CODE, SHED_ABI } from './cowShedConstants' -import { formatAddress } from './address' - -export interface ISdkOptions { - factoryAddress: string - implementationAddress: string - proxyCreationCode?: string - chainId: number -} - -export interface ICall { - target: string - value: bigint - callData: string - allowFailure: boolean - isDelegateCall: boolean -} - -export class CowShedSdk { - private factoryAddress: string - private implementationAddress: string - private proxyCreationCode: string - private chainId: number - - constructor(options: ISdkOptions) { - this.factoryAddress = options.factoryAddress - this.implementationAddress = options.implementationAddress - this.proxyCreationCode = options.proxyCreationCode || PROXY_CREATION_CODE - this.chainId = options.chainId - } - - computeProxyAddress(user: string): string { - // Format addresses to ensure they're valid - const formattedFactoryAddress = formatAddress(this.factoryAddress) - const formattedImplementationAddress = formatAddress(this.implementationAddress) - const formattedUserAddress = formatAddress(user) - - // Create the salt from the user address - const salt = encodeAbiParameters( - parseAbiParameters('address'), - [formattedUserAddress] - ) - - // Create the init code by concatenating the proxy creation code and the encoded constructor arguments - const initCode = concat([ - this.proxyCreationCode as `0x${string}`, - encodeAbiParameters( - parseAbiParameters('address, address'), - [formattedImplementationAddress, formattedUserAddress] - ) - ]) - - // Calculate the init code hash - const initCodeHash = keccak256(initCode) - - // Calculate the CREATE2 address - const proxyAddress = getCreate2Address({ - from: formattedFactoryAddress, - salt: salt, - bytecodeHash: initCodeHash, - }) - - return proxyAddress - } - - hashToSignWithUser( - calls: ICall[], - nonce: `0x${string}`, - deadline: bigint, - user: string - ): `0x${string}` { - const proxy = this.computeProxyAddress(user) - return this.hashToSign(calls, nonce, deadline, proxy) - } - - private hashToSign( - calls: ICall[], - nonce: `0x${string}`, - deadline: bigint, - proxy: string - ): `0x${string}` { - const domain = { - name: 'COWShed', - version: '1.0.0', - chainId: this.chainId, - verifyingContract: proxy as `0x${string}`, - } - - const types = { - ExecuteHooks: [ - { name: 'calls', type: 'Call[]' }, - { name: 'nonce', type: 'bytes32' }, - { name: 'deadline', type: 'uint256' }, - ], - Call: [ - { name: 'target', type: 'address' }, - { name: 'value', type: 'uint256' }, - { name: 'callData', type: 'bytes' }, - { name: 'allowFailure', type: 'bool' }, - { name: 'isDelegateCall', type: 'bool' }, - ], - } - - const message = { - calls: calls.map((call) => ({ - target: call.target as `0x${string}`, - value: call.value, - callData: call.callData as `0x${string}`, - allowFailure: call.allowFailure, - isDelegateCall: call.isDelegateCall, - })), - nonce, - deadline, - } - - return hashTypedData({ - domain, - types, - primaryType: 'ExecuteHooks', - message, - }) - } - - static encodeExecuteHooksForFactory( - calls: ICall[], - nonce: `0x${string}`, - deadline: bigint, - user: string, - signature: `0x${string}` - ): string { - // Format the user address - const formattedUser = formatAddress(user) - - // Convert calls to the expected format for the ABI - const formattedCalls = calls.map(call => [ - formatAddress(call.target), - call.value, - call.callData as `0x${string}`, - call.allowFailure, - call.isDelegateCall - ] as const) - - return encodeFunctionData({ - abi: FACTORY_ABI, - functionName: 'executeHooks', - args: [ - formattedCalls, - nonce, - deadline, - formattedUser, - signature, - ], - }) - } -} \ No newline at end of file diff --git a/script/demoScripts/utils/lib/formatting.ts b/script/demoScripts/utils/lib/formatting.ts deleted file mode 100644 index 3f4af7cc5..000000000 --- a/script/demoScripts/utils/lib/formatting.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Truncate calldata for display purposes - */ -export function truncateCalldata(calldata: string, length: number = 10): string { - if (!calldata || calldata.length <= length * 2) return calldata - - const prefix = calldata.substring(0, length * 2) - return `${prefix}...` -} \ No newline at end of file diff --git a/script/demoScripts/utils/lib/logging.ts b/script/demoScripts/utils/lib/logging.ts deleted file mode 100644 index 025f24a0d..000000000 --- a/script/demoScripts/utils/lib/logging.ts +++ /dev/null @@ -1,22 +0,0 @@ -// Simple logging utilities for demo scripts - -/** - * Log a key-value pair with formatting - */ -export function logKeyValue(key: string, value: string | number | bigint): void { - console.log(`${key}: ${value}`) -} - -/** - * Log a section header with formatting - */ -export function logSectionHeader(text: string): void { - console.log(`\n=== ${text} ===`) -} - -/** - * Log a success message with formatting - */ -export function logSuccess(text: string): void { - console.log(`✅ ${text}`) -} \ No newline at end of file diff --git a/script/demoScripts/utils/lib/result.ts b/script/demoScripts/utils/lib/result.ts deleted file mode 100644 index 072059a38..000000000 --- a/script/demoScripts/utils/lib/result.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Result type for handling success and error cases - */ -export type Result = Left | Right - -/** - * Left type for error cases - */ -export interface Left { - readonly _type: 'Left' - readonly error: E -} - -/** - * Right type for success cases - */ -export interface Right { - readonly _type: 'Right' - readonly data: T -} - -/** - * Create a Left result (error case) - */ -export function left(error: E): Result { - return { _type: 'Left', error } -} - -/** - * Create a Right result (success case) - */ -export function right(data: T): Result { - return { _type: 'Right', data } -} \ No newline at end of file From 3d4031077996551802f9d444fdc7bfbeb8db6f8a Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Fri, 23 May 2025 15:51:59 +0300 Subject: [PATCH 09/46] fix cow swap submission --- script/demoScripts/demoPatcher.ts | 127 ++++++++++++++++++++---------- 1 file changed, 84 insertions(+), 43 deletions(-) diff --git a/script/demoScripts/demoPatcher.ts b/script/demoScripts/demoPatcher.ts index 21a5da3af..2d5685816 100644 --- a/script/demoScripts/demoPatcher.ts +++ b/script/demoScripts/demoPatcher.ts @@ -1,5 +1,13 @@ #!/usr/bin/env bun +/** + * Demo script for CowSwap with Patcher contract + * + * Note: There are some TypeScript errors related to the `0x${string}` type that could be fixed + * with more type assertions, but the script should work correctly as is. The main issue with + * the TraderParameters has been fixed. + */ + import { parseAbi, parseUnits, @@ -37,8 +45,8 @@ const PATCHER_ABI = patcherArtifact.abi * CowShed SDK for computing deterministic proxy addresses and encoding hook calls */ class CowShedSdk { - factoryAddress: string - implementationAddress: string + factoryAddress: `0x${string}` + implementationAddress: `0x${string}` chainId: number constructor({ @@ -50,13 +58,13 @@ class CowShedSdk { implementationAddress: string chainId: number }) { - this.factoryAddress = factoryAddress - this.implementationAddress = implementationAddress + this.factoryAddress = factoryAddress as `0x${string}` + this.implementationAddress = implementationAddress as `0x${string}` this.chainId = chainId } // Compute the deterministic proxy address for a user - computeProxyAddress(owner: string): string { + computeProxyAddress(owner: string): `0x${string}` { // This uses CREATE2 to compute the deterministic address const salt = ethers.utils.solidityKeccak256( ['address', 'address'], @@ -91,16 +99,18 @@ class CowShedSdk { ] ) - return ethers.utils.getAddress('0x' + create2Input.slice(26)) + return ethers.utils.getAddress( + '0x' + create2Input.slice(26) + ) as `0x${string}` } // Encode the executeHooks call for the factory static encodeExecuteHooksForFactory( calls: any[], - nonce: string, + nonce: `0x${string}`, deadline: bigint, - owner: string, - signature: string + owner: `0x${string}`, + signature: `0x${string}` ): string { const cowShedFactoryAbi = parseAbi([ 'function deployProxyAndExecuteHooks(address owner, address implementation, (address target, uint256 value, bytes callData, bool allowFailure, bool isDelegateCall)[] calls, bytes32 nonce, uint256 deadline, bytes signature) returns (address proxy)', @@ -111,9 +121,9 @@ class CowShedSdk { functionName: 'deployProxyAndExecuteHooks', args: [ owner, - COW_SHED_IMPLEMENTATION, + COW_SHED_IMPLEMENTATION as `0x${string}`, calls.map((call) => ({ - target: call.target, + target: call.target as `0x${string}`, value: call.value, callData: call.callData, allowFailure: call.allowFailure, @@ -148,7 +158,7 @@ async function setupCowShedPostHooks( // Generate a random nonce const nonce = `0x${Array.from({ length: 64 }, () => Math.floor(Math.random() * 16).toString(16) - ).join('')}` + ).join('')}` as `0x${string}` // Set a deadline 24 hours from now const deadline = BigInt(Math.floor(Date.now() / 1000) + 24 * 60 * 60) @@ -159,13 +169,13 @@ async function setupCowShedPostHooks( // Create the bridge data for LiFi const bridgeData = { - transactionId: `0x${randomBytes(32).toString('hex')}`, + transactionId: `0x${randomBytes(32).toString('hex')}` as `0x${string}`, bridge: 'across', integrator: 'lifi-demo', - referrer: '0x0000000000000000000000000000000000000000', - sendingAssetId: usdcAddress, - receiver: signerAddress, - destinationChainId: 8453, // BASE chain ID + referrer: '0x0000000000000000000000000000000000000000' as `0x${string}`, + sendingAssetId: usdcAddress as `0x${string}`, + receiver: signerAddress as `0x${string}`, + destinationChainId: 8453n, // BASE chain ID minAmount: receivedAmount, hasSourceSwaps: false, hasDestinationCall: false, @@ -173,16 +183,17 @@ async function setupCowShedPostHooks( // Create AcrossV3Data const acrossData = { - receiverAddress: signerAddress, - refundAddress: signerAddress, - receivingAssetId: BASE_USDC, // USDC on BASE + receiverAddress: signerAddress as `0x${string}`, + refundAddress: signerAddress as `0x${string}`, + receivingAssetId: BASE_USDC as `0x${string}`, // USDC on BASE outputAmount: 0n, // This will be patched dynamically outputAmountPercent: parseUnits('0.995', 18), // 0.5% fee (example) - exclusiveRelayer: '0x0000000000000000000000000000000000000000', + exclusiveRelayer: + '0x0000000000000000000000000000000000000000' as `0x${string}`, quoteTimestamp: Math.floor(Date.now() / 1000), fillDeadline: Math.floor(Date.now() / 1000) + 3600, // 1 hour exclusivityDeadline: Math.floor(Date.now() / 1000), - message: '0x', // No message + message: '0x' as `0x${string}`, // No message } // Encode the AcrossFacetV3 call @@ -190,16 +201,44 @@ async function setupCowShedPostHooks( 'function startBridgeTokensViaAcrossV3((bytes32 transactionId, string bridge, string integrator, address referrer, address sendingAssetId, address receiver, uint256 destinationChainId, uint256 minAmount, bool hasSourceSwaps, bool hasDestinationCall) _bridgeData, (address receiverAddress, address refundAddress, address receivingAssetId, uint256 outputAmount, uint64 outputAmountPercent, address exclusiveRelayer, uint32 quoteTimestamp, uint32 fillDeadline, uint32 exclusivityDeadline, bytes message) _acrossData) payable', ]) + // Create the bridge data with proper types + const typedBridgeData = { + transactionId: bridgeData.transactionId, + bridge: bridgeData.bridge, + integrator: bridgeData.integrator, + referrer: bridgeData.referrer, + sendingAssetId: bridgeData.sendingAssetId, + receiver: bridgeData.receiver, + destinationChainId: bridgeData.destinationChainId, + minAmount: bridgeData.minAmount, + hasSourceSwaps: bridgeData.hasSourceSwaps, + hasDestinationCall: bridgeData.hasDestinationCall, + } + + // Create the across data with proper types + const typedAcrossData = { + receiverAddress: acrossData.receiverAddress, + refundAddress: acrossData.refundAddress, + receivingAssetId: acrossData.receivingAssetId, + outputAmount: acrossData.outputAmount, + outputAmountPercent: acrossData.outputAmountPercent, + exclusiveRelayer: acrossData.exclusiveRelayer, + quoteTimestamp: acrossData.quoteTimestamp, + fillDeadline: acrossData.fillDeadline, + exclusivityDeadline: acrossData.exclusivityDeadline, + message: acrossData.message, + } + const acrossCalldata = encodeFunctionData({ abi: acrossFacetAbi, functionName: 'startBridgeTokensViaAcrossV3', - args: [bridgeData, acrossData], + args: [typedBridgeData, typedAcrossData], }) // Calculate the offset for the outputAmount field in the AcrossV3Data struct // This is a fixed offset in the calldata where the outputAmount value needs to be patched // The offset is calculated based on the ABI encoding of the function call - const outputAmountOffset = 644 // This offset needs to be calculated correctly + const outputAmountOffset = 644n // This offset needs to be calculated correctly // Encode the balanceOf call to get the USDC balance const valueGetter = encodeFunctionData({ @@ -207,7 +246,7 @@ async function setupCowShedPostHooks( 'function balanceOf(address account) view returns (uint256)', ]), functionName: 'balanceOf', - args: [shedDeterministicAddress], + args: [shedDeterministicAddress as `0x${string}`], }) // Encode the patcher call @@ -217,12 +256,12 @@ async function setupCowShedPostHooks( ]), functionName: 'executeWithDynamicPatches', args: [ - usdcAddress, // valueSource - USDC contract + usdcAddress as `0x${string}`, // valueSource - USDC contract valueGetter, // valueGetter - balanceOf call - LIFI_DIAMOND_ARBITRUM, // finalTarget - LiFiDiamond contract + LIFI_DIAMOND_ARBITRUM as `0x${string}`, // finalTarget - LiFiDiamond contract 0n, // value - no ETH being sent acrossCalldata, // data - the encoded AcrossFacetV3 call - [outputAmountOffset], // offsets - position of outputAmount in the calldata + [BigInt(outputAmountOffset)], // offsets - position of outputAmount in the calldata false, // delegateCall - regular call, not delegateCall ], }) @@ -230,7 +269,7 @@ async function setupCowShedPostHooks( // Define the post-swap call to the patcher const postSwapCalls = [ { - target: PATCHER_ARBITRUM, + target: PATCHER_ARBITRUM as `0x${string}`, callData: patcherCalldata, value: 0n, allowFailure: false, @@ -239,7 +278,7 @@ async function setupCowShedPostHooks( ] // Sign the typed data for the hooks - const signature = await walletClient.signTypedData({ + const signature = (await walletClient.signTypedData({ account, domain: { name: 'COWShed', @@ -273,21 +312,21 @@ async function setupCowShedPostHooks( nonce, deadline, }, - }) + })) as `0x${string}` // Encode the post hooks call data const shedEncodedPostHooksCallData = CowShedSdk.encodeExecuteHooksForFactory( postSwapCalls, nonce, deadline, - signerAddress, + signerAddress as `0x${string}`, signature ) // Create the post hooks const postHooks = [ { - target: COW_SHED_FACTORY, + target: PATCHER_ARBITRUM as `0x${string}`, callData: shedEncodedPostHooksCallData, gasLimit: '3000000', }, @@ -328,7 +367,9 @@ async function main(options: { privateKey: string; dryRun: boolean }) { client: { public: walletClient, wallet: walletClient }, }) - const wethBalance = await wethContract.read.balanceOf([walletAddress]) + const wethBalance = (await wethContract.read.balanceOf([ + walletAddress, + ])) as bigint consola.info(`WETH balance: ${wethBalance}`) if (wethBalance < swapAmount) { @@ -337,17 +378,17 @@ async function main(options: { privateKey: string; dryRun: boolean }) { } // Check allowance - const allowance = await wethContract.read.allowance([ + const allowance = (await wethContract.read.allowance([ walletAddress, VAULT_RELAYER_ARBITRUM, - ]) + ])) as bigint consola.info(`Current allowance: ${allowance}`) if (allowance < swapAmount) { consola.info('Approving WETH for CoW Protocol VaultRelayer...') if (!options.dryRun) { const approveTx = await wethContract.write.approve([ - VAULT_RELAYER_ARBITRUM, + VAULT_RELAYER_ARBITRUM as `0x${string}`, BigInt( '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' ), // Max uint256 @@ -372,22 +413,22 @@ async function main(options: { privateKey: string; dryRun: boolean }) { ) const ethersSigner = new ethers.Wallet(options.privateKey, provider) - // Initialize CoW SDK + // Initialize CoW SDK with proper TraderParameters const cowSdk = new TradingSdk({ - chainId: SupportedChainId.ARBITRUM, + chainId: SupportedChainId.ARBITRUM_ONE, signer: ethersSigner, - appCode: 'lifi-demo', + appCode: 'lifi-demo' as any, // Cast to any to satisfy the AppCode type }) // Create the order parameters const parameters = { kind: OrderKind.SELL, - sellToken: ARBITRUM_WETH, + sellToken: ARBITRUM_WETH as `0x${string}`, sellTokenDecimals: 18, - buyToken: ARBITRUM_USDC, + buyToken: ARBITRUM_USDC as `0x${string}`, buyTokenDecimals: 6, amount: swapAmount.toString(), - receiver: shedDeterministicAddress, // Important: Set the receiver to the CowShed proxy + receiver: shedDeterministicAddress as `0x${string}`, // Important: Set the receiver to the CowShed proxy validFor: 30 * 60, // 30 minutes in seconds slippageBps: 50, // 0.5% slippage } From cc451c7e2f33e491204c082dc6601dfd3f3bc833 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Fri, 23 May 2025 15:58:02 +0300 Subject: [PATCH 10/46] use relay --- script/demoScripts/demoPatcher.ts | 99 +++++++++++++++++-------------- 1 file changed, 55 insertions(+), 44 deletions(-) diff --git a/script/demoScripts/demoPatcher.ts b/script/demoScripts/demoPatcher.ts index 2d5685816..096533375 100644 --- a/script/demoScripts/demoPatcher.ts +++ b/script/demoScripts/demoPatcher.ts @@ -1,7 +1,10 @@ #!/usr/bin/env bun /** - * Demo script for CowSwap with Patcher contract + * Demo script for CowSwap with Patcher contract using RelayFacet + * + * Relay is a cross-chain payments system enabling instant, low-cost bridging + * and cross-chain execution using relayers as financial agents. * * Note: There are some TypeScript errors related to the `0x${string}` type that could be fixed * with more type assertions, but the script should work correctly as is. The main issue with @@ -138,7 +141,7 @@ class CowShedSdk { } /** - * Setup CowShed post hooks for bridging USDC to BASE + * Setup CowShed post hooks for bridging USDC to BASE using Relay */ async function setupCowShedPostHooks( chainId: number, @@ -170,7 +173,7 @@ async function setupCowShedPostHooks( // Create the bridge data for LiFi const bridgeData = { transactionId: `0x${randomBytes(32).toString('hex')}` as `0x${string}`, - bridge: 'across', + bridge: 'relay', integrator: 'lifi-demo', referrer: '0x0000000000000000000000000000000000000000' as `0x${string}`, sendingAssetId: usdcAddress as `0x${string}`, @@ -181,24 +184,28 @@ async function setupCowShedPostHooks( hasDestinationCall: false, } - // Create AcrossV3Data - const acrossData = { - receiverAddress: signerAddress as `0x${string}`, - refundAddress: signerAddress as `0x${string}`, - receivingAssetId: BASE_USDC as `0x${string}`, // USDC on BASE - outputAmount: 0n, // This will be patched dynamically - outputAmountPercent: parseUnits('0.995', 18), // 0.5% fee (example) - exclusiveRelayer: - '0x0000000000000000000000000000000000000000' as `0x${string}`, - quoteTimestamp: Math.floor(Date.now() / 1000), - fillDeadline: Math.floor(Date.now() / 1000) + 3600, // 1 hour - exclusivityDeadline: Math.floor(Date.now() / 1000), - message: '0x' as `0x${string}`, // No message + // Create RelayData + // Generate a random requestId for the demo + const requestId = `0x${randomBytes(32).toString('hex')}` as `0x${string}` + + // Create a dummy signature for demo purposes + // In a real scenario, this would be obtained from the Relay API + const relaySignature = `0x${randomBytes(65).toString('hex')}` as `0x${string}` + + const relayData = { + requestId: requestId, + nonEVMReceiver: + '0x0000000000000000000000000000000000000000000000000000000000000000' as `0x${string}`, // Not bridging to non-EVM chain + receivingAssetId: `0x${BASE_USDC.slice(2).padStart( + 64, + '0' + )}` as `0x${string}`, // USDC on BASE as bytes32 + signature: relaySignature, // This would be obtained from the Relay API in a real scenario } - // Encode the AcrossFacetV3 call - const acrossFacetAbi = parseAbi([ - 'function startBridgeTokensViaAcrossV3((bytes32 transactionId, string bridge, string integrator, address referrer, address sendingAssetId, address receiver, uint256 destinationChainId, uint256 minAmount, bool hasSourceSwaps, bool hasDestinationCall) _bridgeData, (address receiverAddress, address refundAddress, address receivingAssetId, uint256 outputAmount, uint64 outputAmountPercent, address exclusiveRelayer, uint32 quoteTimestamp, uint32 fillDeadline, uint32 exclusivityDeadline, bytes message) _acrossData) payable', + // Encode the RelayFacet call + const relayFacetAbi = parseAbi([ + 'function startBridgeTokensViaRelay((bytes32 transactionId, string bridge, string integrator, address referrer, address sendingAssetId, address receiver, uint256 destinationChainId, uint256 minAmount, bool hasSourceSwaps, bool hasDestinationCall) _bridgeData, (bytes32 requestId, bytes32 nonEVMReceiver, bytes32 receivingAssetId, bytes signature) _relayData) payable', ]) // Create the bridge data with proper types @@ -215,30 +222,25 @@ async function setupCowShedPostHooks( hasDestinationCall: bridgeData.hasDestinationCall, } - // Create the across data with proper types - const typedAcrossData = { - receiverAddress: acrossData.receiverAddress, - refundAddress: acrossData.refundAddress, - receivingAssetId: acrossData.receivingAssetId, - outputAmount: acrossData.outputAmount, - outputAmountPercent: acrossData.outputAmountPercent, - exclusiveRelayer: acrossData.exclusiveRelayer, - quoteTimestamp: acrossData.quoteTimestamp, - fillDeadline: acrossData.fillDeadline, - exclusivityDeadline: acrossData.exclusivityDeadline, - message: acrossData.message, + // Create the relay data with proper types + const typedRelayData = { + requestId: relayData.requestId, + nonEVMReceiver: relayData.nonEVMReceiver, + receivingAssetId: relayData.receivingAssetId, + signature: relayData.signature, } - const acrossCalldata = encodeFunctionData({ - abi: acrossFacetAbi, - functionName: 'startBridgeTokensViaAcrossV3', - args: [typedBridgeData, typedAcrossData], + const relayCalldata = encodeFunctionData({ + abi: relayFacetAbi, + functionName: 'startBridgeTokensViaRelay', + args: [typedBridgeData, typedRelayData], }) - // Calculate the offset for the outputAmount field in the AcrossV3Data struct - // This is a fixed offset in the calldata where the outputAmount value needs to be patched + // Calculate the offset for the minAmount field in the BridgeData struct + // This is a fixed offset in the calldata where the minAmount value needs to be patched // The offset is calculated based on the ABI encoding of the function call - const outputAmountOffset = 644n // This offset needs to be calculated correctly + // Note: In a production environment, this offset should be calculated precisely + const minAmountOffset = 644n // Encode the balanceOf call to get the USDC balance const valueGetter = encodeFunctionData({ @@ -260,8 +262,8 @@ async function setupCowShedPostHooks( valueGetter, // valueGetter - balanceOf call LIFI_DIAMOND_ARBITRUM as `0x${string}`, // finalTarget - LiFiDiamond contract 0n, // value - no ETH being sent - acrossCalldata, // data - the encoded AcrossFacetV3 call - [BigInt(outputAmountOffset)], // offsets - position of outputAmount in the calldata + relayCalldata, // data - the encoded RelayFacet call + [BigInt(minAmountOffset)], // offsets - position of minAmount in the calldata false, // delegateCall - regular call, not delegateCall ], }) @@ -277,8 +279,8 @@ async function setupCowShedPostHooks( }, ] - // Sign the typed data for the hooks - const signature = (await walletClient.signTypedData({ + // Create the typed data for the hooks + const typedData = { account, domain: { name: 'COWShed', @@ -312,7 +314,12 @@ async function setupCowShedPostHooks( nonce, deadline, }, - })) as `0x${string}` + } + + // Sign the typed data for the hooks + const hookSignature = (await walletClient.signTypedData( + typedData + )) as `0x${string}` // Encode the post hooks call data const shedEncodedPostHooksCallData = CowShedSdk.encodeExecuteHooksForFactory( @@ -320,7 +327,7 @@ async function setupCowShedPostHooks( nonce, deadline, signerAddress as `0x${string}`, - signature + hookSignature ) // Create the post hooks @@ -340,6 +347,10 @@ async function setupCowShedPostHooks( /** * Main function to execute the demo + * + * Note: There are several TypeScript errors related to the `0x${string}` type + * that don't affect the functionality of the script. In a production environment, + * these should be fixed with proper type assertions. */ async function main(options: { privateKey: string; dryRun: boolean }) { try { From aae891491d218ce87624d633b56459f0474ae2f9 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Fri, 23 May 2025 16:24:21 +0300 Subject: [PATCH 11/46] use correct calc --- script/demoScripts/demoPatcher.ts | 47 ++++++++++--------------------- 1 file changed, 15 insertions(+), 32 deletions(-) diff --git a/script/demoScripts/demoPatcher.ts b/script/demoScripts/demoPatcher.ts index 096533375..423100a1d 100644 --- a/script/demoScripts/demoPatcher.ts +++ b/script/demoScripts/demoPatcher.ts @@ -19,6 +19,7 @@ import { http, getContract, Hex, + getCreate2Address, } from 'viem' import { privateKeyToAccount } from 'viem/accounts' import { arbitrum, base } from 'viem/chains' @@ -39,6 +40,9 @@ const PATCHER_ARBITRUM = '0xE65b50EcF482f97f53557f0E02946aa27f8839EC' const COW_SHED_FACTORY = '0x00E989b87700514118Fa55326CD1cCE82faebEF6' const COW_SHED_IMPLEMENTATION = '0x2CFFA8cf11B90C9F437567b86352169dF4009F73' const VAULT_RELAYER_ARBITRUM = '0xC92E8bdf79f0507f65a392b0ab4667716BFE0110' +// The creationCode of CoWShedProxy (type(COWShedProxy).creationCode) +const PROXY_CREATION_CODE = + '0x60a034608e57601f61037138819003918201601f19168301916001600160401b038311848410176093578084926040948552833981010312608e57604b602060458360a9565b920160a9565b6080527f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc556040516102b490816100bd8239608051818181608f01526101720152f35b600080fd5b634e487b7160e01b600052604160045260246000fd5b51906001600160a01b0382168203608e5756fe60806040526004361015610018575b3661019457610194565b6000803560e01c908163025b22bc1461003b575063f851a4400361000e5761010d565b3461010a5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261010a5773ffffffffffffffffffffffffffffffffffffffff60043581811691828203610106577f0000000000000000000000000000000000000000000000000000000000000000163314600014610101577f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc557fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b8280a280f35b61023d565b8380fd5b80fd5b346101645760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610164576020610146610169565b73ffffffffffffffffffffffffffffffffffffffff60405191168152f35b600080fd5b333003610101577f000000000000000000000000000000000000000000000000000000000000000090565b60ff7f68df44b1011761f481358c0f49a711192727fb02c377d697bcb0ea8ff8393ac0541615806101ef575b1561023d5760046040517ff92ee8a9000000000000000000000000000000000000000000000000000000008152fd5b507f400ada75000000000000000000000000000000000000000000000000000000007fffffffff000000000000000000000000000000000000000000000000000000006000351614156101c0565b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc546000808092368280378136915af43d82803e1561027a573d90f35b3d90fdfea2646970667358221220c7c26ff3040b96a28e96d6d27b743972943aeaef81cc821544c5fe1e24f9b17264736f6c63430008190033' // ABIs const ERC20_ABI = erc20Artifact.abi @@ -68,42 +72,21 @@ class CowShedSdk { // Compute the deterministic proxy address for a user computeProxyAddress(owner: string): `0x${string}` { - // This uses CREATE2 to compute the deterministic address - const salt = ethers.utils.solidityKeccak256( - ['address', 'address'], - [owner, this.implementationAddress] - ) - - const proxyBytecode = - '0x' + - ethers.utils - .solidityPack( - ['bytes', 'address'], - [ - '0x3d602d80600a3d3981f3363d3d373d3d3d363d73', - this.implementationAddress, - ] - ) - .slice(2) + - '5af43d82803e903d91602b57fd5bf3' - - const create2Input = ethers.utils.solidityKeccak256( - ['bytes'], + const salt = ethers.utils.defaultAbiCoder.encode(['address'], [owner]) + const initCodeHash = ethers.utils.solidityKeccak256( + ['bytes', 'bytes'], [ - ethers.utils.solidityPack( - ['bytes1', 'address', 'bytes32', 'bytes32'], - [ - '0xff', - this.factoryAddress, - salt, - ethers.utils.keccak256(proxyBytecode), - ] + PROXY_CREATION_CODE, + ethers.utils.defaultAbiCoder.encode( + ['address', 'address'], + [COW_SHED_IMPLEMENTATION, owner] ), ] ) - - return ethers.utils.getAddress( - '0x' + create2Input.slice(26) + return ethers.utils.getCreate2Address( + COW_SHED_FACTORY, + salt, + initCodeHash ) as `0x${string}` } From fa932d123d84408f6da5cfab0b8591246db0dc87 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Fri, 23 May 2025 16:32:08 +0300 Subject: [PATCH 12/46] fixes --- script/demoScripts/demoPatcher.ts | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/script/demoScripts/demoPatcher.ts b/script/demoScripts/demoPatcher.ts index 423100a1d..bb07482c6 100644 --- a/script/demoScripts/demoPatcher.ts +++ b/script/demoScripts/demoPatcher.ts @@ -6,9 +6,6 @@ * Relay is a cross-chain payments system enabling instant, low-cost bridging * and cross-chain execution using relayers as financial agents. * - * Note: There are some TypeScript errors related to the `0x${string}` type that could be fixed - * with more type assertions, but the script should work correctly as is. The main issue with - * the TraderParameters has been fixed. */ import { @@ -19,7 +16,6 @@ import { http, getContract, Hex, - getCreate2Address, } from 'viem' import { privateKeyToAccount } from 'viem/accounts' import { arbitrum, base } from 'viem/chains' @@ -28,17 +24,16 @@ import { ethers } from 'ethers' import { SupportedChainId, OrderKind, TradingSdk } from '@cowprotocol/cow-sdk' import { defineCommand, runMain } from 'citty' import { consola } from 'consola' -import patcherArtifact from '../../out/Patcher.sol/Patcher.json' import erc20Artifact from '../../out/ERC20/ERC20.sol/ERC20.json' // Constants const ARBITRUM_WETH = '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1' const ARBITRUM_USDC = '0xaf88d065e77c8cC2239327C5EDb3A432268e5831' const BASE_USDC = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' -const LIFI_DIAMOND_ARBITRUM = '0xD3b2b0aC0AFdd0d166a495f5E9fca4eCc715a782' -const PATCHER_ARBITRUM = '0xE65b50EcF482f97f53557f0E02946aa27f8839EC' -const COW_SHED_FACTORY = '0x00E989b87700514118Fa55326CD1cCE82faebEF6' -const COW_SHED_IMPLEMENTATION = '0x2CFFA8cf11B90C9F437567b86352169dF4009F73' +import arbitrumDeployments from '../../deployments/arbitrum.staging.json' +const LIFI_DIAMOND_ARBITRUM = arbitrumDeployments.LiFiDiamond +const PATCHER_ARBITRUM = arbitrumDeployments.Patcher +import { COW_SHED_FACTORY, COW_SHED_IMPLEMENTATION } from '@cowprotocol/cow-sdk' const VAULT_RELAYER_ARBITRUM = '0xC92E8bdf79f0507f65a392b0ab4667716BFE0110' // The creationCode of CoWShedProxy (type(COWShedProxy).creationCode) const PROXY_CREATION_CODE = @@ -46,7 +41,6 @@ const PROXY_CREATION_CODE = // ABIs const ERC20_ABI = erc20Artifact.abi -const PATCHER_ABI = patcherArtifact.abi /** * CowShed SDK for computing deterministic proxy addresses and encoding hook calls @@ -79,12 +73,12 @@ class CowShedSdk { PROXY_CREATION_CODE, ethers.utils.defaultAbiCoder.encode( ['address', 'address'], - [COW_SHED_IMPLEMENTATION, owner] + [this.implementationAddress, owner] ), ] ) return ethers.utils.getCreate2Address( - COW_SHED_FACTORY, + this.factoryAddress, salt, initCodeHash ) as `0x${string}` From a71e1ab58b2eaea3aa1a264c92d2e1d77038b27c Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Fri, 23 May 2025 17:14:00 +0300 Subject: [PATCH 13/46] use correct cowshed calls --- script/demoScripts/demoPatcher.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/script/demoScripts/demoPatcher.ts b/script/demoScripts/demoPatcher.ts index bb07482c6..41e1281fd 100644 --- a/script/demoScripts/demoPatcher.ts +++ b/script/demoScripts/demoPatcher.ts @@ -93,15 +93,13 @@ class CowShedSdk { signature: `0x${string}` ): string { const cowShedFactoryAbi = parseAbi([ - 'function deployProxyAndExecuteHooks(address owner, address implementation, (address target, uint256 value, bytes callData, bool allowFailure, bool isDelegateCall)[] calls, bytes32 nonce, uint256 deadline, bytes signature) returns (address proxy)', + 'function executeHooks((address target, uint256 value, bytes callData, bool allowFailure, bool isDelegateCall)[] calls, bytes32 nonce, uint256 deadline, address user, bytes signature) returns (address proxy)', ]) return encodeFunctionData({ abi: cowShedFactoryAbi, - functionName: 'deployProxyAndExecuteHooks', + functionName: 'executeHooks', args: [ - owner, - COW_SHED_IMPLEMENTATION as `0x${string}`, calls.map((call) => ({ target: call.target as `0x${string}`, value: call.value, @@ -111,6 +109,7 @@ class CowShedSdk { })), nonce, deadline, + owner, signature, ], }) @@ -231,16 +230,16 @@ async function setupCowShedPostHooks( // Encode the patcher call const patcherCalldata = encodeFunctionData({ abi: parseAbi([ - 'function executeWithDynamicPatches(address valueSource, bytes valueGetter, address finalTarget, uint256 value, bytes data, uint256[] offsets, bool delegateCall) returns (bool success, bytes returnData)', + 'function executeWithMultiplePatches(address[] valueSources, bytes[] valueGetters, address finalTarget, uint256 value, bytes data, uint256[][] offsetGroups, bool delegateCall) returns (bool success, bytes returnData)', ]), - functionName: 'executeWithDynamicPatches', + functionName: 'executeWithMultiplePatches', args: [ - usdcAddress as `0x${string}`, // valueSource - USDC contract - valueGetter, // valueGetter - balanceOf call + [usdcAddress as `0x${string}`], // valueSources - Array with USDC contract + [valueGetter], // valueGetters - Array with balanceOf call LIFI_DIAMOND_ARBITRUM as `0x${string}`, // finalTarget - LiFiDiamond contract 0n, // value - no ETH being sent relayCalldata, // data - the encoded RelayFacet call - [BigInt(minAmountOffset)], // offsets - position of minAmount in the calldata + [[BigInt(minAmountOffset)]], // offsetGroups - Array of arrays with positions of minAmount in the calldata false, // delegateCall - regular call, not delegateCall ], }) @@ -310,7 +309,7 @@ async function setupCowShedPostHooks( // Create the post hooks const postHooks = [ { - target: PATCHER_ARBITRUM as `0x${string}`, + target: COW_SHED_FACTORY, callData: shedEncodedPostHooksCallData, gasLimit: '3000000', }, From df48f1ccfea3a0ba5dec6f86c92e709b1c056c48 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Fri, 23 May 2025 17:26:55 +0300 Subject: [PATCH 14/46] use real sig --- script/demoScripts/demoPatcher.ts | 67 +++++++++++++++++++++++++++---- 1 file changed, 60 insertions(+), 7 deletions(-) diff --git a/script/demoScripts/demoPatcher.ts b/script/demoScripts/demoPatcher.ts index 41e1281fd..ccdd185db 100644 --- a/script/demoScripts/demoPatcher.ts +++ b/script/demoScripts/demoPatcher.ts @@ -161,22 +161,75 @@ async function setupCowShedPostHooks( } // Create RelayData - // Generate a random requestId for the demo - const requestId = `0x${randomBytes(32).toString('hex')}` as `0x${string}` + // First, create a quote request with a realistic amount + const estimatedUsdcAmount = '1000000' // 1 USDC (6 decimals) + + // Get a real signature from the Relay API + // First, create a quote request + const quoteParams = { + user: LIFI_DIAMOND_ARBITRUM, + originChainId: 42161, // Arbitrum + destinationChainId: 8453, // BASE + originCurrency: ARBITRUM_USDC, + destinationCurrency: BASE_USDC, + recipient: signerAddress, + tradeType: 'EXACT_INPUT', + amount: estimatedUsdcAmount, // Use a realistic amount instead of 0 + referrer: 'lifi-demo', + useExternalLiquidity: false, + } + + // Fetch the quote from the Relay API + consola.info('Fetching quote from Relay API...') + const quoteResponse = await fetch('https://api.relay.link/quote', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(quoteParams), + }) + + if (!quoteResponse.ok) { + throw new Error( + `Failed to get quote from Relay API: ${quoteResponse.statusText}` + ) + } - // Create a dummy signature for demo purposes - // In a real scenario, this would be obtained from the Relay API - const relaySignature = `0x${randomBytes(65).toString('hex')}` as `0x${string}` + const quoteData = await quoteResponse.json() + const relayRequestId = quoteData.steps[0].requestId + consola.info(`Got requestId from Relay API: ${relayRequestId}`) + + // Fetch the signature from the Relay API + consola.info('Fetching signature from Relay API...') + const signatureResponse = await fetch( + `https://api.relay.link/requests/${relayRequestId}/signature/v2`, + { headers: { 'Content-Type': 'application/json' } } + ) + + if (!signatureResponse.ok) { + throw new Error( + `Failed to get signature from Relay API: ${signatureResponse.statusText}` + ) + } + + const signatureData = await signatureResponse.json() + const relaySignature = signatureData.signature as `0x${string}` + consola.info( + `Got signature from Relay API: ${relaySignature.slice( + 0, + 10 + )}...${relaySignature.slice(-8)}` + ) const relayData = { - requestId: requestId, + requestId: relayRequestId, nonEVMReceiver: '0x0000000000000000000000000000000000000000000000000000000000000000' as `0x${string}`, // Not bridging to non-EVM chain receivingAssetId: `0x${BASE_USDC.slice(2).padStart( 64, '0' )}` as `0x${string}`, // USDC on BASE as bytes32 - signature: relaySignature, // This would be obtained from the Relay API in a real scenario + signature: relaySignature, // Real signature from the Relay API } // Encode the RelayFacet call From b709aef2b1da6256ed4c2f63b41e0933d678912d Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Fri, 23 May 2025 18:13:45 +0300 Subject: [PATCH 15/46] updates --- deployments/_deployments_log_file.json | 4 +- deployments/arbitrum.staging.json | 2 +- script/demoScripts/demoPatcher.ts | 73 +++++++++++++++++++------- 3 files changed, 57 insertions(+), 22 deletions(-) diff --git a/deployments/_deployments_log_file.json b/deployments/_deployments_log_file.json index f16ec139d..a23ce0693 100644 --- a/deployments/_deployments_log_file.json +++ b/deployments/_deployments_log_file.json @@ -31960,9 +31960,9 @@ "staging": { "1.0.0": [ { - "ADDRESS": "0x3cf7dE0e31e13C93c8Aada774ADF1C7eD58157f5", + "ADDRESS": "0x681a3409c35F12224c436D50Ce14F25f954B6Ea2", "OPTIMIZER_RUNS": "1000000", - "TIMESTAMP": "2024-11-07 14:15:11", + "TIMESTAMP": "2025-05-23 17:41:51", "CONSTRUCTOR_ARGS": "0x000000000000000000000000a5f565650890fba1824ee0f21ebbbf660a179934000000000000000000000000f70da97812cb96acdf810712aa562db8dfa3dbef", "SALT": "", "VERIFIED": "true" diff --git a/deployments/arbitrum.staging.json b/deployments/arbitrum.staging.json index 6d5c3f8d0..6d1cce23d 100644 --- a/deployments/arbitrum.staging.json +++ b/deployments/arbitrum.staging.json @@ -50,7 +50,7 @@ "AcrossFacetV3": "0x08BfAc22A3B41637edB8A7920754fDb30B18f740", "ReceiverAcrossV3": "0xe4F3DEF14D61e47c696374453CD64d438FD277F8", "AcrossFacetPackedV3": "0x21767081Ff52CE5563A29f27149D01C7127775A2", - "RelayFacet": "0x3cf7dE0e31e13C93c8Aada774ADF1C7eD58157f5", + "RelayFacet": "0x681a3409c35F12224c436D50Ce14F25f954B6Ea2", "GlacisFacet": "0xF82830B952Bc60b93206FA22f1cD4770cedb2840", "GasZipFacet": "0x37f3F3E9d909fB1163448C511193b8481e541C62", "ChainflipFacet": "0xa884c21873A671bD010567cf97c937b153F842Cc", diff --git a/script/demoScripts/demoPatcher.ts b/script/demoScripts/demoPatcher.ts index ccdd185db..6570c2177 100644 --- a/script/demoScripts/demoPatcher.ts +++ b/script/demoScripts/demoPatcher.ts @@ -1,13 +1,5 @@ #!/usr/bin/env bun -/** - * Demo script for CowSwap with Patcher contract using RelayFacet - * - * Relay is a cross-chain payments system enabling instant, low-cost bridging - * and cross-chain execution using relayers as financial agents. - * - */ - import { parseAbi, parseUnits, @@ -16,9 +8,11 @@ import { http, getContract, Hex, + recoverMessageAddress, + keccak256, } from 'viem' import { privateKeyToAccount } from 'viem/accounts' -import { arbitrum, base } from 'viem/chains' +import { arbitrum } from 'viem/chains' import { randomBytes } from 'crypto' import { ethers } from 'ethers' import { SupportedChainId, OrderKind, TradingSdk } from '@cowprotocol/cow-sdk' @@ -26,7 +20,6 @@ import { defineCommand, runMain } from 'citty' import { consola } from 'consola' import erc20Artifact from '../../out/ERC20/ERC20.sol/ERC20.json' -// Constants const ARBITRUM_WETH = '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1' const ARBITRUM_USDC = '0xaf88d065e77c8cC2239327C5EDb3A432268e5831' const BASE_USDC = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' @@ -35,16 +28,10 @@ const LIFI_DIAMOND_ARBITRUM = arbitrumDeployments.LiFiDiamond const PATCHER_ARBITRUM = arbitrumDeployments.Patcher import { COW_SHED_FACTORY, COW_SHED_IMPLEMENTATION } from '@cowprotocol/cow-sdk' const VAULT_RELAYER_ARBITRUM = '0xC92E8bdf79f0507f65a392b0ab4667716BFE0110' -// The creationCode of CoWShedProxy (type(COWShedProxy).creationCode) const PROXY_CREATION_CODE = '0x60a034608e57601f61037138819003918201601f19168301916001600160401b038311848410176093578084926040948552833981010312608e57604b602060458360a9565b920160a9565b6080527f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc556040516102b490816100bd8239608051818181608f01526101720152f35b600080fd5b634e487b7160e01b600052604160045260246000fd5b51906001600160a01b0382168203608e5756fe60806040526004361015610018575b3661019457610194565b6000803560e01c908163025b22bc1461003b575063f851a4400361000e5761010d565b3461010a5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261010a5773ffffffffffffffffffffffffffffffffffffffff60043581811691828203610106577f0000000000000000000000000000000000000000000000000000000000000000163314600014610101577f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc557fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b8280a280f35b61023d565b8380fd5b80fd5b346101645760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610164576020610146610169565b73ffffffffffffffffffffffffffffffffffffffff60405191168152f35b600080fd5b333003610101577f000000000000000000000000000000000000000000000000000000000000000090565b60ff7f68df44b1011761f481358c0f49a711192727fb02c377d697bcb0ea8ff8393ac0541615806101ef575b1561023d5760046040517ff92ee8a9000000000000000000000000000000000000000000000000000000008152fd5b507f400ada75000000000000000000000000000000000000000000000000000000007fffffffff000000000000000000000000000000000000000000000000000000006000351614156101c0565b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc546000808092368280378136915af43d82803e1561027a573d90f35b3d90fdfea2646970667358221220c7c26ff3040b96a28e96d6d27b743972943aeaef81cc821544c5fe1e24f9b17264736f6c63430008190033' -// ABIs const ERC20_ABI = erc20Artifact.abi - -/** - * CowShed SDK for computing deterministic proxy addresses and encoding hook calls - */ class CowShedSdk { factoryAddress: `0x${string}` implementationAddress: `0x${string}` @@ -221,6 +208,56 @@ async function setupCowShedPostHooks( )}...${relaySignature.slice(-8)}` ) + // Log the request origin user (not the signer) + if (signatureData.requestData?.originUser) { + const originUser = signatureData.requestData.originUser + consola.info(`Request origin user: ${originUser} (LiFi Diamond)`) + } + + // Recover the actual signer using the same message format as the contract + try { + // Construct the message exactly as the contract does: + // keccak256(abi.encodePacked( + // requestId, + // block.chainid, + // bytes32(uint256(uint160(address(this)))), + // bytes32(uint256(uint160(sendingAssetId))), + // _getMappedChainId(destinationChainId), + // bytes32(uint256(uint160(receiver))), + // receivingAssetId + // )) + + const requestId = relayRequestId.slice(2) // Remove 0x prefix + const chainId = (42161).toString(16).padStart(64, '0') // Arbitrum chain ID as bytes32 + const contractAddress = LIFI_DIAMOND_ARBITRUM.slice(2).padStart(64, '0') // LiFi Diamond as bytes32 + const sendingAssetId = ARBITRUM_USDC.slice(2).padStart(64, '0') // USDC as bytes32 + const destinationChainId = (8453).toString(16).padStart(64, '0') // Base chain ID as bytes32 + const receiver = signerAddress.slice(2).padStart(64, '0') // Receiver as bytes32 + const receivingAssetId = BASE_USDC.slice(2).padStart(64, '0') // USDC on Base as bytes32 + + // Concatenate all parameters (abi.encodePacked equivalent) + const packedData = + `0x${requestId}${chainId}${contractAddress}${sendingAssetId}${destinationChainId}${receiver}${receivingAssetId}` as `0x${string}` + + // Hash the packed data + const messageHash = keccak256(packedData) + + // Recover the signer using the message hash (Ethereum signed message format) + const recoveredSigner = await recoverMessageAddress({ + message: { raw: messageHash }, + signature: relaySignature, + }) + + consola.success(`Relay attestation signer: ${recoveredSigner}`) + } catch (error) { + consola.warn('Could not recover signer address:', error) + // Fallback: log full response for debugging + consola.debug( + 'Full signature response:', + JSON.stringify(signatureData, null, 2) + ) + } + const relayData = { requestId: relayRequestId, nonEVMReceiver: @@ -267,8 +304,6 @@ async function setupCowShedPostHooks( // Calculate the offset for the minAmount field in the BridgeData struct // This is a fixed offset in the calldata where the minAmount value needs to be patched - // The offset is calculated based on the ABI encoding of the function call - // Note: In a production environment, this offset should be calculated precisely const minAmountOffset = 644n // Encode the balanceOf call to get the USDC balance @@ -292,7 +327,7 @@ async function setupCowShedPostHooks( LIFI_DIAMOND_ARBITRUM as `0x${string}`, // finalTarget - LiFiDiamond contract 0n, // value - no ETH being sent relayCalldata, // data - the encoded RelayFacet call - [[BigInt(minAmountOffset)]], // offsetGroups - Array of arrays with positions of minAmount in the calldata + [[minAmountOffset]], // offsetGroups - Array of arrays with positions of minAmount in the calldata false, // delegateCall - regular call, not delegateCall ], }) From 043c9a1d99abbf34881b38f5375904c93c0d3858 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Fri, 23 May 2025 18:35:39 +0300 Subject: [PATCH 16/46] calc offset --- script/demoScripts/demoPatcher.ts | 57 +++++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 3 deletions(-) diff --git a/script/demoScripts/demoPatcher.ts b/script/demoScripts/demoPatcher.ts index 6570c2177..45e47da3f 100644 --- a/script/demoScripts/demoPatcher.ts +++ b/script/demoScripts/demoPatcher.ts @@ -31,6 +31,29 @@ const VAULT_RELAYER_ARBITRUM = '0xC92E8bdf79f0507f65a392b0ab4667716BFE0110' const PROXY_CREATION_CODE = '0x60a034608e57601f61037138819003918201601f19168301916001600160401b038311848410176093578084926040948552833981010312608e57604b602060458360a9565b920160a9565b6080527f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc556040516102b490816100bd8239608051818181608f01526101720152f35b600080fd5b634e487b7160e01b600052604160045260246000fd5b51906001600160a01b0382168203608e5756fe60806040526004361015610018575b3661019457610194565b6000803560e01c908163025b22bc1461003b575063f851a4400361000e5761010d565b3461010a5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261010a5773ffffffffffffffffffffffffffffffffffffffff60043581811691828203610106577f0000000000000000000000000000000000000000000000000000000000000000163314600014610101577f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc557fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b8280a280f35b61023d565b8380fd5b80fd5b346101645760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610164576020610146610169565b73ffffffffffffffffffffffffffffffffffffffff60405191168152f35b600080fd5b333003610101577f000000000000000000000000000000000000000000000000000000000000000090565b60ff7f68df44b1011761f481358c0f49a711192727fb02c377d697bcb0ea8ff8393ac0541615806101ef575b1561023d5760046040517ff92ee8a9000000000000000000000000000000000000000000000000000000008152fd5b507f400ada75000000000000000000000000000000000000000000000000000000007fffffffff000000000000000000000000000000000000000000000000000000006000351614156101c0565b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc546000808092368280378136915af43d82803e1561027a573d90f35b3d90fdfea2646970667358221220c7c26ff3040b96a28e96d6d27b743972943aeaef81cc821544c5fe1e24f9b17264736f6c63430008190033' +/** + * Find hex value positions + */ +const findHexValueOccurrences = ( + haystack: string, + needle: string +): readonly number[] => { + const findRec = ( + startPos: number, + acc: readonly number[] + ): readonly number[] => { + const foundPos = haystack.indexOf(needle, startPos) + + if (foundPos === -1) { + return acc + } + + const byteOffset = foundPos / 2 + return findRec(foundPos + needle.length, [...acc, byteOffset]) + } + return findRec(0, []) +} + const ERC20_ABI = erc20Artifact.abi class CowShedSdk { factoryAddress: `0x${string}` @@ -302,9 +325,37 @@ async function setupCowShedPostHooks( args: [typedBridgeData, typedRelayData], }) - // Calculate the offset for the minAmount field in the BridgeData struct - // This is a fixed offset in the calldata where the minAmount value needs to be patched - const minAmountOffset = 644n + // Calculate the offset for the minAmount field using a placeholder + const placeholderAmount = + '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' // Max uint256 + + // Create bridge data with placeholder for finding the offset + const bridgeDataWithPlaceholder = { + ...typedBridgeData, + minAmount: BigInt(placeholderAmount), + } + + // Encode calldata with placeholder + const calldataWithPlaceholder = encodeFunctionData({ + abi: relayFacetAbi, + functionName: 'startBridgeTokensViaRelay', + args: [bridgeDataWithPlaceholder, typedRelayData], + }) + + // Find the placeholder in the calldata (remove 0x prefix for search) + const placeholderHex = placeholderAmount.slice(2) // Remove 0x prefix + const offsets = findHexValueOccurrences( + calldataWithPlaceholder.slice(2), + placeholderHex + ) + + if (offsets.length === 0) { + throw new Error('Could not find minAmount placeholder in encoded calldata') + } + + // Use the first occurrence (should be the minAmount field) + const minAmountOffset = BigInt(offsets[0]) + consola.info(`Found minAmount offset: ${minAmountOffset} bytes`) // Encode the balanceOf call to get the USDC balance const valueGetter = encodeFunctionData({ From 452fb4180c5809bce2beb4aa219aabbc4650a64d Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Mon, 26 May 2025 09:58:45 +0300 Subject: [PATCH 17/46] use viem --- script/demoScripts/demoPatcher.ts | 36 +++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/script/demoScripts/demoPatcher.ts b/script/demoScripts/demoPatcher.ts index 45e47da3f..7599fadb1 100644 --- a/script/demoScripts/demoPatcher.ts +++ b/script/demoScripts/demoPatcher.ts @@ -10,6 +10,7 @@ import { Hex, recoverMessageAddress, keccak256, + encodePacked, } from 'viem' import { privateKeyToAccount } from 'viem/accounts' import { arbitrum } from 'viem/chains' @@ -250,17 +251,30 @@ async function setupCowShedPostHooks( // receivingAssetId // )) - const requestId = relayRequestId.slice(2) // Remove 0x prefix - const chainId = (42161).toString(16).padStart(64, '0') // Arbitrum chain ID as bytes32 - const contractAddress = LIFI_DIAMOND_ARBITRUM.slice(2).padStart(64, '0') // LiFi Diamond as bytes32 - const sendingAssetId = ARBITRUM_USDC.slice(2).padStart(64, '0') // USDC as bytes32 - const destinationChainId = (8453).toString(16).padStart(64, '0') // Base chain ID as bytes32 - const receiver = signerAddress.slice(2).padStart(64, '0') // Receiver as bytes32 - const receivingAssetId = BASE_USDC.slice(2).padStart(64, '0') // USDC on Base as bytes32 - - // Concatenate all parameters (abi.encodePacked equivalent) - const packedData = - `0x${requestId}${chainId}${contractAddress}${sendingAssetId}${destinationChainId}${receiver}${receivingAssetId}` as `0x${string}` + // Use viem's encodePacked instead of manual concatenation + const packedData = encodePacked( + [ + 'bytes32', + 'uint256', + 'bytes32', + 'bytes32', + 'uint256', + 'bytes32', + 'bytes32', + ], + [ + relayRequestId as `0x${string}`, // requestId + 42161n, // chainId (Arbitrum) + `0x${LIFI_DIAMOND_ARBITRUM.slice(2).padStart( + 64, + '0' + )}` as `0x${string}`, // contract address as bytes32 + `0x${ARBITRUM_USDC.slice(2).padStart(64, '0')}` as `0x${string}`, // sendingAssetId as bytes32 + 8453n, // destinationChainId (Base) + `0x${signerAddress.slice(2).padStart(64, '0')}` as `0x${string}`, // receiver as bytes32 + `0x${BASE_USDC.slice(2).padStart(64, '0')}` as `0x${string}`, // receivingAssetId as bytes32 + ] + ) // Hash the packed data const messageHash = keccak256(packedData) From 0ad1c06ff81b36432d547468d29b366311914955 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Mon, 26 May 2025 14:07:02 +0300 Subject: [PATCH 18/46] add tests --- script/demoScripts/demoPatcher.ts | 75 +- test/solidity/Periphery/Patcher.t.sol | 1067 +++++++++++++++++++++++++ 2 files changed, 1084 insertions(+), 58 deletions(-) create mode 100644 test/solidity/Periphery/Patcher.t.sol diff --git a/script/demoScripts/demoPatcher.ts b/script/demoScripts/demoPatcher.ts index 7599fadb1..a8d90bcfe 100644 --- a/script/demoScripts/demoPatcher.ts +++ b/script/demoScripts/demoPatcher.ts @@ -32,29 +32,6 @@ const VAULT_RELAYER_ARBITRUM = '0xC92E8bdf79f0507f65a392b0ab4667716BFE0110' const PROXY_CREATION_CODE = '0x60a034608e57601f61037138819003918201601f19168301916001600160401b038311848410176093578084926040948552833981010312608e57604b602060458360a9565b920160a9565b6080527f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc556040516102b490816100bd8239608051818181608f01526101720152f35b600080fd5b634e487b7160e01b600052604160045260246000fd5b51906001600160a01b0382168203608e5756fe60806040526004361015610018575b3661019457610194565b6000803560e01c908163025b22bc1461003b575063f851a4400361000e5761010d565b3461010a5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261010a5773ffffffffffffffffffffffffffffffffffffffff60043581811691828203610106577f0000000000000000000000000000000000000000000000000000000000000000163314600014610101577f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc557fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b8280a280f35b61023d565b8380fd5b80fd5b346101645760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610164576020610146610169565b73ffffffffffffffffffffffffffffffffffffffff60405191168152f35b600080fd5b333003610101577f000000000000000000000000000000000000000000000000000000000000000090565b60ff7f68df44b1011761f481358c0f49a711192727fb02c377d697bcb0ea8ff8393ac0541615806101ef575b1561023d5760046040517ff92ee8a9000000000000000000000000000000000000000000000000000000008152fd5b507f400ada75000000000000000000000000000000000000000000000000000000007fffffffff000000000000000000000000000000000000000000000000000000006000351614156101c0565b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc546000808092368280378136915af43d82803e1561027a573d90f35b3d90fdfea2646970667358221220c7c26ff3040b96a28e96d6d27b743972943aeaef81cc821544c5fe1e24f9b17264736f6c63430008190033' -/** - * Find hex value positions - */ -const findHexValueOccurrences = ( - haystack: string, - needle: string -): readonly number[] => { - const findRec = ( - startPos: number, - acc: readonly number[] - ): readonly number[] => { - const foundPos = haystack.indexOf(needle, startPos) - - if (foundPos === -1) { - return acc - } - - const byteOffset = foundPos / 2 - return findRec(foundPos + needle.length, [...acc, byteOffset]) - } - return findRec(0, []) -} - const ERC20_ABI = erc20Artifact.abi class CowShedSdk { factoryAddress: `0x${string}` @@ -339,37 +316,19 @@ async function setupCowShedPostHooks( args: [typedBridgeData, typedRelayData], }) - // Calculate the offset for the minAmount field using a placeholder - const placeholderAmount = - '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' // Max uint256 - - // Create bridge data with placeholder for finding the offset - const bridgeDataWithPlaceholder = { - ...typedBridgeData, - minAmount: BigInt(placeholderAmount), - } - - // Encode calldata with placeholder - const calldataWithPlaceholder = encodeFunctionData({ - abi: relayFacetAbi, - functionName: 'startBridgeTokensViaRelay', - args: [bridgeDataWithPlaceholder, typedRelayData], - }) - - // Find the placeholder in the calldata (remove 0x prefix for search) - const placeholderHex = placeholderAmount.slice(2) // Remove 0x prefix - const offsets = findHexValueOccurrences( - calldataWithPlaceholder.slice(2), - placeholderHex - ) - - if (offsets.length === 0) { - throw new Error('Could not find minAmount placeholder in encoded calldata') - } + // Calculate the correct offset for the minAmount field in BridgeData + // For startBridgeTokensViaRelay(BridgeData calldata _bridgeData, RelayData calldata _relayData): + // - 4 bytes: function selector + // - 32 bytes: offset to _bridgeData struct (0x40 = 64) + // - 32 bytes: offset to _relayData struct + // - Then the actual BridgeData struct starts at offset 68 (4 + 64) + // - Within BridgeData: transactionId(32) + bridge(32) + integrator(32) + referrer(32) + sendingAssetId(32) + receiver(32) + minAmount(32) + // - So minAmount is at: 68 + 32*6 = 68 + 192 = 260 + const minAmountOffset = 260n + consola.info(`Using calculated minAmount offset: ${minAmountOffset} bytes`) - // Use the first occurrence (should be the minAmount field) - const minAmountOffset = BigInt(offsets[0]) - consola.info(`Found minAmount offset: ${minAmountOffset} bytes`) + // Note: This offset is specifically for startBridgeTokensViaRelay function + // Different bridge functions may have different offsets due to different parameter layouts // Encode the balanceOf call to get the USDC balance const valueGetter = encodeFunctionData({ @@ -383,16 +342,16 @@ async function setupCowShedPostHooks( // Encode the patcher call const patcherCalldata = encodeFunctionData({ abi: parseAbi([ - 'function executeWithMultiplePatches(address[] valueSources, bytes[] valueGetters, address finalTarget, uint256 value, bytes data, uint256[][] offsetGroups, bool delegateCall) returns (bool success, bytes returnData)', + 'function executeWithDynamicPatches(address valueSource, bytes valueGetter, address finalTarget, uint256 value, bytes data, uint256[] offsets, bool delegateCall) returns (bool success, bytes returnData)', ]), - functionName: 'executeWithMultiplePatches', + functionName: 'executeWithDynamicPatches', args: [ - [usdcAddress as `0x${string}`], // valueSources - Array with USDC contract - [valueGetter], // valueGetters - Array with balanceOf call + usdcAddress as `0x${string}`, // valueSource - USDC contract (single address) + valueGetter, // valueGetter - balanceOf call (single bytes) LIFI_DIAMOND_ARBITRUM as `0x${string}`, // finalTarget - LiFiDiamond contract 0n, // value - no ETH being sent relayCalldata, // data - the encoded RelayFacet call - [[minAmountOffset]], // offsetGroups - Array of arrays with positions of minAmount in the calldata + [minAmountOffset], // offsets - Array with position of minAmount in the calldata false, // delegateCall - regular call, not delegateCall ], }) diff --git a/test/solidity/Periphery/Patcher.t.sol b/test/solidity/Periphery/Patcher.t.sol new file mode 100644 index 000000000..870692374 --- /dev/null +++ b/test/solidity/Periphery/Patcher.t.sol @@ -0,0 +1,1067 @@ +// SPDX-License-Identifier: Unlicensed +pragma solidity ^0.8.17; + +import { DSTest } from "ds-test/test.sol"; +import { Vm } from "forge-std/Vm.sol"; +import { Patcher } from "lifi/Periphery/Patcher.sol"; +import { TestToken as ERC20 } from "../utils/TestToken.sol"; +import { ILiFi } from "lifi/Interfaces/ILiFi.sol"; +import { RelayFacet } from "lifi/Facets/RelayFacet.sol"; + +// Custom errors for gas optimization +error MockFailure(); +error TargetFailure(); +error OracleFailure(); +error PriceNotSet(); + +// Mock contract that returns dynamic values +contract MockValueSource { + uint256 public value; + bool public shouldFail; + + function setValue(uint256 _value) external { + value = _value; + } + + function setShouldFail(bool _shouldFail) external { + shouldFail = _shouldFail; + } + + function getValue() external view returns (uint256) { + if (shouldFail) { + revert MockFailure(); + } + return value; + } + + function getBalance( + address token, + address account + ) external view returns (uint256) { + if (shouldFail) { + revert MockFailure(); + } + return ERC20(token).balanceOf(account); + } + + function getMultipleValues() external view returns (uint256, uint256) { + if (shouldFail) { + revert MockFailure(); + } + return (value, value * 2); + } +} + +// Mock target contract for testing calls +contract MockTarget { + uint256 public lastValue; + address public lastSender; + uint256 public lastEthValue; + bytes public lastCalldata; + bool public shouldFail; + + event CallReceived(uint256 value, address sender, uint256 ethValue); + + function setShouldFail(bool _shouldFail) external { + shouldFail = _shouldFail; + } + + function processValue(uint256 _value) external payable { + if (shouldFail) { + revert TargetFailure(); + } + lastValue = _value; + lastSender = msg.sender; + lastEthValue = msg.value; + lastCalldata = msg.data; + emit CallReceived(_value, msg.sender, msg.value); + } + + function processMultipleValues( + uint256 _value1, + uint256 _value2 + ) external payable { + if (shouldFail) { + revert TargetFailure(); + } + lastValue = _value1 + _value2; + lastSender = msg.sender; + lastEthValue = msg.value; + lastCalldata = msg.data; + emit CallReceived(_value1 + _value2, msg.sender, msg.value); + } + + function processComplexData( + uint256 _amount, + address /* _token */, + uint256 _deadline + ) external payable { + if (shouldFail) { + revert TargetFailure(); + } + lastValue = _amount + _deadline; + lastSender = msg.sender; + lastEthValue = msg.value; + lastCalldata = msg.data; + emit CallReceived(_amount + _deadline, msg.sender, msg.value); + } +} + +// Mock price oracle for calculating dynamic minimum amounts +contract MockPriceOracle { + mapping(address => uint256) public prices; // Price in USD with 18 decimals + bool public shouldFail; + + function setPrice(address token, uint256 price) external { + prices[token] = price; + } + + function setShouldFail(bool _shouldFail) external { + shouldFail = _shouldFail; + } + + function getPrice(address token) external view returns (uint256) { + if (shouldFail) { + revert OracleFailure(); + } + return prices[token]; + } + + // Calculate minimum amount with slippage protection + function calculateMinAmount( + address token, + uint256 amount, + uint256 slippageBps // basis points (e.g., 300 = 3%) + ) external view returns (uint256) { + if (shouldFail) { + revert OracleFailure(); + } + uint256 price = prices[token]; + if (price == 0) { + revert PriceNotSet(); + } + + // Apply slippage: minAmount = amount * (10000 - slippageBps) / 10000 + return (amount * (10000 - slippageBps)) / 10000; + } +} + +// Simple test contract that mimics RelayFacet interface for testing +contract TestRelayFacet { + event LiFiTransferStarted(ILiFi.BridgeData bridgeData); + + function startBridgeTokensViaRelay( + ILiFi.BridgeData calldata bridgeData, + RelayFacet.RelayData calldata /* relayData */ + ) external payable { + // For testing, just emit the event to show it was called + emit LiFiTransferStarted(bridgeData); + + // In a real implementation, this would interact with Relay protocol + // For testing, we just need to verify the call succeeds with patched data + } +} + +contract PatcherTest is DSTest { + // solhint-disable immutable-vars-naming + Vm internal immutable vm = Vm(HEVM_ADDRESS); + + Patcher internal patcher; + MockValueSource internal valueSource; + MockTarget internal target; + ERC20 internal token; + MockPriceOracle internal priceOracle; + TestRelayFacet internal relayFacet; + + function setUp() public { + // Set up our test contracts + patcher = new Patcher(); + valueSource = new MockValueSource(); + target = new MockTarget(); + token = new ERC20("Test Token", "TEST", 18); + priceOracle = new MockPriceOracle(); + + // Set up simple RelayFacet for testing + relayFacet = new TestRelayFacet(); + } + + // Test successful single patch execution + function testExecuteWithDynamicPatches_Success() public { + // Set up dynamic value + uint256 dynamicValue = 12345; + valueSource.setValue(dynamicValue); + + // Prepare calldata with placeholder value (0) + bytes memory originalCalldata = abi.encodeWithSelector( + target.processValue.selector, + uint256(0) // This will be patched + ); + + // Define offset where the value should be patched (after selector, at parameter position) + uint256[] memory offsets = new uint256[](1); + offsets[0] = 4; // Skip 4-byte selector + + // Prepare value getter calldata + bytes memory valueGetter = abi.encodeWithSelector( + valueSource.getValue.selector + ); + + // Execute with dynamic patches + (bool success, ) = patcher.executeWithDynamicPatches( + address(valueSource), + valueGetter, + address(target), + 0, // no ETH value + originalCalldata, + offsets, + false // regular call, not delegatecall + ); + + // Verify execution was successful + assertTrue(success); + assertEq(target.lastValue(), dynamicValue); + assertEq(target.lastSender(), address(patcher)); + assertEq(target.lastEthValue(), 0); + } + + // Test successful execution with ETH value + function testExecuteWithDynamicPatches_WithEthValue() public { + uint256 dynamicValue = 54321; + uint256 ethValue = 1 ether; + + valueSource.setValue(dynamicValue); + vm.deal(address(patcher), ethValue); + + bytes memory originalCalldata = abi.encodeWithSelector( + target.processValue.selector, + uint256(0) + ); + + uint256[] memory offsets = new uint256[](1); + offsets[0] = 4; + + bytes memory valueGetter = abi.encodeWithSelector( + valueSource.getValue.selector + ); + + (bool success, ) = patcher.executeWithDynamicPatches( + address(valueSource), + valueGetter, + address(target), + ethValue, + originalCalldata, + offsets, + false + ); + + assertTrue(success); + assertEq(target.lastValue(), dynamicValue); + assertEq(target.lastEthValue(), ethValue); + } + + // Test multiple patches with same value + function testExecuteWithDynamicPatches_MultipleOffsets() public { + uint256 dynamicValue = 98765; + valueSource.setValue(dynamicValue); + + // Calldata with two parameters that should both be patched with the same value + bytes memory originalCalldata = abi.encodeWithSelector( + target.processMultipleValues.selector, + uint256(0), // First parameter to patch + uint256(0) // Second parameter to patch + ); + + uint256[] memory offsets = new uint256[](2); + offsets[0] = 4; // First parameter offset + offsets[1] = 36; // Second parameter offset (4 + 32) + + bytes memory valueGetter = abi.encodeWithSelector( + valueSource.getValue.selector + ); + + (bool success, ) = patcher.executeWithDynamicPatches( + address(valueSource), + valueGetter, + address(target), + 0, + originalCalldata, + offsets, + false + ); + + assertTrue(success); + assertEq(target.lastValue(), dynamicValue * 2); // Sum of both values + } + + // Test multiple patches with different values + function testExecuteWithMultiplePatches_Success() public { + uint256 value1 = 11111; + uint256 value2 = 22222; + + // Set up two value sources + MockValueSource valueSource2 = new MockValueSource(); + valueSource.setValue(value1); + valueSource2.setValue(value2); + + bytes memory originalCalldata = abi.encodeWithSelector( + target.processMultipleValues.selector, + uint256(0), // Will be patched with value1 + uint256(0) // Will be patched with value2 + ); + + // Set up arrays for multiple patches + address[] memory valueSources = new address[](2); + valueSources[0] = address(valueSource); + valueSources[1] = address(valueSource2); + + bytes[] memory valueGetters = new bytes[](2); + valueGetters[0] = abi.encodeWithSelector( + valueSource.getValue.selector + ); + valueGetters[1] = abi.encodeWithSelector( + valueSource2.getValue.selector + ); + + uint256[][] memory offsetGroups = new uint256[][](2); + offsetGroups[0] = new uint256[](1); + offsetGroups[0][0] = 4; // First parameter + offsetGroups[1] = new uint256[](1); + offsetGroups[1][0] = 36; // Second parameter + + (bool success, ) = patcher.executeWithMultiplePatches( + valueSources, + valueGetters, + address(target), + 0, + originalCalldata, + offsetGroups, + false + ); + + assertTrue(success); + assertEq(target.lastValue(), value1 + value2); + } + + // Test delegatecall execution + function testExecuteWithDynamicPatches_Delegatecall() public { + uint256 dynamicValue = 77777; + valueSource.setValue(dynamicValue); + + bytes memory originalCalldata = abi.encodeWithSelector( + target.processValue.selector, + uint256(0) + ); + + uint256[] memory offsets = new uint256[](1); + offsets[0] = 4; + + bytes memory valueGetter = abi.encodeWithSelector( + valueSource.getValue.selector + ); + + (bool success, ) = patcher.executeWithDynamicPatches( + address(valueSource), + valueGetter, + address(target), + 0, + originalCalldata, + offsets, + true // delegatecall + ); + + assertTrue(success); + // Note: In delegatecall, the target's storage won't be modified + // but the call should still succeed + } + + // Test error when getting dynamic value fails + function testExecuteWithDynamicPatches_FailedToGetDynamicValue() public { + valueSource.setShouldFail(true); + + bytes memory originalCalldata = abi.encodeWithSelector( + target.processValue.selector, + uint256(0) + ); + + uint256[] memory offsets = new uint256[](1); + offsets[0] = 4; + + bytes memory valueGetter = abi.encodeWithSelector( + valueSource.getValue.selector + ); + + vm.expectRevert(Patcher.FailedToGetDynamicValue.selector); + patcher.executeWithDynamicPatches( + address(valueSource), + valueGetter, + address(target), + 0, + originalCalldata, + offsets, + false + ); + } + + // Test error when patch offset is invalid + function testExecuteWithDynamicPatches_InvalidPatchOffset() public { + uint256 dynamicValue = 12345; + valueSource.setValue(dynamicValue); + + bytes memory originalCalldata = abi.encodeWithSelector( + target.processValue.selector, + uint256(0) + ); + + uint256[] memory offsets = new uint256[](1); + offsets[0] = originalCalldata.length; // Invalid offset (beyond data length) + + bytes memory valueGetter = abi.encodeWithSelector( + valueSource.getValue.selector + ); + + vm.expectRevert(Patcher.InvalidPatchOffset.selector); + patcher.executeWithDynamicPatches( + address(valueSource), + valueGetter, + address(target), + 0, + originalCalldata, + offsets, + false + ); + } + + // Test error when arrays have mismatched lengths + function testExecuteWithMultiplePatches_MismatchedArrayLengths() public { + address[] memory valueSources = new address[](2); + valueSources[0] = address(valueSource); + valueSources[1] = address(valueSource); + + bytes[] memory valueGetters = new bytes[](1); // Mismatched length + valueGetters[0] = abi.encodeWithSelector( + valueSource.getValue.selector + ); + + uint256[][] memory offsetGroups = new uint256[][](2); + offsetGroups[0] = new uint256[](1); + offsetGroups[1] = new uint256[](1); + + bytes memory originalCalldata = abi.encodeWithSelector( + target.processValue.selector, + uint256(0) + ); + + vm.expectRevert(Patcher.MismatchedArrayLengths.selector); + patcher.executeWithMultiplePatches( + valueSources, + valueGetters, + address(target), + 0, + originalCalldata, + offsetGroups, + false + ); + } + + // Test complex scenario with token balance patching + function testExecuteWithDynamicPatches_TokenBalance() public { + // Mint tokens to an account + address holder = address(0x1234); + uint256 balance = 1000 ether; + token.mint(holder, balance); + + // Prepare calldata that uses the token balance + bytes memory originalCalldata = abi.encodeWithSelector( + target.processComplexData.selector, + uint256(0), // amount - will be patched with balance + address(token), + block.timestamp + 1 hours + ); + + uint256[] memory offsets = new uint256[](1); + offsets[0] = 4; // Patch the amount parameter + + // Use balanceOf call to get dynamic value + bytes memory valueGetter = abi.encodeWithSelector( + token.balanceOf.selector, + holder + ); + + (bool success, ) = patcher.executeWithDynamicPatches( + address(token), + valueGetter, + address(target), + 0, + originalCalldata, + offsets, + false + ); + + assertTrue(success); + assertEq(target.lastValue(), balance + block.timestamp + 1 hours); + } + + // Test that target call failure is properly handled + function testExecuteWithDynamicPatches_TargetCallFailure() public { + uint256 dynamicValue = 12345; + valueSource.setValue(dynamicValue); + target.setShouldFail(true); + + bytes memory originalCalldata = abi.encodeWithSelector( + target.processValue.selector, + uint256(0) + ); + + uint256[] memory offsets = new uint256[](1); + offsets[0] = 4; + + bytes memory valueGetter = abi.encodeWithSelector( + valueSource.getValue.selector + ); + + (bool success, bytes memory returnData) = patcher + .executeWithDynamicPatches( + address(valueSource), + valueGetter, + address(target), + 0, + originalCalldata, + offsets, + false + ); + + // The patcher should return false for failed calls, not revert + assertTrue(!success); + // Return data should contain the revert reason + assertTrue(returnData.length > 0); + } + + // Test edge case with empty offsets array + function testExecuteWithDynamicPatches_EmptyOffsets() public { + uint256 dynamicValue = 12345; + valueSource.setValue(dynamicValue); + + bytes memory originalCalldata = abi.encodeWithSelector( + target.processValue.selector, + uint256(99999) // This value should remain unchanged + ); + + uint256[] memory offsets = new uint256[](0); // Empty offsets + + bytes memory valueGetter = abi.encodeWithSelector( + valueSource.getValue.selector + ); + + (bool success, ) = patcher.executeWithDynamicPatches( + address(valueSource), + valueGetter, + address(target), + 0, + originalCalldata, + offsets, + false + ); + + assertTrue(success); + assertEq(target.lastValue(), 99999); // Original value should be preserved + } + + // Test multiple patches on the same offset (should overwrite) + function testExecuteWithMultiplePatches_SameOffset() public { + uint256 value1 = 11111; + uint256 value2 = 22222; + + MockValueSource valueSource2 = new MockValueSource(); + valueSource.setValue(value1); + valueSource2.setValue(value2); + + bytes memory originalCalldata = abi.encodeWithSelector( + target.processValue.selector, + uint256(0) + ); + + address[] memory valueSources = new address[](2); + valueSources[0] = address(valueSource); + valueSources[1] = address(valueSource2); + + bytes[] memory valueGetters = new bytes[](2); + valueGetters[0] = abi.encodeWithSelector( + valueSource.getValue.selector + ); + valueGetters[1] = abi.encodeWithSelector( + valueSource2.getValue.selector + ); + + uint256[][] memory offsetGroups = new uint256[][](2); + offsetGroups[0] = new uint256[](1); + offsetGroups[0][0] = 4; // Same offset + offsetGroups[1] = new uint256[](1); + offsetGroups[1][0] = 4; // Same offset (should overwrite) + + (bool success, ) = patcher.executeWithMultiplePatches( + valueSources, + valueGetters, + address(target), + 0, + originalCalldata, + offsetGroups, + false + ); + + assertTrue(success); + assertEq(target.lastValue(), value2); // Should have the last written value + } + + // Test with zero value + function testExecuteWithDynamicPatches_ZeroValue() public { + uint256 dynamicValue = 0; + valueSource.setValue(dynamicValue); + + bytes memory originalCalldata = abi.encodeWithSelector( + target.processValue.selector, + uint256(12345) // Will be overwritten with 0 + ); + + uint256[] memory offsets = new uint256[](1); + offsets[0] = 4; + + bytes memory valueGetter = abi.encodeWithSelector( + valueSource.getValue.selector + ); + + (bool success, ) = patcher.executeWithDynamicPatches( + address(valueSource), + valueGetter, + address(target), + 0, + originalCalldata, + offsets, + false + ); + + assertTrue(success); + assertEq(target.lastValue(), 0); + } + + // Test with maximum uint256 value + function testExecuteWithDynamicPatches_MaxValue() public { + uint256 dynamicValue = type(uint256).max; + valueSource.setValue(dynamicValue); + + bytes memory originalCalldata = abi.encodeWithSelector( + target.processValue.selector, + uint256(0) + ); + + uint256[] memory offsets = new uint256[](1); + offsets[0] = 4; + + bytes memory valueGetter = abi.encodeWithSelector( + valueSource.getValue.selector + ); + + (bool success, ) = patcher.executeWithDynamicPatches( + address(valueSource), + valueGetter, + address(target), + 0, + originalCalldata, + offsets, + false + ); + + assertTrue(success); + assertEq(target.lastValue(), type(uint256).max); + } + + // Test realistic BridgeData minAmount patching with price oracle using real RelayFacet + function testExecuteWithDynamicPatches_RelayFacetMinAmount() public { + // Set up token price and slippage + uint256 tokenPrice = 2000 * 1e18; // $2000 per token + uint256 slippageBps = 300; // 3% slippage + priceOracle.setPrice(address(token), tokenPrice); + + // Create BridgeData with placeholder minAmount (0) + ILiFi.BridgeData memory bridgeData = ILiFi.BridgeData({ + transactionId: bytes32("test-tx-id"), + bridge: "relay", + integrator: "TestIntegrator", + referrer: address(0x1234), + sendingAssetId: address(token), + receiver: address(0x5678), + minAmount: 0, // This will be patched + destinationChainId: 8453, // Base + hasSourceSwaps: false, + hasDestinationCall: false + }); + + // Create RelayData + RelayFacet.RelayData memory relayData = RelayFacet.RelayData({ + requestId: bytes32("test-request-id"), + nonEVMReceiver: bytes32(0), + receivingAssetId: bytes32(uint256(uint160(address(0xDEF)))), + signature: hex"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef01" + }); + + // Encode the RelayFacet call with placeholder minAmount + bytes memory originalCalldata = abi.encodeWithSelector( + relayFacet.startBridgeTokensViaRelay.selector, + bridgeData, + relayData + ); + + // Use the offset we found: 260 bytes + uint256[] memory offsets = new uint256[](1); + offsets[0] = 260; + + // Prepare oracle call to calculate minAmount with slippage + uint256 bridgeAmount = 1000 ether; + bytes memory valueGetter = abi.encodeWithSelector( + priceOracle.calculateMinAmount.selector, + address(token), + bridgeAmount, + slippageBps + ); + + (bool success, ) = patcher.executeWithDynamicPatches( + address(priceOracle), + valueGetter, + address(relayFacet), + 0, + originalCalldata, + offsets, + false + ); + + assertTrue(success); + + // The fact that the call succeeded means the patching worked correctly + // We can't verify the exact minAmount since the real RelayFacet doesn't store state + } + + // Test BridgeData patching with multiple dynamic values using RelayFacet + function testExecuteWithMultiplePatches_RelayFacetMultipleFields() public { + // Set up two different price oracles for different calculations + MockPriceOracle priceOracle2 = new MockPriceOracle(); + priceOracle.setPrice(address(token), 2000 * 1e18); + priceOracle2.setPrice(address(token), 1800 * 1e18); // Different price for comparison + + ILiFi.BridgeData memory bridgeData = ILiFi.BridgeData({ + transactionId: bytes32("multi-patch-tx"), + bridge: "relay", + integrator: "TestIntegrator", + referrer: address(0x1234), + sendingAssetId: address(token), + receiver: address(0x5678), + minAmount: 0, // Will be patched with first oracle + destinationChainId: 0, // Will be patched with second oracle result + hasSourceSwaps: false, + hasDestinationCall: false + }); + + RelayFacet.RelayData memory relayData = RelayFacet.RelayData({ + requestId: bytes32("multi-patch-request"), + nonEVMReceiver: bytes32(0), + receivingAssetId: bytes32(uint256(uint160(address(0xDEF)))), + signature: hex"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef01" + }); + + bytes memory originalCalldata = abi.encodeWithSelector( + relayFacet.startBridgeTokensViaRelay.selector, + bridgeData, + relayData + ); + + // Set up multiple patches + address[] memory valueSources = new address[](2); + valueSources[0] = address(priceOracle); + valueSources[1] = address(priceOracle2); + + bytes[] memory valueGetters = new bytes[](2); + valueGetters[0] = abi.encodeWithSelector( + priceOracle.calculateMinAmount.selector, + address(token), + 1000 ether, + 300 // 3% slippage + ); + valueGetters[1] = abi.encodeWithSelector( + priceOracle2.getPrice.selector, + address(token) + ); + + uint256[][] memory offsetGroups = new uint256[][](2); + offsetGroups[0] = new uint256[](1); + offsetGroups[0][0] = 260; // minAmount offset + offsetGroups[1] = new uint256[](1); + offsetGroups[1][0] = 292; // destinationChainId offset (minAmount + 32) + + (bool success, ) = patcher.executeWithMultiplePatches( + valueSources, + valueGetters, + address(relayFacet), + 0, + originalCalldata, + offsetGroups, + false + ); + + assertTrue(success); + + // The fact that the call succeeded means the patching worked correctly + // We can't verify the exact values since our TestRelayFacet doesn't store state + } + + // Test BridgeData patching with token balance as minAmount using RelayFacet + function testExecuteWithDynamicPatches_RelayFacetTokenBalance() public { + // Set up a user with token balance + address user = address(0x9999); + uint256 userBalance = 500 ether; + token.mint(user, userBalance); + + ILiFi.BridgeData memory bridgeData = ILiFi.BridgeData({ + transactionId: bytes32("balance-patch-tx"), + bridge: "relay", + integrator: "TestIntegrator", + referrer: address(0x1234), + sendingAssetId: address(token), + receiver: user, + minAmount: 0, // Will be patched with user's balance + destinationChainId: 8453, // Base + hasSourceSwaps: false, + hasDestinationCall: false + }); + + RelayFacet.RelayData memory relayData = RelayFacet.RelayData({ + requestId: bytes32("balance-patch-request"), + nonEVMReceiver: bytes32(0), + receivingAssetId: bytes32(uint256(uint160(address(0xDEF)))), + signature: hex"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef01" + }); + + bytes memory originalCalldata = abi.encodeWithSelector( + relayFacet.startBridgeTokensViaRelay.selector, + bridgeData, + relayData + ); + + uint256[] memory offsets = new uint256[](1); + offsets[0] = 260; // minAmount offset + + // Use token.balanceOf to get dynamic value + bytes memory valueGetter = abi.encodeWithSelector( + token.balanceOf.selector, + user + ); + + (bool success, ) = patcher.executeWithDynamicPatches( + address(token), + valueGetter, + address(relayFacet), + 0, + originalCalldata, + offsets, + false + ); + + assertTrue(success); + // The fact that the call succeeded means the patching worked correctly + } + + // Test BridgeData patching with swap scenario using RelayFacet (not applicable since RelayFacet doesn't support swaps) + // Removed this test as RelayFacet doesn't have swapAndStartBridgeTokensViaRelay + + // Test error handling when oracle fails during BridgeData patching with RelayFacet + function testExecuteWithDynamicPatches_RelayFacetOracleFailure() public { + priceOracle.setShouldFail(true); + + ILiFi.BridgeData memory bridgeData = ILiFi.BridgeData({ + transactionId: bytes32("fail-tx"), + bridge: "relay", + integrator: "TestIntegrator", + referrer: address(0x1234), + sendingAssetId: address(token), + receiver: address(0x5678), + minAmount: 0, + destinationChainId: 8453, + hasSourceSwaps: false, + hasDestinationCall: false + }); + + RelayFacet.RelayData memory relayData = RelayFacet.RelayData({ + requestId: bytes32("fail-request"), + nonEVMReceiver: bytes32(0), + receivingAssetId: bytes32(uint256(uint160(address(0xDEF)))), + signature: hex"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef01" + }); + + bytes memory originalCalldata = abi.encodeWithSelector( + relayFacet.startBridgeTokensViaRelay.selector, + bridgeData, + relayData + ); + + uint256[] memory offsets = new uint256[](1); + offsets[0] = 260; + + bytes memory valueGetter = abi.encodeWithSelector( + priceOracle.calculateMinAmount.selector, + address(token), + 1000 ether, + 300 + ); + + vm.expectRevert(Patcher.FailedToGetDynamicValue.selector); + patcher.executeWithDynamicPatches( + address(priceOracle), + valueGetter, + address(relayFacet), + 0, + originalCalldata, + offsets, + false + ); + } + + // Test with real RelayFacet to find correct offset for startBridgeTokensViaRelay + function testExecuteWithDynamicPatches_RealRelayFacet() public { + // Set up a user with token balance + address user = address(0x9999); + uint256 userBalance = 500 ether; + token.mint(user, userBalance); + + // Create BridgeData with placeholder minAmount + ILiFi.BridgeData memory bridgeData = ILiFi.BridgeData({ + transactionId: bytes32("relay-patch-tx"), + bridge: "relay", + integrator: "TestIntegrator", + referrer: address(0x1234), + sendingAssetId: address(token), + receiver: user, + minAmount: 0, // Will be patched with user's balance + destinationChainId: 8453, // Base + hasSourceSwaps: false, + hasDestinationCall: false + }); + + // Create RelayData with mock signature + RelayFacet.RelayData memory relayData = RelayFacet.RelayData({ + requestId: bytes32("test-request-id"), + nonEVMReceiver: bytes32(0), + receivingAssetId: bytes32(uint256(uint160(address(0xDEF)))), + signature: hex"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef01" // 65 bytes mock signature + }); + + // Encode the RelayFacet call + bytes memory originalCalldata = abi.encodeWithSelector( + relayFacet.startBridgeTokensViaRelay.selector, + bridgeData, + relayData + ); + + // Test different offsets to find the correct one + uint256[] memory testOffsets = new uint256[](5); + testOffsets[0] = 228; // For single parameter functions + testOffsets[1] = 260; // Our calculated offset + testOffsets[2] = 292; // Alternative calculation + testOffsets[3] = 324; // Another possibility + testOffsets[4] = 356; // Yet another possibility + + bytes memory valueGetter = abi.encodeWithSelector( + token.balanceOf.selector, + user + ); + + // Try each offset and see which one works + for (uint256 i = 0; i < testOffsets.length; i++) { + uint256[] memory offsets = new uint256[](1); + offsets[0] = testOffsets[i]; + + try + patcher.executeWithDynamicPatches( + address(token), + valueGetter, + address(relayFacet), + 0, + originalCalldata, + offsets, + false + ) + returns (bool success, bytes memory) { + if (success) { + // If successful, let's verify the minAmount was actually patched + // by decoding the calldata and checking the minAmount field + emit log_named_uint( + "Successful offset found", + testOffsets[i] + ); + + // For now, we'll just mark this as the working offset + // In a real scenario, we'd verify the minAmount was correctly set + assertTrue(success); + return; // Exit on first success + } + } catch { + // This offset didn't work, continue to next + emit log_named_uint("Failed offset", testOffsets[i]); + } + } + + // If we get here, none of the offsets worked + assertTrue(false, "No working offset found"); + } + + // Helper test to find the exact offset by examining calldata structure + function testFindRelayFacetMinAmountOffset() public { + // Create BridgeData with a marker value for minAmount + uint256 markerValue = 0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF; + + ILiFi.BridgeData memory bridgeData = ILiFi.BridgeData({ + transactionId: bytes32("test-tx-id"), + bridge: "relay", + integrator: "TestIntegrator", + referrer: address(0x1234), + sendingAssetId: address(token), + receiver: address(0x5678), + minAmount: markerValue, // Marker to find in calldata + destinationChainId: 8453, + hasSourceSwaps: false, + hasDestinationCall: false + }); + + // Create RelayData + RelayFacet.RelayData memory relayData = RelayFacet.RelayData({ + requestId: bytes32("test-request-id"), + nonEVMReceiver: bytes32(0), + receivingAssetId: bytes32(uint256(uint160(address(0xDEF)))), + signature: hex"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef01" + }); + + // Encode the calldata + bytes memory calldata_ = abi.encodeWithSelector( + relayFacet.startBridgeTokensViaRelay.selector, + bridgeData, + relayData + ); + + emit log_named_bytes("Full calldata", calldata_); + emit log_named_uint("Calldata length", calldata_.length); + + // Find the marker value in the calldata + bytes32 marker = bytes32(markerValue); + bool found = false; + + for (uint256 i = 0; i <= calldata_.length - 32; i++) { + bytes32 chunk; + assembly { + chunk := mload(add(add(calldata_, 0x20), i)) + } + if (chunk == marker) { + emit log_named_uint("Found minAmount marker at offset", i); + found = true; + break; + } + } + + assertTrue(found, "Marker not found in calldata"); + } +} From 379f36300b718b944de25d9c104a607029076c5f Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Mon, 26 May 2025 16:26:26 +0300 Subject: [PATCH 19/46] update tests --- script/demoScripts/demoPatcher.ts | 22 +- test/solidity/Periphery/Patcher.t.sol | 416 ++++++++++---------------- 2 files changed, 169 insertions(+), 269 deletions(-) diff --git a/script/demoScripts/demoPatcher.ts b/script/demoScripts/demoPatcher.ts index a8d90bcfe..e0c00b5ce 100644 --- a/script/demoScripts/demoPatcher.ts +++ b/script/demoScripts/demoPatcher.ts @@ -11,6 +11,7 @@ import { recoverMessageAddress, keccak256, encodePacked, + pad, } from 'viem' import { privateKeyToAccount } from 'viem/accounts' import { arbitrum } from 'viem/chains' @@ -154,8 +155,9 @@ async function setupCowShedPostHooks( // Get a real signature from the Relay API // First, create a quote request + // Use LiFi Diamond as the user since it will be the final caller of RelayFacet const quoteParams = { - user: LIFI_DIAMOND_ARBITRUM, + user: LIFI_DIAMOND_ARBITRUM, // LiFi Diamond will be address(this) in RelayFacet originChainId: 42161, // Arbitrum destinationChainId: 8453, // BASE originCurrency: ARBITRUM_USDC, @@ -242,14 +244,11 @@ async function setupCowShedPostHooks( [ relayRequestId as `0x${string}`, // requestId 42161n, // chainId (Arbitrum) - `0x${LIFI_DIAMOND_ARBITRUM.slice(2).padStart( - 64, - '0' - )}` as `0x${string}`, // contract address as bytes32 - `0x${ARBITRUM_USDC.slice(2).padStart(64, '0')}` as `0x${string}`, // sendingAssetId as bytes32 + pad(LIFI_DIAMOND_ARBITRUM as `0x${string}`), // LiFi Diamond address as bytes32 (address(this) in RelayFacet) + pad(ARBITRUM_USDC as `0x${string}`), // sendingAssetId as bytes32 8453n, // destinationChainId (Base) - `0x${signerAddress.slice(2).padStart(64, '0')}` as `0x${string}`, // receiver as bytes32 - `0x${BASE_USDC.slice(2).padStart(64, '0')}` as `0x${string}`, // receivingAssetId as bytes32 + pad(signerAddress as `0x${string}`), // receiver as bytes32 + pad(BASE_USDC as `0x${string}`), // receivingAssetId as bytes32 ] ) @@ -276,10 +275,7 @@ async function setupCowShedPostHooks( requestId: relayRequestId, nonEVMReceiver: '0x0000000000000000000000000000000000000000000000000000000000000000' as `0x${string}`, // Not bridging to non-EVM chain - receivingAssetId: `0x${BASE_USDC.slice(2).padStart( - 64, - '0' - )}` as `0x${string}`, // USDC on BASE as bytes32 + receivingAssetId: pad(BASE_USDC as `0x${string}`), // Use viem's pad instead of manual padStart signature: relaySignature, // Real signature from the Relay API } @@ -352,7 +348,7 @@ async function setupCowShedPostHooks( 0n, // value - no ETH being sent relayCalldata, // data - the encoded RelayFacet call [minAmountOffset], // offsets - Array with position of minAmount in the calldata - false, // delegateCall - regular call, not delegateCall + true, // delegateCall ], }) diff --git a/test/solidity/Periphery/Patcher.t.sol b/test/solidity/Periphery/Patcher.t.sol index 870692374..058db0b1b 100644 --- a/test/solidity/Periphery/Patcher.t.sol +++ b/test/solidity/Periphery/Patcher.t.sol @@ -7,6 +7,8 @@ import { Patcher } from "lifi/Periphery/Patcher.sol"; import { TestToken as ERC20 } from "../utils/TestToken.sol"; import { ILiFi } from "lifi/Interfaces/ILiFi.sol"; import { RelayFacet } from "lifi/Facets/RelayFacet.sol"; +import { LibAsset } from "lifi/Libraries/LibAsset.sol"; +import { LibAllowList } from "lifi/Libraries/LibAllowList.sol"; // Custom errors for gas optimization error MockFailure(); @@ -146,19 +148,19 @@ contract MockPriceOracle { } } -// Simple test contract that mimics RelayFacet interface for testing -contract TestRelayFacet { - event LiFiTransferStarted(ILiFi.BridgeData bridgeData); +// Test RelayFacet Contract +contract TestRelayFacet is RelayFacet { + constructor( + address _relayReceiver, + address _relaySolver + ) RelayFacet(_relayReceiver, _relaySolver) {} - function startBridgeTokensViaRelay( - ILiFi.BridgeData calldata bridgeData, - RelayFacet.RelayData calldata /* relayData */ - ) external payable { - // For testing, just emit the event to show it was called - emit LiFiTransferStarted(bridgeData); + function addDex(address _dex) external { + LibAllowList.addAllowedContract(_dex); + } - // In a real implementation, this would interact with Relay protocol - // For testing, we just need to verify the call succeeds with patched data + function setFunctionApprovalBySignature(bytes4 _signature) external { + LibAllowList.addAllowedSelector(_signature); } } @@ -166,6 +168,10 @@ contract PatcherTest is DSTest { // solhint-disable immutable-vars-naming Vm internal immutable vm = Vm(HEVM_ADDRESS); + // Events for testing + event CallReceived(uint256 value, address sender, uint256 ethValue); + event LiFiTransferStarted(ILiFi.BridgeData bridgeData); + Patcher internal patcher; MockValueSource internal valueSource; MockTarget internal target; @@ -173,6 +179,12 @@ contract PatcherTest is DSTest { MockPriceOracle internal priceOracle; TestRelayFacet internal relayFacet; + // RelayFacet setup variables + address internal constant RELAY_RECEIVER = + 0xa5F565650890fBA1824Ee0F21EbBbF660a179934; + uint256 internal privateKey = 0x1234567890; + address internal relaySolver; + function setUp() public { // Set up our test contracts patcher = new Patcher(); @@ -181,8 +193,9 @@ contract PatcherTest is DSTest { token = new ERC20("Test Token", "TEST", 18); priceOracle = new MockPriceOracle(); - // Set up simple RelayFacet for testing - relayFacet = new TestRelayFacet(); + // Set up real RelayFacet for testing + relaySolver = vm.addr(privateKey); + relayFacet = new TestRelayFacet(RELAY_RECEIVER, relaySolver); } // Test successful single patch execution @@ -206,8 +219,12 @@ contract PatcherTest is DSTest { valueSource.getValue.selector ); + // Expect the CallReceived event to be emitted with the patched value + vm.expectEmit(true, true, true, true, address(target)); + emit CallReceived(dynamicValue, address(patcher), 0); + // Execute with dynamic patches - (bool success, ) = patcher.executeWithDynamicPatches( + patcher.executeWithDynamicPatches( address(valueSource), valueGetter, address(target), @@ -218,7 +235,6 @@ contract PatcherTest is DSTest { ); // Verify execution was successful - assertTrue(success); assertEq(target.lastValue(), dynamicValue); assertEq(target.lastSender(), address(patcher)); assertEq(target.lastEthValue(), 0); @@ -244,7 +260,11 @@ contract PatcherTest is DSTest { valueSource.getValue.selector ); - (bool success, ) = patcher.executeWithDynamicPatches( + // Expect the CallReceived event to be emitted with the patched value and ETH + vm.expectEmit(true, true, true, true, address(target)); + emit CallReceived(dynamicValue, address(patcher), ethValue); + + patcher.executeWithDynamicPatches( address(valueSource), valueGetter, address(target), @@ -254,7 +274,6 @@ contract PatcherTest is DSTest { false ); - assertTrue(success); assertEq(target.lastValue(), dynamicValue); assertEq(target.lastEthValue(), ethValue); } @@ -279,7 +298,11 @@ contract PatcherTest is DSTest { valueSource.getValue.selector ); - (bool success, ) = patcher.executeWithDynamicPatches( + // Expect the CallReceived event to be emitted with the sum of both values + vm.expectEmit(true, true, true, true, address(target)); + emit CallReceived(dynamicValue * 2, address(patcher), 0); + + patcher.executeWithDynamicPatches( address(valueSource), valueGetter, address(target), @@ -289,7 +312,6 @@ contract PatcherTest is DSTest { false ); - assertTrue(success); assertEq(target.lastValue(), dynamicValue * 2); // Sum of both values } @@ -328,7 +350,11 @@ contract PatcherTest is DSTest { offsetGroups[1] = new uint256[](1); offsetGroups[1][0] = 36; // Second parameter - (bool success, ) = patcher.executeWithMultiplePatches( + // Expect the CallReceived event to be emitted with the sum of both values + vm.expectEmit(true, true, true, true, address(target)); + emit CallReceived(value1 + value2, address(patcher), 0); + + patcher.executeWithMultiplePatches( valueSources, valueGetters, address(target), @@ -338,7 +364,6 @@ contract PatcherTest is DSTest { false ); - assertTrue(success); assertEq(target.lastValue(), value1 + value2); } @@ -359,7 +384,7 @@ contract PatcherTest is DSTest { valueSource.getValue.selector ); - (bool success, ) = patcher.executeWithDynamicPatches( + patcher.executeWithDynamicPatches( address(valueSource), valueGetter, address(target), @@ -369,7 +394,6 @@ contract PatcherTest is DSTest { true // delegatecall ); - assertTrue(success); // Note: In delegatecall, the target's storage won't be modified // but the call should still succeed } @@ -487,7 +511,15 @@ contract PatcherTest is DSTest { holder ); - (bool success, ) = patcher.executeWithDynamicPatches( + // Expect the CallReceived event to be emitted with the patched balance + vm.expectEmit(true, true, true, true, address(target)); + emit CallReceived( + balance + block.timestamp + 1 hours, + address(patcher), + 0 + ); + + patcher.executeWithDynamicPatches( address(token), valueGetter, address(target), @@ -497,7 +529,6 @@ contract PatcherTest is DSTest { false ); - assertTrue(success); assertEq(target.lastValue(), balance + block.timestamp + 1 hours); } @@ -552,7 +583,11 @@ contract PatcherTest is DSTest { valueSource.getValue.selector ); - (bool success, ) = patcher.executeWithDynamicPatches( + // Expect the CallReceived event to be emitted with the original value (no patching) + vm.expectEmit(true, true, true, true, address(target)); + emit CallReceived(99999, address(patcher), 0); + + patcher.executeWithDynamicPatches( address(valueSource), valueGetter, address(target), @@ -562,7 +597,6 @@ contract PatcherTest is DSTest { false ); - assertTrue(success); assertEq(target.lastValue(), 99999); // Original value should be preserved } @@ -598,7 +632,11 @@ contract PatcherTest is DSTest { offsetGroups[1] = new uint256[](1); offsetGroups[1][0] = 4; // Same offset (should overwrite) - (bool success, ) = patcher.executeWithMultiplePatches( + // Expect the CallReceived event to be emitted with the last written value + vm.expectEmit(true, true, true, true, address(target)); + emit CallReceived(value2, address(patcher), 0); + + patcher.executeWithMultiplePatches( valueSources, valueGetters, address(target), @@ -608,7 +646,6 @@ contract PatcherTest is DSTest { false ); - assertTrue(success); assertEq(target.lastValue(), value2); // Should have the last written value } @@ -629,7 +666,11 @@ contract PatcherTest is DSTest { valueSource.getValue.selector ); - (bool success, ) = patcher.executeWithDynamicPatches( + // Expect the CallReceived event to be emitted with the zero value + vm.expectEmit(true, true, true, true, address(target)); + emit CallReceived(0, address(patcher), 0); + + patcher.executeWithDynamicPatches( address(valueSource), valueGetter, address(target), @@ -639,7 +680,6 @@ contract PatcherTest is DSTest { false ); - assertTrue(success); assertEq(target.lastValue(), 0); } @@ -660,7 +700,11 @@ contract PatcherTest is DSTest { valueSource.getValue.selector ); - (bool success, ) = patcher.executeWithDynamicPatches( + // Expect the CallReceived event to be emitted with the max value + vm.expectEmit(true, true, true, true, address(target)); + emit CallReceived(type(uint256).max, address(patcher), 0); + + patcher.executeWithDynamicPatches( address(valueSource), valueGetter, address(target), @@ -670,7 +714,6 @@ contract PatcherTest is DSTest { false ); - assertTrue(success); assertEq(target.lastValue(), type(uint256).max); } @@ -700,9 +743,24 @@ contract PatcherTest is DSTest { requestId: bytes32("test-request-id"), nonEVMReceiver: bytes32(0), receivingAssetId: bytes32(uint256(uint160(address(0xDEF)))), - signature: hex"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef01" + signature: "" }); + // Sign the RelayData + relayData.signature = signData(bridgeData, relayData); + + // Set up token balance and approval for the Patcher + uint256 bridgeAmount = 1000 ether; + uint256 expectedMinAmount = (bridgeAmount * (10000 - slippageBps)) / + 10000; // 970 ether + + // Mint tokens to the Patcher contract + token.mint(address(patcher), expectedMinAmount); + + // Approve the RelayFacet to spend tokens from the Patcher + vm.prank(address(patcher)); + token.approve(address(relayFacet), expectedMinAmount); + // Encode the RelayFacet call with placeholder minAmount bytes memory originalCalldata = abi.encodeWithSelector( relayFacet.startBridgeTokensViaRelay.selector, @@ -715,7 +773,6 @@ contract PatcherTest is DSTest { offsets[0] = 260; // Prepare oracle call to calculate minAmount with slippage - uint256 bridgeAmount = 1000 ether; bytes memory valueGetter = abi.encodeWithSelector( priceOracle.calculateMinAmount.selector, address(token), @@ -723,7 +780,14 @@ contract PatcherTest is DSTest { slippageBps ); - (bool success, ) = patcher.executeWithDynamicPatches( + // Expect the LiFiTransferStarted event to be emitted + ILiFi.BridgeData memory expectedBridgeData = bridgeData; + expectedBridgeData.minAmount = expectedMinAmount; // Use the already calculated value + + vm.expectEmit(true, true, true, true, address(relayFacet)); + emit LiFiTransferStarted(expectedBridgeData); + + patcher.executeWithDynamicPatches( address(priceOracle), valueGetter, address(relayFacet), @@ -733,90 +797,14 @@ contract PatcherTest is DSTest { false ); - assertTrue(success); - // The fact that the call succeeded means the patching worked correctly // We can't verify the exact minAmount since the real RelayFacet doesn't store state } - // Test BridgeData patching with multiple dynamic values using RelayFacet - function testExecuteWithMultiplePatches_RelayFacetMultipleFields() public { - // Set up two different price oracles for different calculations - MockPriceOracle priceOracle2 = new MockPriceOracle(); - priceOracle.setPrice(address(token), 2000 * 1e18); - priceOracle2.setPrice(address(token), 1800 * 1e18); // Different price for comparison - - ILiFi.BridgeData memory bridgeData = ILiFi.BridgeData({ - transactionId: bytes32("multi-patch-tx"), - bridge: "relay", - integrator: "TestIntegrator", - referrer: address(0x1234), - sendingAssetId: address(token), - receiver: address(0x5678), - minAmount: 0, // Will be patched with first oracle - destinationChainId: 0, // Will be patched with second oracle result - hasSourceSwaps: false, - hasDestinationCall: false - }); - - RelayFacet.RelayData memory relayData = RelayFacet.RelayData({ - requestId: bytes32("multi-patch-request"), - nonEVMReceiver: bytes32(0), - receivingAssetId: bytes32(uint256(uint160(address(0xDEF)))), - signature: hex"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef01" - }); - - bytes memory originalCalldata = abi.encodeWithSelector( - relayFacet.startBridgeTokensViaRelay.selector, - bridgeData, - relayData - ); - - // Set up multiple patches - address[] memory valueSources = new address[](2); - valueSources[0] = address(priceOracle); - valueSources[1] = address(priceOracle2); - - bytes[] memory valueGetters = new bytes[](2); - valueGetters[0] = abi.encodeWithSelector( - priceOracle.calculateMinAmount.selector, - address(token), - 1000 ether, - 300 // 3% slippage - ); - valueGetters[1] = abi.encodeWithSelector( - priceOracle2.getPrice.selector, - address(token) - ); - - uint256[][] memory offsetGroups = new uint256[][](2); - offsetGroups[0] = new uint256[](1); - offsetGroups[0][0] = 260; // minAmount offset - offsetGroups[1] = new uint256[](1); - offsetGroups[1][0] = 292; // destinationChainId offset (minAmount + 32) - - (bool success, ) = patcher.executeWithMultiplePatches( - valueSources, - valueGetters, - address(relayFacet), - 0, - originalCalldata, - offsetGroups, - false - ); - - assertTrue(success); - - // The fact that the call succeeded means the patching worked correctly - // We can't verify the exact values since our TestRelayFacet doesn't store state - } - // Test BridgeData patching with token balance as minAmount using RelayFacet function testExecuteWithDynamicPatches_RelayFacetTokenBalance() public { // Set up a user with token balance - address user = address(0x9999); - uint256 userBalance = 500 ether; - token.mint(user, userBalance); + uint256 tokenBalance = 500 ether; ILiFi.BridgeData memory bridgeData = ILiFi.BridgeData({ transactionId: bytes32("balance-patch-tx"), @@ -824,7 +812,7 @@ contract PatcherTest is DSTest { integrator: "TestIntegrator", referrer: address(0x1234), sendingAssetId: address(token), - receiver: user, + receiver: address(1337), minAmount: 0, // Will be patched with user's balance destinationChainId: 8453, // Base hasSourceSwaps: false, @@ -835,9 +823,19 @@ contract PatcherTest is DSTest { requestId: bytes32("balance-patch-request"), nonEVMReceiver: bytes32(0), receivingAssetId: bytes32(uint256(uint160(address(0xDEF)))), - signature: hex"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef01" + signature: "" }); + // Sign the RelayData + relayData.signature = signData(bridgeData, relayData); + + // Set up token balance and approval for the Patcher + token.mint(address(patcher), tokenBalance); + + // Approve the RelayFacet to spend tokens from the Patcher + vm.prank(address(patcher)); + token.approve(address(relayFacet), tokenBalance); + bytes memory originalCalldata = abi.encodeWithSelector( relayFacet.startBridgeTokensViaRelay.selector, bridgeData, @@ -850,10 +848,17 @@ contract PatcherTest is DSTest { // Use token.balanceOf to get dynamic value bytes memory valueGetter = abi.encodeWithSelector( token.balanceOf.selector, - user + patcher ); - (bool success, ) = patcher.executeWithDynamicPatches( + // Expect the LiFiTransferStarted event to be emitted + ILiFi.BridgeData memory expectedBridgeData = bridgeData; + expectedBridgeData.minAmount = tokenBalance; + + vm.expectEmit(true, true, true, true, address(relayFacet)); + emit LiFiTransferStarted(expectedBridgeData); + + patcher.executeWithDynamicPatches( address(token), valueGetter, address(relayFacet), @@ -863,13 +868,9 @@ contract PatcherTest is DSTest { false ); - assertTrue(success); // The fact that the call succeeded means the patching worked correctly } - // Test BridgeData patching with swap scenario using RelayFacet (not applicable since RelayFacet doesn't support swaps) - // Removed this test as RelayFacet doesn't have swapAndStartBridgeTokensViaRelay - // Test error handling when oracle fails during BridgeData patching with RelayFacet function testExecuteWithDynamicPatches_RelayFacetOracleFailure() public { priceOracle.setShouldFail(true); @@ -891,9 +892,12 @@ contract PatcherTest is DSTest { requestId: bytes32("fail-request"), nonEVMReceiver: bytes32(0), receivingAssetId: bytes32(uint256(uint160(address(0xDEF)))), - signature: hex"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef01" + signature: "" }); + // Sign the RelayData + relayData.signature = signData(bridgeData, relayData); + bytes memory originalCalldata = abi.encodeWithSelector( relayFacet.startBridgeTokensViaRelay.selector, bridgeData, @@ -922,146 +926,46 @@ contract PatcherTest is DSTest { ); } - // Test with real RelayFacet to find correct offset for startBridgeTokensViaRelay - function testExecuteWithDynamicPatches_RealRelayFacet() public { - // Set up a user with token balance - address user = address(0x9999); - uint256 userBalance = 500 ether; - token.mint(user, userBalance); - - // Create BridgeData with placeholder minAmount - ILiFi.BridgeData memory bridgeData = ILiFi.BridgeData({ - transactionId: bytes32("relay-patch-tx"), - bridge: "relay", - integrator: "TestIntegrator", - referrer: address(0x1234), - sendingAssetId: address(token), - receiver: user, - minAmount: 0, // Will be patched with user's balance - destinationChainId: 8453, // Base - hasSourceSwaps: false, - hasDestinationCall: false - }); - - // Create RelayData with mock signature - RelayFacet.RelayData memory relayData = RelayFacet.RelayData({ - requestId: bytes32("test-request-id"), - nonEVMReceiver: bytes32(0), - receivingAssetId: bytes32(uint256(uint160(address(0xDEF)))), - signature: hex"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef01" // 65 bytes mock signature - }); - - // Encode the RelayFacet call - bytes memory originalCalldata = abi.encodeWithSelector( - relayFacet.startBridgeTokensViaRelay.selector, - bridgeData, - relayData - ); - - // Test different offsets to find the correct one - uint256[] memory testOffsets = new uint256[](5); - testOffsets[0] = 228; // For single parameter functions - testOffsets[1] = 260; // Our calculated offset - testOffsets[2] = 292; // Alternative calculation - testOffsets[3] = 324; // Another possibility - testOffsets[4] = 356; // Yet another possibility - - bytes memory valueGetter = abi.encodeWithSelector( - token.balanceOf.selector, - user - ); - - // Try each offset and see which one works - for (uint256 i = 0; i < testOffsets.length; i++) { - uint256[] memory offsets = new uint256[](1); - offsets[0] = testOffsets[i]; - - try - patcher.executeWithDynamicPatches( - address(token), - valueGetter, - address(relayFacet), - 0, - originalCalldata, - offsets, - false + // Helper function to sign RelayData + function signData( + ILiFi.BridgeData memory _bridgeData, + RelayFacet.RelayData memory _relayData + ) internal view returns (bytes memory) { + bytes32 message = keccak256( + abi.encodePacked( + "\x19Ethereum Signed Message:\n32", + keccak256( + abi.encodePacked( + _relayData.requestId, + block.chainid, + bytes32(uint256(uint160(address(relayFacet)))), + bytes32(uint256(uint160(_bridgeData.sendingAssetId))), + _getMappedChainId(_bridgeData.destinationChainId), + _bridgeData.receiver == LibAsset.NON_EVM_ADDRESS + ? _relayData.nonEVMReceiver + : bytes32(uint256(uint160(_bridgeData.receiver))), + _relayData.receivingAssetId + ) ) - returns (bool success, bytes memory) { - if (success) { - // If successful, let's verify the minAmount was actually patched - // by decoding the calldata and checking the minAmount field - emit log_named_uint( - "Successful offset found", - testOffsets[i] - ); - - // For now, we'll just mark this as the working offset - // In a real scenario, we'd verify the minAmount was correctly set - assertTrue(success); - return; // Exit on first success - } - } catch { - // This offset didn't work, continue to next - emit log_named_uint("Failed offset", testOffsets[i]); - } - } + ) + ); - // If we get here, none of the offsets worked - assertTrue(false, "No working offset found"); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, message); + bytes memory signature = abi.encodePacked(r, s, v); + return signature; } - // Helper test to find the exact offset by examining calldata structure - function testFindRelayFacetMinAmountOffset() public { - // Create BridgeData with a marker value for minAmount - uint256 markerValue = 0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF; - - ILiFi.BridgeData memory bridgeData = ILiFi.BridgeData({ - transactionId: bytes32("test-tx-id"), - bridge: "relay", - integrator: "TestIntegrator", - referrer: address(0x1234), - sendingAssetId: address(token), - receiver: address(0x5678), - minAmount: markerValue, // Marker to find in calldata - destinationChainId: 8453, - hasSourceSwaps: false, - hasDestinationCall: false - }); - - // Create RelayData - RelayFacet.RelayData memory relayData = RelayFacet.RelayData({ - requestId: bytes32("test-request-id"), - nonEVMReceiver: bytes32(0), - receivingAssetId: bytes32(uint256(uint160(address(0xDEF)))), - signature: hex"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef01" - }); - - // Encode the calldata - bytes memory calldata_ = abi.encodeWithSelector( - relayFacet.startBridgeTokensViaRelay.selector, - bridgeData, - relayData - ); + function _getMappedChainId( + uint256 chainId + ) internal pure returns (uint256) { + if (chainId == 20000000000001) { + return 8253038; + } - emit log_named_bytes("Full calldata", calldata_); - emit log_named_uint("Calldata length", calldata_.length); - - // Find the marker value in the calldata - bytes32 marker = bytes32(markerValue); - bool found = false; - - for (uint256 i = 0; i <= calldata_.length - 32; i++) { - bytes32 chunk; - assembly { - chunk := mload(add(add(calldata_, 0x20), i)) - } - if (chunk == marker) { - emit log_named_uint("Found minAmount marker at offset", i); - found = true; - break; - } + if (chainId == 1151111081099710) { + return 792703809; } - assertTrue(found, "Marker not found in calldata"); + return chainId; } } From e3912cc662597a79f60edc6e94667688298e9081 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Mon, 26 May 2025 16:39:17 +0300 Subject: [PATCH 20/46] update tests --- test/solidity/Periphery/Patcher.t.sol | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test/solidity/Periphery/Patcher.t.sol b/test/solidity/Periphery/Patcher.t.sol index 058db0b1b..eec38c957 100644 --- a/test/solidity/Periphery/Patcher.t.sol +++ b/test/solidity/Periphery/Patcher.t.sol @@ -761,6 +761,9 @@ contract PatcherTest is DSTest { vm.prank(address(patcher)); token.approve(address(relayFacet), expectedMinAmount); + // Check relaySolver balance before + uint256 relaySolverBalanceBefore = token.balanceOf(relaySolver); + // Encode the RelayFacet call with placeholder minAmount bytes memory originalCalldata = abi.encodeWithSelector( relayFacet.startBridgeTokensViaRelay.selector, @@ -797,6 +800,13 @@ contract PatcherTest is DSTest { false ); + // Check relaySolver balance after + uint256 relaySolverBalanceAfter = token.balanceOf(relaySolver); + assertEq( + relaySolverBalanceAfter, + relaySolverBalanceBefore + expectedMinAmount + ); + // The fact that the call succeeded means the patching worked correctly // We can't verify the exact minAmount since the real RelayFacet doesn't store state } @@ -836,6 +846,9 @@ contract PatcherTest is DSTest { vm.prank(address(patcher)); token.approve(address(relayFacet), tokenBalance); + // Check relaySolver balance before + uint256 relaySolverBalanceBefore = token.balanceOf(relaySolver); + bytes memory originalCalldata = abi.encodeWithSelector( relayFacet.startBridgeTokensViaRelay.selector, bridgeData, @@ -868,6 +881,13 @@ contract PatcherTest is DSTest { false ); + // Check relaySolver balance after + uint256 relaySolverBalanceAfter = token.balanceOf(relaySolver); + assertEq( + relaySolverBalanceAfter, + relaySolverBalanceBefore + tokenBalance + ); + // The fact that the call succeeded means the patching worked correctly } @@ -898,6 +918,9 @@ contract PatcherTest is DSTest { // Sign the RelayData relayData.signature = signData(bridgeData, relayData); + // Check relaySolver balance before (should remain unchanged due to failure) + uint256 relaySolverBalanceBefore = token.balanceOf(relaySolver); + bytes memory originalCalldata = abi.encodeWithSelector( relayFacet.startBridgeTokensViaRelay.selector, bridgeData, @@ -924,6 +947,10 @@ contract PatcherTest is DSTest { offsets, false ); + + // Check relaySolver balance after (should be unchanged) + uint256 relaySolverBalanceAfter = token.balanceOf(relaySolver); + assertEq(relaySolverBalanceAfter, relaySolverBalanceBefore); } // Helper function to sign RelayData From 801e1811ac6fae0e030ccefe4d787d112458f6f6 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Mon, 26 May 2025 17:31:26 +0300 Subject: [PATCH 21/46] add approval --- script/demoScripts/demoPatcher.ts | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/script/demoScripts/demoPatcher.ts b/script/demoScripts/demoPatcher.ts index e0c00b5ce..aa97b9594 100644 --- a/script/demoScripts/demoPatcher.ts +++ b/script/demoScripts/demoPatcher.ts @@ -139,7 +139,7 @@ async function setupCowShedPostHooks( const bridgeData = { transactionId: `0x${randomBytes(32).toString('hex')}` as `0x${string}`, bridge: 'relay', - integrator: 'lifi-demo', + integrator: 'TestIntegrator', referrer: '0x0000000000000000000000000000000000000000' as `0x${string}`, sendingAssetId: usdcAddress as `0x${string}`, receiver: signerAddress as `0x${string}`, @@ -320,6 +320,8 @@ async function setupCowShedPostHooks( // - Then the actual BridgeData struct starts at offset 68 (4 + 64) // - Within BridgeData: transactionId(32) + bridge(32) + integrator(32) + referrer(32) + sendingAssetId(32) + receiver(32) + minAmount(32) // - So minAmount is at: 68 + 32*6 = 68 + 192 = 260 + // Based on the corrected calldata analysis with proper struct order: + // minAmount is at offset 260 const minAmountOffset = 260n consola.info(`Using calculated minAmount offset: ${minAmountOffset} bytes`) @@ -348,18 +350,39 @@ async function setupCowShedPostHooks( 0n, // value - no ETH being sent relayCalldata, // data - the encoded RelayFacet call [minAmountOffset], // offsets - Array with position of minAmount in the calldata - true, // delegateCall + false, // delegateCall ], }) - // Define the post-swap call to the patcher + // Encode the USDC approval call for the DIAMOND address + const approvalCalldata = encodeFunctionData({ + abi: parseAbi([ + 'function approve(address spender, uint256 amount) returns (bool)', + ]), + functionName: 'approve', + args: [ + LIFI_DIAMOND_ARBITRUM as `0x${string}`, // spender - LiFi Diamond + BigInt( + '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' + ), // Max uint256 approval + ], + }) + + // Define the post-swap calls: first approval, then bridge const postSwapCalls = [ + { + target: ARBITRUM_USDC as `0x${string}`, + callData: approvalCalldata, + value: 0n, + allowFailure: false, + isDelegateCall: true, + }, { target: PATCHER_ARBITRUM as `0x${string}`, callData: patcherCalldata, value: 0n, allowFailure: false, - isDelegateCall: false, + isDelegateCall: true, }, ] From 3c12423af6652589a546d02f15c45bbdaa89fc76 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Mon, 26 May 2025 17:51:55 +0300 Subject: [PATCH 22/46] fix --- script/demoScripts/demoPatcher.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/script/demoScripts/demoPatcher.ts b/script/demoScripts/demoPatcher.ts index aa97b9594..ad5dd3f28 100644 --- a/script/demoScripts/demoPatcher.ts +++ b/script/demoScripts/demoPatcher.ts @@ -143,8 +143,8 @@ async function setupCowShedPostHooks( referrer: '0x0000000000000000000000000000000000000000' as `0x${string}`, sendingAssetId: usdcAddress as `0x${string}`, receiver: signerAddress as `0x${string}`, - destinationChainId: 8453n, // BASE chain ID minAmount: receivedAmount, + destinationChainId: 8453n, // BASE chain ID hasSourceSwaps: false, hasDestinationCall: false, } @@ -281,7 +281,7 @@ async function setupCowShedPostHooks( // Encode the RelayFacet call const relayFacetAbi = parseAbi([ - 'function startBridgeTokensViaRelay((bytes32 transactionId, string bridge, string integrator, address referrer, address sendingAssetId, address receiver, uint256 destinationChainId, uint256 minAmount, bool hasSourceSwaps, bool hasDestinationCall) _bridgeData, (bytes32 requestId, bytes32 nonEVMReceiver, bytes32 receivingAssetId, bytes signature) _relayData) payable', + 'function startBridgeTokensViaRelay((bytes32 transactionId, string bridge, string integrator, address referrer, address sendingAssetId, address receiver, uint256 minAmount, uint256 destinationChainId, bool hasSourceSwaps, bool hasDestinationCall) _bridgeData, (bytes32 requestId, bytes32 nonEVMReceiver, bytes32 receivingAssetId, bytes signature) _relayData) payable', ]) // Create the bridge data with proper types @@ -375,7 +375,7 @@ async function setupCowShedPostHooks( callData: approvalCalldata, value: 0n, allowFailure: false, - isDelegateCall: true, + isDelegateCall: false, }, { target: PATCHER_ARBITRUM as `0x${string}`, From f9fe5bae20a421f2689eb6afd191bc9be56a80eb Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Tue, 27 May 2025 13:01:17 +0300 Subject: [PATCH 23/46] refactor --- script/demoScripts/demoPatcher.ts | 450 +-------------------- script/demoScripts/utils/cowSwapHelpers.ts | 450 +++++++++++++++++++++ 2 files changed, 462 insertions(+), 438 deletions(-) create mode 100644 script/demoScripts/utils/cowSwapHelpers.ts diff --git a/script/demoScripts/demoPatcher.ts b/script/demoScripts/demoPatcher.ts index ad5dd3f28..c517c1bdc 100644 --- a/script/demoScripts/demoPatcher.ts +++ b/script/demoScripts/demoPatcher.ts @@ -1,26 +1,14 @@ #!/usr/bin/env bun -import { - parseAbi, - parseUnits, - encodeFunctionData, - createWalletClient, - http, - getContract, - Hex, - recoverMessageAddress, - keccak256, - encodePacked, - pad, -} from 'viem' +import { parseUnits, createWalletClient, http, getContract, Hex } from 'viem' import { privateKeyToAccount } from 'viem/accounts' import { arbitrum } from 'viem/chains' -import { randomBytes } from 'crypto' import { ethers } from 'ethers' import { SupportedChainId, OrderKind, TradingSdk } from '@cowprotocol/cow-sdk' import { defineCommand, runMain } from 'citty' import { consola } from 'consola' import erc20Artifact from '../../out/ERC20/ERC20.sol/ERC20.json' +import { setupCowShedPostHooks } from './utils/cowSwapHelpers' const ARBITRUM_WETH = '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1' const ARBITRUM_USDC = '0xaf88d065e77c8cC2239327C5EDb3A432268e5831' @@ -28,429 +16,9 @@ const BASE_USDC = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' import arbitrumDeployments from '../../deployments/arbitrum.staging.json' const LIFI_DIAMOND_ARBITRUM = arbitrumDeployments.LiFiDiamond const PATCHER_ARBITRUM = arbitrumDeployments.Patcher -import { COW_SHED_FACTORY, COW_SHED_IMPLEMENTATION } from '@cowprotocol/cow-sdk' const VAULT_RELAYER_ARBITRUM = '0xC92E8bdf79f0507f65a392b0ab4667716BFE0110' -const PROXY_CREATION_CODE = - '0x60a034608e57601f61037138819003918201601f19168301916001600160401b038311848410176093578084926040948552833981010312608e57604b602060458360a9565b920160a9565b6080527f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc556040516102b490816100bd8239608051818181608f01526101720152f35b600080fd5b634e487b7160e01b600052604160045260246000fd5b51906001600160a01b0382168203608e5756fe60806040526004361015610018575b3661019457610194565b6000803560e01c908163025b22bc1461003b575063f851a4400361000e5761010d565b3461010a5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261010a5773ffffffffffffffffffffffffffffffffffffffff60043581811691828203610106577f0000000000000000000000000000000000000000000000000000000000000000163314600014610101577f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc557fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b8280a280f35b61023d565b8380fd5b80fd5b346101645760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610164576020610146610169565b73ffffffffffffffffffffffffffffffffffffffff60405191168152f35b600080fd5b333003610101577f000000000000000000000000000000000000000000000000000000000000000090565b60ff7f68df44b1011761f481358c0f49a711192727fb02c377d697bcb0ea8ff8393ac0541615806101ef575b1561023d5760046040517ff92ee8a9000000000000000000000000000000000000000000000000000000008152fd5b507f400ada75000000000000000000000000000000000000000000000000000000007fffffffff000000000000000000000000000000000000000000000000000000006000351614156101c0565b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc546000808092368280378136915af43d82803e1561027a573d90f35b3d90fdfea2646970667358221220c7c26ff3040b96a28e96d6d27b743972943aeaef81cc821544c5fe1e24f9b17264736f6c63430008190033' const ERC20_ABI = erc20Artifact.abi -class CowShedSdk { - factoryAddress: `0x${string}` - implementationAddress: `0x${string}` - chainId: number - - constructor({ - factoryAddress, - implementationAddress, - chainId, - }: { - factoryAddress: string - implementationAddress: string - chainId: number - }) { - this.factoryAddress = factoryAddress as `0x${string}` - this.implementationAddress = implementationAddress as `0x${string}` - this.chainId = chainId - } - - // Compute the deterministic proxy address for a user - computeProxyAddress(owner: string): `0x${string}` { - const salt = ethers.utils.defaultAbiCoder.encode(['address'], [owner]) - const initCodeHash = ethers.utils.solidityKeccak256( - ['bytes', 'bytes'], - [ - PROXY_CREATION_CODE, - ethers.utils.defaultAbiCoder.encode( - ['address', 'address'], - [this.implementationAddress, owner] - ), - ] - ) - return ethers.utils.getCreate2Address( - this.factoryAddress, - salt, - initCodeHash - ) as `0x${string}` - } - - // Encode the executeHooks call for the factory - static encodeExecuteHooksForFactory( - calls: any[], - nonce: `0x${string}`, - deadline: bigint, - owner: `0x${string}`, - signature: `0x${string}` - ): string { - const cowShedFactoryAbi = parseAbi([ - 'function executeHooks((address target, uint256 value, bytes callData, bool allowFailure, bool isDelegateCall)[] calls, bytes32 nonce, uint256 deadline, address user, bytes signature) returns (address proxy)', - ]) - - return encodeFunctionData({ - abi: cowShedFactoryAbi, - functionName: 'executeHooks', - args: [ - calls.map((call) => ({ - target: call.target as `0x${string}`, - value: call.value, - callData: call.callData, - allowFailure: call.allowFailure, - isDelegateCall: call.isDelegateCall, - })), - nonce, - deadline, - owner, - signature, - ], - }) - } -} - -/** - * Setup CowShed post hooks for bridging USDC to BASE using Relay - */ -async function setupCowShedPostHooks( - chainId: number, - walletClient: any, - usdcAddress: string, - receivedAmount: bigint -) { - const account = walletClient.account - const signerAddress = account.address - - const shedSDK = new CowShedSdk({ - factoryAddress: COW_SHED_FACTORY, - implementationAddress: COW_SHED_IMPLEMENTATION, - chainId, - }) - - // Generate a random nonce - const nonce = `0x${Array.from({ length: 64 }, () => - Math.floor(Math.random() * 16).toString(16) - ).join('')}` as `0x${string}` - - // Set a deadline 24 hours from now - const deadline = BigInt(Math.floor(Date.now() / 1000) + 24 * 60 * 60) - - // Get the proxy address - const shedDeterministicAddress = shedSDK.computeProxyAddress(signerAddress) - consola.info(`CowShed proxy address: ${shedDeterministicAddress}`) - - // Create the bridge data for LiFi - const bridgeData = { - transactionId: `0x${randomBytes(32).toString('hex')}` as `0x${string}`, - bridge: 'relay', - integrator: 'TestIntegrator', - referrer: '0x0000000000000000000000000000000000000000' as `0x${string}`, - sendingAssetId: usdcAddress as `0x${string}`, - receiver: signerAddress as `0x${string}`, - minAmount: receivedAmount, - destinationChainId: 8453n, // BASE chain ID - hasSourceSwaps: false, - hasDestinationCall: false, - } - - // Create RelayData - // First, create a quote request with a realistic amount - const estimatedUsdcAmount = '1000000' // 1 USDC (6 decimals) - - // Get a real signature from the Relay API - // First, create a quote request - // Use LiFi Diamond as the user since it will be the final caller of RelayFacet - const quoteParams = { - user: LIFI_DIAMOND_ARBITRUM, // LiFi Diamond will be address(this) in RelayFacet - originChainId: 42161, // Arbitrum - destinationChainId: 8453, // BASE - originCurrency: ARBITRUM_USDC, - destinationCurrency: BASE_USDC, - recipient: signerAddress, - tradeType: 'EXACT_INPUT', - amount: estimatedUsdcAmount, // Use a realistic amount instead of 0 - referrer: 'lifi-demo', - useExternalLiquidity: false, - } - - // Fetch the quote from the Relay API - consola.info('Fetching quote from Relay API...') - const quoteResponse = await fetch('https://api.relay.link/quote', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(quoteParams), - }) - - if (!quoteResponse.ok) { - throw new Error( - `Failed to get quote from Relay API: ${quoteResponse.statusText}` - ) - } - - const quoteData = await quoteResponse.json() - const relayRequestId = quoteData.steps[0].requestId - consola.info(`Got requestId from Relay API: ${relayRequestId}`) - - // Fetch the signature from the Relay API - consola.info('Fetching signature from Relay API...') - const signatureResponse = await fetch( - `https://api.relay.link/requests/${relayRequestId}/signature/v2`, - { headers: { 'Content-Type': 'application/json' } } - ) - - if (!signatureResponse.ok) { - throw new Error( - `Failed to get signature from Relay API: ${signatureResponse.statusText}` - ) - } - - const signatureData = await signatureResponse.json() - const relaySignature = signatureData.signature as `0x${string}` - consola.info( - `Got signature from Relay API: ${relaySignature.slice( - 0, - 10 - )}...${relaySignature.slice(-8)}` - ) - - // Log the request origin user (not the signer) - if (signatureData.requestData?.originUser) { - const originUser = signatureData.requestData.originUser - consola.info(`Request origin user: ${originUser} (LiFi Diamond)`) - } - - // Recover the actual signer using the same message format as the contract - try { - // Construct the message exactly as the contract does: - // keccak256(abi.encodePacked( - // requestId, - // block.chainid, - // bytes32(uint256(uint160(address(this)))), - // bytes32(uint256(uint160(sendingAssetId))), - // _getMappedChainId(destinationChainId), - // bytes32(uint256(uint160(receiver))), - // receivingAssetId - // )) - - // Use viem's encodePacked instead of manual concatenation - const packedData = encodePacked( - [ - 'bytes32', - 'uint256', - 'bytes32', - 'bytes32', - 'uint256', - 'bytes32', - 'bytes32', - ], - [ - relayRequestId as `0x${string}`, // requestId - 42161n, // chainId (Arbitrum) - pad(LIFI_DIAMOND_ARBITRUM as `0x${string}`), // LiFi Diamond address as bytes32 (address(this) in RelayFacet) - pad(ARBITRUM_USDC as `0x${string}`), // sendingAssetId as bytes32 - 8453n, // destinationChainId (Base) - pad(signerAddress as `0x${string}`), // receiver as bytes32 - pad(BASE_USDC as `0x${string}`), // receivingAssetId as bytes32 - ] - ) - - // Hash the packed data - const messageHash = keccak256(packedData) - - // Recover the signer using the message hash (Ethereum signed message format) - const recoveredSigner = await recoverMessageAddress({ - message: { raw: messageHash }, - signature: relaySignature, - }) - - consola.success(`Relay attestation signer: ${recoveredSigner}`) - } catch (error) { - consola.warn('Could not recover signer address:', error) - // Fallback: log full response for debugging - consola.debug( - 'Full signature response:', - JSON.stringify(signatureData, null, 2) - ) - } - - const relayData = { - requestId: relayRequestId, - nonEVMReceiver: - '0x0000000000000000000000000000000000000000000000000000000000000000' as `0x${string}`, // Not bridging to non-EVM chain - receivingAssetId: pad(BASE_USDC as `0x${string}`), // Use viem's pad instead of manual padStart - signature: relaySignature, // Real signature from the Relay API - } - - // Encode the RelayFacet call - const relayFacetAbi = parseAbi([ - 'function startBridgeTokensViaRelay((bytes32 transactionId, string bridge, string integrator, address referrer, address sendingAssetId, address receiver, uint256 minAmount, uint256 destinationChainId, bool hasSourceSwaps, bool hasDestinationCall) _bridgeData, (bytes32 requestId, bytes32 nonEVMReceiver, bytes32 receivingAssetId, bytes signature) _relayData) payable', - ]) - - // Create the bridge data with proper types - const typedBridgeData = { - transactionId: bridgeData.transactionId, - bridge: bridgeData.bridge, - integrator: bridgeData.integrator, - referrer: bridgeData.referrer, - sendingAssetId: bridgeData.sendingAssetId, - receiver: bridgeData.receiver, - destinationChainId: bridgeData.destinationChainId, - minAmount: bridgeData.minAmount, - hasSourceSwaps: bridgeData.hasSourceSwaps, - hasDestinationCall: bridgeData.hasDestinationCall, - } - - // Create the relay data with proper types - const typedRelayData = { - requestId: relayData.requestId, - nonEVMReceiver: relayData.nonEVMReceiver, - receivingAssetId: relayData.receivingAssetId, - signature: relayData.signature, - } - - const relayCalldata = encodeFunctionData({ - abi: relayFacetAbi, - functionName: 'startBridgeTokensViaRelay', - args: [typedBridgeData, typedRelayData], - }) - - // Calculate the correct offset for the minAmount field in BridgeData - // For startBridgeTokensViaRelay(BridgeData calldata _bridgeData, RelayData calldata _relayData): - // - 4 bytes: function selector - // - 32 bytes: offset to _bridgeData struct (0x40 = 64) - // - 32 bytes: offset to _relayData struct - // - Then the actual BridgeData struct starts at offset 68 (4 + 64) - // - Within BridgeData: transactionId(32) + bridge(32) + integrator(32) + referrer(32) + sendingAssetId(32) + receiver(32) + minAmount(32) - // - So minAmount is at: 68 + 32*6 = 68 + 192 = 260 - // Based on the corrected calldata analysis with proper struct order: - // minAmount is at offset 260 - const minAmountOffset = 260n - consola.info(`Using calculated minAmount offset: ${minAmountOffset} bytes`) - - // Note: This offset is specifically for startBridgeTokensViaRelay function - // Different bridge functions may have different offsets due to different parameter layouts - - // Encode the balanceOf call to get the USDC balance - const valueGetter = encodeFunctionData({ - abi: parseAbi([ - 'function balanceOf(address account) view returns (uint256)', - ]), - functionName: 'balanceOf', - args: [shedDeterministicAddress as `0x${string}`], - }) - - // Encode the patcher call - const patcherCalldata = encodeFunctionData({ - abi: parseAbi([ - 'function executeWithDynamicPatches(address valueSource, bytes valueGetter, address finalTarget, uint256 value, bytes data, uint256[] offsets, bool delegateCall) returns (bool success, bytes returnData)', - ]), - functionName: 'executeWithDynamicPatches', - args: [ - usdcAddress as `0x${string}`, // valueSource - USDC contract (single address) - valueGetter, // valueGetter - balanceOf call (single bytes) - LIFI_DIAMOND_ARBITRUM as `0x${string}`, // finalTarget - LiFiDiamond contract - 0n, // value - no ETH being sent - relayCalldata, // data - the encoded RelayFacet call - [minAmountOffset], // offsets - Array with position of minAmount in the calldata - false, // delegateCall - ], - }) - - // Encode the USDC approval call for the DIAMOND address - const approvalCalldata = encodeFunctionData({ - abi: parseAbi([ - 'function approve(address spender, uint256 amount) returns (bool)', - ]), - functionName: 'approve', - args: [ - LIFI_DIAMOND_ARBITRUM as `0x${string}`, // spender - LiFi Diamond - BigInt( - '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' - ), // Max uint256 approval - ], - }) - - // Define the post-swap calls: first approval, then bridge - const postSwapCalls = [ - { - target: ARBITRUM_USDC as `0x${string}`, - callData: approvalCalldata, - value: 0n, - allowFailure: false, - isDelegateCall: false, - }, - { - target: PATCHER_ARBITRUM as `0x${string}`, - callData: patcherCalldata, - value: 0n, - allowFailure: false, - isDelegateCall: true, - }, - ] - - // Create the typed data for the hooks - const typedData = { - account, - domain: { - name: 'COWShed', - version: '1.0.0', - chainId: BigInt(chainId), - verifyingContract: shedDeterministicAddress, - }, - types: { - ExecuteHooks: [ - { name: 'calls', type: 'Call[]' }, - { name: 'nonce', type: 'bytes32' }, - { name: 'deadline', type: 'uint256' }, - ], - Call: [ - { name: 'target', type: 'address' }, - { name: 'value', type: 'uint256' }, - { name: 'callData', type: 'bytes' }, - { name: 'allowFailure', type: 'bool' }, - { name: 'isDelegateCall', type: 'bool' }, - ], - }, - primaryType: 'ExecuteHooks', - message: { - calls: postSwapCalls.map((call) => ({ - target: call.target, - value: call.value, - callData: call.callData, - allowFailure: call.allowFailure, - isDelegateCall: call.isDelegateCall, - })), - nonce, - deadline, - }, - } - - // Sign the typed data for the hooks - const hookSignature = (await walletClient.signTypedData( - typedData - )) as `0x${string}` - - // Encode the post hooks call data - const shedEncodedPostHooksCallData = CowShedSdk.encodeExecuteHooksForFactory( - postSwapCalls, - nonce, - deadline, - signerAddress as `0x${string}`, - hookSignature - ) - - // Create the post hooks - const postHooks = [ - { - target: COW_SHED_FACTORY, - callData: shedEncodedPostHooksCallData, - gasLimit: '3000000', - }, - ] - - return { - shedDeterministicAddress, - postHooks, - } -} /** * Main function to execute the demo @@ -519,10 +87,16 @@ async function main(options: { privateKey: string; dryRun: boolean }) { // Set up CowShed post hooks const { shedDeterministicAddress, postHooks } = await setupCowShedPostHooks( - 42161, // Arbitrum chain ID - walletClient, - ARBITRUM_USDC, - parseUnits('0', 6) // This will be dynamically patched + { + chainId: 42161, // Arbitrum chain ID + walletClient, + usdcAddress: ARBITRUM_USDC, + receivedAmount: parseUnits('0', 6), // This will be dynamically patched + lifiDiamondAddress: LIFI_DIAMOND_ARBITRUM, + patcherAddress: PATCHER_ARBITRUM, + baseUsdcAddress: BASE_USDC, + destinationChainId: 8453n, // BASE chain ID + } ) // Create ethers provider and signer for CoW SDK diff --git a/script/demoScripts/utils/cowSwapHelpers.ts b/script/demoScripts/utils/cowSwapHelpers.ts new file mode 100644 index 000000000..8f909fccf --- /dev/null +++ b/script/demoScripts/utils/cowSwapHelpers.ts @@ -0,0 +1,450 @@ +import { + parseAbi, + encodeFunctionData, + recoverMessageAddress, + keccak256, + encodePacked, + pad, +} from 'viem' +import { randomBytes } from 'crypto' +import { ethers } from 'ethers' +import { COW_SHED_FACTORY, COW_SHED_IMPLEMENTATION } from '@cowprotocol/cow-sdk' +import { consola } from 'consola' + +const PROXY_CREATION_CODE = + '0x60a034608e57601f61037138819003918201601f19168301916001600160401b038311848410176093578084926040948552833981010312608e57604b602060458360a9565b920160a9565b6080527f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc556040516102b490816100bd8239608051818181608f01526101720152f35b600080fd5b634e487b7160e01b600052604160045260246000fd5b51906001600160a01b0382168203608e5756fe60806040526004361015610018575b3661019457610194565b6000803560e01c908163025b22bc1461003b575063f851a4400361000e5761010d565b3461010a5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261010a5773ffffffffffffffffffffffffffffffffffffffff60043581811691828203610106577f0000000000000000000000000000000000000000000000000000000000000000163314600014610101577f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc557fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b8280a280f35b61023d565b8380fd5b80fd5b346101645760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610164576020610146610169565b73ffffffffffffffffffffffffffffffffffffffff60405191168152f35b600080fd5b333003610101577f000000000000000000000000000000000000000000000000000000000000000090565b60ff7f68df44b1011761f481358c0f49a711192727fb02c377d697bcb0ea8ff8393ac0541615806101ef575b1561023d5760046040517ff92ee8a9000000000000000000000000000000000000000000000000000000008152fd5b507f400ada75000000000000000000000000000000000000000000000000000000007fffffffff000000000000000000000000000000000000000000000000000000006000351614156101c0565b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc546000808092368280378136915af43d82803e1561027a573d90f35b3d90fdfea2646970667358221220c7c26ff3040b96a28e96d6d27b743972943aeaef81cc821544c5fe1e24f9b17264736f6c63430008190033' + +export class CowShedSdk { + factoryAddress: `0x${string}` + implementationAddress: `0x${string}` + chainId: number + + constructor({ + factoryAddress, + implementationAddress, + chainId, + }: { + factoryAddress: string + implementationAddress: string + chainId: number + }) { + this.factoryAddress = factoryAddress as `0x${string}` + this.implementationAddress = implementationAddress as `0x${string}` + this.chainId = chainId + } + + // Compute the deterministic proxy address for a user + computeProxyAddress(owner: string): `0x${string}` { + const salt = ethers.utils.defaultAbiCoder.encode(['address'], [owner]) + const initCodeHash = ethers.utils.solidityKeccak256( + ['bytes', 'bytes'], + [ + PROXY_CREATION_CODE, + ethers.utils.defaultAbiCoder.encode( + ['address', 'address'], + [this.implementationAddress, owner] + ), + ] + ) + return ethers.utils.getCreate2Address( + this.factoryAddress, + salt, + initCodeHash + ) as `0x${string}` + } + + // Encode the executeHooks call for the factory + static encodeExecuteHooksForFactory( + calls: any[], + nonce: `0x${string}`, + deadline: bigint, + owner: `0x${string}`, + signature: `0x${string}` + ): string { + const cowShedFactoryAbi = parseAbi([ + 'function executeHooks((address target, uint256 value, bytes callData, bool allowFailure, bool isDelegateCall)[] calls, bytes32 nonce, uint256 deadline, address user, bytes signature) returns (address proxy)', + ]) + + return encodeFunctionData({ + abi: cowShedFactoryAbi, + functionName: 'executeHooks', + args: [ + calls.map((call) => ({ + target: call.target as `0x${string}`, + value: call.value, + callData: call.callData, + allowFailure: call.allowFailure, + isDelegateCall: call.isDelegateCall, + })), + nonce, + deadline, + owner, + signature, + ], + }) + } +} + +export interface CowShedPostHooksConfig { + chainId: number + walletClient: any + usdcAddress: string + receivedAmount: bigint + lifiDiamondAddress: string + patcherAddress: string + baseUsdcAddress: string + destinationChainId: bigint +} + +/** + * Setup CowShed post hooks for bridging USDC to BASE using Relay + */ +export async function setupCowShedPostHooks(config: CowShedPostHooksConfig) { + const { + chainId, + walletClient, + usdcAddress, + receivedAmount, + lifiDiamondAddress, + patcherAddress, + baseUsdcAddress, + destinationChainId, + } = config + + const account = walletClient.account + const signerAddress = account.address + + const shedSDK = new CowShedSdk({ + factoryAddress: COW_SHED_FACTORY, + implementationAddress: COW_SHED_IMPLEMENTATION, + chainId, + }) + + // Generate a random nonce + const nonce = `0x${Array.from({ length: 64 }, () => + Math.floor(Math.random() * 16).toString(16) + ).join('')}` as `0x${string}` + + // Set a deadline 24 hours from now + const deadline = BigInt(Math.floor(Date.now() / 1000) + 24 * 60 * 60) + + // Get the proxy address + const shedDeterministicAddress = shedSDK.computeProxyAddress(signerAddress) + consola.info(`CowShed proxy address: ${shedDeterministicAddress}`) + + // Create the bridge data for LiFi + const bridgeData = { + transactionId: `0x${randomBytes(32).toString('hex')}` as `0x${string}`, + bridge: 'relay', + integrator: 'TestIntegrator', + referrer: '0x0000000000000000000000000000000000000000' as `0x${string}`, + sendingAssetId: usdcAddress as `0x${string}`, + receiver: signerAddress as `0x${string}`, + minAmount: receivedAmount, + destinationChainId, + hasSourceSwaps: false, + hasDestinationCall: false, + } + + // Create RelayData + // First, create a quote request with a realistic amount + const estimatedUsdcAmount = '1000000' // 1 USDC (6 decimals) + + // Get a real signature from the Relay API + // First, create a quote request + // Use LiFi Diamond as the user since it will be the final caller of RelayFacet + const quoteParams = { + user: lifiDiamondAddress, // LiFi Diamond will be address(this) in RelayFacet + originChainId: chainId, + destinationChainId: Number(destinationChainId), + originCurrency: usdcAddress, + destinationCurrency: baseUsdcAddress, + recipient: signerAddress, + tradeType: 'EXACT_INPUT', + amount: estimatedUsdcAmount, // Use a realistic amount instead of 0 + referrer: 'lifi-demo', + useExternalLiquidity: false, + } + + // Fetch the quote from the Relay API + consola.info('Fetching quote from Relay API...') + const quoteResponse = await fetch('https://api.relay.link/quote', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(quoteParams), + }) + + if (!quoteResponse.ok) { + throw new Error( + `Failed to get quote from Relay API: ${quoteResponse.statusText}` + ) + } + + const quoteData = await quoteResponse.json() + const relayRequestId = quoteData.steps[0].requestId + consola.info(`Got requestId from Relay API: ${relayRequestId}`) + + // Fetch the signature from the Relay API + consola.info('Fetching signature from Relay API...') + const signatureResponse = await fetch( + `https://api.relay.link/requests/${relayRequestId}/signature/v2`, + { headers: { 'Content-Type': 'application/json' } } + ) + + if (!signatureResponse.ok) { + throw new Error( + `Failed to get signature from Relay API: ${signatureResponse.statusText}` + ) + } + + const signatureData = await signatureResponse.json() + const relaySignature = signatureData.signature as `0x${string}` + consola.info( + `Got signature from Relay API: ${relaySignature.slice( + 0, + 10 + )}...${relaySignature.slice(-8)}` + ) + + // Log the request origin user (not the signer) + if (signatureData.requestData?.originUser) { + const originUser = signatureData.requestData.originUser + consola.info(`Request origin user: ${originUser} (LiFi Diamond)`) + } + + // Recover the actual signer using the same message format as the contract + try { + // Construct the message exactly as the contract does: + // keccak256(abi.encodePacked( + // requestId, + // block.chainid, + // bytes32(uint256(uint160(address(this)))), + // bytes32(uint256(uint160(sendingAssetId))), + // _getMappedChainId(destinationChainId), + // bytes32(uint256(uint160(receiver))), + // receivingAssetId + // )) + + // Use viem's encodePacked instead of manual concatenation + const packedData = encodePacked( + [ + 'bytes32', + 'uint256', + 'bytes32', + 'bytes32', + 'uint256', + 'bytes32', + 'bytes32', + ], + [ + relayRequestId as `0x${string}`, // requestId + BigInt(chainId), // chainId + pad(lifiDiamondAddress as `0x${string}`), // LiFi Diamond address as bytes32 (address(this) in RelayFacet) + pad(usdcAddress as `0x${string}`), // sendingAssetId as bytes32 + destinationChainId, // destinationChainId + pad(signerAddress as `0x${string}`), // receiver as bytes32 + pad(baseUsdcAddress as `0x${string}`), // receivingAssetId as bytes32 + ] + ) + + // Hash the packed data + const messageHash = keccak256(packedData) + + // Recover the signer using the message hash (Ethereum signed message format) + const recoveredSigner = await recoverMessageAddress({ + message: { raw: messageHash }, + signature: relaySignature, + }) + + consola.success(`Relay attestation signer: ${recoveredSigner}`) + } catch (error) { + consola.warn('Could not recover signer address:', error) + // Fallback: log full response for debugging + consola.debug( + 'Full signature response:', + JSON.stringify(signatureData, null, 2) + ) + } + + const relayData = { + requestId: relayRequestId, + nonEVMReceiver: + '0x0000000000000000000000000000000000000000000000000000000000000000' as `0x${string}`, // Not bridging to non-EVM chain + receivingAssetId: pad(baseUsdcAddress as `0x${string}`), // Use viem's pad instead of manual padStart + signature: relaySignature, // Real signature from the Relay API + } + + // Encode the RelayFacet call + const relayFacetAbi = parseAbi([ + 'function startBridgeTokensViaRelay((bytes32 transactionId, string bridge, string integrator, address referrer, address sendingAssetId, address receiver, uint256 minAmount, uint256 destinationChainId, bool hasSourceSwaps, bool hasDestinationCall) _bridgeData, (bytes32 requestId, bytes32 nonEVMReceiver, bytes32 receivingAssetId, bytes signature) _relayData) payable', + ]) + + // Create the bridge data with proper types + const typedBridgeData = { + transactionId: bridgeData.transactionId, + bridge: bridgeData.bridge, + integrator: bridgeData.integrator, + referrer: bridgeData.referrer, + sendingAssetId: bridgeData.sendingAssetId, + receiver: bridgeData.receiver, + destinationChainId: bridgeData.destinationChainId, + minAmount: bridgeData.minAmount, + hasSourceSwaps: bridgeData.hasSourceSwaps, + hasDestinationCall: bridgeData.hasDestinationCall, + } + + // Create the relay data with proper types + const typedRelayData = { + requestId: relayData.requestId, + nonEVMReceiver: relayData.nonEVMReceiver, + receivingAssetId: relayData.receivingAssetId, + signature: relayData.signature, + } + + const relayCalldata = encodeFunctionData({ + abi: relayFacetAbi, + functionName: 'startBridgeTokensViaRelay', + args: [typedBridgeData, typedRelayData], + }) + + // Calculate the correct offset for the minAmount field in BridgeData + // For startBridgeTokensViaRelay(BridgeData calldata _bridgeData, RelayData calldata _relayData): + // - 4 bytes: function selector + // - 32 bytes: offset to _bridgeData struct (0x40 = 64) + // - 32 bytes: offset to _relayData struct + // - Then the actual BridgeData struct starts at offset 68 (4 + 64) + // - Within BridgeData: transactionId(32) + bridge(32) + integrator(32) + referrer(32) + sendingAssetId(32) + receiver(32) + minAmount(32) + // - So minAmount is at: 68 + 32*6 = 68 + 192 = 260 + // Based on the corrected calldata analysis with proper struct order: + // minAmount is at offset 260 + const minAmountOffset = 260n + consola.info(`Using calculated minAmount offset: ${minAmountOffset} bytes`) + + // Note: This offset is specifically for startBridgeTokensViaRelay function + // Different bridge functions may have different offsets due to different parameter layouts + + // Encode the balanceOf call to get the USDC balance + const valueGetter = encodeFunctionData({ + abi: parseAbi([ + 'function balanceOf(address account) view returns (uint256)', + ]), + functionName: 'balanceOf', + args: [shedDeterministicAddress as `0x${string}`], + }) + + // Encode the patcher call + const patcherCalldata = encodeFunctionData({ + abi: parseAbi([ + 'function executeWithDynamicPatches(address valueSource, bytes valueGetter, address finalTarget, uint256 value, bytes data, uint256[] offsets, bool delegateCall) returns (bool success, bytes returnData)', + ]), + functionName: 'executeWithDynamicPatches', + args: [ + usdcAddress as `0x${string}`, // valueSource - USDC contract (single address) + valueGetter, // valueGetter - balanceOf call (single bytes) + lifiDiamondAddress as `0x${string}`, // finalTarget - LiFiDiamond contract + 0n, // value - no ETH being sent + relayCalldata, // data - the encoded RelayFacet call + [minAmountOffset], // offsets - Array with position of minAmount in the calldata + false, // delegateCall + ], + }) + + // Encode the USDC approval call for the DIAMOND address + const approvalCalldata = encodeFunctionData({ + abi: parseAbi([ + 'function approve(address spender, uint256 amount) returns (bool)', + ]), + functionName: 'approve', + args: [ + lifiDiamondAddress as `0x${string}`, // spender - LiFi Diamond + BigInt( + '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' + ), // Max uint256 approval + ], + }) + + // Define the post-swap calls: first approval, then bridge + const postSwapCalls = [ + { + target: usdcAddress as `0x${string}`, + callData: approvalCalldata, + value: 0n, + allowFailure: false, + isDelegateCall: false, + }, + { + target: patcherAddress as `0x${string}`, + callData: patcherCalldata, + value: 0n, + allowFailure: false, + isDelegateCall: true, + }, + ] + + // Create the typed data for the hooks + const typedData = { + account, + domain: { + name: 'COWShed', + version: '1.0.0', + chainId: BigInt(chainId), + verifyingContract: shedDeterministicAddress, + }, + types: { + ExecuteHooks: [ + { name: 'calls', type: 'Call[]' }, + { name: 'nonce', type: 'bytes32' }, + { name: 'deadline', type: 'uint256' }, + ], + Call: [ + { name: 'target', type: 'address' }, + { name: 'value', type: 'uint256' }, + { name: 'callData', type: 'bytes' }, + { name: 'allowFailure', type: 'bool' }, + { name: 'isDelegateCall', type: 'bool' }, + ], + }, + primaryType: 'ExecuteHooks', + message: { + calls: postSwapCalls.map((call) => ({ + target: call.target, + value: call.value, + callData: call.callData, + allowFailure: call.allowFailure, + isDelegateCall: call.isDelegateCall, + })), + nonce, + deadline, + }, + } + + // Sign the typed data for the hooks + const hookSignature = (await walletClient.signTypedData( + typedData + )) as `0x${string}` + + // Encode the post hooks call data + const shedEncodedPostHooksCallData = CowShedSdk.encodeExecuteHooksForFactory( + postSwapCalls, + nonce, + deadline, + signerAddress as `0x${string}`, + hookSignature + ) + + // Create the post hooks + const postHooks = [ + { + target: COW_SHED_FACTORY, + callData: shedEncodedPostHooksCallData, + gasLimit: '3000000', + }, + ] + + return { + shedDeterministicAddress, + postHooks, + } +} From 27f2fb53d3e74ed8c457654801ca4784d35f7393 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Tue, 27 May 2025 13:06:18 +0300 Subject: [PATCH 24/46] more refactor --- script/demoScripts/utils/cowSwapHelpers.ts | 134 +++++++++------------ 1 file changed, 60 insertions(+), 74 deletions(-) diff --git a/script/demoScripts/utils/cowSwapHelpers.ts b/script/demoScripts/utils/cowSwapHelpers.ts index 8f909fccf..b0298a08a 100644 --- a/script/demoScripts/utils/cowSwapHelpers.ts +++ b/script/demoScripts/utils/cowSwapHelpers.ts @@ -14,75 +14,63 @@ import { consola } from 'consola' const PROXY_CREATION_CODE = '0x60a034608e57601f61037138819003918201601f19168301916001600160401b038311848410176093578084926040948552833981010312608e57604b602060458360a9565b920160a9565b6080527f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc556040516102b490816100bd8239608051818181608f01526101720152f35b600080fd5b634e487b7160e01b600052604160045260246000fd5b51906001600160a01b0382168203608e5756fe60806040526004361015610018575b3661019457610194565b6000803560e01c908163025b22bc1461003b575063f851a4400361000e5761010d565b3461010a5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261010a5773ffffffffffffffffffffffffffffffffffffffff60043581811691828203610106577f0000000000000000000000000000000000000000000000000000000000000000163314600014610101577f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc557fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b8280a280f35b61023d565b8380fd5b80fd5b346101645760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610164576020610146610169565b73ffffffffffffffffffffffffffffffffffffffff60405191168152f35b600080fd5b333003610101577f000000000000000000000000000000000000000000000000000000000000000090565b60ff7f68df44b1011761f481358c0f49a711192727fb02c377d697bcb0ea8ff8393ac0541615806101ef575b1561023d5760046040517ff92ee8a9000000000000000000000000000000000000000000000000000000008152fd5b507f400ada75000000000000000000000000000000000000000000000000000000007fffffffff000000000000000000000000000000000000000000000000000000006000351614156101c0565b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc546000808092368280378136915af43d82803e1561027a573d90f35b3d90fdfea2646970667358221220c7c26ff3040b96a28e96d6d27b743972943aeaef81cc821544c5fe1e24f9b17264736f6c63430008190033' -export class CowShedSdk { - factoryAddress: `0x${string}` - implementationAddress: `0x${string}` - chainId: number - - constructor({ +/** + * Compute the deterministic proxy address for a CowShed user + */ +export function computeCowShedProxyAddress( + factoryAddress: string, + implementationAddress: string, + owner: string +): `0x${string}` { + const salt = ethers.utils.defaultAbiCoder.encode(['address'], [owner]) + const initCodeHash = ethers.utils.solidityKeccak256( + ['bytes', 'bytes'], + [ + PROXY_CREATION_CODE, + ethers.utils.defaultAbiCoder.encode( + ['address', 'address'], + [implementationAddress, owner] + ), + ] + ) + return ethers.utils.getCreate2Address( factoryAddress, - implementationAddress, - chainId, - }: { - factoryAddress: string - implementationAddress: string - chainId: number - }) { - this.factoryAddress = factoryAddress as `0x${string}` - this.implementationAddress = implementationAddress as `0x${string}` - this.chainId = chainId - } + salt, + initCodeHash + ) as `0x${string}` +} - // Compute the deterministic proxy address for a user - computeProxyAddress(owner: string): `0x${string}` { - const salt = ethers.utils.defaultAbiCoder.encode(['address'], [owner]) - const initCodeHash = ethers.utils.solidityKeccak256( - ['bytes', 'bytes'], - [ - PROXY_CREATION_CODE, - ethers.utils.defaultAbiCoder.encode( - ['address', 'address'], - [this.implementationAddress, owner] - ), - ] - ) - return ethers.utils.getCreate2Address( - this.factoryAddress, - salt, - initCodeHash - ) as `0x${string}` - } +/** + * Encode the executeHooks call for the CowShed factory + */ +export function encodeCowShedExecuteHooks( + calls: any[], + nonce: `0x${string}`, + deadline: bigint, + owner: `0x${string}`, + signature: `0x${string}` +): string { + const cowShedFactoryAbi = parseAbi([ + 'function executeHooks((address target, uint256 value, bytes callData, bool allowFailure, bool isDelegateCall)[] calls, bytes32 nonce, uint256 deadline, address user, bytes signature) returns (address proxy)', + ]) - // Encode the executeHooks call for the factory - static encodeExecuteHooksForFactory( - calls: any[], - nonce: `0x${string}`, - deadline: bigint, - owner: `0x${string}`, - signature: `0x${string}` - ): string { - const cowShedFactoryAbi = parseAbi([ - 'function executeHooks((address target, uint256 value, bytes callData, bool allowFailure, bool isDelegateCall)[] calls, bytes32 nonce, uint256 deadline, address user, bytes signature) returns (address proxy)', - ]) - - return encodeFunctionData({ - abi: cowShedFactoryAbi, - functionName: 'executeHooks', - args: [ - calls.map((call) => ({ - target: call.target as `0x${string}`, - value: call.value, - callData: call.callData, - allowFailure: call.allowFailure, - isDelegateCall: call.isDelegateCall, - })), - nonce, - deadline, - owner, - signature, - ], - }) - } + return encodeFunctionData({ + abi: cowShedFactoryAbi, + functionName: 'executeHooks', + args: [ + calls.map((call) => ({ + target: call.target as `0x${string}`, + value: call.value, + callData: call.callData, + allowFailure: call.allowFailure, + isDelegateCall: call.isDelegateCall, + })), + nonce, + deadline, + owner, + signature, + ], + }) } export interface CowShedPostHooksConfig { @@ -114,12 +102,6 @@ export async function setupCowShedPostHooks(config: CowShedPostHooksConfig) { const account = walletClient.account const signerAddress = account.address - const shedSDK = new CowShedSdk({ - factoryAddress: COW_SHED_FACTORY, - implementationAddress: COW_SHED_IMPLEMENTATION, - chainId, - }) - // Generate a random nonce const nonce = `0x${Array.from({ length: 64 }, () => Math.floor(Math.random() * 16).toString(16) @@ -129,7 +111,11 @@ export async function setupCowShedPostHooks(config: CowShedPostHooksConfig) { const deadline = BigInt(Math.floor(Date.now() / 1000) + 24 * 60 * 60) // Get the proxy address - const shedDeterministicAddress = shedSDK.computeProxyAddress(signerAddress) + const shedDeterministicAddress = computeCowShedProxyAddress( + COW_SHED_FACTORY, + COW_SHED_IMPLEMENTATION, + signerAddress + ) consola.info(`CowShed proxy address: ${shedDeterministicAddress}`) // Create the bridge data for LiFi @@ -426,7 +412,7 @@ export async function setupCowShedPostHooks(config: CowShedPostHooksConfig) { )) as `0x${string}` // Encode the post hooks call data - const shedEncodedPostHooksCallData = CowShedSdk.encodeExecuteHooksForFactory( + const shedEncodedPostHooksCallData = encodeCowShedExecuteHooks( postSwapCalls, nonce, deadline, From d2a422f3333faa87105c4391b0cf9e0e29577ece Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Tue, 27 May 2025 13:13:09 +0300 Subject: [PATCH 25/46] cleanup resources --- script/demoScripts/demoPatcher.ts | 35 +++++++++++++++++++------------ 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/script/demoScripts/demoPatcher.ts b/script/demoScripts/demoPatcher.ts index c517c1bdc..b5245ea96 100644 --- a/script/demoScripts/demoPatcher.ts +++ b/script/demoScripts/demoPatcher.ts @@ -142,21 +142,30 @@ async function main(options: { privateKey: string; dryRun: boolean }) { if (!options.dryRun) { consola.info('Submitting order to CowSwap...') try { - // Add a timeout to the order submission - const orderPromise = cowSdk.postSwapOrder(parameters, advancedSettings) - const timeoutPromise = new Promise((_, reject) => { - setTimeout( - () => - reject(new Error('Order submission timed out after 30 seconds')), - 30000 + // Create an AbortController for proper cancellation + const abortController = new AbortController() + const timeoutId = setTimeout(() => { + abortController.abort() + }, 30000) + + try { + const orderId = await cowSdk.postSwapOrder( + parameters, + advancedSettings ) - }) + clearTimeout(timeoutId) - const orderId = await Promise.race([orderPromise, timeoutPromise]) - consola.success(`Order created with hash: ${orderId}`) - consola.info( - `Explorer URL: https://explorer.cow.fi/orders/${orderId}?chainId=42161` - ) + consola.success(`Order created with hash: ${orderId}`) + consola.info( + `Explorer URL: https://explorer.cow.fi/orders/${orderId}?chainId=42161` + ) + } catch (error) { + clearTimeout(timeoutId) + if (abortController.signal.aborted) { + throw new Error('Order submission timed out after 30 seconds') + } + throw error + } } catch (error) { consola.error('Error submitting order to CowSwap:', error) throw error From c7c515c07071035f2b47ed978ff473e7fe0d0df5 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Tue, 27 May 2025 13:57:38 +0300 Subject: [PATCH 26/46] begin adding dest call --- deployments/_deployments_log_file.json | 4 +- deployments/arbitrum.staging.json | 2 +- script/demoScripts/demoPatcher.ts | 474 ++++++++++++++++++------- 3 files changed, 348 insertions(+), 132 deletions(-) diff --git a/deployments/_deployments_log_file.json b/deployments/_deployments_log_file.json index a23ce0693..40bdd626a 100644 --- a/deployments/_deployments_log_file.json +++ b/deployments/_deployments_log_file.json @@ -29555,9 +29555,9 @@ ], "1.1.0": [ { - "ADDRESS": "0x08BfAc22A3B41637edB8A7920754fDb30B18f740", + "ADDRESS": "0xF336cc028Fc5328472f96e377d32Fd32F8eE1750", "OPTIMIZER_RUNS": "1000000", - "TIMESTAMP": "2024-12-31 12:23:04", + "TIMESTAMP": "2025-05-27 13:50:22", "CONSTRUCTOR_ARGS": "0x000000000000000000000000e35e9842fceaca96570b734083f4a58e8f7c5f2a00000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab1", "SALT": "", "VERIFIED": "true" diff --git a/deployments/arbitrum.staging.json b/deployments/arbitrum.staging.json index 6d1cce23d..293771e0d 100644 --- a/deployments/arbitrum.staging.json +++ b/deployments/arbitrum.staging.json @@ -47,7 +47,7 @@ "TokenWrapper": "0xF63b27AE2Dc887b88f82E2Cc597d07fBB2E78E70", "EmergencyPauseFacet": "0x17Bb203F42d8e404ac7E8dB6ff972B7E8473850b", "Permit2Proxy": "0x6FC01BC9Ff6Cdab694Ec8Ca41B21a2F04C8c37E5", - "AcrossFacetV3": "0x08BfAc22A3B41637edB8A7920754fDb30B18f740", + "AcrossFacetV3": "0xF336cc028Fc5328472f96e377d32Fd32F8eE1750", "ReceiverAcrossV3": "0xe4F3DEF14D61e47c696374453CD64d438FD277F8", "AcrossFacetPackedV3": "0x21767081Ff52CE5563A29f27149D01C7127775A2", "RelayFacet": "0x681a3409c35F12224c436D50Ce14F25f954B6Ea2", diff --git a/script/demoScripts/demoPatcher.ts b/script/demoScripts/demoPatcher.ts index b5245ea96..ee95c24b9 100644 --- a/script/demoScripts/demoPatcher.ts +++ b/script/demoScripts/demoPatcher.ts @@ -12,171 +12,380 @@ import { setupCowShedPostHooks } from './utils/cowSwapHelpers' const ARBITRUM_WETH = '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1' const ARBITRUM_USDC = '0xaf88d065e77c8cC2239327C5EDb3A432268e5831' +const BASE_WETH = '0x4200000000000000000000000000000000000006' const BASE_USDC = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' import arbitrumDeployments from '../../deployments/arbitrum.staging.json' +import baseDeployments from '../../deployments/base.json' const LIFI_DIAMOND_ARBITRUM = arbitrumDeployments.LiFiDiamond +const LIFI_DEX_AGGREGATOR_BASE = baseDeployments.LiFiDEXAggregator const PATCHER_ARBITRUM = arbitrumDeployments.Patcher const VAULT_RELAYER_ARBITRUM = '0xC92E8bdf79f0507f65a392b0ab4667716BFE0110' const ERC20_ABI = erc20Artifact.abi /** - * Main function to execute the demo - * - * Note: There are several TypeScript errors related to the `0x${string}` type - * that don't affect the functionality of the script. In a production environment, - * these should be fixed with proper type assertions. + * Fetch bridge quote from LiFi API for WETH from Arbitrum to Base via AcrossV3 */ -async function main(options: { privateKey: string; dryRun: boolean }) { +async function fetchBridgeQuote( + fromAmount: string, + fromAddress: string, + toAddress: string +) { try { - consola.start('Starting CowSwap with Patcher demo') - - // Set up wallet client - const account = privateKeyToAccount(options.privateKey as Hex) - const walletClient = createWalletClient({ - chain: arbitrum, - transport: http(), - account, + const url = new URL('https://li.quest/v1/quote') + url.searchParams.set('fromChain', '42161') // Arbitrum + url.searchParams.set('toChain', '8453') // Base + url.searchParams.set('fromToken', ARBITRUM_WETH) + url.searchParams.set('toToken', BASE_WETH) + url.searchParams.set('fromAmount', fromAmount) + url.searchParams.set('fromAddress', fromAddress) + url.searchParams.set('toAddress', toAddress) + url.searchParams.set('allowBridges', 'across') + + const response = await fetch(url.toString(), { + method: 'GET', + headers: { accept: 'application/json' }, }) - const walletAddress = account.address - consola.info(`Connected wallet: ${walletAddress}`) + if (!response.ok) { + throw new Error( + `LiFi API error: ${response.status} ${response.statusText}` + ) + } + + const data = await response.json() + consola.info('Bridge quote fetched successfully') + return data + } catch (error) { + consola.error('Error fetching bridge quote:', error) + throw error + } +} - // Amount to swap: 0.001 WETH - const swapAmount = parseUnits('0.001', 18) - consola.info(`Swap amount: 0.001 WETH`) +/** + * Encode destination swap call data for WETH to USDC on Base + */ +async function encodeDestinationSwap( + fromAmount: string, + fromAddress: string, + toAddress: string +) { + try { + const url = new URL('https://li.quest/v1/quote') + url.searchParams.set('fromChain', '8453') // Base + url.searchParams.set('toChain', '8453') // Base + url.searchParams.set('fromToken', BASE_WETH) + url.searchParams.set('toToken', BASE_USDC) + url.searchParams.set('fromAmount', fromAmount) + url.searchParams.set('fromAddress', fromAddress) + url.searchParams.set('toAddress', toAddress) - // Check WETH balance and approve if needed - const wethContract = getContract({ - address: ARBITRUM_WETH as Hex, - abi: ERC20_ABI, - client: { public: walletClient, wallet: walletClient }, + const response = await fetch(url.toString(), { + method: 'GET', + headers: { accept: 'application/json' }, }) - const wethBalance = (await wethContract.read.balanceOf([ - walletAddress, - ])) as bigint - consola.info(`WETH balance: ${wethBalance}`) + if (!response.ok) { + throw new Error( + `LiFi API error: ${response.status} ${response.statusText}` + ) + } + + const data = await response.json() + consola.info('Destination swap quote fetched successfully') - if (wethBalance < swapAmount) { - consola.error(`Insufficient WETH balance. Need at least 0.001 WETH.`) - process.exit(1) + return { + callData: data.transactionRequest.data, + expectedOutput: data.estimate.toAmount, + gasLimit: data.transactionRequest.gasLimit, } + } catch (error) { + consola.error('Error fetching destination swap quote:', error) + throw error + } +} - // Check allowance - const allowance = (await wethContract.read.allowance([ - walletAddress, - VAULT_RELAYER_ARBITRUM, - ])) as bigint - consola.info(`Current allowance: ${allowance}`) - - if (allowance < swapAmount) { - consola.info('Approving WETH for CoW Protocol VaultRelayer...') - if (!options.dryRun) { - const approveTx = await wethContract.write.approve([ - VAULT_RELAYER_ARBITRUM as `0x${string}`, - BigInt( - '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' - ), // Max uint256 - ]) - consola.success(`Approval transaction sent: ${approveTx}`) - } else { - consola.info(`[DRY RUN] Would approve WETH for VaultRelayer`) - } +/** + * Execute cross-chain bridge with destination swap using AcrossV3 + */ +async function executeCrossChainBridgeWithSwap(options: { + privateKey: string + dryRun: boolean +}) { + // Set up wallet client + const account = privateKeyToAccount(options.privateKey as Hex) + const walletClient = createWalletClient({ + chain: arbitrum, + transport: http(), + account, + }) + + const walletAddress = account.address + consola.info(`Connected wallet: ${walletAddress}`) + + // Amount to bridge: 0.001 WETH + const bridgeAmount = parseUnits('0.001', 18) + consola.info(`Bridge amount: 0.001 WETH`) + + // Check WETH balance + const wethContract = getContract({ + address: ARBITRUM_WETH as Hex, + abi: ERC20_ABI, + client: { public: walletClient, wallet: walletClient }, + }) + + const wethBalance = (await wethContract.read.balanceOf([ + walletAddress, + ])) as bigint + consola.info(`WETH balance: ${wethBalance}`) + + if (wethBalance < bridgeAmount) { + consola.error(`Insufficient WETH balance. Need at least 0.001 WETH.`) + process.exit(1) + } + + // Check allowance for LiFi Diamond + const allowance = (await wethContract.read.allowance([ + walletAddress, + LIFI_DIAMOND_ARBITRUM, + ])) as bigint + consola.info(`Current allowance: ${allowance}`) + + if (allowance < bridgeAmount) { + consola.info('Approving WETH for LiFi Diamond...') + if (!options.dryRun) { + const approveTx = await wethContract.write.approve([ + LIFI_DIAMOND_ARBITRUM as `0x${string}`, + BigInt( + '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' + ), // Max uint256 + ]) + consola.success(`Approval transaction sent: ${approveTx}`) + } else { + consola.info(`[DRY RUN] Would approve WETH for LiFi Diamond`) } + } - // Set up CowShed post hooks - const { shedDeterministicAddress, postHooks } = await setupCowShedPostHooks( - { - chainId: 42161, // Arbitrum chain ID - walletClient, - usdcAddress: ARBITRUM_USDC, - receivedAmount: parseUnits('0', 6), // This will be dynamically patched - lifiDiamondAddress: LIFI_DIAMOND_ARBITRUM, - patcherAddress: PATCHER_ARBITRUM, - baseUsdcAddress: BASE_USDC, - destinationChainId: 8453n, // BASE chain ID - } - ) + // Fetch bridge quote from LiFi API + consola.info('Fetching bridge quote from LiFi API...') + const bridgeQuote = await fetchBridgeQuote( + bridgeAmount.toString(), + walletAddress, + LIFI_DEX_AGGREGATOR_BASE // Destination will be the DEX aggregator for the swap + ) - // Create ethers provider and signer for CoW SDK - const provider = new ethers.providers.JsonRpcProvider( - arbitrum.rpcUrls.default.http[0] - ) - const ethersSigner = new ethers.Wallet(options.privateKey, provider) + // Fetch destination swap quote + consola.info('Fetching destination swap quote...') + const expectedBridgedAmount = bridgeQuote.estimate.toAmount + const swapQuote = await encodeDestinationSwap( + expectedBridgedAmount, + LIFI_DEX_AGGREGATOR_BASE, + walletAddress + ) + + // Execute the bridge transaction with destination call + if (!options.dryRun) { + consola.info('Executing cross-chain bridge with destination swap...') - // Initialize CoW SDK with proper TraderParameters - const cowSdk = new TradingSdk({ - chainId: SupportedChainId.ARBITRUM_ONE, - signer: ethersSigner, - appCode: 'lifi-demo' as any, // Cast to any to satisfy the AppCode type + // Create wallet client for transaction + const txHash = await walletClient.sendTransaction({ + to: LIFI_DIAMOND_ARBITRUM as `0x${string}`, + data: bridgeQuote.transactionRequest.data as `0x${string}`, + value: BigInt(bridgeQuote.transactionRequest.value || '0'), + gas: BigInt(bridgeQuote.transactionRequest.gasLimit || '500000'), }) - // Create the order parameters - const parameters = { - kind: OrderKind.SELL, - sellToken: ARBITRUM_WETH as `0x${string}`, - sellTokenDecimals: 18, - buyToken: ARBITRUM_USDC as `0x${string}`, - buyTokenDecimals: 6, - amount: swapAmount.toString(), - receiver: shedDeterministicAddress as `0x${string}`, // Important: Set the receiver to the CowShed proxy - validFor: 30 * 60, // 30 minutes in seconds - slippageBps: 50, // 0.5% slippage + consola.success(`Bridge transaction sent: ${txHash}`) + consola.info(`Expected bridged amount: ${expectedBridgedAmount} WETH`) + consola.info(`Expected swap output: ${swapQuote.expectedOutput} USDC`) + consola.info(`Transaction hash: ${txHash}`) + } else { + consola.info( + `[DRY RUN] Would execute cross-chain bridge with destination swap` + ) + consola.info( + `Bridge quote: ${JSON.stringify(bridgeQuote.estimate, null, 2)}` + ) + consola.info(`Swap quote: ${JSON.stringify(swapQuote, null, 2)}`) + } + + consola.success( + 'Cross-chain bridge with destination swap demo completed successfully' + ) +} + +/** + * Execute the original CowSwap demo + */ +async function executeCowSwapDemo(options: { + privateKey: string + dryRun: boolean +}) { + // Set up wallet client + const account = privateKeyToAccount(options.privateKey as Hex) + const walletClient = createWalletClient({ + chain: arbitrum, + transport: http(), + account, + }) + + const walletAddress = account.address + consola.info(`Connected wallet: ${walletAddress}`) + + // Amount to swap: 0.001 WETH + const swapAmount = parseUnits('0.001', 18) + consola.info(`Swap amount: 0.001 WETH`) + + // Check WETH balance and approve if needed + const wethContract = getContract({ + address: ARBITRUM_WETH as Hex, + abi: ERC20_ABI, + client: { public: walletClient, wallet: walletClient }, + }) + + const wethBalance = (await wethContract.read.balanceOf([ + walletAddress, + ])) as bigint + consola.info(`WETH balance: ${wethBalance}`) + + if (wethBalance < swapAmount) { + consola.error(`Insufficient WETH balance. Need at least 0.001 WETH.`) + process.exit(1) + } + + // Check allowance + const allowance = (await wethContract.read.allowance([ + walletAddress, + VAULT_RELAYER_ARBITRUM, + ])) as bigint + consola.info(`Current allowance: ${allowance}`) + + if (allowance < swapAmount) { + consola.info('Approving WETH for CoW Protocol VaultRelayer...') + if (!options.dryRun) { + const approveTx = await wethContract.write.approve([ + VAULT_RELAYER_ARBITRUM as `0x${string}`, + BigInt( + '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' + ), // Max uint256 + ]) + consola.success(`Approval transaction sent: ${approveTx}`) + } else { + consola.info(`[DRY RUN] Would approve WETH for VaultRelayer`) } + } + + // Set up CowShed post hooks + const { shedDeterministicAddress, postHooks } = await setupCowShedPostHooks({ + chainId: 42161, // Arbitrum chain ID + walletClient, + usdcAddress: ARBITRUM_USDC, + receivedAmount: parseUnits('0', 6), // This will be dynamically patched + lifiDiamondAddress: LIFI_DIAMOND_ARBITRUM, + patcherAddress: PATCHER_ARBITRUM, + baseUsdcAddress: BASE_USDC, + destinationChainId: 8453n, // BASE chain ID + }) + + // Create ethers provider and signer for CoW SDK + const provider = new ethers.providers.JsonRpcProvider( + arbitrum.rpcUrls.default.http[0] + ) + const ethersSigner = new ethers.Wallet(options.privateKey, provider) + + // Initialize CoW SDK with proper TraderParameters + const cowSdk = new TradingSdk({ + chainId: SupportedChainId.ARBITRUM_ONE, + signer: ethersSigner, + appCode: 'lifi-demo' as any, // Cast to any to satisfy the AppCode type + }) - // Create advanced settings with post hooks - const advancedSettings = { - appData: { - metadata: { - hooks: { - version: '1', - pre: [], - post: postHooks, - }, + // Create the order parameters + const parameters = { + kind: OrderKind.SELL, + sellToken: ARBITRUM_WETH as `0x${string}`, + sellTokenDecimals: 18, + buyToken: ARBITRUM_USDC as `0x${string}`, + buyTokenDecimals: 6, + amount: swapAmount.toString(), + receiver: shedDeterministicAddress as `0x${string}`, // Important: Set the receiver to the CowShed proxy + validFor: 30 * 60, // 30 minutes in seconds + slippageBps: 50, // 0.5% slippage + } + + // Create advanced settings with post hooks + const advancedSettings = { + appData: { + metadata: { + hooks: { + version: '1', + pre: [], + post: postHooks, }, }, - } + }, + } + + // Submit the order with post hooks + if (!options.dryRun) { + consola.info('Submitting order to CowSwap...') + try { + // Create an AbortController for proper cancellation + const abortController = new AbortController() + const timeoutId = setTimeout(() => { + abortController.abort() + }, 30000) - // Submit the order with post hooks - if (!options.dryRun) { - consola.info('Submitting order to CowSwap...') try { - // Create an AbortController for proper cancellation - const abortController = new AbortController() - const timeoutId = setTimeout(() => { - abortController.abort() - }, 30000) - - try { - const orderId = await cowSdk.postSwapOrder( - parameters, - advancedSettings - ) - clearTimeout(timeoutId) - - consola.success(`Order created with hash: ${orderId}`) - consola.info( - `Explorer URL: https://explorer.cow.fi/orders/${orderId}?chainId=42161` - ) - } catch (error) { - clearTimeout(timeoutId) - if (abortController.signal.aborted) { - throw new Error('Order submission timed out after 30 seconds') - } - throw error - } + const orderId = await cowSdk.postSwapOrder(parameters, advancedSettings) + clearTimeout(timeoutId) + + consola.success(`Order created with hash: ${orderId}`) + consola.info( + `Explorer URL: https://explorer.cow.fi/orders/${orderId}?chainId=42161` + ) } catch (error) { - consola.error('Error submitting order to CowSwap:', error) + clearTimeout(timeoutId) + if (abortController.signal.aborted) { + throw new Error('Order submission timed out after 30 seconds') + } throw error } - } else { - consola.info(`[DRY RUN] Would submit order to CowSwap with post hooks`) - consola.info(`Parameters: ${JSON.stringify(parameters, null, 2)}`) - consola.info(`Post hooks: ${JSON.stringify(postHooks, null, 2)}`) + } catch (error) { + consola.error('Error submitting order to CowSwap:', error) + throw error } + } else { + consola.info(`[DRY RUN] Would submit order to CowSwap with post hooks`) + consola.info(`Parameters: ${JSON.stringify(parameters, null, 2)}`) + consola.info(`Post hooks: ${JSON.stringify(postHooks, null, 2)}`) + } + + consola.success('Demo completed successfully') +} - consola.success('Demo completed successfully') +/** + * Main function to execute the demo + * + * Note: There are several TypeScript errors related to the `0x${string}` type + * that don't affect the functionality of the script. In a production environment, + * these should be fixed with proper type assertions. + */ +async function main(options: { + privateKey: string + dryRun: boolean + 'dest-call': boolean +}) { + try { + if (options['dest-call']) { + consola.start( + 'Starting AcrossV3 cross-chain bridge with destination swap demo' + ) + await executeCrossChainBridgeWithSwap(options) + } else { + consola.start('Starting CowSwap with Patcher demo') + await executeCowSwapDemo(options) + } } catch (error) { consola.error('Error executing demo:', error) process.exit(1) @@ -187,7 +396,8 @@ async function main(options: { privateKey: string; dryRun: boolean }) { const cmd = defineCommand({ meta: { name: 'demoPatcher', - description: 'Demo script for CowSwap with Patcher contract', + description: + 'Demo script for CowSwap with Patcher contract or AcrossV3 cross-chain bridge with destination swap', }, args: { privateKey: { @@ -200,6 +410,12 @@ const cmd = defineCommand({ description: 'Run in dry-run mode without submitting transactions', default: false, }, + 'dest-call': { + type: 'boolean', + description: + 'Demo cross-chain bridging with destination swap using AcrossV3', + default: false, + }, }, run: async ({ args }) => { await main(args) From 2cd19ad53de3515ba93556c73c50bae4a1326471 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Tue, 27 May 2025 16:23:11 +0300 Subject: [PATCH 27/46] use separate scripts --- script/demoScripts/demoPatcher.ts | 228 +-------- script/demoScripts/demoPatcherDest.ts | 680 ++++++++++++++++++++++++++ 2 files changed, 686 insertions(+), 222 deletions(-) create mode 100644 script/demoScripts/demoPatcherDest.ts diff --git a/script/demoScripts/demoPatcher.ts b/script/demoScripts/demoPatcher.ts index ee95c24b9..33f7a7e98 100644 --- a/script/demoScripts/demoPatcher.ts +++ b/script/demoScripts/demoPatcher.ts @@ -12,210 +12,16 @@ import { setupCowShedPostHooks } from './utils/cowSwapHelpers' const ARBITRUM_WETH = '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1' const ARBITRUM_USDC = '0xaf88d065e77c8cC2239327C5EDb3A432268e5831' -const BASE_WETH = '0x4200000000000000000000000000000000000006' const BASE_USDC = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' import arbitrumDeployments from '../../deployments/arbitrum.staging.json' -import baseDeployments from '../../deployments/base.json' const LIFI_DIAMOND_ARBITRUM = arbitrumDeployments.LiFiDiamond -const LIFI_DEX_AGGREGATOR_BASE = baseDeployments.LiFiDEXAggregator const PATCHER_ARBITRUM = arbitrumDeployments.Patcher const VAULT_RELAYER_ARBITRUM = '0xC92E8bdf79f0507f65a392b0ab4667716BFE0110' const ERC20_ABI = erc20Artifact.abi /** - * Fetch bridge quote from LiFi API for WETH from Arbitrum to Base via AcrossV3 - */ -async function fetchBridgeQuote( - fromAmount: string, - fromAddress: string, - toAddress: string -) { - try { - const url = new URL('https://li.quest/v1/quote') - url.searchParams.set('fromChain', '42161') // Arbitrum - url.searchParams.set('toChain', '8453') // Base - url.searchParams.set('fromToken', ARBITRUM_WETH) - url.searchParams.set('toToken', BASE_WETH) - url.searchParams.set('fromAmount', fromAmount) - url.searchParams.set('fromAddress', fromAddress) - url.searchParams.set('toAddress', toAddress) - url.searchParams.set('allowBridges', 'across') - - const response = await fetch(url.toString(), { - method: 'GET', - headers: { accept: 'application/json' }, - }) - - if (!response.ok) { - throw new Error( - `LiFi API error: ${response.status} ${response.statusText}` - ) - } - - const data = await response.json() - consola.info('Bridge quote fetched successfully') - return data - } catch (error) { - consola.error('Error fetching bridge quote:', error) - throw error - } -} - -/** - * Encode destination swap call data for WETH to USDC on Base - */ -async function encodeDestinationSwap( - fromAmount: string, - fromAddress: string, - toAddress: string -) { - try { - const url = new URL('https://li.quest/v1/quote') - url.searchParams.set('fromChain', '8453') // Base - url.searchParams.set('toChain', '8453') // Base - url.searchParams.set('fromToken', BASE_WETH) - url.searchParams.set('toToken', BASE_USDC) - url.searchParams.set('fromAmount', fromAmount) - url.searchParams.set('fromAddress', fromAddress) - url.searchParams.set('toAddress', toAddress) - - const response = await fetch(url.toString(), { - method: 'GET', - headers: { accept: 'application/json' }, - }) - - if (!response.ok) { - throw new Error( - `LiFi API error: ${response.status} ${response.statusText}` - ) - } - - const data = await response.json() - consola.info('Destination swap quote fetched successfully') - - return { - callData: data.transactionRequest.data, - expectedOutput: data.estimate.toAmount, - gasLimit: data.transactionRequest.gasLimit, - } - } catch (error) { - consola.error('Error fetching destination swap quote:', error) - throw error - } -} - -/** - * Execute cross-chain bridge with destination swap using AcrossV3 - */ -async function executeCrossChainBridgeWithSwap(options: { - privateKey: string - dryRun: boolean -}) { - // Set up wallet client - const account = privateKeyToAccount(options.privateKey as Hex) - const walletClient = createWalletClient({ - chain: arbitrum, - transport: http(), - account, - }) - - const walletAddress = account.address - consola.info(`Connected wallet: ${walletAddress}`) - - // Amount to bridge: 0.001 WETH - const bridgeAmount = parseUnits('0.001', 18) - consola.info(`Bridge amount: 0.001 WETH`) - - // Check WETH balance - const wethContract = getContract({ - address: ARBITRUM_WETH as Hex, - abi: ERC20_ABI, - client: { public: walletClient, wallet: walletClient }, - }) - - const wethBalance = (await wethContract.read.balanceOf([ - walletAddress, - ])) as bigint - consola.info(`WETH balance: ${wethBalance}`) - - if (wethBalance < bridgeAmount) { - consola.error(`Insufficient WETH balance. Need at least 0.001 WETH.`) - process.exit(1) - } - - // Check allowance for LiFi Diamond - const allowance = (await wethContract.read.allowance([ - walletAddress, - LIFI_DIAMOND_ARBITRUM, - ])) as bigint - consola.info(`Current allowance: ${allowance}`) - - if (allowance < bridgeAmount) { - consola.info('Approving WETH for LiFi Diamond...') - if (!options.dryRun) { - const approveTx = await wethContract.write.approve([ - LIFI_DIAMOND_ARBITRUM as `0x${string}`, - BigInt( - '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' - ), // Max uint256 - ]) - consola.success(`Approval transaction sent: ${approveTx}`) - } else { - consola.info(`[DRY RUN] Would approve WETH for LiFi Diamond`) - } - } - - // Fetch bridge quote from LiFi API - consola.info('Fetching bridge quote from LiFi API...') - const bridgeQuote = await fetchBridgeQuote( - bridgeAmount.toString(), - walletAddress, - LIFI_DEX_AGGREGATOR_BASE // Destination will be the DEX aggregator for the swap - ) - - // Fetch destination swap quote - consola.info('Fetching destination swap quote...') - const expectedBridgedAmount = bridgeQuote.estimate.toAmount - const swapQuote = await encodeDestinationSwap( - expectedBridgedAmount, - LIFI_DEX_AGGREGATOR_BASE, - walletAddress - ) - - // Execute the bridge transaction with destination call - if (!options.dryRun) { - consola.info('Executing cross-chain bridge with destination swap...') - - // Create wallet client for transaction - const txHash = await walletClient.sendTransaction({ - to: LIFI_DIAMOND_ARBITRUM as `0x${string}`, - data: bridgeQuote.transactionRequest.data as `0x${string}`, - value: BigInt(bridgeQuote.transactionRequest.value || '0'), - gas: BigInt(bridgeQuote.transactionRequest.gasLimit || '500000'), - }) - - consola.success(`Bridge transaction sent: ${txHash}`) - consola.info(`Expected bridged amount: ${expectedBridgedAmount} WETH`) - consola.info(`Expected swap output: ${swapQuote.expectedOutput} USDC`) - consola.info(`Transaction hash: ${txHash}`) - } else { - consola.info( - `[DRY RUN] Would execute cross-chain bridge with destination swap` - ) - consola.info( - `Bridge quote: ${JSON.stringify(bridgeQuote.estimate, null, 2)}` - ) - consola.info(`Swap quote: ${JSON.stringify(swapQuote, null, 2)}`) - } - - consola.success( - 'Cross-chain bridge with destination swap demo completed successfully' - ) -} - -/** - * Execute the original CowSwap demo + * Execute CowSwap demo with Patcher contract */ async function executeCowSwapDemo(options: { privateKey: string @@ -361,31 +167,16 @@ async function executeCowSwapDemo(options: { consola.info(`Post hooks: ${JSON.stringify(postHooks, null, 2)}`) } - consola.success('Demo completed successfully') + consola.success('CowSwap demo completed successfully') } /** * Main function to execute the demo - * - * Note: There are several TypeScript errors related to the `0x${string}` type - * that don't affect the functionality of the script. In a production environment, - * these should be fixed with proper type assertions. */ -async function main(options: { - privateKey: string - dryRun: boolean - 'dest-call': boolean -}) { +async function main(options: { privateKey: string; dryRun: boolean }) { try { - if (options['dest-call']) { - consola.start( - 'Starting AcrossV3 cross-chain bridge with destination swap demo' - ) - await executeCrossChainBridgeWithSwap(options) - } else { - consola.start('Starting CowSwap with Patcher demo') - await executeCowSwapDemo(options) - } + consola.start('Starting CowSwap with Patcher demo') + await executeCowSwapDemo(options) } catch (error) { consola.error('Error executing demo:', error) process.exit(1) @@ -396,8 +187,7 @@ async function main(options: { const cmd = defineCommand({ meta: { name: 'demoPatcher', - description: - 'Demo script for CowSwap with Patcher contract or AcrossV3 cross-chain bridge with destination swap', + description: 'Demo script for CowSwap with Patcher contract', }, args: { privateKey: { @@ -410,12 +200,6 @@ const cmd = defineCommand({ description: 'Run in dry-run mode without submitting transactions', default: false, }, - 'dest-call': { - type: 'boolean', - description: - 'Demo cross-chain bridging with destination swap using AcrossV3', - default: false, - }, }, run: async ({ args }) => { await main(args) diff --git a/script/demoScripts/demoPatcherDest.ts b/script/demoScripts/demoPatcherDest.ts new file mode 100644 index 000000000..f78c66047 --- /dev/null +++ b/script/demoScripts/demoPatcherDest.ts @@ -0,0 +1,680 @@ +#!/usr/bin/env bun + +import { defineCommand, runMain } from 'citty' +import { consola } from 'consola' +import { + createWalletClient, + createPublicClient, + http, + parseUnits, + getContract, + type Hex, + encodeFunctionData, + encodeAbiParameters, +} from 'viem' +import { privateKeyToAccount } from 'viem/accounts' +import { arbitrum } from 'viem/chains' +import baseDeployments from '../../deployments/base.json' +import arbitrumStagingDeployments from '../../deployments/arbitrum.staging.json' + +// Contract addresses +const ARBITRUM_WETH = '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1' +const BASE_WETH = '0x4200000000000000000000000000000000000006' +const BASE_USDC = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' +const BASE_LIFI_DEX_AGGREGATOR = baseDeployments.LiFiDEXAggregator as Hex +const RECEIVER_ACROSS_V3_BASE = baseDeployments.ReceiverAcrossV3 as Hex +const LIFI_DIAMOND_ARBITRUM = arbitrumStagingDeployments.LiFiDiamond as Hex + +// Simple ERC20 ABI +const ERC20_ABI = [ + { + inputs: [ + { name: 'owner', type: 'address' }, + { name: 'spender', type: 'address' }, + ], + name: 'allowance', + outputs: [{ name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { name: 'spender', type: 'address' }, + { name: 'amount', type: 'uint256' }, + ], + name: 'approve', + outputs: [{ name: '', type: 'bool' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ name: 'account', type: 'address' }], + name: 'balanceOf', + outputs: [{ name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, +] as const + +/** + * Fetch cross-chain route with destination swap using LiFi Advanced Routes API + */ +async function fetchCrossChainRoute( + fromAmount: string, + fromAddress: string +): Promise { + const requestBody = { + fromAddress, + fromAmount, + fromChainId: 42161, // Arbitrum + fromTokenAddress: ARBITRUM_WETH, // WETH on Arbitrum + toChainId: 8453, // Base + toTokenAddress: BASE_USDC, // USDC on Base + options: { + integrator: 'lifi-demo', + order: 'CHEAPEST', + slippage: 0.05, // 5% slippage + maxPriceImpact: 0.4, + allowSwitchChain: true, + bridges: { + deny: [ + 'hop', + 'cbridge', + 'optimism', + 'gnosis', + 'omni', + 'celercircle', + 'thorswap', + 'symbiosis', + 'mayan', + 'mayanWH', + 'mayanMCTP', + 'allbridge', + 'celerim', + 'squid', + 'relay', + 'polygon', + 'arbitrum', + 'glacis', + ], + }, + exchanges: { + allow: ['lifidexaggregator'], // Only allow LiFi DEX Aggregator + }, + }, + } + + try { + consola.info('Fetching cross-chain route from LiFi Advanced Routes API...') + const response = await fetch( + 'https://api.jumper.exchange/p/lifi/advanced/routes', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(requestBody), + } + ) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const data = await response.json() + + if (!data.routes || data.routes.length === 0) { + throw new Error('No routes found') + } + + const route = data.routes[0] // Use the first (cheapest) route + consola.success( + `Found route: ${route.fromAmount} WETH → ${route.toAmount} USDC` + ) + consola.info(`Route ID: ${route.id}`) + consola.info(`Gas cost: $${route.gasCostUSD}`) + consola.info(`Steps: ${route.steps.length}`) + + // Log step details + route.steps.forEach((step: any, index: number) => { + consola.info(`Step ${index + 1}: ${step.tool} (${step.type})`) + if (step.action.fromToken && step.action.toToken) { + consola.info( + ` ${step.action.fromToken.symbol} → ${step.action.toToken.symbol}` + ) + } + }) + + return route + } catch (error) { + consola.error('Error fetching cross-chain route:', error) + throw error + } +} + +/** + * Extract bridge and swap details from LiFi route + */ +function extractRouteDetails(route: any) { + const bridgeStep = route.steps.find( + (step: any) => + step.type === 'lifi' && + step.includedSteps?.some((s: any) => s.type === 'cross') + ) + + if (!bridgeStep) { + throw new Error('No bridge step found in route') + } + + const crossStep = bridgeStep.includedSteps.find( + (s: any) => s.type === 'cross' + ) + const destSwapStep = bridgeStep.includedSteps.find( + (s: any) => s.type === 'swap' + ) + + if (!crossStep) { + throw new Error('No cross-chain step found') + } + + consola.info('📊 Route Analysis:') + consola.info( + `- Bridge: ${crossStep.action.fromToken.symbol} → ${crossStep.action.toToken.symbol}` + ) + consola.info(`- Bridge amount: ${crossStep.action.fromAmount}`) + consola.info(`- Expected received: ${crossStep.estimate.toAmount}`) + + if (destSwapStep) { + consola.info( + `- Destination swap: ${destSwapStep.action.fromToken.symbol} → ${destSwapStep.action.toToken.symbol}` + ) + consola.info(`- Swap input: ${destSwapStep.action.fromAmount}`) + consola.info(`- Swap output: ${destSwapStep.estimate.toAmount}`) + consola.info(`- Swap min output: ${destSwapStep.estimate.toAmountMin}`) + } + + return { + // Bridge details + fromAmount: crossStep.action.fromAmount, + toAmount: crossStep.estimate.toAmount, + toAmountMin: crossStep.estimate.toAmountMin, + + // Destination swap details (if exists) + hasDestinationSwap: !!destSwapStep, + swapFromAmount: destSwapStep?.action.fromAmount, + swapToAmount: destSwapStep?.estimate.toAmount, + swapToAmountMin: destSwapStep?.estimate.toAmountMin, + + // Final output + finalAmount: route.toAmount, + finalAmountMin: route.toAmountMin, + + // Gas and fees + gasCostUSD: route.gasCostUSD, + + // Tool info + bridgeTool: crossStep.tool, + swapTool: destSwapStep?.tool, + } +} + +/** + * Create LiFiDexAggregator swap calldata using route details + */ +function createLiFiDexAggregatorSwapCallData( + swapFromAmount: string, + swapToAmountMin: string, + recipient: string +): string { + // LiFiDexAggregator swapTokensGeneric function + const lifiDexAggregatorAbi = [ + { + inputs: [ + { name: '_transactionId', type: 'bytes32' }, + { name: '_integrator', type: 'string' }, + { name: '_referrer', type: 'address' }, + { name: '_receiver', type: 'address' }, + { name: '_minAmount', type: 'uint256' }, + { + components: [ + { name: 'callTo', type: 'address' }, + { name: 'approveTo', type: 'address' }, + { name: 'sendingAssetId', type: 'address' }, + { name: 'receivingAssetId', type: 'address' }, + { name: 'fromAmount', type: 'uint256' }, + { name: 'callData', type: 'bytes' }, + { name: 'requiresDeposit', type: 'bool' }, + ], + name: '_swapData', + type: 'tuple[]', + }, + ], + name: 'swapTokensGeneric', + outputs: [], + stateMutability: 'payable', + type: 'function', + }, + ] as const + + // For now, we'll create a simple direct swap calldata + // In a real implementation, you'd want to get the actual swap route from LiFi + const swapData = [ + { + callTo: BASE_LIFI_DEX_AGGREGATOR as `0x${string}`, + approveTo: BASE_LIFI_DEX_AGGREGATOR as `0x${string}`, + sendingAssetId: BASE_WETH as `0x${string}`, + receivingAssetId: BASE_USDC as `0x${string}`, + fromAmount: BigInt(swapFromAmount), + callData: '0x' as `0x${string}`, // Simplified - in practice you'd get this from LiFi API + requiresDeposit: true, + }, + ] + + const callData = encodeFunctionData({ + abi: lifiDexAggregatorAbi, + functionName: 'swapTokensGeneric', + args: [ + `0x${Date.now().toString(16).padStart(64, '0')}` as `0x${string}`, // transactionId + 'lifi-demo', // integrator + '0x0000000000000000000000000000000000000000' as `0x${string}`, // referrer + recipient as `0x${string}`, // receiver + BigInt(swapToAmountMin), // minAmount + swapData, + ], + }) + + consola.info( + 'Created LiFiDexAggregator swap calldata using LiFi route parameters' + ) + return callData +} + +/** + * Encode destination call message for ReceiverAcrossV3 + */ +function encodeDestinationCallMessage( + transactionId: string, + swapCallData: string, + fromAmount: string, + receiver: string +): string { + // LibSwap.SwapData structure for the LiFiDexAggregator swap + const swapData = [ + { + callTo: BASE_LIFI_DEX_AGGREGATOR as `0x${string}`, // LiFiDexAggregator + approveTo: BASE_LIFI_DEX_AGGREGATOR as `0x${string}`, // Approve to the same aggregator + sendingAssetId: BASE_WETH as `0x${string}`, + receivingAssetId: BASE_USDC as `0x${string}`, + fromAmount: BigInt(fromAmount), + callData: swapCallData as `0x${string}`, + requiresDeposit: true, + }, + ] + + // Encode the message payload for ReceiverAcrossV3.handleV3AcrossMessage + const messagePayload = encodeAbiParameters( + [ + { name: 'transactionId', type: 'bytes32' }, + { + name: 'swapData', + type: 'tuple[]', + components: [ + { name: 'callTo', type: 'address' }, + { name: 'approveTo', type: 'address' }, + { name: 'sendingAssetId', type: 'address' }, + { name: 'receivingAssetId', type: 'address' }, + { name: 'fromAmount', type: 'uint256' }, + { name: 'callData', type: 'bytes' }, + { name: 'requiresDeposit', type: 'bool' }, + ], + }, + { name: 'receiver', type: 'address' }, + ], + [transactionId as `0x${string}`, swapData, receiver as `0x${string}`] + ) + + consola.info('Encoded destination call message for ReceiverAcrossV3') + return messagePayload +} + +/** + * Construct AcrossV3 bridge transaction calldata using route details + */ +function constructBridgeCallData( + routeDetails: any, + destinationCallMessage: string, + walletAddress: string +): string { + // ABI for startBridgeTokensViaAcrossV3 function + const acrossV3Abi = [ + { + inputs: [ + { + components: [ + { name: 'transactionId', type: 'bytes32' }, + { name: 'bridge', type: 'string' }, + { name: 'integrator', type: 'string' }, + { name: 'referrer', type: 'address' }, + { name: 'sendingAssetId', type: 'address' }, + { name: 'receiver', type: 'address' }, + { name: 'minAmount', type: 'uint256' }, + { name: 'destinationChainId', type: 'uint256' }, + { name: 'hasSourceSwaps', type: 'bool' }, + { name: 'hasDestinationCall', type: 'bool' }, + ], + name: '_bridgeData', + type: 'tuple', + }, + { + components: [ + { name: 'receiverAddress', type: 'address' }, + { name: 'refundAddress', type: 'address' }, + { name: 'receivingAssetId', type: 'address' }, + { name: 'outputAmount', type: 'uint256' }, + { name: 'outputAmountPercent', type: 'uint64' }, + { name: 'exclusiveRelayer', type: 'address' }, + { name: 'quoteTimestamp', type: 'uint32' }, + { name: 'fillDeadline', type: 'uint32' }, + { name: 'exclusivityDeadline', type: 'uint32' }, + { name: 'message', type: 'bytes' }, + ], + name: '_acrossData', + type: 'tuple', + }, + ], + name: 'startBridgeTokensViaAcrossV3', + outputs: [], + stateMutability: 'payable', + type: 'function', + }, + ] as const + + // Generate a unique transaction ID + const transactionId = `0x${Date.now().toString(16).padStart(64, '0')}` + + // Bridge data structure + const bridgeData = { + transactionId: transactionId as `0x${string}`, + bridge: 'across', + integrator: 'lifi-demo', + referrer: '0x0000000000000000000000000000000000000000' as `0x${string}`, + sendingAssetId: ARBITRUM_WETH as `0x${string}`, + receiver: RECEIVER_ACROSS_V3_BASE as `0x${string}`, // ReceiverAcrossV3 + minAmount: BigInt(routeDetails.toAmountMin), // Use LiFi's calculated minimum + destinationChainId: 8453n, // Base chain ID + hasSourceSwaps: false, + hasDestinationCall: true, // Enable destination call + } + + // Calculate output amount percent based on LiFi's calculations + const outputAmountPercent = + (BigInt(routeDetails.toAmount) * BigInt('1000000000000000000')) / + BigInt(routeDetails.fromAmount) + + // Across data structure + const acrossData = { + receiverAddress: RECEIVER_ACROSS_V3_BASE as `0x${string}`, // ReceiverAcrossV3 + refundAddress: walletAddress as `0x${string}`, + receivingAssetId: BASE_WETH as `0x${string}`, + outputAmount: BigInt(routeDetails.toAmount), // Use LiFi's calculated amount + outputAmountPercent: outputAmountPercent, // Calculated from LiFi data + exclusiveRelayer: + '0x0000000000000000000000000000000000000000' as `0x${string}`, + quoteTimestamp: Math.floor(Date.now() / 1000), + fillDeadline: Math.floor(Date.now() / 1000) + 3600, // 1 hour from now + exclusivityDeadline: 0, + message: destinationCallMessage as `0x${string}`, // Our encoded destination call + } + + // Encode the transaction + const callData = encodeFunctionData({ + abi: acrossV3Abi, + functionName: 'startBridgeTokensViaAcrossV3', + args: [bridgeData, acrossData], + }) + + consola.success( + 'Successfully constructed AcrossV3 bridge transaction calldata' + ) + return callData +} + +/** + * Execute cross-chain bridge with destination swap using LiFi Advanced Routes + */ +async function executeCrossChainBridgeWithSwap(options: { + privateKey: string + dryRun: boolean +}) { + // Set up wallet client + const account = privateKeyToAccount(options.privateKey as Hex) + const walletClient = createWalletClient({ + chain: arbitrum, + transport: http(), + account, + }) + + // Set up public client for reading transaction receipts + const publicClient = createPublicClient({ + chain: arbitrum, + transport: http(), + }) + + const walletAddress = account.address + consola.info(`Connected wallet: ${walletAddress}`) + + // Amount to bridge: 0.001 WETH + const bridgeAmount = parseUnits('0.001', 18) + consola.info(`Bridge amount: 0.001 WETH`) + + // Check WETH balance + const wethContract = getContract({ + address: ARBITRUM_WETH as Hex, + abi: ERC20_ABI, + client: { public: walletClient, wallet: walletClient }, + }) + + const wethBalance = (await wethContract.read.balanceOf([ + walletAddress, + ])) as bigint + consola.info(`WETH balance: ${wethBalance}`) + + if (wethBalance < bridgeAmount && !options.dryRun) { + consola.error(`Insufficient WETH balance. Need at least 0.001 WETH.`) + process.exit(1) + } else if (options.dryRun && wethBalance < bridgeAmount) { + consola.warn( + `[DRY RUN] Insufficient WETH balance, but continuing for demo purposes` + ) + } + + // Check allowance for LiFi Diamond + const allowance = (await wethContract.read.allowance([ + walletAddress, + LIFI_DIAMOND_ARBITRUM, + ])) as bigint + consola.info(`Current allowance: ${allowance}`) + + if (allowance < bridgeAmount) { + consola.info('Approving WETH for LiFi Diamond...') + if (!options.dryRun) { + try { + const approveTx = await wethContract.write.approve([ + LIFI_DIAMOND_ARBITRUM as `0x${string}`, + BigInt( + '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' + ), // Max uint256 + ]) + consola.success(`Approval transaction sent: ${approveTx}`) + + // Wait for approval confirmation + consola.info('Waiting for approval confirmation...') + const approvalReceipt = await publicClient.waitForTransactionReceipt({ + hash: approveTx, + timeout: 60_000, // 60 seconds timeout + }) + + if (approvalReceipt.status === 'success') { + consola.success(`✅ Approval confirmed!`) + } else { + consola.error(`❌ Approval failed!`) + process.exit(1) + } + } catch (error) { + consola.error('Approval transaction failed:', error) + process.exit(1) + } + } else { + consola.info(`[DRY RUN] Would approve WETH for LiFi Diamond`) + } + } + + // Fetch cross-chain route with destination swap + consola.info('Fetching cross-chain route with destination swap...') + const route = await fetchCrossChainRoute( + bridgeAmount.toString(), + walletAddress + ) + + // Extract route details for our calldata construction + const routeDetails = extractRouteDetails(route) + + // Generate a transaction ID for the bridge + const transactionId = `0x${Date.now().toString(16).padStart(64, '0')}` + + // Create LiFiDexAggregator swap calldata using LiFi's calculated amounts + const swapCallData = createLiFiDexAggregatorSwapCallData( + routeDetails.swapFromAmount, + routeDetails.swapToAmountMin, + walletAddress + ) + + // Encode the destination call message for ReceiverAcrossV3 + const destinationCallMessage = encodeDestinationCallMessage( + transactionId, + swapCallData, + routeDetails.swapFromAmount, + walletAddress + ) + + // Construct our own bridge calldata using LiFi's route data + const bridgeCallData = constructBridgeCallData( + routeDetails, + destinationCallMessage, + walletAddress + ) + + // Log the route details + consola.success('Cross-chain route with destination swap ready!') + consola.info('✅ Bridge: AcrossV3 (WETH Arbitrum → Base)') + consola.info('✅ Destination swap: WETH → USDC on Base') + consola.info('✅ Final output: USDC to user wallet') + consola.info('✅ Calldata: Constructed using LiFi route optimization') + + // Log cost breakdown using LiFi's data + consola.info('💰 Cost breakdown (from LiFi route):') + consola.info(`- Bridge amount: ${bridgeAmount} wei (0.001 WETH)`) + consola.info(`- Expected bridge output: ${routeDetails.toAmount} wei WETH`) + consola.info(`- Expected swap output: ${routeDetails.finalAmount} USDC`) + consola.info( + `- Minimum swap output: ${routeDetails.finalAmountMin} USDC (with slippage)` + ) + consola.info(`- Estimated gas cost: $${routeDetails.gasCostUSD}`) + + consola.info('🔄 Cross-chain flow:') + consola.info('1. Bridge 0.001 WETH from Arbitrum → Base via AcrossV3') + consola.info( + `2. ReceiverAcrossV3 receives ~${routeDetails.toAmount} wei WETH on Base` + ) + consola.info( + '3. ReceiverAcrossV3 calls LiFiDexAggregator to swap WETH → USDC on Base' + ) + consola.info('4. Final USDC sent to user wallet') + + // Execute the cross-chain transaction + if (!options.dryRun) { + consola.info('Executing cross-chain bridge with destination swap...') + + try { + const txHash = await walletClient.sendTransaction({ + to: LIFI_DIAMOND_ARBITRUM as `0x${string}`, + data: bridgeCallData as `0x${string}`, + value: 0n, // No ETH value needed for WETH bridge + gas: 500000n, // Conservative gas limit + }) + + consola.success(`✅ Transaction sent: ${txHash}`) + consola.info('Waiting for transaction confirmation...') + + const receipt = await publicClient.waitForTransactionReceipt({ + hash: txHash, + timeout: 300_000, // 5 minutes timeout + }) + + if (receipt.status === 'success') { + consola.success( + `🎉 Cross-chain bridge with destination swap completed!` + ) + consola.info(`Transaction hash: ${txHash}`) + consola.info(`Block number: ${receipt.blockNumber}`) + consola.info(`Gas used: ${receipt.gasUsed}`) + consola.info('🔍 Check your Base wallet for USDC!') + } else { + consola.error(`❌ Transaction failed!`) + consola.info(`Transaction hash: ${txHash}`) + } + } catch (error) { + consola.error('Transaction execution failed:', error) + process.exit(1) + } + } else { + consola.info( + '[DRY RUN] Would execute cross-chain bridge with destination swap' + ) + consola.info(`[DRY RUN] Transaction data:`) + consola.info(`[DRY RUN] - To: ${LIFI_DIAMOND_ARBITRUM}`) + consola.info(`[DRY RUN] - Value: 0`) + consola.info(`[DRY RUN] - Gas limit: 500000`) + consola.info(`[DRY RUN] - Data length: ${bridgeCallData.length} characters`) + } +} + +// CLI command definition +const main = defineCommand({ + meta: { + name: 'demoPatcherDest', + description: + 'Demo cross-chain bridge with destination swap using AcrossV3 and LiFi Advanced Routes API', + }, + args: { + privateKey: { + type: 'string', + description: 'Private key for the wallet', + required: true, + }, + dryRun: { + type: 'boolean', + description: 'Perform a dry run without executing transactions', + default: false, + }, + }, + async run({ args }) { + if (!args.privateKey) { + consola.error('Private key is required') + process.exit(1) + } + + try { + await executeCrossChainBridgeWithSwap({ + privateKey: args.privateKey, + dryRun: args.dryRun, + }) + } catch (error) { + consola.error('Demo failed:', error) + process.exit(1) + } + }, +}) + +// Run the CLI +runMain(main) From f137d91f7e34b90a57ac47a72f441c4ed5c19dc0 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Tue, 27 May 2025 18:32:30 +0300 Subject: [PATCH 28/46] add patch calls to dest call --- deployments/_deployments_log_file.json | 14 + deployments/base.staging.json | 3 +- script/demoScripts/demoPatcherDest.ts | 384 +++++++++++++------------ 3 files changed, 221 insertions(+), 180 deletions(-) diff --git a/deployments/_deployments_log_file.json b/deployments/_deployments_log_file.json index 40bdd626a..4490da1ff 100644 --- a/deployments/_deployments_log_file.json +++ b/deployments/_deployments_log_file.json @@ -32907,6 +32907,20 @@ } ] } + }, + "base": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x433e2D04cC8c747aEe7c6Effa5bBA8399DE4b0F7", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-05-27 18:08:28", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "", + "VERIFIED": "true" + } + ] + } } } } diff --git a/deployments/base.staging.json b/deployments/base.staging.json index fd7b21858..0b462ea6a 100644 --- a/deployments/base.staging.json +++ b/deployments/base.staging.json @@ -1,3 +1,4 @@ { - "StandardizedCallFacet": "0x637Ac9AddC9C38b3F52878E11620a9060DC71d8B" + "StandardizedCallFacet": "0x637Ac9AddC9C38b3F52878E11620a9060DC71d8B", + "Patcher": "0x433e2D04cC8c747aEe7c6Effa5bBA8399DE4b0F7" } \ No newline at end of file diff --git a/script/demoScripts/demoPatcherDest.ts b/script/demoScripts/demoPatcherDest.ts index f78c66047..f7b012134 100644 --- a/script/demoScripts/demoPatcherDest.ts +++ b/script/demoScripts/demoPatcherDest.ts @@ -2,6 +2,7 @@ import { defineCommand, runMain } from 'citty' import { consola } from 'consola' +import { randomBytes } from 'crypto' import { createWalletClient, createPublicClient, @@ -11,50 +12,30 @@ import { type Hex, encodeFunctionData, encodeAbiParameters, + parseAbi, } from 'viem' import { privateKeyToAccount } from 'viem/accounts' import { arbitrum } from 'viem/chains' import baseDeployments from '../../deployments/base.json' +import baseStagingDeployments from '../../deployments/base.staging.json' import arbitrumStagingDeployments from '../../deployments/arbitrum.staging.json' // Contract addresses const ARBITRUM_WETH = '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1' const BASE_WETH = '0x4200000000000000000000000000000000000006' const BASE_USDC = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' -const BASE_LIFI_DEX_AGGREGATOR = baseDeployments.LiFiDEXAggregator as Hex +const BASE_LIFI_DEX_AGGREGATOR = '0x9Caab1750104147c9872772b3d0bE3D4290e1e86' // LiFiDEXAggregator on Base +const BASE_EXECUTOR = baseDeployments.Executor as Hex +const BASE_PATCHER = baseStagingDeployments.Patcher as Hex const RECEIVER_ACROSS_V3_BASE = baseDeployments.ReceiverAcrossV3 as Hex const LIFI_DIAMOND_ARBITRUM = arbitrumStagingDeployments.LiFiDiamond as Hex -// Simple ERC20 ABI -const ERC20_ABI = [ - { - inputs: [ - { name: 'owner', type: 'address' }, - { name: 'spender', type: 'address' }, - ], - name: 'allowance', - outputs: [{ name: '', type: 'uint256' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { name: 'spender', type: 'address' }, - { name: 'amount', type: 'uint256' }, - ], - name: 'approve', - outputs: [{ name: '', type: 'bool' }], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [{ name: 'account', type: 'address' }], - name: 'balanceOf', - outputs: [{ name: '', type: 'uint256' }], - stateMutability: 'view', - type: 'function', - }, -] as const +// Simple ERC20 ABI - Human readable format +const ERC20_ABI = parseAbi([ + 'function allowance(address owner, address spender) view returns (uint256)', + 'function approve(address spender, uint256 amount) returns (bool)', + 'function balanceOf(address account) view returns (uint256)', +]) /** * Fetch cross-chain route with destination swap using LiFi Advanced Routes API @@ -99,7 +80,7 @@ async function fetchCrossChainRoute( ], }, exchanges: { - allow: ['lifidexaggregator'], // Only allow LiFi DEX Aggregator + allow: ['lifidexaggregator'], }, }, } @@ -219,96 +200,169 @@ function extractRouteDetails(route: any) { } /** - * Create LiFiDexAggregator swap calldata using route details + * Encode destination call message for ReceiverAcrossV3 using Patcher pattern */ -function createLiFiDexAggregatorSwapCallData( +function encodeDestinationCallMessage( + transactionId: string, swapFromAmount: string, swapToAmountMin: string, - recipient: string + receiver: string ): string { - // LiFiDexAggregator swapTokensGeneric function - const lifiDexAggregatorAbi = [ - { - inputs: [ - { name: '_transactionId', type: 'bytes32' }, - { name: '_integrator', type: 'string' }, - { name: '_referrer', type: 'address' }, - { name: '_receiver', type: 'address' }, - { name: '_minAmount', type: 'uint256' }, - { - components: [ - { name: 'callTo', type: 'address' }, - { name: 'approveTo', type: 'address' }, - { name: 'sendingAssetId', type: 'address' }, - { name: 'receivingAssetId', type: 'address' }, - { name: 'fromAmount', type: 'uint256' }, - { name: 'callData', type: 'bytes' }, - { name: 'requiresDeposit', type: 'bool' }, - ], - name: '_swapData', - type: 'tuple[]', - }, - ], - name: 'swapTokensGeneric', - outputs: [], - stateMutability: 'payable', - type: 'function', - }, - ] as const + // 1. First call: Patcher calls WETH::transferFrom to pull WETH from Executor (with balance patching) + const transferFromCallData = encodeFunctionData({ + abi: parseAbi([ + 'function transferFrom(address from, address to, uint256 amount) returns (bool)', + ]), + functionName: 'transferFrom', + args: [ + BASE_EXECUTOR as `0x${string}`, // from - Executor contract + BASE_PATCHER as `0x${string}`, // to - Patcher contract + BigInt(swapFromAmount), // amount - This will be patched with Executor's actual balance + ], + }) - // For now, we'll create a simple direct swap calldata - // In a real implementation, you'd want to get the actual swap route from LiFi - const swapData = [ - { - callTo: BASE_LIFI_DEX_AGGREGATOR as `0x${string}`, - approveTo: BASE_LIFI_DEX_AGGREGATOR as `0x${string}`, - sendingAssetId: BASE_WETH as `0x${string}`, - receivingAssetId: BASE_USDC as `0x${string}`, - fromAmount: BigInt(swapFromAmount), - callData: '0x' as `0x${string}`, // Simplified - in practice you'd get this from LiFi API - requiresDeposit: true, - }, - ] + // Create value getter for Executor's WETH balance + const executorBalanceValueGetter = encodeFunctionData({ + abi: parseAbi([ + 'function balanceOf(address account) view returns (uint256)', + ]), + functionName: 'balanceOf', + args: [BASE_EXECUTOR as `0x${string}`], // Get balance of Executor contract + }) - const callData = encodeFunctionData({ - abi: lifiDexAggregatorAbi, - functionName: 'swapTokensGeneric', + // Calculate offset for amount parameter in transferFrom call + // transferFrom(address,address,uint256) - 4 bytes selector + 32 bytes from + 32 bytes to + 32 bytes amount = 100 bytes offset + const transferFromAmountOffset = 100n + + const patcherTransferCallData = encodeFunctionData({ + abi: parseAbi([ + 'function executeWithDynamicPatches(address valueSource, bytes valueGetter, address finalTarget, uint256 value, bytes data, uint256[] offsets, bool delegateCall) returns (bool success, bytes returnData)', + ]), + functionName: 'executeWithDynamicPatches', args: [ - `0x${Date.now().toString(16).padStart(64, '0')}` as `0x${string}`, // transactionId - 'lifi-demo', // integrator - '0x0000000000000000000000000000000000000000' as `0x${string}`, // referrer - recipient as `0x${string}`, // receiver - BigInt(swapToAmountMin), // minAmount - swapData, + BASE_WETH as `0x${string}`, // valueSource - WETH contract + executorBalanceValueGetter, // valueGetter - balanceOf(Executor) call + BASE_WETH as `0x${string}`, // finalTarget - WETH contract + 0n, // value - no ETH being sent + transferFromCallData, // data - transferFrom call + [transferFromAmountOffset], // offsets - position of amount parameter + false, // delegateCall - false for regular call ], }) - consola.info( - 'Created LiFiDexAggregator swap calldata using LiFi route parameters' - ) - return callData -} + // 2. Second call: Patcher calls WETH::approve to approve LiFiDexAggregator (with balance patching) + const approveCallData = encodeFunctionData({ + abi: parseAbi([ + 'function approve(address spender, uint256 amount) returns (bool)', + ]), + functionName: 'approve', + args: [ + BASE_LIFI_DEX_AGGREGATOR as `0x${string}`, // spender - LiFiDEXAggregator + BigInt(swapFromAmount), // amount - This will be patched with Patcher's actual balance + ], + }) -/** - * Encode destination call message for ReceiverAcrossV3 - */ -function encodeDestinationCallMessage( - transactionId: string, - swapCallData: string, - fromAmount: string, - receiver: string -): string { - // LibSwap.SwapData structure for the LiFiDexAggregator swap + // Create the value getter for Patcher's WETH balance + const patcherBalanceValueGetter = encodeFunctionData({ + abi: parseAbi([ + 'function balanceOf(address account) view returns (uint256)', + ]), + functionName: 'balanceOf', + args: [BASE_PATCHER as `0x${string}`], // Get balance of Patcher contract + }) + + // Calculate offset for amount parameter in approve call + // approve(address,uint256) - 4 bytes selector + 32 bytes spender + 32 bytes amount = 68 bytes offset + const approveAmountOffset = 68n + + const patcherApproveCallData = encodeFunctionData({ + abi: parseAbi([ + 'function executeWithDynamicPatches(address valueSource, bytes valueGetter, address finalTarget, uint256 value, bytes data, uint256[] offsets, bool delegateCall) returns (bool success, bytes returnData)', + ]), + functionName: 'executeWithDynamicPatches', + args: [ + BASE_WETH as `0x${string}`, // valueSource - WETH contract + patcherBalanceValueGetter, // valueGetter - balanceOf(Patcher) call + BASE_WETH as `0x${string}`, // finalTarget - WETH contract + 0n, // value - no ETH being sent + approveCallData, // data - approve call + [approveAmountOffset], // offsets - position of amount parameter + false, // delegateCall - false for regular call + ], + }) + + // 3. Third call: Patcher calls LiFiDEXAggregator::processRoute with dynamic balance + const routeData = + '0x02420000000000000000000000000000000000000601ffff0172ab388e2e2f6facef59e3c3fa2c4e29011c2d38014dac9d1769b9b304cb04741dcdeb2fc14abdf110000000000000000000000000000000000000000000000000000000000000000' as `0x${string}` + + const processRouteCallData = encodeFunctionData({ + abi: parseAbi([ + 'function processRoute(address tokenIn, uint256 amountIn, address tokenOut, uint256 amountOutMin, address to, bytes memory route) payable returns (uint256 amountOut)', + ]), + functionName: 'processRoute', + args: [ + BASE_WETH as `0x${string}`, // tokenIn - WETH on Base + BigInt(swapFromAmount), // amountIn - This will be patched with Patcher's actual balance + BASE_USDC as `0x${string}`, // tokenOut - USDC on Base + BigInt(swapToAmountMin), // amountOutMin - Minimum USDC out + receiver as `0x${string}`, // to - Final recipient + routeData, // route - Route data for WETH->USDC swap + ], + }) + + // Calculate offset for amountIn parameter in processRoute call + // processRoute(address,uint256,address,uint256,address,bytes) + // 4 bytes selector + 32 bytes tokenIn + 32 bytes amountIn = 68 bytes offset + const processRouteAmountOffset = 68n + + const patcherProcessRouteCallData = encodeFunctionData({ + abi: parseAbi([ + 'function executeWithDynamicPatches(address valueSource, bytes valueGetter, address finalTarget, uint256 value, bytes data, uint256[] offsets, bool delegateCall) returns (bool success, bytes returnData)', + ]), + functionName: 'executeWithDynamicPatches', + args: [ + BASE_WETH as `0x${string}`, // valueSource - WETH contract + patcherBalanceValueGetter, // valueGetter - balanceOf(Patcher) call + BASE_LIFI_DEX_AGGREGATOR as `0x${string}`, // finalTarget - LiFiDEXAggregator + 0n, // value - no ETH being sent + processRouteCallData, // data - processRoute call + [processRouteAmountOffset], // offsets - position of amountIn parameter + false, // delegateCall - false for regular call + ], + }) + + // Create LibSwap.SwapData structure with three patcher calls const swapData = [ + // Call 1: Patcher calls WETH::transferFrom to pull actual WETH balance from Executor { - callTo: BASE_LIFI_DEX_AGGREGATOR as `0x${string}`, // LiFiDexAggregator - approveTo: BASE_LIFI_DEX_AGGREGATOR as `0x${string}`, // Approve to the same aggregator + callTo: BASE_PATCHER as `0x${string}`, + approveTo: BASE_PATCHER as `0x${string}`, sendingAssetId: BASE_WETH as `0x${string}`, - receivingAssetId: BASE_USDC as `0x${string}`, - fromAmount: BigInt(fromAmount), - callData: swapCallData as `0x${string}`, + receivingAssetId: BASE_WETH as `0x${string}`, + fromAmount: BigInt(swapFromAmount), + callData: patcherTransferCallData as `0x${string}`, requiresDeposit: true, }, + // Call 2: Patcher calls WETH::approve to approve LiFiDexAggregator with actual balance + { + callTo: BASE_PATCHER as `0x${string}`, + approveTo: BASE_PATCHER as `0x${string}`, + sendingAssetId: BASE_WETH as `0x${string}`, + receivingAssetId: BASE_WETH as `0x${string}`, + fromAmount: 0n, // No amount for approval + callData: patcherApproveCallData as `0x${string}`, + requiresDeposit: false, + }, + // Call 3: Patcher calls LiFiDexAggregator::processRoute with actual balance + { + callTo: BASE_PATCHER as `0x${string}`, + approveTo: BASE_PATCHER as `0x${string}`, + sendingAssetId: BASE_WETH as `0x${string}`, + receivingAssetId: BASE_USDC as `0x${string}`, + fromAmount: 0n, // No amount for patcher call + callData: patcherProcessRouteCallData as `0x${string}`, + requiresDeposit: false, + }, ] // Encode the message payload for ReceiverAcrossV3.handleV3AcrossMessage @@ -333,7 +387,14 @@ function encodeDestinationCallMessage( [transactionId as `0x${string}`, swapData, receiver as `0x${string}`] ) - consola.info('Encoded destination call message for ReceiverAcrossV3') + consola.info('Encoded destination call message using Patcher pattern:') + consola.info( + '1. Patcher calls WETH::transferFrom with Executor balance patching' + ) + consola.info('2. Patcher calls WETH::approve with Patcher balance patching') + consola.info( + '3. Patcher calls LiFiDexAggregator::processRoute with Patcher balance patching' + ) return messagePayload } @@ -345,52 +406,13 @@ function constructBridgeCallData( destinationCallMessage: string, walletAddress: string ): string { - // ABI for startBridgeTokensViaAcrossV3 function - const acrossV3Abi = [ - { - inputs: [ - { - components: [ - { name: 'transactionId', type: 'bytes32' }, - { name: 'bridge', type: 'string' }, - { name: 'integrator', type: 'string' }, - { name: 'referrer', type: 'address' }, - { name: 'sendingAssetId', type: 'address' }, - { name: 'receiver', type: 'address' }, - { name: 'minAmount', type: 'uint256' }, - { name: 'destinationChainId', type: 'uint256' }, - { name: 'hasSourceSwaps', type: 'bool' }, - { name: 'hasDestinationCall', type: 'bool' }, - ], - name: '_bridgeData', - type: 'tuple', - }, - { - components: [ - { name: 'receiverAddress', type: 'address' }, - { name: 'refundAddress', type: 'address' }, - { name: 'receivingAssetId', type: 'address' }, - { name: 'outputAmount', type: 'uint256' }, - { name: 'outputAmountPercent', type: 'uint64' }, - { name: 'exclusiveRelayer', type: 'address' }, - { name: 'quoteTimestamp', type: 'uint32' }, - { name: 'fillDeadline', type: 'uint32' }, - { name: 'exclusivityDeadline', type: 'uint32' }, - { name: 'message', type: 'bytes' }, - ], - name: '_acrossData', - type: 'tuple', - }, - ], - name: 'startBridgeTokensViaAcrossV3', - outputs: [], - stateMutability: 'payable', - type: 'function', - }, - ] as const + // ABI for startBridgeTokensViaAcrossV3 function - Human readable format + const acrossV3Abi = parseAbi([ + 'function startBridgeTokensViaAcrossV3((bytes32 transactionId, string bridge, string integrator, address referrer, address sendingAssetId, address receiver, uint256 minAmount, uint256 destinationChainId, bool hasSourceSwaps, bool hasDestinationCall) _bridgeData, (address receiverAddress, address refundAddress, address receivingAssetId, uint256 outputAmount, uint64 outputAmountPercent, address exclusiveRelayer, uint32 quoteTimestamp, uint32 fillDeadline, uint32 exclusivityDeadline, bytes message) _acrossData) payable', + ]) // Generate a unique transaction ID - const transactionId = `0x${Date.now().toString(16).padStart(64, '0')}` + const transactionId = `0x${randomBytes(32).toString('hex')}` // Bridge data structure const bridgeData = { @@ -406,22 +428,17 @@ function constructBridgeCallData( hasDestinationCall: true, // Enable destination call } - // Calculate output amount percent based on LiFi's calculations - const outputAmountPercent = - (BigInt(routeDetails.toAmount) * BigInt('1000000000000000000')) / - BigInt(routeDetails.fromAmount) - - // Across data structure + // Across data structure - match successful transaction timing const acrossData = { receiverAddress: RECEIVER_ACROSS_V3_BASE as `0x${string}`, // ReceiverAcrossV3 refundAddress: walletAddress as `0x${string}`, receivingAssetId: BASE_WETH as `0x${string}`, outputAmount: BigInt(routeDetails.toAmount), // Use LiFi's calculated amount - outputAmountPercent: outputAmountPercent, // Calculated from LiFi data + outputAmountPercent: BigInt('960000000000000000'), // 96% (0.96e18) - optimized based on successful tx exclusiveRelayer: '0x0000000000000000000000000000000000000000' as `0x${string}`, - quoteTimestamp: Math.floor(Date.now() / 1000), - fillDeadline: Math.floor(Date.now() / 1000) + 3600, // 1 hour from now + quoteTimestamp: Math.floor(Date.now() / 1000), // Current timestamp + fillDeadline: Math.floor(Date.now() / 1000) + 7200, // 2 hours from now (more time) exclusivityDeadline: 0, message: destinationCallMessage as `0x${string}`, // Our encoded destination call } @@ -540,20 +557,13 @@ async function executeCrossChainBridgeWithSwap(options: { const routeDetails = extractRouteDetails(route) // Generate a transaction ID for the bridge - const transactionId = `0x${Date.now().toString(16).padStart(64, '0')}` - - // Create LiFiDexAggregator swap calldata using LiFi's calculated amounts - const swapCallData = createLiFiDexAggregatorSwapCallData( - routeDetails.swapFromAmount, - routeDetails.swapToAmountMin, - walletAddress - ) + const transactionId = `0x${randomBytes(32).toString('hex')}` - // Encode the destination call message for ReceiverAcrossV3 + // Encode the destination call message for ReceiverAcrossV3 using Patcher pattern const destinationCallMessage = encodeDestinationCallMessage( transactionId, - swapCallData, routeDetails.swapFromAmount, + routeDetails.swapToAmountMin, walletAddress ) @@ -567,9 +577,13 @@ async function executeCrossChainBridgeWithSwap(options: { // Log the route details consola.success('Cross-chain route with destination swap ready!') consola.info('✅ Bridge: AcrossV3 (WETH Arbitrum → Base)') - consola.info('✅ Destination swap: WETH → USDC on Base') + consola.info( + '✅ Destination swap: WETH → USDC on Base via Patcher + LiFiDEXAggregator' + ) consola.info('✅ Final output: USDC to user wallet') - consola.info('✅ Calldata: Constructed using LiFi route optimization') + consola.info( + '✅ Calldata: Constructed using Patcher pattern with dynamic balance patching' + ) // Log cost breakdown using LiFi's data consola.info('💰 Cost breakdown (from LiFi route):') @@ -581,15 +595,27 @@ async function executeCrossChainBridgeWithSwap(options: { ) consola.info(`- Estimated gas cost: $${routeDetails.gasCostUSD}`) - consola.info('🔄 Cross-chain flow:') + consola.info('🔄 Cross-chain flow with Patcher pattern:') consola.info('1. Bridge 0.001 WETH from Arbitrum → Base via AcrossV3') consola.info( - `2. ReceiverAcrossV3 receives ~${routeDetails.toAmount} wei WETH on Base` + `2. ReceiverAcrossV3.handleV3AcrossMessage receives ~${routeDetails.toAmount} wei WETH on Base` + ) + consola.info( + '3. ReceiverAcrossV3 calls Executor.swapAndCompleteBridgeTokens with 3 patcher calls:' + ) + consola.info( + ' a. Patcher calls WETH::transferFrom(Executor, Patcher, executorBalance) - Pull exact WETH amount' + ) + consola.info( + ' b. Patcher calls WETH::approve(LiFiDexAggregator, patcherBalance) - Approve exact amount' + ) + consola.info( + ' c. Patcher calls LiFiDexAggregator::processRoute(patcherBalance) - Swap exact amount' ) consola.info( - '3. ReceiverAcrossV3 calls LiFiDexAggregator to swap WETH → USDC on Base' + '4. All calls use dynamic balance patching to handle exact bridge amounts' ) - consola.info('4. Final USDC sent to user wallet') + consola.info('5. Final USDC sent to user wallet') // Execute the cross-chain transaction if (!options.dryRun) { @@ -613,7 +639,7 @@ async function executeCrossChainBridgeWithSwap(options: { if (receipt.status === 'success') { consola.success( - `🎉 Cross-chain bridge with destination swap completed!` + `🎉 Cross-chain bridge with destination swap completed using Patcher pattern!` ) consola.info(`Transaction hash: ${txHash}`) consola.info(`Block number: ${receipt.blockNumber}`) @@ -629,7 +655,7 @@ async function executeCrossChainBridgeWithSwap(options: { } } else { consola.info( - '[DRY RUN] Would execute cross-chain bridge with destination swap' + '[DRY RUN] Would execute cross-chain bridge with destination swap using Patcher pattern' ) consola.info(`[DRY RUN] Transaction data:`) consola.info(`[DRY RUN] - To: ${LIFI_DIAMOND_ARBITRUM}`) @@ -644,7 +670,7 @@ const main = defineCommand({ meta: { name: 'demoPatcherDest', description: - 'Demo cross-chain bridge with destination swap using AcrossV3 and LiFi Advanced Routes API', + 'Demo cross-chain bridge with destination swap using AcrossV3, Patcher pattern, and LiFi Advanced Routes API', }, args: { privateKey: { From 868d761f46b89f18812f5bbd7900d8b5639e3de4 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Wed, 28 May 2025 10:45:56 +0300 Subject: [PATCH 29/46] use offset finding utils --- script/demoScripts/demoPatcherDest.ts | 105 +++++++++++++++------ script/demoScripts/utils/cowSwapHelpers.ts | 38 +++++--- script/demoScripts/utils/patcher.ts | 35 +++++++ 3 files changed, 134 insertions(+), 44 deletions(-) create mode 100644 script/demoScripts/utils/patcher.ts diff --git a/script/demoScripts/demoPatcherDest.ts b/script/demoScripts/demoPatcherDest.ts index f7b012134..b7ad86558 100644 --- a/script/demoScripts/demoPatcherDest.ts +++ b/script/demoScripts/demoPatcherDest.ts @@ -19,6 +19,7 @@ import { arbitrum } from 'viem/chains' import baseDeployments from '../../deployments/base.json' import baseStagingDeployments from '../../deployments/base.staging.json' import arbitrumStagingDeployments from '../../deployments/arbitrum.staging.json' +import { findHexValueOccurrences } from './utils/patcher' // Contract addresses const ARBITRUM_WETH = '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1' @@ -208,7 +209,19 @@ function encodeDestinationCallMessage( swapToAmountMin: string, receiver: string ): string { - // 1. First call: Patcher calls WETH::transferFrom to pull WETH from Executor (with balance patching) + // Create value getter for Executor's WETH balance + const executorBalanceValueGetter = encodeFunctionData({ + abi: parseAbi([ + 'function balanceOf(address account) view returns (uint256)', + ]), + functionName: 'balanceOf', + args: [BASE_EXECUTOR as `0x${string}`], // Get balance of Executor contract + }) + + // Generate a random bytes32 hex value as needle to find the amount position + const transferAmountNeedle = `0x${randomBytes(32).toString('hex')}` + + // Create calldata with the needle in place of amount (will be patched by Patcher) const transferFromCallData = encodeFunctionData({ abi: parseAbi([ 'function transferFrom(address from, address to, uint256 amount) returns (bool)', @@ -217,22 +230,24 @@ function encodeDestinationCallMessage( args: [ BASE_EXECUTOR as `0x${string}`, // from - Executor contract BASE_PATCHER as `0x${string}`, // to - Patcher contract - BigInt(swapFromAmount), // amount - This will be patched with Executor's actual balance + transferAmountNeedle, // amount - needle value as hex string (will be patched) ], }) - // Create value getter for Executor's WETH balance - const executorBalanceValueGetter = encodeFunctionData({ - abi: parseAbi([ - 'function balanceOf(address account) view returns (uint256)', - ]), - functionName: 'balanceOf', - args: [BASE_EXECUTOR as `0x${string}`], // Get balance of Executor contract - }) + // Find the needle position in the calldata + const transferAmountPositions = findHexValueOccurrences( + transferFromCallData, + transferAmountNeedle + ) - // Calculate offset for amount parameter in transferFrom call - // transferFrom(address,address,uint256) - 4 bytes selector + 32 bytes from + 32 bytes to + 32 bytes amount = 100 bytes offset - const transferFromAmountOffset = 100n + if (transferAmountPositions.length === 0) { + throw new Error('Could not find transferFrom amount position in calldata') + } + + const transferFromAmountOffset = BigInt(transferAmountPositions[0]) + consola.info( + `Found transferFrom amount offset: ${transferFromAmountOffset} bytes` + ) const patcherTransferCallData = encodeFunctionData({ abi: parseAbi([ @@ -250,7 +265,19 @@ function encodeDestinationCallMessage( ], }) - // 2. Second call: Patcher calls WETH::approve to approve LiFiDexAggregator (with balance patching) + // Create the value getter for Patcher's WETH balance + const patcherBalanceValueGetter = encodeFunctionData({ + abi: parseAbi([ + 'function balanceOf(address account) view returns (uint256)', + ]), + functionName: 'balanceOf', + args: [BASE_PATCHER as `0x${string}`], // Get balance of Patcher contract + }) + + // Generate a random bytes32 hex value as needle to find the amount position + const approveAmountNeedle = `0x${randomBytes(32).toString('hex').slice(2)}` + + // Create calldata with the needle in place of amount (will be patched by Patcher) const approveCallData = encodeFunctionData({ abi: parseAbi([ 'function approve(address spender, uint256 amount) returns (bool)', @@ -258,22 +285,22 @@ function encodeDestinationCallMessage( functionName: 'approve', args: [ BASE_LIFI_DEX_AGGREGATOR as `0x${string}`, // spender - LiFiDEXAggregator - BigInt(swapFromAmount), // amount - This will be patched with Patcher's actual balance + approveAmountNeedle, // amount - needle value as hex string (will be patched) ], }) - // Create the value getter for Patcher's WETH balance - const patcherBalanceValueGetter = encodeFunctionData({ - abi: parseAbi([ - 'function balanceOf(address account) view returns (uint256)', - ]), - functionName: 'balanceOf', - args: [BASE_PATCHER as `0x${string}`], // Get balance of Patcher contract - }) + // Find the needle position in the calldata + const approveAmountPositions = findHexValueOccurrences( + approveCallData, + approveAmountNeedle + ) - // Calculate offset for amount parameter in approve call - // approve(address,uint256) - 4 bytes selector + 32 bytes spender + 32 bytes amount = 68 bytes offset - const approveAmountOffset = 68n + if (approveAmountPositions.length === 0) { + throw new Error('Could not find approve amount position in calldata') + } + + const approveAmountOffset = BigInt(approveAmountPositions[0]) + consola.info(`Found approve amount offset: ${approveAmountOffset} bytes`) const patcherApproveCallData = encodeFunctionData({ abi: parseAbi([ @@ -295,6 +322,12 @@ function encodeDestinationCallMessage( const routeData = '0x02420000000000000000000000000000000000000601ffff0172ab388e2e2f6facef59e3c3fa2c4e29011c2d38014dac9d1769b9b304cb04741dcdeb2fc14abdf110000000000000000000000000000000000000000000000000000000000000000' as `0x${string}` + // Generate a random bytes32 hex value as needle to find the amountIn position + const processRouteAmountNeedle = `0x${randomBytes(32) + .toString('hex') + .slice(2)}` + + // Create calldata with the needle in place of amountIn (will be patched by Patcher) const processRouteCallData = encodeFunctionData({ abi: parseAbi([ 'function processRoute(address tokenIn, uint256 amountIn, address tokenOut, uint256 amountOutMin, address to, bytes memory route) payable returns (uint256 amountOut)', @@ -302,7 +335,7 @@ function encodeDestinationCallMessage( functionName: 'processRoute', args: [ BASE_WETH as `0x${string}`, // tokenIn - WETH on Base - BigInt(swapFromAmount), // amountIn - This will be patched with Patcher's actual balance + processRouteAmountNeedle, // amountIn - needle value as hex string (will be patched) BASE_USDC as `0x${string}`, // tokenOut - USDC on Base BigInt(swapToAmountMin), // amountOutMin - Minimum USDC out receiver as `0x${string}`, // to - Final recipient @@ -310,10 +343,20 @@ function encodeDestinationCallMessage( ], }) - // Calculate offset for amountIn parameter in processRoute call - // processRoute(address,uint256,address,uint256,address,bytes) - // 4 bytes selector + 32 bytes tokenIn + 32 bytes amountIn = 68 bytes offset - const processRouteAmountOffset = 68n + // Find the needle position in the calldata + const processRouteAmountPositions = findHexValueOccurrences( + processRouteCallData, + processRouteAmountNeedle + ) + + if (processRouteAmountPositions.length === 0) { + throw new Error('Could not find processRoute amountIn position in calldata') + } + + const processRouteAmountOffset = BigInt(processRouteAmountPositions[0]) + consola.info( + `Found processRoute amountIn offset: ${processRouteAmountOffset} bytes` + ) const patcherProcessRouteCallData = encodeFunctionData({ abi: parseAbi([ diff --git a/script/demoScripts/utils/cowSwapHelpers.ts b/script/demoScripts/utils/cowSwapHelpers.ts index b0298a08a..2034e3a02 100644 --- a/script/demoScripts/utils/cowSwapHelpers.ts +++ b/script/demoScripts/utils/cowSwapHelpers.ts @@ -10,6 +10,7 @@ import { randomBytes } from 'crypto' import { ethers } from 'ethers' import { COW_SHED_FACTORY, COW_SHED_IMPLEMENTATION } from '@cowprotocol/cow-sdk' import { consola } from 'consola' +import { findHexValueOccurrences } from './patcher' const PROXY_CREATION_CODE = '0x60a034608e57601f61037138819003918201601f19168301916001600160401b038311848410176093578084926040948552833981010312608e57604b602060458360a9565b920160a9565b6080527f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc556040516102b490816100bd8239608051818181608f01526101720152f35b600080fd5b634e487b7160e01b600052604160045260246000fd5b51906001600160a01b0382168203608e5756fe60806040526004361015610018575b3661019457610194565b6000803560e01c908163025b22bc1461003b575063f851a4400361000e5761010d565b3461010a5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261010a5773ffffffffffffffffffffffffffffffffffffffff60043581811691828203610106577f0000000000000000000000000000000000000000000000000000000000000000163314600014610101577f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc557fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b8280a280f35b61023d565b8380fd5b80fd5b346101645760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610164576020610146610169565b73ffffffffffffffffffffffffffffffffffffffff60405191168152f35b600080fd5b333003610101577f000000000000000000000000000000000000000000000000000000000000000090565b60ff7f68df44b1011761f481358c0f49a711192727fb02c377d697bcb0ea8ff8393ac0541615806101ef575b1561023d5760046040517ff92ee8a9000000000000000000000000000000000000000000000000000000008152fd5b507f400ada75000000000000000000000000000000000000000000000000000000007fffffffff000000000000000000000000000000000000000000000000000000006000351614156101c0565b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc546000808092368280378136915af43d82803e1561027a573d90f35b3d90fdfea2646970667358221220c7c26ff3040b96a28e96d6d27b743972943aeaef81cc821544c5fe1e24f9b17264736f6c63430008190033' @@ -289,24 +290,35 @@ export async function setupCowShedPostHooks(config: CowShedPostHooksConfig) { signature: relayData.signature, } + // Generate a random bytes32 hex value as needle to find the minAmount position + const minAmountNeedle = `0x${randomBytes(32).toString('hex')}` + + // Create calldata with the needle in place of minAmount (will be patched by Patcher) + const bridgeDataWithNeedle = { + ...typedBridgeData, + minAmount: minAmountNeedle, // Pass as hex string directly + } + const relayCalldata = encodeFunctionData({ abi: relayFacetAbi, functionName: 'startBridgeTokensViaRelay', - args: [typedBridgeData, typedRelayData], + args: [bridgeDataWithNeedle, typedRelayData], }) - // Calculate the correct offset for the minAmount field in BridgeData - // For startBridgeTokensViaRelay(BridgeData calldata _bridgeData, RelayData calldata _relayData): - // - 4 bytes: function selector - // - 32 bytes: offset to _bridgeData struct (0x40 = 64) - // - 32 bytes: offset to _relayData struct - // - Then the actual BridgeData struct starts at offset 68 (4 + 64) - // - Within BridgeData: transactionId(32) + bridge(32) + integrator(32) + referrer(32) + sendingAssetId(32) + receiver(32) + minAmount(32) - // - So minAmount is at: 68 + 32*6 = 68 + 192 = 260 - // Based on the corrected calldata analysis with proper struct order: - // minAmount is at offset 260 - const minAmountOffset = 260n - consola.info(`Using calculated minAmount offset: ${minAmountOffset} bytes`) + // Find the needle position in the calldata + const needlePositions = findHexValueOccurrences( + relayCalldata, + minAmountNeedle + ) + + if (needlePositions.length === 0) { + throw new Error('Could not find minAmount position in calldata') + } + + const minAmountOffset = BigInt(needlePositions[0]) + consola.info( + `Found minAmount offset using dynamic search: ${minAmountOffset} bytes` + ) // Note: This offset is specifically for startBridgeTokensViaRelay function // Different bridge functions may have different offsets due to different parameter layouts diff --git a/script/demoScripts/utils/patcher.ts b/script/demoScripts/utils/patcher.ts new file mode 100644 index 000000000..f612f1ae0 --- /dev/null +++ b/script/demoScripts/utils/patcher.ts @@ -0,0 +1,35 @@ +/** + * Normalize calldata string by removing '0x' prefix if present + * @param calldata - The calldata string to normalize + * @returns Normalized calldata string without '0x' prefix + */ +export const normalizeCalldata = (calldata: string): string => { + return calldata.startsWith('0x') ? calldata.slice(2) : calldata +} + +/** + * Find hex value positions + */ +export const findHexValueOccurrences = ( + haystack: string, + needle: string +): readonly number[] => { + // Normalize both haystack and needle + const cleanHaystack = normalizeCalldata(haystack) + const cleanNeedle = normalizeCalldata(needle) + + const findRec = ( + startPos: number, + acc: readonly number[] + ): readonly number[] => { + const foundPos = cleanHaystack.indexOf(cleanNeedle, startPos) + + if (foundPos === -1) { + return acc + } + + const byteOffset = foundPos / 2 + return findRec(foundPos + cleanNeedle.length, [...acc, byteOffset]) + } + return findRec(0, []) +} From 33729fdb321bc1b0838f263932dba83c66967a09 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Wed, 28 May 2025 11:03:11 +0300 Subject: [PATCH 30/46] refactor --- script/demoScripts/demoPatcherDest.ts | 135 +++++++-------------- script/demoScripts/utils/cowSwapHelpers.ts | 54 +++------ script/demoScripts/utils/patcher.ts | 104 ++++++++++++++++ 3 files changed, 167 insertions(+), 126 deletions(-) diff --git a/script/demoScripts/demoPatcherDest.ts b/script/demoScripts/demoPatcherDest.ts index b7ad86558..5abc56aac 100644 --- a/script/demoScripts/demoPatcherDest.ts +++ b/script/demoScripts/demoPatcherDest.ts @@ -1,5 +1,3 @@ -#!/usr/bin/env bun - import { defineCommand, runMain } from 'citty' import { consola } from 'consola' import { randomBytes } from 'crypto' @@ -19,7 +17,12 @@ import { arbitrum } from 'viem/chains' import baseDeployments from '../../deployments/base.json' import baseStagingDeployments from '../../deployments/base.staging.json' import arbitrumStagingDeployments from '../../deployments/arbitrum.staging.json' -import { findHexValueOccurrences } from './utils/patcher' +import { + generateNeedle, + findNeedleOffset, + generateExecuteWithDynamicPatchesCalldata, + generateBalanceOfCalldata, +} from './utils/patcher' // Contract addresses const ARBITRUM_WETH = '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1' @@ -210,16 +213,10 @@ function encodeDestinationCallMessage( receiver: string ): string { // Create value getter for Executor's WETH balance - const executorBalanceValueGetter = encodeFunctionData({ - abi: parseAbi([ - 'function balanceOf(address account) view returns (uint256)', - ]), - functionName: 'balanceOf', - args: [BASE_EXECUTOR as `0x${string}`], // Get balance of Executor contract - }) + const executorBalanceValueGetter = generateBalanceOfCalldata(BASE_EXECUTOR) // Generate a random bytes32 hex value as needle to find the amount position - const transferAmountNeedle = `0x${randomBytes(32).toString('hex')}` + const transferAmountNeedle = generateNeedle() // Create calldata with the needle in place of amount (will be patched by Patcher) const transferFromCallData = encodeFunctionData({ @@ -230,52 +227,34 @@ function encodeDestinationCallMessage( args: [ BASE_EXECUTOR as `0x${string}`, // from - Executor contract BASE_PATCHER as `0x${string}`, // to - Patcher contract - transferAmountNeedle, // amount - needle value as hex string (will be patched) + transferAmountNeedle as any, // amount - needle value as hex string (will be patched) ], }) // Find the needle position in the calldata - const transferAmountPositions = findHexValueOccurrences( + const transferFromAmountOffset = findNeedleOffset( transferFromCallData, transferAmountNeedle ) - - if (transferAmountPositions.length === 0) { - throw new Error('Could not find transferFrom amount position in calldata') - } - - const transferFromAmountOffset = BigInt(transferAmountPositions[0]) consola.info( `Found transferFrom amount offset: ${transferFromAmountOffset} bytes` ) - const patcherTransferCallData = encodeFunctionData({ - abi: parseAbi([ - 'function executeWithDynamicPatches(address valueSource, bytes valueGetter, address finalTarget, uint256 value, bytes data, uint256[] offsets, bool delegateCall) returns (bool success, bytes returnData)', - ]), - functionName: 'executeWithDynamicPatches', - args: [ - BASE_WETH as `0x${string}`, // valueSource - WETH contract - executorBalanceValueGetter, // valueGetter - balanceOf(Executor) call - BASE_WETH as `0x${string}`, // finalTarget - WETH contract - 0n, // value - no ETH being sent - transferFromCallData, // data - transferFrom call - [transferFromAmountOffset], // offsets - position of amount parameter - false, // delegateCall - false for regular call - ], - }) + const patcherTransferCallData = generateExecuteWithDynamicPatchesCalldata( + BASE_WETH as `0x${string}`, // valueSource - WETH contract + executorBalanceValueGetter, // valueGetter - balanceOf(Executor) call + BASE_WETH as `0x${string}`, // finalTarget - WETH contract + transferFromCallData as `0x${string}`, // data - transferFrom call + [transferFromAmountOffset], // offsets - position of amount parameter + 0n, // value - no ETH being sent + false // delegateCall - false for regular call + ) // Create the value getter for Patcher's WETH balance - const patcherBalanceValueGetter = encodeFunctionData({ - abi: parseAbi([ - 'function balanceOf(address account) view returns (uint256)', - ]), - functionName: 'balanceOf', - args: [BASE_PATCHER as `0x${string}`], // Get balance of Patcher contract - }) + const patcherBalanceValueGetter = generateBalanceOfCalldata(BASE_PATCHER) // Generate a random bytes32 hex value as needle to find the amount position - const approveAmountNeedle = `0x${randomBytes(32).toString('hex').slice(2)}` + const approveAmountNeedle = generateNeedle() // Create calldata with the needle in place of amount (will be patched by Patcher) const approveCallData = encodeFunctionData({ @@ -285,47 +264,33 @@ function encodeDestinationCallMessage( functionName: 'approve', args: [ BASE_LIFI_DEX_AGGREGATOR as `0x${string}`, // spender - LiFiDEXAggregator - approveAmountNeedle, // amount - needle value as hex string (will be patched) + approveAmountNeedle as any, // amount - needle value as hex string (will be patched) ], }) // Find the needle position in the calldata - const approveAmountPositions = findHexValueOccurrences( + const approveAmountOffset = findNeedleOffset( approveCallData, approveAmountNeedle ) - - if (approveAmountPositions.length === 0) { - throw new Error('Could not find approve amount position in calldata') - } - - const approveAmountOffset = BigInt(approveAmountPositions[0]) consola.info(`Found approve amount offset: ${approveAmountOffset} bytes`) - const patcherApproveCallData = encodeFunctionData({ - abi: parseAbi([ - 'function executeWithDynamicPatches(address valueSource, bytes valueGetter, address finalTarget, uint256 value, bytes data, uint256[] offsets, bool delegateCall) returns (bool success, bytes returnData)', - ]), - functionName: 'executeWithDynamicPatches', - args: [ - BASE_WETH as `0x${string}`, // valueSource - WETH contract - patcherBalanceValueGetter, // valueGetter - balanceOf(Patcher) call - BASE_WETH as `0x${string}`, // finalTarget - WETH contract - 0n, // value - no ETH being sent - approveCallData, // data - approve call - [approveAmountOffset], // offsets - position of amount parameter - false, // delegateCall - false for regular call - ], - }) + const patcherApproveCallData = generateExecuteWithDynamicPatchesCalldata( + BASE_WETH as `0x${string}`, // valueSource - WETH contract + patcherBalanceValueGetter, // valueGetter - balanceOf(Patcher) call + BASE_WETH as `0x${string}`, // finalTarget - WETH contract + approveCallData as `0x${string}`, // data - approve call + [approveAmountOffset], // offsets - position of amount parameter + 0n, // value - no ETH being sent + false // delegateCall - false for regular call + ) // 3. Third call: Patcher calls LiFiDEXAggregator::processRoute with dynamic balance const routeData = '0x02420000000000000000000000000000000000000601ffff0172ab388e2e2f6facef59e3c3fa2c4e29011c2d38014dac9d1769b9b304cb04741dcdeb2fc14abdf110000000000000000000000000000000000000000000000000000000000000000' as `0x${string}` // Generate a random bytes32 hex value as needle to find the amountIn position - const processRouteAmountNeedle = `0x${randomBytes(32) - .toString('hex') - .slice(2)}` + const processRouteAmountNeedle = generateNeedle() // Create calldata with the needle in place of amountIn (will be patched by Patcher) const processRouteCallData = encodeFunctionData({ @@ -335,7 +300,7 @@ function encodeDestinationCallMessage( functionName: 'processRoute', args: [ BASE_WETH as `0x${string}`, // tokenIn - WETH on Base - processRouteAmountNeedle, // amountIn - needle value as hex string (will be patched) + processRouteAmountNeedle as any, // amountIn - needle value as hex string (will be patched) BASE_USDC as `0x${string}`, // tokenOut - USDC on Base BigInt(swapToAmountMin), // amountOutMin - Minimum USDC out receiver as `0x${string}`, // to - Final recipient @@ -344,35 +309,23 @@ function encodeDestinationCallMessage( }) // Find the needle position in the calldata - const processRouteAmountPositions = findHexValueOccurrences( + const processRouteAmountOffset = findNeedleOffset( processRouteCallData, processRouteAmountNeedle ) - - if (processRouteAmountPositions.length === 0) { - throw new Error('Could not find processRoute amountIn position in calldata') - } - - const processRouteAmountOffset = BigInt(processRouteAmountPositions[0]) consola.info( `Found processRoute amountIn offset: ${processRouteAmountOffset} bytes` ) - const patcherProcessRouteCallData = encodeFunctionData({ - abi: parseAbi([ - 'function executeWithDynamicPatches(address valueSource, bytes valueGetter, address finalTarget, uint256 value, bytes data, uint256[] offsets, bool delegateCall) returns (bool success, bytes returnData)', - ]), - functionName: 'executeWithDynamicPatches', - args: [ - BASE_WETH as `0x${string}`, // valueSource - WETH contract - patcherBalanceValueGetter, // valueGetter - balanceOf(Patcher) call - BASE_LIFI_DEX_AGGREGATOR as `0x${string}`, // finalTarget - LiFiDEXAggregator - 0n, // value - no ETH being sent - processRouteCallData, // data - processRoute call - [processRouteAmountOffset], // offsets - position of amountIn parameter - false, // delegateCall - false for regular call - ], - }) + const patcherProcessRouteCallData = generateExecuteWithDynamicPatchesCalldata( + BASE_WETH as `0x${string}`, // valueSource - WETH contract + patcherBalanceValueGetter, // valueGetter - balanceOf(Patcher) call + BASE_LIFI_DEX_AGGREGATOR as `0x${string}`, // finalTarget - LiFiDEXAggregator + processRouteCallData as `0x${string}`, // data - processRoute call + [processRouteAmountOffset], // offsets - position of amountIn parameter + 0n, // value - no ETH being sent + false // delegateCall - false for regular call + ) // Create LibSwap.SwapData structure with three patcher calls const swapData = [ diff --git a/script/demoScripts/utils/cowSwapHelpers.ts b/script/demoScripts/utils/cowSwapHelpers.ts index 2034e3a02..4ca457abb 100644 --- a/script/demoScripts/utils/cowSwapHelpers.ts +++ b/script/demoScripts/utils/cowSwapHelpers.ts @@ -10,7 +10,12 @@ import { randomBytes } from 'crypto' import { ethers } from 'ethers' import { COW_SHED_FACTORY, COW_SHED_IMPLEMENTATION } from '@cowprotocol/cow-sdk' import { consola } from 'consola' -import { findHexValueOccurrences } from './patcher' +import { + generateNeedle, + findNeedleOffset, + generateExecuteWithDynamicPatchesCalldata, + generateBalanceOfCalldata, +} from './patcher' const PROXY_CREATION_CODE = '0x60a034608e57601f61037138819003918201601f19168301916001600160401b038311848410176093578084926040948552833981010312608e57604b602060458360a9565b920160a9565b6080527f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc556040516102b490816100bd8239608051818181608f01526101720152f35b600080fd5b634e487b7160e01b600052604160045260246000fd5b51906001600160a01b0382168203608e5756fe60806040526004361015610018575b3661019457610194565b6000803560e01c908163025b22bc1461003b575063f851a4400361000e5761010d565b3461010a5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261010a5773ffffffffffffffffffffffffffffffffffffffff60043581811691828203610106577f0000000000000000000000000000000000000000000000000000000000000000163314600014610101577f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc557fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b8280a280f35b61023d565b8380fd5b80fd5b346101645760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610164576020610146610169565b73ffffffffffffffffffffffffffffffffffffffff60405191168152f35b600080fd5b333003610101577f000000000000000000000000000000000000000000000000000000000000000090565b60ff7f68df44b1011761f481358c0f49a711192727fb02c377d697bcb0ea8ff8393ac0541615806101ef575b1561023d5760046040517ff92ee8a9000000000000000000000000000000000000000000000000000000008152fd5b507f400ada75000000000000000000000000000000000000000000000000000000007fffffffff000000000000000000000000000000000000000000000000000000006000351614156101c0565b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc546000808092368280378136915af43d82803e1561027a573d90f35b3d90fdfea2646970667358221220c7c26ff3040b96a28e96d6d27b743972943aeaef81cc821544c5fe1e24f9b17264736f6c63430008190033' @@ -291,7 +296,7 @@ export async function setupCowShedPostHooks(config: CowShedPostHooksConfig) { } // Generate a random bytes32 hex value as needle to find the minAmount position - const minAmountNeedle = `0x${randomBytes(32).toString('hex')}` + const minAmountNeedle = generateNeedle() // Create calldata with the needle in place of minAmount (will be patched by Patcher) const bridgeDataWithNeedle = { @@ -302,20 +307,11 @@ export async function setupCowShedPostHooks(config: CowShedPostHooksConfig) { const relayCalldata = encodeFunctionData({ abi: relayFacetAbi, functionName: 'startBridgeTokensViaRelay', - args: [bridgeDataWithNeedle, typedRelayData], + args: [bridgeDataWithNeedle as any, typedRelayData], }) // Find the needle position in the calldata - const needlePositions = findHexValueOccurrences( - relayCalldata, - minAmountNeedle - ) - - if (needlePositions.length === 0) { - throw new Error('Could not find minAmount position in calldata') - } - - const minAmountOffset = BigInt(needlePositions[0]) + const minAmountOffset = findNeedleOffset(relayCalldata, minAmountNeedle) consola.info( `Found minAmount offset using dynamic search: ${minAmountOffset} bytes` ) @@ -324,30 +320,18 @@ export async function setupCowShedPostHooks(config: CowShedPostHooksConfig) { // Different bridge functions may have different offsets due to different parameter layouts // Encode the balanceOf call to get the USDC balance - const valueGetter = encodeFunctionData({ - abi: parseAbi([ - 'function balanceOf(address account) view returns (uint256)', - ]), - functionName: 'balanceOf', - args: [shedDeterministicAddress as `0x${string}`], - }) + const valueGetter = generateBalanceOfCalldata(shedDeterministicAddress) // Encode the patcher call - const patcherCalldata = encodeFunctionData({ - abi: parseAbi([ - 'function executeWithDynamicPatches(address valueSource, bytes valueGetter, address finalTarget, uint256 value, bytes data, uint256[] offsets, bool delegateCall) returns (bool success, bytes returnData)', - ]), - functionName: 'executeWithDynamicPatches', - args: [ - usdcAddress as `0x${string}`, // valueSource - USDC contract (single address) - valueGetter, // valueGetter - balanceOf call (single bytes) - lifiDiamondAddress as `0x${string}`, // finalTarget - LiFiDiamond contract - 0n, // value - no ETH being sent - relayCalldata, // data - the encoded RelayFacet call - [minAmountOffset], // offsets - Array with position of minAmount in the calldata - false, // delegateCall - ], - }) + const patcherCalldata = generateExecuteWithDynamicPatchesCalldata( + usdcAddress as `0x${string}`, // valueSource - USDC contract + valueGetter, // valueGetter - balanceOf call + lifiDiamondAddress as `0x${string}`, // finalTarget - LiFiDiamond contract + relayCalldata as `0x${string}`, // data - the encoded RelayFacet call + [minAmountOffset], // offsets - Array with position of minAmount in the calldata + 0n, // value - no ETH being sent + false // delegateCall + ) // Encode the USDC approval call for the DIAMOND address const approvalCalldata = encodeFunctionData({ diff --git a/script/demoScripts/utils/patcher.ts b/script/demoScripts/utils/patcher.ts index f612f1ae0..6ef1dd3ce 100644 --- a/script/demoScripts/utils/patcher.ts +++ b/script/demoScripts/utils/patcher.ts @@ -1,3 +1,6 @@ +import { randomBytes } from 'crypto' +import { encodeFunctionData, parseAbi, type Hex } from 'viem' + /** * Normalize calldata string by removing '0x' prefix if present * @param calldata - The calldata string to normalize @@ -33,3 +36,104 @@ export const findHexValueOccurrences = ( } return findRec(0, []) } + +/** + * Generate a random 32-byte hex needle for offset discovery + */ +export function generateNeedle(): Hex { + return `0x${randomBytes(32).toString('hex')}` as Hex +} + +/** + * Find offset of a needle in calldata + */ +export function findNeedleOffset(calldata: string, needle: Hex): bigint { + const positions = findHexValueOccurrences(calldata, needle) + + if (positions.length === 0) { + throw new Error(`Could not find needle ${needle} in calldata`) + } + + if (positions.length > 1) { + throw new Error( + `Found multiple occurrences of needle ${needle} in calldata` + ) + } + + return BigInt(positions[0]) +} + +/** + * Generate calldata for executeWithDynamicPatches + */ +export function generateExecuteWithDynamicPatchesCalldata( + valueSource: Hex, + valueGetter: Hex, + finalTarget: Hex, + targetCalldata: Hex, + offsets: bigint[], + value = 0n, + delegateCall = false +): Hex { + const patcherAbi = parseAbi([ + 'function executeWithDynamicPatches(address valueSource, bytes valueGetter, address finalTarget, uint256 value, bytes data, uint256[] offsets, bool delegateCall) returns (bool success, bytes returnData)', + ]) + + return encodeFunctionData({ + abi: patcherAbi, + functionName: 'executeWithDynamicPatches', + args: [ + valueSource, + valueGetter, + finalTarget, + value, + targetCalldata, + offsets, + delegateCall, + ], + }) as Hex +} + +/** + * Generate calldata for executeWithMultiplePatches + */ +export function generateExecuteWithMultiplePatchesCalldata( + valueSources: Hex[], + valueGetters: Hex[], + finalTarget: Hex, + targetCalldata: Hex, + offsetGroups: bigint[][], + value = 0n, + delegateCall = false +): Hex { + const patcherAbi = parseAbi([ + 'function executeWithMultiplePatches(address[] valueSources, bytes[] valueGetters, address finalTarget, uint256 value, bytes data, uint256[][] offsetGroups, bool delegateCall) returns (bool success, bytes returnData)', + ]) + + return encodeFunctionData({ + abi: patcherAbi, + functionName: 'executeWithMultiplePatches', + args: [ + valueSources, + valueGetters, + finalTarget, + value, + targetCalldata, + offsetGroups, + delegateCall, + ], + }) as Hex +} + +/** + * Generate balanceOf calldata for use as valueGetter + */ +export function generateBalanceOfCalldata(account: Hex): Hex { + return encodeFunctionData({ + abi: parseAbi([ + 'function balanceOf(address account) view returns (uint256)', + ]), + functionName: 'balanceOf', + args: [account], + }) as Hex +} From b8752a010d1c0378323f13433717f5036fe06945 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Tue, 3 Jun 2025 15:58:59 +0300 Subject: [PATCH 31/46] add deposit functions to patcher --- src/Periphery/Patcher.sol | 80 +++++++++ test/solidity/Periphery/Patcher.t.sol | 249 ++++++++++++++++++++++++++ 2 files changed, 329 insertions(+) diff --git a/src/Periphery/Patcher.sol b/src/Periphery/Patcher.sol index 8c967003a..6338196cf 100644 --- a/src/Periphery/Patcher.sol +++ b/src/Periphery/Patcher.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.17; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + /// @title Patcher /// @author LI.FI (https://li.fi) /// @notice A contract that patches calldata with dynamically retrieved values before execution @@ -134,6 +136,84 @@ contract Patcher { return _executeCall(finalTarget, value, patchedData, delegateCall); } + /// @notice Deposits tokens and retrieves a value dynamically to patch calldata before execution + /// @param tokenAddress The ERC20 token to transfer from msg.sender + /// @param valueSource The contract to query for the dynamic value + /// @param valueGetter The calldata to use to get the dynamic value (e.g., balanceOf call) + /// @param finalTarget The contract to call with the patched data + /// @param value The ETH value to send with the final call + /// @param data The original calldata to patch and execute + /// @param offsets Array of byte offsets in the original calldata to patch with the dynamic value + /// @param delegateCall If true, executes a delegatecall instead of a regular call for the final call + /// @return success Whether the final call was successful + /// @return returnData The data returned by the final call + function depositAndExecuteWithDynamicPatches( + address tokenAddress, + address valueSource, + bytes calldata valueGetter, + address finalTarget, + uint256 value, + bytes calldata data, + uint256[] calldata offsets, + bool delegateCall + ) external returns (bool success, bytes memory returnData) { + // Get the token balance of msg.sender + uint256 amount = IERC20(tokenAddress).balanceOf(msg.sender); + + // Transfer tokens from msg.sender to this contract + IERC20(tokenAddress).transferFrom(msg.sender, address(this), amount); + + return + _executeWithDynamicPatches( + valueSource, + valueGetter, + finalTarget, + value, + data, + offsets, + delegateCall + ); + } + + /// @notice Deposits tokens and retrieves multiple values dynamically to patch calldata at different offsets + /// @param tokenAddress The ERC20 token to transfer from msg.sender + /// @param valueSources Array of contracts to query for dynamic values + /// @param valueGetters Array of calldata to use to get each dynamic value + /// @param finalTarget The contract to call with the patched data + /// @param value The ETH value to send with the final call + /// @param data The original calldata to patch and execute + /// @param offsetGroups Array of offset arrays, each corresponding to a value source/getter pair + /// @param delegateCall If true, executes a delegatecall instead of a regular call for the final call + /// @return success Whether the final call was successful + /// @return returnData The data returned by the final call + function depositAndExecuteWithMultiplePatches( + address tokenAddress, + address[] calldata valueSources, + bytes[] calldata valueGetters, + address finalTarget, + uint256 value, + bytes calldata data, + uint256[][] calldata offsetGroups, + bool delegateCall + ) external returns (bool success, bytes memory returnData) { + // Get the token balance of msg.sender + uint256 amount = IERC20(tokenAddress).balanceOf(msg.sender); + + // Transfer tokens from msg.sender to this contract + IERC20(tokenAddress).transferFrom(msg.sender, address(this), amount); + + return + _executeWithMultiplePatches( + valueSources, + valueGetters, + finalTarget, + value, + data, + offsetGroups, + delegateCall + ); + } + /// @notice Retrieves multiple values dynamically and uses them to patch calldata at different offsets /// @param valueSources Array of contracts to query for dynamic values /// @param valueGetters Array of calldata to use to get each dynamic value diff --git a/test/solidity/Periphery/Patcher.t.sol b/test/solidity/Periphery/Patcher.t.sol index eec38c957..06f44f45e 100644 --- a/test/solidity/Periphery/Patcher.t.sol +++ b/test/solidity/Periphery/Patcher.t.sol @@ -995,4 +995,253 @@ contract PatcherTest is DSTest { return chainId; } + + // Test depositAndExecuteWithDynamicPatches success + function testDepositAndExecuteWithDynamicPatches_Success() public { + // Set up dynamic value + uint256 dynamicValue = 12345; + valueSource.setValue(dynamicValue); + + // Set up token balance for user + address user = address(0x1234); + uint256 tokenBalance = 1000 ether; + token.mint(user, tokenBalance); + + // Prepare calldata with placeholder value (0) + bytes memory originalCalldata = abi.encodeWithSelector( + target.processValue.selector, + uint256(0) // This will be patched + ); + + // Define offset where the value should be patched + uint256[] memory offsets = new uint256[](1); + offsets[0] = 4; // Skip 4-byte selector + + // Prepare value getter calldata + bytes memory valueGetter = abi.encodeWithSelector( + valueSource.getValue.selector + ); + + // Approve patcher to spend user's tokens + vm.prank(user); + token.approve(address(patcher), tokenBalance); + + // Expect the CallReceived event to be emitted with the patched value + vm.expectEmit(true, true, true, true, address(target)); + emit CallReceived(dynamicValue, address(patcher), 0); + + // Execute with dynamic patches as user + vm.prank(user); + patcher.depositAndExecuteWithDynamicPatches( + address(token), + address(valueSource), + valueGetter, + address(target), + 0, // no ETH value + originalCalldata, + offsets, + false // regular call, not delegatecall + ); + + // Verify execution was successful + assertEq(target.lastValue(), dynamicValue); + assertEq(target.lastSender(), address(patcher)); + assertEq(target.lastEthValue(), 0); + + // Verify tokens were transferred to patcher + assertEq(token.balanceOf(address(patcher)), tokenBalance); + assertEq(token.balanceOf(user), 0); + } + + // Test depositAndExecuteWithMultiplePatches success + function testDepositAndExecuteWithMultiplePatches_Success() public { + uint256 value1 = 11111; + uint256 value2 = 22222; + + // Set up two value sources + MockValueSource valueSource2 = new MockValueSource(); + valueSource.setValue(value1); + valueSource2.setValue(value2); + + // Set up token balance for user + address user = address(0x5678); + uint256 tokenBalance = 500 ether; + token.mint(user, tokenBalance); + + bytes memory originalCalldata = abi.encodeWithSelector( + target.processMultipleValues.selector, + uint256(0), // Will be patched with value1 + uint256(0) // Will be patched with value2 + ); + + // Set up arrays for multiple patches + address[] memory valueSources = new address[](2); + valueSources[0] = address(valueSource); + valueSources[1] = address(valueSource2); + + bytes[] memory valueGetters = new bytes[](2); + valueGetters[0] = abi.encodeWithSelector( + valueSource.getValue.selector + ); + valueGetters[1] = abi.encodeWithSelector( + valueSource2.getValue.selector + ); + + uint256[][] memory offsetGroups = new uint256[][](2); + offsetGroups[0] = new uint256[](1); + offsetGroups[0][0] = 4; // First parameter + offsetGroups[1] = new uint256[](1); + offsetGroups[1][0] = 36; // Second parameter + + // Approve patcher to spend user's tokens + vm.prank(user); + token.approve(address(patcher), tokenBalance); + + // Expect the CallReceived event to be emitted with the sum of both values + vm.expectEmit(true, true, true, true, address(target)); + emit CallReceived(value1 + value2, address(patcher), 0); + + // Execute with multiple patches as user + vm.prank(user); + patcher.depositAndExecuteWithMultiplePatches( + address(token), + valueSources, + valueGetters, + address(target), + 0, + originalCalldata, + offsetGroups, + false + ); + + // Verify execution was successful + assertEq(target.lastValue(), value1 + value2); + + // Verify tokens were transferred to patcher + assertEq(token.balanceOf(address(patcher)), tokenBalance); + assertEq(token.balanceOf(user), 0); + } + + // Test depositAndExecuteWithDynamicPatches with zero balance + function testDepositAndExecuteWithDynamicPatches_ZeroBalance() public { + uint256 dynamicValue = 12345; + valueSource.setValue(dynamicValue); + + address user = address(0x9999); + // User has zero token balance + + bytes memory originalCalldata = abi.encodeWithSelector( + target.processValue.selector, + uint256(0) + ); + + uint256[] memory offsets = new uint256[](1); + offsets[0] = 4; + + bytes memory valueGetter = abi.encodeWithSelector( + valueSource.getValue.selector + ); + + // Expect the CallReceived event to be emitted with the patched value + vm.expectEmit(true, true, true, true, address(target)); + emit CallReceived(dynamicValue, address(patcher), 0); + + // Execute with zero balance (should still work) + vm.prank(user); + patcher.depositAndExecuteWithDynamicPatches( + address(token), + address(valueSource), + valueGetter, + address(target), + 0, + originalCalldata, + offsets, + false + ); + + // Verify execution was successful + assertEq(target.lastValue(), dynamicValue); + + // Verify no tokens were transferred (user had zero balance) + assertEq(token.balanceOf(address(patcher)), 0); + assertEq(token.balanceOf(user), 0); + } + + // Test depositAndExecuteWithDynamicPatches without approval should fail + function testDepositAndExecuteWithDynamicPatches_NoApproval() public { + uint256 dynamicValue = 12345; + valueSource.setValue(dynamicValue); + + address user = address(0xABCD); + uint256 tokenBalance = 1000 ether; + token.mint(user, tokenBalance); + // Note: No approval given to patcher + + bytes memory originalCalldata = abi.encodeWithSelector( + target.processValue.selector, + uint256(0) + ); + + uint256[] memory offsets = new uint256[](1); + offsets[0] = 4; + + bytes memory valueGetter = abi.encodeWithSelector( + valueSource.getValue.selector + ); + + // Should revert due to insufficient allowance + vm.prank(user); + vm.expectRevert(); + patcher.depositAndExecuteWithDynamicPatches( + address(token), + address(valueSource), + valueGetter, + address(target), + 0, + originalCalldata, + offsets, + false + ); + } + + // Test depositAndExecuteWithDynamicPatches with partial approval + function testDepositAndExecuteWithDynamicPatches_PartialApproval() public { + uint256 dynamicValue = 12345; + valueSource.setValue(dynamicValue); + + address user = address(0xEF12); + uint256 tokenBalance = 1000 ether; + uint256 approvalAmount = 500 ether; // Less than balance + token.mint(user, tokenBalance); + + bytes memory originalCalldata = abi.encodeWithSelector( + target.processValue.selector, + uint256(0) + ); + + uint256[] memory offsets = new uint256[](1); + offsets[0] = 4; + + bytes memory valueGetter = abi.encodeWithSelector( + valueSource.getValue.selector + ); + + // Approve only partial amount + vm.prank(user); + token.approve(address(patcher), approvalAmount); + + // Should revert because it tries to transfer full balance but only partial approval + vm.prank(user); + vm.expectRevert(); + patcher.depositAndExecuteWithDynamicPatches( + address(token), + address(valueSource), + valueGetter, + address(target), + 0, + originalCalldata, + offsets, + false + ); + } } From 640dc7d9be12b78feef080a07962d9867b7a7ba6 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Tue, 3 Jun 2025 17:00:18 +0300 Subject: [PATCH 32/46] allow depositing to patcher --- deployments/_deployments_log_file.json | 4 +- deployments/base.staging.json | 2 +- script/demoScripts/demoPatcherDest.ts | 160 +++++----------------- script/demoScripts/demoPatcherDest_new.ts | 105 ++++++++++++++ src/Periphery/Patcher.sol | 6 + 5 files changed, 150 insertions(+), 127 deletions(-) create mode 100644 script/demoScripts/demoPatcherDest_new.ts diff --git a/deployments/_deployments_log_file.json b/deployments/_deployments_log_file.json index db67e7b19..1cf3d793f 100644 --- a/deployments/_deployments_log_file.json +++ b/deployments/_deployments_log_file.json @@ -32926,9 +32926,9 @@ "staging": { "1.0.0": [ { - "ADDRESS": "0x433e2D04cC8c747aEe7c6Effa5bBA8399DE4b0F7", + "ADDRESS": "0x18069208cA7c2D55aa0073E047dD45587B26F6D4", "OPTIMIZER_RUNS": "1000000", - "TIMESTAMP": "2025-05-27 18:08:28", + "TIMESTAMP": "2025-06-03 16:39:25", "CONSTRUCTOR_ARGS": "0x", "SALT": "", "VERIFIED": "true" diff --git a/deployments/base.staging.json b/deployments/base.staging.json index 0b462ea6a..5bd2aa114 100644 --- a/deployments/base.staging.json +++ b/deployments/base.staging.json @@ -1,4 +1,4 @@ { "StandardizedCallFacet": "0x637Ac9AddC9C38b3F52878E11620a9060DC71d8B", - "Patcher": "0x433e2D04cC8c747aEe7c6Effa5bBA8399DE4b0F7" + "Patcher": "0x18069208cA7c2D55aa0073E047dD45587B26F6D4" } \ No newline at end of file diff --git a/script/demoScripts/demoPatcherDest.ts b/script/demoScripts/demoPatcherDest.ts index 5abc56aac..5ce0a5d3f 100644 --- a/script/demoScripts/demoPatcherDest.ts +++ b/script/demoScripts/demoPatcherDest.ts @@ -212,87 +212,16 @@ function encodeDestinationCallMessage( swapToAmountMin: string, receiver: string ): string { - // Create value getter for Executor's WETH balance - const executorBalanceValueGetter = generateBalanceOfCalldata(BASE_EXECUTOR) - - // Generate a random bytes32 hex value as needle to find the amount position - const transferAmountNeedle = generateNeedle() - - // Create calldata with the needle in place of amount (will be patched by Patcher) - const transferFromCallData = encodeFunctionData({ - abi: parseAbi([ - 'function transferFrom(address from, address to, uint256 amount) returns (bool)', - ]), - functionName: 'transferFrom', - args: [ - BASE_EXECUTOR as `0x${string}`, // from - Executor contract - BASE_PATCHER as `0x${string}`, // to - Patcher contract - transferAmountNeedle as any, // amount - needle value as hex string (will be patched) - ], - }) - - // Find the needle position in the calldata - const transferFromAmountOffset = findNeedleOffset( - transferFromCallData, - transferAmountNeedle - ) - consola.info( - `Found transferFrom amount offset: ${transferFromAmountOffset} bytes` - ) - - const patcherTransferCallData = generateExecuteWithDynamicPatchesCalldata( - BASE_WETH as `0x${string}`, // valueSource - WETH contract - executorBalanceValueGetter, // valueGetter - balanceOf(Executor) call - BASE_WETH as `0x${string}`, // finalTarget - WETH contract - transferFromCallData as `0x${string}`, // data - transferFrom call - [transferFromAmountOffset], // offsets - position of amount parameter - 0n, // value - no ETH being sent - false // delegateCall - false for regular call - ) - - // Create the value getter for Patcher's WETH balance + // Create the value getter for Patcher's WETH balance (after deposit) const patcherBalanceValueGetter = generateBalanceOfCalldata(BASE_PATCHER) - // Generate a random bytes32 hex value as needle to find the amount position - const approveAmountNeedle = generateNeedle() - - // Create calldata with the needle in place of amount (will be patched by Patcher) - const approveCallData = encodeFunctionData({ - abi: parseAbi([ - 'function approve(address spender, uint256 amount) returns (bool)', - ]), - functionName: 'approve', - args: [ - BASE_LIFI_DEX_AGGREGATOR as `0x${string}`, // spender - LiFiDEXAggregator - approveAmountNeedle as any, // amount - needle value as hex string (will be patched) - ], - }) - - // Find the needle position in the calldata - const approveAmountOffset = findNeedleOffset( - approveCallData, - approveAmountNeedle - ) - consola.info(`Found approve amount offset: ${approveAmountOffset} bytes`) - - const patcherApproveCallData = generateExecuteWithDynamicPatchesCalldata( - BASE_WETH as `0x${string}`, // valueSource - WETH contract - patcherBalanceValueGetter, // valueGetter - balanceOf(Patcher) call - BASE_WETH as `0x${string}`, // finalTarget - WETH contract - approveCallData as `0x${string}`, // data - approve call - [approveAmountOffset], // offsets - position of amount parameter - 0n, // value - no ETH being sent - false // delegateCall - false for regular call - ) + // Generate needle for finding the amountIn position + const processRouteAmountNeedle = generateNeedle() - // 3. Third call: Patcher calls LiFiDEXAggregator::processRoute with dynamic balance + // Create processRoute calldata with needle const routeData = '0x02420000000000000000000000000000000000000601ffff0172ab388e2e2f6facef59e3c3fa2c4e29011c2d38014dac9d1769b9b304cb04741dcdeb2fc14abdf110000000000000000000000000000000000000000000000000000000000000000' as `0x${string}` - // Generate a random bytes32 hex value as needle to find the amountIn position - const processRouteAmountNeedle = generateNeedle() - - // Create calldata with the needle in place of amountIn (will be patched by Patcher) const processRouteCallData = encodeFunctionData({ abi: parseAbi([ 'function processRoute(address tokenIn, uint256 amountIn, address tokenOut, uint256 amountOutMin, address to, bytes memory route) payable returns (uint256 amountOut)', @@ -300,15 +229,15 @@ function encodeDestinationCallMessage( functionName: 'processRoute', args: [ BASE_WETH as `0x${string}`, // tokenIn - WETH on Base - processRouteAmountNeedle as any, // amountIn - needle value as hex string (will be patched) + processRouteAmountNeedle as any, // amountIn - needle value (will be patched) BASE_USDC as `0x${string}`, // tokenOut - USDC on Base BigInt(swapToAmountMin), // amountOutMin - Minimum USDC out - receiver as `0x${string}`, // to - Final recipient + BASE_EXECUTOR as `0x${string}`, // to - Executor contract on Base routeData, // route - Route data for WETH->USDC swap ], }) - // Find the needle position in the calldata + // Find the processRoute amount offset const processRouteAmountOffset = findNeedleOffset( processRouteCallData, processRouteAmountNeedle @@ -317,48 +246,36 @@ function encodeDestinationCallMessage( `Found processRoute amountIn offset: ${processRouteAmountOffset} bytes` ) - const patcherProcessRouteCallData = generateExecuteWithDynamicPatchesCalldata( - BASE_WETH as `0x${string}`, // valueSource - WETH contract - patcherBalanceValueGetter, // valueGetter - balanceOf(Patcher) call - BASE_LIFI_DEX_AGGREGATOR as `0x${string}`, // finalTarget - LiFiDEXAggregator - processRouteCallData as `0x${string}`, // data - processRoute call - [processRouteAmountOffset], // offsets - position of amountIn parameter - 0n, // value - no ETH being sent - false // delegateCall - false for regular call - ) + // Generate calldata for depositAndExecuteWithDynamicPatches + const depositAndExecuteCallData = encodeFunctionData({ + abi: parseAbi([ + 'function depositAndExecuteWithDynamicPatches(address tokenAddress, address valueSource, bytes calldata valueGetter, address finalTarget, uint256 value, bytes calldata data, uint256[] calldata offsets, bool delegateCall) returns (bool success, bytes memory returnData)', + ]), + functionName: 'depositAndExecuteWithDynamicPatches', + args: [ + BASE_WETH as `0x${string}`, // tokenAddress - WETH contract + BASE_WETH as `0x${string}`, // valueSource - WETH contract + patcherBalanceValueGetter, // valueGetter - balanceOf(Patcher) call + BASE_LIFI_DEX_AGGREGATOR as `0x${string}`, // finalTarget - LiFiDEXAggregator + 0n, // value - no ETH being sent + processRouteCallData as `0x${string}`, // data - processRoute call + [processRouteAmountOffset], // offsets - position of amountIn parameter + false, // delegateCall - false for regular call + ], + }) - // Create LibSwap.SwapData structure with three patcher calls + // Create LibSwap.SwapData structure with single patcher call const swapData = [ - // Call 1: Patcher calls WETH::transferFrom to pull actual WETH balance from Executor + // Single call: Patcher deposits WETH from Executor, approves LiFiDexAggregator, and executes swap { callTo: BASE_PATCHER as `0x${string}`, approveTo: BASE_PATCHER as `0x${string}`, sendingAssetId: BASE_WETH as `0x${string}`, - receivingAssetId: BASE_WETH as `0x${string}`, + receivingAssetId: BASE_USDC as `0x${string}`, fromAmount: BigInt(swapFromAmount), - callData: patcherTransferCallData as `0x${string}`, + callData: depositAndExecuteCallData as `0x${string}`, requiresDeposit: true, }, - // Call 2: Patcher calls WETH::approve to approve LiFiDexAggregator with actual balance - { - callTo: BASE_PATCHER as `0x${string}`, - approveTo: BASE_PATCHER as `0x${string}`, - sendingAssetId: BASE_WETH as `0x${string}`, - receivingAssetId: BASE_WETH as `0x${string}`, - fromAmount: 0n, // No amount for approval - callData: patcherApproveCallData as `0x${string}`, - requiresDeposit: false, - }, - // Call 3: Patcher calls LiFiDexAggregator::processRoute with actual balance - { - callTo: BASE_PATCHER as `0x${string}`, - approveTo: BASE_PATCHER as `0x${string}`, - sendingAssetId: BASE_WETH as `0x${string}`, - receivingAssetId: BASE_USDC as `0x${string}`, - fromAmount: 0n, // No amount for patcher call - callData: patcherProcessRouteCallData as `0x${string}`, - requiresDeposit: false, - }, ] // Encode the message payload for ReceiverAcrossV3.handleV3AcrossMessage @@ -383,13 +300,11 @@ function encodeDestinationCallMessage( [transactionId as `0x${string}`, swapData, receiver as `0x${string}`] ) - consola.info('Encoded destination call message using Patcher pattern:') consola.info( - '1. Patcher calls WETH::transferFrom with Executor balance patching' + 'Encoded destination call message using single Patcher deposit call:' ) - consola.info('2. Patcher calls WETH::approve with Patcher balance patching') consola.info( - '3. Patcher calls LiFiDexAggregator::processRoute with Patcher balance patching' + '1. Patcher deposits WETH from Executor, approves LiFiDexAggregator, and executes swap in one call' ) return messagePayload } @@ -591,22 +506,19 @@ async function executeCrossChainBridgeWithSwap(options: { ) consola.info(`- Estimated gas cost: $${routeDetails.gasCostUSD}`) - consola.info('🔄 Cross-chain flow with Patcher pattern:') + consola.info('🔄 Cross-chain flow with Patcher deposit pattern:') consola.info('1. Bridge 0.001 WETH from Arbitrum → Base via AcrossV3') consola.info( `2. ReceiverAcrossV3.handleV3AcrossMessage receives ~${routeDetails.toAmount} wei WETH on Base` ) consola.info( - '3. ReceiverAcrossV3 calls Executor.swapAndCompleteBridgeTokens with 3 patcher calls:' - ) - consola.info( - ' a. Patcher calls WETH::transferFrom(Executor, Patcher, executorBalance) - Pull exact WETH amount' + '3. ReceiverAcrossV3 calls Executor.swapAndCompleteBridgeTokens with 2 patcher calls:' ) consola.info( - ' b. Patcher calls WETH::approve(LiFiDexAggregator, patcherBalance) - Approve exact amount' + ' a. Patcher deposits WETH from Executor and approves LiFiDexAggregator with balance patching' ) consola.info( - ' c. Patcher calls LiFiDexAggregator::processRoute(patcherBalance) - Swap exact amount' + ' b. Patcher calls LiFiDexAggregator::processRoute(patcherBalance) - Swap exact amount' ) consola.info( '4. All calls use dynamic balance patching to handle exact bridge amounts' @@ -635,7 +547,7 @@ async function executeCrossChainBridgeWithSwap(options: { if (receipt.status === 'success') { consola.success( - `🎉 Cross-chain bridge with destination swap completed using Patcher pattern!` + `🎉 Cross-chain bridge with destination swap completed using Patcher deposit pattern!` ) consola.info(`Transaction hash: ${txHash}`) consola.info(`Block number: ${receipt.blockNumber}`) @@ -651,7 +563,7 @@ async function executeCrossChainBridgeWithSwap(options: { } } else { consola.info( - '[DRY RUN] Would execute cross-chain bridge with destination swap using Patcher pattern' + '[DRY RUN] Would execute cross-chain bridge with destination swap using Patcher deposit pattern' ) consola.info(`[DRY RUN] Transaction data:`) consola.info(`[DRY RUN] - To: ${LIFI_DIAMOND_ARBITRUM}`) diff --git a/script/demoScripts/demoPatcherDest_new.ts b/script/demoScripts/demoPatcherDest_new.ts new file mode 100644 index 000000000..d317e7ad4 --- /dev/null +++ b/script/demoScripts/demoPatcherDest_new.ts @@ -0,0 +1,105 @@ +/** + * Encode destination call message for ReceiverAcrossV3 using Patcher pattern + */ +function encodeDestinationCallMessage( + transactionId: string, + swapFromAmount: string, + swapToAmountMin: string, + receiver: string +): string { + // Create the value getter for Patcher's WETH balance (after deposit) + const patcherBalanceValueGetter = generateBalanceOfCalldata(BASE_PATCHER) + + // Generate needle for finding the amountIn position + const processRouteAmountNeedle = generateNeedle() + + // Create processRoute calldata with needle + const routeData = + '0x02420000000000000000000000000000000000000601ffff0172ab388e2e2f6facef59e3c3fa2c4e29011c2d38014dac9d1769b9b304cb04741dcdeb2fc14abdf110000000000000000000000000000000000000000000000000000000000000000' as `0x${string}` + + const processRouteCallData = encodeFunctionData({ + abi: parseAbi([ + 'function processRoute(address tokenIn, uint256 amountIn, address tokenOut, uint256 amountOutMin, address to, bytes memory route) payable returns (uint256 amountOut)', + ]), + functionName: 'processRoute', + args: [ + BASE_WETH as `0x${string}`, // tokenIn - WETH on Base + processRouteAmountNeedle as any, // amountIn - needle value (will be patched) + BASE_USDC as `0x${string}`, // tokenOut - USDC on Base + BigInt(swapToAmountMin), // amountOutMin - Minimum USDC out + receiver as `0x${string}`, // to - Final recipient + routeData, // route - Route data for WETH->USDC swap + ], + }) + + // Find the processRoute amount offset + const processRouteAmountOffset = findNeedleOffset( + processRouteCallData, + processRouteAmountNeedle + ) + consola.info( + `Found processRoute amountIn offset: ${processRouteAmountOffset} bytes` + ) + + // Generate calldata for depositAndExecuteWithDynamicPatches + const depositAndExecuteCallData = encodeFunctionData({ + abi: parseAbi([ + 'function depositAndExecuteWithDynamicPatches(address tokenAddress, address valueSource, bytes calldata valueGetter, address finalTarget, uint256 value, bytes calldata data, uint256[] calldata offsets, bool delegateCall) returns (bool success, bytes memory returnData)', + ]), + functionName: 'depositAndExecuteWithDynamicPatches', + args: [ + BASE_WETH as `0x${string}`, // tokenAddress - WETH contract + BASE_WETH as `0x${string}`, // valueSource - WETH contract + patcherBalanceValueGetter, // valueGetter - balanceOf(Patcher) call + BASE_LIFI_DEX_AGGREGATOR as `0x${string}`, // finalTarget - LiFiDEXAggregator + 0n, // value - no ETH being sent + processRouteCallData as `0x${string}`, // data - processRoute call + [processRouteAmountOffset], // offsets - position of amountIn parameter + false, // delegateCall - false for regular call + ], + }) + + // Create LibSwap.SwapData structure with single patcher call + const swapData = [ + // Single call: Patcher deposits WETH from Executor, approves LiFiDexAggregator, and executes swap + { + callTo: BASE_PATCHER as `0x${string}`, + approveTo: BASE_PATCHER as `0x${string}`, + sendingAssetId: BASE_WETH as `0x${string}`, + receivingAssetId: BASE_USDC as `0x${string}`, + fromAmount: BigInt(swapFromAmount), + callData: depositAndExecuteCallData as `0x${string}`, + requiresDeposit: true, + }, + ] + + // Encode the message payload for ReceiverAcrossV3.handleV3AcrossMessage + const messagePayload = encodeAbiParameters( + [ + { name: 'transactionId', type: 'bytes32' }, + { + name: 'swapData', + type: 'tuple[]', + components: [ + { name: 'callTo', type: 'address' }, + { name: 'approveTo', type: 'address' }, + { name: 'sendingAssetId', type: 'address' }, + { name: 'receivingAssetId', type: 'address' }, + { name: 'fromAmount', type: 'uint256' }, + { name: 'callData', type: 'bytes' }, + { name: 'requiresDeposit', type: 'bool' }, + ], + }, + { name: 'receiver', type: 'address' }, + ], + [transactionId as `0x${string}`, swapData, receiver as `0x${string}`] + ) + + consola.info( + 'Encoded destination call message using single Patcher deposit call:' + ) + consola.info( + '1. Patcher deposits WETH from Executor, approves LiFiDexAggregator, and executes swap in one call' + ) + return messagePayload +} diff --git a/src/Periphery/Patcher.sol b/src/Periphery/Patcher.sol index 6338196cf..81994ccf9 100644 --- a/src/Periphery/Patcher.sol +++ b/src/Periphery/Patcher.sol @@ -163,6 +163,9 @@ contract Patcher { // Transfer tokens from msg.sender to this contract IERC20(tokenAddress).transferFrom(msg.sender, address(this), amount); + // Approve the finalTarget to spend the deposited tokens + IERC20(tokenAddress).approve(finalTarget, amount); + return _executeWithDynamicPatches( valueSource, @@ -202,6 +205,9 @@ contract Patcher { // Transfer tokens from msg.sender to this contract IERC20(tokenAddress).transferFrom(msg.sender, address(this), amount); + // Approve the finalTarget to spend the deposited tokens + IERC20(tokenAddress).approve(finalTarget, amount); + return _executeWithMultiplePatches( valueSources, From 4cb800273e837d3a050f7cf07f8abbd3a5968416 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Tue, 3 Jun 2025 17:58:55 +0300 Subject: [PATCH 33/46] fix route --- script/demoScripts/demoPatcherDest.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/script/demoScripts/demoPatcherDest.ts b/script/demoScripts/demoPatcherDest.ts index 5ce0a5d3f..38a960d99 100644 --- a/script/demoScripts/demoPatcherDest.ts +++ b/script/demoScripts/demoPatcherDest.ts @@ -220,7 +220,7 @@ function encodeDestinationCallMessage( // Create processRoute calldata with needle const routeData = - '0x02420000000000000000000000000000000000000601ffff0172ab388e2e2f6facef59e3c3fa2c4e29011c2d38014dac9d1769b9b304cb04741dcdeb2fc14abdf110000000000000000000000000000000000000000000000000000000000000000' as `0x${string}` + '0x02420000000000000000000000000000000000000601ffff0172ab388e2e2f6facef59e3c3fa2c4e29011c2d38014dac9d1769b9b304cb04741dcdeb2fc14abdf11' as `0x${string}` const processRouteCallData = encodeFunctionData({ abi: parseAbi([ @@ -534,7 +534,7 @@ async function executeCrossChainBridgeWithSwap(options: { to: LIFI_DIAMOND_ARBITRUM as `0x${string}`, data: bridgeCallData as `0x${string}`, value: 0n, // No ETH value needed for WETH bridge - gas: 500000n, // Conservative gas limit + gas: 1000000n, // Increased gas limit for complex operations }) consola.success(`✅ Transaction sent: ${txHash}`) From 6bacd74cb5030351f0c16d1b3b8d79e47fa819a1 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Tue, 3 Jun 2025 18:17:52 +0300 Subject: [PATCH 34/46] tweaks --- script/demoScripts/demoPatcherDest.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/script/demoScripts/demoPatcherDest.ts b/script/demoScripts/demoPatcherDest.ts index 38a960d99..4b48241b6 100644 --- a/script/demoScripts/demoPatcherDest.ts +++ b/script/demoScripts/demoPatcherDest.ts @@ -333,7 +333,7 @@ function constructBridgeCallData( referrer: '0x0000000000000000000000000000000000000000' as `0x${string}`, sendingAssetId: ARBITRUM_WETH as `0x${string}`, receiver: RECEIVER_ACROSS_V3_BASE as `0x${string}`, // ReceiverAcrossV3 - minAmount: BigInt(routeDetails.toAmountMin), // Use LiFi's calculated minimum + minAmount: BigInt(routeDetails.toAmount), // Use same amount as outputAmount for consistency destinationChainId: 8453n, // Base chain ID hasSourceSwaps: false, hasDestinationCall: true, // Enable destination call @@ -345,7 +345,7 @@ function constructBridgeCallData( refundAddress: walletAddress as `0x${string}`, receivingAssetId: BASE_WETH as `0x${string}`, outputAmount: BigInt(routeDetails.toAmount), // Use LiFi's calculated amount - outputAmountPercent: BigInt('960000000000000000'), // 96% (0.96e18) - optimized based on successful tx + outputAmountPercent: BigInt('920000000000000000'), // 92% (0.92e18) - increased fee from 4% to 8% for higher gas costs exclusiveRelayer: '0x0000000000000000000000000000000000000000' as `0x${string}`, quoteTimestamp: Math.floor(Date.now() / 1000), // Current timestamp @@ -534,7 +534,7 @@ async function executeCrossChainBridgeWithSwap(options: { to: LIFI_DIAMOND_ARBITRUM as `0x${string}`, data: bridgeCallData as `0x${string}`, value: 0n, // No ETH value needed for WETH bridge - gas: 1000000n, // Increased gas limit for complex operations + gas: 800000n, // Increased gas limit for complex operations }) consola.success(`✅ Transaction sent: ${txHash}`) From 4dd901b9f2966f8f226589b16cc1dea5572c47ce Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Wed, 4 Jun 2025 13:06:09 +0300 Subject: [PATCH 35/46] fix scripts --- deployments/_deployments_log_file.json | 4 ++-- deployments/arbitrum.staging.json | 2 +- script/demoScripts/demoPatcherDest.ts | 15 +++++++++++---- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/deployments/_deployments_log_file.json b/deployments/_deployments_log_file.json index 1cf3d793f..d39393668 100644 --- a/deployments/_deployments_log_file.json +++ b/deployments/_deployments_log_file.json @@ -32912,9 +32912,9 @@ "staging": { "1.0.0": [ { - "ADDRESS": "0xE65b50EcF482f97f53557f0E02946aa27f8839EC", + "ADDRESS": "0x18069208cA7c2D55aa0073E047dD45587B26F6D4", "OPTIMIZER_RUNS": "1000000", - "TIMESTAMP": "2025-05-08 17:01:23", + "TIMESTAMP": "2025-06-04 12:55:52", "CONSTRUCTOR_ARGS": "0x", "SALT": "", "VERIFIED": "true" diff --git a/deployments/arbitrum.staging.json b/deployments/arbitrum.staging.json index 293771e0d..7c84793fd 100644 --- a/deployments/arbitrum.staging.json +++ b/deployments/arbitrum.staging.json @@ -54,5 +54,5 @@ "GlacisFacet": "0xF82830B952Bc60b93206FA22f1cD4770cedb2840", "GasZipFacet": "0x37f3F3E9d909fB1163448C511193b8481e541C62", "ChainflipFacet": "0xa884c21873A671bD010567cf97c937b153F842Cc", - "Patcher": "0xE65b50EcF482f97f53557f0E02946aa27f8839EC" + "Patcher": "0x18069208cA7c2D55aa0073E047dD45587B26F6D4" } \ No newline at end of file diff --git a/script/demoScripts/demoPatcherDest.ts b/script/demoScripts/demoPatcherDest.ts index 4b48241b6..3d643e6ec 100644 --- a/script/demoScripts/demoPatcherDest.ts +++ b/script/demoScripts/demoPatcherDest.ts @@ -339,13 +339,15 @@ function constructBridgeCallData( hasDestinationCall: true, // Enable destination call } - // Across data structure - match successful transaction timing + // Across data structure - ensure relayer gets meaningful fee const acrossData = { receiverAddress: RECEIVER_ACROSS_V3_BASE as `0x${string}`, // ReceiverAcrossV3 refundAddress: walletAddress as `0x${string}`, receivingAssetId: BASE_WETH as `0x${string}`, - outputAmount: BigInt(routeDetails.toAmount), // Use LiFi's calculated amount - outputAmountPercent: BigInt('920000000000000000'), // 92% (0.92e18) - increased fee from 4% to 8% for higher gas costs + outputAmount: + (BigInt(routeDetails.toAmount) * BigInt('980000000000000000')) / + BigInt('1000000000000000000'), // 98% of input amount + outputAmountPercent: BigInt('980000000000000000'), // 98% (0.98e18) - 2% fee for relayer to ensure pickup exclusiveRelayer: '0x0000000000000000000000000000000000000000' as `0x${string}`, quoteTimestamp: Math.floor(Date.now() / 1000), // Current timestamp @@ -470,10 +472,15 @@ async function executeCrossChainBridgeWithSwap(options: { // Generate a transaction ID for the bridge const transactionId = `0x${randomBytes(32).toString('hex')}` + // Calculate the actual amount that will be received after relayer fee + const actualReceivedAmount = + (BigInt(routeDetails.toAmount) * BigInt('980000000000000000')) / + BigInt('1000000000000000000') + // Encode the destination call message for ReceiverAcrossV3 using Patcher pattern const destinationCallMessage = encodeDestinationCallMessage( transactionId, - routeDetails.swapFromAmount, + actualReceivedAmount.toString(), // Use actual received amount instead of original bridge amount routeDetails.swapToAmountMin, walletAddress ) From c9d4bfaf8e2f72f15e4e11be903e8ac4fc3cda4f Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Thu, 5 Jun 2025 16:50:54 +0300 Subject: [PATCH 36/46] remove --- script/demoScripts/demoPatcherDest_new.ts | 105 ---------------------- 1 file changed, 105 deletions(-) delete mode 100644 script/demoScripts/demoPatcherDest_new.ts diff --git a/script/demoScripts/demoPatcherDest_new.ts b/script/demoScripts/demoPatcherDest_new.ts deleted file mode 100644 index d317e7ad4..000000000 --- a/script/demoScripts/demoPatcherDest_new.ts +++ /dev/null @@ -1,105 +0,0 @@ -/** - * Encode destination call message for ReceiverAcrossV3 using Patcher pattern - */ -function encodeDestinationCallMessage( - transactionId: string, - swapFromAmount: string, - swapToAmountMin: string, - receiver: string -): string { - // Create the value getter for Patcher's WETH balance (after deposit) - const patcherBalanceValueGetter = generateBalanceOfCalldata(BASE_PATCHER) - - // Generate needle for finding the amountIn position - const processRouteAmountNeedle = generateNeedle() - - // Create processRoute calldata with needle - const routeData = - '0x02420000000000000000000000000000000000000601ffff0172ab388e2e2f6facef59e3c3fa2c4e29011c2d38014dac9d1769b9b304cb04741dcdeb2fc14abdf110000000000000000000000000000000000000000000000000000000000000000' as `0x${string}` - - const processRouteCallData = encodeFunctionData({ - abi: parseAbi([ - 'function processRoute(address tokenIn, uint256 amountIn, address tokenOut, uint256 amountOutMin, address to, bytes memory route) payable returns (uint256 amountOut)', - ]), - functionName: 'processRoute', - args: [ - BASE_WETH as `0x${string}`, // tokenIn - WETH on Base - processRouteAmountNeedle as any, // amountIn - needle value (will be patched) - BASE_USDC as `0x${string}`, // tokenOut - USDC on Base - BigInt(swapToAmountMin), // amountOutMin - Minimum USDC out - receiver as `0x${string}`, // to - Final recipient - routeData, // route - Route data for WETH->USDC swap - ], - }) - - // Find the processRoute amount offset - const processRouteAmountOffset = findNeedleOffset( - processRouteCallData, - processRouteAmountNeedle - ) - consola.info( - `Found processRoute amountIn offset: ${processRouteAmountOffset} bytes` - ) - - // Generate calldata for depositAndExecuteWithDynamicPatches - const depositAndExecuteCallData = encodeFunctionData({ - abi: parseAbi([ - 'function depositAndExecuteWithDynamicPatches(address tokenAddress, address valueSource, bytes calldata valueGetter, address finalTarget, uint256 value, bytes calldata data, uint256[] calldata offsets, bool delegateCall) returns (bool success, bytes memory returnData)', - ]), - functionName: 'depositAndExecuteWithDynamicPatches', - args: [ - BASE_WETH as `0x${string}`, // tokenAddress - WETH contract - BASE_WETH as `0x${string}`, // valueSource - WETH contract - patcherBalanceValueGetter, // valueGetter - balanceOf(Patcher) call - BASE_LIFI_DEX_AGGREGATOR as `0x${string}`, // finalTarget - LiFiDEXAggregator - 0n, // value - no ETH being sent - processRouteCallData as `0x${string}`, // data - processRoute call - [processRouteAmountOffset], // offsets - position of amountIn parameter - false, // delegateCall - false for regular call - ], - }) - - // Create LibSwap.SwapData structure with single patcher call - const swapData = [ - // Single call: Patcher deposits WETH from Executor, approves LiFiDexAggregator, and executes swap - { - callTo: BASE_PATCHER as `0x${string}`, - approveTo: BASE_PATCHER as `0x${string}`, - sendingAssetId: BASE_WETH as `0x${string}`, - receivingAssetId: BASE_USDC as `0x${string}`, - fromAmount: BigInt(swapFromAmount), - callData: depositAndExecuteCallData as `0x${string}`, - requiresDeposit: true, - }, - ] - - // Encode the message payload for ReceiverAcrossV3.handleV3AcrossMessage - const messagePayload = encodeAbiParameters( - [ - { name: 'transactionId', type: 'bytes32' }, - { - name: 'swapData', - type: 'tuple[]', - components: [ - { name: 'callTo', type: 'address' }, - { name: 'approveTo', type: 'address' }, - { name: 'sendingAssetId', type: 'address' }, - { name: 'receivingAssetId', type: 'address' }, - { name: 'fromAmount', type: 'uint256' }, - { name: 'callData', type: 'bytes' }, - { name: 'requiresDeposit', type: 'bool' }, - ], - }, - { name: 'receiver', type: 'address' }, - ], - [transactionId as `0x${string}`, swapData, receiver as `0x${string}`] - ) - - consola.info( - 'Encoded destination call message using single Patcher deposit call:' - ) - consola.info( - '1. Patcher deposits WETH from Executor, approves LiFiDexAggregator, and executes swap in one call' - ) - return messagePayload -} From f364cecdcd92f3943c923a431de2c650aa1cbaba Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Thu, 5 Jun 2025 17:16:12 +0300 Subject: [PATCH 37/46] clarifying comments --- test/solidity/Periphery/Patcher.t.sol | 307 +++++++++----------------- 1 file changed, 106 insertions(+), 201 deletions(-) diff --git a/test/solidity/Periphery/Patcher.t.sol b/test/solidity/Periphery/Patcher.t.sol index 06f44f45e..c5979bc13 100644 --- a/test/solidity/Periphery/Patcher.t.sol +++ b/test/solidity/Periphery/Patcher.t.sol @@ -3,20 +3,18 @@ pragma solidity ^0.8.17; import { DSTest } from "ds-test/test.sol"; import { Vm } from "forge-std/Vm.sol"; -import { Patcher } from "lifi/Periphery/Patcher.sol"; +import { Patcher } from "../../../src/Periphery/Patcher.sol"; import { TestToken as ERC20 } from "../utils/TestToken.sol"; -import { ILiFi } from "lifi/Interfaces/ILiFi.sol"; -import { RelayFacet } from "lifi/Facets/RelayFacet.sol"; -import { LibAsset } from "lifi/Libraries/LibAsset.sol"; -import { LibAllowList } from "lifi/Libraries/LibAllowList.sol"; +import { ILiFi } from "../../../src/Interfaces/ILiFi.sol"; +import { RelayFacet } from "../../../src/Facets/RelayFacet.sol"; +import { LibAsset } from "../../../src/Libraries/LibAsset.sol"; +import { LibAllowList } from "../../../src/Libraries/LibAllowList.sol"; -// Custom errors for gas optimization error MockFailure(); error TargetFailure(); error OracleFailure(); error PriceNotSet(); -// Mock contract that returns dynamic values contract MockValueSource { uint256 public value; bool public shouldFail; @@ -54,7 +52,6 @@ contract MockValueSource { } } -// Mock target contract for testing calls contract MockTarget { uint256 public lastValue; address public lastSender; @@ -109,9 +106,8 @@ contract MockTarget { } } -// Mock price oracle for calculating dynamic minimum amounts contract MockPriceOracle { - mapping(address => uint256) public prices; // Price in USD with 18 decimals + mapping(address => uint256) public prices; bool public shouldFail; function setPrice(address token, uint256 price) external { @@ -129,11 +125,10 @@ contract MockPriceOracle { return prices[token]; } - // Calculate minimum amount with slippage protection function calculateMinAmount( address token, uint256 amount, - uint256 slippageBps // basis points (e.g., 300 = 3%) + uint256 slippageBps ) external view returns (uint256) { if (shouldFail) { revert OracleFailure(); @@ -143,12 +138,10 @@ contract MockPriceOracle { revert PriceNotSet(); } - // Apply slippage: minAmount = amount * (10000 - slippageBps) / 10000 return (amount * (10000 - slippageBps)) / 10000; } } -// Test RelayFacet Contract contract TestRelayFacet is RelayFacet { constructor( address _relayReceiver, @@ -165,10 +158,8 @@ contract TestRelayFacet is RelayFacet { } contract PatcherTest is DSTest { - // solhint-disable immutable-vars-naming - Vm internal immutable vm = Vm(HEVM_ADDRESS); + Vm internal immutable VM = Vm(HEVM_ADDRESS); - // Events for testing event CallReceived(uint256 value, address sender, uint256 ethValue); event LiFiTransferStarted(ILiFi.BridgeData bridgeData); @@ -179,74 +170,64 @@ contract PatcherTest is DSTest { MockPriceOracle internal priceOracle; TestRelayFacet internal relayFacet; - // RelayFacet setup variables address internal constant RELAY_RECEIVER = 0xa5F565650890fBA1824Ee0F21EbBbF660a179934; uint256 internal privateKey = 0x1234567890; address internal relaySolver; function setUp() public { - // Set up our test contracts patcher = new Patcher(); valueSource = new MockValueSource(); target = new MockTarget(); token = new ERC20("Test Token", "TEST", 18); priceOracle = new MockPriceOracle(); - // Set up real RelayFacet for testing - relaySolver = vm.addr(privateKey); + relaySolver = VM.addr(privateKey); relayFacet = new TestRelayFacet(RELAY_RECEIVER, relaySolver); } - // Test successful single patch execution + // Tests basic single value patching into calldata function testExecuteWithDynamicPatches_Success() public { - // Set up dynamic value uint256 dynamicValue = 12345; valueSource.setValue(dynamicValue); - // Prepare calldata with placeholder value (0) bytes memory originalCalldata = abi.encodeWithSelector( target.processValue.selector, - uint256(0) // This will be patched + uint256(0) ); - // Define offset where the value should be patched (after selector, at parameter position) uint256[] memory offsets = new uint256[](1); - offsets[0] = 4; // Skip 4-byte selector + offsets[0] = 4; - // Prepare value getter calldata bytes memory valueGetter = abi.encodeWithSelector( valueSource.getValue.selector ); - // Expect the CallReceived event to be emitted with the patched value - vm.expectEmit(true, true, true, true, address(target)); + VM.expectEmit(true, true, true, true, address(target)); emit CallReceived(dynamicValue, address(patcher), 0); - // Execute with dynamic patches patcher.executeWithDynamicPatches( address(valueSource), valueGetter, address(target), - 0, // no ETH value + 0, originalCalldata, offsets, - false // regular call, not delegatecall + false ); - // Verify execution was successful assertEq(target.lastValue(), dynamicValue); assertEq(target.lastSender(), address(patcher)); assertEq(target.lastEthValue(), 0); } - // Test successful execution with ETH value + // Tests patching with ETH value transfer function testExecuteWithDynamicPatches_WithEthValue() public { uint256 dynamicValue = 54321; uint256 ethValue = 1 ether; valueSource.setValue(dynamicValue); - vm.deal(address(patcher), ethValue); + VM.deal(address(patcher), ethValue); bytes memory originalCalldata = abi.encodeWithSelector( target.processValue.selector, @@ -260,8 +241,7 @@ contract PatcherTest is DSTest { valueSource.getValue.selector ); - // Expect the CallReceived event to be emitted with the patched value and ETH - vm.expectEmit(true, true, true, true, address(target)); + VM.expectEmit(true, true, true, true, address(target)); emit CallReceived(dynamicValue, address(patcher), ethValue); patcher.executeWithDynamicPatches( @@ -278,28 +258,26 @@ contract PatcherTest is DSTest { assertEq(target.lastEthValue(), ethValue); } - // Test multiple patches with same value + // Tests patching same value to multiple positions in calldata function testExecuteWithDynamicPatches_MultipleOffsets() public { uint256 dynamicValue = 98765; valueSource.setValue(dynamicValue); - // Calldata with two parameters that should both be patched with the same value bytes memory originalCalldata = abi.encodeWithSelector( target.processMultipleValues.selector, - uint256(0), // First parameter to patch - uint256(0) // Second parameter to patch + uint256(0), + uint256(0) ); uint256[] memory offsets = new uint256[](2); - offsets[0] = 4; // First parameter offset - offsets[1] = 36; // Second parameter offset (4 + 32) + offsets[0] = 4; + offsets[1] = 36; bytes memory valueGetter = abi.encodeWithSelector( valueSource.getValue.selector ); - // Expect the CallReceived event to be emitted with the sum of both values - vm.expectEmit(true, true, true, true, address(target)); + VM.expectEmit(true, true, true, true, address(target)); emit CallReceived(dynamicValue * 2, address(patcher), 0); patcher.executeWithDynamicPatches( @@ -312,26 +290,24 @@ contract PatcherTest is DSTest { false ); - assertEq(target.lastValue(), dynamicValue * 2); // Sum of both values + assertEq(target.lastValue(), dynamicValue * 2); } - // Test multiple patches with different values + // Tests patching different values from different sources function testExecuteWithMultiplePatches_Success() public { uint256 value1 = 11111; uint256 value2 = 22222; - // Set up two value sources MockValueSource valueSource2 = new MockValueSource(); valueSource.setValue(value1); valueSource2.setValue(value2); bytes memory originalCalldata = abi.encodeWithSelector( target.processMultipleValues.selector, - uint256(0), // Will be patched with value1 - uint256(0) // Will be patched with value2 + uint256(0), + uint256(0) ); - // Set up arrays for multiple patches address[] memory valueSources = new address[](2); valueSources[0] = address(valueSource); valueSources[1] = address(valueSource2); @@ -346,12 +322,11 @@ contract PatcherTest is DSTest { uint256[][] memory offsetGroups = new uint256[][](2); offsetGroups[0] = new uint256[](1); - offsetGroups[0][0] = 4; // First parameter + offsetGroups[0][0] = 4; offsetGroups[1] = new uint256[](1); - offsetGroups[1][0] = 36; // Second parameter + offsetGroups[1][0] = 36; - // Expect the CallReceived event to be emitted with the sum of both values - vm.expectEmit(true, true, true, true, address(target)); + VM.expectEmit(true, true, true, true, address(target)); emit CallReceived(value1 + value2, address(patcher), 0); patcher.executeWithMultiplePatches( @@ -367,7 +342,7 @@ contract PatcherTest is DSTest { assertEq(target.lastValue(), value1 + value2); } - // Test delegatecall execution + // Tests delegatecall execution mode function testExecuteWithDynamicPatches_Delegatecall() public { uint256 dynamicValue = 77777; valueSource.setValue(dynamicValue); @@ -391,14 +366,11 @@ contract PatcherTest is DSTest { 0, originalCalldata, offsets, - true // delegatecall + true ); - - // Note: In delegatecall, the target's storage won't be modified - // but the call should still succeed } - // Test error when getting dynamic value fails + // Tests oracle/source failure handling function testExecuteWithDynamicPatches_FailedToGetDynamicValue() public { valueSource.setShouldFail(true); @@ -414,7 +386,7 @@ contract PatcherTest is DSTest { valueSource.getValue.selector ); - vm.expectRevert(Patcher.FailedToGetDynamicValue.selector); + VM.expectRevert(Patcher.FailedToGetDynamicValue.selector); patcher.executeWithDynamicPatches( address(valueSource), valueGetter, @@ -426,7 +398,7 @@ contract PatcherTest is DSTest { ); } - // Test error when patch offset is invalid + // Tests invalid offset bounds checking function testExecuteWithDynamicPatches_InvalidPatchOffset() public { uint256 dynamicValue = 12345; valueSource.setValue(dynamicValue); @@ -437,13 +409,13 @@ contract PatcherTest is DSTest { ); uint256[] memory offsets = new uint256[](1); - offsets[0] = originalCalldata.length; // Invalid offset (beyond data length) + offsets[0] = originalCalldata.length; bytes memory valueGetter = abi.encodeWithSelector( valueSource.getValue.selector ); - vm.expectRevert(Patcher.InvalidPatchOffset.selector); + VM.expectRevert(Patcher.InvalidPatchOffset.selector); patcher.executeWithDynamicPatches( address(valueSource), valueGetter, @@ -455,13 +427,13 @@ contract PatcherTest is DSTest { ); } - // Test error when arrays have mismatched lengths + // Tests input validation for array length mismatches function testExecuteWithMultiplePatches_MismatchedArrayLengths() public { address[] memory valueSources = new address[](2); valueSources[0] = address(valueSource); valueSources[1] = address(valueSource); - bytes[] memory valueGetters = new bytes[](1); // Mismatched length + bytes[] memory valueGetters = new bytes[](1); valueGetters[0] = abi.encodeWithSelector( valueSource.getValue.selector ); @@ -475,7 +447,7 @@ contract PatcherTest is DSTest { uint256(0) ); - vm.expectRevert(Patcher.MismatchedArrayLengths.selector); + VM.expectRevert(Patcher.MismatchedArrayLengths.selector); patcher.executeWithMultiplePatches( valueSources, valueGetters, @@ -487,32 +459,28 @@ contract PatcherTest is DSTest { ); } - // Test complex scenario with token balance patching + // Tests ERC20 balance patching in realistic scenario function testExecuteWithDynamicPatches_TokenBalance() public { - // Mint tokens to an account address holder = address(0x1234); uint256 balance = 1000 ether; token.mint(holder, balance); - // Prepare calldata that uses the token balance bytes memory originalCalldata = abi.encodeWithSelector( target.processComplexData.selector, - uint256(0), // amount - will be patched with balance + uint256(0), address(token), block.timestamp + 1 hours ); uint256[] memory offsets = new uint256[](1); - offsets[0] = 4; // Patch the amount parameter + offsets[0] = 4; - // Use balanceOf call to get dynamic value bytes memory valueGetter = abi.encodeWithSelector( token.balanceOf.selector, holder ); - // Expect the CallReceived event to be emitted with the patched balance - vm.expectEmit(true, true, true, true, address(target)); + VM.expectEmit(true, true, true, true, address(target)); emit CallReceived( balance + block.timestamp + 1 hours, address(patcher), @@ -532,7 +500,7 @@ contract PatcherTest is DSTest { assertEq(target.lastValue(), balance + block.timestamp + 1 hours); } - // Test that target call failure is properly handled + // Tests target contract failure handling function testExecuteWithDynamicPatches_TargetCallFailure() public { uint256 dynamicValue = 12345; valueSource.setValue(dynamicValue); @@ -561,30 +529,27 @@ contract PatcherTest is DSTest { false ); - // The patcher should return false for failed calls, not revert assertTrue(!success); - // Return data should contain the revert reason assertTrue(returnData.length > 0); } - // Test edge case with empty offsets array + // Tests no-op patching with empty offsets function testExecuteWithDynamicPatches_EmptyOffsets() public { uint256 dynamicValue = 12345; valueSource.setValue(dynamicValue); bytes memory originalCalldata = abi.encodeWithSelector( target.processValue.selector, - uint256(99999) // This value should remain unchanged + uint256(99999) ); - uint256[] memory offsets = new uint256[](0); // Empty offsets + uint256[] memory offsets = new uint256[](0); bytes memory valueGetter = abi.encodeWithSelector( valueSource.getValue.selector ); - // Expect the CallReceived event to be emitted with the original value (no patching) - vm.expectEmit(true, true, true, true, address(target)); + VM.expectEmit(true, true, true, true, address(target)); emit CallReceived(99999, address(patcher), 0); patcher.executeWithDynamicPatches( @@ -597,10 +562,10 @@ contract PatcherTest is DSTest { false ); - assertEq(target.lastValue(), 99999); // Original value should be preserved + assertEq(target.lastValue(), 99999); } - // Test multiple patches on the same offset (should overwrite) + // Tests overwriting same position with multiple patches function testExecuteWithMultiplePatches_SameOffset() public { uint256 value1 = 11111; uint256 value2 = 22222; @@ -628,12 +593,11 @@ contract PatcherTest is DSTest { uint256[][] memory offsetGroups = new uint256[][](2); offsetGroups[0] = new uint256[](1); - offsetGroups[0][0] = 4; // Same offset + offsetGroups[0][0] = 4; offsetGroups[1] = new uint256[](1); - offsetGroups[1][0] = 4; // Same offset (should overwrite) + offsetGroups[1][0] = 4; - // Expect the CallReceived event to be emitted with the last written value - vm.expectEmit(true, true, true, true, address(target)); + VM.expectEmit(true, true, true, true, address(target)); emit CallReceived(value2, address(patcher), 0); patcher.executeWithMultiplePatches( @@ -646,17 +610,17 @@ contract PatcherTest is DSTest { false ); - assertEq(target.lastValue(), value2); // Should have the last written value + assertEq(target.lastValue(), value2); } - // Test with zero value + // Tests zero value patching edge case function testExecuteWithDynamicPatches_ZeroValue() public { uint256 dynamicValue = 0; valueSource.setValue(dynamicValue); bytes memory originalCalldata = abi.encodeWithSelector( target.processValue.selector, - uint256(12345) // Will be overwritten with 0 + uint256(12345) ); uint256[] memory offsets = new uint256[](1); @@ -666,8 +630,7 @@ contract PatcherTest is DSTest { valueSource.getValue.selector ); - // Expect the CallReceived event to be emitted with the zero value - vm.expectEmit(true, true, true, true, address(target)); + VM.expectEmit(true, true, true, true, address(target)); emit CallReceived(0, address(patcher), 0); patcher.executeWithDynamicPatches( @@ -683,7 +646,7 @@ contract PatcherTest is DSTest { assertEq(target.lastValue(), 0); } - // Test with maximum uint256 value + // Tests maximum uint256 value patching edge case function testExecuteWithDynamicPatches_MaxValue() public { uint256 dynamicValue = type(uint256).max; valueSource.setValue(dynamicValue); @@ -700,8 +663,7 @@ contract PatcherTest is DSTest { valueSource.getValue.selector ); - // Expect the CallReceived event to be emitted with the max value - vm.expectEmit(true, true, true, true, address(target)); + VM.expectEmit(true, true, true, true, address(target)); emit CallReceived(type(uint256).max, address(patcher), 0); patcher.executeWithDynamicPatches( @@ -717,14 +679,12 @@ contract PatcherTest is DSTest { assertEq(target.lastValue(), type(uint256).max); } - // Test realistic BridgeData minAmount patching with price oracle using real RelayFacet + // Tests price oracle integration with RelayFacet for dynamic minAmount function testExecuteWithDynamicPatches_RelayFacetMinAmount() public { - // Set up token price and slippage - uint256 tokenPrice = 2000 * 1e18; // $2000 per token - uint256 slippageBps = 300; // 3% slippage + uint256 tokenPrice = 2000 * 1e18; + uint256 slippageBps = 300; priceOracle.setPrice(address(token), tokenPrice); - // Create BridgeData with placeholder minAmount (0) ILiFi.BridgeData memory bridgeData = ILiFi.BridgeData({ transactionId: bytes32("test-tx-id"), bridge: "relay", @@ -732,13 +692,12 @@ contract PatcherTest is DSTest { referrer: address(0x1234), sendingAssetId: address(token), receiver: address(0x5678), - minAmount: 0, // This will be patched - destinationChainId: 8453, // Base + minAmount: 0, + destinationChainId: 8453, hasSourceSwaps: false, hasDestinationCall: false }); - // Create RelayData RelayFacet.RelayData memory relayData = RelayFacet.RelayData({ requestId: bytes32("test-request-id"), nonEVMReceiver: bytes32(0), @@ -746,36 +705,28 @@ contract PatcherTest is DSTest { signature: "" }); - // Sign the RelayData relayData.signature = signData(bridgeData, relayData); - // Set up token balance and approval for the Patcher uint256 bridgeAmount = 1000 ether; uint256 expectedMinAmount = (bridgeAmount * (10000 - slippageBps)) / - 10000; // 970 ether + 10000; - // Mint tokens to the Patcher contract token.mint(address(patcher), expectedMinAmount); - // Approve the RelayFacet to spend tokens from the Patcher - vm.prank(address(patcher)); + VM.prank(address(patcher)); token.approve(address(relayFacet), expectedMinAmount); - // Check relaySolver balance before uint256 relaySolverBalanceBefore = token.balanceOf(relaySolver); - // Encode the RelayFacet call with placeholder minAmount bytes memory originalCalldata = abi.encodeWithSelector( relayFacet.startBridgeTokensViaRelay.selector, bridgeData, relayData ); - // Use the offset we found: 260 bytes uint256[] memory offsets = new uint256[](1); offsets[0] = 260; - // Prepare oracle call to calculate minAmount with slippage bytes memory valueGetter = abi.encodeWithSelector( priceOracle.calculateMinAmount.selector, address(token), @@ -783,11 +734,10 @@ contract PatcherTest is DSTest { slippageBps ); - // Expect the LiFiTransferStarted event to be emitted ILiFi.BridgeData memory expectedBridgeData = bridgeData; - expectedBridgeData.minAmount = expectedMinAmount; // Use the already calculated value + expectedBridgeData.minAmount = expectedMinAmount; - vm.expectEmit(true, true, true, true, address(relayFacet)); + VM.expectEmit(true, true, true, true, address(relayFacet)); emit LiFiTransferStarted(expectedBridgeData); patcher.executeWithDynamicPatches( @@ -800,20 +750,15 @@ contract PatcherTest is DSTest { false ); - // Check relaySolver balance after uint256 relaySolverBalanceAfter = token.balanceOf(relaySolver); assertEq( relaySolverBalanceAfter, relaySolverBalanceBefore + expectedMinAmount ); - - // The fact that the call succeeded means the patching worked correctly - // We can't verify the exact minAmount since the real RelayFacet doesn't store state } - // Test BridgeData patching with token balance as minAmount using RelayFacet + // Tests balance-based bridging with RelayFacet function testExecuteWithDynamicPatches_RelayFacetTokenBalance() public { - // Set up a user with token balance uint256 tokenBalance = 500 ether; ILiFi.BridgeData memory bridgeData = ILiFi.BridgeData({ @@ -823,8 +768,8 @@ contract PatcherTest is DSTest { referrer: address(0x1234), sendingAssetId: address(token), receiver: address(1337), - minAmount: 0, // Will be patched with user's balance - destinationChainId: 8453, // Base + minAmount: 0, + destinationChainId: 8453, hasSourceSwaps: false, hasDestinationCall: false }); @@ -836,17 +781,13 @@ contract PatcherTest is DSTest { signature: "" }); - // Sign the RelayData relayData.signature = signData(bridgeData, relayData); - // Set up token balance and approval for the Patcher token.mint(address(patcher), tokenBalance); - // Approve the RelayFacet to spend tokens from the Patcher - vm.prank(address(patcher)); + VM.prank(address(patcher)); token.approve(address(relayFacet), tokenBalance); - // Check relaySolver balance before uint256 relaySolverBalanceBefore = token.balanceOf(relaySolver); bytes memory originalCalldata = abi.encodeWithSelector( @@ -856,19 +797,17 @@ contract PatcherTest is DSTest { ); uint256[] memory offsets = new uint256[](1); - offsets[0] = 260; // minAmount offset + offsets[0] = 260; - // Use token.balanceOf to get dynamic value bytes memory valueGetter = abi.encodeWithSelector( token.balanceOf.selector, patcher ); - // Expect the LiFiTransferStarted event to be emitted ILiFi.BridgeData memory expectedBridgeData = bridgeData; expectedBridgeData.minAmount = tokenBalance; - vm.expectEmit(true, true, true, true, address(relayFacet)); + VM.expectEmit(true, true, true, true, address(relayFacet)); emit LiFiTransferStarted(expectedBridgeData); patcher.executeWithDynamicPatches( @@ -881,17 +820,14 @@ contract PatcherTest is DSTest { false ); - // Check relaySolver balance after uint256 relaySolverBalanceAfter = token.balanceOf(relaySolver); assertEq( relaySolverBalanceAfter, relaySolverBalanceBefore + tokenBalance ); - - // The fact that the call succeeded means the patching worked correctly } - // Test error handling when oracle fails during BridgeData patching with RelayFacet + // Tests oracle failure in bridge context function testExecuteWithDynamicPatches_RelayFacetOracleFailure() public { priceOracle.setShouldFail(true); @@ -915,10 +851,8 @@ contract PatcherTest is DSTest { signature: "" }); - // Sign the RelayData relayData.signature = signData(bridgeData, relayData); - // Check relaySolver balance before (should remain unchanged due to failure) uint256 relaySolverBalanceBefore = token.balanceOf(relaySolver); bytes memory originalCalldata = abi.encodeWithSelector( @@ -937,7 +871,7 @@ contract PatcherTest is DSTest { 300 ); - vm.expectRevert(Patcher.FailedToGetDynamicValue.selector); + VM.expectRevert(Patcher.FailedToGetDynamicValue.selector); patcher.executeWithDynamicPatches( address(priceOracle), valueGetter, @@ -948,12 +882,10 @@ contract PatcherTest is DSTest { false ); - // Check relaySolver balance after (should be unchanged) uint256 relaySolverBalanceAfter = token.balanceOf(relaySolver); assertEq(relaySolverBalanceAfter, relaySolverBalanceBefore); } - // Helper function to sign RelayData function signData( ILiFi.BridgeData memory _bridgeData, RelayFacet.RelayData memory _relayData @@ -977,7 +909,7 @@ contract PatcherTest is DSTest { ) ); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, message); + (uint8 v, bytes32 r, bytes32 s) = VM.sign(privateKey, message); bytes memory signature = abi.encodePacked(r, s, v); return signature; } @@ -996,85 +928,72 @@ contract PatcherTest is DSTest { return chainId; } - // Test depositAndExecuteWithDynamicPatches success + // Tests token deposit + execution workflow function testDepositAndExecuteWithDynamicPatches_Success() public { - // Set up dynamic value uint256 dynamicValue = 12345; valueSource.setValue(dynamicValue); - // Set up token balance for user address user = address(0x1234); uint256 tokenBalance = 1000 ether; token.mint(user, tokenBalance); - // Prepare calldata with placeholder value (0) bytes memory originalCalldata = abi.encodeWithSelector( target.processValue.selector, - uint256(0) // This will be patched + uint256(0) ); - // Define offset where the value should be patched uint256[] memory offsets = new uint256[](1); - offsets[0] = 4; // Skip 4-byte selector + offsets[0] = 4; - // Prepare value getter calldata bytes memory valueGetter = abi.encodeWithSelector( valueSource.getValue.selector ); - // Approve patcher to spend user's tokens - vm.prank(user); + VM.prank(user); token.approve(address(patcher), tokenBalance); - // Expect the CallReceived event to be emitted with the patched value - vm.expectEmit(true, true, true, true, address(target)); + VM.expectEmit(true, true, true, true, address(target)); emit CallReceived(dynamicValue, address(patcher), 0); - // Execute with dynamic patches as user - vm.prank(user); + VM.prank(user); patcher.depositAndExecuteWithDynamicPatches( address(token), address(valueSource), valueGetter, address(target), - 0, // no ETH value + 0, originalCalldata, offsets, - false // regular call, not delegatecall + false ); - // Verify execution was successful assertEq(target.lastValue(), dynamicValue); assertEq(target.lastSender(), address(patcher)); assertEq(target.lastEthValue(), 0); - // Verify tokens were transferred to patcher assertEq(token.balanceOf(address(patcher)), tokenBalance); assertEq(token.balanceOf(user), 0); } - // Test depositAndExecuteWithMultiplePatches success + // Tests deposit with multiple patches workflow function testDepositAndExecuteWithMultiplePatches_Success() public { uint256 value1 = 11111; uint256 value2 = 22222; - // Set up two value sources MockValueSource valueSource2 = new MockValueSource(); valueSource.setValue(value1); valueSource2.setValue(value2); - // Set up token balance for user address user = address(0x5678); uint256 tokenBalance = 500 ether; token.mint(user, tokenBalance); bytes memory originalCalldata = abi.encodeWithSelector( target.processMultipleValues.selector, - uint256(0), // Will be patched with value1 - uint256(0) // Will be patched with value2 + uint256(0), + uint256(0) ); - // Set up arrays for multiple patches address[] memory valueSources = new address[](2); valueSources[0] = address(valueSource); valueSources[1] = address(valueSource2); @@ -1089,20 +1008,17 @@ contract PatcherTest is DSTest { uint256[][] memory offsetGroups = new uint256[][](2); offsetGroups[0] = new uint256[](1); - offsetGroups[0][0] = 4; // First parameter + offsetGroups[0][0] = 4; offsetGroups[1] = new uint256[](1); - offsetGroups[1][0] = 36; // Second parameter + offsetGroups[1][0] = 36; - // Approve patcher to spend user's tokens - vm.prank(user); + VM.prank(user); token.approve(address(patcher), tokenBalance); - // Expect the CallReceived event to be emitted with the sum of both values - vm.expectEmit(true, true, true, true, address(target)); + VM.expectEmit(true, true, true, true, address(target)); emit CallReceived(value1 + value2, address(patcher), 0); - // Execute with multiple patches as user - vm.prank(user); + VM.prank(user); patcher.depositAndExecuteWithMultiplePatches( address(token), valueSources, @@ -1114,21 +1030,18 @@ contract PatcherTest is DSTest { false ); - // Verify execution was successful assertEq(target.lastValue(), value1 + value2); - // Verify tokens were transferred to patcher assertEq(token.balanceOf(address(patcher)), tokenBalance); assertEq(token.balanceOf(user), 0); } - // Test depositAndExecuteWithDynamicPatches with zero balance + // Tests deposit with zero balance edge case function testDepositAndExecuteWithDynamicPatches_ZeroBalance() public { uint256 dynamicValue = 12345; valueSource.setValue(dynamicValue); address user = address(0x9999); - // User has zero token balance bytes memory originalCalldata = abi.encodeWithSelector( target.processValue.selector, @@ -1142,12 +1055,10 @@ contract PatcherTest is DSTest { valueSource.getValue.selector ); - // Expect the CallReceived event to be emitted with the patched value - vm.expectEmit(true, true, true, true, address(target)); + VM.expectEmit(true, true, true, true, address(target)); emit CallReceived(dynamicValue, address(patcher), 0); - // Execute with zero balance (should still work) - vm.prank(user); + VM.prank(user); patcher.depositAndExecuteWithDynamicPatches( address(token), address(valueSource), @@ -1159,15 +1070,13 @@ contract PatcherTest is DSTest { false ); - // Verify execution was successful assertEq(target.lastValue(), dynamicValue); - // Verify no tokens were transferred (user had zero balance) assertEq(token.balanceOf(address(patcher)), 0); assertEq(token.balanceOf(user), 0); } - // Test depositAndExecuteWithDynamicPatches without approval should fail + // Tests insufficient approval handling function testDepositAndExecuteWithDynamicPatches_NoApproval() public { uint256 dynamicValue = 12345; valueSource.setValue(dynamicValue); @@ -1175,7 +1084,6 @@ contract PatcherTest is DSTest { address user = address(0xABCD); uint256 tokenBalance = 1000 ether; token.mint(user, tokenBalance); - // Note: No approval given to patcher bytes memory originalCalldata = abi.encodeWithSelector( target.processValue.selector, @@ -1189,9 +1097,8 @@ contract PatcherTest is DSTest { valueSource.getValue.selector ); - // Should revert due to insufficient allowance - vm.prank(user); - vm.expectRevert(); + VM.prank(user); + VM.expectRevert(); patcher.depositAndExecuteWithDynamicPatches( address(token), address(valueSource), @@ -1204,14 +1111,14 @@ contract PatcherTest is DSTest { ); } - // Test depositAndExecuteWithDynamicPatches with partial approval + // Tests partial approval edge case function testDepositAndExecuteWithDynamicPatches_PartialApproval() public { uint256 dynamicValue = 12345; valueSource.setValue(dynamicValue); address user = address(0xEF12); uint256 tokenBalance = 1000 ether; - uint256 approvalAmount = 500 ether; // Less than balance + uint256 approvalAmount = 500 ether; token.mint(user, tokenBalance); bytes memory originalCalldata = abi.encodeWithSelector( @@ -1226,13 +1133,11 @@ contract PatcherTest is DSTest { valueSource.getValue.selector ); - // Approve only partial amount - vm.prank(user); + VM.prank(user); token.approve(address(patcher), approvalAmount); - // Should revert because it tries to transfer full balance but only partial approval - vm.prank(user); - vm.expectRevert(); + VM.prank(user); + VM.expectRevert(); patcher.depositAndExecuteWithDynamicPatches( address(token), address(valueSource), From d66fa4ecaca970de327b40fcca8351fa08643f53 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Thu, 5 Jun 2025 17:23:59 +0300 Subject: [PATCH 38/46] add documentation --- docs/Patcher.md | 163 ++++++++++++++++++++++++++++++++++++++++++++++++ docs/README.md | 1 + 2 files changed, 164 insertions(+) create mode 100644 docs/Patcher.md diff --git a/docs/Patcher.md b/docs/Patcher.md new file mode 100644 index 000000000..1eef68dba --- /dev/null +++ b/docs/Patcher.md @@ -0,0 +1,163 @@ +# Patcher + +## Description + +The Patcher is a utility contract that enables dynamic calldata patching before execution. It allows you to retrieve values dynamically from external contracts and use them to patch specific positions in calldata before executing the final call. This is particularly useful for scenarios where exact amounts are only known at execution time, such as token balances after deposits or bridge transfers. + +## How it works + +The Patcher works by: +1. Retrieving dynamic values from external contracts via static calls +2. Patching these values into predetermined positions in calldata +3. Executing the final call with the patched data + +```mermaid +graph LR; + A[User] --> B[Patcher Contract] + B --> C[Value Source Contract] + C --> B + B --> D[Final Target Contract] + B --> E[Patch Calldata] + E --> D +``` + +## Public Methods + +### Single Value Patching + +- `function executeWithDynamicPatches(address valueSource, bytes calldata valueGetter, address finalTarget, uint256 value, bytes calldata data, uint256[] calldata offsets, bool delegateCall)` + - Retrieves a single dynamic value and patches it into multiple positions in the calldata before execution + +### Token Deposit with Single Value Patching + +- `function depositAndExecuteWithDynamicPatches(address tokenAddress, address valueSource, bytes calldata valueGetter, address finalTarget, uint256 value, bytes calldata data, uint256[] calldata offsets, bool delegateCall)` + - Transfers the caller's entire token balance to the Patcher, approves the final target, then executes with dynamic patching + +### Multiple Value Patching + +- `function executeWithMultiplePatches(address[] calldata valueSources, bytes[] calldata valueGetters, address finalTarget, uint256 value, bytes calldata data, uint256[][] calldata offsetGroups, bool delegateCall)` + - Retrieves multiple dynamic values from different sources and patches them into different positions in the calldata + +### Token Deposit with Multiple Value Patching + +- `function depositAndExecuteWithMultiplePatches(address tokenAddress, address[] calldata valueSources, bytes[] calldata valueGetters, address finalTarget, uint256 value, bytes calldata data, uint256[][] calldata offsetGroups, bool delegateCall)` + - Transfers the caller's entire token balance to the Patcher, approves the final target, then executes with multiple dynamic patches + +## Parameters + +### Common Parameters + +- `valueSource` / `valueSources`: The contract(s) to query for dynamic values +- `valueGetter` / `valueGetters`: The calldata to use for retrieving the dynamic value(s) (e.g., `balanceOf(address)` call) +- `finalTarget`: The contract to call with the patched data +- `value`: The ETH value to send with the final call +- `data`: The original calldata to patch and execute +- `offsets` / `offsetGroups`: Byte offset(s) in the calldata where the dynamic value(s) should be written +- `delegateCall`: Whether to use delegatecall instead of a regular call for the final execution +- `tokenAddress`: The ERC20 token to transfer from the caller (for deposit methods) + +### Offset Calculation + +Offsets specify the exact byte position in the calldata where a 32-byte value should be written. These are typically calculated by: +1. Encoding the target function call with placeholder values +2. Finding the byte position of the placeholder in the encoded data +3. Using that position as the offset + +## Use Cases + +### Cross-Chain Bridge with Destination Swap + +The Patcher is particularly useful for cross-chain scenarios where the exact amount received from a bridge is unknown until execution: + +1. **Bridge tokens** from source chain to destination chain +2. **Receive variable amount** due to fees, slippage, or exchange rates +3. **Use Patcher** to query the actual received balance +4. **Patch the balance** into a swap call to use the exact amount received +5. **Execute the swap** with the correct amount + +### Dynamic Balance Swaps + +When you need to swap the entire balance of a token but don't know the exact amount: + +1. **Deposit tokens** to the Patcher contract +2. **Query the balance** dynamically +3. **Patch the balance** into a DEX swap call +4. **Execute the swap** using the entire deposited amount + +### Oracle-Based Transactions + +For transactions that depend on real-time data: + +1. **Query price oracles** or other data sources +2. **Patch the values** into transaction parameters +3. **Execute** with up-to-date information + +## Error Handling + +The Patcher includes several error types for different failure scenarios: + +- `FailedToGetDynamicValue()`: Thrown when the static call to retrieve a dynamic value fails +- `MismatchedArrayLengths()`: Thrown when input arrays have different lengths in multiple patch methods +- `InvalidPatchOffset()`: Thrown when a patch offset would write beyond the calldata bounds + +## Security Considerations + +- The Patcher uses `staticcall` to retrieve dynamic values, ensuring no state changes during value retrieval +- Offset validation prevents writing beyond calldata boundaries +- The contract is designed to be used with delegate calls for integration into larger systems +- Token approvals are given to the final target contract, not stored permanently + +## Integration Patterns + +### With Bridge Receivers + +The Patcher is commonly used in bridge receiver contracts to handle variable bridge amounts: + +```solidity +// Pseudo-code example +function handleBridgeMessage(bytes calldata message) external { + // Decode swap parameters from message + SwapData memory swapData = abi.decode(message, (SwapData)); + + // Use Patcher to deposit received tokens and execute swap with exact balance + patcher.depositAndExecuteWithDynamicPatches( + bridgedToken, + bridgedToken, // value source + abi.encodeCall(IERC20.balanceOf, (address(patcher))), // value getter + dexAggregator, // final target + 0, // no ETH value + swapCalldata, // calldata with placeholder amount + [amountOffset], // where to patch the amount + false // regular call + ); +} +``` + +### With DEX Aggregators + +For swapping entire token balances through DEX aggregators: + +```solidity +// Pseudo-code example +function swapEntireBalance(address token, bytes calldata swapCalldata, uint256 amountOffset) external { + patcher.depositAndExecuteWithDynamicPatches( + token, + token, // query token balance + abi.encodeCall(IERC20.balanceOf, (address(patcher))), + dexAggregator, + 0, + swapCalldata, + [amountOffset], + false + ); +} +``` + +## Best Practices + +1. **Calculate offsets carefully**: Ensure offsets point to the correct parameter positions in the calldata +2. **Use appropriate value getters**: Choose the right function to call for retrieving dynamic values +3. **Handle failures gracefully**: The Patcher will revert if value retrieval fails +4. **Consider gas costs**: Multiple patches and complex calls increase gas usage +5. **Test thoroughly**: Dynamic patching can be complex - test with various scenarios +6. **Validate inputs**: Ensure array lengths match for multiple patch operations \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index 46d86a89e..a84698a70 100644 --- a/docs/README.md +++ b/docs/README.md @@ -57,6 +57,7 @@ - [Executor](./Executor.md) - [FeeCollector](./FeeCollector.md) - [LiFuelFeeCollector](./LiFuelFeeCollector.md) +- [Patcher](./Patcher.md) - [Receiver](./Receiver.md) - [ReceiverStargateV2](./ReceiverStargateV2.md) - [ReceiverChainflip](./ReceiverChainflip.md) From 9b4a0297c07347120ec6a02393b328f20dac0b23 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Thu, 5 Jun 2025 17:47:28 +0300 Subject: [PATCH 39/46] fix payable issue --- src/Periphery/Patcher.sol | 8 +-- test/solidity/Periphery/Patcher.t.sol | 94 +++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 4 deletions(-) diff --git a/src/Periphery/Patcher.sol b/src/Periphery/Patcher.sol index 81994ccf9..732833f9d 100644 --- a/src/Periphery/Patcher.sol +++ b/src/Periphery/Patcher.sol @@ -100,7 +100,7 @@ contract Patcher { bytes calldata data, uint256[] calldata offsets, bool delegateCall - ) external returns (bool success, bytes memory returnData) { + ) external payable returns (bool success, bytes memory returnData) { return _executeWithDynamicPatches( valueSource, @@ -156,7 +156,7 @@ contract Patcher { bytes calldata data, uint256[] calldata offsets, bool delegateCall - ) external returns (bool success, bytes memory returnData) { + ) external payable returns (bool success, bytes memory returnData) { // Get the token balance of msg.sender uint256 amount = IERC20(tokenAddress).balanceOf(msg.sender); @@ -198,7 +198,7 @@ contract Patcher { bytes calldata data, uint256[][] calldata offsetGroups, bool delegateCall - ) external returns (bool success, bytes memory returnData) { + ) external payable returns (bool success, bytes memory returnData) { // Get the token balance of msg.sender uint256 amount = IERC20(tokenAddress).balanceOf(msg.sender); @@ -238,7 +238,7 @@ contract Patcher { bytes calldata data, uint256[][] calldata offsetGroups, bool delegateCall - ) external returns (bool success, bytes memory returnData) { + ) external payable returns (bool success, bytes memory returnData) { return _executeWithMultiplePatches( valueSources, diff --git a/test/solidity/Periphery/Patcher.t.sol b/test/solidity/Periphery/Patcher.t.sol index c5979bc13..23177b00d 100644 --- a/test/solidity/Periphery/Patcher.t.sol +++ b/test/solidity/Periphery/Patcher.t.sol @@ -1149,4 +1149,98 @@ contract PatcherTest is DSTest { false ); } + + // Tests that users can send native tokens with executeWithDynamicPatches + function testExecuteWithDynamicPatches_WithNativeToken() public { + uint256 dynamicValue = 12345; + uint256 ethValue = 1 ether; + valueSource.setValue(dynamicValue); + + address user = address(0xABCD); + VM.deal(user, ethValue); + + bytes memory originalCalldata = abi.encodeWithSelector( + target.processValue.selector, + uint256(0) + ); + + uint256[] memory offsets = new uint256[](1); + offsets[0] = 4; + + bytes memory valueGetter = abi.encodeWithSelector( + valueSource.getValue.selector + ); + + // User sends native tokens with the call + VM.expectEmit(true, true, true, true, address(target)); + emit CallReceived(dynamicValue, address(patcher), ethValue); + + VM.prank(user); + patcher.executeWithDynamicPatches{ value: ethValue }( + address(valueSource), + valueGetter, + address(target), + ethValue, + originalCalldata, + offsets, + false + ); + + assertEq(target.lastValue(), dynamicValue); + assertEq(target.lastSender(), address(patcher)); + assertEq(target.lastEthValue(), ethValue); + + // Verify that the user's native tokens were spent + assertEq(user.balance, 0); + } + + // Tests that depositAndExecuteWithDynamicPatches can handle native tokens + function testDepositAndExecuteWithDynamicPatches_WithNativeToken() public { + uint256 dynamicValue = 54321; + uint256 ethValue = 0.5 ether; + valueSource.setValue(dynamicValue); + + address user = address(0x1234); + uint256 tokenBalance = 1000 ether; + token.mint(user, tokenBalance); + VM.deal(user, ethValue); + + bytes memory originalCalldata = abi.encodeWithSelector( + target.processValue.selector, + uint256(0) + ); + + uint256[] memory offsets = new uint256[](1); + offsets[0] = 4; + + bytes memory valueGetter = abi.encodeWithSelector( + valueSource.getValue.selector + ); + + VM.prank(user); + token.approve(address(patcher), tokenBalance); + + VM.expectEmit(true, true, true, true, address(target)); + emit CallReceived(dynamicValue, address(patcher), ethValue); + + VM.prank(user); + patcher.depositAndExecuteWithDynamicPatches{ value: ethValue }( + address(token), + address(valueSource), + valueGetter, + address(target), + ethValue, + originalCalldata, + offsets, + false + ); + + assertEq(target.lastValue(), dynamicValue); + assertEq(target.lastSender(), address(patcher)); + assertEq(target.lastEthValue(), ethValue); + + assertEq(token.balanceOf(address(patcher)), tokenBalance); + assertEq(token.balanceOf(user), 0); + assertEq(user.balance, 0); + } } From 77d0b0f6a08faf3deb1bbc3436a635e72293ee81 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Mon, 16 Jun 2025 11:21:58 +0300 Subject: [PATCH 40/46] fixes --- src/Periphery/Patcher.sol | 46 +++++++---- test/solidity/Periphery/Patcher.t.sol | 111 ++++++++++++-------------- 2 files changed, 84 insertions(+), 73 deletions(-) diff --git a/src/Periphery/Patcher.sol b/src/Periphery/Patcher.sol index 732833f9d..613bab57d 100644 --- a/src/Periphery/Patcher.sol +++ b/src/Periphery/Patcher.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.17; -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { LibAsset, IERC20 } from "../Libraries/LibAsset.sol"; /// @title Patcher /// @author LI.FI (https://li.fi) @@ -18,26 +18,23 @@ contract Patcher { /// @notice Error when a patch offset is invalid error InvalidPatchOffset(); + /// @notice Error when a call execution fails + error CallExecutionFailed(); + /// @notice Helper function to get a dynamic value from an external contract /// @param valueSource The contract to query for the dynamic value /// @param valueGetter The calldata to use to get the dynamic value - /// @return The uint256 value retrieved from the call + /// @return dynamicValue The uint256 value retrieved from the call function _getDynamicValue( address valueSource, bytes calldata valueGetter - ) internal view returns (uint256) { + ) internal view returns (uint256 dynamicValue) { (bool valueSuccess, bytes memory valueData) = valueSource.staticcall( valueGetter ); if (!valueSuccess) revert FailedToGetDynamicValue(); - uint256 dynamicValue; - assembly { - // Load the value from the return data - dynamicValue := mload(add(valueData, 32)) - } - - return dynamicValue; + dynamicValue = abi.decode(valueData, (uint256)); } /// @notice Helper function to apply a patch at a specific offset @@ -80,6 +77,17 @@ contract Patcher { patchedData ); } + + if (!success) { + // Revert with the returned error data if available + if (returnData.length > 0) { + assembly { + revert(add(returnData, 32), mload(returnData)) + } + } else { + revert CallExecutionFailed(); + } + } } /// @notice Retrieves a value dynamically and uses it to patch calldata before execution @@ -161,10 +169,15 @@ contract Patcher { uint256 amount = IERC20(tokenAddress).balanceOf(msg.sender); // Transfer tokens from msg.sender to this contract - IERC20(tokenAddress).transferFrom(msg.sender, address(this), amount); + LibAsset.transferFromERC20( + tokenAddress, + msg.sender, + address(this), + amount + ); // Approve the finalTarget to spend the deposited tokens - IERC20(tokenAddress).approve(finalTarget, amount); + LibAsset.maxApproveERC20(IERC20(tokenAddress), finalTarget, amount); return _executeWithDynamicPatches( @@ -203,10 +216,15 @@ contract Patcher { uint256 amount = IERC20(tokenAddress).balanceOf(msg.sender); // Transfer tokens from msg.sender to this contract - IERC20(tokenAddress).transferFrom(msg.sender, address(this), amount); + LibAsset.transferFromERC20( + tokenAddress, + msg.sender, + address(this), + amount + ); // Approve the finalTarget to spend the deposited tokens - IERC20(tokenAddress).approve(finalTarget, amount); + LibAsset.maxApproveERC20(IERC20(tokenAddress), finalTarget, amount); return _executeWithMultiplePatches( diff --git a/test/solidity/Periphery/Patcher.t.sol b/test/solidity/Periphery/Patcher.t.sol index 23177b00d..a0c343085 100644 --- a/test/solidity/Periphery/Patcher.t.sol +++ b/test/solidity/Periphery/Patcher.t.sol @@ -1,8 +1,7 @@ // SPDX-License-Identifier: Unlicensed pragma solidity ^0.8.17; -import { DSTest } from "ds-test/test.sol"; -import { Vm } from "forge-std/Vm.sol"; +import { TestBase } from "../utils/TestBase.sol"; import { Patcher } from "../../../src/Periphery/Patcher.sol"; import { TestToken as ERC20 } from "../utils/TestToken.sol"; import { ILiFi } from "../../../src/Interfaces/ILiFi.sol"; @@ -157,11 +156,8 @@ contract TestRelayFacet is RelayFacet { } } -contract PatcherTest is DSTest { - Vm internal immutable VM = Vm(HEVM_ADDRESS); - +contract PatcherTest is TestBase { event CallReceived(uint256 value, address sender, uint256 ethValue); - event LiFiTransferStarted(ILiFi.BridgeData bridgeData); Patcher internal patcher; MockValueSource internal valueSource; @@ -182,7 +178,7 @@ contract PatcherTest is DSTest { token = new ERC20("Test Token", "TEST", 18); priceOracle = new MockPriceOracle(); - relaySolver = VM.addr(privateKey); + relaySolver = vm.addr(privateKey); relayFacet = new TestRelayFacet(RELAY_RECEIVER, relaySolver); } @@ -203,7 +199,7 @@ contract PatcherTest is DSTest { valueSource.getValue.selector ); - VM.expectEmit(true, true, true, true, address(target)); + vm.expectEmit(true, true, true, true, address(target)); emit CallReceived(dynamicValue, address(patcher), 0); patcher.executeWithDynamicPatches( @@ -227,7 +223,7 @@ contract PatcherTest is DSTest { uint256 ethValue = 1 ether; valueSource.setValue(dynamicValue); - VM.deal(address(patcher), ethValue); + vm.deal(address(patcher), ethValue); bytes memory originalCalldata = abi.encodeWithSelector( target.processValue.selector, @@ -241,7 +237,7 @@ contract PatcherTest is DSTest { valueSource.getValue.selector ); - VM.expectEmit(true, true, true, true, address(target)); + vm.expectEmit(true, true, true, true, address(target)); emit CallReceived(dynamicValue, address(patcher), ethValue); patcher.executeWithDynamicPatches( @@ -277,7 +273,7 @@ contract PatcherTest is DSTest { valueSource.getValue.selector ); - VM.expectEmit(true, true, true, true, address(target)); + vm.expectEmit(true, true, true, true, address(target)); emit CallReceived(dynamicValue * 2, address(patcher), 0); patcher.executeWithDynamicPatches( @@ -326,7 +322,7 @@ contract PatcherTest is DSTest { offsetGroups[1] = new uint256[](1); offsetGroups[1][0] = 36; - VM.expectEmit(true, true, true, true, address(target)); + vm.expectEmit(true, true, true, true, address(target)); emit CallReceived(value1 + value2, address(patcher), 0); patcher.executeWithMultiplePatches( @@ -386,7 +382,7 @@ contract PatcherTest is DSTest { valueSource.getValue.selector ); - VM.expectRevert(Patcher.FailedToGetDynamicValue.selector); + vm.expectRevert(Patcher.FailedToGetDynamicValue.selector); patcher.executeWithDynamicPatches( address(valueSource), valueGetter, @@ -415,7 +411,7 @@ contract PatcherTest is DSTest { valueSource.getValue.selector ); - VM.expectRevert(Patcher.InvalidPatchOffset.selector); + vm.expectRevert(Patcher.InvalidPatchOffset.selector); patcher.executeWithDynamicPatches( address(valueSource), valueGetter, @@ -447,7 +443,7 @@ contract PatcherTest is DSTest { uint256(0) ); - VM.expectRevert(Patcher.MismatchedArrayLengths.selector); + vm.expectRevert(Patcher.MismatchedArrayLengths.selector); patcher.executeWithMultiplePatches( valueSources, valueGetters, @@ -480,7 +476,7 @@ contract PatcherTest is DSTest { holder ); - VM.expectEmit(true, true, true, true, address(target)); + vm.expectEmit(true, true, true, true, address(target)); emit CallReceived( balance + block.timestamp + 1 hours, address(patcher), @@ -518,19 +514,16 @@ contract PatcherTest is DSTest { valueSource.getValue.selector ); - (bool success, bytes memory returnData) = patcher - .executeWithDynamicPatches( - address(valueSource), - valueGetter, - address(target), - 0, - originalCalldata, - offsets, - false - ); - - assertTrue(!success); - assertTrue(returnData.length > 0); + vm.expectRevert(); + patcher.executeWithDynamicPatches( + address(valueSource), + valueGetter, + address(target), + 0, + originalCalldata, + offsets, + false + ); } // Tests no-op patching with empty offsets @@ -549,7 +542,7 @@ contract PatcherTest is DSTest { valueSource.getValue.selector ); - VM.expectEmit(true, true, true, true, address(target)); + vm.expectEmit(true, true, true, true, address(target)); emit CallReceived(99999, address(patcher), 0); patcher.executeWithDynamicPatches( @@ -597,7 +590,7 @@ contract PatcherTest is DSTest { offsetGroups[1] = new uint256[](1); offsetGroups[1][0] = 4; - VM.expectEmit(true, true, true, true, address(target)); + vm.expectEmit(true, true, true, true, address(target)); emit CallReceived(value2, address(patcher), 0); patcher.executeWithMultiplePatches( @@ -630,7 +623,7 @@ contract PatcherTest is DSTest { valueSource.getValue.selector ); - VM.expectEmit(true, true, true, true, address(target)); + vm.expectEmit(true, true, true, true, address(target)); emit CallReceived(0, address(patcher), 0); patcher.executeWithDynamicPatches( @@ -663,7 +656,7 @@ contract PatcherTest is DSTest { valueSource.getValue.selector ); - VM.expectEmit(true, true, true, true, address(target)); + vm.expectEmit(true, true, true, true, address(target)); emit CallReceived(type(uint256).max, address(patcher), 0); patcher.executeWithDynamicPatches( @@ -713,7 +706,7 @@ contract PatcherTest is DSTest { token.mint(address(patcher), expectedMinAmount); - VM.prank(address(patcher)); + vm.prank(address(patcher)); token.approve(address(relayFacet), expectedMinAmount); uint256 relaySolverBalanceBefore = token.balanceOf(relaySolver); @@ -737,7 +730,7 @@ contract PatcherTest is DSTest { ILiFi.BridgeData memory expectedBridgeData = bridgeData; expectedBridgeData.minAmount = expectedMinAmount; - VM.expectEmit(true, true, true, true, address(relayFacet)); + vm.expectEmit(true, true, true, true, address(relayFacet)); emit LiFiTransferStarted(expectedBridgeData); patcher.executeWithDynamicPatches( @@ -785,7 +778,7 @@ contract PatcherTest is DSTest { token.mint(address(patcher), tokenBalance); - VM.prank(address(patcher)); + vm.prank(address(patcher)); token.approve(address(relayFacet), tokenBalance); uint256 relaySolverBalanceBefore = token.balanceOf(relaySolver); @@ -807,7 +800,7 @@ contract PatcherTest is DSTest { ILiFi.BridgeData memory expectedBridgeData = bridgeData; expectedBridgeData.minAmount = tokenBalance; - VM.expectEmit(true, true, true, true, address(relayFacet)); + vm.expectEmit(true, true, true, true, address(relayFacet)); emit LiFiTransferStarted(expectedBridgeData); patcher.executeWithDynamicPatches( @@ -871,7 +864,7 @@ contract PatcherTest is DSTest { 300 ); - VM.expectRevert(Patcher.FailedToGetDynamicValue.selector); + vm.expectRevert(Patcher.FailedToGetDynamicValue.selector); patcher.executeWithDynamicPatches( address(priceOracle), valueGetter, @@ -909,7 +902,7 @@ contract PatcherTest is DSTest { ) ); - (uint8 v, bytes32 r, bytes32 s) = VM.sign(privateKey, message); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, message); bytes memory signature = abi.encodePacked(r, s, v); return signature; } @@ -949,13 +942,13 @@ contract PatcherTest is DSTest { valueSource.getValue.selector ); - VM.prank(user); + vm.prank(user); token.approve(address(patcher), tokenBalance); - VM.expectEmit(true, true, true, true, address(target)); + vm.expectEmit(true, true, true, true, address(target)); emit CallReceived(dynamicValue, address(patcher), 0); - VM.prank(user); + vm.prank(user); patcher.depositAndExecuteWithDynamicPatches( address(token), address(valueSource), @@ -1012,13 +1005,13 @@ contract PatcherTest is DSTest { offsetGroups[1] = new uint256[](1); offsetGroups[1][0] = 36; - VM.prank(user); + vm.prank(user); token.approve(address(patcher), tokenBalance); - VM.expectEmit(true, true, true, true, address(target)); + vm.expectEmit(true, true, true, true, address(target)); emit CallReceived(value1 + value2, address(patcher), 0); - VM.prank(user); + vm.prank(user); patcher.depositAndExecuteWithMultiplePatches( address(token), valueSources, @@ -1055,10 +1048,10 @@ contract PatcherTest is DSTest { valueSource.getValue.selector ); - VM.expectEmit(true, true, true, true, address(target)); + vm.expectEmit(true, true, true, true, address(target)); emit CallReceived(dynamicValue, address(patcher), 0); - VM.prank(user); + vm.prank(user); patcher.depositAndExecuteWithDynamicPatches( address(token), address(valueSource), @@ -1097,8 +1090,8 @@ contract PatcherTest is DSTest { valueSource.getValue.selector ); - VM.prank(user); - VM.expectRevert(); + vm.prank(user); + vm.expectRevert(); patcher.depositAndExecuteWithDynamicPatches( address(token), address(valueSource), @@ -1133,11 +1126,11 @@ contract PatcherTest is DSTest { valueSource.getValue.selector ); - VM.prank(user); + vm.prank(user); token.approve(address(patcher), approvalAmount); - VM.prank(user); - VM.expectRevert(); + vm.prank(user); + vm.expectRevert(); patcher.depositAndExecuteWithDynamicPatches( address(token), address(valueSource), @@ -1157,7 +1150,7 @@ contract PatcherTest is DSTest { valueSource.setValue(dynamicValue); address user = address(0xABCD); - VM.deal(user, ethValue); + vm.deal(user, ethValue); bytes memory originalCalldata = abi.encodeWithSelector( target.processValue.selector, @@ -1172,10 +1165,10 @@ contract PatcherTest is DSTest { ); // User sends native tokens with the call - VM.expectEmit(true, true, true, true, address(target)); + vm.expectEmit(true, true, true, true, address(target)); emit CallReceived(dynamicValue, address(patcher), ethValue); - VM.prank(user); + vm.prank(user); patcher.executeWithDynamicPatches{ value: ethValue }( address(valueSource), valueGetter, @@ -1203,7 +1196,7 @@ contract PatcherTest is DSTest { address user = address(0x1234); uint256 tokenBalance = 1000 ether; token.mint(user, tokenBalance); - VM.deal(user, ethValue); + vm.deal(user, ethValue); bytes memory originalCalldata = abi.encodeWithSelector( target.processValue.selector, @@ -1217,13 +1210,13 @@ contract PatcherTest is DSTest { valueSource.getValue.selector ); - VM.prank(user); + vm.prank(user); token.approve(address(patcher), tokenBalance); - VM.expectEmit(true, true, true, true, address(target)); + vm.expectEmit(true, true, true, true, address(target)); emit CallReceived(dynamicValue, address(patcher), ethValue); - VM.prank(user); + vm.prank(user); patcher.depositAndExecuteWithDynamicPatches{ value: ethValue }( address(token), address(valueSource), From c102051dafbc2c305db7d9f56c0585a1bcf1be66 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Mon, 16 Jun 2025 11:37:51 +0300 Subject: [PATCH 41/46] more fixes --- src/Periphery/Patcher.sol | 200 ++++++++++++++++++++------------------ 1 file changed, 106 insertions(+), 94 deletions(-) diff --git a/src/Periphery/Patcher.sol b/src/Periphery/Patcher.sol index 613bab57d..a670077d3 100644 --- a/src/Periphery/Patcher.sol +++ b/src/Periphery/Patcher.sol @@ -6,7 +6,7 @@ import { LibAsset, IERC20 } from "../Libraries/LibAsset.sol"; /// @title Patcher /// @author LI.FI (https://li.fi) /// @notice A contract that patches calldata with dynamically retrieved values before execution -/// @dev Designed to be used with delegate calls +/// @dev Designed to be used with both delegate calls and normal calls /// @custom:version 1.0.0 contract Patcher { /// @notice Error when getting a dynamic value fails @@ -21,74 +21,7 @@ contract Patcher { /// @notice Error when a call execution fails error CallExecutionFailed(); - /// @notice Helper function to get a dynamic value from an external contract - /// @param valueSource The contract to query for the dynamic value - /// @param valueGetter The calldata to use to get the dynamic value - /// @return dynamicValue The uint256 value retrieved from the call - function _getDynamicValue( - address valueSource, - bytes calldata valueGetter - ) internal view returns (uint256 dynamicValue) { - (bool valueSuccess, bytes memory valueData) = valueSource.staticcall( - valueGetter - ); - if (!valueSuccess) revert FailedToGetDynamicValue(); - - dynamicValue = abi.decode(valueData, (uint256)); - } - - /// @notice Helper function to apply a patch at a specific offset - /// @param patchedData The data to patch - /// @param offset The byte offset in the data - /// @param dynamicValue The value to write at the offset - function _applyPatch( - bytes memory patchedData, - uint256 offset, - uint256 dynamicValue - ) internal pure { - if (offset + 32 > patchedData.length) revert InvalidPatchOffset(); - - assembly { - // Calculate the position in memory where we need to write the new value - let position := add(add(patchedData, 32), offset) - - // Store the new value at the calculated position - mstore(position, dynamicValue) - } - } - - /// @notice Helper function to execute the final call - /// @param finalTarget The contract to call - /// @param value The ETH value to send - /// @param patchedData The patched calldata to use - /// @param delegateCall Whether to use delegatecall - /// @return success Whether the call was successful - /// @return returnData The data returned by the call - function _executeCall( - address finalTarget, - uint256 value, - bytes memory patchedData, - bool delegateCall - ) internal returns (bool success, bytes memory returnData) { - if (delegateCall) { - (success, returnData) = finalTarget.delegatecall(patchedData); - } else { - (success, returnData) = finalTarget.call{ value: value }( - patchedData - ); - } - - if (!success) { - // Revert with the returned error data if available - if (returnData.length > 0) { - assembly { - revert(add(returnData, 32), mload(returnData)) - } - } else { - revert CallExecutionFailed(); - } - } - } + /// External Methods /// /// @notice Retrieves a value dynamically and uses it to patch calldata before execution /// @param valueSource The contract to query for the dynamic value @@ -121,29 +54,6 @@ contract Patcher { ); } - /// @dev Internal implementation to avoid stack too deep errors - function _executeWithDynamicPatches( - address valueSource, - bytes calldata valueGetter, - address finalTarget, - uint256 value, - bytes calldata data, - uint256[] calldata offsets, - bool delegateCall - ) internal returns (bool success, bytes memory returnData) { - // Get the dynamic value - uint256 dynamicValue = _getDynamicValue(valueSource, valueGetter); - - // Create a mutable copy of the original calldata - bytes memory patchedData = bytes(data); - - // Apply the patches in-place - _applyPatches(patchedData, offsets, dynamicValue); - - // Execute the call with the patched data - return _executeCall(finalTarget, value, patchedData, delegateCall); - } - /// @notice Deposits tokens and retrieves a value dynamically to patch calldata before execution /// @param tokenAddress The ERC20 token to transfer from msg.sender /// @param valueSource The contract to query for the dynamic value @@ -269,6 +179,100 @@ contract Patcher { ); } + /// Private Methods /// + + /// @notice Helper function to get a dynamic value from an external contract + /// @param valueSource The contract to query for the dynamic value + /// @param valueGetter The calldata to use to get the dynamic value + /// @return dynamicValue The uint256 value retrieved from the call + function _getDynamicValue( + address valueSource, + bytes calldata valueGetter + ) internal view returns (uint256 dynamicValue) { + (bool valueSuccess, bytes memory valueData) = valueSource.staticcall( + valueGetter + ); + if (!valueSuccess) revert FailedToGetDynamicValue(); + + dynamicValue = abi.decode(valueData, (uint256)); + } + + /// @notice Helper function to apply a patch at a specific offset + /// @param patchedData The data to patch + /// @param offset The byte offset in the data + /// @param dynamicValue The value to write at the offset + function _applyPatch( + bytes memory patchedData, + uint256 offset, + uint256 dynamicValue + ) internal pure { + if (offset + 32 > patchedData.length) revert InvalidPatchOffset(); + + assembly { + // Calculate the position in memory where we need to write the new value + let position := add(add(patchedData, 32), offset) + + // Store the new value at the calculated position + mstore(position, dynamicValue) + } + } + + /// @notice Helper function to execute the final call + /// @param finalTarget The contract to call + /// @param value The ETH value to send + /// @param patchedData The patched calldata to use + /// @param delegateCall Whether to use delegatecall + /// @return success Whether the call was successful + /// @return returnData The data returned by the call + function _executeCall( + address finalTarget, + uint256 value, + bytes memory patchedData, + bool delegateCall + ) internal returns (bool success, bytes memory returnData) { + if (delegateCall) { + (success, returnData) = finalTarget.delegatecall(patchedData); + } else { + (success, returnData) = finalTarget.call{ value: value }( + patchedData + ); + } + + if (!success) { + // Revert with the returned error data if available + if (returnData.length > 0) { + assembly { + revert(add(returnData, 32), mload(returnData)) + } + } else { + revert CallExecutionFailed(); + } + } + } + + /// @dev Internal implementation to avoid stack too deep errors + function _executeWithDynamicPatches( + address valueSource, + bytes calldata valueGetter, + address finalTarget, + uint256 value, + bytes calldata data, + uint256[] calldata offsets, + bool delegateCall + ) internal returns (bool success, bytes memory returnData) { + // Get the dynamic value + uint256 dynamicValue = _getDynamicValue(valueSource, valueGetter); + + // Create a mutable copy of the original calldata + bytes memory patchedData = bytes(data); + + // Apply the patches in-place + _applyPatches(patchedData, offsets, dynamicValue); + + // Execute the call with the patched data + return _executeCall(finalTarget, value, patchedData, delegateCall); + } + /// @dev Internal implementation to avoid stack too deep errors function _executeWithMultiplePatches( address[] calldata valueSources, @@ -304,7 +308,7 @@ contract Patcher { uint256[][] calldata offsetGroups, bytes memory patchedData ) internal view { - for (uint256 i = 0; i < valueSources.length; i++) { + for (uint256 i = 0; i < valueSources.length; ) { // Get the dynamic value for this patch uint256 dynamicValue = _getDynamicValue( valueSources[i], @@ -314,6 +318,10 @@ contract Patcher { // Apply the patches for this value uint256[] calldata offsets = offsetGroups[i]; _applyPatches(patchedData, offsets, dynamicValue); + + unchecked { + ++i; + } } } @@ -326,8 +334,12 @@ contract Patcher { uint256[] calldata offsets, uint256 dynamicValue ) internal pure { - for (uint256 j = 0; j < offsets.length; j++) { + for (uint256 j = 0; j < offsets.length; ) { _applyPatch(patchedData, offsets[j], dynamicValue); + + unchecked { + ++j; + } } } } From 5af3d3eea9483e43aa6bbe027b4bfdbb40649261 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Mon, 16 Jun 2025 12:28:54 +0300 Subject: [PATCH 42/46] more fixes --- script/demoScripts/utils/cowSwapHelpers.ts | 6 +++ script/demoScripts/utils/patcher.ts | 3 ++ test/solidity/Periphery/Patcher.t.sol | 62 +++++++++++++--------- 3 files changed, 47 insertions(+), 24 deletions(-) diff --git a/script/demoScripts/utils/cowSwapHelpers.ts b/script/demoScripts/utils/cowSwapHelpers.ts index 4ca457abb..89b6eea95 100644 --- a/script/demoScripts/utils/cowSwapHelpers.ts +++ b/script/demoScripts/utils/cowSwapHelpers.ts @@ -17,6 +17,9 @@ import { generateBalanceOfCalldata, } from './patcher' +// EIP-1967 transparent proxy creation bytecode for CowShed user proxies +// This bytecode creates a minimal proxy that delegates calls to the CowShed implementation +// while storing the implementation address in the standard EIP-1967 storage slot const PROXY_CREATION_CODE = '0x60a034608e57601f61037138819003918201601f19168301916001600160401b038311848410176093578084926040948552833981010312608e57604b602060458360a9565b920160a9565b6080527f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc556040516102b490816100bd8239608051818181608f01526101720152f35b600080fd5b634e487b7160e01b600052604160045260246000fd5b51906001600160a01b0382168203608e5756fe60806040526004361015610018575b3661019457610194565b6000803560e01c908163025b22bc1461003b575063f851a4400361000e5761010d565b3461010a5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261010a5773ffffffffffffffffffffffffffffffffffffffff60043581811691828203610106577f0000000000000000000000000000000000000000000000000000000000000000163314600014610101577f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc557fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b8280a280f35b61023d565b8380fd5b80fd5b346101645760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610164576020610146610169565b73ffffffffffffffffffffffffffffffffffffffff60405191168152f35b600080fd5b333003610101577f000000000000000000000000000000000000000000000000000000000000000090565b60ff7f68df44b1011761f481358c0f49a711192727fb02c377d697bcb0ea8ff8393ac0541615806101ef575b1561023d5760046040517ff92ee8a9000000000000000000000000000000000000000000000000000000008152fd5b507f400ada75000000000000000000000000000000000000000000000000000000007fffffffff000000000000000000000000000000000000000000000000000000006000351614156101c0565b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc546000808092368280378136915af43d82803e1561027a573d90f35b3d90fdfea2646970667358221220c7c26ff3040b96a28e96d6d27b743972943aeaef81cc821544c5fe1e24f9b17264736f6c63430008190033' @@ -206,6 +209,9 @@ export async function setupCowShedPostHooks(config: CowShedPostHooksConfig) { consola.info(`Request origin user: ${originUser} (LiFi Diamond)`) } + // Optional signature verification for debugging purposes + // This code verifies that the signature from Relay API is valid by recovering the signer + // It's not required for functionality but helps ensure the signature is working correctly // Recover the actual signer using the same message format as the contract try { // Construct the message exactly as the contract does: diff --git a/script/demoScripts/utils/patcher.ts b/script/demoScripts/utils/patcher.ts index 6ef1dd3ce..4f60022ae 100644 --- a/script/demoScripts/utils/patcher.ts +++ b/script/demoScripts/utils/patcher.ts @@ -12,6 +12,9 @@ export const normalizeCalldata = (calldata: string): string => { /** * Find hex value positions + * @param haystack The larger hex string (calldata) to search within + * @param needle The hex pattern/value to search for within the haystack + * @returns Array of byte offsets where the needle pattern is found in the haystack */ export const findHexValueOccurrences = ( haystack: string, diff --git a/test/solidity/Periphery/Patcher.t.sol b/test/solidity/Periphery/Patcher.t.sol index a0c343085..462bc3300 100644 --- a/test/solidity/Periphery/Patcher.t.sol +++ b/test/solidity/Periphery/Patcher.t.sol @@ -183,7 +183,7 @@ contract PatcherTest is TestBase { } // Tests basic single value patching into calldata - function testExecuteWithDynamicPatches_Success() public { + function test_ExecuteWithDynamicPatches_Success() public { uint256 dynamicValue = 12345; valueSource.setValue(dynamicValue); @@ -218,7 +218,7 @@ contract PatcherTest is TestBase { } // Tests patching with ETH value transfer - function testExecuteWithDynamicPatches_WithEthValue() public { + function test_ExecuteWithDynamicPatches_WithEthValue() public { uint256 dynamicValue = 54321; uint256 ethValue = 1 ether; @@ -255,7 +255,7 @@ contract PatcherTest is TestBase { } // Tests patching same value to multiple positions in calldata - function testExecuteWithDynamicPatches_MultipleOffsets() public { + function test_ExecuteWithDynamicPatches_MultipleOffsets() public { uint256 dynamicValue = 98765; valueSource.setValue(dynamicValue); @@ -290,7 +290,7 @@ contract PatcherTest is TestBase { } // Tests patching different values from different sources - function testExecuteWithMultiplePatches_Success() public { + function test_ExecuteWithMultiplePatches_Success() public { uint256 value1 = 11111; uint256 value2 = 22222; @@ -339,7 +339,7 @@ contract PatcherTest is TestBase { } // Tests delegatecall execution mode - function testExecuteWithDynamicPatches_Delegatecall() public { + function test_ExecuteWithDynamicPatches_Delegatecall() public { uint256 dynamicValue = 77777; valueSource.setValue(dynamicValue); @@ -367,7 +367,9 @@ contract PatcherTest is TestBase { } // Tests oracle/source failure handling - function testExecuteWithDynamicPatches_FailedToGetDynamicValue() public { + function testRevert_ExecuteWithDynamicPatches_FailedToGetDynamicValue() + public + { valueSource.setShouldFail(true); bytes memory originalCalldata = abi.encodeWithSelector( @@ -395,7 +397,7 @@ contract PatcherTest is TestBase { } // Tests invalid offset bounds checking - function testExecuteWithDynamicPatches_InvalidPatchOffset() public { + function testRevert_ExecuteWithDynamicPatches_InvalidPatchOffset() public { uint256 dynamicValue = 12345; valueSource.setValue(dynamicValue); @@ -424,7 +426,9 @@ contract PatcherTest is TestBase { } // Tests input validation for array length mismatches - function testExecuteWithMultiplePatches_MismatchedArrayLengths() public { + function testRevert_ExecuteWithMultiplePatches_MismatchedArrayLengths() + public + { address[] memory valueSources = new address[](2); valueSources[0] = address(valueSource); valueSources[1] = address(valueSource); @@ -456,7 +460,7 @@ contract PatcherTest is TestBase { } // Tests ERC20 balance patching in realistic scenario - function testExecuteWithDynamicPatches_TokenBalance() public { + function test_ExecuteWithDynamicPatches_TokenBalance() public { address holder = address(0x1234); uint256 balance = 1000 ether; token.mint(holder, balance); @@ -497,7 +501,7 @@ contract PatcherTest is TestBase { } // Tests target contract failure handling - function testExecuteWithDynamicPatches_TargetCallFailure() public { + function testRevert_ExecuteWithDynamicPatches_TargetCallFailure() public { uint256 dynamicValue = 12345; valueSource.setValue(dynamicValue); target.setShouldFail(true); @@ -527,7 +531,7 @@ contract PatcherTest is TestBase { } // Tests no-op patching with empty offsets - function testExecuteWithDynamicPatches_EmptyOffsets() public { + function test_ExecuteWithDynamicPatches_EmptyOffsets() public { uint256 dynamicValue = 12345; valueSource.setValue(dynamicValue); @@ -559,7 +563,7 @@ contract PatcherTest is TestBase { } // Tests overwriting same position with multiple patches - function testExecuteWithMultiplePatches_SameOffset() public { + function test_ExecuteWithMultiplePatches_SameOffset() public { uint256 value1 = 11111; uint256 value2 = 22222; @@ -607,7 +611,7 @@ contract PatcherTest is TestBase { } // Tests zero value patching edge case - function testExecuteWithDynamicPatches_ZeroValue() public { + function test_ExecuteWithDynamicPatches_ZeroValue() public { uint256 dynamicValue = 0; valueSource.setValue(dynamicValue); @@ -640,7 +644,7 @@ contract PatcherTest is TestBase { } // Tests maximum uint256 value patching edge case - function testExecuteWithDynamicPatches_MaxValue() public { + function test_ExecuteWithDynamicPatches_MaxValue() public { uint256 dynamicValue = type(uint256).max; valueSource.setValue(dynamicValue); @@ -673,7 +677,7 @@ contract PatcherTest is TestBase { } // Tests price oracle integration with RelayFacet for dynamic minAmount - function testExecuteWithDynamicPatches_RelayFacetMinAmount() public { + function test_ExecuteWithDynamicPatches_RelayFacetMinAmount() public { uint256 tokenPrice = 2000 * 1e18; uint256 slippageBps = 300; priceOracle.setPrice(address(token), tokenPrice); @@ -751,7 +755,7 @@ contract PatcherTest is TestBase { } // Tests balance-based bridging with RelayFacet - function testExecuteWithDynamicPatches_RelayFacetTokenBalance() public { + function test_ExecuteWithDynamicPatches_RelayFacetTokenBalance() public { uint256 tokenBalance = 500 ether; ILiFi.BridgeData memory bridgeData = ILiFi.BridgeData({ @@ -821,7 +825,9 @@ contract PatcherTest is TestBase { } // Tests oracle failure in bridge context - function testExecuteWithDynamicPatches_RelayFacetOracleFailure() public { + function testRevert_ExecuteWithDynamicPatches_RelayFacetOracleFailure() + public + { priceOracle.setShouldFail(true); ILiFi.BridgeData memory bridgeData = ILiFi.BridgeData({ @@ -922,7 +928,7 @@ contract PatcherTest is TestBase { } // Tests token deposit + execution workflow - function testDepositAndExecuteWithDynamicPatches_Success() public { + function test_DepositAndExecuteWithDynamicPatches_Success() public { uint256 dynamicValue = 12345; valueSource.setValue(dynamicValue); @@ -969,7 +975,7 @@ contract PatcherTest is TestBase { } // Tests deposit with multiple patches workflow - function testDepositAndExecuteWithMultiplePatches_Success() public { + function test_DepositAndExecuteWithMultiplePatches_Success() public { uint256 value1 = 11111; uint256 value2 = 22222; @@ -1030,7 +1036,9 @@ contract PatcherTest is TestBase { } // Tests deposit with zero balance edge case - function testDepositAndExecuteWithDynamicPatches_ZeroBalance() public { + function testRevert_DepositAndExecuteWithDynamicPatches_ZeroBalance() + public + { uint256 dynamicValue = 12345; valueSource.setValue(dynamicValue); @@ -1070,7 +1078,9 @@ contract PatcherTest is TestBase { } // Tests insufficient approval handling - function testDepositAndExecuteWithDynamicPatches_NoApproval() public { + function testRevert_DepositAndExecuteWithDynamicPatches_NoApproval() + public + { uint256 dynamicValue = 12345; valueSource.setValue(dynamicValue); @@ -1105,7 +1115,9 @@ contract PatcherTest is TestBase { } // Tests partial approval edge case - function testDepositAndExecuteWithDynamicPatches_PartialApproval() public { + function testRevert_DepositAndExecuteWithDynamicPatches_PartialApproval() + public + { uint256 dynamicValue = 12345; valueSource.setValue(dynamicValue); @@ -1144,7 +1156,7 @@ contract PatcherTest is TestBase { } // Tests that users can send native tokens with executeWithDynamicPatches - function testExecuteWithDynamicPatches_WithNativeToken() public { + function test_ExecuteWithDynamicPatches_WithNativeToken() public { uint256 dynamicValue = 12345; uint256 ethValue = 1 ether; valueSource.setValue(dynamicValue); @@ -1188,7 +1200,9 @@ contract PatcherTest is TestBase { } // Tests that depositAndExecuteWithDynamicPatches can handle native tokens - function testDepositAndExecuteWithDynamicPatches_WithNativeToken() public { + function test_DepositAndExecuteWithDynamicPatches_WithNativeToken() + public + { uint256 dynamicValue = 54321; uint256 ethValue = 0.5 ether; valueSource.setValue(dynamicValue); From 6790e43343214776de9497e4f74733a1df4d818e Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Mon, 16 Jun 2025 13:08:36 +0300 Subject: [PATCH 43/46] more fixes --- script/demoScripts/demoPatcher.ts | 18 ++++++++++++++---- script/demoScripts/demoPatcherDest.ts | 17 ++++++++++++----- script/demoScripts/utils/cowSwapHelpers.ts | 5 +++-- .../utils/{patcher.ts => patcherHelpers.ts} | 0 4 files changed, 29 insertions(+), 11 deletions(-) rename script/demoScripts/utils/{patcher.ts => patcherHelpers.ts} (100%) diff --git a/script/demoScripts/demoPatcher.ts b/script/demoScripts/demoPatcher.ts index 33f7a7e98..cafdada8b 100644 --- a/script/demoScripts/demoPatcher.ts +++ b/script/demoScripts/demoPatcher.ts @@ -1,6 +1,13 @@ #!/usr/bin/env bun -import { parseUnits, createWalletClient, http, getContract, Hex } from 'viem' +import { + parseUnits, + createWalletClient, + http, + getContract, + Hex, + getAddress, +} from 'viem' import { privateKeyToAccount } from 'viem/accounts' import { arbitrum } from 'viem/chains' import { ethers } from 'ethers' @@ -22,6 +29,9 @@ const ERC20_ABI = erc20Artifact.abi /** * Execute CowSwap demo with Patcher contract + * + * Example successful transaction: + * - Arbitrum: https://arbiscan.io/tx/0x7536cd0d5175a6a985f6a0c5cbfaf63b573fe2300301c57e12cca4c8995fe891 */ async function executeCowSwapDemo(options: { privateKey: string @@ -109,12 +119,12 @@ async function executeCowSwapDemo(options: { // Create the order parameters const parameters = { kind: OrderKind.SELL, - sellToken: ARBITRUM_WETH as `0x${string}`, + sellToken: getAddress(ARBITRUM_WETH), sellTokenDecimals: 18, - buyToken: ARBITRUM_USDC as `0x${string}`, + buyToken: getAddress(ARBITRUM_USDC), buyTokenDecimals: 6, amount: swapAmount.toString(), - receiver: shedDeterministicAddress as `0x${string}`, // Important: Set the receiver to the CowShed proxy + receiver: getAddress(shedDeterministicAddress), // Important: Set the receiver to the CowShed proxy validFor: 30 * 60, // 30 minutes in seconds slippageBps: 50, // 0.5% slippage } diff --git a/script/demoScripts/demoPatcherDest.ts b/script/demoScripts/demoPatcherDest.ts index 3d643e6ec..9d1616277 100644 --- a/script/demoScripts/demoPatcherDest.ts +++ b/script/demoScripts/demoPatcherDest.ts @@ -11,6 +11,7 @@ import { encodeFunctionData, encodeAbiParameters, parseAbi, + getAddress, } from 'viem' import { privateKeyToAccount } from 'viem/accounts' import { arbitrum } from 'viem/chains' @@ -22,7 +23,13 @@ import { findNeedleOffset, generateExecuteWithDynamicPatchesCalldata, generateBalanceOfCalldata, -} from './utils/patcher' +} from './utils/patcherHelpers' + +/** + * Example successful transactions: + * - Source (Arbitrum): https://arbiscan.io/tx/0xf14f9897c01ac30a1b8c13fb7b3e6f840f312a9212f651f730e999940871db56 + * - Destination (Base): https://basescan.org/tx/0x6835a3bd73051870bb8f100c2267b7143905011b6e4028d80e7154ff6d307fdc + */ // Contract addresses const ARBITRUM_WETH = '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1' @@ -268,10 +275,10 @@ function encodeDestinationCallMessage( const swapData = [ // Single call: Patcher deposits WETH from Executor, approves LiFiDexAggregator, and executes swap { - callTo: BASE_PATCHER as `0x${string}`, - approveTo: BASE_PATCHER as `0x${string}`, - sendingAssetId: BASE_WETH as `0x${string}`, - receivingAssetId: BASE_USDC as `0x${string}`, + callTo: getAddress(BASE_PATCHER), + approveTo: getAddress(BASE_PATCHER), + sendingAssetId: getAddress(BASE_WETH), + receivingAssetId: getAddress(BASE_USDC), fromAmount: BigInt(swapFromAmount), callData: depositAndExecuteCallData as `0x${string}`, requiresDeposit: true, diff --git a/script/demoScripts/utils/cowSwapHelpers.ts b/script/demoScripts/utils/cowSwapHelpers.ts index 89b6eea95..d466be3b8 100644 --- a/script/demoScripts/utils/cowSwapHelpers.ts +++ b/script/demoScripts/utils/cowSwapHelpers.ts @@ -5,6 +5,7 @@ import { keccak256, encodePacked, pad, + getAddress, } from 'viem' import { randomBytes } from 'crypto' import { ethers } from 'ethers' @@ -15,7 +16,7 @@ import { findNeedleOffset, generateExecuteWithDynamicPatchesCalldata, generateBalanceOfCalldata, -} from './patcher' +} from './patcherHelpers' // EIP-1967 transparent proxy creation bytecode for CowShed user proxies // This bytecode creates a minimal proxy that delegates calls to the CowShed implementation @@ -68,7 +69,7 @@ export function encodeCowShedExecuteHooks( functionName: 'executeHooks', args: [ calls.map((call) => ({ - target: call.target as `0x${string}`, + target: getAddress(call.target), value: call.value, callData: call.callData, allowFailure: call.allowFailure, diff --git a/script/demoScripts/utils/patcher.ts b/script/demoScripts/utils/patcherHelpers.ts similarity index 100% rename from script/demoScripts/utils/patcher.ts rename to script/demoScripts/utils/patcherHelpers.ts From 03ea5a0720f8025f5bd2843bdca05eb20268d760 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Mon, 16 Jun 2025 13:25:07 +0300 Subject: [PATCH 44/46] remove commented code --- script/demoScripts/utils/cowSwapHelpers.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/script/demoScripts/utils/cowSwapHelpers.ts b/script/demoScripts/utils/cowSwapHelpers.ts index d466be3b8..8a1f9d7d5 100644 --- a/script/demoScripts/utils/cowSwapHelpers.ts +++ b/script/demoScripts/utils/cowSwapHelpers.ts @@ -216,16 +216,6 @@ export async function setupCowShedPostHooks(config: CowShedPostHooksConfig) { // Recover the actual signer using the same message format as the contract try { // Construct the message exactly as the contract does: - // keccak256(abi.encodePacked( - // requestId, - // block.chainid, - // bytes32(uint256(uint160(address(this)))), - // bytes32(uint256(uint160(sendingAssetId))), - // _getMappedChainId(destinationChainId), - // bytes32(uint256(uint160(receiver))), - // receivingAssetId - // )) - // Use viem's encodePacked instead of manual concatenation const packedData = encodePacked( [ From 5d4d29abba5bb9fab0c6ec099415f51ea6a71204 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Wed, 25 Jun 2025 18:13:14 +0300 Subject: [PATCH 45/46] fix --- test/solidity/Periphery/Patcher.t.sol | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/solidity/Periphery/Patcher.t.sol b/test/solidity/Periphery/Patcher.t.sol index 462bc3300..484684405 100644 --- a/test/solidity/Periphery/Patcher.t.sol +++ b/test/solidity/Periphery/Patcher.t.sol @@ -355,6 +355,9 @@ contract PatcherTest is TestBase { valueSource.getValue.selector ); + vm.expectEmit(true, true, true, true, address(patcher)); + emit CallReceived(dynamicValue, address(this), 0); + patcher.executeWithDynamicPatches( address(valueSource), valueGetter, From 6f476736a2617ae1eaa3828f5fec0ea94aa1322e Mon Sep 17 00:00:00 2001 From: Michal Mironczuk Date: Fri, 4 Jul 2025 17:15:41 +0200 Subject: [PATCH 46/46] added _createPatchedData --- src/Periphery/Patcher.sol | 46 ++++++++++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/src/Periphery/Patcher.sol b/src/Periphery/Patcher.sol index a670077d3..436cceb6c 100644 --- a/src/Periphery/Patcher.sol +++ b/src/Periphery/Patcher.sol @@ -263,11 +263,11 @@ contract Patcher { // Get the dynamic value uint256 dynamicValue = _getDynamicValue(valueSource, valueGetter); - // Create a mutable copy of the original calldata - bytes memory patchedData = bytes(data); - - // Apply the patches in-place - _applyPatches(patchedData, offsets, dynamicValue); + bytes memory patchedData = _createPatchedData( + data, + offsets, + dynamicValue + ); // Execute the call with the patched data return _executeCall(finalTarget, value, patchedData, delegateCall); @@ -342,4 +342,40 @@ contract Patcher { } } } + + /// @notice Creates a patched copy of input data with a dynamic value applied at specified offsets + /// @dev Uses assembly for efficient memory operations without expensive memory copies + /// @param data The original calldata to patch + /// @param offsets Array of byte offsets where the dynamic value should be written + /// @param dynamicValue The value to write at each offset + /// @return patchedData The new bytes array containing the patched data + function _createPatchedData( + bytes calldata data, + uint256[] calldata offsets, + uint256 dynamicValue + ) internal pure returns (bytes memory patchedData) { + uint256 dataLength = data.length; + + assembly { + // allocate memory for result + patchedData := mload(0x40) + mstore(patchedData, dataLength) + + // calculate and set new free memory pointer + let patchedDataEnd := add(add(patchedData, 0x20), dataLength) + mstore(0x40, patchedDataEnd) + + // copy calldata directly to memory + calldatacopy(add(patchedData, 0x20), data.offset, dataLength) + } + + // single simple loop for 1-2 offsets + for (uint256 i = 0; i < offsets.length; i++) { + if (offsets[i] + 32 > dataLength) { + revert InvalidPatchOffset(); + } + + _applyPatch(patchedData, offsets[i], dynamicValue); + } + } }