diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..74fa06b --- /dev/null +++ b/.env.example @@ -0,0 +1,54 @@ +# Private key of the deployer wallet (no 0x prefix) +DEPLOYER_PRIVATE_KEY= + +# Private key used by e2e scripts (may be the same as DEPLOYER_PRIVATE_KEY) +PRIVATE_KEY= + +# Constructor arguments +OWNER_ADDRESS= + +# External API keys +RELAY_API_KEY= # optional, relay.link x-api-key header +OPEN_OCEAN_API_KEY= # optional, OpenOcean API key + +# RPC endpoints (public fallbacks are pre-configured in hardhat.config.ts) +ETHEREUM_RPC= +POLYGON_RPC= +ARBITRUM_RPC= +OPTIMISM_RPC= +BASE_RPC= +AVALANCHE_RPC= +BSC_RPC= +LINEA_RPC= +SCROLL_RPC= +BLAST_RPC= +MODE_RPC= +MANTLE_RPC= +GNOSIS_RPC= +SONIC_RPC= +UNICHAIN_RPC= +BERACHAIN_RPC= +INK_RPC= +SONEIUM_RPC= +WORLDCHAIN_RPC= +SEI_RPC= +ARBITRUM_SEPOLIA_RPC= +OPTIMISM_SEPOLIA_RPC= + +# Block explorer API keys (for --verify) +MAINNET_ETHERSCAN_KEY= +POLYGON_ETHERSCAN_KEY= +ARBITRUM_ETHERSCAN_KEY= +OPTIMISM_ETHERSCAN_KEY= +BASE_ETHERSCAN_KEY= +BSC_ETHERSCAN_KEY= +AVALANCHE_ETHERSCAN_KEY= +LINEA_ETHERSCAN_KEY= +SCROLL_ETHERSCAN_KEY= +BLAST_ETHERSCAN_KEY= +MANTLE_ETHERSCAN_KEY= +GNOSIS_ETHERSCAN_KEY= +SONIC_ETHERSCAN_KEY= +UNICHAIN_ETHERSCAN_KEY= +BERACHAIN_ETHERSCAN_KEY= +SEI_ETHERSCAN_KEY= diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b79c8d4..7422605 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,6 +4,8 @@ permissions: {} on: push: + branches: + - main pull_request: workflow_dispatch: @@ -28,9 +30,6 @@ jobs: - name: Show Forge version run: forge --version - - name: Run Forge fmt - run: forge fmt --check - - name: Run Forge build run: forge build --sizes diff --git a/.gitignore b/.gitignore index 85198aa..e18c785 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,13 @@ docs/ # Dotenv file .env + + +node_modules + +/typechain +/artifacts +/cache_hardhat +/cache-hh +/artifacts-tron +/cache_hardhat-tron \ No newline at end of file diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..4885094 --- /dev/null +++ b/.npmrc @@ -0,0 +1,4 @@ +# Only resolve dependency versions published at least this many days ago. +# Helps limit supply-chain risk from freshly published malicious releases. +# Supported in npm 11+ (see `npm config get min-release-age`). +min-release-age=14 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..7e55c30 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,6 @@ +# Project Context + +For OpenRouter contract work read files which are relevant for the task. general context - `OPENROUTER_CONTEXT.md` +assumptions - `OPENROUTER_ASSUMPTIONS.md` first. + +Main ship target is `src/combined/OpenRouterV2Unchecked.sol`. If its ABI changes, update the backend encoders in `bungee-backend/src/modules/dex/utils.ts` and `bungee-backend/src/modules/router/utils/directQuotesOpenRouter.ts`. diff --git a/OPENROUTER.md b/OPENROUTER.md index 6ab67b1..ca6020c 100644 --- a/OPENROUTER.md +++ b/OPENROUTER.md @@ -1,255 +1,285 @@ -# BungeeOpenRouter — Contract Variants +# OpenRouter -> **Monolithic** — non-generic; purpose-built with fees, swap, bridge functionality +**Contract:** [`src/OpenRouter.sol`](src/OpenRouter.sol) -> **Modular** — generic; supports arbitrary actions; uses returndata from previous calls and modifies parts of next calldata +OpenRouter is a single on-chain executor that combines two earlier designs: -> **Minimal** — generic; supports arbitrary actions; but no calldata modification; each subsequent action destination contract can read state eg. balanceOf() and uses them as needed; +1. **Structured (monolithic) routes** — fixed pull → fee → swap → bridge semantics, exposed as separate entrypoints (`swap`, `swapAndBridge`, `bridge`) instead of one giant `Execution` struct and `performExecution`. +2. **Generic (modular) routes** — an ordered `performActions` loop with returndata splicing between steps, for flows that do not fit the structured pipeline. -All versions uses signature verification. +There is **no backend signature verification**, **no nonce**, and **no deadline** on this contract. ERC-20 fund safety for structured pulls relies on [0x AllowanceHolder](https://github.com/0xProject/0x-settler) transient allowances plus `_msgSender() == input.user` in `_pullFromUser`. Native input uses `msg.value` on the outer call. -Three versions of the OpenRouter contract exist, each making a different trade-off between rigidity and generality. All three share the same authentication model; they differ only in how the execution steps are expressed and how outputs flow between steps. +--- -**Source layout** (under `src/`): +## Source layout ```text src/ - Counter.sol # scaffold only - common/ # shared by every variant + AH offshoots - OpenRouterAuthBase.sol - lib/AuthenticationLib.sol + OpenRouter.sol # ship target + common/ + allowance/AllowanceHolderContext.sol + interfaces/IAllowanceHolder.sol lib/BytesSpliceLib.sol lib/CurrencyLib.sol - utils/Ownable.sol - interfaces/IAllowanceHolder.sol - allowance/AllowanceHolderContext.sol - monolithic/ - BungeeOpenRouter.sol - BungeeOpenRouterAH.sol - modular/ - BungeeOpenRouterModular.sol - BungeeOpenRouterModularAH.sol - minimal/ - BungeeOpenRouterMinimal.sol - BungeeOpenRouterMinimalAH.sol + lib/RescueFundsLib.sol + utils/AccessControl.sol + manipulators/ # optional off-router helpers for PoCs (Across, math) ``` -Each variant subdirectory holds the ERC20-facing contract and its AllowanceHolder sibling; imports reach into `../common/`. - --- -## What is shared across all three +## How users call the router -Every version inherits `OpenRouterAuthBase` from [`src/common/OpenRouterAuthBase.sol`](src/common/OpenRouterAuthBase.sol). The only things hard-wired in the contract are: +ERC-20 inputs must be submitted through **AllowanceHolder**, not by calling OpenRouter directly: -- **A single trusted signer** (`OPEN_ROUTER_SIGNER`), rotatable by the owner via two-step `Ownable`. This is the backend solver/orchestration service address. -- **Per-nonce replay protection.** A `nonceUsed` mapping is written with an assembly `sstore` the moment a valid signature is verified. Any attempt to resubmit the same nonce reverts with `InvalidNonce()` before touching any funds. -- **A deadline field.** The signature carries a `deadline` (unix timestamp). Expired payloads revert with `DeadlineExpired()`. -- **Chain + deployment binding.** The signed digest always includes `block.chainid` and `address(this)`. A payload signed for one deployment cannot be replayed on a different chain or a different deployment of the same contract. +1. User approves AllowanceHolder (not OpenRouter). +2. User calls `AllowanceHolder.exec(operator, token, amount, target, data)` with `target = OpenRouter` and `data` encoding one of the router entrypoints. +3. AllowanceHolder forwards the call and appends the user address to calldata (ERC-2771 style). OpenRouter’s `_msgSender()` resolves to that user. +4. `_pullFromUser` calls `AllowanceHolder.transferFrom` and reverts with `CallerNotSignedUser()` unless `_msgSender() == input.user`. -The signature itself is a plain personal_sign (`\x19Ethereum Signed Message:\n32` prefix, 65-byte `r,s,v`) over `keccak256(abi.encode(chainid, address(this), executionPayload))`. This matches the scheme used in the marketplace `Solver` and `StakedRouterReceiver` contracts. +Native token input skips AllowanceHolder pull: the caller must forward sufficient `msg.value` on the outer transaction. -```solidity -// src/common/OpenRouterAuthBase.sol — `_verifyAndConsume` -if (AuthenticationLib.authenticate(digest, signature) != OPEN_ROUTER_SIGNER) { - assembly { - mstore(0x00, 0x815e1d64) // InvalidSigner() - revert(0x1c, 0x04) - } -} +`AllowanceHolderContext` also implements a harmless `balanceOf` so AllowanceHolder’s confused-deputy probe succeeds (same pattern as 0x Settler + AH). -assembly { - mstore(0, nonce) - mstore(0x20, nonceUsed.slot) - let dataSlot := keccak256(0, 0x40) - if and(sload(dataSlot), 0xff) { - mstore(0x00, 0x756688fe) // InvalidNonce() - revert(0x1c, 0x04) - } - sstore(dataSlot, 0x01) -} -``` +--- -The contract has no reentrancy guard, matching `Solver` and `StakedRouterReceiver`. The combination of a fresh nonce per call and a signature that covers the entire payload is the security boundary. +## External entrypoints ---- +| Function | Purpose | +|----------|---------| +| `swap` | Same-chain: pull → optional pre/post fee → swap → deliver output to `receiver` | +| `swapAndBridge` | Cross-chain: pull → optional pre/post swap fee → swap (output stays on router) → bridge | +| `bridge` | Direct bridge: pull → optional pre-bridge fee → bridge (amount baked into calldata) | +| `performActions` | Generic action loop with optional returndata splices | +| `rescueFunds` | Owner `RESCUE_ROLE` recovery of stuck tokens (operational, not a security boundary) | -## v1 — BungeeOpenRouter (monolithic) +Each structured entrypoint emits `RequestExecuted(bytes32 quoteId)` for off-chain correlation. `quoteId` is caller-defined; the contract does not validate it. -**File:** [`src/monolithic/BungeeOpenRouter.sol`](src/monolithic/BungeeOpenRouter.sol). AllowanceHolder variant: [`src/monolithic/BungeeOpenRouterAH.sol`](src/monolithic/BungeeOpenRouterAH.sol). +--- -This version encodes the full execution pipeline directly in the contract. The steps are explicit, ordered, and named. The signed payload is a single `Execution` struct: +## Structured routes — structs ```solidity -struct Execution { +struct InputData { address user; address inputToken; uint256 inputAmount; +} - address preFeeReceiver; // address(0) to skip - uint256 preFeeAmount; // taken in inputToken, before swap - - address swapTarget; // address(0) to skip swap entirely - address swapApprovalSpender; - address swapOutputToken; - uint256 swapValue; - uint256 swapMinOutput; - bytes swapData; - - address postFeeReceiver; // address(0) to skip - uint256 postFeeAmount; // taken in finalToken, after swap +struct FeeData { + address receiver; + uint256 amount; // 0 skips fee collection +} - address bridgeTarget; - address bridgeApprovalSpender; - uint256 bridgeValue; - bytes bridgeData; - uint256[] bridgeAmountPositions; // byte offsets where finalAmount is written +struct SwapData { + address target; + address approvalSpender; + address outputToken; + uint256 value; + uint256 minOutput; + uint256 returnDataWordOffset; // word index when using returndata output mode +} - uint256 nonce; - uint256 deadline; +struct BridgeData { + address target; + address approvalSpender; + uint256 value; // static msg.value addend (see BRIDGE_VALUE flag) } ``` -The contract `performExecution` function walks through this struct in a fixed order: +### `swap` -1. Pull `inputAmount` of `inputToken` from `user` (ERC20 `transferFrom` into the contract). -2. If `preFeeAmount > 0`, send that amount to `preFeeReceiver` immediately. -3. If `swapTarget != address(0)`, take a pre-swap balance snapshot of `swapOutputToken`, call the swap target, measure the balance delta, enforce `delta >= swapMinOutput`. The delta becomes `finalAmount` and `swapOutputToken` becomes `finalToken`. If there is no swap, `finalToken = inputToken` and `finalAmount = inputAmount - preFeeAmount`. -4. If `postFeeAmount > 0`, send that amount from `finalToken` to `postFeeReceiver`. -5. Write `finalAmount` into `bridgeData` at every byte offset in `bridgeAmountPositions` using an in-place `mstore`. This is the same pattern as `GenericStakedRoute.executeData`: +1. Pull `inputAmount` of `inputToken` from `user`. +2. If `fee.amount > 0` and **pre-fee** (`flags & 0x01 == 0`): transfer fee in input token, swap the remainder. +3. Approve `swapData.approvalSpender` when needed (max allowance, only if current allowance is insufficient). +4. Execute swap via `_execSwap` (see flags below). +5. Enforce `finalAmount >= swapData.minOutput` on **gross** swap output. +6. If **post-fee** (`flags & 0x01 != 0`): swap output lands on the router; fee is taken from output token; net is sent to `receiver`. +7. If **pre-fee / no fee**: swap calldata must send tokens **directly to `receiver`**; the router never holds swap output. -```solidity -// src/common/lib/BytesSpliceLib.sol — `spliceWord`, called for each position -assembly ("memory-safe") { - mstore(add(add(data, 0x20), position), word) -} -``` +### `swapAndBridge` -6. If `bridgeApprovalSpender != address(0)`, approve it for `finalAmount`. -7. Call `bridgeTarget` with the patched `bridgeData`, forwarding `bridgeValue` ETH. Any revert bubbles up with its original error data. +Same pull / pre-fee / swap / post-fee logic as above, but swap output **always** remains on `address(this)` for bridging. Then `_doBridge` splices the post-fee amount into bridge calldata (when flagged), approves the bridge spender, and calls the bridge target. -**When to use this.** Routes where the shape of the flow is always the same: pull → optional pre-fee → optional swap → optional post-fee → bridge. The contract knows the meaning of every field and enforces sensible preconditions (e.g. `finalAmount` cannot underflow below a fee). Adding a step that does not fit this shape — like a second bridge call, a pre-swap approval to a different address, or an intermediate hop — is not possible without deploying a new version of the contract. +### `bridge` -**AllowanceHolder variant (`BungeeOpenRouterAH`).** Instead of pulling with ERC20 `transferFrom` from the user to the router, the pull step calls 0x `AllowanceHolder.transferFrom` so funds move under that contract’s transient allowance (user approves AllowanceHolder, user calls `AllowanceHolder.exec` with `target = this router` and calldata invoking `performExecution`). The AH entry decodes `_msgSender()` as the original user appended by AllowanceHolder; `_pullFromUser` requires `_msgSender() == user`, so only the signer-named user matches the ephemeral allowance binding. Like Settler + AH patterns, `AllowanceHolderContext` exposes a harmless `balanceOf` on the router so AllowanceHolder’s confused-deputy probe succeeds; the rest of the pipeline is unchanged. +No swap. Pull → optional pre-bridge fee in input token → approve bridge spender → call bridge with `bridgeCallData` **unchanged**. + +Because `finalAmount = inputAmount - fee` is known up front, the caller must **bake the bridge amount into `bridgeCallData`** before submission. There is no runtime calldata splice on this path. --- -## v2 — BungeeOpenRouterModular (generic actions + returndata splicing) +## Packed `flags` (structured routes) + +One `uint256` packs switches for `swap` and `swapAndBridge` (not used by `bridge` or `performActions`): + +| Bits | Mask | Meaning | +|------|------|---------| +| 0 | `0x01` | Post-swap fee: fee taken from output token after swap. Clear = pre-swap fee from input. | +| 1 | `0x02` | Swap output via `balanceOf` delta on `outputToken`. Clear = decode return word at `swapData.returnDataWordOffset`. | +| 2 | `0x04` | Bridge `msg.value = finalAmount + bridgeData.value` (e.g. LayerZero `nativeFee` addend in `bridgeData.value`). Clear = `bridgeData.value` only. | +| 3 | `0x08` | Splice `finalAmount` into bridge calldata at byte offset `(flags >> 16) & 0xffff`. | +| 16–31 | — | Byte offset for bridge amount splice when bit 3 is set. | + +Common combinations: + +| `flags` | Fee | Swap output | Bridge `msg.value` | +|---------|-----|-------------|-------------------| +| `0x00` | pre | returndata | `bridgeData.value` | +| `0x01` | post | returndata | `bridgeData.value` | +| `0x02` | pre | balance delta | `bridgeData.value` | +| `0x03` | post | balance delta | `bridgeData.value` | +| `0x04` | pre | returndata | `finalAmount + bridgeData.value` | + +Add `0x08` and set bits 16–31 when bridge calldata needs the live swap output at a fixed offset (same idea as `GenericStakedRoute` / `BytesSpliceLib.spliceWord`). -**File:** [`src/modular/BungeeOpenRouterModular.sol`](src/modular/BungeeOpenRouterModular.sol). AllowanceHolder variant: [`src/modular/BungeeOpenRouterModularAH.sol`](src/modular/BungeeOpenRouterModularAH.sol). +--- + +## Generic routes — `performActions` -This version removes all domain-specific knowledge from the contract. The only signed payload is a list of `Action`s: +For flows that need extra hops, manipulator contracts, or multiple splices into one calldata blob, use the modular path: ```solidity struct Action { - CallType callType; // CALL, DELEGATECALL, or STATICCALL - address target; - uint256 value; // ETH forwarded; must be zero for non-CALL - bytes data; // base calldata, may be partially overwritten by splices - Splice[] splices; // applied to data before this action runs + uint256 actionInfo; // packed call metadata (see below) + bytes data; // calldata; patched by splices before the call + uint256[] splices; // packed splice descriptors (see below) } -struct Splice { - uint256 srcOffset; // byte offset within the *previous* action's returndata - uint256 dstOffset; // byte offset within this action's data - uint256 length; // how many bytes to copy -} +enum CallType { CALL, STATICCALL, CALL_WITH_NATIVE } ``` -The loop is: +### `actionInfo` layout -``` -prevReturn = empty bytes -for each action: - apply all splices (copy ranges from prevReturn into action.data) - dispatch the call - prevReturn = returndata from this call -``` - -**How splicing works.** The problem it solves: after a swap, the exact output amount is not known until runtime. The signed `data` for the subsequent bridge call contains a placeholder value at some byte offset. A splice says "before you make this call, copy bytes `[srcOffset, srcOffset+length)` from what the previous call returned into `data[dstOffset, dstOffset+length)`". After the copy, the call is made with the updated data. +One `uint256` per action. All fields are uint64-safe except `target` (uint160). -A concrete example: suppose action 0 is a STATICCALL to `balanceOf(address(this))` on the output token. Its returndata is 32 bytes encoding the current balance. Action 1 is the bridge call. Its `splices` list contains one entry: `{ srcOffset: 0, dstOffset: 68, length: 32 }`, which says "take the 32-byte balance from action 0's returndata and write it at byte 68 of the bridge calldata". When action 1 runs, its calldata already has the live balance written in. +| Bits | Field | Type | Meaning | +|------|-------|------|---------| +| 0–2 | `callType` | `uint8` | `CallType`: `CALL` (0), `STATICCALL` (1), `CALL_WITH_NATIVE` (2) | +| 3–7 | — | — | reserved (0) | +| 8 | `storeResult` | `bool` | When set, returndata is saved to `results[i]` even on success so later actions can splice from it | +| 9–15 | — | — | reserved (0) | +| 16–175 | `target` | `address` | Callee address (`uint160`, shifted left 16) | +| 176–255 | — | — | reserved (0) | -Under the hood, the copy uses `mcopy` (Cancun, EIP-5656): +Packing (matches `packActionInfo` in [`scripts/e2e/utils/modularActionsBuilder/index.js`](scripts/e2e/utils/modularActionsBuilder/index.js)): -```solidity -// BytesSpliceLib.spliceBytes -assembly ("memory-safe") { - mcopy( - add(add(dst, 0x20), dstOffset), - add(add(src, 0x20), srcOffset), - length - ) -} +```text +callType | (storeResult ? 1 << 8 : 0) | (uint160(target) << 16) ``` -Both source and destination offsets are bounds-checked before the copy; zero-length splices are rejected. +`CALL_WITH_NATIVE`: first 32 bytes of `data` are `msg.value`; remaining bytes are calldata. -**Security note on splices.** The base `data` for every action is part of the signed payload. A splice can only overwrite bytes within that signed data — it cannot change the call target, add extra function arguments, or replace the entire calldata. An adversarial return value can only influence the specific byte ranges the signer chose to splice. The signer controls which offsets are writable by choosing which splices to include. +### `splices[]` entry layout -**DELEGATECALL support.** When `callType == DELEGATECALL`, the call runs with this contract's storage and `address(this)`. This is how you plug in a separate implementation contract (analogous to how `BungeeGateway` delegates to its impl) without giving it the whitelist status required by the gateway. Caution applies: a delegatecall target can modify the contract's storage, so only trusted, audited implementation contracts should be used in this slot. +Each `splices[j]` is one `uint256` describing a byte-range copy from a prior action’s returndata into this action’s `data`. Offsets are into the **payload** bytes (the bytes-array contents), not including Solidity’s 32-byte length prefix. -**When to use this.** Any route where the exact amount flowing between steps is not known until runtime and must be piped into the next step's calldata. The canonical motivating case is an integration like Across, where two separate fields in the bridge calldata both need to reflect the swap output amount. With `GenericStakedRoute` you can only patch one offset; with this contract you declare as many splices as needed, each targeting a different offset. +| Bits | Field | Type | Meaning | +|------|-------|------|---------| +| 0–63 | `sourceActionIndex` | `uint64` | Index of the prior action whose returndata is the copy source | +| 64–127 | `srcOffset` | `uint64` | Byte offset into `results[sourceActionIndex]` payload | +| 128–191 | `dstOffset` | `uint64` | Byte offset into this action’s `data` payload | +| 192–255 | `length` | `uint64` | Number of bytes to copy (must be > 0) | -**AllowanceHolder variant (`BungeeOpenRouterModularAH`).** The action loop is identical after verification: no built-in pull. You choose how to compose an AllowanceHolder `transferFrom` (or delegatecall shim) as one or more ordinary `CALL` actions signed with everything else; `performExecutionAH` wraps that by binding the signature to `(chainId, this, signedUser, exec)` instead of omitting `signedUser`. It asserts `_msgSender() == signedUser` so nobody can burn another user’s nonce by submitting their payload inside a stranger’s `AH.exec`; real fund safety still comes from AllowanceHolder’s operator/owner/token scoping; `AllowanceHolderContext` only supplies the dummy `balanceOf` for AH’s probing. +Packing (matches `packSpliceInfo` in the modular actions builder): ---- +```text +sourceActionIndex | (srcOffset << 64) | (dstOffset << 128) | (length << 192) +``` -## v3 — BungeeOpenRouterMinimal (generic actions, no splicing) +Before action `i` runs, each splice copies `length` bytes from `results[sourceActionIndex]` at `srcOffset` into this action’s `data` at `dstOffset` (via `mcopy`). Constraints enforced on-chain: -**File:** [`src/minimal/BungeeOpenRouterMinimal.sol`](src/minimal/BungeeOpenRouterMinimal.sol). AllowanceHolder variant: [`src/minimal/BungeeOpenRouterMinimalAH.sol`](src/minimal/BungeeOpenRouterMinimalAH.sol). +- `sourceActionIndex < i` — otherwise `FutureSplice` +- `srcOffset + length <= source.length` and `dstOffset + length <= data.length` — otherwise `SpliceOutOfBounds` +- The source action must have `storeResult` set (bit 8 of its `actionInfo`); the JS builder sets this automatically when a splice references that action -This version is the stripped-down sibling of v2. The `Action` struct has no `splices` field: +**Destination offset conventions** (builder helpers in `modularActionsBuilder/index.js`): -```solidity -struct Action { - CallType callType; - address target; - uint256 value; - bytes data; // used exactly as signed; never mutated -} -``` +| Helper | `dstOffset` for… | +|--------|------------------| +| `spliceArg(n, source)` | ABI arg `n` in a normal call: `4 + n * 32` (past the 4-byte selector) | +| `valueFrom(source)` / `spliceNativeValue` | Leading value word of `CALL_WITH_NATIVE`: `0` | +| `splicePayloadWord(off, source)` | Payload of `CALL_WITH_NATIVE`: `32 + off` | +| `patchWord(off, source)` | Absolute payload offset `off` | -The loop dispatches each action with its signed data verbatim and discards the return value. There is no mechanism to move output from one call into the input of the next. +Example: splice the first 32 bytes of action 0’s returndata into byte offset 132 of action 2’s calldata: -``` -for each action: - dispatch the call (no splice step) - discard returndata +```js +const { packSpliceInfo } = require("./scripts/e2e/utils/modularActionsBuilder/index"); + +packSpliceInfo({ + sourceActionIndex: 0, + srcOffset: 0, + dstOffset: 132, + length: 32, +}); +// => 0n | (0n << 64n) | (132n << 128n) | (32n << 192n) ``` -**How steps communicate without splicing.** They don't — at least not through the router. Instead, the called contracts are responsible for reading whatever state they need at runtime. The most common pattern is pre/post balance accounting: the bridge target (e.g. a `GenericStakedRoute`-style contract or `BungeeApproveAndBridge`) calls `balanceOf(address(this))` itself to discover how much of the token it holds after the previous step deposited it, rather than receiving the amount as an argument. +There is **no built-in pull** in `performActions`. Compose AllowanceHolder `transferFrom` (or other setup) as ordinary actions in the signed/off-chain-built sequence. -This is exactly how `BaseRouterSingleOutput` works: it measures the swap output by comparing balances before and after the swap call, then passes the delta to `_execute`. With v3, that accounting logic lives inside the called contracts, not in the router. +--- + +## Internal helpers (shared behavior) -**When to use this.** Routes where every action is self-contained — the called contracts know what token to look at, query their own balance, and use that as their amount. This covers most `GenericStakedRoute` flows today, since those contracts already contain the offset-patching and balance-reading logic. v3 is the right choice when you do not need cross-action data passing at the router layer, and you want the smallest possible trusted surface in the router contract itself. +- **`_pullFromUser`** — AllowanceHolder ERC-20 pull or native `msg.value` check. +- **`_execSwap`** — balance-delta or returndata word decode; enforces `minOutput` at the entrypoint. +- **`_doBridge`** — optional `BytesSpliceLib.spliceWord` on bridge calldata, approval, then `_doCall`. +- **`_performActions`** — splice loop + low-level `call` / `staticcall` with bubbled revert data. -**AllowanceHolder variant (`BungeeOpenRouterMinimalAH`).** Same idea as the modular AH: use `performExecutionAH` plus `AllowanceHolderContext`’s `balanceOf`; sign over `signedUser` and require `_msgSender() == signedUser` for nonce-binding; compose the AH pull as ordinary actions in `exec.actions`. +Approvals use Solady `safeApproveWithRetry` to `type(uint256).max` only when current allowance is below the needed amount. --- -## Choosing between them +## Choosing structured vs generic -The three versions exist on a spectrum from "the contract knows everything" to "the contract knows nothing except who signed". +| Use | When | +|-----|------| +| `swap` | Same-chain DEX with optional fee; output to a known `receiver`. | +| `swapAndBridge` | Swap then bridge; runtime bridge amount and/or native bridge value from swap output. | +| `bridge` | No swap; amount and calldata fixed before the tx. | +| `performActions` | Multi-step or integration-specific flows (e.g. swap → manipulator → splice into `SpokePool.deposit`). | -**v1** is the right choice when you want the router to be the authoritative record of what the flow does — you can read one struct and understand the entire execution. The cost is that every variant of the flow (different fee timing, multi-hop bridge, etc.) needs a new contract or a new version. It is also the easiest to audit because the control flow is linear and every named step has an explicit precondition check. +Structured entrypoints keep audit surface small: linear control flow and explicit preconditions. `performActions` is the escape hatch when the pipeline is not pull → fee → swap → bridge. -**v2** is the right choice when you need to pipe outputs between steps in ways the called contracts cannot handle themselves. The key example is when a bridge call has two separate amount fields that both need to reflect the swap output — one splice entry per field, both handled in one atomic execution. The contract becomes a thin orchestrator and the "business logic" of each step lives in the action targets. +--- -**v3** is the right choice when the called contracts already handle their own amount discovery (balance-check style) and you just need a trusted sequencer that ensures the actions run in the signed order. It is the most gas-efficient version at the router layer because there is no splice computation overhead, and it is the easiest to build new action targets for because those targets do not need to conform to any returndata shape. +## Security model (summary) + +| Enforced on-chain | Not enforced | +|-------------------|--------------| +| `_msgSender() == user` on ERC-20 pull | Backend signature / nonce / deadline | +| `minOutput` after swap | That calldata matches user intent | +| Splice bounds and `FutureSplice` | That `performActions` targets are benign | +| AllowanceHolder scoping for pulls | Router must not accumulate balances or receive direct user approvals | + +`performActions` is **public**. Any caller can execute arbitrary action lists. Operational safety depends on users only approving AllowanceHolder, never OpenRouter directly, and on backend/frontend validating routes before `AllowanceHolder.exec`. See [`OPENROUTER_ASSUMPTIONS.md`](OPENROUTER_ASSUMPTIONS.md) for the full assumption set. --- -## Shared libraries +## Shared libraries (`src/common/`) + +| Module | Role | +|--------|------| +| `CurrencyLib` | Native sentinel + transfers / `balanceOf` | +| `BytesSpliceLib` | `spliceWord` for bridge calldata; `mcopy`-based `spliceBytes` in modular path | +| `RescueFundsLib` | `rescueFunds` implementation | +| `AllowanceHolderContext` | `_msgSender()` / dummy `balanceOf` for AH | + +`OpenRouterAuthBase` and signed-router variants are **not** used by this contract. + +--- -All live under `src/common/`. +## Backend and tests -**`OpenRouterAuthBase.sol`** — abstract base all three inherit. Owns the signer address, the nonce mapping, and `_verifyAndConsume`. +ABI encoders (update if the Solidity ABI changes): -**`lib/AuthenticationLib.sol`** — personal_sign recovery (`\x19Ethereum Signed Message:\n32` + ecrecover). Matches `marketplace/src/lib/AuthenticationLib.sol` exactly. +- `bungee-backend/src/modules/dex/utils.ts` — `swap`, AllowanceHolder `exec` +- `bungee-backend/src/modules/router/utils/directQuotesOpenRouter.ts` — `bridge`, `swapAndBridge` -**`lib/CurrencyLib.sol`** — wraps Solady `SafeTransferLib` with a native token shortcut (address `0xEee...EEe`), identical in spirit to the marketplace `CurrencyLib`. +Tests: -**`lib/BytesSpliceLib.sol`** — used by v1 (writing `finalAmount` to multiple positions in bridge calldata) and v2 (the per-splice `mcopy`). Exposes `spliceWord` (32-byte in-place overwrite, same assembly as `GenericStakedRoute`), `spliceWords` (repeat for multiple positions), and `spliceBytes` (arbitrary-length copy via `mcopy`, bounds-checked). +- `test/combined/OpenRouterV2Unchecked*.t.sol` — unit tests against `src/OpenRouter.sol` +- `test/poc/*OpenRouterPoC.t.sol` — fork PoCs using `performActions` + manipulators -**`allowance/AllowanceHolderContext.sol`**, **`interfaces/IAllowanceHolder.sol`** — imported only by the `*AH` contracts in each variant folder. +Deploy: `scripts/deploy/deployOpenRouter.ts` (`constructor(address _owner)` grants `RESCUE_ROLE`). diff --git a/OPENROUTER_ASSUMPTIONS.md b/OPENROUTER_ASSUMPTIONS.md new file mode 100644 index 0000000..98bff2a --- /dev/null +++ b/OPENROUTER_ASSUMPTIONS.md @@ -0,0 +1,248 @@ +# OpenRouter Assumptions + +Last reviewed: 2026-05-19. + +Scope: `src/combined/OpenRouterV2Unchecked.sol`. + +This document captures the assumptions that make the unchecked OpenRouter safe to operate. Many of these are business and integration assumptions, not guarantees enforced by the contract. + +## Source Of Truth + +`OpenRouterV2Unchecked` intentionally removes backend signature verification, nonces, and deadlines. Public entrypoints can be called by anyone. + +Current checked-in public surface: + +- `swap(...)` +- `swapAndBridge(...)` +- `bridge(...)` +- `performActions()(...)` +- `rescueFunds(...)` + +`OPENROUTER_CONTEXT.md` and `scripts/e2e/utils/routerAbi.ts` may mention `performExecution(...)`; verify against the Solidity file before relying on that ABI. + +## Enforcement Classes + +Use this distinction when reviewing any route or integration: + +- On-chain enforced: checked directly by the router. +- Operationally enforced: must be true because frontend, backend, deploy config, or runbooks enforce it. +- Policy assumption: not enforced by code. If it becomes false, the unchecked router can become unsafe. + +## Critical Business Assumptions + +### Router Never Holds Durable Funds + +The router may temporarily hold funds during one transaction, but it should not end routes with meaningful token or native balances. + +Failure mode: `performActions()` lets any caller make the router call arbitrary contracts. If the router holds ERC20s, native ETH, bridged refunds, swap dust, rebates, or protocol refunds, a public caller can move or approve those assets through modular actions before owner rescue. + +Operational requirements: + +- Do not use the router as a treasury, escrow, settlement account, refund address, or fee vault. +- Route calldata should send final assets to the user, bridge, or fee recipient in the same transaction. +- Monitor router token/native balances and treat non-zero balances as an incident or stuck-funds condition. +- Owner rescue is an operational recovery tool, not a security boundary. + +### Users Never Directly Approve The Router + +Users must not give persistent ERC20, Permit2, ERC721, ERC1155, or protocol-specific approvals directly to the router. + +Failure mode: if a user directly approves the router, any caller can use `performActions()` to make the router call `transferFrom`, `approve`, or equivalent privileged token functions against that user allowance. + +Operational requirements: + +- User ERC20 approvals should go to 0x AllowanceHolder, not OpenRouter. +- UI copy and wallet flows must never ask users to approve OpenRouter directly. +- Monitoring should flag direct allowances from users to the router. +- If a direct approval is discovered, revoke it before treating that user as safe. + +### Router Has No Privileged Role On Other Contracts + +No external contract should treat OpenRouter as a privileged actor unless every public caller is allowed to exercise that privilege. + +Failure mode: if another contract has `onlyRouter`, allowlists the router, grants it minter/burner/pauser/admin/operator/bridge-agent permissions, or keys permissions off `msg.sender == router`, any caller can exercise that role through modular execution. + +Operational requirements: + +- Do not grant OpenRouter roles in bridges, vaults, tokens, staking systems, receivers, relayers, or settlement contracts. +- Do not whitelist OpenRouter in downstream contracts as a trusted caller unless the called operation is safe for arbitrary public callers. +- Review new integrations for hidden trust checks against `msg.sender`. + +### Router Is Not A User-Intent Authority + +The unchecked router does not prove that a route reflects user intent. It only executes calldata. + +Failure mode: a malicious UI or compromised backend can make the user call `AllowanceHolder.exec` with calldata that pays an attacker, charges an arbitrary fee, bridges to a wrong recipient, or approves a malicious spender. + +Operational requirements: + +- The frontend/backend must validate recipients, fee receivers, fee amounts, swap targets, bridge targets, approval spenders, destination chain/domain, bridge min amounts, and refund addresses before presenting a transaction. +- Wallet simulation and transaction review should show the actual route effects where possible. +- `requestHash` is only an event correlation id. It does not enforce uniqueness, replay protection, or user consent. + +## Fund Pull Assumptions + +### ERC20 Inputs Use AllowanceHolder + +ERC20 input safety depends on 0x AllowanceHolder transient allowance scoping plus `_msgSender() == input.user`. + +On-chain enforced: + +- `_pullFromUser` reverts unless `_msgSender() == input.user` for ERC20 inputs. +- When called through AllowanceHolder, `_msgSender()` is decoded from the appended user address. + +Operational assumptions: + +- The user calls `AllowanceHolder.exec(operator, token, amount, target, data)`. +- `operator` is the router. +- `target` is the router. +- `token` and `amount` match the route input. +- The user has a persistent approval to AllowanceHolder, not to the router. + +Failure modes: + +- Direct ERC20 calls to the router fail because `_msgSender()` is not the user. +- Bad AH calldata can still execute a bad route if the user submits it. +- AH protects fund pulling for the route input, but it does not validate swap/bridge semantics. + +### Native Inputs Are Not User-Bound + +Native-token input routes only check that `msg.value >= inputAmount`. + +Failure mode: `input.user` is not authenticated for native routes. Anyone can submit native routes if they provide the ETH. This is usually acceptable because the caller funds the transaction, but downstream analytics must not treat `input.user` as authenticated identity for native paths. + +Operational requirements: + +- Native route attribution should come from transaction signer / AH sender / product context, not only `input.user`. +- Excess `msg.value` is not automatically refunded by the router. + +## Execution Assumptions + +### External Targets Are Trusted Per Route + +The router does not whitelist swap targets, bridge targets, approval spenders, manipulators, receivers, or fee recipients. + +Failure modes: + +- Malicious swap target can consume approved input and return misleading returndata. +- Malicious bridge target can consume approved output or native value. +- Malicious approval spender can use allowance after the route if allowance remains and the router later receives the same token. +- Malicious fee receiver can reject native fee transfers and revert the route. + +Operational requirements: + +- Backend/frontend must maintain target and spender allowlists or equivalent route validation. +- Approval spender should be the minimum necessary protocol spender. +- Prefer route patterns that leave no router balance and no meaningful residual allowance. + +### Swap Output Measurement Matches The Aggregator + +The router supports two output modes: + +- Returndata mode: decode a 32-byte word at `swapData.returnDataWordOffset`. +- Balance-delta mode: measure `balanceOf(outputReceiver)` before and after the swap. + +Failure modes: + +- Returndata mode is unsafe if the target return word is not the actual output amount. +- Balance-delta mode is unsafe if unrelated balance changes occur during the call, or if the token has rebasing/fee-on-transfer behavior that breaks expected deltas. +- In standalone pre-fee/no-fee swaps, the swap calldata must send output directly to `receiver`; the router will not forward output afterward. +- In standalone post-fee swaps and all `swapAndBridge` paths, the swap output must land on the router. + +Operational requirements: + +- Choose output mode per aggregator and route. +- Verify `returnDataWordOffset` against the concrete swap target ABI. +- Verify output recipient encoded in `swapCallData` matches the router mode. +- Treat `minOutput` as gross swap output, not guaranteed net-to-user output after post-fee or bridge fees. + +### Fee Semantics Are Caller-Defined + +The router does not enforce fee policy. + +Assumptions: + +- Pre-fee amounts are denominated in the input token. +- Post-fee amounts are denominated in the output token. +- `fee.receiver` is trusted and product-approved. +- `fee.amount` is within product policy. + +Failure modes: + +- A malicious caller can set an arbitrary fee receiver and amount if the user submits the calldata. +- Post-fee is applied after gross `minOutput` validation, so net user proceeds can be lower than `minOutput`. + +### Bridge Calldata Is Semantically Correct + +The router does not understand bridge-specific fields. + +Assumptions: + +- Destination chain/domain is correct. +- Recipient is correct. +- Refund address is not the router unless intentionally safe. +- Bridge min amount / slippage fields are correct. +- Bridge fee quote and native fee buffer are current enough. +- Token and amount fields in calldata match the route. + +Failure modes: + +- `bridge()` performs no runtime amount splicing; the amount must already be encoded. +- `swapAndBridge()` can splice one 32-byte amount word only. +- The bridge-value flag forwards `finalAmount + bridgeData.value` as native value. It must only be used when the bridge expects the bridged asset itself as native value plus a static fee. +- Excess native fee behavior depends on the bridge target and refund address, not OpenRouter. + +## Modular Execution Assumptions + +`performActions()` is the broadest surface. It makes the router a public generic call executor. + +Assumptions: + +- The router has no durable funds. +- No user has directly approved the router. +- No external contract gives the router privileged rights. +- Each action target is safe for the router to call. +- Splice offsets and lengths are generated by trusted tooling. +- Actions that are splice sources store their returndata. + +Failure modes: + +- Any public caller can transfer, approve, or spend assets already held by the router. +- Any public caller can exercise downstream privileges granted to the router. +- `CALL_WITH_NATIVE` can spend native ETH already sitting in the router. +- Invalid `callType` values fall through to normal `CALL`; encoders must emit only known call types. +- Splices are bounds-checked but not semantically validated. A bad splice can write a valid but wrong bridge amount, recipient field, fee field, or payload word. + +## Token Assumptions + +Assumptions: + +- ERC20s follow sane `transfer`, `transferFrom`, `approve`, and `balanceOf` behavior. +- The native token sentinel is exactly `0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE`. +- Tokens do not rebase or charge transfer fees in ways that invalidate route amounts, unless route tooling explicitly accounts for that. +- Approval reset/retry behavior in Solady `safeApproveWithRetry` is acceptable for the token. + +Failure modes: + +- Fee-on-transfer tokens can cause bridge approvals or calldata amounts to exceed actual received balances. +- Rebasing tokens can corrupt balance-delta output measurement. +- Non-standard tokens can revert, return false, or have allowance quirks. + +## Operational Checklist + +Before enabling a route or integration, confirm: + +- Users approve AllowanceHolder only. +- The router has no direct user allowances. +- The router has no privileged roles on any touched contract. +- The router is not used as recipient, refund address, treasury, or settlement vault unless public draining is acceptable. +- Swap target, bridge target, approval spenders, manipulators, fee receiver, and receiver are validated. +- Swap output mode and `returnDataWordOffset` are correct for the aggregator. +- Standalone swap recipient is correct for pre-fee/no-fee versus post-fee mode. +- Bridge calldata encodes the correct recipient, destination, min amount, refund address, and fees. +- Bridge amount splice offset is correct for the exact calldata shape. +- Native `msg.value` covers input amount plus all downstream native call values. +- Excess native value and bridge refunds do not end up on the router. +- Monitoring exists for router balances, direct allowances to router, and unexpected downstream roles. + +If any critical business assumption is false, do not rely on `OpenRouterV2Unchecked` as-is. Add access control, use a signed variant, or remove the downstream privilege/funds/allowance that makes the public call surface dangerous. diff --git a/OPENROUTER_CONTEXT.md b/OPENROUTER_CONTEXT.md new file mode 100644 index 0000000..2736eb4 --- /dev/null +++ b/OPENROUTER_CONTEXT.md @@ -0,0 +1,108 @@ +# OpenRouter Contract Context + +Last researched: 2026-05-18. + +Main ship target: + +- `src/combined/OpenRouterV2Unchecked.sol` + +Use `src/combined/OpenRouterV2.sol` as the signed sibling/reference, but the backend branch researched here targets the unchecked ABI. + +## V2Unchecked Surface + +`OpenRouterV2Unchecked` removes backend signature verification, nonce, and deadline fields. Fund safety for ERC20 inputs depends on 0x AllowanceHolder transient approvals plus `_msgSender() == input.user` in `_pullFromUser`. + +External entrypoints: + +- `performExecution(bytes32 requestHash, MonolithicExecution exec, bytes swapCallData, bytes bridgeCallData)` + - Pulls via AllowanceHolder. + - Optional pre-fee, optional swap, optional post-fee. + - Bridges with optional single amount-word splice controlled by flags. + - Bit 0 fee flag is ignored here; fee placement comes from `preFee` and `postFee`. +- `swap(bytes32 requestHash, InputData input, address receiver, uint256 flags, FeeData fee, SwapData swapData, bytes swapCallData)` + - Same-chain DEX path. + - Pre-fee/no-fee swaps can send output directly to `receiver`. + - Post-fee swaps send output to the router, then the router skims fee and forwards net. +- `swapAndBridge(bytes32 requestHash, InputData input, uint256 flags, FeeData fee, SwapData swapData, bytes swapCallData, BridgeData bridgeData, bytes bridgeCallData)` + - Swap output always lands on the router so it can be bridged. + - Supports runtime bridge amount splice and native bridge-value mode via flags. +- `bridge(bytes32 requestHash, InputData input, FeeData fee, BridgeData bridgeData, bytes bridgeCallData)` + - Direct bridge, no swap. + - No runtime splice; bridge amount must already be encoded in `bridgeCallData`. +- `performActions()(bytes32 requestHash, Action[] actions)` + - Generic action loop with packed action metadata and packed splices. + +## Flags + +Flag constants in `OpenRouterV2Unchecked.sol`: + +- `0x01` - post-swap fee for `swap` and `swapAndBridge`; clear means pre-fee from input. Ignored by `performExecution`. +- `0x02` - measure swap output by `balanceOf` delta; clear means decode return word at `SwapData.returnDataWordOffset`. +- `0x04` - bridge `msg.value = finalAmount + BridgeData.value`; used for native bridge assets. +- `0x08` - splice `finalAmount` into `bridgeCallData`. +- Bits `16..31` - byte offset for the bridge amount splice when `0x08` is set. + +Backend constants live in both: + +- `bungee-backend/src/modules/dex/dex.config.ts` +- `bungee-backend/src/modules/router/router.config.ts` + +Keep those masks and deployed addresses in sync with this contract. + +## Modular Packing + +`Action.actionInfo` is packed as: + +```text +uint8(callType) | (storeResult ? 1 << 8 : 0) | (uint160(target) << 16) +``` + +`Action.splices[]` entries are packed as: + +```text +sourceActionIndex | (srcOffset << 64) | (dstOffset << 128) | (length << 192) +``` + +`CallType.CALL_WITH_NATIVE` treats the first 32 bytes of `action.data` as the call value and the remaining bytes as calldata. PoCs use this for native fee transfers and Stargate native sends. + +## Current PoCs + +- `test/poc/OpenOceanAcrossOpenRouterPoC.t.sol` + - Modular OpenOcean USDC -> WETH swap. + - `AcrossERC20AmountManipulator` derives the Across output amount. + - Splices swap output and derived output into `SpokePool.deposit`. +- `test/poc/OpenOceanStargateNativeOpenRouterPoC.t.sol` + - Modular OpenOcean USDC -> native ETH. + - `MathManipulator` derives fee, post-fee amount, and bridge amount. + - Uses `CALL_WITH_NATIVE` and splices Stargate `amountLD`. +- `test/poc/OneInchCctpOpenRouterPoC.t.sol` + - CCTP-oriented PoC. + +Fork tests need RPC env vars and sometimes block pins. Example: + +```bash +ARBITRUM_RPC=... ARBITRUM_FORK_BLOCK=461716058 forge test --match-path test/poc/OpenOceanAcrossOpenRouterPoC.t.sol -vv +``` + +## Backend ABI Expectations + +The backend encodes the unchecked ABI in: + +- `bungee-backend/src/modules/dex/utils.ts` + - `swap(...)` + - `AllowanceHolder.exec(...)` +- `bungee-backend/src/modules/router/utils/directQuotesOpenRouter.ts` + - `bridge(...)` + - `swapAndBridge(...)` + - `AllowanceHolder.exec(...)` + +If the Solidity ABI changes, update those hard-coded ABI strings first. Direct DEX and direct bridge quote builders depend on them. + +## Gotchas + +- ERC20 inputs must be submitted through 0x AllowanceHolder, not directly to the router, or `_msgSender() == user` fails. +- Native input paths send ETH with the outer `AllowanceHolder.exec` call; no ERC20 pull happens. +- `bridge()` cannot splice runtime amounts. Use `swapAndBridge()` when bridge calldata needs the live swap output. +- `swapAndBridge()` uses balance-delta output measurement in backend builders today. +- `performExecution` and `swapAndBridge` share helpers but have different fee semantics. +- Production use of `OpenRouterV2Unchecked` needs an operational access-control decision; the contract itself has no signature or nonce checks. diff --git a/foundry.toml b/foundry.toml index 34b3732..94adb66 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,15 +2,20 @@ src = "src" out = "out" libs = ["lib"] -solc_version = "0.8.25" +solc_version = "0.8.34" evm_version = "cancun" optimizer = true optimizer_runs = 2_000 via_ir = false +no_match_path = "test/poc/**" remappings = [ "solady/=lib/solady/", "forge-std/=lib/forge-std/src/", ] +[profile.poc] +match_path = "test/poc/*.t.sol" +no_match_path = "NO_MATCHING_TEST_PATH" + # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/hardhat.config.ts b/hardhat.config.ts new file mode 100644 index 0000000..60d2ffb --- /dev/null +++ b/hardhat.config.ts @@ -0,0 +1,358 @@ +import '@nomicfoundation/hardhat-foundry'; +import '@nomicfoundation/hardhat-toolbox'; +import { config as dotenvConfig } from 'dotenv'; +import { HardhatUserConfig } from 'hardhat/config'; +import { resolve } from 'path'; + +dotenvConfig({ path: resolve(__dirname, './.env') }); + +const deployerKey = process.env.DEPLOYER_PRIVATE_KEY; +const accounts = deployerKey ? [deployerKey] : []; + +const config: HardhatUserConfig = { + solidity: { + version: '0.8.34', + settings: { + optimizer: { + enabled: true, + runs: 2000, + }, + evmVersion: 'cancun', + }, + }, + networks: { + hardhat: { + allowUnlimitedContractSize: true, + }, + ethereum: { + url: process.env.ETHEREUM_RPC ?? 'https://eth.llamarpc.com', + chainId: 1, + accounts, + }, + polygon: { + url: process.env.POLYGON_RPC ?? 'https://polygon.llamarpc.com', + chainId: 137, + accounts, + }, + arbitrum: { + url: process.env.ARBITRUM_RPC ?? 'https://rpc.ankr.com/arbitrum', + chainId: 42161, + accounts, + }, + optimism: { + url: process.env.OPTIMISM_RPC ?? 'https://mainnet.optimism.io', + chainId: 10, + accounts, + }, + base: { + url: process.env.BASE_RPC ?? 'https://mainnet.base.org', + chainId: 8453, + accounts, + }, + avalanche: { + url: process.env.AVALANCHE_RPC ?? 'https://rpc.ankr.com/avalanche', + chainId: 43114, + accounts, + }, + bsc: { + url: process.env.BSC_RPC ?? 'https://bsc-dataseed.binance.org/', + chainId: 56, + accounts, + }, + linea: { + url: process.env.LINEA_RPC ?? 'https://rpc.linea.build', + chainId: 59144, + accounts, + }, + scroll: { + url: process.env.SCROLL_RPC ?? 'https://1rpc.io/scroll', + chainId: 534352, + accounts, + }, + blast: { + url: + process.env.BLAST_RPC ?? 'https://blastl2-mainnet.public.blastapi.io', + chainId: 81457, + accounts, + }, + mode: { + url: process.env.MODE_RPC ?? 'https://1rpc.io/mode', + chainId: 34443, + accounts, + }, + mantle: { + url: process.env.MANTLE_RPC ?? 'https://rpc.mantle.xyz', + chainId: 5000, + accounts, + }, + gnosis: { + url: process.env.GNOSIS_RPC ?? 'https://rpc.ankr.com/gnosis', + chainId: 100, + accounts, + }, + sonic: { + url: process.env.SONIC_RPC ?? 'https://rpc.ankr.com/sonic_mainnet', + chainId: 146, + accounts, + }, + unichain: { + url: process.env.UNICHAIN_RPC ?? 'https://0xrpc.io/uni', + chainId: 130, + accounts, + }, + berachain: { + url: process.env.BERACHAIN_RPC ?? 'https://berachain-rpc.publicnode.com', + chainId: 80094, + accounts, + }, + ink: { + url: process.env.INK_RPC ?? 'https://rpc-gel.inkonchain.com', + chainId: 57073, + accounts, + }, + soneium: { + url: process.env.SONEIUM_RPC ?? 'https://soneium.drpc.org', + chainId: 1868, + accounts, + }, + worldchain: { + url: + process.env.WORLDCHAIN_RPC ?? + 'https://worldchain-mainnet.g.alchemy.com/public', + chainId: 480, + accounts, + }, + sei: { + url: process.env.SEI_RPC ?? 'https://evm-rpc.sei-apis.com', + chainId: 1329, + accounts, + }, + // testnets + arbitrumSepolia: { + url: + process.env.ARBITRUM_SEPOLIA_RPC ?? + 'https://arbitrum-sepolia-rpc.publicnode.com', + chainId: 421614, + accounts, + }, + optimismSepolia: { + url: process.env.OPTIMISM_SEPOLIA_RPC ?? 'https://sepolia.optimism.io', + chainId: 11155420, + accounts, + }, + }, + etherscan: { + enabled: true, + apiKey: { + mainnet: process.env.MAINNET_ETHERSCAN_KEY ?? '', + ethereum: process.env.MAINNET_ETHERSCAN_KEY ?? '', + polygon: process.env.POLYGON_ETHERSCAN_KEY ?? '', + arbitrumOne: process.env.ARBITRUM_ETHERSCAN_KEY ?? '', + optimism: process.env.OPTIMISM_ETHERSCAN_KEY ?? '', + base: process.env.BASE_ETHERSCAN_KEY ?? '', + bsc: process.env.BSC_ETHERSCAN_KEY ?? '', + avalanche: process.env.AVALANCHE_ETHERSCAN_KEY ?? '', + linea: process.env.LINEA_ETHERSCAN_KEY ?? '', + scroll: process.env.SCROLL_ETHERSCAN_KEY ?? '', + blast: process.env.BLAST_ETHERSCAN_KEY ?? '', + mantle: process.env.MANTLE_ETHERSCAN_KEY ?? '', + gnosis: process.env.GNOSIS_ETHERSCAN_KEY ?? '', + sonic: process.env.SONIC_ETHERSCAN_KEY ?? '', + unichain: process.env.UNICHAIN_ETHERSCAN_KEY ?? '', + berachain: process.env.BERACHAIN_ETHERSCAN_KEY ?? '', + sei: process.env.SEI_ETHERSCAN_KEY ?? '', + arbitrumSepolia: process.env.ARBITRUM_ETHERSCAN_KEY ?? '', + optimismSepolia: process.env.OPTIMISM_ETHERSCAN_KEY ?? '', + }, + customChains: [ + { + network: 'ethereum', + chainId: 1, + urls: { + apiURL: 'https://api.etherscan.io/v2/api?chainid=1', + browserURL: 'https://etherscan.io', + }, + }, + { + network: 'optimism', + chainId: 10, + urls: { + apiURL: 'https://api.etherscan.io/v2/api?chainid=10', + browserURL: 'https://optimistic.etherscan.io', + }, + }, + { + network: 'bsc', + chainId: 56, + urls: { + apiURL: 'https://api.etherscan.io/v2/api?chainid=56', + browserURL: 'https://bscscan.com', + }, + }, + { + network: 'polygon', + chainId: 137, + urls: { + apiURL: 'https://api.etherscan.io/v2/api?chainid=137', + browserURL: 'https://polygonscan.com', + }, + }, + { + network: 'mantle', + chainId: 5000, + urls: { + apiURL: 'https://api.etherscan.io/v2/api?chainid=5000', + browserURL: 'https://mantlescan.xyz', + }, + }, + { + network: 'arbitrumOne', + chainId: 42161, + urls: { + apiURL: 'https://api.etherscan.io/v2/api?chainid=42161', + browserURL: 'https://arbiscan.io', + }, + }, + { + network: 'avalanche', + chainId: 43114, + urls: { + apiURL: 'https://api.etherscan.io/v2/api?chainid=43114', + browserURL: 'https://snowscan.xyz', + }, + }, + { + network: 'linea', + chainId: 59144, + urls: { + apiURL: 'https://api.etherscan.io/v2/api?chainid=59144', + browserURL: 'https://lineascan.build', + }, + }, + { + network: 'base', + chainId: 8453, + urls: { + apiURL: 'https://api.etherscan.io/v2/api?chainid=8453', + browserURL: 'https://basescan.org', + }, + }, + { + network: 'gnosis', + chainId: 100, + urls: { + apiURL: 'https://api.etherscan.io/v2/api?chainid=100', + browserURL: 'https://gnosisscan.io', + }, + }, + { + network: 'blast', + chainId: 81457, + urls: { + apiURL: 'https://api.etherscan.io/v2/api?chainid=81457', + browserURL: 'https://blastscan.io', + }, + }, + { + network: 'scroll', + chainId: 534352, + urls: { + apiURL: 'https://api.etherscan.io/v2/api?chainid=534352', + browserURL: 'https://scrollscan.com', + }, + }, + { + network: 'mode', + chainId: 34443, + urls: { + apiURL: 'https://explorer.mode.network/api', + browserURL: 'https://explorer.mode.network', + }, + }, + { + network: 'sonic', + chainId: 146, + urls: { + apiURL: 'https://api.etherscan.io/v2/api?chainid=146', + browserURL: 'https://sonicscan.org', + }, + }, + { + network: 'unichain', + chainId: 130, + urls: { + apiURL: 'https://api.etherscan.io/v2/api?chainid=130', + browserURL: 'https://uniscan.xyz', + }, + }, + { + network: 'berachain', + chainId: 80094, + urls: { + apiURL: 'https://api.etherscan.io/v2/api?chainid=80094', + browserURL: 'https://berascan.com', + }, + }, + { + network: 'ink', + chainId: 57073, + urls: { + apiURL: 'https://explorer.inkonchain.com/api', + browserURL: 'https://explorer.inkonchain.com', + }, + }, + { + network: 'soneium', + chainId: 1868, + urls: { + apiURL: 'https://soneium.blockscout.com/api', + browserURL: 'https://soneium.blockscout.com', + }, + }, + { + network: 'worldchain', + chainId: 480, + urls: { + apiURL: 'https://api.etherscan.io/v2/api?chainid=480', + browserURL: 'https://worldscan.org', + }, + }, + { + network: 'sei', + chainId: 1329, + urls: { + apiURL: 'https://seitrace.com/pacific-1/api', + browserURL: 'https://seitrace.com', + }, + }, + { + network: 'arbitrumSepolia', + chainId: 421614, + urls: { + apiURL: 'https://api-sepolia.arbiscan.io/api', + browserURL: 'https://sepolia.arbiscan.io', + }, + }, + { + network: 'optimismSepolia', + chainId: 11155420, + urls: { + apiURL: 'https://api-sepolia-optimistic.etherscan.io/api', + browserURL: 'https://sepolia-optimism.etherscan.io', + }, + }, + ], + }, + typechain: { + outDir: 'typechain', + alwaysGenerateOverloads: true, + }, + paths: { + sources: './src', + scripts: './scripts', + cache: './cache-hh', + artifacts: './artifacts', + }, +}; + +export default config; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..53d4d7a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,8168 @@ +{ + "name": "poc-openrouter", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "poc-openrouter", + "version": "1.0.0", + "dependencies": { + "@layerzerolabs/lz-v2-utilities": "^3.0.168" + }, + "devDependencies": { + "@arbitrum/sdk": "^4.0.5", + "@nomicfoundation/hardhat-foundry": "^1.1.2", + "@nomicfoundation/hardhat-toolbox": "^5.0.0", + "axios": "^1.16.0", + "dotenv": "^16.0.0", + "ethers": "^6.16.0", + "hardhat": "^2.22.7", + "ts-node": "^10.9.0", + "typescript": "^5.0.0" + } + }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", + "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@arbitrum/sdk": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@arbitrum/sdk/-/sdk-4.0.5.tgz", + "integrity": "sha512-bADi4kVzSBUAV+GkxuKMx7zrkCVahIE4+fkBi0Ee18EPqGt1Wiub+yQCGTh+llApn1RpRtwwtYeZXhz9XelqGQ==", + "dev": true, + "license": "Apache-2.0", + "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" + }, + "engines": { + "node": ">=v11", + "npm": ">=7", + "yarn": ">= 1.0.0" + } + }, + "node_modules/@arbitrum/sdk/node_modules/ethers": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-5.8.0.tgz", + "integrity": "sha512-DUq+7fHrCg1aPDFCHx6UIPb3nmt2XMpM7Y/g2gLhsl3lIBqeAfOJIl1qEvRf2uq3BiKxmh6Fh5pfp2ieyek7Kg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abi": "5.8.0", + "@ethersproject/abstract-provider": "5.8.0", + "@ethersproject/abstract-signer": "5.8.0", + "@ethersproject/address": "5.8.0", + "@ethersproject/base64": "5.8.0", + "@ethersproject/basex": "5.8.0", + "@ethersproject/bignumber": "5.8.0", + "@ethersproject/bytes": "5.8.0", + "@ethersproject/constants": "5.8.0", + "@ethersproject/contracts": "5.8.0", + "@ethersproject/hash": "5.8.0", + "@ethersproject/hdnode": "5.8.0", + "@ethersproject/json-wallets": "5.8.0", + "@ethersproject/keccak256": "5.8.0", + "@ethersproject/logger": "5.8.0", + "@ethersproject/networks": "5.8.0", + "@ethersproject/pbkdf2": "5.8.0", + "@ethersproject/properties": "5.8.0", + "@ethersproject/providers": "5.8.0", + "@ethersproject/random": "5.8.0", + "@ethersproject/rlp": "5.8.0", + "@ethersproject/sha2": "5.8.0", + "@ethersproject/signing-key": "5.8.0", + "@ethersproject/solidity": "5.8.0", + "@ethersproject/strings": "5.8.0", + "@ethersproject/transactions": "5.8.0", + "@ethersproject/units": "5.8.0", + "@ethersproject/wallet": "5.8.0", + "@ethersproject/web": "5.8.0", + "@ethersproject/wordlists": "5.8.0" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@ethereumjs/rlp": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@ethereumjs/rlp/-/rlp-5.0.2.tgz", + "integrity": "sha512-DziebCdg4JpGlEqEdGgXmjqcFoJi+JGulUXwEjsZGAscAQ7MyD/7LE/GVCP29vEQxKc7AAwjT3A2ywHp2xfoCA==", + "dev": true, + "license": "MPL-2.0", + "bin": { + "rlp": "bin/rlp.cjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ethereumjs/util": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@ethereumjs/util/-/util-9.1.0.tgz", + "integrity": "sha512-XBEKsYqLGXLah9PNJbgdkigthkG7TAGvlD/sH12beMXEyHDyigfcbdvHhmLyDWgDyOJn4QwiQUaF7yeuhnjdog==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "@ethereumjs/rlp": "^5.0.2", + "ethereum-cryptography": "^2.2.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ethereumjs/util/node_modules/@noble/curves": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz", + "integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@ethereumjs/util/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@ethereumjs/util/node_modules/ethereum-cryptography": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-2.2.1.tgz", + "integrity": "sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/curves": "1.4.2", + "@noble/hashes": "1.4.0", + "@scure/bip32": "1.4.0", + "@scure/bip39": "1.3.0" + } + }, + "node_modules/@ethersproject/abi": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abi/-/abi-5.8.0.tgz", + "integrity": "sha512-b9YS/43ObplgyV6SlyQsG53/vkSal0MNA1fskSC4mbnCMi8R+NkcH8K9FPYNESf6jUefBUniE4SOKms0E/KK1Q==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/address": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/hash": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/strings": "^5.8.0" + } + }, + "node_modules/@ethersproject/abstract-provider": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abstract-provider/-/abstract-provider-5.8.0.tgz", + "integrity": "sha512-wC9SFcmh4UK0oKuLJQItoQdzS/qZ51EJegK6EmAWlh+OptpQ/npECOR3QqECd8iGHC0RJb4WKbVdSfif4ammrg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/networks": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/transactions": "^5.8.0", + "@ethersproject/web": "^5.8.0" + } + }, + "node_modules/@ethersproject/abstract-signer": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abstract-signer/-/abstract-signer-5.8.0.tgz", + "integrity": "sha512-N0XhZTswXcmIZQdYtUnd79VJzvEwXQw6PK0dTl9VoYrEBxxCPXqS0Eod7q5TNKRxe1/5WUMuR0u0nqTF/avdCA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-provider": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0" + } + }, + "node_modules/@ethersproject/address": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/address/-/address-5.8.0.tgz", + "integrity": "sha512-GhH/abcC46LJwshoN+uBNoKVFPxUuZm6dA257z0vZkKmU1+t8xTn8oK7B9qrj8W2rFRMch4gbJl6PmVxjxBEBA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/rlp": "^5.8.0" + } + }, + "node_modules/@ethersproject/base64": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/base64/-/base64-5.8.0.tgz", + "integrity": "sha512-lN0oIwfkYj9LbPx4xEkie6rAMJtySbpOAFXSDVQaBnAzYfB4X2Qr+FXJGxMoc3Bxp2Sm8OwvzMrywxyw0gLjIQ==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0" + } + }, + "node_modules/@ethersproject/basex": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/basex/-/basex-5.8.0.tgz", + "integrity": "sha512-PIgTszMlDRmNwW9nhS6iqtVfdTAKosA7llYXNmGPw4YAI1PUyMv28988wAb41/gHF/WqGdoLv0erHaRcHRKW2Q==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/properties": "^5.8.0" + } + }, + "node_modules/@ethersproject/bignumber": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.8.0.tgz", + "integrity": "sha512-ZyaT24bHaSeJon2tGPKIiHszWjD/54Sz8t57Toch475lCLljC6MgPmxk7Gtzz+ddNN5LuHea9qhAe0x3D+uYPA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "bn.js": "^5.2.1" + } + }, + "node_modules/@ethersproject/bytes": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/bytes/-/bytes-5.8.0.tgz", + "integrity": "sha512-vTkeohgJVCPVHu5c25XWaWQOZ4v+DkGoC42/TS2ond+PARCxTJvgTFUNDZovyQ/uAQ4EcpqqowKydcdmRKjg7A==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/constants": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/constants/-/constants-5.8.0.tgz", + "integrity": "sha512-wigX4lrf5Vu+axVTIvNsuL6YrV4O5AXl5ubcURKMEME5TnWBouUh0CDTWxZ2GpnRn1kcCgE7l8O5+VbV9QTTcg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bignumber": "^5.8.0" + } + }, + "node_modules/@ethersproject/contracts": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/contracts/-/contracts-5.8.0.tgz", + "integrity": "sha512-0eFjGz9GtuAi6MZwhb4uvUM216F38xiuR0yYCjKJpNfSEy4HUM8hvqqBj9Jmm0IUz8l0xKEhWwLIhPgxNY0yvQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abi": "^5.8.0", + "@ethersproject/abstract-provider": "^5.8.0", + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/transactions": "^5.8.0" + } + }, + "node_modules/@ethersproject/hash": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/hash/-/hash-5.8.0.tgz", + "integrity": "sha512-ac/lBcTbEWW/VGJij0CNSw/wPcw9bSRgCB0AIBz8CvED/jfvDoV9hsIIiWfvWmFEi8RcXtlNwp2jv6ozWOsooA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/base64": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/strings": "^5.8.0" + } + }, + "node_modules/@ethersproject/hdnode": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/hdnode/-/hdnode-5.8.0.tgz", + "integrity": "sha512-4bK1VF6E83/3/Im0ERnnUeWOY3P1BZml4ZD3wcH8Ys0/d1h1xaFt6Zc+Dh9zXf9TapGro0T4wvO71UTCp3/uoA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/basex": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/pbkdf2": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/sha2": "^5.8.0", + "@ethersproject/signing-key": "^5.8.0", + "@ethersproject/strings": "^5.8.0", + "@ethersproject/transactions": "^5.8.0", + "@ethersproject/wordlists": "^5.8.0" + } + }, + "node_modules/@ethersproject/json-wallets": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/json-wallets/-/json-wallets-5.8.0.tgz", + "integrity": "sha512-HxblNck8FVUtNxS3VTEYJAcwiKYsBIF77W15HufqlBF9gGfhmYOJtYZp8fSDZtn9y5EaXTE87zDwzxRoTFk11w==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/hdnode": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/pbkdf2": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/random": "^5.8.0", + "@ethersproject/strings": "^5.8.0", + "@ethersproject/transactions": "^5.8.0", + "aes-js": "3.0.0", + "scrypt-js": "3.0.1" + } + }, + "node_modules/@ethersproject/json-wallets/node_modules/aes-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.0.0.tgz", + "integrity": "sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ethersproject/keccak256": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/keccak256/-/keccak256-5.8.0.tgz", + "integrity": "sha512-A1pkKLZSz8pDaQ1ftutZoaN46I6+jvuqugx5KYNeQOPqq+JZ0Txm7dlWesCHB5cndJSu5vP2VKptKf7cksERng==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "js-sha3": "0.8.0" + } + }, + "node_modules/@ethersproject/logger": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/logger/-/logger-5.8.0.tgz", + "integrity": "sha512-Qe6knGmY+zPPWTC+wQrpitodgBfH7XoceCGL5bJVejmH+yCS3R8jJm8iiWuvWbG76RUmyEG53oqv6GMVWqunjA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT" + }, + "node_modules/@ethersproject/networks": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/networks/-/networks-5.8.0.tgz", + "integrity": "sha512-egPJh3aPVAzbHwq8DD7Po53J4OUSsA1MjQp8Vf/OZPav5rlmWUaFLiq8cvQiGK0Z5K6LYzm29+VA/p4RL1FzNg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/pbkdf2": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/pbkdf2/-/pbkdf2-5.8.0.tgz", + "integrity": "sha512-wuHiv97BrzCmfEaPbUFpMjlVg/IDkZThp9Ri88BpjRleg4iePJaj2SW8AIyE8cXn5V1tuAaMj6lzvsGJkGWskg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/sha2": "^5.8.0" + } + }, + "node_modules/@ethersproject/properties": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/properties/-/properties-5.8.0.tgz", + "integrity": "sha512-PYuiEoQ+FMaZZNGrStmN7+lWjlsoufGIHdww7454FIaGdbe/p5rnaCXTr5MtBYl3NkeoVhHZuyzChPeGeKIpQw==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/providers": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/providers/-/providers-5.8.0.tgz", + "integrity": "sha512-3Il3oTzEx3o6kzcg9ZzbE+oCZYyY+3Zh83sKkn4s1DZfTUjIegHnN2Cm0kbn9YFy45FDVcuCLLONhU7ny0SsCw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-provider": "^5.8.0", + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/base64": "^5.8.0", + "@ethersproject/basex": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/hash": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/networks": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/random": "^5.8.0", + "@ethersproject/rlp": "^5.8.0", + "@ethersproject/sha2": "^5.8.0", + "@ethersproject/strings": "^5.8.0", + "@ethersproject/transactions": "^5.8.0", + "@ethersproject/web": "^5.8.0", + "bech32": "1.1.4", + "ws": "8.18.0" + } + }, + "node_modules/@ethersproject/providers/node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@ethersproject/random": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/random/-/random-5.8.0.tgz", + "integrity": "sha512-E4I5TDl7SVqyg4/kkA/qTfuLWAQGXmSOgYyO01So8hLfwgKvYK5snIlzxJMk72IFdG/7oh8yuSqY2KX7MMwg+A==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/rlp": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/rlp/-/rlp-5.8.0.tgz", + "integrity": "sha512-LqZgAznqDbiEunaUvykH2JAoXTT9NV0Atqk8rQN9nx9SEgThA/WMx5DnW8a9FOufo//6FZOCHZ+XiClzgbqV9Q==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/sha2": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/sha2/-/sha2-5.8.0.tgz", + "integrity": "sha512-dDOUrXr9wF/YFltgTBYS0tKslPEKr6AekjqDW2dbn1L1xmjGR+9GiKu4ajxovnrDbwxAKdHjW8jNcwfz8PAz4A==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "hash.js": "1.1.7" + } + }, + "node_modules/@ethersproject/signing-key": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/signing-key/-/signing-key-5.8.0.tgz", + "integrity": "sha512-LrPW2ZxoigFi6U6aVkFN/fa9Yx/+4AtIUe4/HACTvKJdhm0eeb107EVCIQcrLZkxaSIgc/eCrX8Q1GtbH+9n3w==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "bn.js": "^5.2.1", + "elliptic": "6.6.1", + "hash.js": "1.1.7" + } + }, + "node_modules/@ethersproject/solidity": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/solidity/-/solidity-5.8.0.tgz", + "integrity": "sha512-4CxFeCgmIWamOHwYN9d+QWGxye9qQLilpgTU0XhYs1OahkclF+ewO+3V1U0mvpiuQxm5EHHmv8f7ClVII8EHsA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/sha2": "^5.8.0", + "@ethersproject/strings": "^5.8.0" + } + }, + "node_modules/@ethersproject/strings": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/strings/-/strings-5.8.0.tgz", + "integrity": "sha512-qWEAk0MAvl0LszjdfnZ2uC8xbR2wdv4cDabyHiBh3Cldq/T8dPH3V4BbBsAYJUeonwD+8afVXld274Ls+Y1xXg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/transactions": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/transactions/-/transactions-5.8.0.tgz", + "integrity": "sha512-UglxSDjByHG0TuU17bDfCemZ3AnKO2vYrL5/2n2oXvKzvb7Cz+W9gOWXKARjp2URVwcWlQlPOEQyAviKwT4AHg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/address": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/rlp": "^5.8.0", + "@ethersproject/signing-key": "^5.8.0" + } + }, + "node_modules/@ethersproject/units": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/units/-/units-5.8.0.tgz", + "integrity": "sha512-lxq0CAnc5kMGIiWW4Mr041VT8IhNM+Pn5T3haO74XZWFulk7wH1Gv64HqE96hT4a7iiNMdOCFEBgaxWuk8ETKQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/wallet": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/wallet/-/wallet-5.8.0.tgz", + "integrity": "sha512-G+jnzmgg6UxurVKRKvw27h0kvG75YKXZKdlLYmAHeF32TGUzHkOFd7Zn6QHOTYRFWnfjtSSFjBowKo7vfrXzPA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-provider": "^5.8.0", + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/hash": "^5.8.0", + "@ethersproject/hdnode": "^5.8.0", + "@ethersproject/json-wallets": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/random": "^5.8.0", + "@ethersproject/signing-key": "^5.8.0", + "@ethersproject/transactions": "^5.8.0", + "@ethersproject/wordlists": "^5.8.0" + } + }, + "node_modules/@ethersproject/web": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/web/-/web-5.8.0.tgz", + "integrity": "sha512-j7+Ksi/9KfGviws6Qtf9Q7KCqRhpwrYKQPs+JBA/rKVFF/yaWLHJEH3zfVP2plVu+eys0d2DlFmhoQJayFewcw==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/base64": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/strings": "^5.8.0" + } + }, + "node_modules/@ethersproject/wordlists": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/wordlists/-/wordlists-5.8.0.tgz", + "integrity": "sha512-2df9bbXicZws2Sb5S6ET493uJ0Z84Fjr3pC4tu/qlnZERibZCeUVuqdtt+7Tv9xxhUxHoIekIA7avrKUWHrezg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/hash": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/strings": "^5.8.0" + } + }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@layerzerolabs/lz-v2-utilities": { + "version": "3.0.168", + "resolved": "https://registry.npmjs.org/@layerzerolabs/lz-v2-utilities/-/lz-v2-utilities-3.0.168.tgz", + "integrity": "sha512-5gb5QH3q+JIOkwuJnmv3hWidwLE7ySC0G4IYCL9pwl80bkdkuY9TwfG3KqWri2F5mCzRYs6xx7vaYP5zFqY7yA==", + "license": "BUSL-1.1", + "dependencies": { + "@ethersproject/abi": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/solidity": "^5.8.0", + "bs58": "^5.0.0", + "tiny-invariant": "^1.3.1" + } + }, + "node_modules/@layerzerolabs/lz-v2-utilities/node_modules/base-x": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.1.tgz", + "integrity": "sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw==", + "license": "MIT" + }, + "node_modules/@layerzerolabs/lz-v2-utilities/node_modules/bs58": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz", + "integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==", + "license": "MIT", + "dependencies": { + "base-x": "^4.0.0" + } + }, + "node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/secp256k1": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.7.1.tgz", + "integrity": "sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nomicfoundation/edr": { + "version": "0.12.0-next.23", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr/-/edr-0.12.0-next.23.tgz", + "integrity": "sha512-F2/6HZh8Q9RsgkOIkRrckldbhPjIZY7d4mT9LYuW68miwGQ5l7CkAgcz9fRRiurA0+YJhtsbx/EyrD9DmX9BOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nomicfoundation/edr-darwin-arm64": "0.12.0-next.23", + "@nomicfoundation/edr-darwin-x64": "0.12.0-next.23", + "@nomicfoundation/edr-linux-arm64-gnu": "0.12.0-next.23", + "@nomicfoundation/edr-linux-arm64-musl": "0.12.0-next.23", + "@nomicfoundation/edr-linux-x64-gnu": "0.12.0-next.23", + "@nomicfoundation/edr-linux-x64-musl": "0.12.0-next.23", + "@nomicfoundation/edr-win32-x64-msvc": "0.12.0-next.23" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@nomicfoundation/edr-darwin-arm64": { + "version": "0.12.0-next.23", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-darwin-arm64/-/edr-darwin-arm64-0.12.0-next.23.tgz", + "integrity": "sha512-Amh7mRoDzZyJJ4efqoePqdoZOzharmSOttZuJDlVE5yy07BoE8hL6ZRpa5fNYn0LCqn/KoWs8OHANWxhKDGhvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@nomicfoundation/edr-darwin-x64": { + "version": "0.12.0-next.23", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-darwin-x64/-/edr-darwin-x64-0.12.0-next.23.tgz", + "integrity": "sha512-9wn489FIQm7m0UCD+HhktjWx6vskZzeZD9oDc2k9ZvbBzdXwPp5tiDqUBJ+eQpByAzCDfteAJwRn2lQCE0U+Iw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@nomicfoundation/edr-linux-arm64-gnu": { + "version": "0.12.0-next.23", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-arm64-gnu/-/edr-linux-arm64-gnu-0.12.0-next.23.tgz", + "integrity": "sha512-nlk5EejSzEUfEngv0Jkhqq3/wINIfF2ED9wAofc22w/V1DV99ASh9l3/e/MIHOQFecIZ9MDqt0Em9/oDyB1Uew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@nomicfoundation/edr-linux-arm64-musl": { + "version": "0.12.0-next.23", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-arm64-musl/-/edr-linux-arm64-musl-0.12.0-next.23.tgz", + "integrity": "sha512-SJuPBp3Rc6vM92UtVTUxZQ/QlLhLfwTftt2XUiYohmGKB3RjGzpgduEFMCA0LEnucUckU6UHrJNFHiDm77C4PQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@nomicfoundation/edr-linux-x64-gnu": { + "version": "0.12.0-next.23", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-x64-gnu/-/edr-linux-x64-gnu-0.12.0-next.23.tgz", + "integrity": "sha512-NU+Qs3u7Qt6t3bJFdmmjd5CsvgI2bPPzO31KifM2Ez96/jsXYho5debtTQnimlb5NAqiHTSlxjh/F8ROcptmeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@nomicfoundation/edr-linux-x64-musl": { + "version": "0.12.0-next.23", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-x64-musl/-/edr-linux-x64-musl-0.12.0-next.23.tgz", + "integrity": "sha512-F78fZA2h6/ssiCSZOovlgIu0dUeI7ItKPsDDF3UUlIibef052GCXmliMinC90jVPbrjUADMd1BUwjfI0Z8OllQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@nomicfoundation/edr-win32-x64-msvc": { + "version": "0.12.0-next.23", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-win32-x64-msvc/-/edr-win32-x64-msvc-0.12.0-next.23.tgz", + "integrity": "sha512-IfJZQJn7d/YyqhmguBIGoCKjE9dKjbu6V6iNEPApfwf5JyyjHYyyfkLU4rf7hygj57bfH4sl1jtQ6r8HnT62lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@nomicfoundation/hardhat-chai-matchers": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-chai-matchers/-/hardhat-chai-matchers-2.1.2.tgz", + "integrity": "sha512-NlUlde/ycXw2bLzA2gWjjbxQaD9xIRbAF30nsoEprAWzH8dXEI1ILZUKZMyux9n9iygEXTzN0SDVjE6zWDZi9g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/chai-as-promised": "^7.1.3", + "chai-as-promised": "^7.1.1", + "deep-eql": "^4.0.1", + "ordinal": "^1.0.3" + }, + "peerDependencies": { + "@nomicfoundation/hardhat-ethers": "^3.1.0", + "chai": "^4.2.0", + "ethers": "^6.14.0", + "hardhat": "^2.26.0" + } + }, + "node_modules/@nomicfoundation/hardhat-ethers": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-ethers/-/hardhat-ethers-3.1.3.tgz", + "integrity": "sha512-208JcDeVIl+7Wu3MhFUUtiA8TJ7r2Rn3Wr+lSx9PfsDTKkbsAsWPY6N6wQ4mtzDv0/pB9nIbJhkjoHe1EsgNsA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.1.1", + "lodash.isequal": "^4.5.0" + }, + "peerDependencies": { + "ethers": "^6.14.0", + "hardhat": "^2.28.0" + } + }, + "node_modules/@nomicfoundation/hardhat-foundry": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-foundry/-/hardhat-foundry-1.2.1.tgz", + "integrity": "sha512-pH1KeyI0sysgi7I7uQKPLXWl895EkuS6V41rSi820Ipqp/FScIwDh27RbevgC9zJ4ufSsSz34njm9cvRMGMNVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.1.0" + }, + "peerDependencies": { + "hardhat": "^2.26.0" + } + }, + "node_modules/@nomicfoundation/hardhat-ignition": { + "version": "0.15.16", + "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-ignition/-/hardhat-ignition-0.15.16.tgz", + "integrity": "sha512-T0JTnuib7QcpsWkHCPLT7Z6F483EjTdcdjb1e00jqS9zTGCPqinPB66LLtR/duDLdvgoiCVS6K8WxTQkA/xR1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@nomicfoundation/ignition-core": "^0.15.15", + "@nomicfoundation/ignition-ui": "^0.15.13", + "chalk": "^4.0.0", + "debug": "^4.3.2", + "fs-extra": "^10.0.0", + "json5": "^2.2.3", + "prompts": "^2.4.2" + }, + "peerDependencies": { + "@nomicfoundation/hardhat-verify": "^2.1.0", + "hardhat": "^2.26.0" + } + }, + "node_modules/@nomicfoundation/hardhat-ignition-ethers": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-ignition-ethers/-/hardhat-ignition-ethers-0.15.17.tgz", + "integrity": "sha512-io6Wrp1dUsJ94xEI3pw6qkPfhc9TFA+e6/+o16yQ8pvBTFMjgK5x8wIHKrrIHr9L3bkuTMtmDjyN4doqO2IqFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "peerDependencies": { + "@nomicfoundation/hardhat-ethers": "^3.1.0", + "@nomicfoundation/hardhat-ignition": "^0.15.16", + "@nomicfoundation/ignition-core": "^0.15.15", + "ethers": "^6.14.0", + "hardhat": "^2.26.0" + } + }, + "node_modules/@nomicfoundation/hardhat-network-helpers": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-network-helpers/-/hardhat-network-helpers-1.1.2.tgz", + "integrity": "sha512-p7HaUVDbLj7ikFivQVNhnfMHUBgiHYMwQWvGn9AriieuopGOELIrwj2KjyM2a6z70zai5YKO264Vwz+3UFJZPQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ethereumjs-util": "^7.1.4" + }, + "peerDependencies": { + "hardhat": "^2.26.0" + } + }, + "node_modules/@nomicfoundation/hardhat-toolbox": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-toolbox/-/hardhat-toolbox-5.0.0.tgz", + "integrity": "sha512-FnUtUC5PsakCbwiVNsqlXVIWG5JIb5CEZoSXbJUsEBun22Bivx2jhF1/q9iQbzuaGpJKFQyOhemPB2+XlEE6pQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@nomicfoundation/hardhat-chai-matchers": "^2.0.0", + "@nomicfoundation/hardhat-ethers": "^3.0.0", + "@nomicfoundation/hardhat-ignition-ethers": "^0.15.0", + "@nomicfoundation/hardhat-network-helpers": "^1.0.0", + "@nomicfoundation/hardhat-verify": "^2.0.0", + "@typechain/ethers-v6": "^0.5.0", + "@typechain/hardhat": "^9.0.0", + "@types/chai": "^4.2.0", + "@types/mocha": ">=9.1.0", + "@types/node": ">=18.0.0", + "chai": "^4.2.0", + "ethers": "^6.4.0", + "hardhat": "^2.11.0", + "hardhat-gas-reporter": "^1.0.8", + "solidity-coverage": "^0.8.1", + "ts-node": ">=8.0.0", + "typechain": "^8.3.0", + "typescript": ">=4.5.0" + } + }, + "node_modules/@nomicfoundation/hardhat-verify": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-verify/-/hardhat-verify-2.1.3.tgz", + "integrity": "sha512-danbGjPp2WBhLkJdQy9/ARM3WQIK+7vwzE0urNem1qZJjh9f54Kf5f1xuQv8DvqewUAkuPxVt/7q4Grz5WjqSg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@ethersproject/abi": "^5.1.2", + "@ethersproject/address": "^5.0.2", + "cbor": "^8.1.0", + "debug": "^4.1.1", + "lodash.clonedeep": "^4.5.0", + "picocolors": "^1.1.0", + "semver": "^6.3.0", + "table": "^6.8.0", + "undici": "^5.14.0" + }, + "peerDependencies": { + "hardhat": "^2.26.0" + } + }, + "node_modules/@nomicfoundation/ignition-core": { + "version": "0.15.15", + "resolved": "https://registry.npmjs.org/@nomicfoundation/ignition-core/-/ignition-core-0.15.15.tgz", + "integrity": "sha512-JdKFxYknTfOYtFXMN6iFJ1vALJPednuB+9p9OwGIRdoI6HYSh4ZBzyRURgyXtHFyaJ/SF9lBpsYV9/1zEpcYwg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@ethersproject/address": "5.6.1", + "@nomicfoundation/solidity-analyzer": "^0.1.1", + "cbor": "^9.0.0", + "debug": "^4.3.2", + "ethers": "^6.14.0", + "fs-extra": "^10.0.0", + "immer": "10.0.2", + "lodash": "4.17.21", + "ndjson": "2.0.0" + } + }, + "node_modules/@nomicfoundation/ignition-core/node_modules/@ethersproject/address": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@ethersproject/address/-/address-5.6.1.tgz", + "integrity": "sha512-uOgF0kS5MJv9ZvCz7x6T2EXJSzotiybApn4XlOgoTX0xdtyVIJ7pF+6cGPxiEq/dpBiTfMiw7Yc81JcwhSYA0Q==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "@ethersproject/bignumber": "^5.6.2", + "@ethersproject/bytes": "^5.6.1", + "@ethersproject/keccak256": "^5.6.1", + "@ethersproject/logger": "^5.6.0", + "@ethersproject/rlp": "^5.6.1" + } + }, + "node_modules/@nomicfoundation/ignition-core/node_modules/cbor": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/cbor/-/cbor-9.0.2.tgz", + "integrity": "sha512-JPypkxsB10s9QOWwa6zwPzqE1Md3vqpPc+cai4sAecuCsRyAtAl/pMyhPlMbT/xtPnm2dznJZYRLui57qiRhaQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "nofilter": "^3.1.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@nomicfoundation/ignition-ui": { + "version": "0.15.13", + "resolved": "https://registry.npmjs.org/@nomicfoundation/ignition-ui/-/ignition-ui-0.15.13.tgz", + "integrity": "sha512-HbTszdN1iDHCkUS9hLeooqnLEW2U45FaqFwFEYT8nIno2prFZhG+n68JEERjmfFCB5u0WgbuJwk3CgLoqtSL7Q==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@nomicfoundation/solidity-analyzer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer/-/solidity-analyzer-0.1.2.tgz", + "integrity": "sha512-q4n32/FNKIhQ3zQGGw5CvPF6GTvDCpYwIf7bEY/dZTZbgfDsHyjJwURxUJf3VQuuJj+fDIFl4+KkBVbw4Ef6jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + }, + "optionalDependencies": { + "@nomicfoundation/solidity-analyzer-darwin-arm64": "0.1.2", + "@nomicfoundation/solidity-analyzer-darwin-x64": "0.1.2", + "@nomicfoundation/solidity-analyzer-linux-arm64-gnu": "0.1.2", + "@nomicfoundation/solidity-analyzer-linux-arm64-musl": "0.1.2", + "@nomicfoundation/solidity-analyzer-linux-x64-gnu": "0.1.2", + "@nomicfoundation/solidity-analyzer-linux-x64-musl": "0.1.2", + "@nomicfoundation/solidity-analyzer-win32-x64-msvc": "0.1.2" + } + }, + "node_modules/@nomicfoundation/solidity-analyzer-darwin-arm64": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-darwin-arm64/-/solidity-analyzer-darwin-arm64-0.1.2.tgz", + "integrity": "sha512-JaqcWPDZENCvm++lFFGjrDd8mxtf+CtLd2MiXvMNTBD33dContTZ9TWETwNFwg7JTJT5Q9HEecH7FA+HTSsIUw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/@nomicfoundation/solidity-analyzer-darwin-x64": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-darwin-x64/-/solidity-analyzer-darwin-x64-0.1.2.tgz", + "integrity": "sha512-fZNmVztrSXC03e9RONBT+CiksSeYcxI1wlzqyr0L7hsQlK1fzV+f04g2JtQ1c/Fe74ZwdV6aQBdd6Uwl1052sw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/@nomicfoundation/solidity-analyzer-linux-arm64-gnu": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-arm64-gnu/-/solidity-analyzer-linux-arm64-gnu-0.1.2.tgz", + "integrity": "sha512-3d54oc+9ZVBuB6nbp8wHylk4xh0N0Gc+bk+/uJae+rUgbOBwQSfuGIbAZt1wBXs5REkSmynEGcqx6DutoK0tPA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/@nomicfoundation/solidity-analyzer-linux-arm64-musl": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-arm64-musl/-/solidity-analyzer-linux-arm64-musl-0.1.2.tgz", + "integrity": "sha512-iDJfR2qf55vgsg7BtJa7iPiFAsYf2d0Tv/0B+vhtnI16+wfQeTbP7teookbGvAo0eJo7aLLm0xfS/GTkvHIucA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/@nomicfoundation/solidity-analyzer-linux-x64-gnu": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-x64-gnu/-/solidity-analyzer-linux-x64-gnu-0.1.2.tgz", + "integrity": "sha512-9dlHMAt5/2cpWyuJ9fQNOUXFB/vgSFORg1jpjX1Mh9hJ/MfZXlDdHQ+DpFCs32Zk5pxRBb07yGvSHk9/fezL+g==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/@nomicfoundation/solidity-analyzer-linux-x64-musl": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-x64-musl/-/solidity-analyzer-linux-x64-musl-0.1.2.tgz", + "integrity": "sha512-GzzVeeJob3lfrSlDKQw2bRJ8rBf6mEYaWY+gW0JnTDHINA0s2gPR4km5RLIj1xeZZOYz4zRw+AEeYgLRqB2NXg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/@nomicfoundation/solidity-analyzer-win32-x64-msvc": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-win32-x64-msvc/-/solidity-analyzer-win32-x64-msvc-0.1.2.tgz", + "integrity": "sha512-Fdjli4DCcFHb4Zgsz0uEJXZ2K7VEO+w5KVv7HmT7WO10iODdU9csC2az4jrhEsRtiR9Gfd74FlG0NYlw1BMdyA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/@scure/base": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.4.0.tgz", + "integrity": "sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.4.0", + "@noble/hashes": "~1.4.0", + "@scure/base": "~1.1.6" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@noble/curves": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz", + "integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@scure/base": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", + "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.3.0.tgz", + "integrity": "sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.4.0", + "@scure/base": "~1.1.6" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39/node_modules/@scure/base": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", + "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@sentry/core": { + "version": "5.30.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-5.30.0.tgz", + "integrity": "sha512-TmfrII8w1PQZSZgPpUESqjB+jC6MvZJZdLtE/0hZ+SrnKhW3x5WlYLvTXZpcWePYBku7rl2wn1RZu6uT0qCTeg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sentry/hub": "5.30.0", + "@sentry/minimal": "5.30.0", + "@sentry/types": "5.30.0", + "@sentry/utils": "5.30.0", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@sentry/core/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@sentry/hub": { + "version": "5.30.0", + "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-5.30.0.tgz", + "integrity": "sha512-2tYrGnzb1gKz2EkMDQcfLrDTvmGcQPuWxLnJKXJvYTQDGLlEvi2tWz1VIHjunmOvJrB5aIQLhm+dcMRwFZDCqQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sentry/types": "5.30.0", + "@sentry/utils": "5.30.0", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@sentry/hub/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@sentry/minimal": { + "version": "5.30.0", + "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-5.30.0.tgz", + "integrity": "sha512-BwWb/owZKtkDX+Sc4zCSTNcvZUq7YcH3uAVlmh/gtR9rmUvbzAA3ewLuB3myi4wWRAMEtny6+J/FN/x+2wn9Xw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sentry/hub": "5.30.0", + "@sentry/types": "5.30.0", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@sentry/minimal/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@sentry/node": { + "version": "5.30.0", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-5.30.0.tgz", + "integrity": "sha512-Br5oyVBF0fZo6ZS9bxbJZG4ApAjRqAnqFFurMVJJdunNb80brh7a5Qva2kjhm+U6r9NJAB5OmDyPkA1Qnt+QVg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sentry/core": "5.30.0", + "@sentry/hub": "5.30.0", + "@sentry/tracing": "5.30.0", + "@sentry/types": "5.30.0", + "@sentry/utils": "5.30.0", + "cookie": "^0.4.1", + "https-proxy-agent": "^5.0.0", + "lru_map": "^0.3.3", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@sentry/node/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@sentry/tracing": { + "version": "5.30.0", + "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-5.30.0.tgz", + "integrity": "sha512-dUFowCr0AIMwiLD7Fs314Mdzcug+gBVo/+NCMyDw8tFxJkwWAKl7Qa2OZxLQ0ZHjakcj1hNKfCQJ9rhyfOl4Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sentry/hub": "5.30.0", + "@sentry/minimal": "5.30.0", + "@sentry/types": "5.30.0", + "@sentry/utils": "5.30.0", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@sentry/tracing/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@sentry/types": { + "version": "5.30.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-5.30.0.tgz", + "integrity": "sha512-R8xOqlSTZ+htqrfteCWU5Nk0CDN5ApUTvrlvBuiH1DyP6czDZ4ktbZB0hAgBlVcK0U+qpD3ag3Tqqpa5Q67rPw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=6" + } + }, + "node_modules/@sentry/utils": { + "version": "5.30.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-5.30.0.tgz", + "integrity": "sha512-zaYmoH0NWWtvnJjC9/CBseXMtKHm/tm40sz3YfJRxeQjyzRqNQPgivpd9R/oDJCYj999mzdW382p/qi2ypjLww==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sentry/types": "5.30.0", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@sentry/utils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@solidity-parser/parser": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.14.5.tgz", + "integrity": "sha512-6dKnHZn7fg/iQATVEzqyUOyEidbn05q7YA2mQ9hC0MMXhhV3/JrsxmFSYZAcr7j1yUP700LLhTruvJ3MiQmjJg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "antlr4ts": "^0.5.0-alpha.4" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typechain/ethers-v6": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@typechain/ethers-v6/-/ethers-v6-0.5.1.tgz", + "integrity": "sha512-F+GklO8jBWlsaVV+9oHaPh5NJdd6rAKN4tklGfInX1Q7h0xPgVLP39Jl3eCulPB5qexI71ZFHwbljx4ZXNfouA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "lodash": "^4.17.15", + "ts-essentials": "^7.0.1" + }, + "peerDependencies": { + "ethers": "6.x", + "typechain": "^8.3.2", + "typescript": ">=4.7.0" + } + }, + "node_modules/@typechain/hardhat": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@typechain/hardhat/-/hardhat-9.1.0.tgz", + "integrity": "sha512-mtaUlzLlkqTlfPwB3FORdejqBskSnh+Jl8AIJGjXNAQfRQ4ofHADPl1+oU7Z3pAJzmZbUXII8MhOLQltcHgKnA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fs-extra": "^9.1.0" + }, + "peerDependencies": { + "@typechain/ethers-v6": "^0.5.1", + "ethers": "^6.1.0", + "hardhat": "^2.9.9", + "typechain": "^8.3.2" + } + }, + "node_modules/@typechain/hardhat/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@types/bn.js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.2.0.tgz", + "integrity": "sha512-DLbJ1BPqxvQhIGbeu8VbUC1DiAiahHtAYvA0ZEAa4P31F7IaArc8z3C3BRQdWX4mtLQuABG4yzp76ZrS02Ui1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/chai": { + "version": "4.3.20", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.20.tgz", + "integrity": "sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/chai-as-promised": { + "version": "7.1.8", + "resolved": "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-7.1.8.tgz", + "integrity": "sha512-ThlRVIJhr69FLlh6IctTXFkmhtP3NpMZ2QGq69StYLyKZFp/HOp1VdKZj7RvfNWYYcJ1xlbLGLLWj1UvP5u/Gw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/chai": "*" + } + }, + "node_modules/@types/concat-stream": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@types/concat-stream/-/concat-stream-1.6.1.tgz", + "integrity": "sha512-eHE4cQPoj6ngxBZMvVf6Hw7Mh4jMW4U9lpGmS5GBPB9RYxlFg+CHaVN7ErNY4W9XfLIEn20b4VDYaIrbq0q4uA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/form-data": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-0.0.33.tgz", + "integrity": "sha512-8BSvG1kGm83cyJITQMZSulnl6QV8jqAGreJsc5tPu1Jq0vTSOiY/k24Wx82JRpWwZSqrala6sd5rWi6aNXvqcw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/mocha": { + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/node": { + "version": "25.6.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.2.tgz", + "integrity": "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~7.19.0" + } + }, + "node_modules/@types/pbkdf2": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@types/pbkdf2/-/pbkdf2-3.1.2.tgz", + "integrity": "sha512-uRwJqmiXmh9++aSu1VNEn3iIxWOhd8AHXNSdlaLfdAAdSTY9jYVeGWnzejM3dvrkbqE3/hyQkQQ29IFATEGlew==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/prettier": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz", + "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/secp256k1": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@types/secp256k1/-/secp256k1-4.0.7.tgz", + "integrity": "sha512-Rcvjl6vARGAKRO6jHeKMatGrvOMGrR/AR11N1x2LqintPCyDZ7NBhrh238Z2VZc7aM7KIwnFpFQ7fnfK4H/9Qw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/abbrev": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", + "integrity": "sha512-LEyx4aLEC3x6T0UguF6YILf+ntvmOaWsVfENmIW0E9H09vKlLDGelMjjSm0jkDHALj8A8quZ/HapKNigzwge+Q==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/adm-zip": { + "version": "0.4.16", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.16.tgz", + "integrity": "sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.3.0" + } + }, + "node_modules/aes-js": { + "version": "4.0.0-beta.5", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", + "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==", + "dev": true, + "license": "BSD-3-Clause OR MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.4.2" + } + }, + "node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.1.0" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/antlr4ts": { + "version": "0.5.0-alpha.4", + "resolved": "https://registry.npmjs.org/antlr4ts/-/antlr4ts-0.5.0-alpha.4.tgz", + "integrity": "sha512-WPQDt1B74OfPv/IMS2ekXAKkTZIHl88uMetg6q3OTqgFxZ/dxDXI0EWLyZid/1Pe6hTftyg5N7gel5wNAGxXyQ==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-back": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", + "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "*" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/async-mutex": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.4.1.tgz", + "integrity": "sha512-WfoBo4E/TbCX1G95XTjbWTE3X2XLG0m1Xbv2cwOtuPdyH9CZvnaA5nCt1ucjaKEgW2A5IF71hxrRhr83Je5xjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "peer": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axios": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz", + "integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base-x": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.11.tgz", + "integrity": "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/bech32": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", + "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/blakejs": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/blakejs/-/blakejs-1.2.1.tgz", + "integrity": "sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/bn.js": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz", + "integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==", + "license": "MIT" + }, + "node_modules/boxen": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz", + "integrity": "sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-align": "^3.0.0", + "camelcase": "^6.2.0", + "chalk": "^4.1.0", + "cli-boxes": "^2.2.1", + "string-width": "^4.2.2", + "type-fest": "^0.20.2", + "widest-line": "^3.1.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", + "license": "MIT" + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true, + "license": "ISC" + }, + "node_modules/browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "base-x": "^3.0.2" + } + }, + "node_modules/bs58check": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-2.1.2.tgz", + "integrity": "sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "bs58": "^4.0.0", + "create-hash": "^1.1.0", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "dev": true, + "license": "Apache-2.0", + "peer": true + }, + "node_modules/cbor": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/cbor/-/cbor-8.1.0.tgz", + "integrity": "sha512-DwGjNW9omn6EwP70aXsn7FQJx5kO12tX0bZkaTjzdVFM6/7nhA4t0EENocKGx6D2Bch9PE2KzCUf5SceBdeijg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "nofilter": "^3.1.0" + }, + "engines": { + "node": ">=12.19" + } + }, + "node_modules/chai": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", + "dev": true, + "license": "MIT", + "peer": true, + "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" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chai-as-promised": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.2.tgz", + "integrity": "sha512-aBDHZxRzYnUYuIAIPBH2s511DjlKPzXNlXSGFC8CwmroWQLfrW0LtE1nK3MAwwNhJPa9raEjNCmRoFpG0Hurdw==", + "dev": true, + "license": "WTFPL", + "peer": true, + "dependencies": { + "check-error": "^1.0.2" + }, + "peerDependencies": { + "chai": ">= 2.1.2 < 6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": "*" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cipher-base": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.7.tgz", + "integrity": "sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-boxes": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", + "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-table3": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.5.1.tgz", + "integrity": "sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "object-assign": "^4.1.0", + "string-width": "^2.1.1" + }, + "engines": { + "node": ">=6" + }, + "optionalDependencies": { + "colors": "^1.1.2" + } + }, + "node_modules/cli-table3/node_modules/ansi-regex": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/cli-table3/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/cli-table3/node_modules/string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cli-table3/node_modules/strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/command-exists": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz", + "integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==", + "dev": true, + "license": "MIT" + }, + "node_modules/command-line-args": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.1.tgz", + "integrity": "sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "array-back": "^3.1.0", + "find-replace": "^3.0.0", + "lodash.camelcase": "^4.3.0", + "typical": "^4.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/command-line-usage": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-6.1.3.tgz", + "integrity": "sha512-sH5ZSPr+7UStsloltmDh7Ce5fb8XPlHyoPzTpyyMuYCtervL65+ubVZ6Q61cFtFl62UyJlc8/JwERRbAFPUqgw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "array-back": "^4.0.2", + "chalk": "^2.4.2", + "table-layout": "^1.0.2", + "typical": "^5.2.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/command-line-usage/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/command-line-usage/node_modules/array-back": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.2.tgz", + "integrity": "sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/command-line-usage/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/command-line-usage/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/command-line-usage/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/command-line-usage/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/command-line-usage/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/command-line-usage/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/command-line-usage/node_modules/typical": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", + "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "engines": [ + "node >= 0.8" + ], + "license": "MIT", + "peer": true, + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/concat-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "peer": true, + "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" + } + }, + "node_modules/concat-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/concat-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "node_modules/create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": "*" + } + }, + "node_modules/death": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/death/-/death-1.1.0.tgz", + "integrity": "sha512-vsV6S4KVHvTGxbEcij7hkWRv0It+sGGWVOM67dQde/o5Xjnr+KmLjxWJii2uEObIrt1CcM9w0Yaovx+iOlIL+w==", + "dev": true, + "peer": true + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/diff": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz", + "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/difflib": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/difflib/-/difflib-0.2.4.tgz", + "integrity": "sha512-9YVwmMb0wQHQNr5J9m6BSj6fk4pfGITGQOOs+D9Fl+INODWFOfvhIU1hNv6GgR1RBoC/9NJcwu77zShxV0kT7w==", + "dev": true, + "peer": true, + "dependencies": { + "heap": ">= 0.2.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/elliptic": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", + "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/elliptic/node_modules/bn.js": { + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escodegen": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz", + "integrity": "sha512-yhi5S+mNTOuRvyW4gWlg5W1byMaQGWWSYHXsuFZ7GBo7tpyOwi2EdzMP/QWxh9hwkD2m+wDVHJsxhRIj+v/b/A==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "esprima": "^2.7.1", + "estraverse": "^1.9.1", + "esutils": "^2.0.2", + "optionator": "^0.8.1" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=0.12.0" + }, + "optionalDependencies": { + "source-map": "~0.2.0" + } + }, + "node_modules/esprima": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha512-OarPfz0lFCiW4/AV2Oy1Rp9qu0iusTKqykwTspGCZtPxmF81JR4MmIebvF1F9+UOKth2ZubLQ4XGGaU+hSn99A==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/estraverse": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", + "integrity": "sha512-25w1fMXQrGdoquWnScXZGckOv+Wes+JDnuN/+7ex3SauFRS72r2lFDec0EKPt2YD1wUJ/IrfEex+9yp4hfSOJA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eth-gas-reporter": { + "version": "0.2.27", + "resolved": "https://registry.npmjs.org/eth-gas-reporter/-/eth-gas-reporter-0.2.27.tgz", + "integrity": "sha512-femhvoAM7wL0GcI8ozTdxfuBtBFJ9qsyIAsmKVjlWAHUbdnnXHt+lKzz/kmldM5lA9jLuNHGwuIxorNpLbR1Zw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@solidity-parser/parser": "^0.14.0", + "axios": "^1.5.1", + "cli-table3": "^0.5.0", + "colors": "1.4.0", + "ethereum-cryptography": "^1.0.3", + "ethers": "^5.7.2", + "fs-readdir-recursive": "^1.1.0", + "lodash": "^4.17.14", + "markdown-table": "^1.1.3", + "mocha": "^10.2.0", + "req-cwd": "^2.0.0", + "sha1": "^1.1.1", + "sync-request": "^6.0.0" + }, + "peerDependencies": { + "@codechecks/client": "^0.1.0" + }, + "peerDependenciesMeta": { + "@codechecks/client": { + "optional": true + } + } + }, + "node_modules/eth-gas-reporter/node_modules/@noble/hashes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.2.0.tgz", + "integrity": "sha512-FZfhjEDbT5GRswV3C6uvLPHMiVD6lQBmpoX5+eSiPaMTXte/IKqI5dykDxzZB/WBeK/CDuQRBWarPdi3FNY2zQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT", + "peer": true + }, + "node_modules/eth-gas-reporter/node_modules/@scure/base": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", + "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/eth-gas-reporter/node_modules/@scure/bip32": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.1.5.tgz", + "integrity": "sha512-XyNh1rB0SkEqd3tXcXMi+Xe1fvg+kUIcoRIEujP1Jgv7DqW2r9lg3Ah0NkFaCs9sTkQAQA8kw7xiRXzENi9Rtw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "@noble/hashes": "~1.2.0", + "@noble/secp256k1": "~1.7.0", + "@scure/base": "~1.1.0" + } + }, + "node_modules/eth-gas-reporter/node_modules/@scure/bip39": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.1.1.tgz", + "integrity": "sha512-t+wDck2rVkh65Hmv280fYdVdY25J9YeEUIgn2LG1WM6gxFkGzcksoDiUkWVpVp3Oex9xGC68JU2dSbUfwZ2jPg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "@noble/hashes": "~1.2.0", + "@scure/base": "~1.1.0" + } + }, + "node_modules/eth-gas-reporter/node_modules/ethereum-cryptography": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-1.2.0.tgz", + "integrity": "sha512-6yFQC9b5ug6/17CQpCyE3k9eKBMdhyVjzUy1WkiuY/E4vj/SXDBbCw8QEIaXqf0Mf2SnY6RmpDcwlUmBSS0EJw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@noble/hashes": "1.2.0", + "@noble/secp256k1": "1.7.1", + "@scure/bip32": "1.1.5", + "@scure/bip39": "1.1.1" + } + }, + "node_modules/eth-gas-reporter/node_modules/ethers": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-5.8.0.tgz", + "integrity": "sha512-DUq+7fHrCg1aPDFCHx6UIPb3nmt2XMpM7Y/g2gLhsl3lIBqeAfOJIl1qEvRf2uq3BiKxmh6Fh5pfp2ieyek7Kg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "@ethersproject/abi": "5.8.0", + "@ethersproject/abstract-provider": "5.8.0", + "@ethersproject/abstract-signer": "5.8.0", + "@ethersproject/address": "5.8.0", + "@ethersproject/base64": "5.8.0", + "@ethersproject/basex": "5.8.0", + "@ethersproject/bignumber": "5.8.0", + "@ethersproject/bytes": "5.8.0", + "@ethersproject/constants": "5.8.0", + "@ethersproject/contracts": "5.8.0", + "@ethersproject/hash": "5.8.0", + "@ethersproject/hdnode": "5.8.0", + "@ethersproject/json-wallets": "5.8.0", + "@ethersproject/keccak256": "5.8.0", + "@ethersproject/logger": "5.8.0", + "@ethersproject/networks": "5.8.0", + "@ethersproject/pbkdf2": "5.8.0", + "@ethersproject/properties": "5.8.0", + "@ethersproject/providers": "5.8.0", + "@ethersproject/random": "5.8.0", + "@ethersproject/rlp": "5.8.0", + "@ethersproject/sha2": "5.8.0", + "@ethersproject/signing-key": "5.8.0", + "@ethersproject/solidity": "5.8.0", + "@ethersproject/strings": "5.8.0", + "@ethersproject/transactions": "5.8.0", + "@ethersproject/units": "5.8.0", + "@ethersproject/wallet": "5.8.0", + "@ethersproject/web": "5.8.0", + "@ethersproject/wordlists": "5.8.0" + } + }, + "node_modules/ethereum-bloom-filters": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ethereum-bloom-filters/-/ethereum-bloom-filters-1.2.0.tgz", + "integrity": "sha512-28hyiE7HVsWubqhpVLVmZXFd4ITeHi+BUu05o9isf0GUpMtzBUi+8/gFrGaGYzvGAJQmJ3JKj77Mk9G98T84rA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@noble/hashes": "^1.4.0" + } + }, + "node_modules/ethereum-bloom-filters/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ethereum-cryptography": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-0.1.3.tgz", + "integrity": "sha512-w8/4x1SGGzc+tO97TASLja6SLd3fRIK2tLVcV2Gx4IB21hE19atll5Cq9o3d0ZmAYC/8aw0ipieTSiekAea4SQ==", + "dev": true, + "license": "MIT", + "peer": true, + "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" + } + }, + "node_modules/ethereumjs-util": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-7.1.5.tgz", + "integrity": "sha512-SDl5kKrQAudFBUe5OJM9Ac6WmMyYmXX/6sTmLZ3ffG2eY6ZIGBes3pEDxNN6V72WyOw4CPD5RomKdsa8DAAwLg==", + "dev": true, + "license": "MPL-2.0", + "peer": true, + "dependencies": { + "@types/bn.js": "^5.1.0", + "bn.js": "^5.1.2", + "create-hash": "^1.1.2", + "ethereum-cryptography": "^0.1.3", + "rlp": "^2.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/ethers": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.16.0.tgz", + "integrity": "sha512-U1wulmetNymijEhpSEQ7Ct/P/Jw9/e7R1j5XIbPRydgV2DjLVMsULDlNksq3RQnFgKoLlZf88ijYtWEXcPa07A==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/ethers-io/" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "1.10.1", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", + "@types/node": "22.7.5", + "aes-js": "4.0.0-beta.5", + "tslib": "2.7.0", + "ws": "8.17.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ethers/node_modules/@types/node": { + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/ethers/node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ethjs-unit": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/ethjs-unit/-/ethjs-unit-0.1.6.tgz", + "integrity": "sha512-/Sn9Y0oKl0uqQuvgFk/zQgR7aw1g36qX/jzSQ5lSwlO0GigPymk4eGQfeNTD03w1dPOqfz8V77Cy43jH56pagw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "bn.js": "4.11.6", + "number-to-bn": "1.7.0" + }, + "engines": { + "node": ">=6.5.0", + "npm": ">=3" + } + }, + "node_modules/ethjs-unit/node_modules/bn.js": { + "version": "4.11.6", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.6.tgz", + "integrity": "sha512-XWwnNNFCuuSQ0m3r3C4LE3EiORltHd9M05pq6FOlVeiophzRbMo50Sbz1ehl8K3Z+jw9+vmgnXefY1hz8X+2wA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-replace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz", + "integrity": "sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "array-back": "^3.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fp-ts": { + "version": "1.19.3", + "resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-1.19.3.tgz", + "integrity": "sha512-H5KQDspykdHuztLTg+ajGN0Z2qUjcEf3Ybxc6hLt0k7/zPkn29XnKnxlBPyW2XIddWrGaJBzBl4VLYOtk39yZg==", + "dev": true, + "license": "MIT" + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs-readdir-recursive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", + "integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-port": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz", + "integrity": "sha512-x5UJKlgeUiNT8nyo/AcnwLnZuZNcSjSw0kogRB+Whd1fjjFq4B1hySFxSFWWSn4mIBzg3sRNUDFYc4g5gjPoLg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ghost-testrpc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/ghost-testrpc/-/ghost-testrpc-0.0.2.tgz", + "integrity": "sha512-i08dAEgJ2g8z5buJIrCTduwPIhih3DP+hOCTyyryikfV8T0bNvHnGXO67i0DD1H4GBDETTclPy9njZbfluQYrQ==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "chalk": "^2.4.2", + "node-emoji": "^1.10.0" + }, + "bin": { + "testrpc-sc": "index.js" + } + }, + "node_modules/ghost-testrpc/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ghost-testrpc/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ghost-testrpc/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/ghost-testrpc/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/ghost-testrpc/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/ghost-testrpc/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/ghost-testrpc/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/global-modules": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", + "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "global-prefix": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/global-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", + "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ini": "^1.3.5", + "kind-of": "^6.0.2", + "which": "^1.3.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/globby": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-10.0.2.tgz", + "integrity": "sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/glob": "^7.1.1", + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.0.3", + "glob": "^7.1.3", + "ignore": "^5.1.1", + "merge2": "^1.2.3", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/globby/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/globby/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globby/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/handlebars": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/handlebars/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/hardhat": { + "version": "2.28.6", + "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.28.6.tgz", + "integrity": "sha512-zQze7qe+8ltwHvhX5NQ8sN1N37WWZGw8L63y+2XcPxGwAjc/SMF829z3NS6o1krX0sryhAsVBK/xrwUqlsot4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ethereumjs/util": "^9.1.0", + "@ethersproject/abi": "^5.1.2", + "@nomicfoundation/edr": "0.12.0-next.23", + "@nomicfoundation/solidity-analyzer": "^0.1.0", + "@sentry/node": "^5.18.1", + "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", + "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", + "micro-eth-signer": "^0.14.0", + "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" + }, + "bin": { + "hardhat": "internal/cli/bootstrap.js" + }, + "peerDependencies": { + "ts-node": "*", + "typescript": "*" + }, + "peerDependenciesMeta": { + "ts-node": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/hardhat-gas-reporter": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/hardhat-gas-reporter/-/hardhat-gas-reporter-1.0.10.tgz", + "integrity": "sha512-02N4+So/fZrzJ88ci54GqwVA3Zrf0C9duuTyGt0CFRIh/CdNwbnTgkXkRfojOMLBQ+6t+lBIkgbsOtqMvNwikA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "array-uniq": "1.0.3", + "eth-gas-reporter": "^0.2.25", + "sha1": "^1.1.1" + }, + "peerDependencies": { + "hardhat": "^2.0.2" + } + }, + "node_modules/hardhat/node_modules/@noble/hashes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.2.0.tgz", + "integrity": "sha512-FZfhjEDbT5GRswV3C6uvLPHMiVD6lQBmpoX5+eSiPaMTXte/IKqI5dykDxzZB/WBeK/CDuQRBWarPdi3FNY2zQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT" + }, + "node_modules/hardhat/node_modules/@scure/base": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", + "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/hardhat/node_modules/@scure/bip32": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.1.5.tgz", + "integrity": "sha512-XyNh1rB0SkEqd3tXcXMi+Xe1fvg+kUIcoRIEujP1Jgv7DqW2r9lg3Ah0NkFaCs9sTkQAQA8kw7xiRXzENi9Rtw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.2.0", + "@noble/secp256k1": "~1.7.0", + "@scure/base": "~1.1.0" + } + }, + "node_modules/hardhat/node_modules/@scure/bip39": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.1.1.tgz", + "integrity": "sha512-t+wDck2rVkh65Hmv280fYdVdY25J9YeEUIgn2LG1WM6gxFkGzcksoDiUkWVpVp3Oex9xGC68JU2dSbUfwZ2jPg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.2.0", + "@scure/base": "~1.1.0" + } + }, + "node_modules/hardhat/node_modules/ethereum-cryptography": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-1.2.0.tgz", + "integrity": "sha512-6yFQC9b5ug6/17CQpCyE3k9eKBMdhyVjzUy1WkiuY/E4vj/SXDBbCw8QEIaXqf0Mf2SnY6RmpDcwlUmBSS0EJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.2.0", + "@noble/secp256k1": "1.7.1", + "@scure/bip32": "1.1.5", + "@scure/bip39": "1.1.1" + } + }, + "node_modules/hardhat/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/hardhat/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/hardhat/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/hardhat/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hash-base": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.2.tgz", + "integrity": "sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "^2.3.8", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/hash-base/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/hash-base/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "peer": true, + "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" + } + }, + "node_modules/hash-base/node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/hash-base/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/hash-base/node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/heap": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/heap/-/heap-0.2.7.tgz", + "integrity": "sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "license": "MIT", + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/http-basic": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/http-basic/-/http-basic-8.1.3.tgz", + "integrity": "sha512-/EcDMwJZh3mABI2NhGfHOGOeOZITqfkEO4p/xK+l3NpyncIHUQBoMvCSF/b5GqvKtySC2srL/GGG3+EtlqlmCw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "caseless": "^0.12.0", + "concat-stream": "^1.6.2", + "http-response-object": "^3.0.1", + "parse-cache-control": "^1.0.1" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-response-object": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-3.0.2.tgz", + "integrity": "sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "^10.0.3" + } + }, + "node_modules/http-response-object/node_modules/@types/node": { + "version": "10.17.60", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", + "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/immer": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.0.2.tgz", + "integrity": "sha512-Rx3CqeqQ19sxUtYV9CU911Vhy8/721wRFnJv3REVGWUmoAcIwzifTsdmJte/MV+0/XpM35LZdQMBGkRIoLPwQA==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/immutable": { + "version": "4.3.8", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.8.tgz", + "integrity": "sha512-d/Ld9aLbKpNwyl0KiM2CT1WYvkitQ1TSvmRtkcV8FKStiDoA7Slzgjmb/1G2yhKM1p0XeNOieaTbFZmU1d3Xuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/io-ts": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/io-ts/-/io-ts-1.10.4.tgz", + "integrity": "sha512-b23PteSnYXSONJ6JQXRAlvJhuw8KOtkqa87W4wDtvMrud/DTJd5X+NpOOI+O/zZwVq6v0VLAaJ+1EDViKEuN9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fp-ts": "^1.0.0" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hex-prefixed": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-hex-prefixed/-/is-hex-prefixed-1.0.0.tgz", + "integrity": "sha512-WvtOiug1VFrE9v1Cydwm+FnXd3+w9GaeVUss5W4v/SLy3UW00vP+6iNF2SdnfiBoLy4bTqVdkftNGTUeOFVsbA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.5.0", + "npm": ">=3" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/js-sha3": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/json-stream-stringify": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/json-stream-stringify/-/json-stream-stringify-3.1.6.tgz", + "integrity": "sha512-x7fpwxOkbhFCaJDJ8vb1fBY3DdSa4AlITaz+HHILQJzdPMnHEFjxPwVUi1ALIbcIxDE0PNe/0i7frnY8QnBQog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=7.10.1" + } + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonschema": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.5.0.tgz", + "integrity": "sha512-K+A9hhqbn0f3pJX17Q/7H6yQfD/5OXgdrR5UE12gMXCiN9D5Xq2o5mddV2QEcX/bjla99ASsAAQUyMCCRWAEhw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "*" + } + }, + "node_modules/keccak": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.4.tgz", + "integrity": "sha512-3vKuW0jV8J3XNTzvfyicFR5qvxrSAGl7KIhvgOu5cmWwM7tZRj3fMbj/pfIf4be7aznbc+prBWGjywox/g2Y6Q==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^2.0.0", + "node-gyp-build": "^4.2.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/lru_map": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/lru_map/-/lru_map-0.3.3.tgz", + "integrity": "sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/markdown-table": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-1.1.3.tgz", + "integrity": "sha512-1RUZVgQlpJSPWYbFSpmudq5nHY1doEIv89gBtF0s4gW1GF2XorxcA/70M5vq7rLv0a6mhOUccRsqkwhwLCIQ2Q==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==", + "dev": true, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micro-eth-signer": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/micro-eth-signer/-/micro-eth-signer-0.14.0.tgz", + "integrity": "sha512-5PLLzHiVYPWClEvZIXXFu5yutzpadb73rnQCpUqIHu3No3coFuWQNfE5tkBQJ7djuLYl6aRLaS0MgWJYGoqiBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.8.1", + "@noble/hashes": "~1.7.1", + "micro-packed": "~0.7.2" + } + }, + "node_modules/micro-eth-signer/node_modules/@noble/curves": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.2.tgz", + "integrity": "sha512-vnI7V6lFNe0tLAuJMu+2sX+FcL14TaCWy1qiczg1VwRmPrpQCdq5ESXQMqUc2tluRNf6irBXrWbl1mGN8uaU/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.7.2" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/micro-eth-signer/node_modules/@noble/hashes": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.2.tgz", + "integrity": "sha512-biZ0NUSxyjLLqo6KxEJ1b+C2NAx0wtDoFvCaXHGgUkeHzf3Xc1xKumFKREuT7f7DARNZ/slvYUwFG6B0f2b6hQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/micro-ftch": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/micro-ftch/-/micro-ftch-0.3.1.tgz", + "integrity": "sha512-/0LLxhzP0tfiR5hcQebtudP56gUurs2CLkGarnCiB/OqEyUFQ6U3paQi/tgLv0hBJYt2rnr9MNpxz4fiiugstg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/micro-packed": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/micro-packed/-/micro-packed-0.7.3.tgz", + "integrity": "sha512-2Milxs+WNC00TRlem41oRswvw31146GiSaoCT7s3Xi2gMUglW5QBeqlQaZeHr5tJx9nm3i57LNXPqxOOaWtTYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "license": "ISC" + }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", + "license": "MIT" + }, + "node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mnemonist": { + "version": "0.38.5", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.38.5.tgz", + "integrity": "sha512-bZTFT5rrPKtPJxj8KSV0WkPyNxl72vQepqqVUAW2ARUpUSF2qXMB6jZj7hW5/k7C1rtpzqbD/IIbJwLXUjCHeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "obliterator": "^2.0.0" + } + }, + "node_modules/mocha": { + "version": "10.8.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", + "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/mocha/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/mocha/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ndjson": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ndjson/-/ndjson-2.0.0.tgz", + "integrity": "sha512-nGl7LRGrzugTtaFcJMhLbpzJM6XdivmbkdlaGcrk/LXg2KL/YBC6z1g70xh0/al+oFuVFP8N8kiWRucmeEH/qQ==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "json-stringify-safe": "^5.0.1", + "minimist": "^1.2.5", + "readable-stream": "^3.6.0", + "split2": "^3.0.0", + "through2": "^4.0.0" + }, + "bin": { + "ndjson": "cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/node-addon-api": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz", + "integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-emoji": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", + "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "dev": true, + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/nofilter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-3.1.0.tgz", + "integrity": "sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12.19" + } + }, + "node_modules/nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha512-4GUt3kSEYmk4ITxzB/b9vaIDfUVWN/Ml1Fwl11IlnIG2iaJ9O6WXZ9SrYM9NLI8OCBieN2Y8SWC2oJV0RQ7qYg==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/number-to-bn": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/number-to-bn/-/number-to-bn-1.7.0.tgz", + "integrity": "sha512-wsJ9gfSz1/s4ZsJN01lyonwuxA1tml6X1yBDnfpMglypcBRFZZkus26EdPSlqS5GJfYddVZa22p3VNb3z5m5Ig==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "bn.js": "4.11.6", + "strip-hex-prefix": "1.0.0" + }, + "engines": { + "node": ">=6.5.0", + "npm": ">=3" + } + }, + "node_modules/number-to-bn/node_modules/bn.js": { + "version": "4.11.6", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.6.tgz", + "integrity": "sha512-XWwnNNFCuuSQ0m3r3C4LE3EiORltHd9M05pq6FOlVeiophzRbMo50Sbz1ehl8K3Z+jw9+vmgnXefY1hz8X+2wA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obliterator": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.5.tgz", + "integrity": "sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==", + "dev": true, + "license": "MIT" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ordinal": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ordinal/-/ordinal-1.0.3.tgz", + "integrity": "sha512-cMddMgb2QElm8G7vdaa02jhUNbTSrhsgAGUz1OokD83uJTwSUn+nKoNoKVVaRa08yF6sgfO7Maou1+bgLd9rdQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-cache-control": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz", + "integrity": "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==", + "dev": true, + "peer": true + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "*" + } + }, + "node_modules/pbkdf2": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.5.tgz", + "integrity": "sha512-Q3CG/cYvCO1ye4QKkuH7EXxs3VC/rI1/trd+qX2+PolbaKG0H+bgcZzrTt96mMyRtejk+JMCiLUn3y29W8qmFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "ripemd160": "^2.0.3", + "safe-buffer": "^5.2.1", + "sha.js": "^2.4.12", + "to-buffer": "^1.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/promise": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz", + "integrity": "sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "asap": "~2.0.6" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "peer": true + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", + "dev": true, + "peer": true, + "dependencies": { + "resolve": "^1.1.6" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/recursive-readdir": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", + "integrity": "sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/recursive-readdir/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/recursive-readdir/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/reduce-flatten": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-2.0.0.tgz", + "integrity": "sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/req-cwd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/req-cwd/-/req-cwd-2.0.0.tgz", + "integrity": "sha512-ueoIoLo1OfB6b05COxAA9UpeoscNpYyM+BqYlA7H6LVF4hKGPXQQSSaD2YmvDVJMkk4UDpAHIeU1zG53IqjvlQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "req-from": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/req-from": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/req-from/-/req-from-2.0.0.tgz", + "integrity": "sha512-LzTfEVDVQHBRfjOUMgNBA+V6DWsSnoeKzf42J7l0xa/B4jyPOuuF5MlNSmomLNGemWTnV2TIdjSSLnEn95fOQA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "resolve-from": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", + "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/ripemd160": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.3.tgz", + "integrity": "sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "hash-base": "^3.1.2", + "inherits": "^2.0.4" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rlp": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/rlp/-/rlp-2.2.7.tgz", + "integrity": "sha512-d5gdPmgQ0Z+AklL2NVXr/IoSjNZFfTVvQWzL/AM2AOcSzYP2xjlb0AC8YyCLc41MSNf6P6QVtjgPdmVtzb+4lQ==", + "dev": true, + "license": "MPL-2.0", + "peer": true, + "dependencies": { + "bn.js": "^5.2.0" + }, + "bin": { + "rlp": "bin/rlp" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sc-istanbul": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/sc-istanbul/-/sc-istanbul-0.4.6.tgz", + "integrity": "sha512-qJFF/8tW/zJsbyfh/iT/ZM5QNHE3CXxtLJbZsL+CzdJLBsPD7SedJZoUA4d8iAcN2IoMp/Dx80shOOd2x96X/g==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "abbrev": "1.0.x", + "async": "1.x", + "escodegen": "1.8.x", + "esprima": "2.7.x", + "glob": "^5.0.15", + "handlebars": "^4.0.1", + "js-yaml": "3.x", + "mkdirp": "0.5.x", + "nopt": "3.x", + "once": "1.x", + "resolve": "1.1.x", + "supports-color": "^3.1.0", + "which": "^1.1.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "istanbul": "lib/cli.js" + } + }, + "node_modules/sc-istanbul/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/sc-istanbul/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/sc-istanbul/node_modules/glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha512-c9IPMazfRITpmAAKi22dK1VKxGDX9ehhqfABDriL/lzO92xcUKEJPQHrVA/2YHSNFB4iFlykVmWvwo48nr3OxA==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/sc-istanbul/node_modules/has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha512-DyYHfIYwAJmjAjSSPKANxI8bFY9YtFrgkAfinBojQ8YJTOuOuav64tMUJv584SES4xl74PmuaevIyaLESHdTAA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sc-istanbul/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/sc-istanbul/node_modules/js-yaml/node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/sc-istanbul/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/sc-istanbul/node_modules/resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha512-9znBF0vBcaSN3W2j7wKvdERPwqTxSpCq+if5C0WoTCyV9n24rua28jeuQ2pL/HOf+yUe/Mef+H/5p60K0Id3bg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/sc-istanbul/node_modules/supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha512-Jds2VIYDrlp5ui7t8abHN2bjAu4LV/q4N2KivFPpGH0lrka0BMq/33AmECUXlKPcHigkNaqfXRENFju+rlcy+A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "has-flag": "^1.0.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/scrypt-js": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/scrypt-js/-/scrypt-js-3.0.1.tgz", + "integrity": "sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA==", + "dev": true, + "license": "MIT" + }, + "node_modules/secp256k1": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-4.0.4.tgz", + "integrity": "sha512-6JfvwvjUOn8F/jUoBY2Q1v5WY5XS+rj8qSe0v8Y4ezH4InLgTEeOOPQsRll9OV429Pvo6BCHGavIyJfr3TAhsw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "dependencies": { + "elliptic": "^6.5.7", + "node-addon-api": "^5.0.0", + "node-gyp-build": "^4.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/secp256k1/node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, + "node_modules/sha.js": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "dev": true, + "license": "(MIT AND BSD-3-Clause)", + "peer": true, + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" + }, + "bin": { + "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sha1": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/sha1/-/sha1-1.1.1.tgz", + "integrity": "sha512-dZBS6OrMjtgVkopB1Gmo4RQCDKiZsqcpAQpkV/aaj+FCrCg8r4I4qMkDPQjBgLIxlmu9k4nUbWq6ohXahOneYA==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "charenc": ">= 0.0.1", + "crypt": ">= 0.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/shelljs": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", + "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + }, + "bin": { + "shjs": "bin/shjs" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/shelljs/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/shelljs/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/shelljs/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/solc": { + "version": "0.8.26", + "resolved": "https://registry.npmjs.org/solc/-/solc-0.8.26.tgz", + "integrity": "sha512-yiPQNVf5rBFHwN6SIf3TUUvVAFKcQqmSUFeq+fb6pNRCo0ZCgpYOZDi3BVoezCPIAcKrVYd/qXlBLUP9wVrZ9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "command-exists": "^1.2.8", + "commander": "^8.1.0", + "follow-redirects": "^1.12.1", + "js-sha3": "0.8.0", + "memorystream": "^0.3.1", + "semver": "^5.5.0", + "tmp": "0.0.33" + }, + "bin": { + "solcjs": "solc.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/solc/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/solidity-coverage": { + "version": "0.8.17", + "resolved": "https://registry.npmjs.org/solidity-coverage/-/solidity-coverage-0.8.17.tgz", + "integrity": "sha512-5P8vnB6qVX9tt1MfuONtCTEaEGO/O4WuEidPHIAJjx4sktHHKhO3rFvnE0q8L30nWJPTrcqGQMT7jpE29B2qow==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "@ethersproject/abi": "^5.0.9", + "@solidity-parser/parser": "^0.20.1", + "chalk": "^2.4.2", + "death": "^1.1.0", + "difflib": "^0.2.4", + "fs-extra": "^8.1.0", + "ghost-testrpc": "^0.0.2", + "global-modules": "^2.0.0", + "globby": "^10.0.1", + "jsonschema": "^1.2.4", + "lodash": "^4.17.21", + "mocha": "^10.2.0", + "node-emoji": "^1.10.0", + "pify": "^4.0.1", + "recursive-readdir": "^2.2.2", + "sc-istanbul": "^0.4.5", + "semver": "^7.3.4", + "shelljs": "^0.8.3", + "web3-utils": "^1.3.6" + }, + "bin": { + "solidity-coverage": "plugins/bin.js" + }, + "peerDependencies": { + "hardhat": "^2.11.0" + } + }, + "node_modules/solidity-coverage/node_modules/@solidity-parser/parser": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.20.2.tgz", + "integrity": "sha512-rbu0bzwNvMcwAjH86hiEAcOeRI2EeK8zCkHDrFykh/Al8mvJeFmjy3UrE7GYQjNwOgbGUUtCn5/k8CB8zIu7QA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/solidity-coverage/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/solidity-coverage/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/solidity-coverage/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/solidity-coverage/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/solidity-coverage/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/solidity-coverage/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/solidity-coverage/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/solidity-coverage/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "peer": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/solidity-coverage/node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "dev": true, + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/solidity-coverage/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/solidity-coverage/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/source-map": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", + "integrity": "sha512-CBdZ2oa/BHhS4xj5DlhjWNHcan57/5YuvfdLf17iVmIpd9KRm+DFLmC6nBNj+6Ua7Kt3TmOjDpQT1aTYOQtoUA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "amdefine": ">=0.0.4" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", + "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "readable-stream": "^3.0.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/stacktrace-parser": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.11.tgz", + "integrity": "sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.7.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/stacktrace-parser/node_modules/type-fest": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", + "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-format": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/string-format/-/string-format-2.0.0.tgz", + "integrity": "sha512-bbEs3scLeYNXLecRRuk6uJxdXUSj6le/8rNPHChIJTn2V79aXVTR1EH2OH5zLKKoz0V02fOUKZZcw01pLUShZA==", + "dev": true, + "license": "WTFPL OR MIT", + "peer": true + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-hex-prefix": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-hex-prefix/-/strip-hex-prefix-1.0.0.tgz", + "integrity": "sha512-q8d4ue7JGEiVcypji1bALTos+0pWtyGlivAWyPuTkHzuTCJqrK9sWxYQZUq6Nq3cuyv3bm734IhHvHtGGURU6A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "is-hex-prefixed": "1.0.0" + }, + "engines": { + "node": ">=6.5.0", + "npm": ">=3" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sync-request": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/sync-request/-/sync-request-6.1.0.tgz", + "integrity": "sha512-8fjNkrNlNCrVc/av+Jn+xxqfCjYaBoHqCsDz6mt030UMxJGr+GSfCV1dQt2gRtlL63+VPidwDVLr7V2OcTSdRw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "http-response-object": "^3.0.1", + "sync-rpc": "^1.2.1", + "then-request": "^6.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/sync-rpc": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/sync-rpc/-/sync-rpc-1.3.6.tgz", + "integrity": "sha512-J8jTXuZzRlvU7HemDgHi3pGnh/rkoqR/OZSjhTyyZrEkkYQbk7Z33AXp37mkPfPpfdOuj7Ex3H/TJM1z48uPQw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "get-port": "^3.1.0" + } + }, + "node_modules/table": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", + "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/table-layout": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-1.0.2.tgz", + "integrity": "sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "array-back": "^4.0.1", + "deep-extend": "~0.6.0", + "typical": "^5.2.0", + "wordwrapjs": "^4.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/table-layout/node_modules/array-back": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.2.tgz", + "integrity": "sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/table-layout/node_modules/typical": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", + "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/then-request": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/then-request/-/then-request-6.0.2.tgz", + "integrity": "sha512-3ZBiG7JvP3wbDzA9iNY5zJQcHL4jn/0BWtXIkagfz7QgOL/LqjCEOBQuJNZfu0XYnv5JhKh+cDxCPM4ILrqruA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/concat-stream": "^1.6.0", + "@types/form-data": "0.0.33", + "@types/node": "^8.0.0", + "@types/qs": "^6.2.31", + "caseless": "~0.12.0", + "concat-stream": "^1.6.0", + "form-data": "^2.2.0", + "http-basic": "^8.1.1", + "http-response-object": "^3.0.1", + "promise": "^8.0.0", + "qs": "^6.4.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/then-request/node_modules/@types/node": { + "version": "8.10.66", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.66.tgz", + "integrity": "sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/then-request/node_modules/form-data": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz", + "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.35", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/through2": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", + "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "readable-stream": "3" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/ts-command-line-args": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/ts-command-line-args/-/ts-command-line-args-2.5.1.tgz", + "integrity": "sha512-H69ZwTw3rFHb5WYpQya40YAX2/w7Ut75uUECbgBIsLmM+BNuYnxsltfyyLMxy6sEeKxgijLTnQtLd0nKd6+IYw==", + "dev": true, + "license": "ISC", + "peer": true, + "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" + } + }, + "node_modules/ts-essentials": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-7.0.3.tgz", + "integrity": "sha512-8+gr5+lqO3G84KdiTSMRLtuyJ+nTBVRKuCrK4lidMPdVeEp0uqC875uE5NMcaA7YYMN7XsNiFQuMvasF8HT/xQ==", + "dev": true, + "license": "MIT", + "peer": true, + "peerDependencies": { + "typescript": ">=3.7.0" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tsort": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/tsort/-/tsort-0.0.1.tgz", + "integrity": "sha512-Tyrf5mxF8Ofs1tNoxA13lFeZ2Zrbd6cKbuH3V+MQ5sb6DtBj5FjrXVsRWT8YvNAQTqNoz66dz1WsbigI22aEnw==", + "dev": true, + "license": "MIT" + }, + "node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typechain": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/typechain/-/typechain-8.3.2.tgz", + "integrity": "sha512-x/sQYr5w9K7yv3es7jo4KTX05CLxOf7TRWwoHlrjRh8H82G64g+k7VuWPJlgMo6qrjfCulOdfBjiaDtmhFYD/Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/prettier": "^2.1.1", + "debug": "^4.3.1", + "fs-extra": "^7.0.0", + "glob": "7.1.7", + "js-sha3": "^0.8.0", + "lodash": "^4.17.15", + "mkdirp": "^1.0.4", + "prettier": "^2.3.1", + "ts-command-line-args": "^2.2.0", + "ts-essentials": "^7.0.1" + }, + "bin": { + "typechain": "dist/cli/cli.js" + }, + "peerDependencies": { + "typescript": ">=4.3.0" + } + }, + "node_modules/typechain/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/typechain/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/typechain/node_modules/glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typechain/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "peer": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/typechain/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/typechain/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/typechain/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typical": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz", + "integrity": "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "peer": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/undici": { + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", + "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utf8": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/utf8/-/utf8-3.0.0.tgz", + "integrity": "sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/web3-utils": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.10.4.tgz", + "integrity": "sha512-tsu8FiKJLk2PzhDl9fXbGUWTkkVXYhtTA+SmEFkKft+9BgwLxfCRpU96sWv7ICC8zixBNd3JURVoiR3dUXgP8A==", + "dev": true, + "license": "LGPL-3.0", + "peer": true, + "dependencies": { + "@ethereumjs/util": "^8.1.0", + "bn.js": "^5.2.1", + "ethereum-bloom-filters": "^1.0.6", + "ethereum-cryptography": "^2.1.2", + "ethjs-unit": "0.1.6", + "number-to-bn": "1.7.0", + "randombytes": "^2.1.0", + "utf8": "3.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/web3-utils/node_modules/@ethereumjs/rlp": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@ethereumjs/rlp/-/rlp-4.0.1.tgz", + "integrity": "sha512-tqsQiBQDQdmPWE1xkkBq4rlSW5QZpLOUJ5RJh2/9fug+q9tnUhuZoVLk7s0scUIKTOzEtR72DFBXI4WiZcMpvw==", + "dev": true, + "license": "MPL-2.0", + "peer": true, + "bin": { + "rlp": "bin/rlp" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/web3-utils/node_modules/@ethereumjs/util": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@ethereumjs/util/-/util-8.1.0.tgz", + "integrity": "sha512-zQ0IqbdX8FZ9aw11vP+dZkKDkS+kgIvQPHnSAXzP9pLu+Rfu3D3XEeLbicvoXJTYnhZiPmsZUxgdzXwNKxRPbA==", + "dev": true, + "license": "MPL-2.0", + "peer": true, + "dependencies": { + "@ethereumjs/rlp": "^4.0.1", + "ethereum-cryptography": "^2.0.0", + "micro-ftch": "^0.3.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/web3-utils/node_modules/@noble/curves": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz", + "integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@noble/hashes": "1.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/web3-utils/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/web3-utils/node_modules/ethereum-cryptography": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-2.2.1.tgz", + "integrity": "sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@noble/curves": "1.4.2", + "@noble/hashes": "1.4.0", + "@scure/bip32": "1.4.0", + "@scure/bip39": "1.3.0" + } + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/widest-line": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", + "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/wordwrapjs": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-4.0.1.tgz", + "integrity": "sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "reduce-flatten": "^2.0.0", + "typical": "^5.2.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/wordwrapjs/node_modules/typical": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", + "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/workerpool": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..b1a1749 --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "poc-openrouter", + "version": "1.0.0", + "private": true, + "scripts": { + "compile": "hardhat compile", + "deploy": "hardhat run scripts/deploy/deployOpenRouter.ts --network", + "deploy:v2": "hardhat run scripts/deploy/deployOpenRouterV2.ts --network", + "typechain": "hardhat typechain", + "slither": "bash scripts/docker-slither.sh" + }, + "devDependencies": { + "@arbitrum/sdk": "^4.0.5", + "@nomicfoundation/hardhat-foundry": "^1.1.2", + "@nomicfoundation/hardhat-toolbox": "^5.0.0", + "axios": "^1.16.0", + "dotenv": "^16.0.0", + "ethers": "^6.16.0", + "hardhat": "^2.22.7", + "ts-node": "^10.9.0", + "typescript": "^5.0.0" + }, + "dependencies": { + "@layerzerolabs/lz-v2-utilities": "^3.0.168" + } +} diff --git a/remappings.txt b/remappings.txt new file mode 100644 index 0000000..fe98d3e --- /dev/null +++ b/remappings.txt @@ -0,0 +1,2 @@ +solady/=lib/solady/ +forge-std/=lib/forge-std/src/ diff --git a/scripts/deploy/create3.ts b/scripts/deploy/create3.ts new file mode 100644 index 0000000..4009a7d --- /dev/null +++ b/scripts/deploy/create3.ts @@ -0,0 +1,39 @@ +import { Log, TransactionReceipt, keccak256, toUtf8Bytes } from 'ethers'; + +// CreateX factory — https://createx.rocks/ +export const CREATE_X_FACTORY = '0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed'; + +export const Create3ABI = [ + 'function computeCreate2Address(bytes32,bytes32,address) view returns (address)', + 'function deployCreate2(bytes32,bytes) payable returns (address)', + 'function computeCreate3Address(bytes32,address) view returns (address)', + 'function deployCreate3(bytes32,bytes) payable returns (address)', +]; + +const Create3ContractCreationEvent = 'ContractCreation(address)'; +const Create3ContractCreationEventTopicHash = keccak256( + toUtf8Bytes(Create3ContractCreationEvent), +); + +/** + * Reads the deployed contract address from a CreateX CREATE3 deployment receipt. + */ +export function decodeCreate3DeploymentFromTxReceipt(params: { + receipt: TransactionReceipt; +}): string | null { + const { receipt } = params; + const filteredLogs: Log[] = receipt.logs.filter((log: Log) => + log.topics.includes(Create3ContractCreationEventTopicHash), + ); + + if (filteredLogs.length === 0) { + return null; + } + + const eventData = filteredLogs[0].topics[1]; + if (!eventData) { + return null; + } + + return '0x' + eventData.slice(26); +} diff --git a/scripts/deploy/deployOpenRouter.ts b/scripts/deploy/deployOpenRouter.ts new file mode 100644 index 0000000..624ba30 --- /dev/null +++ b/scripts/deploy/deployOpenRouter.ts @@ -0,0 +1,85 @@ +/** + * Deployment script for OpenRouter via CreateX CREATE3. + * + * Usage: + * npx hardhat run scripts/deploy/deployOpenRouter.ts --network + * + * Required env vars: + * DEPLOYER_PRIVATE_KEY — deployer wallet private key + */ + +import hre from 'hardhat'; +import { ethers } from 'hardhat'; +import { keccak256, toUtf8Bytes } from 'ethers'; +import { + CREATE_X_FACTORY, + Create3ABI, + decodeCreate3DeploymentFromTxReceipt, +} from './create3'; + +async function main() { + const [deployer] = await ethers.getSigners(); + const networkName = hre.network.name; + const owner = deployer.address; + + console.log('Deployer: ', deployer.address); + console.log('Owner: ', owner); + console.log('Network: ', networkName); + console.log(''); + + const constructorArgs = { _owner: owner }; + console.log('constructorArgs', constructorArgs); + + const create3Factory = new ethers.Contract( + CREATE_X_FACTORY, + Create3ABI, + deployer, + ); + + const factory = await ethers.getContractFactory('OpenRouter'); + const deployTransaction = await factory.getDeployTransaction(owner); + + const saltText = 'OpenRouter' + 1; + const salt = keccak256(toUtf8Bytes(saltText)); + + const deployAddress = await create3Factory.deployCreate3.staticCall( + salt, + deployTransaction.data, + ); + console.log('Contract address will be:', deployAddress); + + console.log('Deploying OpenRouter via CREATE3...'); + const create3Deployment = await create3Factory.deployCreate3( + salt, + deployTransaction.data, + ); + console.log('CREATE3 deployment tx:', create3Deployment.hash); + + const receipt = await create3Deployment.wait(); + const routerAddress = decodeCreate3DeploymentFromTxReceipt({ receipt }); + if (!routerAddress) { + throw new Error( + 'OpenRouter address not found in CREATE3 deployment receipt', + ); + } + + console.log('OpenRouter deployed to:', routerAddress); + + console.log('\n=== Deployment Summary ==='); + console.log(`OpenRouter: ${routerAddress}`); + + const chainId = (await ethers.provider.getNetwork()).chainId; + if (chainId !== 31337n) { + await new Promise((resolve) => setTimeout(resolve, 5000)); + + await hre.run('verify:verify', { + address: routerAddress, + constructorArguments: [owner], + }); + } +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/docker-slither.sh b/scripts/docker-slither.sh new file mode 100644 index 0000000..b9e25d5 --- /dev/null +++ b/scripts/docker-slither.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# Run Slither inside trailofbits/eth-security-toolbox with Foundry compilation. +# Uses forge in the container instead of solc-select (avoids solc-select 403s on binary list fetch). +# Remappings are read from remappings.txt so npm does not need a multiline tr(1) in package.json. + +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" + +if [[ ! -f remappings.txt ]]; then + echo "docker-slither.sh: remappings.txt not found in ${ROOT}" >&2 + exit 1 +fi + +REMAPS="" +while IFS= read -r line || [[ -n "${line}" ]]; do + if [[ -n "${line}" ]]; then + if [[ -n "${REMAPS}" ]]; then + REMAPS+=" " + fi + REMAPS+="${line}" + fi +done < remappings.txt + +SLITHER_ARGS=("$@") +if [[ ${#SLITHER_ARGS[@]} -eq 0 ]]; then + SLITHER_ARGS=(.) +fi +if [[ ${#SLITHER_ARGS[@]} -eq 1 && "${SLITHER_ARGS[0]}" == *.sol ]]; then + sol_file="${SLITHER_ARGS[0]}" + base="$(basename "${sol_file}")" + # --include-paths takes a regex; escape dots so ".sol" is literal. + include_regex="${base//./\\.}" + SLITHER_ARGS=(. --include-paths "${include_regex}") +fi + +DOCKER_FLAGS=( + -t + --rm + -v "${ROOT}:/poc-openrouter" + -w /poc-openrouter + --platform linux/amd64 + --entrypoint slither +) + +# Do not mount ~/.foundry: host macOS forge/solc binaries break Linux exec (126 / Exec format error). + +exec docker run "${DOCKER_FLAGS[@]}" trailofbits/eth-security-toolbox "${SLITHER_ARGS[@]}" \ + --compile-force-framework forge \ + --solc-remaps "${REMAPS}" \ + --solc-args '--allow-paths /' diff --git a/scripts/e2e/approveViaModular.ts b/scripts/e2e/approveViaModular.ts new file mode 100644 index 0000000..891a7e2 --- /dev/null +++ b/scripts/e2e/approveViaModular.ts @@ -0,0 +1,145 @@ +/** + * Script — Call ERC-20 approve(spender, amount) through the router using + * `performActions(Action[])`. + * + * DISABLED by default: `OpenRouter` now sets max allowance inside + * `swap`, `bridge`, and `swapAndBridge`. Use those entrypoints instead of a + * standalone approval tx. Set `E2E_ENABLE_MODULAR_PRE_APPROVE=1` only if you + * need this legacy helper for manual modular debugging. + * + * Usage: + * TOKEN=0x... SPENDER=0x... AMOUNT=1000000 PRIVATE_KEY=0x... \ + * ts-node scripts/e2e/approveViaModular.ts + * + * Optional: + * CHAIN_ID=137 (default: 137, Polygon) + * AMOUNT=max (uses MaxUint256) + * + * actionInfo packing (from the contract): + * bits 0-7 : callType (0 = CALL) + * bits 8-15 : storeResult flag (0 = don't store) + * bits 16+ : target address (uint160) + */ +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { CHAIN_IDS, routerAddressForChain, RPC, TOKENS } from './config'; +import { encodeApprove } from './utils/erc20'; +import { ROUTER_ABI } from './utils/routerAbi'; +import { ZERO_BYTES32 } from './utils/contractTypes'; + +// ─── actionInfo helpers ─────────────────────────────────────────────────────── + +const CallType = { CALL: 0n, STATICCALL: 1n, CALL_WITH_NATIVE: 2n } as const; + +function packActionInfo( + target: string, + callType = CallType.CALL, + storeResult = false, +): bigint { + return (BigInt(target) << 16n) | (storeResult ? 0x100n : 0n) | callType; +} + +// ─── build + send ───────────────────────────────────────────────────────────── + +async function run(): Promise { + if (process.env.E2E_ENABLE_MODULAR_PRE_APPROVE !== '1') { + console.log( + 'approveViaModular is disabled (router pre-approves in swap/bridge/swapAndBridge).', + ); + console.log('Set E2E_ENABLE_MODULAR_PRE_APPROVE=1 to run this script.'); + return; + } + + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error('PRIVATE_KEY env var required'); + + const token = TOKENS.USDC_POLYGON_CIRCLE; + if (!token || !/^0x[a-fA-F0-9]{40}$/.test(token)) { + throw new Error( + 'TOKEN env var required (checksummed or lowercase ERC-20 address)', + ); + } + + const spender = '0x28b5a0e9c621a5badaa536219b3a228c8168cf5d'; // cctp tokenmessengerv2 + if (!spender || !/^0x[a-fA-F0-9]{40}$/.test(spender)) { + throw new Error( + 'SPENDER env var required (checksummed or lowercase address)', + ); + } + + const amount = ethers.MaxUint256; + + const chainId = CHAIN_IDS.POLYGON; + const rpcUrl: string = + process.env.RPC_URL ?? + (chainId === CHAIN_IDS.POLYGON + ? RPC.POLYGON + : chainId === CHAIN_IDS.ARBITRUM + ? RPC.ARBITRUM + : chainId === CHAIN_IDS.BASE + ? RPC.BASE + : chainId === CHAIN_IDS.ETHEREUM + ? RPC.ETHEREUM + : (() => { + throw new Error(`No default RPC for chain ${chainId}; set RPC_URL`); + })()); + + const routerAddress = routerAddressForChain(chainId); + + const provider = new ethers.JsonRpcProvider(rpcUrl); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + const approveCalldata = encodeApprove(spender, amount); + + // Single action: CALL token.approve(spender, amount) from the router. + const actions = [ + { + actionInfo: packActionInfo(token), + data: approveCalldata, + splices: [], + }, + ]; + + const calldata = routerIface.encodeFunctionData('performActions', [ + ZERO_BYTES32, + actions, + ]); + + console.log(`Signer: ${signerAddress}`); + console.log(`Chain: ${chainId}`); + console.log(`Router: ${routerAddress}`); + console.log(`Token: ${token}`); + console.log(`Spender: ${spender}`); + console.log( + `Amount: ${ + amount === ethers.MaxUint256 ? 'MaxUint256' : amount.toString() + }`, + ); + console.log('Sending performActions → token.approve...'); + + const tx = await signer.sendTransaction({ + to: routerAddress, + data: calldata, + }); + console.log(`Tx hash: ${tx.hash}`); + const receipt = await tx.wait(); + console.log( + `Status: ${receipt?.status === 1 ? 'success' : 'reverted'} (block ${ + receipt?.blockNumber + })`, + ); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); + +async function main(): Promise { + await run(); +} diff --git a/scripts/e2e/arbitrum/performExecution.postFee.ts b/scripts/e2e/arbitrum/performExecution.postFee.ts new file mode 100644 index 0000000..3e97d1e --- /dev/null +++ b/scripts/e2e/arbitrum/performExecution.postFee.ts @@ -0,0 +1,153 @@ +/** + * Route: Ethereum AAVE → ETH (OpenOcean) → Arbitrum ETH (inbox depositEth) + * Function: swapAndBridge + * Fee: postFee — FEE_BPS of estimatedOut ETH deducted after swap + * + * BRIDGE_VALUE_FLAG: router forwards swap output as msg.value to inbox.depositEth(). + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/arbitrum/performExecution.postFee.ts + */ +import axios from 'axios'; +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + ARBITRUM_INBOX, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, + NATIVE_TOKEN_ADDRESS, +} from '../config'; +import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; +import { getWalletErc20Balance } from '../utils/erc20'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { + BRIDGE_VALUE_FLAG, + POST_FEE_FLAG, + ZERO_ADDRESS, + ZERO_BYTES32, + swapAndBridgeArgs, +} from '../utils/contractTypes'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterErc20Balance, ensureRouterNativeBalance } from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; + +const FLAGS = POST_FEE_FLAG | BRIDGE_VALUE_FLAG; +const ROUTER_ETH = routerAddressForChain(CHAIN_IDS.ETHEREUM); + +interface OoQuoteResponse { + data: { to: string; data: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.AAVE_ETH, + outTokenAddress: NATIVE_TOKEN_ADDRESS, + amount: ethers.formatUnits(inputAmount, 18), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_ETH, + account: ROUTER_ETH, + gasPrice: '20', + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.ETHEREUM}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + }; +} + +function buildDepositEthCalldata(): string { + return new ethers.Interface([ + 'function depositEth() external payable returns (uint256)', + ]).encodeFunctionData('depositEth', []); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error('PRIVATE_KEY env var required'); + + const provider = new ethers.JsonRpcProvider(RPC.ETHEREUM); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance, decimals } = await getWalletErc20Balance(TOKENS.AAVE_ETH, signerAddress, provider); + if (walletBalance === 0n) throw new Error(`Signer ${signerAddress} has zero AAVE on Ethereum`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error('Balance too small'); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_ETH}`); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, decimals)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log('Fetching OpenOcean quote (AAVE → ETH)...'); + const { ooRouter, swapData, estimatedOut, minAmountOut } = await fetchOpenOceanQuote(inputAmount); + const feeAmount = bpsOf(estimatedOut, FEE_BPS); + console.log(` OO router: ${ooRouter}`); + console.log(` Est. ETH: ${ethers.formatEther(estimatedOut)}`); + console.log(` Post-fee: ${ethers.formatEther(feeAmount)} ETH (${FEE_BPS} bps)`); + console.log(` Min ETH: ${ethers.formatEther(minAmountOut)}`); + + await ensureRouterErc20Balance(signer, TOKENS.AAVE_ETH, ROUTER_ETH); + await ensureRouterNativeBalance(signer, ROUTER_ETH); + + const swapApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_ETH, + TOKENS.AAVE_ETH, + ooRouter, + inputAmount, + ); + + const callData = routerIface.encodeFunctionData( + 'swapAndBridge', + swapAndBridgeArgs( + ZERO_BYTES32, + FLAGS, + { user: signerAddress, inputToken: TOKENS.AAVE_ETH, inputAmount }, + { receiver: signerAddress, amount: feeAmount }, + { + target: ooRouter, + approvalSpender: swapApprovalSpender, + outputToken: NATIVE_TOKEN_ADDRESS, + value: 0n, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + swapData, + { target: ARBITRUM_INBOX, approvalSpender: ZERO_ADDRESS, value: 0n }, + buildDepositEthCalldata(), + ), + ); + + await ensureAllowanceForAllowanceHolder(signer, TOKENS.AAVE_ETH, inputAmount); + const receipt = await execViaAH(signer, ROUTER_ETH, TOKENS.AAVE_ETH, inputAmount, ROUTER_ETH, callData, 0n); + + logTxnSummary('Ethereum AAVE → Arbitrum ETH (depositEth) — swapAndBridge postFee', CHAIN_IDS.ETHEREUM, receipt); + + console.log('\nETH arrives on Arbitrum once the retryable ticket is processed.'); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/arbitrum/performExecution.preFee.ts b/scripts/e2e/arbitrum/performExecution.preFee.ts new file mode 100644 index 0000000..bbcd01a --- /dev/null +++ b/scripts/e2e/arbitrum/performExecution.preFee.ts @@ -0,0 +1,118 @@ +/** + * Route: Ethereum ETH → Arbitrum ETH (Arbitrum inbox depositEth, no swap) + * Function: bridge + * Fee: preFee — FEE_BPS of inputAmount ETH deducted before bridge + * + * Input is native ETH — call router.bridge directly (msg.value = inputAmount). + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/arbitrum/performExecution.preFee.ts + */ +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + ARBITRUM_INBOX, + FEE_BPS, + bpsOf, + RPC, + NATIVE_TOKEN_ADDRESS, +} from '../config'; +import { execDirect } from '../utils/allowanceHolder'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { ZERO_ADDRESS, ZERO_BYTES32, bridgeArgs, type BridgeData, type FeeData, type InputData } from '../utils/contractTypes'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterNativeBalance } from '../utils/reproducibility'; + +const ROUTER_ETH = routerAddressForChain(CHAIN_IDS.ETHEREUM); + +const GAS_RESERVE = ethers.parseEther('0.005'); + +function buildDepositEthCalldata(): string { + return new ethers.Interface([ + 'function depositEth() external payable returns (uint256)', + ]).encodeFunctionData('depositEth', []); +} + +async function estimateArbitrumBridgeFee(provider: ethers.Provider): Promise { + try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { ParentToChildMessageGasEstimator } = require('@arbitrum/sdk'); + const estimator = new ParentToChildMessageGasEstimator(provider); + const l2GasPrice = (await new ethers.JsonRpcProvider(RPC.ARBITRUM).getFeeData()).gasPrice ?? 0n; + const submissionFee = await estimator.estimateSubmissionFee(provider, 0n, 0n); + const executionCost = 250000n * (l2GasPrice + (l2GasPrice * 20n) / 100n); + const totalFee = BigInt(submissionFee.toString()) + executionCost; + console.log(` Estimated Arbitrum bridge fee: ${ethers.formatEther(totalFee)} ETH`); + return totalFee; + } catch (err) { + const fallback = ethers.parseEther('0.001'); + console.warn( + ` Arb fee estimation failed (${(err as Error).message}), using fallback: ${ethers.formatEther(fallback)} ETH`, + ); + return fallback; + } +} + +async function main(): Promise { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) { + throw new Error('PRIVATE_KEY env var required'); + } + + const provider = new ethers.JsonRpcProvider(RPC.ETHEREUM); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const rawBalance = await provider.getBalance(signerAddress); + const inputAmount = rawBalance - GAS_RESERVE - 20n; + if (inputAmount <= 0n) { + throw new Error(`Signer ${signerAddress} has insufficient ETH on Ethereum (balance: ${ethers.formatEther(rawBalance)})`); + } + + const feeAmount = bpsOf(inputAmount, FEE_BPS); + const bridgeValue = inputAmount - feeAmount; + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_ETH}`); + console.log(`ETH balance: ${ethers.formatEther(rawBalance)}`); + console.log(`Input amount: ${ethers.formatEther(inputAmount)} (balance minus gas reserve)`); + console.log(`Pre-bridge fee: ${ethers.formatEther(feeAmount)} ETH (${FEE_BPS} bps)`); + console.log(`Bridge value: ${ethers.formatEther(bridgeValue)} ETH`); + + const arbFee = await estimateArbitrumBridgeFee(provider); + if (bridgeValue < arbFee) { + console.warn( + ` Warning: bridgeValue (${ethers.formatEther(bridgeValue)}) may be below Arbitrum bridge cost (${ethers.formatEther(arbFee)})`, + ); + } + + await ensureRouterNativeBalance(signer, ROUTER_ETH); + + const input: InputData = { user: signerAddress, inputToken: NATIVE_TOKEN_ADDRESS, inputAmount }; + const fee: FeeData = { receiver: signerAddress, amount: feeAmount }; + const bridgeData: BridgeData = { target: ARBITRUM_INBOX, approvalSpender: ZERO_ADDRESS, value: bridgeValue }; + + const routerIface = new ethers.Interface(ROUTER_ABI); + const callData = routerIface.encodeFunctionData( + 'bridge', + bridgeArgs(ZERO_BYTES32, input, fee, bridgeData, buildDepositEthCalldata()), + ); + + console.log('Sending direct router tx → router.bridge...'); + const receipt = await execDirect(signer, ROUTER_ETH, callData, inputAmount); + + logTxnSummary('Ethereum ETH → Arbitrum ETH (depositEth direct) — bridge preFee', CHAIN_IDS.ETHEREUM, receipt); + + console.log('\nETH arrives on Arbitrum once the retryable ticket is processed.'); + + void arbFee; +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/arbitrum/performModularExecution.postFee.ts b/scripts/e2e/arbitrum/performModularExecution.postFee.ts new file mode 100644 index 0000000..069e29d --- /dev/null +++ b/scripts/e2e/arbitrum/performModularExecution.postFee.ts @@ -0,0 +1,165 @@ +/** + * Route: Ethereum AAVE → ETH (OpenOcean) → Arbitrum ETH (inbox depositEth) + * Function: performActions (modular) + * Fee: postFee — FEE_BPS of estimatedOut ETH sent to signer after swap + * + * Modular action sequence: + * [0] AH.transferFrom(AAVE, signer, router, inputAmount) + * [1] AAVE.approve(ooRouter, inputAmount) + * [2] call(ooRouter, swapData) — AAVE → ETH lands in router + * [3] nativeCall(signer, '0x', feeAmount) — ETH fee to signer + * [4] nativeCall(inbox, depositEth(), bridgeValue) + * + * Input is AAVE (ERC-20) so AllowanceHolder.exec is required. + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/arbitrum/performActions.postFee.ts + */ +import axios from 'axios'; +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + ARBITRUM_INBOX, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, + ALLOWANCE_HOLDER, + NATIVE_TOKEN_ADDRESS, +} from '../config'; +import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; +import { getWalletErc20Balance } from '../utils/erc20'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { ModularActionsBuilder } from '../utils/modularActionsBuilder/index'; +import { ZERO_BYTES32 } from '../utils/contractTypes'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterErc20Balance, ensureRouterNativeBalance } from '../utils/reproducibility'; +import { modularApproveIfNeeded } from '../utils/routerAllowance'; + +const ROUTER_ETH = routerAddressForChain(CHAIN_IDS.ETHEREUM); + +interface OoQuoteResponse { + data: { to: string; data: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.AAVE_ETH, + outTokenAddress: NATIVE_TOKEN_ADDRESS, + amount: ethers.formatUnits(inputAmount, 18), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_ETH, + account: ROUTER_ETH, + gasPrice: '20', + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.ETHEREUM}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + }; +} + +function buildDepositEthCalldata(): string { + return new ethers.Interface([ + 'function depositEth() external payable returns (uint256)', + ]).encodeFunctionData('depositEth', []); +} + +async function estimateArbitrumBridgeFee(provider: ethers.Provider): Promise { + try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { ParentToChildMessageGasEstimator } = require('@arbitrum/sdk'); + const estimator = new ParentToChildMessageGasEstimator(provider); + const l2GasPrice = (await new ethers.JsonRpcProvider(RPC.ARBITRUM).getFeeData()).gasPrice ?? 0n; + const submissionFee = await estimator.estimateSubmissionFee(provider, 0n, 0n); + const executionCost = 250000n * (l2GasPrice + (l2GasPrice * 20n) / 100n); + const totalFee = BigInt(submissionFee.toString()) + executionCost; + console.log(` Estimated Arbitrum bridge fee: ${ethers.formatEther(totalFee)} ETH`); + return totalFee; + } catch (err) { + const fallback = ethers.parseEther('0.001'); + console.warn(` Arb fee estimation failed (${(err as Error).message}), using fallback: ${ethers.formatEther(fallback)} ETH`); + return fallback; + } +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error('PRIVATE_KEY env var required'); + + const provider = new ethers.JsonRpcProvider(RPC.ETHEREUM); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance, decimals } = await getWalletErc20Balance(TOKENS.AAVE_ETH, signerAddress, provider); + if (walletBalance === 0n) throw new Error(`Signer ${signerAddress} has zero AAVE on Ethereum`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error('Balance too small'); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_ETH}`); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, decimals)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log('Fetching OpenOcean quote (AAVE → ETH)...'); + const { ooRouter, swapData, estimatedOut, minAmountOut } = await fetchOpenOceanQuote(inputAmount); + const feeAmount = bpsOf(estimatedOut, FEE_BPS); + console.log(` OO router: ${ooRouter}`); + console.log(` Est. ETH: ${ethers.formatEther(estimatedOut)}`); + console.log(` Post-fee: ${ethers.formatEther(feeAmount)} ETH (${FEE_BPS} bps)`); + console.log(` Min ETH: ${ethers.formatEther(minAmountOut)}`); + + const arbFee = await estimateArbitrumBridgeFee(provider); + // bridgeValue uses minAmountOut-based floor so the nativeCall carries at least the bridge cost + const bridgeValue = minAmountOut > feeAmount ? minAmountOut - feeAmount : 0n; + console.log(` Bridge value: ${ethers.formatEther(bridgeValue)} ETH (floor for nativeCall)`); + + await ensureRouterErc20Balance(signer, TOKENS.AAVE_ETH, ROUTER_ETH); + await ensureRouterNativeBalance(signer, ROUTER_ETH); + + const ahIface = new ethers.Interface([ + 'function transferFrom(address token, address owner, address recipient, uint256 amount)', + ]); + const exec = new ModularActionsBuilder(); + exec.call(ALLOWANCE_HOLDER, ahIface.encodeFunctionData('transferFrom', [TOKENS.AAVE_ETH, signerAddress, ROUTER_ETH, inputAmount])); + await modularApproveIfNeeded(exec, provider, ROUTER_ETH, TOKENS.AAVE_ETH, ooRouter, inputAmount, inputAmount); + exec.call(ooRouter, swapData); + exec.nativeCall(signerAddress, '0x', feeAmount); + exec.nativeCall(ARBITRUM_INBOX, buildDepositEthCalldata(), bridgeValue); + + const callData = routerIface.encodeFunctionData('performActions', [ZERO_BYTES32, exec.toActions()]); + + await ensureAllowanceForAllowanceHolder(signer, TOKENS.AAVE_ETH, inputAmount); + const receipt = await execViaAH(signer, ROUTER_ETH, TOKENS.AAVE_ETH, inputAmount, ROUTER_ETH, callData, 0n); + + logTxnSummary( + 'Ethereum AAVE → Arbitrum ETH (depositEth) — performActions postFee', + CHAIN_IDS.ETHEREUM, + receipt, + ); + + console.log('\nETH arrives on Arbitrum once the retryable ticket is processed.'); + + // Suppress unused-variable warning for arbFee (kept for informational logging above) + void arbFee; +} + +main().catch((err) => { console.error(err); process.exit(1); }); diff --git a/scripts/e2e/arbitrum/performModularExecution.preFee.ts b/scripts/e2e/arbitrum/performModularExecution.preFee.ts new file mode 100644 index 0000000..ab9c1d9 --- /dev/null +++ b/scripts/e2e/arbitrum/performModularExecution.preFee.ts @@ -0,0 +1,118 @@ +/** + * Route: Ethereum ETH → Arbitrum ETH (Arbitrum inbox depositEth, no swap) + * Function: performActions (modular) + * Fee: preFee — FEE_BPS of inputAmount ETH deducted before bridge + * + * Modular action sequence: + * [0] nativeCall(signer, '0x', feeAmount) — preFee ETH to signer + * [1] nativeCall(inbox, depositEth(), bridgeValue) — bridge remaining ETH + * + * Input is native ETH so we call execDirect (no AllowanceHolder needed — + * performActions has no _pullFromUser; ETH arrives via msg.value). + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/arbitrum/performActions.preFee.ts + */ +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + ARBITRUM_INBOX, + FEE_BPS, + bpsOf, + RPC, +} from '../config'; +import { execDirect } from '../utils/allowanceHolder'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { ModularActionsBuilder } from '../utils/modularActionsBuilder/index'; +import { ZERO_BYTES32 } from '../utils/contractTypes'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterNativeBalance } from '../utils/reproducibility'; + +const ROUTER_ETH = routerAddressForChain(CHAIN_IDS.ETHEREUM); + +/** Gas reserve kept in the signer's wallet to cover the transaction itself. */ +const GAS_RESERVE = ethers.parseEther('0.005'); + +function buildDepositEthCalldata(): string { + return new ethers.Interface([ + 'function depositEth() external payable returns (uint256)', + ]).encodeFunctionData('depositEth', []); +} + +async function estimateArbitrumBridgeFee(provider: ethers.Provider): Promise { + try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { ParentToChildMessageGasEstimator } = require('@arbitrum/sdk'); + const estimator = new ParentToChildMessageGasEstimator(provider); + const l2GasPrice = (await new ethers.JsonRpcProvider(RPC.ARBITRUM).getFeeData()).gasPrice ?? 0n; + const submissionFee = await estimator.estimateSubmissionFee(provider, 0n, 0n); + const executionCost = 250000n * (l2GasPrice + (l2GasPrice * 20n) / 100n); + const totalFee = BigInt(submissionFee.toString()) + executionCost; + console.log(` Estimated Arbitrum bridge fee: ${ethers.formatEther(totalFee)} ETH`); + return totalFee; + } catch (err) { + const fallback = ethers.parseEther('0.001'); + console.warn(` Arb fee estimation failed (${(err as Error).message}), using fallback: ${ethers.formatEther(fallback)} ETH`); + return fallback; + } +} + +async function main(): Promise { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) { + throw new Error('PRIVATE_KEY env var required'); + } + + const provider = new ethers.JsonRpcProvider(RPC.ETHEREUM); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const rawBalance = await provider.getBalance(signerAddress); + const inputAmount = rawBalance - GAS_RESERVE - 20n; + if (inputAmount <= 0n) { + throw new Error(`Signer ${signerAddress} has insufficient ETH on Ethereum (balance: ${ethers.formatEther(rawBalance)})`); + } + + const feeAmount = bpsOf(inputAmount, FEE_BPS); + const bridgeValue = inputAmount - feeAmount; + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_ETH}`); + console.log(`ETH balance: ${ethers.formatEther(rawBalance)}`); + console.log(`Input amount: ${ethers.formatEther(inputAmount)} (balance minus gas reserve)`); + console.log(`Pre-bridge fee: ${ethers.formatEther(feeAmount)} ETH (${FEE_BPS} bps)`); + console.log(`Bridge value: ${ethers.formatEther(bridgeValue)} ETH`); + + const arbFee = await estimateArbitrumBridgeFee(provider); + if (bridgeValue < arbFee) { + console.warn(` Warning: bridgeValue (${ethers.formatEther(bridgeValue)}) may be below Arbitrum bridge cost (${ethers.formatEther(arbFee)})`); + } + + await ensureRouterNativeBalance(signer, ROUTER_ETH); + + const exec = new ModularActionsBuilder(); + exec.nativeCall(signerAddress, '0x', feeAmount); + exec.nativeCall(ARBITRUM_INBOX, buildDepositEthCalldata(), bridgeValue); + + const routerIface = new ethers.Interface(ROUTER_ABI); + const callData = routerIface.encodeFunctionData('performActions', [ZERO_BYTES32, exec.toActions()]); + + // Native ETH input — send directly to the router; no AllowanceHolder needed. + console.log('Sending direct router tx → router.performActions...'); + const receipt = await execDirect(signer, ROUTER_ETH, callData, inputAmount); + + logTxnSummary('Ethereum ETH → Arbitrum ETH (depositEth direct) — performActions preFee', CHAIN_IDS.ETHEREUM, receipt); + + console.log('\nETH arrives on Arbitrum once the retryable ticket is processed.'); + + void arbFee; +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/cctp/bridge.preFee.ts b/scripts/e2e/cctp/bridge.preFee.ts new file mode 100644 index 0000000..bbac9a1 --- /dev/null +++ b/scripts/e2e/cctp/bridge.preFee.ts @@ -0,0 +1,126 @@ +/** + * Route: Polygon USDC → Base USDC (CCTP v2, no swap) + * Function: bridge (simple bridge entrypoint) + * Fee: preFee — FEE_BPS of inputAmount USDC deducted before bridge + * + * Bridge amount is pre-encoded in depositForBurn calldata (no splice needed). + * Uses router.bridge() rather than performExecution / performActions. + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/cctp/bridge.preFee.ts + */ +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + CCTP_CONFIG, + FEE_BPS, + bpsOf, + RPC, +} from '../config'; +import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; +import { getWalletErc20Balance } from '../utils/erc20'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { ZERO_BYTES32, type BridgeData, type FeeData, type InputData } from '../utils/contractTypes'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterErc20Balance } from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; + +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); + +function buildDepositForBurnCalldata( + recipientAddress: string, + burnToken: string, + destinationCctpDomain: number, + amount: bigint, + fastPath: boolean = true, +): string { + const iface = new ethers.Interface([ + 'function depositForBurn(uint256 amount, uint32 destinationDomain, bytes32 mintRecipient, address burnToken, bytes32 destinationCaller, uint256 maxFee, uint32 minFinalityThreshold) external', + ]); + const mintRecipient = ethers.zeroPadValue(recipientAddress, 32); + const maxFee = fastPath ? 1_000_000n : 0n; + const minFinalityThreshold = fastPath ? 1000 : 2000; + return iface.encodeFunctionData('depositForBurn', [ + amount, + destinationCctpDomain, + mintRecipient, + burnToken, + ethers.ZeroHash, + maxFee, + minFinalityThreshold, + ]); +} + +async function main(): Promise { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) { + throw new Error('PRIVATE_KEY env var required'); + } + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const inputToken = TOKENS.USDC_POLYGON_CIRCLE; + const { balance: walletBalance } = await getWalletErc20Balance(inputToken, signerAddress, provider); + if (walletBalance === 0n) { + throw new Error(`Signer ${signerAddress} has zero Circle native USDC on Polygon. Fund ${inputToken} on Polygon PoS.`); + } + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error('Balance too small'); + + const feeAmount = bpsOf(inputAmount, FEE_BPS); + const bridgeAmount = inputAmount - feeAmount; + + const polyCctp = CCTP_CONFIG[CHAIN_IDS.POLYGON]; + const baseCctp = CCTP_CONFIG[CHAIN_IDS.BASE]; + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`Input USDC: ${ethers.formatUnits(inputAmount, 6)}`); + console.log(`Pre-bridge fee: ${ethers.formatUnits(feeAmount, 6)} (${FEE_BPS} bps)`); + console.log(`Net to bridge: ${ethers.formatUnits(bridgeAmount, 6)}`); + + const depositData = buildDepositForBurnCalldata( + signerAddress, + polyCctp.usdcAddress, + baseCctp.cctpDomain, + bridgeAmount, + ); + + const bridgeApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + inputToken, + polyCctp.tokenMessenger, + bridgeAmount, + ); + + const input: InputData = { user: signerAddress, inputToken, inputAmount }; + const fee: FeeData = { receiver: signerAddress, amount: feeAmount }; + const bridgeData: BridgeData = { target: polyCctp.tokenMessenger, approvalSpender: bridgeApprovalSpender, value: 0n }; + + const routerIface = new ethers.Interface(ROUTER_ABI); + const execCalldata = routerIface.encodeFunctionData('bridge', [ZERO_BYTES32, input, fee, bridgeData, depositData]); + + await ensureRouterErc20Balance(signer, inputToken, ROUTER_POLYGON); + await ensureAllowanceForAllowanceHolder(signer, inputToken, inputAmount); + + console.log('Sending AllowanceHolder.exec → router.bridge...'); + const receipt = await execViaAH(signer, ROUTER_POLYGON, inputToken, inputAmount, ROUTER_POLYGON, execCalldata); + + logTxnSummary('Polygon USDC → Base USDC — CCTP — bridge preFee', CHAIN_IDS.POLYGON, receipt); + + console.log(`\nUSDC mints on Base at ${signerAddress} once CCTP attestation completes.`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/cctp/performExecution.postFee.ts b/scripts/e2e/cctp/performExecution.postFee.ts new file mode 100644 index 0000000..36d0a50 --- /dev/null +++ b/scripts/e2e/cctp/performExecution.postFee.ts @@ -0,0 +1,173 @@ +/** + * Route: Polygon AAVE → USDC (OpenOcean) → Base USDC (CCTP depositForBurn) + * Function: swapAndBridge + * Fee: postFee — FEE_BPS of estimatedOut USDC deducted after swap + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/cctp/performExecution.postFee.ts + */ +import axios from 'axios'; +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + CCTP_CONFIG, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, +} from '../config'; +import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; +import { getWalletErc20Balance } from '../utils/erc20'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { + POST_FEE_FLAG, + ZERO_BYTES32, + bridgeAmountPositionFlag, + swapAndBridgeArgs, +} from '../utils/contractTypes'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterErc20Balance } from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; + +const FLAGS = POST_FEE_FLAG | bridgeAmountPositionFlag(4); +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); + +interface OoQuoteResponse { + data: { to: string; data: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.AAVE_POLYGON, + outTokenAddress: TOKENS.USDC_POLYGON_CIRCLE, + amount: ethers.formatUnits(inputAmount, 18), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_POLYGON, + account: ROUTER_POLYGON, + gasPrice: '1', + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.POLYGON}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + }; +} + +function buildDepositForBurnCalldata( + recipientAddress: string, + burnToken: string, + destinationCctpDomain: number, +): string { + const iface = new ethers.Interface([ + 'function depositForBurn(uint256 amount, uint32 destinationDomain, bytes32 mintRecipient, address burnToken, bytes32 destinationCaller, uint256 maxFee, uint32 minFinalityThreshold) external', + ]); + return iface.encodeFunctionData('depositForBurn', [ + 0n, + destinationCctpDomain, + ethers.zeroPadValue(recipientAddress, 32), + burnToken, + ethers.ZeroHash, + 1_000_000n, + 1000, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error('PRIVATE_KEY env var required'); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance(TOKENS.AAVE_POLYGON, signerAddress, provider); + if (walletBalance === 0n) throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error('Balance too small'); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + const polyCctp = CCTP_CONFIG[CHAIN_IDS.POLYGON]; + const baseCctp = CCTP_CONFIG[CHAIN_IDS.BASE]; + + console.log('Fetching OpenOcean quote (AAVE → USDC)...'); + const { ooRouter, swapData, estimatedOut, minAmountOut } = await fetchOpenOceanQuote(inputAmount); + const feeAmount = bpsOf(estimatedOut, FEE_BPS); + console.log(` OO router: ${ooRouter}`); + console.log(` Est. USDC: ${ethers.formatUnits(estimatedOut, 6)}`); + console.log(` Post-fee: ${ethers.formatUnits(feeAmount, 6)} USDC (${FEE_BPS} bps)`); + console.log(` Min USDC: ${ethers.formatUnits(minAmountOut, 6)}`); + + const depositForBurnData = buildDepositForBurnCalldata(signerAddress, polyCctp.usdcAddress, baseCctp.cctpDomain); + + await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); + await ensureRouterErc20Balance(signer, TOKENS.USDC_POLYGON_CIRCLE, ROUTER_POLYGON); + + const swapApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + ooRouter, + inputAmount, + ); + const bridgeApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + TOKENS.USDC_POLYGON_CIRCLE, + polyCctp.tokenMessenger, + estimatedOut - feeAmount, + ); + + const callData = routerIface.encodeFunctionData( + 'swapAndBridge', + swapAndBridgeArgs( + ZERO_BYTES32, + FLAGS, + { user: signerAddress, inputToken: TOKENS.AAVE_POLYGON, inputAmount }, + { receiver: signerAddress, amount: feeAmount }, + { + target: ooRouter, + approvalSpender: swapApprovalSpender, + outputToken: TOKENS.USDC_POLYGON_CIRCLE, + value: 0n, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + swapData, + { target: polyCctp.tokenMessenger, approvalSpender: bridgeApprovalSpender, value: 0n }, + depositForBurnData, + ), + ); + + await ensureAllowanceForAllowanceHolder(signer, TOKENS.AAVE_POLYGON, inputAmount); + const receipt = await execViaAH(signer, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, inputAmount, ROUTER_POLYGON, callData); + + logTxnSummary('Polygon AAVE → Base USDC (CCTP) — swapAndBridge postFee', CHAIN_IDS.POLYGON, receipt); + + console.log(`\nUSDC mints on Base at ${signerAddress} once CCTP attestation completes.`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/cctp/performExecution.preFee.ts b/scripts/e2e/cctp/performExecution.preFee.ts new file mode 100644 index 0000000..cc53631 --- /dev/null +++ b/scripts/e2e/cctp/performExecution.preFee.ts @@ -0,0 +1,122 @@ +/** + * Route: Polygon USDC → Base USDC (CCTP depositForBurn, no swap) + * Function: bridge + * Fee: preFee — FEE_BPS of inputAmount USDC deducted before bridge + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/cctp/performExecution.preFee.ts + */ +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + CCTP_CONFIG, + FEE_BPS, + bpsOf, + RPC, +} from '../config'; +import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; +import { getWalletErc20Balance } from '../utils/erc20'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { + ZERO_BYTES32, + bridgeArgs, + type BridgeData, + type FeeData, + type InputData, +} from '../utils/contractTypes'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterErc20Balance } from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; + +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); + +function buildDepositForBurnCalldata( + recipientAddress: string, + burnToken: string, + destinationCctpDomain: number, + amount: bigint, +): string { + const iface = new ethers.Interface([ + 'function depositForBurn(uint256 amount, uint32 destinationDomain, bytes32 mintRecipient, address burnToken, bytes32 destinationCaller, uint256 maxFee, uint32 minFinalityThreshold) external', + ]); + return iface.encodeFunctionData('depositForBurn', [ + amount, + destinationCctpDomain, + ethers.zeroPadValue(recipientAddress, 32), + burnToken, + ethers.ZeroHash, + 1_000_000n, + 1000, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error('PRIVATE_KEY env var required'); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance(TOKENS.USDC_POLYGON_CIRCLE, signerAddress, provider); + if (walletBalance === 0n) throw new Error(`Signer ${signerAddress} has zero Circle native USDC on Polygon`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error('Balance too small'); + + const feeAmount = bpsOf(inputAmount, FEE_BPS); + const bridgeAmount = inputAmount - feeAmount; + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`USDC balance: ${ethers.formatUnits(walletBalance, 6)}`); + console.log(`Pre-fee: ${ethers.formatUnits(feeAmount, 6)} USDC (${FEE_BPS} bps)`); + console.log(`Net to bridge: ${ethers.formatUnits(bridgeAmount, 6)}`); + + const polyCctp = CCTP_CONFIG[CHAIN_IDS.POLYGON]; + const baseCctp = CCTP_CONFIG[CHAIN_IDS.BASE]; + const depositForBurnData = buildDepositForBurnCalldata( + signerAddress, + polyCctp.usdcAddress, + baseCctp.cctpDomain, + bridgeAmount, + ); + + const input: InputData = { user: signerAddress, inputToken: TOKENS.USDC_POLYGON_CIRCLE, inputAmount }; + const fee: FeeData = { receiver: signerAddress, amount: feeAmount }; + const bridgeApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + TOKENS.USDC_POLYGON_CIRCLE, + polyCctp.tokenMessenger, + bridgeAmount, + ); + + const bridgeData: BridgeData = { + target: polyCctp.tokenMessenger, + approvalSpender: bridgeApprovalSpender, + value: 0n, + }; + + await ensureRouterErc20Balance(signer, TOKENS.USDC_POLYGON_CIRCLE, ROUTER_POLYGON); + + const routerIface = new ethers.Interface(ROUTER_ABI); + const callData = routerIface.encodeFunctionData('bridge', bridgeArgs(ZERO_BYTES32, input, fee, bridgeData, depositForBurnData)); + + await ensureAllowanceForAllowanceHolder(signer, TOKENS.USDC_POLYGON_CIRCLE, inputAmount); + const receipt = await execViaAH(signer, ROUTER_POLYGON, TOKENS.USDC_POLYGON_CIRCLE, inputAmount, ROUTER_POLYGON, callData); + + logTxnSummary('Polygon USDC → Base USDC (CCTP) — bridge preFee', CHAIN_IDS.POLYGON, receipt); + + console.log(`\nUSDC mints on Base at ${signerAddress} once CCTP attestation completes.`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/cctp/performModularExecution.postFee.ts b/scripts/e2e/cctp/performModularExecution.postFee.ts new file mode 100644 index 0000000..c040a37 --- /dev/null +++ b/scripts/e2e/cctp/performModularExecution.postFee.ts @@ -0,0 +1,165 @@ +/** + * Route: Polygon AAVE → USDC (OpenOcean) → Base USDC (CCTP depositForBurn) + * Function: performActions (modular) + * Fee: postFee — FEE_BPS of estimatedOut USDC transferred to signer after swap + * + * Modular action sequence: + * [0] AH.transferFrom(AAVE, signer, router, inputAmount) + * [1] AAVE.approve(ooRouter, inputAmount) + * [2] ooRouter swap calldata — AAVE → USDC lands in router + * [3] USDC.transfer(signer, feeAmount) — post-swap fee + * [4] USDC.approve(tokenMessenger, MaxUint256) + * [5] STATICCALL USDC.balanceOf(router) + * [6] tokenMessenger.depositForBurn(...) — spliceArg(0) patches amount from [5] + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/cctp/performActions.postFee.ts + */ +import axios from 'axios'; +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + CCTP_CONFIG, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, + ALLOWANCE_HOLDER, +} from '../config'; +import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; +import { encodeTransfer, encodeBalanceOf, getWalletErc20Balance } from '../utils/erc20'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { ModularActionsBuilder } from '../utils/modularActionsBuilder/index'; +import { ZERO_BYTES32 } from '../utils/contractTypes'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterErc20Balance } from '../utils/reproducibility'; +import { modularApproveIfNeeded } from '../utils/routerAllowance'; + +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); + +interface OoQuoteResponse { + data: { to: string; data: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.AAVE_POLYGON, + outTokenAddress: TOKENS.USDC_POLYGON_CIRCLE, + amount: ethers.formatUnits(inputAmount, 18), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_POLYGON, + account: ROUTER_POLYGON, + gasPrice: '1', + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.POLYGON}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + }; +} + +function buildDepositForBurnCalldata( + recipientAddress: string, + burnToken: string, + destinationCctpDomain: number, +): string { + const iface = new ethers.Interface([ + 'function depositForBurn(uint256 amount, uint32 destinationDomain, bytes32 mintRecipient, address burnToken, bytes32 destinationCaller, uint256 maxFee, uint32 minFinalityThreshold) external', + ]); + return iface.encodeFunctionData('depositForBurn', [ + 0n, + destinationCctpDomain, + ethers.zeroPadValue(recipientAddress, 32), + burnToken, + ethers.ZeroHash, + 1_000_000n, + 1000, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error('PRIVATE_KEY env var required'); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance(TOKENS.AAVE_POLYGON, signerAddress, provider); + if (walletBalance === 0n) throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error('Balance too small'); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + const polyCctp = CCTP_CONFIG[CHAIN_IDS.POLYGON]; + const baseCctp = CCTP_CONFIG[CHAIN_IDS.BASE]; + + console.log('Fetching OpenOcean quote (AAVE → USDC)...'); + const { ooRouter, swapData, estimatedOut, minAmountOut } = await fetchOpenOceanQuote(inputAmount); + const feeAmount = bpsOf(estimatedOut, FEE_BPS); + console.log(` OO router: ${ooRouter}`); + console.log(` Est. USDC: ${ethers.formatUnits(estimatedOut, 6)}`); + console.log(` Post-fee: ${ethers.formatUnits(feeAmount, 6)} USDC (${FEE_BPS} bps)`); + console.log(` Min USDC: ${ethers.formatUnits(minAmountOut, 6)}`); + + const depositForBurnData = buildDepositForBurnCalldata(signerAddress, polyCctp.usdcAddress, baseCctp.cctpDomain); + + await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); + await ensureRouterErc20Balance(signer, TOKENS.USDC_POLYGON_CIRCLE, ROUTER_POLYGON); + + const ahIface = new ethers.Interface([ + 'function transferFrom(address token, address owner, address recipient, uint256 amount)', + ]); + const exec = new ModularActionsBuilder(); + exec.call(ALLOWANCE_HOLDER, ahIface.encodeFunctionData('transferFrom', [TOKENS.AAVE_POLYGON, signerAddress, ROUTER_POLYGON, inputAmount])); + await modularApproveIfNeeded(exec, provider, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, ooRouter, inputAmount, inputAmount); + exec.call(ooRouter, swapData); + exec.call(TOKENS.USDC_POLYGON_CIRCLE, encodeTransfer(signerAddress, feeAmount)); + await modularApproveIfNeeded( + exec, + provider, + ROUTER_POLYGON, + TOKENS.USDC_POLYGON_CIRCLE, + polyCctp.tokenMessenger, + estimatedOut - feeAmount, + ethers.MaxUint256, + ); + const usdcBalance = exec.staticCall(TOKENS.USDC_POLYGON_CIRCLE, encodeBalanceOf(ROUTER_POLYGON)); + exec.call(polyCctp.tokenMessenger, depositForBurnData).spliceArg(0, usdcBalance.returnWord()); + + const callData = routerIface.encodeFunctionData('performActions', [ZERO_BYTES32, exec.toActions()]); + + await ensureAllowanceForAllowanceHolder(signer, TOKENS.AAVE_POLYGON, inputAmount); + const receipt = await execViaAH(signer, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, inputAmount, ROUTER_POLYGON, callData); + + logTxnSummary( + 'Polygon AAVE → Base USDC (CCTP) — performActions postFee', + CHAIN_IDS.POLYGON, + receipt, + ); + + console.log(`\nUSDC mints on Base at ${signerAddress} once CCTP attestation completes.`); +} + +main().catch((err) => { console.error(err); process.exit(1); }); diff --git a/scripts/e2e/cctp/performModularExecution.preFee.ts b/scripts/e2e/cctp/performModularExecution.preFee.ts new file mode 100644 index 0000000..163391f --- /dev/null +++ b/scripts/e2e/cctp/performModularExecution.preFee.ts @@ -0,0 +1,123 @@ +/** + * Route: Polygon USDC → Base USDC (CCTP depositForBurn, no swap) + * Function: performActions (modular) + * Fee: preFee — FEE_BPS of inputAmount USDC transferred to signer before bridge + * + * Modular action sequence: + * [0] AH.transferFrom(USDC, signer, router, inputAmount) + * [1] USDC.transfer(signer, feeAmount) — pre-bridge fee + * [2] USDC.approve(tokenMessenger, MaxUint256) + * [3] STATICCALL USDC.balanceOf(router) + * [4] tokenMessenger.depositForBurn(...) — spliceArg(0) patches amount from [3] + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/cctp/performActions.preFee.ts + */ +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + CCTP_CONFIG, + FEE_BPS, + bpsOf, + RPC, + ALLOWANCE_HOLDER, +} from '../config'; +import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; +import { encodeTransfer, encodeBalanceOf, getWalletErc20Balance } from '../utils/erc20'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { ModularActionsBuilder } from '../utils/modularActionsBuilder/index'; +import { ZERO_BYTES32 } from '../utils/contractTypes'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterErc20Balance } from '../utils/reproducibility'; +import { modularApproveIfNeeded } from '../utils/routerAllowance'; + +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); + +function buildDepositForBurnCalldata( + recipientAddress: string, + burnToken: string, + destinationCctpDomain: number, +): string { + const iface = new ethers.Interface([ + 'function depositForBurn(uint256 amount, uint32 destinationDomain, bytes32 mintRecipient, address burnToken, bytes32 destinationCaller, uint256 maxFee, uint32 minFinalityThreshold) external', + ]); + return iface.encodeFunctionData('depositForBurn', [ + 0n, + destinationCctpDomain, + ethers.zeroPadValue(recipientAddress, 32), + burnToken, + ethers.ZeroHash, + 1_000_000n, + 1000, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error('PRIVATE_KEY env var required'); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance(TOKENS.USDC_POLYGON_CIRCLE, signerAddress, provider); + if (walletBalance === 0n) throw new Error(`Signer ${signerAddress} has zero Circle native USDC on Polygon`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error('Balance too small'); + + const feeAmount = bpsOf(inputAmount, FEE_BPS); + const bridgeAmount = inputAmount - feeAmount; + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`USDC balance: ${ethers.formatUnits(walletBalance, 6)}`); + console.log(`Pre-fee: ${ethers.formatUnits(feeAmount, 6)} USDC (${FEE_BPS} bps)`); + console.log(`Net to bridge: ${ethers.formatUnits(inputAmount - feeAmount, 6)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + const polyCctp = CCTP_CONFIG[CHAIN_IDS.POLYGON]; + const baseCctp = CCTP_CONFIG[CHAIN_IDS.BASE]; + + const depositForBurnData = buildDepositForBurnCalldata(signerAddress, polyCctp.usdcAddress, baseCctp.cctpDomain); + + await ensureRouterErc20Balance(signer, TOKENS.USDC_POLYGON_CIRCLE, ROUTER_POLYGON); + + const ahIface = new ethers.Interface([ + 'function transferFrom(address token, address owner, address recipient, uint256 amount)', + ]); + const exec = new ModularActionsBuilder(); + exec.call(ALLOWANCE_HOLDER, ahIface.encodeFunctionData('transferFrom', [TOKENS.USDC_POLYGON_CIRCLE, signerAddress, ROUTER_POLYGON, inputAmount])); + exec.call(TOKENS.USDC_POLYGON_CIRCLE, encodeTransfer(signerAddress, feeAmount)); + await modularApproveIfNeeded( + exec, + provider, + ROUTER_POLYGON, + TOKENS.USDC_POLYGON_CIRCLE, + polyCctp.tokenMessenger, + bridgeAmount, + ethers.MaxUint256, + ); + const usdcBalance = exec.staticCall(TOKENS.USDC_POLYGON_CIRCLE, encodeBalanceOf(ROUTER_POLYGON)); + exec.call(polyCctp.tokenMessenger, depositForBurnData).spliceArg(0, usdcBalance.returnWord()); + + const callData = routerIface.encodeFunctionData('performActions', [ZERO_BYTES32, exec.toActions()]); + + await ensureAllowanceForAllowanceHolder(signer, TOKENS.USDC_POLYGON_CIRCLE, inputAmount); + const receipt = await execViaAH(signer, ROUTER_POLYGON, TOKENS.USDC_POLYGON_CIRCLE, inputAmount, ROUTER_POLYGON, callData); + + logTxnSummary( + 'Polygon USDC → Base USDC (CCTP) — performActions preFee', + CHAIN_IDS.POLYGON, + receipt, + ); + + console.log(`\nUSDC mints on Base at ${signerAddress} once CCTP attestation completes.`); +} + +main().catch((err) => { console.error(err); process.exit(1); }); diff --git a/scripts/e2e/cctp/swapAndBridge.postFee.balanceOf.ts b/scripts/e2e/cctp/swapAndBridge.postFee.balanceOf.ts new file mode 100644 index 0000000..243e0a7 --- /dev/null +++ b/scripts/e2e/cctp/swapAndBridge.postFee.balanceOf.ts @@ -0,0 +1,222 @@ +/** + * Route: Polygon AAVE → USDC (OpenOcean) → Base USDC (CCTP depositForBurn) + * Flags: post-fee (fee taken from USDC output after swap), output measured as USDC balanceOf delta + * bridge amount spliced into depositForBurn calldata at byte offset 4 (amount param) + * + * Post-fee (bit0=1): feeAmount = FEE_BPS of estimatedOut USDC, deducted from swap output. + * BalanceOf (bit1=1): final USDC amount is measured as router USDC balance change (not returndata). + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/cctp/swapAndBridge.postFee.balanceOf.ts + */ +import axios from "axios"; +import { ethers } from "ethers"; +import * as dotenv from "dotenv"; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + CCTP_CONFIG, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, +} from "../config"; +import { + execViaAH, + ensureAllowanceForAllowanceHolder, +} from "../utils/allowanceHolder"; +import { getWalletErc20Balance } from "../utils/erc20"; +import { ROUTER_ABI } from "../utils/routerAbi"; +import { ZERO_BYTES32, bridgeAmountPositionFlag, swapAndBridgeArgs } from "../utils/contractTypes"; +import { logTxnSummary } from "../utils/txnLogSummary"; +import { + ensureRouterErc20Balance, +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; + +// post-fee (0x01) | balance-of (0x02) | bridge amount at byte offset 4 (depositForBurn amount param) +const FLAGS = 0x03n | bridgeAmountPositionFlag(4); +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); + +interface OoQuoteResponse { + data: { to: string; data: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.AAVE_POLYGON, + outTokenAddress: TOKENS.USDC_POLYGON_CIRCLE, + amount: ethers.formatUnits(inputAmount, 18), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_POLYGON, + account: ROUTER_POLYGON, + gasPrice: "1", + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.POLYGON}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + }; +} + +function buildDepositForBurnCalldata( + recipientAddress: string, + burnToken: string, + destinationCctpDomain: number +): string { + const iface = new ethers.Interface([ + "function depositForBurn(uint256 amount, uint32 destinationDomain, bytes32 mintRecipient, address burnToken, bytes32 destinationCaller, uint256 maxFee, uint32 minFinalityThreshold) external", + ]); + return iface.encodeFunctionData("depositForBurn", [ + 0n, + destinationCctpDomain, + ethers.zeroPadValue(recipientAddress, 32), + burnToken, + ethers.ZeroHash, + 1_000_000n, + 1000, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error("PRIVATE_KEY env var required"); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.AAVE_POLYGON, + signerAddress, + provider + ); + if (walletBalance === 0n) + throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error("Balance too small"); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log( + `Flags: 0x${FLAGS.toString( + 16 + )} (post-fee, balanceOf, bridge-amount-pos=4)` + ); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + const polyCctp = CCTP_CONFIG[CHAIN_IDS.POLYGON]; + const baseCctp = CCTP_CONFIG[CHAIN_IDS.BASE]; + + console.log("Fetching OpenOcean quote (AAVE → USDC)..."); + const { ooRouter, swapData, estimatedOut, minAmountOut } = + await fetchOpenOceanQuote(inputAmount); + + // post-fee: deduct from estimated USDC output after swap + const feeAmount = bpsOf(estimatedOut, FEE_BPS); + console.log(` OO router: ${ooRouter}`); + console.log(` Est. USDC: ${ethers.formatUnits(estimatedOut, 6)}`); + console.log( + ` Post-fee: ${ethers.formatUnits(feeAmount, 6)} USDC (${FEE_BPS} bps)` + ); + console.log(` Min USDC: ${ethers.formatUnits(minAmountOut, 6)}`); + + const depositForBurnData = buildDepositForBurnCalldata( + signerAddress, + polyCctp.usdcAddress, + baseCctp.cctpDomain + ); + + await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); + await ensureRouterErc20Balance( + signer, + TOKENS.USDC_POLYGON_CIRCLE, + ROUTER_POLYGON + ); + + const swapApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + ooRouter, + inputAmount, + ); + const bridgeApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + TOKENS.USDC_POLYGON_CIRCLE, + polyCctp.tokenMessenger, + estimatedOut - feeAmount, + ); + + const callData = routerIface.encodeFunctionData("swapAndBridge", swapAndBridgeArgs( + ZERO_BYTES32, + FLAGS, + { + user: signerAddress, + inputToken: TOKENS.AAVE_POLYGON, + inputAmount: inputAmount, + }, + { receiver: signerAddress, amount: feeAmount }, + { + target: ooRouter, + approvalSpender: swapApprovalSpender, + outputToken: TOKENS.USDC_POLYGON_CIRCLE, + value: 0n, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + swapData, + { + target: polyCctp.tokenMessenger, + approvalSpender: bridgeApprovalSpender, + value: 0n, + }, + depositForBurnData, + )); + + await ensureAllowanceForAllowanceHolder( + signer, + TOKENS.AAVE_POLYGON, + inputAmount + ); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + inputAmount, + ROUTER_POLYGON, + callData + ); + + logTxnSummary( + `Polygon AAVE → Base USDC (CCTP) — swapAndBridge postFee/balanceOf`, + CHAIN_IDS.POLYGON, + receipt + ); + + console.log( + `\nUSDC mints on Base at ${signerAddress} once CCTP attestation completes.` + ); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/cctp/swapAndBridge.postFee.returndata.kyberswap.ts b/scripts/e2e/cctp/swapAndBridge.postFee.returndata.kyberswap.ts new file mode 100644 index 0000000..0d9c5b0 --- /dev/null +++ b/scripts/e2e/cctp/swapAndBridge.postFee.returndata.kyberswap.ts @@ -0,0 +1,276 @@ +/** + * Route: Polygon AAVE → USDC (KyberSwap) → Base USDC (CCTP depositForBurn) + * Flags: post-fee (fee taken from USDC output after swap), output read from swap returndata word 0 + * bridge amount spliced into depositForBurn calldata at byte offset 4 (amount param) + * + * Post-fee (bit0=1): feeAmount = FEE_BPS of estimatedOut USDC, deducted from swap output. + * Returndata (bit1=0): final USDC amount is read from word 0 of the swap call returndata. + * + * Usage: + * PRIVATE_KEY=0x... KYBERSWAP_API_KEY=... ts-node scripts/e2e/cctp/swapAndBridge.postFee.returndata.kyberswap.ts + */ +import axios from "axios"; +import { ethers } from "ethers"; +import * as dotenv from "dotenv"; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + CCTP_CONFIG, + FEE_BPS, + bpsOf, + RPC, + KYBERSWAP_API_KEY, + SWAP_SLIPPAGE_BPS, +} from "../config"; +import { + execViaAH, + ensureAllowanceForAllowanceHolder, +} from "../utils/allowanceHolder"; +import { getWalletErc20Balance } from "../utils/erc20"; +import { ROUTER_ABI } from "../utils/routerAbi"; +import { ZERO_BYTES32, bridgeAmountPositionFlag, swapAndBridgeArgs } from "../utils/contractTypes"; +import { logTxnSummary } from "../utils/txnLogSummary"; +import { + ensureRouterErc20Balance, +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; + +// post-fee (0x01) | bridge amount at byte offset 4 (depositForBurn amount param) +const FLAGS = 0x01n | bridgeAmountPositionFlag(4); +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); +const KYBERSWAP_BASE_URL = "https://aggregator-api.kyberswap.com"; +const KYBERSWAP_CHAIN = "polygon"; + +interface KyberRouteData { + routeSummary: object; + routerAddress: string; +} + +interface KyberBuildData { + routerAddress: string; + amountOut: string; + data: string; + transactionValue: string; +} + +/** + * Post-fee Kyber build: sender and recipient are the router so gross USDC stays on-contract + * before fee deduction and CCTP burn (same net shape as the OpenOcean post-fee script). + */ +async function fetchKyberSwapQuote( + amountIn: bigint, + routerAddress: string, +): Promise<{ + ksRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; + value: bigint; +}> { + const headers: Record = {}; + if (KYBERSWAP_API_KEY) { + headers["x-client-id"] = KYBERSWAP_API_KEY; + } + + const routeUrl = + `${KYBERSWAP_BASE_URL}/${KYBERSWAP_CHAIN}/api/v1/routes` + + `?tokenIn=${TOKENS.AAVE_POLYGON}&tokenOut=${TOKENS.USDC_POLYGON_CIRCLE}` + + `&amountIn=${amountIn.toString()}&excludedSources=bebop&gasInclude=true`; + + const routeResp = await axios.get<{ code: number; data: KyberRouteData }>( + routeUrl, + { headers }, + ); + if (!routeResp.data?.data?.routeSummary) { + throw new Error( + `KyberSwap routes call failed: ${JSON.stringify(routeResp.data)}`, + ); + } + const { routeSummary } = routeResp.data.data; + + const buildUrl = `${KYBERSWAP_BASE_URL}/${KYBERSWAP_CHAIN}/api/v1/route/build`; + const buildResp = await axios.post<{ code: number; data: KyberBuildData }>( + buildUrl, + { + routeSummary, + sender: routerAddress, + recipient: routerAddress, + slippageTolerance: SWAP_SLIPPAGE_BPS, + }, + { headers }, + ); + if (!buildResp.data?.data?.data) { + throw new Error( + `KyberSwap build call failed: ${JSON.stringify(buildResp.data)}`, + ); + } + + const { routerAddress: ksRouter, amountOut, data, transactionValue } = + buildResp.data.data; + const estimatedOut = BigInt(amountOut); + + return { + ksRouter, + swapData: data, + estimatedOut, + minAmountOut: + (estimatedOut * (10000n - BigInt(SWAP_SLIPPAGE_BPS))) / 10000n, + value: BigInt(transactionValue ?? "0"), + }; +} + +function buildDepositForBurnCalldata( + recipientAddress: string, + burnToken: string, + destinationCctpDomain: number +): string { + const iface = new ethers.Interface([ + "function depositForBurn(uint256 amount, uint32 destinationDomain, bytes32 mintRecipient, address burnToken, bytes32 destinationCaller, uint256 maxFee, uint32 minFinalityThreshold) external", + ]); + return iface.encodeFunctionData("depositForBurn", [ + 0n, + destinationCctpDomain, + ethers.zeroPadValue(recipientAddress, 32), + burnToken, + ethers.ZeroHash, + 1_000_000n, + 1000, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error("PRIVATE_KEY env var required"); + if (!KYBERSWAP_API_KEY) { + console.warn( + "KYBERSWAP_API_KEY not set — unauthenticated requests may be rate-limited", + ); + } + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.AAVE_POLYGON, + signerAddress, + provider, + ); + if (walletBalance === 0n) + throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error("Balance too small"); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log( + `Flags: 0x${FLAGS.toString( + 16 + )} (post-fee, returndata, bridge-amount-pos=4)`, + ); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + const polyCctp = CCTP_CONFIG[CHAIN_IDS.POLYGON]; + const baseCctp = CCTP_CONFIG[CHAIN_IDS.BASE]; + + console.log("Fetching KyberSwap quote (AAVE → USDC)..."); + const { ksRouter, swapData, estimatedOut, minAmountOut, value } = + await fetchKyberSwapQuote(inputAmount, ROUTER_POLYGON); + + // post-fee: deduct from estimated USDC output after swap + const feeAmount = bpsOf(estimatedOut, FEE_BPS); + console.log(` KS router: ${ksRouter}`); + console.log(` Est. USDC: ${ethers.formatUnits(estimatedOut, 6)}`); + console.log( + ` Post-fee: ${ethers.formatUnits(feeAmount, 6)} USDC (${FEE_BPS} bps)`, + ); + console.log(` Min USDC: ${ethers.formatUnits(minAmountOut, 6)}`); + + const depositForBurnData = buildDepositForBurnCalldata( + signerAddress, + polyCctp.usdcAddress, + baseCctp.cctpDomain + ); + + await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); + await ensureRouterErc20Balance( + signer, + TOKENS.USDC_POLYGON_CIRCLE, + ROUTER_POLYGON + ); + + const swapApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + ksRouter, + inputAmount, + ); + const bridgeApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + TOKENS.USDC_POLYGON_CIRCLE, + polyCctp.tokenMessenger, + estimatedOut - feeAmount, + ); + + const callData = routerIface.encodeFunctionData("swapAndBridge", swapAndBridgeArgs( + ZERO_BYTES32, + FLAGS, + { + user: signerAddress, + inputToken: TOKENS.AAVE_POLYGON, + inputAmount: inputAmount, + }, + { receiver: signerAddress, amount: feeAmount }, + { + target: ksRouter, + approvalSpender: swapApprovalSpender, + outputToken: TOKENS.USDC_POLYGON_CIRCLE, + value, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + swapData, + { + target: polyCctp.tokenMessenger, + approvalSpender: bridgeApprovalSpender, + value: 0n, + }, + depositForBurnData, + )); + + await ensureAllowanceForAllowanceHolder( + signer, + TOKENS.AAVE_POLYGON, + inputAmount + ); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + inputAmount, + ROUTER_POLYGON, + callData + ); + + logTxnSummary( + `Polygon AAVE → Base USDC (CCTP) — swapAndBridge postFee/returndata (Kyber)`, + CHAIN_IDS.POLYGON, + receipt + ); + + console.log( + `\nUSDC mints on Base at ${signerAddress} once CCTP attestation completes.` + ); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/cctp/swapAndBridge.postFee.returndata.ts b/scripts/e2e/cctp/swapAndBridge.postFee.returndata.ts new file mode 100644 index 0000000..415e714 --- /dev/null +++ b/scripts/e2e/cctp/swapAndBridge.postFee.returndata.ts @@ -0,0 +1,222 @@ +/** + * Route: Polygon AAVE → USDC (OpenOcean) → Base USDC (CCTP depositForBurn) + * Flags: post-fee (fee taken from USDC output after swap), output read from swap returndata word 0 + * bridge amount spliced into depositForBurn calldata at byte offset 4 (amount param) + * + * Post-fee (bit0=1): feeAmount = FEE_BPS of estimatedOut USDC, deducted from swap output. + * Returndata (bit1=0): final USDC amount is read from word 0 of the swap call returndata. + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/cctp/swapAndBridge.postFee.returndata.ts + */ +import axios from "axios"; +import { ethers } from "ethers"; +import * as dotenv from "dotenv"; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + CCTP_CONFIG, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, +} from "../config"; +import { + execViaAH, + ensureAllowanceForAllowanceHolder, +} from "../utils/allowanceHolder"; +import { getWalletErc20Balance } from "../utils/erc20"; +import { ROUTER_ABI } from "../utils/routerAbi"; +import { ZERO_BYTES32, bridgeAmountPositionFlag, swapAndBridgeArgs } from "../utils/contractTypes"; +import { logTxnSummary } from "../utils/txnLogSummary"; +import { + ensureRouterErc20Balance, +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; + +// post-fee (0x01) | bridge amount at byte offset 4 (depositForBurn amount param) +const FLAGS = 0x01n | bridgeAmountPositionFlag(4); +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); + +interface OoQuoteResponse { + data: { to: string; data: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.AAVE_POLYGON, + outTokenAddress: TOKENS.USDC_POLYGON_CIRCLE, + amount: ethers.formatUnits(inputAmount, 18), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_POLYGON, + account: ROUTER_POLYGON, + gasPrice: "1", + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.POLYGON}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + }; +} + +function buildDepositForBurnCalldata( + recipientAddress: string, + burnToken: string, + destinationCctpDomain: number +): string { + const iface = new ethers.Interface([ + "function depositForBurn(uint256 amount, uint32 destinationDomain, bytes32 mintRecipient, address burnToken, bytes32 destinationCaller, uint256 maxFee, uint32 minFinalityThreshold) external", + ]); + return iface.encodeFunctionData("depositForBurn", [ + 0n, + destinationCctpDomain, + ethers.zeroPadValue(recipientAddress, 32), + burnToken, + ethers.ZeroHash, + 1_000_000n, + 1000, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error("PRIVATE_KEY env var required"); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.AAVE_POLYGON, + signerAddress, + provider + ); + if (walletBalance === 0n) + throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error("Balance too small"); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log( + `Flags: 0x${FLAGS.toString( + 16 + )} (post-fee, returndata, bridge-amount-pos=4)` + ); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + const polyCctp = CCTP_CONFIG[CHAIN_IDS.POLYGON]; + const baseCctp = CCTP_CONFIG[CHAIN_IDS.BASE]; + + console.log("Fetching OpenOcean quote (AAVE → USDC)..."); + const { ooRouter, swapData, estimatedOut, minAmountOut } = + await fetchOpenOceanQuote(inputAmount); + + // post-fee: deduct from estimated USDC output after swap + const feeAmount = bpsOf(estimatedOut, FEE_BPS); + console.log(` OO router: ${ooRouter}`); + console.log(` Est. USDC: ${ethers.formatUnits(estimatedOut, 6)}`); + console.log( + ` Post-fee: ${ethers.formatUnits(feeAmount, 6)} USDC (${FEE_BPS} bps)` + ); + console.log(` Min USDC: ${ethers.formatUnits(minAmountOut, 6)}`); + + const depositForBurnData = buildDepositForBurnCalldata( + signerAddress, + polyCctp.usdcAddress, + baseCctp.cctpDomain + ); + + await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); + await ensureRouterErc20Balance( + signer, + TOKENS.USDC_POLYGON_CIRCLE, + ROUTER_POLYGON + ); + + const swapApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + ooRouter, + inputAmount, + ); + const bridgeApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + TOKENS.USDC_POLYGON_CIRCLE, + polyCctp.tokenMessenger, + estimatedOut - feeAmount, + ); + + const callData = routerIface.encodeFunctionData("swapAndBridge", swapAndBridgeArgs( + ZERO_BYTES32, + FLAGS, + { + user: signerAddress, + inputToken: TOKENS.AAVE_POLYGON, + inputAmount: inputAmount, + }, + { receiver: signerAddress, amount: feeAmount }, + { + target: ooRouter, + approvalSpender: swapApprovalSpender, + outputToken: TOKENS.USDC_POLYGON_CIRCLE, + value: 0n, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + swapData, + { + target: polyCctp.tokenMessenger, + approvalSpender: bridgeApprovalSpender, + value: 0n, + }, + depositForBurnData, + )); + + await ensureAllowanceForAllowanceHolder( + signer, + TOKENS.AAVE_POLYGON, + inputAmount + ); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + inputAmount, + ROUTER_POLYGON, + callData + ); + + logTxnSummary( + `Polygon AAVE → Base USDC (CCTP) — swapAndBridge postFee/returndata`, + CHAIN_IDS.POLYGON, + receipt + ); + + console.log( + `\nUSDC mints on Base at ${signerAddress} once CCTP attestation completes.` + ); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/cctp/swapAndBridge.preFee.balanceOf.ts b/scripts/e2e/cctp/swapAndBridge.preFee.balanceOf.ts new file mode 100644 index 0000000..1e2449a --- /dev/null +++ b/scripts/e2e/cctp/swapAndBridge.preFee.balanceOf.ts @@ -0,0 +1,224 @@ +/** + * Route: Polygon AAVE → USDC (OpenOcean) → Base USDC (CCTP depositForBurn) + * Flags: pre-fee (fee taken from AAVE input before swap), output measured as USDC balanceOf delta + * bridge amount spliced into depositForBurn calldata at byte offset 4 (amount param) + * + * Pre-fee (bit0=0): feeAmount = FEE_BPS of inputAmount, deducted from AAVE before the swap. + * BalanceOf (bit1=1): final USDC amount is measured as router USDC balance change (not returndata). + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/cctp/swapAndBridge.preFee.balanceOf.ts + */ +import axios from "axios"; +import { ethers } from "ethers"; +import * as dotenv from "dotenv"; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + CCTP_CONFIG, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, +} from "../config"; +import { + execViaAH, + ensureAllowanceForAllowanceHolder, +} from "../utils/allowanceHolder"; +import { getWalletErc20Balance } from "../utils/erc20"; +import { ROUTER_ABI } from "../utils/routerAbi"; +import { ZERO_BYTES32, bridgeAmountPositionFlag, swapAndBridgeArgs } from "../utils/contractTypes"; +import { logTxnSummary } from "../utils/txnLogSummary"; +import { + ensureRouterErc20Balance, +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; + +// pre-fee (0x00) | balance-of (0x02) | bridge amount at byte offset 4 (depositForBurn amount param) +const FLAGS = 0x02n | bridgeAmountPositionFlag(4); +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); + +interface OoQuoteResponse { + data: { to: string; data: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.AAVE_POLYGON, + outTokenAddress: TOKENS.USDC_POLYGON_CIRCLE, + amount: ethers.formatUnits(inputAmount, 18), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_POLYGON, + account: ROUTER_POLYGON, + gasPrice: "1", + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.POLYGON}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + }; +} + +function buildDepositForBurnCalldata( + recipientAddress: string, + burnToken: string, + destinationCctpDomain: number +): string { + const iface = new ethers.Interface([ + "function depositForBurn(uint256 amount, uint32 destinationDomain, bytes32 mintRecipient, address burnToken, bytes32 destinationCaller, uint256 maxFee, uint32 minFinalityThreshold) external", + ]); + return iface.encodeFunctionData("depositForBurn", [ + 0n, + destinationCctpDomain, + ethers.zeroPadValue(recipientAddress, 32), + burnToken, + ethers.ZeroHash, + 1_000_000n, + 1000, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error("PRIVATE_KEY env var required"); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.AAVE_POLYGON, + signerAddress, + provider + ); + if (walletBalance === 0n) + throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error("Balance too small"); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log( + `Flags: 0x${FLAGS.toString( + 16 + )} (pre-fee, balanceOf, bridge-amount-pos=4)` + ); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + const polyCctp = CCTP_CONFIG[CHAIN_IDS.POLYGON]; + const baseCctp = CCTP_CONFIG[CHAIN_IDS.BASE]; + + console.log("Fetching OpenOcean quote (AAVE → USDC)..."); + const { ooRouter, swapData, minAmountOut } = await fetchOpenOceanQuote( + inputAmount + ); + + // pre-fee: deduct from input AAVE before the swap + const feeAmount = bpsOf(inputAmount, FEE_BPS); + const swapInputAmount = inputAmount - feeAmount; + console.log(` OO router: ${ooRouter}`); + console.log( + ` Pre-fee: ${ethers.formatUnits(feeAmount, 18)} AAVE (${FEE_BPS} bps)` + ); + console.log(` Swap input: ${ethers.formatUnits(swapInputAmount, 18)} AAVE`); + console.log(` Min USDC: ${ethers.formatUnits(minAmountOut, 6)}`); + + const depositForBurnData = buildDepositForBurnCalldata( + signerAddress, + polyCctp.usdcAddress, + baseCctp.cctpDomain + ); + + await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); + await ensureRouterErc20Balance( + signer, + TOKENS.USDC_POLYGON_CIRCLE, + ROUTER_POLYGON + ); + + const swapApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + ooRouter, + swapInputAmount, + ); + const bridgeApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + TOKENS.USDC_POLYGON_CIRCLE, + polyCctp.tokenMessenger, + minAmountOut, + ); + + const callData = routerIface.encodeFunctionData("swapAndBridge", swapAndBridgeArgs( + ZERO_BYTES32, + FLAGS, + { + user: signerAddress, + inputToken: TOKENS.AAVE_POLYGON, + inputAmount: inputAmount, + }, + { receiver: signerAddress, amount: feeAmount }, + { + target: ooRouter, + approvalSpender: swapApprovalSpender, + outputToken: TOKENS.USDC_POLYGON_CIRCLE, + value: 0n, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + swapData, + { + target: polyCctp.tokenMessenger, + approvalSpender: bridgeApprovalSpender, + value: 0n, + }, + depositForBurnData, + )); + + await ensureAllowanceForAllowanceHolder( + signer, + TOKENS.AAVE_POLYGON, + inputAmount + ); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + inputAmount, + ROUTER_POLYGON, + callData + ); + + logTxnSummary( + `Polygon AAVE → Base USDC (CCTP) — swapAndBridge preFee/balanceOf`, + CHAIN_IDS.POLYGON, + receipt + ); + + console.log( + `\nUSDC mints on Base at ${signerAddress} once CCTP attestation completes.` + ); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/cctp/swapAndBridge.preFee.returndata.ts b/scripts/e2e/cctp/swapAndBridge.preFee.returndata.ts new file mode 100644 index 0000000..72050de --- /dev/null +++ b/scripts/e2e/cctp/swapAndBridge.preFee.returndata.ts @@ -0,0 +1,224 @@ +/** + * Route: Polygon AAVE → USDC (OpenOcean) → Base USDC (CCTP depositForBurn) + * Flags: pre-fee (fee taken from AAVE input before swap), output read from swap returndata word 0 + * bridge amount spliced into depositForBurn calldata at byte offset 4 (amount param) + * + * Pre-fee (bit0=0): feeAmount = FEE_BPS of inputAmount, deducted from AAVE before the swap. + * Returndata (bit1=0): final USDC amount is read from word 0 of the swap call returndata. + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/cctp/swapAndBridge.preFee.returndata.ts + */ +import axios from "axios"; +import { ethers } from "ethers"; +import * as dotenv from "dotenv"; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + CCTP_CONFIG, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, +} from "../config"; +import { + execViaAH, + ensureAllowanceForAllowanceHolder, +} from "../utils/allowanceHolder"; +import { getWalletErc20Balance } from "../utils/erc20"; +import { ROUTER_ABI } from "../utils/routerAbi"; +import { ZERO_BYTES32, bridgeAmountPositionFlag, swapAndBridgeArgs } from "../utils/contractTypes"; +import { logTxnSummary } from "../utils/txnLogSummary"; +import { + ensureRouterErc20Balance, +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; + +// pre-fee (0x00) | bridge amount at byte offset 4 (depositForBurn amount param) +const FLAGS = bridgeAmountPositionFlag(4); +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); + +interface OoQuoteResponse { + data: { to: string; data: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.AAVE_POLYGON, + outTokenAddress: TOKENS.USDC_POLYGON_CIRCLE, + amount: ethers.formatUnits(inputAmount, 18), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_POLYGON, + account: ROUTER_POLYGON, + gasPrice: "1", + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.POLYGON}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + }; +} + +function buildDepositForBurnCalldata( + recipientAddress: string, + burnToken: string, + destinationCctpDomain: number +): string { + const iface = new ethers.Interface([ + "function depositForBurn(uint256 amount, uint32 destinationDomain, bytes32 mintRecipient, address burnToken, bytes32 destinationCaller, uint256 maxFee, uint32 minFinalityThreshold) external", + ]); + return iface.encodeFunctionData("depositForBurn", [ + 0n, + destinationCctpDomain, + ethers.zeroPadValue(recipientAddress, 32), + burnToken, + ethers.ZeroHash, + 1_000_000n, + 1000, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error("PRIVATE_KEY env var required"); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.AAVE_POLYGON, + signerAddress, + provider + ); + if (walletBalance === 0n) + throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error("Balance too small"); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log( + `Flags: 0x${FLAGS.toString( + 16 + )} (pre-fee, returndata, bridge-amount-pos=4)` + ); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + const polyCctp = CCTP_CONFIG[CHAIN_IDS.POLYGON]; + const baseCctp = CCTP_CONFIG[CHAIN_IDS.BASE]; + + console.log("Fetching OpenOcean quote (AAVE → USDC)..."); + const { ooRouter, swapData, minAmountOut } = await fetchOpenOceanQuote( + inputAmount + ); + + // pre-fee: deduct from input AAVE before the swap + const feeAmount = bpsOf(inputAmount, FEE_BPS); + const swapInputAmount = inputAmount - feeAmount; + console.log(` OO router: ${ooRouter}`); + console.log( + ` Pre-fee: ${ethers.formatUnits(feeAmount, 18)} AAVE (${FEE_BPS} bps)` + ); + console.log(` Swap input: ${ethers.formatUnits(swapInputAmount, 18)} AAVE`); + console.log(` Min USDC: ${ethers.formatUnits(minAmountOut, 6)}`); + + const depositForBurnData = buildDepositForBurnCalldata( + signerAddress, + polyCctp.usdcAddress, + baseCctp.cctpDomain + ); + + await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); + await ensureRouterErc20Balance( + signer, + TOKENS.USDC_POLYGON_CIRCLE, + ROUTER_POLYGON + ); + + const swapApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + ooRouter, + swapInputAmount, + ); + const bridgeApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + TOKENS.USDC_POLYGON_CIRCLE, + polyCctp.tokenMessenger, + minAmountOut, + ); + + const callData = routerIface.encodeFunctionData("swapAndBridge", swapAndBridgeArgs( + ZERO_BYTES32, + FLAGS, + { + user: signerAddress, + inputToken: TOKENS.AAVE_POLYGON, + inputAmount: inputAmount, + }, + { receiver: signerAddress, amount: feeAmount }, + { + target: ooRouter, + approvalSpender: swapApprovalSpender, + outputToken: TOKENS.USDC_POLYGON_CIRCLE, + value: 0n, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + swapData, + { + target: polyCctp.tokenMessenger, + approvalSpender: bridgeApprovalSpender, + value: 0n, + }, + depositForBurnData, + )); + + await ensureAllowanceForAllowanceHolder( + signer, + TOKENS.AAVE_POLYGON, + inputAmount + ); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + inputAmount, + ROUTER_POLYGON, + callData + ); + + logTxnSummary( + `Polygon AAVE → Base USDC (CCTP) — swapAndBridge preFee/returndata`, + CHAIN_IDS.POLYGON, + receipt + ); + + console.log( + `\nUSDC mints on Base at ${signerAddress} once CCTP attestation completes.` + ); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/config.ts b/scripts/e2e/config.ts new file mode 100644 index 0000000..5aca8d4 --- /dev/null +++ b/scripts/e2e/config.ts @@ -0,0 +1,247 @@ +/** + * Shared configuration: addresses, chain IDs, token info, and CCTP config + * used across all e2e scripts. + */ +import * as dotenv from 'dotenv'; +dotenv.config(); + +// ─── Chain IDs ─────────────────────────────────────────────────────────────── + +export const CHAIN_IDS = { + ETHEREUM: 1, + ARBITRUM: 42161, + BASE: 8453, + /** Polygon PoS mainnet — used by e2e scripts as the source chain. */ + POLYGON: 137, +} as const; + +/** Base URL for explorer transaction pages: `${prefix}${txHash}`. */ +export const BLOCK_EXPLORER_TX_PREFIX: Record = { + [CHAIN_IDS.ETHEREUM]: 'https://etherscan.io/tx/', + [CHAIN_IDS.ARBITRUM]: 'https://arbiscan.io/tx/', + [CHAIN_IDS.BASE]: 'https://basescan.org/tx/', + [CHAIN_IDS.POLYGON]: 'https://polygonscan.com/tx/', +}; + +// ─── Contract addresses ─────────────────────────────────────────────────────── + +/** 0x AllowanceHolder — same address on every EVM chain */ +export const ALLOWANCE_HOLDER = '0x0000000000001fF3684f28c67538d4D072C22734'; + +/** + * Deployed `OpenRouterV2Unchecked` — one address per chain used by e2e scripts. + * Override per chain with env `ROUTER_CHAIN_` (e.g. ROUTER_CHAIN_1 for Ethereum). + * Chains without an entry fall back to legacy `ROUTER_ADDRESS` when set. + */ +export const ROUTER_BY_CHAIN_ID: Record = { + [CHAIN_IDS.POLYGON]: '0x33654252CEA9c95220Aa1d434a3631d5c0843AA4', + [CHAIN_IDS.ARBITRUM]: '0xe1788A0374EF5D4C35e62478FdB35F37CeE5B951', + [CHAIN_IDS.BASE]: '0x91b536E79cd3607b593f3011937862609D608253', + [CHAIN_IDS.ETHEREUM]: '0xeB5ae85Fe7e3E272Ac77fd316079589C0Ed91648', +}; + +const ADDR_HEX_RE = /^0x[a-fA-F0-9]{40}$/; + +/** + * Resolve router contract address for execution quotes / AH.exec on `chainId`. + * + * Priority: `ROUTER_CHAIN_` env → {@link ROUTER_BY_CHAIN_ID} → `ROUTER_ADDRESS` env. + */ +export function routerAddressForChain(chainId: number): string { + const envSpecific = process.env[`ROUTER_CHAIN_${chainId}`]?.trim(); + if (envSpecific && ADDR_HEX_RE.test(envSpecific)) { + return envSpecific; + } + const mapped = ROUTER_BY_CHAIN_ID[chainId]; + if (mapped) { + return mapped; + } + const legacy = process.env.ROUTER_ADDRESS?.trim(); + if (legacy && ADDR_HEX_RE.test(legacy)) { + return legacy; + } + throw new Error( + `routerAddressForChain(${chainId}): no ROUTER_BY_CHAIN_ID entry and neither ROUTER_CHAIN_${chainId} nor ROUTER_ADDRESS is set.`, + ); +} + +/** Standard ERC-20 "native" sentinel used by CurrencyLib */ +export const NATIVE_TOKEN_ADDRESS = + '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'; + +// ─── Token addresses ────────────────────────────────────────────────────────── + +export const TOKENS = { + AAVE_ARB: '0xba5DdD1f9d7F570dc94a51479a000E3BCE967196', + AAVE_ETH: '0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9', + /** Polygon PoS AAVE (aPolAAVE ERC-4626-backed). */ + AAVE_POLYGON: '0xD6DF932A45C0f255f85145f286eA0b292B21C90B', + USDC_ARB: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', + USDC_BASE: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', + USDC_ETH: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + /** Bridged PoS-USDC on Polygon (6 decimals); not burnable via Circle CCTP. */ + USDC_POLYGON: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359', + /** Circle native USDC on Polygon — required for CCTP `depositForBurn` on PoS. */ + USDC_POLYGON_CIRCLE: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359', + /** Canonical wrapped gas token on Polygon PoS (18 decimals). */ + WMATIC_POLYGON: '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270', + AAVE_BASE: '0x63706e401c06ac8513145b7687a14804d17f814b', + /** + * USDT0 on Polygon PoS — the inner ERC-20 token that the OFT Adapter wraps (6 decimals). + * Users approve the OFT Adapter to pull this token, then call Adapter.send(). + */ + USDT0_POLYGON: '0xc2132D05D31c914a87C6611C10748AEb04B58e8F', + /** + * USDT0 on Base — a native OFT (the token contract itself IS the OFT; no separate adapter). + * Bridged in via LayerZero from Polygon via the USDT0 OFT Adapter on Polygon. + */ + USDT0_BASE: '0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2', + /** + * Arbitrum USDT — inner token for the USDT0 OFT stack (6 decimals). + * OFT adapter pulls this ERC-20, then calls LayerZero `send()` to Base USDT0. + * Matches bungee `oftSupportedTokens` / {@link USDT0_OFT_ADAPTER_ARBITRUM}. + */ + USDT0_ARB: '0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9', +} as const; + +/** + * USDT0 OFT Adapter on Polygon PoS (legacy / alternate route; Polygon→Base may hit LZ NoPeer). + * Type: OFT_ADAPTER — requires ERC-20 approval of TOKENS.USDT0_POLYGON before calling send(). + */ +export const USDT0_OFT_ADAPTER_POLYGON = '0x6ba10300f0dc58b7a1e4c0e41f5dabb7d7829e13'; + +/** + * USDT0 OFT Adapter on Arbitrum — quote `send()` here; approve adapter to spend {@link TOKENS.USDT0_ARB}. + * Seed doc: `oftId` `42161-0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9`, `adapterAddress` on Arbitrum. + */ +export const USDT0_OFT_ADAPTER_ARBITRUM = + '0x14e4a1b13Bf7F943c8fF7C51fb60fa964A298d92'; + +/** + * LayerZero v2 endpoint ID for Polygon PoS (EID 30109). + */ +export const POLYGON_LZ_EID = 30109; + +// ─── CCTP v2 configuration ──────────────────────────────────────────────────── + +export interface CctpChainConfig { + tokenMessenger: string; + /** Circle's domain identifier for CCTP */ + cctpDomain: number; + usdcAddress: string; +} + +export const CCTP_CONFIG: Record = { + [CHAIN_IDS.POLYGON]: { + tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d', + cctpDomain: 7, + usdcAddress: TOKENS.USDC_POLYGON_CIRCLE, + }, + [CHAIN_IDS.ARBITRUM]: { + tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d', + cctpDomain: 3, + usdcAddress: TOKENS.USDC_ARB, + }, + [CHAIN_IDS.BASE]: { + tokenMessenger: '0x1682Ae6375C4E4A97e4B583BC394c861A46D8962', + cctpDomain: 6, + usdcAddress: TOKENS.USDC_BASE, + }, + [CHAIN_IDS.ETHEREUM]: { + tokenMessenger: '0xBd3fa81B58Ba92a82136038B25aDec7066af3155', + cctpDomain: 0, + usdcAddress: TOKENS.USDC_ETH, + }, +}; + +// ─── Arbitrum bridge ────────────────────────────────────────────────────────── + +/** Arbitrum Delayed Inbox — accepts ETH deposits via depositEth() */ +export const ARBITRUM_INBOX = '0x4Dbd4fc535Ac27206064B68FfCf827b0A60BAB3f'; + +// ─── Stargate pools ─────────────────────────────────────────────────────────── + +/** + * Stargate Native ETH OFT adapter on Arbitrum. + * Call send() with msg.value = amountLD + nativeFee to bridge ETH to Base. + * Ref: poc-openrouter/test/poc/OpenOceanStargateNativeSwapFeeBridgeRouterPoC.t.sol + */ +export const STARGATE_NATIVE_ARB = '0xA45B5130f36CDcA45667738e2a258AB09f4A5f7F'; + +/** + * Stargate v2 native ETH OFT pool on Base. + * send() requires msg.value = amountLD + nativeFee; bridges ETH to Arbitrum/other chains. + */ +export const STARGATE_NATIVE_BASE = '0xdc181Bd607330aeeBEF6ea62e03e5e1Fb4B6F7C7'; + +/** + * Stargate v2 USDC pool on Arbitrum. + * ERC20 pool — caller approves USDC to this address; msg.value = nativeFee only. + * Source: https://stargateprotocol.gitbook.io/stargate/v2-developer-docs/technical-reference + */ +export const STARGATE_USDC_ARB = '0xe8CDF27AcD73a434D661C84887215F7598e7d0d3'; + +/** + * Stargate v2 USDC pool on Polygon PoS. + * NOTE: Polygon has no StargatePoolNative for POL/MATIC — only USDC and USDT + * pools are deployed. Bridge token is Circle USDC, not the chain native token. + */ +export const STARGATE_USDC_POLYGON = '0x9Aa02D4Fae7F58b8E8f34c66E756cC734DAc7fe4'; + +/** LayerZero v2 endpoint ID for Base (EID 30184). */ +export const BASE_LZ_EID = 30184; + +/** LayerZero v2 endpoint ID for Arbitrum (EID 30110). */ +export const ARBITRUM_LZ_EID = 30110; + +/** + * Byte offset of sendParam.amountLD within the Stargate send() calldata (after the 4-byte selector). + * Layout: selector(4) + head[sendParam_ptr(32) + nativeFee(32) + lzTokenFee(32) + refundAddr(32)] + + * tail[dstEid(32) + to(32)] + amountLD = 4+128+32+32 = 196 + */ +export const STARGATE_AMOUNT_LD_OFFSET = 196; + +// ─── Fee config ─────────────────────────────────────────────────────────────── + +/** Fee applied in scripts that take pre-/post-route fees (basis points). */ +export const FEE_BPS = Number(process.env.FEE_AMOUNT_BPS ?? '10'); + +/** + * OpenOcean slippage tolerance used when fetching swap quotes. + * The value is passed directly to OO's `slippage` API parameter (percentage string, e.g. '3' = 3%). + * OO embeds this as `minReturn` in the swap calldata — if the actual on-chain output falls below + * `estimatedOut * (1 - slippage/100)`, OO reverts with "Return amount is not enough". + * AAVE's multi-hop route (AAVE→WMATIC→DAI→USDC) can move 2–3% between quote and execution, + * so 1% is too tight; 3% provides a safe margin while still protecting against severe slippage. + * Override via env: OO_SLIPPAGE_PERCENT=5 + */ +export const OO_SLIPPAGE_PERCENT = process.env.OO_SLIPPAGE_PERCENT ?? '3'; + +export function bpsOf(amount: bigint, bps: number): bigint { + return (amount * BigInt(bps)) / 10000n; +} + +// ─── RPC endpoints ──────────────────────────────────────────────────────────── + +export const RPC = { + ARBITRUM: process.env.ARBITRUM_RPC ?? 'https://arb1.arbitrum.io/rpc', + POLYGON: + process.env.POLYGON_RPC ?? 'https://polygon-bor.publicnode.com', + ETHEREUM: process.env.ETHEREUM_RPC ?? 'https://eth.llamarpc.com', + BASE: process.env.BASE_RPC ?? 'https://mainnet.base.org', +} as const; + +// ─── API keys ───────────────────────────────────────────────────────────────── + +export const RELAY_API_KEY: string | undefined = process.env.RELAY_API_KEY; +export const OPEN_OCEAN_API_KEY: string | undefined = + process.env.OPEN_OCEAN_API_KEY; +export const KYBERSWAP_API_KEY: string | undefined = + process.env.KYBERSWAP_API_KEY; +export const ZEROX_API_KEY: string | undefined = process.env.ZEROX_API_KEY; + +/** + * Swap slippage in basis points for KyberSwap and 0x (300 = 3%). + * Matches the default OO_SLIPPAGE_PERCENT of 3%. + */ +export const SWAP_SLIPPAGE_BPS = 300; diff --git a/scripts/e2e/misc/routerUsdc.withdraw.modular.ts b/scripts/e2e/misc/routerUsdc.withdraw.modular.ts new file mode 100644 index 0000000..6ea546f --- /dev/null +++ b/scripts/e2e/misc/routerUsdc.withdraw.modular.ts @@ -0,0 +1,103 @@ +/** + * Polygon: sweep USDC from `OpenRouter` to the tx sender using + * `performActions` only — no AllowanceHolder, no pull step. + * + * Actions: + * [0] STATICCALL USDC.balanceOf(router) — stored returndata (32-byte uint256) + * [1] CALL USDC.transfer(caller, 0) — amount word spliced from [0], so net effect + * is transferring the router's entire USDC balance to `msg.sender` of this tx. + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/misc/routerUsdc.withdraw.modular.ts + * + * Requires the router contract to actually hold Polygon USDC + * ({@link TOKENS.USDC_POLYGON_CIRCLE}). + */ +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { CHAIN_IDS, routerAddressForChain, TOKENS, RPC } from '../config'; +import { + encodeBalanceOf, + encodeTransfer, + getWalletErc20Balance, +} from '../utils/erc20'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { ModularActionsBuilder } from '../utils/modularActionsBuilder/index'; +import { ZERO_BYTES32 } from '../utils/contractTypes'; +import { logTxnSummary } from '../utils/txnLogSummary'; + +async function main(): Promise { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) { + throw new Error('PRIVATE_KEY env var required'); + } + + const chainId = CHAIN_IDS.POLYGON; + const rpcUrl = process.env.POLYGON_RPC ?? process.env.RPC_URL ?? RPC.POLYGON; + const routerAddress = routerAddressForChain(chainId); + const usdc = TOKENS.USDC_POLYGON_CIRCLE; + + const provider = new ethers.JsonRpcProvider(rpcUrl); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: routerBalance } = await getWalletErc20Balance( + usdc, + routerAddress, + provider, + ); + if (routerBalance === 0n) { + throw new Error(`Router ${routerAddress} holds zero USDC on Polygon`); + } + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${routerAddress}`); + console.log(`Router USDC bal: ${ethers.formatUnits(routerBalance, 6)}`); + + const exec = new ModularActionsBuilder(); + const routerBal = exec.staticCall(usdc, encodeBalanceOf(routerAddress)); + + exec + .call(usdc, encodeTransfer(signerAddress, 0n)) + .spliceArg(1, routerBal.ref().returnWord(0)); + + const routerIface = new ethers.Interface(ROUTER_ABI); + const calldata = routerIface.encodeFunctionData('performActions', [ + ZERO_BYTES32, + exec.toActions(), + ]); + + console.log( + 'Sending performActions (balanceOf → transfer with spliced amount)...', + ); + const tx = await signer.sendTransaction({ + to: routerAddress, + data: calldata, + }); + console.log(`Tx hash: ${tx.hash}`); + const receipt = await tx.wait(); + + if (receipt == null || receipt.status !== 1) { + throw new Error('Transaction failed or missing receipt'); + } + + logTxnSummary( + 'Polygon — withdraw router USDC to caller via performActions', + chainId, + receipt, + ); + + const { balance: signerAfter } = await getWalletErc20Balance( + usdc, + signerAddress, + provider, + ); + console.log(`Signer USDC after: ${ethers.formatUnits(signerAfter, 6)}`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/oft/bridge.preFee.ts b/scripts/e2e/oft/bridge.preFee.ts new file mode 100644 index 0000000..756ed5f --- /dev/null +++ b/scripts/e2e/oft/bridge.preFee.ts @@ -0,0 +1,168 @@ +/** + * Route: Polygon USDT0 → Arbitrum USDT0 (USDT0 OFT Adapter, LayerZero v2, no swap) + * Function: bridge (simple bridge entrypoint) + * Fee: preFee — FEE_BPS of inputAmount USDT0 deducted before bridge + * + * Bridge amount is pre-encoded in OFT send() calldata (no splice needed). + * bridge.value = nativeFeeWithBuffer forwarded as LZ msg.value. + * Uses router.bridge() rather than performExecution / performActions. + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/oft/bridge.preFee.ts + */ +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +import { Options } from '@layerzerolabs/lz-v2-utilities'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + ARBITRUM_LZ_EID, + USDT0_OFT_ADAPTER_POLYGON, +} from '../config'; +import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; +import { getWalletErc20Balance } from '../utils/erc20'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { ZERO_BYTES32, type BridgeData, type FeeData, type InputData } from '../utils/contractTypes'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterErc20Balance } from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; + +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); +const LZ_EXTRA_OPTIONS = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toHex(); + +const OFT_ABI = [ + 'function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)', + 'function quoteOFT(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) external view returns (tuple(uint256 minAmountLD, uint256 maxAmountLD) oftLimit, tuple(int256 feeAmountLD, string description)[] oftFeeDetails, tuple(uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt)', + 'function send(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee, address refundAddress) external payable', +]; +const OFT_IFACE = new ethers.Interface(OFT_ABI); + +async function fetchOftQuote( + provider: ethers.JsonRpcProvider, + bridgeAmountLD: bigint, + recipient: string, +): Promise<{ nativeFeeWithBuffer: bigint; amountReceivedLD: bigint }> { + const contract = new ethers.Contract(USDT0_OFT_ADAPTER_POLYGON, OFT_ABI, provider); + const to32 = ethers.zeroPadValue(recipient, 32); + const sendParam = { + dstEid: ARBITRUM_LZ_EID, + to: to32, + amountLD: bridgeAmountLD, + minAmountLD: 0n, + extraOptions: LZ_EXTRA_OPTIONS, + composeMsg: '0x', + oftCmd: '0x', + }; + const [fee, oft] = await Promise.all([ + contract.quoteSend(sendParam, false), + contract.quoteOFT(sendParam), + ]); + return { + nativeFeeWithBuffer: ((fee.nativeFee as bigint) * 105n) / 100n, + amountReceivedLD: oft.oftReceipt.amountReceivedLD as bigint, + }; +} + +/** + * Builds OFT send() calldata with the exact bridgeAmount pre-encoded (no 0 placeholder). + * Used by router.bridge() which has no splice mechanism. + */ +function buildOftSendCalldata(bridgeAmount: bigint, nativeFee: bigint, recipient: string): string { + return OFT_IFACE.encodeFunctionData('send', [ + { + dstEid: ARBITRUM_LZ_EID, + to: ethers.zeroPadValue(recipient, 32), + amountLD: bridgeAmount, + minAmountLD: 0n, + extraOptions: LZ_EXTRA_OPTIONS, + composeMsg: '0x', + oftCmd: '0x', + }, + { nativeFee, lzTokenFee: 0n }, + recipient, + ]); +} + +async function main(): Promise { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) { + throw new Error('PRIVATE_KEY env var required'); + } + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const inputToken = TOKENS.USDT0_POLYGON; + const { balance: walletBalance } = await getWalletErc20Balance(inputToken, signerAddress, provider); + if (walletBalance === 0n) { + throw new Error(`Signer ${signerAddress} has zero USDT0 on Polygon`); + } + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error('Balance too small'); + + const feeAmount = bpsOf(inputAmount, FEE_BPS); + const bridgeAmount = inputAmount - feeAmount; + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`USDT0 balance: ${ethers.formatUnits(walletBalance, 6)}`); + console.log(`Pre-bridge fee: ${ethers.formatUnits(feeAmount, 6)} USDT0 (${FEE_BPS} bps)`); + console.log(`Net to bridge: ${ethers.formatUnits(bridgeAmount, 6)}`); + + console.log('Fetching OFT quote (Polygon → Arbitrum)...'); + const { nativeFeeWithBuffer, amountReceivedLD } = await fetchOftQuote(provider, bridgeAmount, signerAddress); + console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} POL`); + console.log(` Est. received: ${ethers.formatUnits(amountReceivedLD, 6)} USDT0`); + + const sendData = buildOftSendCalldata(bridgeAmount, nativeFeeWithBuffer, signerAddress); + + const input: InputData = { user: signerAddress, inputToken, inputAmount }; + const fee: FeeData = { receiver: signerAddress, amount: feeAmount }; + const bridgeApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + inputToken, + USDT0_OFT_ADAPTER_POLYGON, + bridgeAmount, + ); + + const bridgeData: BridgeData = { + target: USDT0_OFT_ADAPTER_POLYGON, + approvalSpender: bridgeApprovalSpender, + value: nativeFeeWithBuffer, + }; + + const routerIface = new ethers.Interface(ROUTER_ABI); + const execCalldata = routerIface.encodeFunctionData('bridge', [ZERO_BYTES32, input, fee, bridgeData, sendData]); + + await ensureRouterErc20Balance(signer, inputToken, ROUTER_POLYGON); + await ensureAllowanceForAllowanceHolder(signer, inputToken, inputAmount); + + console.log('Sending AllowanceHolder.exec → router.bridge...'); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + inputToken, + inputAmount, + ROUTER_POLYGON, + execCalldata, + nativeFeeWithBuffer, + ); + + logTxnSummary('Polygon USDT0 → Arbitrum USDT0 (OFT) — bridge preFee', CHAIN_IDS.POLYGON, receipt); + + console.log('\nUSDT0 arrives on Arbitrum once LZ delivers the message.'); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/oft/performExecution.postFee.ts b/scripts/e2e/oft/performExecution.postFee.ts new file mode 100644 index 0000000..b8e668f --- /dev/null +++ b/scripts/e2e/oft/performExecution.postFee.ts @@ -0,0 +1,265 @@ +/** + * Route: Polygon AAVE → USDT0 (OpenOcean) → Arbitrum USDT0 (USDT0 OFT Adapter, LayerZero v2) + * Flags: post-fee (fee taken from USDT0 output after swap), output read from swap returndata word 0 + * bridge amount spliced into send() calldata at byte offset 196 (sendParam.amountLD) + * + * Post-fee (bit0=1): feeAmount = FEE_BPS of estimatedOut USDT0, deducted from swap output. + * Returndata (bit1=0): final USDT0 amount is read from word 0 of the swap call returndata. + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/oft/performExecution.postFee.ts + */ +import axios from "axios"; +import { ethers } from "ethers"; +import * as dotenv from "dotenv"; +import { Options } from "@layerzerolabs/lz-v2-utilities"; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, + ARBITRUM_LZ_EID, + USDT0_OFT_ADAPTER_POLYGON, +} from "../config"; +import { + execViaAH, + ensureAllowanceForAllowanceHolder, +} from "../utils/allowanceHolder"; +import { getWalletErc20Balance } from "../utils/erc20"; +import { ROUTER_ABI } from "../utils/routerAbi"; +import { ZERO_BYTES32, bridgeAmountPositionFlag, swapAndBridgeArgs } from "../utils/contractTypes"; +import { logTxnSummary } from "../utils/txnLogSummary"; +import { + ensureRouterErc20Balance, +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; + +// post-fee (0x01) | bridge amount at byte offset 196 (sendParam.amountLD) +const FLAGS = 0x01n | bridgeAmountPositionFlag(196); +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); +const LZ_EXTRA_OPTIONS = Options.newOptions() + .addExecutorLzReceiveOption(65000, 0) + .toHex(); + +const OFT_ABI = [ + "function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)", + "function quoteOFT(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) external view returns (tuple(uint256 minAmountLD, uint256 maxAmountLD) oftLimit, tuple(int256 feeAmountLD, string description)[] oftFeeDetails, tuple(uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt)", + "function send(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee, address refundAddress) external payable", +]; + +const OFT_IFACE = new ethers.Interface(OFT_ABI); + +interface OoQuoteResponse { + data: { to: string; data: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.AAVE_POLYGON, + outTokenAddress: TOKENS.USDT0_POLYGON, + amount: ethers.formatUnits(inputAmount, 18), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_POLYGON, + account: ROUTER_POLYGON, + gasPrice: "1", + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.POLYGON}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + }; +} + +async function fetchOftQuote( + provider: ethers.JsonRpcProvider, + bridgeAmountLD: bigint, + recipient: string +): Promise<{ nativeFeeWithBuffer: bigint; amountReceivedLD: bigint }> { + const contract = new ethers.Contract( + USDT0_OFT_ADAPTER_POLYGON, + OFT_ABI, + provider + ); + const to32 = ethers.zeroPadValue(recipient, 32); + const sendParam = { + dstEid: ARBITRUM_LZ_EID, + to: to32, + amountLD: bridgeAmountLD, + minAmountLD: 0n, + extraOptions: LZ_EXTRA_OPTIONS, + composeMsg: "0x", + oftCmd: "0x", + }; + const [fee, oft] = await Promise.all([ + contract.quoteSend(sendParam, false), + contract.quoteOFT(sendParam), + ]); + const nativeFee = fee.nativeFee as bigint; + return { + nativeFeeWithBuffer: (nativeFee * 105n) / 100n, + amountReceivedLD: oft.oftReceipt.amountReceivedLD as bigint, + }; +} + +function buildOftSendCalldata(nativeFee: bigint, recipient: string): string { + return OFT_IFACE.encodeFunctionData("send", [ + { + dstEid: ARBITRUM_LZ_EID, + to: ethers.zeroPadValue(recipient, 32), + amountLD: 0n, // placeholder — spliced at runtime at offset 196 + minAmountLD: 0n, + extraOptions: LZ_EXTRA_OPTIONS, + composeMsg: "0x", + oftCmd: "0x", + }, + { nativeFee, lzTokenFee: 0n }, + recipient, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error("PRIVATE_KEY env var required"); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.AAVE_POLYGON, + signerAddress, + provider + ); + if (walletBalance === 0n) + throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error("Balance too small"); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log( + `Flags: 0x${FLAGS.toString( + 16 + )} (post-fee, returndata, bridge-amount-pos=196)` + ); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log("Fetching OpenOcean quote (AAVE → USDT0)..."); + const { ooRouter, swapData, estimatedOut, minAmountOut } = + await fetchOpenOceanQuote(inputAmount); + + // post-fee: deduct from estimated USDT0 output after swap + const feeAmount = bpsOf(estimatedOut, FEE_BPS); + const bridgeAmount = estimatedOut - feeAmount; + console.log(` OO router: ${ooRouter}`); + console.log(` Est. USDT0: ${ethers.formatUnits(estimatedOut, 6)}`); + console.log( + ` Post-fee: ${ethers.formatUnits(feeAmount, 6)} USDT0 (${FEE_BPS} bps)` + ); + console.log(` Bridge est: ${ethers.formatUnits(bridgeAmount, 6)}`); + console.log(` Min USDT0: ${ethers.formatUnits(minAmountOut, 6)}`); + + console.log("Fetching OFT quote (Polygon → Arbitrum)..."); + const { nativeFeeWithBuffer, amountReceivedLD } = await fetchOftQuote( + provider, + bridgeAmount, + signerAddress + ); + console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} POL`); + console.log( + ` Est. received: ${ethers.formatUnits(amountReceivedLD, 6)} USDT0` + ); + + await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); + await ensureRouterErc20Balance(signer, TOKENS.USDT0_POLYGON, ROUTER_POLYGON); + + const swapApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + ooRouter, + inputAmount, + ); + const bridgeApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + TOKENS.USDT0_POLYGON, + USDT0_OFT_ADAPTER_POLYGON, + bridgeAmount, + ); + + const oftSendData = buildOftSendCalldata(nativeFeeWithBuffer, signerAddress); + + const callData = routerIface.encodeFunctionData("swapAndBridge", swapAndBridgeArgs( + ZERO_BYTES32, + FLAGS, + { + user: signerAddress, + inputToken: TOKENS.AAVE_POLYGON, + inputAmount: inputAmount, + }, + { receiver: signerAddress, amount: feeAmount }, + { + target: ooRouter, + approvalSpender: swapApprovalSpender, + outputToken: TOKENS.USDT0_POLYGON, + value: 0n, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + swapData, + { + target: USDT0_OFT_ADAPTER_POLYGON, + approvalSpender: bridgeApprovalSpender, + value: nativeFeeWithBuffer, + }, + oftSendData, + )); + + await ensureAllowanceForAllowanceHolder( + signer, + TOKENS.AAVE_POLYGON, + inputAmount + ); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + inputAmount, + ROUTER_POLYGON, + callData, + nativeFeeWithBuffer + ); + + logTxnSummary( + `Polygon AAVE → Arbitrum USDT0 (OFT) — swapAndBridge postFee/returndata`, + CHAIN_IDS.POLYGON, + receipt + ); + + console.log("\nUSDT0 arrives on Arbitrum once LZ delivers the message."); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/oft/performExecution.preFee.ts b/scripts/e2e/oft/performExecution.preFee.ts new file mode 100644 index 0000000..cc16455 --- /dev/null +++ b/scripts/e2e/oft/performExecution.preFee.ts @@ -0,0 +1,168 @@ +/** + * Route: Polygon USDT0 → Arbitrum USDT0 (USDT0 OFT Adapter, LayerZero v2, no swap) + * Function: bridge (simple bridge entrypoint) + * Fee: preFee — FEE_BPS of inputAmount USDT0 deducted before bridge + * + * Bridge amount is pre-encoded in OFT send() calldata (no splice needed). + * bridge.value = nativeFeeWithBuffer forwarded as LZ msg.value. + * Uses router.bridge() rather than performExecution / performActions. + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/oft/performExecution.preFee.ts + */ +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +import { Options } from '@layerzerolabs/lz-v2-utilities'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + ARBITRUM_LZ_EID, + USDT0_OFT_ADAPTER_POLYGON, +} from '../config'; +import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; +import { getWalletErc20Balance } from '../utils/erc20'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { ZERO_BYTES32, type BridgeData, type FeeData, type InputData } from '../utils/contractTypes'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterErc20Balance } from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; + +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); +const LZ_EXTRA_OPTIONS = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toHex(); + +const OFT_ABI = [ + 'function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)', + 'function quoteOFT(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) external view returns (tuple(uint256 minAmountLD, uint256 maxAmountLD) oftLimit, tuple(int256 feeAmountLD, string description)[] oftFeeDetails, tuple(uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt)', + 'function send(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee, address refundAddress) external payable', +]; +const OFT_IFACE = new ethers.Interface(OFT_ABI); + +async function fetchOftQuote( + provider: ethers.JsonRpcProvider, + bridgeAmountLD: bigint, + recipient: string, +): Promise<{ nativeFeeWithBuffer: bigint; amountReceivedLD: bigint }> { + const contract = new ethers.Contract(USDT0_OFT_ADAPTER_POLYGON, OFT_ABI, provider); + const to32 = ethers.zeroPadValue(recipient, 32); + const sendParam = { + dstEid: ARBITRUM_LZ_EID, + to: to32, + amountLD: bridgeAmountLD, + minAmountLD: 0n, + extraOptions: LZ_EXTRA_OPTIONS, + composeMsg: '0x', + oftCmd: '0x', + }; + const [fee, oft] = await Promise.all([ + contract.quoteSend(sendParam, false), + contract.quoteOFT(sendParam), + ]); + return { + nativeFeeWithBuffer: ((fee.nativeFee as bigint) * 105n) / 100n, + amountReceivedLD: oft.oftReceipt.amountReceivedLD as bigint, + }; +} + +/** + * Builds OFT send() calldata with the exact bridgeAmount pre-encoded (no 0 placeholder). + * Used by router.bridge() which has no splice mechanism. + */ +function buildOftSendCalldata(bridgeAmount: bigint, nativeFee: bigint, recipient: string): string { + return OFT_IFACE.encodeFunctionData('send', [ + { + dstEid: ARBITRUM_LZ_EID, + to: ethers.zeroPadValue(recipient, 32), + amountLD: bridgeAmount, + minAmountLD: 0n, + extraOptions: LZ_EXTRA_OPTIONS, + composeMsg: '0x', + oftCmd: '0x', + }, + { nativeFee, lzTokenFee: 0n }, + recipient, + ]); +} + +async function main(): Promise { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) { + throw new Error('PRIVATE_KEY env var required'); + } + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const inputToken = TOKENS.USDT0_POLYGON; + const { balance: walletBalance } = await getWalletErc20Balance(inputToken, signerAddress, provider); + if (walletBalance === 0n) { + throw new Error(`Signer ${signerAddress} has zero USDT0 on Polygon`); + } + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error('Balance too small'); + + const feeAmount = bpsOf(inputAmount, FEE_BPS); + const bridgeAmount = inputAmount - feeAmount; + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`USDT0 balance: ${ethers.formatUnits(walletBalance, 6)}`); + console.log(`Pre-bridge fee: ${ethers.formatUnits(feeAmount, 6)} USDT0 (${FEE_BPS} bps)`); + console.log(`Net to bridge: ${ethers.formatUnits(bridgeAmount, 6)}`); + + console.log('Fetching OFT quote (Polygon → Arbitrum)...'); + const { nativeFeeWithBuffer, amountReceivedLD } = await fetchOftQuote(provider, bridgeAmount, signerAddress); + console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} POL`); + console.log(` Est. received: ${ethers.formatUnits(amountReceivedLD, 6)} USDT0`); + + const sendData = buildOftSendCalldata(bridgeAmount, nativeFeeWithBuffer, signerAddress); + + const input: InputData = { user: signerAddress, inputToken, inputAmount }; + const fee: FeeData = { receiver: signerAddress, amount: feeAmount }; + const bridgeApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + inputToken, + USDT0_OFT_ADAPTER_POLYGON, + bridgeAmount, + ); + + const bridgeData: BridgeData = { + target: USDT0_OFT_ADAPTER_POLYGON, + approvalSpender: bridgeApprovalSpender, + value: nativeFeeWithBuffer, + }; + + const routerIface = new ethers.Interface(ROUTER_ABI); + const execCalldata = routerIface.encodeFunctionData('bridge', [ZERO_BYTES32, input, fee, bridgeData, sendData]); + + await ensureRouterErc20Balance(signer, inputToken, ROUTER_POLYGON); + await ensureAllowanceForAllowanceHolder(signer, inputToken, inputAmount); + + console.log('Sending AllowanceHolder.exec → router.bridge...'); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + inputToken, + inputAmount, + ROUTER_POLYGON, + execCalldata, + nativeFeeWithBuffer, + ); + + logTxnSummary('Polygon USDT0 → Arbitrum USDT0 (OFT) — bridge preFee', CHAIN_IDS.POLYGON, receipt); + + console.log('\nUSDT0 arrives on Arbitrum once LZ delivers the message.'); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/oft/performModularExecution.postFee.ts b/scripts/e2e/oft/performModularExecution.postFee.ts new file mode 100644 index 0000000..be9538b --- /dev/null +++ b/scripts/e2e/oft/performModularExecution.postFee.ts @@ -0,0 +1,185 @@ +/** + * Route: Polygon AAVE → USDT0 (OpenOcean) → Arbitrum USDT0 (USDT0 OFT Adapter, LayerZero v2) + * Function: performActions (modular) + * Fee: postFee — FEE_BPS of estimatedOut USDT0 transferred to signer after swap + * + * Modular action sequence: + * [0] AH.transferFrom(AAVE, signer, router, inputAmount) + * [1] AAVE.approve(ooRouter, inputAmount) + * [2] ooRouter swap — AAVE → USDT0 lands in router + * [3] USDT0.transfer(signer, feeAmount) + * [4] USDT0.approve(adapter, MaxUint256) + * [5] STATICCALL USDT0.balanceOf(router) + * [6] nativeCall adapter.send(...) — spliceWord patches amountLD at offset 196 from [5] + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/oft/performActions.postFee.ts + */ +import axios from 'axios'; +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +import { Options } from '@layerzerolabs/lz-v2-utilities'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, + ALLOWANCE_HOLDER, + ARBITRUM_LZ_EID, + USDT0_OFT_ADAPTER_POLYGON, +} from '../config'; +import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; +import { encodeTransfer, encodeBalanceOf, getWalletErc20Balance } from '../utils/erc20'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { ModularActionsBuilder } from '../utils/modularActionsBuilder/index'; +import { ZERO_BYTES32 } from '../utils/contractTypes'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterErc20Balance } from '../utils/reproducibility'; +import { modularApproveIfNeeded } from '../utils/routerAllowance'; + +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); +const LZ_EXTRA_OPTIONS = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toHex(); +const OFT_AMOUNT_LD_OFFSET = 196; + +const OFT_ABI = [ + 'function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)', + 'function quoteOFT(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) external view returns (tuple(uint256 minAmountLD, uint256 maxAmountLD) oftLimit, tuple(int256 feeAmountLD, string description)[] oftFeeDetails, tuple(uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt)', + 'function send(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee, address refundAddress) external payable', +]; +const OFT_IFACE = new ethers.Interface(OFT_ABI); + +interface OoQuoteResponse { + data: { to: string; data: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.AAVE_POLYGON, + outTokenAddress: TOKENS.USDT0_POLYGON, + amount: ethers.formatUnits(inputAmount, 18), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_POLYGON, + account: ROUTER_POLYGON, + gasPrice: '1', + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.POLYGON}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + }; +} + +async function fetchOftQuote( + provider: ethers.JsonRpcProvider, + bridgeAmountLD: bigint, + recipient: string, +): Promise<{ nativeFeeWithBuffer: bigint; amountReceivedLD: bigint }> { + const contract = new ethers.Contract(USDT0_OFT_ADAPTER_POLYGON, OFT_ABI, provider); + const to32 = ethers.zeroPadValue(recipient, 32); + const sendParam = { dstEid: ARBITRUM_LZ_EID, to: to32, amountLD: bridgeAmountLD, minAmountLD: 0n, extraOptions: LZ_EXTRA_OPTIONS, composeMsg: '0x', oftCmd: '0x' }; + const [fee, oft] = await Promise.all([contract.quoteSend(sendParam, false), contract.quoteOFT(sendParam)]); + return { + nativeFeeWithBuffer: ((fee.nativeFee as bigint) * 105n) / 100n, + amountReceivedLD: oft.oftReceipt.amountReceivedLD as bigint, + }; +} + +function buildOftSendCalldata(nativeFee: bigint, recipient: string): string { + return OFT_IFACE.encodeFunctionData('send', [ + { dstEid: ARBITRUM_LZ_EID, to: ethers.zeroPadValue(recipient, 32), amountLD: 0n, minAmountLD: 0n, extraOptions: LZ_EXTRA_OPTIONS, composeMsg: '0x', oftCmd: '0x' }, + { nativeFee, lzTokenFee: 0n }, + recipient, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error('PRIVATE_KEY env var required'); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance(TOKENS.AAVE_POLYGON, signerAddress, provider); + if (walletBalance === 0n) throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error('Balance too small'); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log('Fetching OpenOcean quote (AAVE → USDT0)...'); + const { ooRouter, swapData, estimatedOut, minAmountOut } = await fetchOpenOceanQuote(inputAmount); + const feeAmount = bpsOf(estimatedOut, FEE_BPS); + const bridgeAmount = estimatedOut - feeAmount; + console.log(` OO router: ${ooRouter}`); + console.log(` Est. USDT0: ${ethers.formatUnits(estimatedOut, 6)}`); + console.log(` Post-fee: ${ethers.formatUnits(feeAmount, 6)} USDT0 (${FEE_BPS} bps)`); + console.log(` Min USDT0: ${ethers.formatUnits(minAmountOut, 6)}`); + + console.log('Fetching OFT quote (Polygon → Arbitrum)...'); + const { nativeFeeWithBuffer, amountReceivedLD } = await fetchOftQuote(provider, bridgeAmount, signerAddress); + console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} POL`); + console.log(` Est. received: ${ethers.formatUnits(amountReceivedLD, 6)} USDT0`); + + await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); + await ensureRouterErc20Balance(signer, TOKENS.USDT0_POLYGON, ROUTER_POLYGON); + + const oftSendData = buildOftSendCalldata(nativeFeeWithBuffer, signerAddress); + + const ahIface = new ethers.Interface([ + 'function transferFrom(address token, address owner, address recipient, uint256 amount)', + ]); + const exec = new ModularActionsBuilder(); + exec.call(ALLOWANCE_HOLDER, ahIface.encodeFunctionData('transferFrom', [TOKENS.AAVE_POLYGON, signerAddress, ROUTER_POLYGON, inputAmount])); + await modularApproveIfNeeded(exec, provider, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, ooRouter, inputAmount, inputAmount); + exec.call(ooRouter, swapData); + exec.call(TOKENS.USDT0_POLYGON, encodeTransfer(signerAddress, feeAmount)); + await modularApproveIfNeeded( + exec, + provider, + ROUTER_POLYGON, + TOKENS.USDT0_POLYGON, + USDT0_OFT_ADAPTER_POLYGON, + bridgeAmount, + ethers.MaxUint256, + ); + const usdt0Balance = exec.staticCall(TOKENS.USDT0_POLYGON, encodeBalanceOf(ROUTER_POLYGON)); + exec.nativeCall(USDT0_OFT_ADAPTER_POLYGON, oftSendData, nativeFeeWithBuffer) + .spliceWord(BigInt(OFT_AMOUNT_LD_OFFSET), usdt0Balance.returnWord()); + + const callData = routerIface.encodeFunctionData('performActions', [ZERO_BYTES32, exec.toActions()]); + + await ensureAllowanceForAllowanceHolder(signer, TOKENS.AAVE_POLYGON, inputAmount); + const receipt = await execViaAH(signer, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, inputAmount, ROUTER_POLYGON, callData, nativeFeeWithBuffer); + + logTxnSummary( + 'Polygon AAVE → Arbitrum USDT0 (OFT) — performActions postFee', + CHAIN_IDS.POLYGON, + receipt, + ); + + console.log('\nUSDT0 arrives on Arbitrum once LZ delivers the message.'); +} + +main().catch((err) => { console.error(err); process.exit(1); }); diff --git a/scripts/e2e/oft/performModularExecution.preFee.ts b/scripts/e2e/oft/performModularExecution.preFee.ts new file mode 100644 index 0000000..297226e --- /dev/null +++ b/scripts/e2e/oft/performModularExecution.preFee.ts @@ -0,0 +1,142 @@ +/** + * Route: Polygon USDT0 → Arbitrum USDT0 (USDT0 OFT Adapter, LayerZero v2, no swap) + * Function: performActions (modular) + * Fee: preFee — FEE_BPS of inputAmount USDT0 transferred to signer before bridge + * + * Modular action sequence: + * [0] AH.transferFrom(USDT0, signer, router, inputAmount) + * [1] USDT0.transfer(signer, feeAmount) — pre-bridge fee + * [2] USDT0.approve(adapter, MaxUint256) + * [3] STATICCALL USDT0.balanceOf(router) + * [4] nativeCall adapter.send(...) — spliceWord patches amountLD at offset 196 from [3] + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/oft/performActions.preFee.ts + */ +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +import { Options } from '@layerzerolabs/lz-v2-utilities'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + ALLOWANCE_HOLDER, + ARBITRUM_LZ_EID, + USDT0_OFT_ADAPTER_POLYGON, +} from '../config'; +import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; +import { encodeTransfer, encodeBalanceOf, getWalletErc20Balance } from '../utils/erc20'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { ModularActionsBuilder } from '../utils/modularActionsBuilder/index'; +import { ZERO_BYTES32 } from '../utils/contractTypes'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterErc20Balance } from '../utils/reproducibility'; +import { modularApproveIfNeeded } from '../utils/routerAllowance'; + +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); +const LZ_EXTRA_OPTIONS = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toHex(); +const OFT_AMOUNT_LD_OFFSET = 196; + +const OFT_ABI = [ + 'function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)', + 'function quoteOFT(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) external view returns (tuple(uint256 minAmountLD, uint256 maxAmountLD) oftLimit, tuple(int256 feeAmountLD, string description)[] oftFeeDetails, tuple(uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt)', + 'function send(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee, address refundAddress) external payable', +]; +const OFT_IFACE = new ethers.Interface(OFT_ABI); + +async function fetchOftQuote( + provider: ethers.JsonRpcProvider, + bridgeAmountLD: bigint, + recipient: string, +): Promise<{ nativeFeeWithBuffer: bigint; amountReceivedLD: bigint }> { + const contract = new ethers.Contract(USDT0_OFT_ADAPTER_POLYGON, OFT_ABI, provider); + const to32 = ethers.zeroPadValue(recipient, 32); + const sendParam = { dstEid: ARBITRUM_LZ_EID, to: to32, amountLD: bridgeAmountLD, minAmountLD: 0n, extraOptions: LZ_EXTRA_OPTIONS, composeMsg: '0x', oftCmd: '0x' }; + const [fee, oft] = await Promise.all([contract.quoteSend(sendParam, false), contract.quoteOFT(sendParam)]); + return { + nativeFeeWithBuffer: ((fee.nativeFee as bigint) * 105n) / 100n, + amountReceivedLD: oft.oftReceipt.amountReceivedLD as bigint, + }; +} + +function buildOftSendCalldata(nativeFee: bigint, recipient: string): string { + return OFT_IFACE.encodeFunctionData('send', [ + { dstEid: ARBITRUM_LZ_EID, to: ethers.zeroPadValue(recipient, 32), amountLD: 0n, minAmountLD: 0n, extraOptions: LZ_EXTRA_OPTIONS, composeMsg: '0x', oftCmd: '0x' }, + { nativeFee, lzTokenFee: 0n }, + recipient, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error('PRIVATE_KEY env var required'); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance(TOKENS.USDT0_POLYGON, signerAddress, provider); + if (walletBalance === 0n) throw new Error(`Signer ${signerAddress} has zero USDT0 on Polygon`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error('Balance too small'); + + const feeAmount = bpsOf(inputAmount, FEE_BPS); + const bridgeAmount = inputAmount - feeAmount; + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`USDT0 balance: ${ethers.formatUnits(walletBalance, 6)}`); + console.log(`Pre-fee: ${ethers.formatUnits(feeAmount, 6)} USDT0 (${FEE_BPS} bps)`); + console.log(`Net to bridge: ${ethers.formatUnits(bridgeAmount, 6)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log('Fetching OFT quote (Polygon → Arbitrum)...'); + const { nativeFeeWithBuffer, amountReceivedLD } = await fetchOftQuote(provider, bridgeAmount, signerAddress); + console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} POL`); + console.log(` Est. received: ${ethers.formatUnits(amountReceivedLD, 6)} USDT0`); + + await ensureRouterErc20Balance(signer, TOKENS.USDT0_POLYGON, ROUTER_POLYGON); + + const oftSendData = buildOftSendCalldata(nativeFeeWithBuffer, signerAddress); + + const ahIface = new ethers.Interface([ + 'function transferFrom(address token, address owner, address recipient, uint256 amount)', + ]); + const exec = new ModularActionsBuilder(); + exec.call(ALLOWANCE_HOLDER, ahIface.encodeFunctionData('transferFrom', [TOKENS.USDT0_POLYGON, signerAddress, ROUTER_POLYGON, inputAmount])); + exec.call(TOKENS.USDT0_POLYGON, encodeTransfer(signerAddress, feeAmount)); + await modularApproveIfNeeded( + exec, + provider, + ROUTER_POLYGON, + TOKENS.USDT0_POLYGON, + USDT0_OFT_ADAPTER_POLYGON, + bridgeAmount, + ethers.MaxUint256, + ); + const usdt0Balance = exec.staticCall(TOKENS.USDT0_POLYGON, encodeBalanceOf(ROUTER_POLYGON)); + exec.nativeCall(USDT0_OFT_ADAPTER_POLYGON, oftSendData, nativeFeeWithBuffer) + .spliceWord(BigInt(OFT_AMOUNT_LD_OFFSET), usdt0Balance.returnWord()); + + const callData = routerIface.encodeFunctionData('performActions', [ZERO_BYTES32, exec.toActions()]); + + await ensureAllowanceForAllowanceHolder(signer, TOKENS.USDT0_POLYGON, inputAmount); + const receipt = await execViaAH(signer, ROUTER_POLYGON, TOKENS.USDT0_POLYGON, inputAmount, ROUTER_POLYGON, callData, nativeFeeWithBuffer); + + logTxnSummary( + 'Polygon USDT0 → Arbitrum USDT0 (OFT direct) — performActions preFee', + CHAIN_IDS.POLYGON, + receipt, + ); + + console.log('\nUSDT0 arrives on Arbitrum once LZ delivers the message.'); +} + +main().catch((err) => { console.error(err); process.exit(1); }); diff --git a/scripts/e2e/oft/swapAndBridge.postFee.balanceOf.ts b/scripts/e2e/oft/swapAndBridge.postFee.balanceOf.ts new file mode 100644 index 0000000..96f4944 --- /dev/null +++ b/scripts/e2e/oft/swapAndBridge.postFee.balanceOf.ts @@ -0,0 +1,265 @@ +/** + * Route: Polygon AAVE → USDT0 (OpenOcean) → Arbitrum USDT0 (USDT0 OFT Adapter, LayerZero v2) + * Flags: post-fee (fee taken from USDT0 output after swap), output measured as USDT0 balanceOf delta + * bridge amount spliced into send() calldata at byte offset 196 (sendParam.amountLD) + * + * Post-fee (bit0=1): feeAmount = FEE_BPS of estimatedOut USDT0, deducted from swap output. + * BalanceOf (bit1=1): final USDT0 amount is measured as router USDT0 balance change (not returndata). + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/oft/swapAndBridge.postFee.balanceOf.ts + */ +import axios from "axios"; +import { ethers } from "ethers"; +import * as dotenv from "dotenv"; +import { Options } from "@layerzerolabs/lz-v2-utilities"; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, + ARBITRUM_LZ_EID, + USDT0_OFT_ADAPTER_POLYGON, +} from "../config"; +import { + execViaAH, + ensureAllowanceForAllowanceHolder, +} from "../utils/allowanceHolder"; +import { getWalletErc20Balance } from "../utils/erc20"; +import { ROUTER_ABI } from "../utils/routerAbi"; +import { ZERO_BYTES32, bridgeAmountPositionFlag, swapAndBridgeArgs } from "../utils/contractTypes"; +import { logTxnSummary } from "../utils/txnLogSummary"; +import { + ensureRouterErc20Balance, +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; + +// post-fee (0x01) | balance-of (0x02) | bridge amount at byte offset 196 (sendParam.amountLD) +const FLAGS = 0x03n | bridgeAmountPositionFlag(196); +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); +const LZ_EXTRA_OPTIONS = Options.newOptions() + .addExecutorLzReceiveOption(65000, 0) + .toHex(); + +const OFT_ABI = [ + "function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)", + "function quoteOFT(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) external view returns (tuple(uint256 minAmountLD, uint256 maxAmountLD) oftLimit, tuple(int256 feeAmountLD, string description)[] oftFeeDetails, tuple(uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt)", + "function send(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee, address refundAddress) external payable", +]; + +const OFT_IFACE = new ethers.Interface(OFT_ABI); + +interface OoQuoteResponse { + data: { to: string; data: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.AAVE_POLYGON, + outTokenAddress: TOKENS.USDT0_POLYGON, + amount: ethers.formatUnits(inputAmount, 18), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_POLYGON, + account: ROUTER_POLYGON, + gasPrice: "1", + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.POLYGON}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + }; +} + +async function fetchOftQuote( + provider: ethers.JsonRpcProvider, + bridgeAmountLD: bigint, + recipient: string +): Promise<{ nativeFeeWithBuffer: bigint; amountReceivedLD: bigint }> { + const contract = new ethers.Contract( + USDT0_OFT_ADAPTER_POLYGON, + OFT_ABI, + provider + ); + const to32 = ethers.zeroPadValue(recipient, 32); + const sendParam = { + dstEid: ARBITRUM_LZ_EID, + to: to32, + amountLD: bridgeAmountLD, + minAmountLD: 0n, + extraOptions: LZ_EXTRA_OPTIONS, + composeMsg: "0x", + oftCmd: "0x", + }; + const [fee, oft] = await Promise.all([ + contract.quoteSend(sendParam, false), + contract.quoteOFT(sendParam), + ]); + const nativeFee = fee.nativeFee as bigint; + return { + nativeFeeWithBuffer: (nativeFee * 105n) / 100n, + amountReceivedLD: oft.oftReceipt.amountReceivedLD as bigint, + }; +} + +function buildOftSendCalldata(nativeFee: bigint, recipient: string): string { + return OFT_IFACE.encodeFunctionData("send", [ + { + dstEid: ARBITRUM_LZ_EID, + to: ethers.zeroPadValue(recipient, 32), + amountLD: 0n, // placeholder — spliced at runtime at offset 196 + minAmountLD: 0n, + extraOptions: LZ_EXTRA_OPTIONS, + composeMsg: "0x", + oftCmd: "0x", + }, + { nativeFee, lzTokenFee: 0n }, + recipient, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error("PRIVATE_KEY env var required"); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.AAVE_POLYGON, + signerAddress, + provider + ); + if (walletBalance === 0n) + throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error("Balance too small"); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log( + `Flags: 0x${FLAGS.toString( + 16 + )} (post-fee, balanceOf, bridge-amount-pos=196)` + ); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log("Fetching OpenOcean quote (AAVE → USDT0)..."); + const { ooRouter, swapData, estimatedOut, minAmountOut } = + await fetchOpenOceanQuote(inputAmount); + + // post-fee: deduct from estimated USDT0 output after swap + const feeAmount = bpsOf(estimatedOut, FEE_BPS); + const bridgeAmount = estimatedOut - feeAmount; + console.log(` OO router: ${ooRouter}`); + console.log(` Est. USDT0: ${ethers.formatUnits(estimatedOut, 6)}`); + console.log( + ` Post-fee: ${ethers.formatUnits(feeAmount, 6)} USDT0 (${FEE_BPS} bps)` + ); + console.log(` Bridge est: ${ethers.formatUnits(bridgeAmount, 6)}`); + console.log(` Min USDT0: ${ethers.formatUnits(minAmountOut, 6)}`); + + console.log("Fetching OFT quote (Polygon → Arbitrum)..."); + const { nativeFeeWithBuffer, amountReceivedLD } = await fetchOftQuote( + provider, + bridgeAmount, + signerAddress + ); + console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} POL`); + console.log( + ` Est. received: ${ethers.formatUnits(amountReceivedLD, 6)} USDT0` + ); + + await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); + await ensureRouterErc20Balance(signer, TOKENS.USDT0_POLYGON, ROUTER_POLYGON); + + const swapApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + ooRouter, + inputAmount, + ); + const bridgeApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + TOKENS.USDT0_POLYGON, + USDT0_OFT_ADAPTER_POLYGON, + bridgeAmount, + ); + + const oftSendData = buildOftSendCalldata(nativeFeeWithBuffer, signerAddress); + + const callData = routerIface.encodeFunctionData("swapAndBridge", swapAndBridgeArgs( + ZERO_BYTES32, + FLAGS, + { + user: signerAddress, + inputToken: TOKENS.AAVE_POLYGON, + inputAmount: inputAmount, + }, + { receiver: signerAddress, amount: feeAmount }, + { + target: ooRouter, + approvalSpender: swapApprovalSpender, + outputToken: TOKENS.USDT0_POLYGON, + value: 0n, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + swapData, + { + target: USDT0_OFT_ADAPTER_POLYGON, + approvalSpender: bridgeApprovalSpender, + value: nativeFeeWithBuffer, + }, + oftSendData, + )); + + await ensureAllowanceForAllowanceHolder( + signer, + TOKENS.AAVE_POLYGON, + inputAmount + ); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + inputAmount, + ROUTER_POLYGON, + callData, + nativeFeeWithBuffer + ); + + logTxnSummary( + `Polygon AAVE → Arbitrum USDT0 (OFT) — swapAndBridge postFee/balanceOf`, + CHAIN_IDS.POLYGON, + receipt + ); + + console.log("\nUSDT0 arrives on Arbitrum once LZ delivers the message."); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/oft/swapAndBridge.postFee.returndata.ts b/scripts/e2e/oft/swapAndBridge.postFee.returndata.ts new file mode 100644 index 0000000..4980ec7 --- /dev/null +++ b/scripts/e2e/oft/swapAndBridge.postFee.returndata.ts @@ -0,0 +1,265 @@ +/** + * Route: Polygon AAVE → USDT0 (OpenOcean) → Arbitrum USDT0 (USDT0 OFT Adapter, LayerZero v2) + * Flags: post-fee (fee taken from USDT0 output after swap), output read from swap returndata word 0 + * bridge amount spliced into send() calldata at byte offset 196 (sendParam.amountLD) + * + * Post-fee (bit0=1): feeAmount = FEE_BPS of estimatedOut USDT0, deducted from swap output. + * Returndata (bit1=0): final USDT0 amount is read from word 0 of the swap call returndata. + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/oft/swapAndBridge.postFee.returndata.ts + */ +import axios from "axios"; +import { ethers } from "ethers"; +import * as dotenv from "dotenv"; +import { Options } from "@layerzerolabs/lz-v2-utilities"; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, + ARBITRUM_LZ_EID, + USDT0_OFT_ADAPTER_POLYGON, +} from "../config"; +import { + execViaAH, + ensureAllowanceForAllowanceHolder, +} from "../utils/allowanceHolder"; +import { getWalletErc20Balance } from "../utils/erc20"; +import { ROUTER_ABI } from "../utils/routerAbi"; +import { ZERO_BYTES32, bridgeAmountPositionFlag, swapAndBridgeArgs } from "../utils/contractTypes"; +import { logTxnSummary } from "../utils/txnLogSummary"; +import { + ensureRouterErc20Balance, +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; + +// post-fee (0x01) | bridge amount at byte offset 196 (sendParam.amountLD) +const FLAGS = 0x01n | bridgeAmountPositionFlag(196); +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); +const LZ_EXTRA_OPTIONS = Options.newOptions() + .addExecutorLzReceiveOption(65000, 0) + .toHex(); + +const OFT_ABI = [ + "function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)", + "function quoteOFT(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) external view returns (tuple(uint256 minAmountLD, uint256 maxAmountLD) oftLimit, tuple(int256 feeAmountLD, string description)[] oftFeeDetails, tuple(uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt)", + "function send(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee, address refundAddress) external payable", +]; + +const OFT_IFACE = new ethers.Interface(OFT_ABI); + +interface OoQuoteResponse { + data: { to: string; data: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.AAVE_POLYGON, + outTokenAddress: TOKENS.USDT0_POLYGON, + amount: ethers.formatUnits(inputAmount, 18), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_POLYGON, + account: ROUTER_POLYGON, + gasPrice: "1", + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.POLYGON}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + }; +} + +async function fetchOftQuote( + provider: ethers.JsonRpcProvider, + bridgeAmountLD: bigint, + recipient: string +): Promise<{ nativeFeeWithBuffer: bigint; amountReceivedLD: bigint }> { + const contract = new ethers.Contract( + USDT0_OFT_ADAPTER_POLYGON, + OFT_ABI, + provider + ); + const to32 = ethers.zeroPadValue(recipient, 32); + const sendParam = { + dstEid: ARBITRUM_LZ_EID, + to: to32, + amountLD: bridgeAmountLD, + minAmountLD: 0n, + extraOptions: LZ_EXTRA_OPTIONS, + composeMsg: "0x", + oftCmd: "0x", + }; + const [fee, oft] = await Promise.all([ + contract.quoteSend(sendParam, false), + contract.quoteOFT(sendParam), + ]); + const nativeFee = fee.nativeFee as bigint; + return { + nativeFeeWithBuffer: (nativeFee * 105n) / 100n, + amountReceivedLD: oft.oftReceipt.amountReceivedLD as bigint, + }; +} + +function buildOftSendCalldata(nativeFee: bigint, recipient: string): string { + return OFT_IFACE.encodeFunctionData("send", [ + { + dstEid: ARBITRUM_LZ_EID, + to: ethers.zeroPadValue(recipient, 32), + amountLD: 0n, // placeholder — spliced at runtime at offset 196 + minAmountLD: 0n, + extraOptions: LZ_EXTRA_OPTIONS, + composeMsg: "0x", + oftCmd: "0x", + }, + { nativeFee, lzTokenFee: 0n }, + recipient, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error("PRIVATE_KEY env var required"); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.AAVE_POLYGON, + signerAddress, + provider + ); + if (walletBalance === 0n) + throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error("Balance too small"); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log( + `Flags: 0x${FLAGS.toString( + 16 + )} (post-fee, returndata, bridge-amount-pos=196)` + ); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log("Fetching OpenOcean quote (AAVE → USDT0)..."); + const { ooRouter, swapData, estimatedOut, minAmountOut } = + await fetchOpenOceanQuote(inputAmount); + + // post-fee: deduct from estimated USDT0 output after swap + const feeAmount = bpsOf(estimatedOut, FEE_BPS); + const bridgeAmount = estimatedOut - feeAmount; + console.log(` OO router: ${ooRouter}`); + console.log(` Est. USDT0: ${ethers.formatUnits(estimatedOut, 6)}`); + console.log( + ` Post-fee: ${ethers.formatUnits(feeAmount, 6)} USDT0 (${FEE_BPS} bps)` + ); + console.log(` Bridge est: ${ethers.formatUnits(bridgeAmount, 6)}`); + console.log(` Min USDT0: ${ethers.formatUnits(minAmountOut, 6)}`); + + console.log("Fetching OFT quote (Polygon → Arbitrum)..."); + const { nativeFeeWithBuffer, amountReceivedLD } = await fetchOftQuote( + provider, + bridgeAmount, + signerAddress + ); + console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} POL`); + console.log( + ` Est. received: ${ethers.formatUnits(amountReceivedLD, 6)} USDT0` + ); + + await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); + await ensureRouterErc20Balance(signer, TOKENS.USDT0_POLYGON, ROUTER_POLYGON); + + const swapApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + ooRouter, + inputAmount, + ); + const bridgeApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + TOKENS.USDT0_POLYGON, + USDT0_OFT_ADAPTER_POLYGON, + bridgeAmount, + ); + + const oftSendData = buildOftSendCalldata(nativeFeeWithBuffer, signerAddress); + + const callData = routerIface.encodeFunctionData("swapAndBridge", swapAndBridgeArgs( + ZERO_BYTES32, + FLAGS, + { + user: signerAddress, + inputToken: TOKENS.AAVE_POLYGON, + inputAmount: inputAmount, + }, + { receiver: signerAddress, amount: feeAmount }, + { + target: ooRouter, + approvalSpender: swapApprovalSpender, + outputToken: TOKENS.USDT0_POLYGON, + value: 0n, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + swapData, + { + target: USDT0_OFT_ADAPTER_POLYGON, + approvalSpender: bridgeApprovalSpender, + value: nativeFeeWithBuffer, + }, + oftSendData, + )); + + await ensureAllowanceForAllowanceHolder( + signer, + TOKENS.AAVE_POLYGON, + inputAmount + ); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + inputAmount, + ROUTER_POLYGON, + callData, + nativeFeeWithBuffer + ); + + logTxnSummary( + `Polygon AAVE → Arbitrum USDT0 (OFT) — swapAndBridge postFee/returndata`, + CHAIN_IDS.POLYGON, + receipt + ); + + console.log("\nUSDT0 arrives on Arbitrum once LZ delivers the message."); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/oft/swapAndBridge.preFee.balanceOf.ts b/scripts/e2e/oft/swapAndBridge.preFee.balanceOf.ts new file mode 100644 index 0000000..8415223 --- /dev/null +++ b/scripts/e2e/oft/swapAndBridge.preFee.balanceOf.ts @@ -0,0 +1,264 @@ +/** + * Route: Polygon AAVE → USDT0 (OpenOcean) → Arbitrum USDT0 (USDT0 OFT Adapter, LayerZero v2) + * Flags: pre-fee (fee taken from AAVE input before swap), output measured as USDT0 balanceOf delta + * bridge amount spliced into send() calldata at byte offset 196 (sendParam.amountLD) + * + * Pre-fee (bit0=0): feeAmount = FEE_BPS of inputAmount, deducted from AAVE before the swap. + * BalanceOf (bit1=1): final USDT0 amount is measured as router USDT0 balance change (not returndata). + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/oft/swapAndBridge.preFee.balanceOf.ts + */ +import axios from "axios"; +import { ethers } from "ethers"; +import * as dotenv from "dotenv"; +import { Options } from "@layerzerolabs/lz-v2-utilities"; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, + ARBITRUM_LZ_EID, + USDT0_OFT_ADAPTER_POLYGON, +} from "../config"; +import { + execViaAH, + ensureAllowanceForAllowanceHolder, +} from "../utils/allowanceHolder"; +import { getWalletErc20Balance } from "../utils/erc20"; +import { ROUTER_ABI } from "../utils/routerAbi"; +import { ZERO_BYTES32, bridgeAmountPositionFlag, swapAndBridgeArgs } from "../utils/contractTypes"; +import { logTxnSummary } from "../utils/txnLogSummary"; +import { + ensureRouterErc20Balance, +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; + +// pre-fee (0x00) | balance-of (0x02) | bridge amount at byte offset 196 (sendParam.amountLD) +const FLAGS = 0x02n | bridgeAmountPositionFlag(196); +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); +const LZ_EXTRA_OPTIONS = Options.newOptions() + .addExecutorLzReceiveOption(65000, 0) + .toHex(); + +const OFT_ABI = [ + "function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)", + "function quoteOFT(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) external view returns (tuple(uint256 minAmountLD, uint256 maxAmountLD) oftLimit, tuple(int256 feeAmountLD, string description)[] oftFeeDetails, tuple(uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt)", + "function send(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee, address refundAddress) external payable", +]; + +const OFT_IFACE = new ethers.Interface(OFT_ABI); + +interface OoQuoteResponse { + data: { to: string; data: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.AAVE_POLYGON, + outTokenAddress: TOKENS.USDT0_POLYGON, + amount: ethers.formatUnits(inputAmount, 18), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_POLYGON, + account: ROUTER_POLYGON, + gasPrice: "1", + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.POLYGON}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + }; +} + +async function fetchOftQuote( + provider: ethers.JsonRpcProvider, + bridgeAmountLD: bigint, + recipient: string +): Promise<{ nativeFeeWithBuffer: bigint; amountReceivedLD: bigint }> { + const contract = new ethers.Contract( + USDT0_OFT_ADAPTER_POLYGON, + OFT_ABI, + provider + ); + const to32 = ethers.zeroPadValue(recipient, 32); + const sendParam = { + dstEid: ARBITRUM_LZ_EID, + to: to32, + amountLD: bridgeAmountLD, + minAmountLD: 0n, + extraOptions: LZ_EXTRA_OPTIONS, + composeMsg: "0x", + oftCmd: "0x", + }; + const [fee, oft] = await Promise.all([ + contract.quoteSend(sendParam, false), + contract.quoteOFT(sendParam), + ]); + const nativeFee = fee.nativeFee as bigint; + return { + nativeFeeWithBuffer: (nativeFee * 105n) / 100n, + amountReceivedLD: oft.oftReceipt.amountReceivedLD as bigint, + }; +} + +function buildOftSendCalldata(nativeFee: bigint, recipient: string): string { + return OFT_IFACE.encodeFunctionData("send", [ + { + dstEid: ARBITRUM_LZ_EID, + to: ethers.zeroPadValue(recipient, 32), + amountLD: 0n, // placeholder — spliced at runtime at offset 196 + minAmountLD: 0n, + extraOptions: LZ_EXTRA_OPTIONS, + composeMsg: "0x", + oftCmd: "0x", + }, + { nativeFee, lzTokenFee: 0n }, + recipient, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error("PRIVATE_KEY env var required"); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.AAVE_POLYGON, + signerAddress, + provider + ); + if (walletBalance === 0n) + throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error("Balance too small"); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log( + `Flags: 0x${FLAGS.toString( + 16 + )} (pre-fee, balanceOf, bridge-amount-pos=196)` + ); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log("Fetching OpenOcean quote (AAVE → USDT0)..."); + const { ooRouter, swapData, estimatedOut, minAmountOut } = + await fetchOpenOceanQuote(inputAmount); + + // pre-fee: deduct from input AAVE before the swap + const feeAmount = bpsOf(inputAmount, FEE_BPS); + const bridgeAmount = estimatedOut; + console.log(` OO router: ${ooRouter}`); + console.log(` Est. USDT0: ${ethers.formatUnits(estimatedOut, 6)}`); + console.log( + ` Pre-fee: ${ethers.formatUnits(feeAmount, 18)} AAVE (${FEE_BPS} bps)` + ); + console.log(` Min USDT0: ${ethers.formatUnits(minAmountOut, 6)}`); + + console.log("Fetching OFT quote (Polygon → Arbitrum)..."); + const { nativeFeeWithBuffer, amountReceivedLD } = await fetchOftQuote( + provider, + bridgeAmount, + signerAddress + ); + console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} POL`); + console.log( + ` Est. received: ${ethers.formatUnits(amountReceivedLD, 6)} USDT0` + ); + + await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); + await ensureRouterErc20Balance(signer, TOKENS.USDT0_POLYGON, ROUTER_POLYGON); + + const swapApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + ooRouter, + inputAmount - feeAmount, + ); + const bridgeApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + TOKENS.USDT0_POLYGON, + USDT0_OFT_ADAPTER_POLYGON, + minAmountOut, + ); + + const oftSendData = buildOftSendCalldata(nativeFeeWithBuffer, signerAddress); + + const callData = routerIface.encodeFunctionData("swapAndBridge", swapAndBridgeArgs( + ZERO_BYTES32, + FLAGS, + { + user: signerAddress, + inputToken: TOKENS.AAVE_POLYGON, + inputAmount: inputAmount, + }, + { receiver: signerAddress, amount: feeAmount }, + { + target: ooRouter, + approvalSpender: swapApprovalSpender, + outputToken: TOKENS.USDT0_POLYGON, + value: 0n, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + swapData, + { + target: USDT0_OFT_ADAPTER_POLYGON, + approvalSpender: bridgeApprovalSpender, + value: nativeFeeWithBuffer, + }, + oftSendData, + )); + + await ensureAllowanceForAllowanceHolder( + signer, + TOKENS.AAVE_POLYGON, + inputAmount + ); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + inputAmount, + ROUTER_POLYGON, + callData, + nativeFeeWithBuffer + ); + + logTxnSummary( + `Polygon AAVE → Arbitrum USDT0 (OFT) — swapAndBridge preFee/balanceOf`, + CHAIN_IDS.POLYGON, + receipt + ); + + console.log("\nUSDT0 arrives on Arbitrum once LZ delivers the message."); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/oft/swapAndBridge.preFee.returndata.ts b/scripts/e2e/oft/swapAndBridge.preFee.returndata.ts new file mode 100644 index 0000000..5e5dcb7 --- /dev/null +++ b/scripts/e2e/oft/swapAndBridge.preFee.returndata.ts @@ -0,0 +1,264 @@ +/** + * Route: Polygon AAVE → USDT0 (OpenOcean) → Arbitrum USDT0 (USDT0 OFT Adapter, LayerZero v2) + * Flags: pre-fee (fee taken from AAVE input before swap), output read from swap returndata word 0 + * bridge amount spliced into send() calldata at byte offset 196 (sendParam.amountLD) + * + * Pre-fee (bit0=0): feeAmount = FEE_BPS of inputAmount, deducted from AAVE before the swap. + * Returndata (bit1=0): final USDT0 amount is read from word 0 of the swap call returndata. + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/oft/swapAndBridge.preFee.returndata.ts + */ +import axios from "axios"; +import { ethers } from "ethers"; +import * as dotenv from "dotenv"; +import { Options } from "@layerzerolabs/lz-v2-utilities"; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, + ARBITRUM_LZ_EID, + USDT0_OFT_ADAPTER_POLYGON, +} from "../config"; +import { + execViaAH, + ensureAllowanceForAllowanceHolder, +} from "../utils/allowanceHolder"; +import { getWalletErc20Balance } from "../utils/erc20"; +import { ROUTER_ABI } from "../utils/routerAbi"; +import { ZERO_BYTES32, bridgeAmountPositionFlag, swapAndBridgeArgs } from "../utils/contractTypes"; +import { logTxnSummary } from "../utils/txnLogSummary"; +import { + ensureRouterErc20Balance, +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; + +// pre-fee (0x00) | bridge amount at byte offset 196 (sendParam.amountLD) +const FLAGS = bridgeAmountPositionFlag(196); +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); +const LZ_EXTRA_OPTIONS = Options.newOptions() + .addExecutorLzReceiveOption(65000, 0) + .toHex(); + +const OFT_ABI = [ + "function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)", + "function quoteOFT(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) external view returns (tuple(uint256 minAmountLD, uint256 maxAmountLD) oftLimit, tuple(int256 feeAmountLD, string description)[] oftFeeDetails, tuple(uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt)", + "function send(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee, address refundAddress) external payable", +]; + +const OFT_IFACE = new ethers.Interface(OFT_ABI); + +interface OoQuoteResponse { + data: { to: string; data: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.AAVE_POLYGON, + outTokenAddress: TOKENS.USDT0_POLYGON, + amount: ethers.formatUnits(inputAmount, 18), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_POLYGON, + account: ROUTER_POLYGON, + gasPrice: "1", + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.POLYGON}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + }; +} + +async function fetchOftQuote( + provider: ethers.JsonRpcProvider, + bridgeAmountLD: bigint, + recipient: string +): Promise<{ nativeFeeWithBuffer: bigint; amountReceivedLD: bigint }> { + const contract = new ethers.Contract( + USDT0_OFT_ADAPTER_POLYGON, + OFT_ABI, + provider + ); + const to32 = ethers.zeroPadValue(recipient, 32); + const sendParam = { + dstEid: ARBITRUM_LZ_EID, + to: to32, + amountLD: bridgeAmountLD, + minAmountLD: 0n, + extraOptions: LZ_EXTRA_OPTIONS, + composeMsg: "0x", + oftCmd: "0x", + }; + const [fee, oft] = await Promise.all([ + contract.quoteSend(sendParam, false), + contract.quoteOFT(sendParam), + ]); + const nativeFee = fee.nativeFee as bigint; + return { + nativeFeeWithBuffer: (nativeFee * 105n) / 100n, + amountReceivedLD: oft.oftReceipt.amountReceivedLD as bigint, + }; +} + +function buildOftSendCalldata(nativeFee: bigint, recipient: string): string { + return OFT_IFACE.encodeFunctionData("send", [ + { + dstEid: ARBITRUM_LZ_EID, + to: ethers.zeroPadValue(recipient, 32), + amountLD: 0n, // placeholder — spliced at runtime at offset 196 + minAmountLD: 0n, + extraOptions: LZ_EXTRA_OPTIONS, + composeMsg: "0x", + oftCmd: "0x", + }, + { nativeFee, lzTokenFee: 0n }, + recipient, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error("PRIVATE_KEY env var required"); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.AAVE_POLYGON, + signerAddress, + provider + ); + if (walletBalance === 0n) + throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error("Balance too small"); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log( + `Flags: 0x${FLAGS.toString( + 16 + )} (pre-fee, returndata, bridge-amount-pos=196)` + ); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log("Fetching OpenOcean quote (AAVE → USDT0)..."); + const { ooRouter, swapData, estimatedOut, minAmountOut } = + await fetchOpenOceanQuote(inputAmount); + + // pre-fee: deduct from input AAVE before the swap + const feeAmount = bpsOf(inputAmount, FEE_BPS); + const bridgeAmount = estimatedOut; // estimated bridge amount (no post-fee subtraction here) + console.log(` OO router: ${ooRouter}`); + console.log(` Est. USDT0: ${ethers.formatUnits(estimatedOut, 6)}`); + console.log( + ` Pre-fee: ${ethers.formatUnits(feeAmount, 18)} AAVE (${FEE_BPS} bps)` + ); + console.log(` Min USDT0: ${ethers.formatUnits(minAmountOut, 6)}`); + + console.log("Fetching OFT quote (Polygon → Arbitrum)..."); + const { nativeFeeWithBuffer, amountReceivedLD } = await fetchOftQuote( + provider, + bridgeAmount, + signerAddress + ); + console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} POL`); + console.log( + ` Est. received: ${ethers.formatUnits(amountReceivedLD, 6)} USDT0` + ); + + await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); + await ensureRouterErc20Balance(signer, TOKENS.USDT0_POLYGON, ROUTER_POLYGON); + + const swapApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + ooRouter, + inputAmount - feeAmount, + ); + const bridgeApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + TOKENS.USDT0_POLYGON, + USDT0_OFT_ADAPTER_POLYGON, + minAmountOut, + ); + + const oftSendData = buildOftSendCalldata(nativeFeeWithBuffer, signerAddress); + + const callData = routerIface.encodeFunctionData("swapAndBridge", swapAndBridgeArgs( + ZERO_BYTES32, + FLAGS, + { + user: signerAddress, + inputToken: TOKENS.AAVE_POLYGON, + inputAmount: inputAmount, + }, + { receiver: signerAddress, amount: feeAmount }, + { + target: ooRouter, + approvalSpender: swapApprovalSpender, + outputToken: TOKENS.USDT0_POLYGON, + value: 0n, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + swapData, + { + target: USDT0_OFT_ADAPTER_POLYGON, + approvalSpender: bridgeApprovalSpender, + value: nativeFeeWithBuffer, + }, + oftSendData, + )); + + await ensureAllowanceForAllowanceHolder( + signer, + TOKENS.AAVE_POLYGON, + inputAmount + ); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + inputAmount, + ROUTER_POLYGON, + callData, + nativeFeeWithBuffer + ); + + logTxnSummary( + `Polygon AAVE → Arbitrum USDT0 (OFT) — swapAndBridge preFee/returndata`, + CHAIN_IDS.POLYGON, + receipt + ); + + console.log("\nUSDT0 arrives on Arbitrum once LZ delivers the message."); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/relay/aave.bridge.preFee.ts b/scripts/e2e/relay/aave.bridge.preFee.ts new file mode 100644 index 0000000..73f4955 --- /dev/null +++ b/scripts/e2e/relay/aave.bridge.preFee.ts @@ -0,0 +1,105 @@ +/** + * Route: Polygon AAVE → Base AAVE via Relay.link (no swap) + * Function: bridge (simple bridge entrypoint) + * Fee: preFee — FEE_BPS of inputAmount AAVE deducted before bridge + * + * Bridge amount is pre-encoded in Relay deposit calldata. + * Uses router.bridge() rather than performExecution / performActions. + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/relay/aave.bridge.preFee.ts + */ +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, +} from '../config'; +import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; +import { getWalletErc20Balance } from '../utils/erc20'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { ZERO_BYTES32, type BridgeData, type FeeData, type InputData } from '../utils/contractTypes'; +import { fetchRelayQuoteV2, parseRelayQuote } from '../utils/relayLinkQuote'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterErc20Balance } from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; + +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); + +async function main(): Promise { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) { + throw new Error('PRIVATE_KEY env var required'); + } + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const inputToken = TOKENS.AAVE_POLYGON; + const { balance: walletBalance } = await getWalletErc20Balance(inputToken, signerAddress, provider); + if (walletBalance === 0n) { + throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + } + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error('Balance too small'); + + const feeAmount = bpsOf(inputAmount, FEE_BPS); + const bridgeAmount = inputAmount - feeAmount; + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + console.log(`Pre-bridge fee: ${ethers.formatUnits(feeAmount, 18)} AAVE (${FEE_BPS} bps)`); + console.log(`Net to bridge: ${ethers.formatUnits(bridgeAmount, 18)}`); + + console.log('Fetching Relay.link quote...'); + const quote = await fetchRelayQuoteV2({ + routerAddress: ROUTER_POLYGON, + recipient: signerAddress, + originChainId: CHAIN_IDS.POLYGON, + destinationChainId: CHAIN_IDS.BASE, + originCurrency: TOKENS.AAVE_POLYGON, + destinationCurrency: TOKENS.AAVE_BASE, + amount: bridgeAmount, + }); + const { relaySpender, depositTarget, depositData } = parseRelayQuote(quote); + console.log(`Relay spender: ${relaySpender}`); + console.log(`Deposit target: ${depositTarget}`); + + await ensureRouterErc20Balance(signer, inputToken, ROUTER_POLYGON); + + const bridgeApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + inputToken, + relaySpender, + bridgeAmount, + ); + + const input: InputData = { user: signerAddress, inputToken, inputAmount }; + const fee: FeeData = { receiver: signerAddress, amount: feeAmount }; + const bridgeData: BridgeData = { target: depositTarget, approvalSpender: bridgeApprovalSpender, value: 0n }; + + const routerIface = new ethers.Interface(ROUTER_ABI); + const execCalldata = routerIface.encodeFunctionData('bridge', [ZERO_BYTES32, input, fee, bridgeData, depositData]); + + await ensureAllowanceForAllowanceHolder(signer, inputToken, inputAmount); + + console.log('Sending AllowanceHolder.exec → router.bridge...'); + const receipt = await execViaAH(signer, ROUTER_POLYGON, inputToken, inputAmount, ROUTER_POLYGON, execCalldata); + + logTxnSummary('Polygon AAVE → Base AAVE — Relay — bridge preFee', CHAIN_IDS.POLYGON, receipt); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/relay/aave.performExecution.preFee.ts b/scripts/e2e/relay/aave.performExecution.preFee.ts new file mode 100644 index 0000000..70098a0 --- /dev/null +++ b/scripts/e2e/relay/aave.performExecution.preFee.ts @@ -0,0 +1,102 @@ +/** + * Route: Polygon AAVE → Base AAVE via Relay.link (no swap) + * Function: bridge + * Fee: preFee — FEE_BPS of inputAmount AAVE deducted before bridge + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/relay/aave.performExecution.preFee.ts + */ +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, +} from '../config'; +import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; +import { getWalletErc20Balance } from '../utils/erc20'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { ZERO_BYTES32, bridgeArgs, type BridgeData, type FeeData, type InputData } from '../utils/contractTypes'; +import { fetchRelayQuoteV2, parseRelayQuote } from '../utils/relayLinkQuote'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterErc20Balance } from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; + +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); + +async function main(): Promise { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) { + throw new Error('PRIVATE_KEY env var required'); + } + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const inputToken = TOKENS.AAVE_POLYGON; + const { balance: walletBalance } = await getWalletErc20Balance(inputToken, signerAddress, provider); + if (walletBalance === 0n) { + throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + } + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error('Balance too small'); + + const feeAmount = bpsOf(inputAmount, FEE_BPS); + const bridgeAmount = inputAmount - feeAmount; + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + console.log(`Pre-bridge fee: ${ethers.formatUnits(feeAmount, 18)} AAVE (${FEE_BPS} bps)`); + console.log(`Net to bridge: ${ethers.formatUnits(bridgeAmount, 18)}`); + + console.log('Fetching Relay.link quote...'); + const quote = await fetchRelayQuoteV2({ + routerAddress: ROUTER_POLYGON, + recipient: signerAddress, + originChainId: CHAIN_IDS.POLYGON, + destinationChainId: CHAIN_IDS.BASE, + originCurrency: TOKENS.AAVE_POLYGON, + destinationCurrency: TOKENS.AAVE_BASE, + amount: bridgeAmount, + }); + const { relaySpender, depositTarget, depositData } = parseRelayQuote(quote); + console.log(`Relay spender: ${relaySpender}`); + console.log(`Deposit target: ${depositTarget}`); + + await ensureRouterErc20Balance(signer, inputToken, ROUTER_POLYGON); + + const input: InputData = { user: signerAddress, inputToken, inputAmount }; + const fee: FeeData = { receiver: signerAddress, amount: feeAmount }; + const bridgeApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + inputToken, + relaySpender, + bridgeAmount, + ); + + const bridgeData: BridgeData = { target: depositTarget, approvalSpender: bridgeApprovalSpender, value: 0n }; + + const routerIface = new ethers.Interface(ROUTER_ABI); + const execCalldata = routerIface.encodeFunctionData('bridge', bridgeArgs(ZERO_BYTES32, input, fee, bridgeData, depositData)); + + await ensureAllowanceForAllowanceHolder(signer, inputToken, inputAmount); + + console.log('Sending AllowanceHolder.exec → router.bridge...'); + const receipt = await execViaAH(signer, ROUTER_POLYGON, inputToken, inputAmount, ROUTER_POLYGON, execCalldata); + + logTxnSummary('Polygon AAVE → Base AAVE — Relay — bridge preFee', CHAIN_IDS.POLYGON, receipt); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/relay/aave.performModularExecution.preFee.ts b/scripts/e2e/relay/aave.performModularExecution.preFee.ts new file mode 100644 index 0000000..dd9207e --- /dev/null +++ b/scripts/e2e/relay/aave.performModularExecution.preFee.ts @@ -0,0 +1,107 @@ +/** + * Route: Polygon AAVE → Base AAVE via Relay.link (no swap) + * Function: performActions (modular) + * Fee: preFee — FEE_BPS of inputAmount AAVE deducted before bridge + * + * Modular action sequence: + * [0] AH.transferFrom(AAVE, signer, router, inputAmount) + * [1] AAVE.transfer(signer, feeAmount) — preFee out + * [2] AAVE.approve(relaySpender, bridgeAmount) + * [3] call(depositTarget, depositData) — Relay bridge + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/relay/aave.performActions.preFee.ts + */ +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + ALLOWANCE_HOLDER, +} from '../config'; +import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; +import { encodeTransfer, getWalletErc20Balance } from '../utils/erc20'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { ModularActionsBuilder } from '../utils/modularActionsBuilder/index'; +import { ZERO_BYTES32 } from '../utils/contractTypes'; +import { fetchRelayQuoteV2, parseRelayQuote } from '../utils/relayLinkQuote'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterErc20Balance } from '../utils/reproducibility'; +import { modularApproveIfNeeded } from '../utils/routerAllowance'; + +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); + +async function main(): Promise { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) { + throw new Error('PRIVATE_KEY env var required'); + } + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const inputToken = TOKENS.AAVE_POLYGON; + const { balance: walletBalance } = await getWalletErc20Balance(inputToken, signerAddress, provider); + if (walletBalance === 0n) { + throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + } + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error('Balance too small'); + + const feeAmount = bpsOf(inputAmount, FEE_BPS); + const bridgeAmount = inputAmount - feeAmount; + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + console.log(`Pre-bridge fee: ${ethers.formatUnits(feeAmount, 18)} AAVE (${FEE_BPS} bps)`); + console.log(`Net to bridge: ${ethers.formatUnits(bridgeAmount, 18)}`); + + console.log('Fetching Relay.link quote...'); + const quote = await fetchRelayQuoteV2({ + routerAddress: ROUTER_POLYGON, + recipient: signerAddress, + originChainId: CHAIN_IDS.POLYGON, + destinationChainId: CHAIN_IDS.BASE, + originCurrency: TOKENS.AAVE_POLYGON, + destinationCurrency: TOKENS.AAVE_BASE, + amount: bridgeAmount, + }); + const { relaySpender, depositTarget, depositData } = parseRelayQuote(quote); + console.log(`Relay spender: ${relaySpender}`); + console.log(`Deposit target: ${depositTarget}`); + + await ensureRouterErc20Balance(signer, inputToken, ROUTER_POLYGON); + + const ahIface = new ethers.Interface([ + 'function transferFrom(address token, address owner, address recipient, uint256 amount)', + ]); + const exec = new ModularActionsBuilder(); + exec.call(ALLOWANCE_HOLDER, ahIface.encodeFunctionData('transferFrom', [inputToken, signerAddress, ROUTER_POLYGON, inputAmount])); + exec.call(inputToken, encodeTransfer(signerAddress, feeAmount)); + await modularApproveIfNeeded(exec, provider, ROUTER_POLYGON, inputToken, relaySpender, bridgeAmount, bridgeAmount); + exec.call(depositTarget, depositData); + + const routerIface = new ethers.Interface(ROUTER_ABI); + const execCalldata = routerIface.encodeFunctionData('performActions', [ZERO_BYTES32, exec.toActions()]); + + await ensureAllowanceForAllowanceHolder(signer, inputToken, inputAmount); + + console.log('Sending AllowanceHolder.exec → router.performActions...'); + const receipt = await execViaAH(signer, ROUTER_POLYGON, inputToken, inputAmount, ROUTER_POLYGON, execCalldata); + + logTxnSummary('Polygon AAVE → Base AAVE — Relay — performActions preFee', CHAIN_IDS.POLYGON, receipt); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/relay/usdc.bridge.preFee.ts b/scripts/e2e/relay/usdc.bridge.preFee.ts new file mode 100644 index 0000000..2fc5cbb --- /dev/null +++ b/scripts/e2e/relay/usdc.bridge.preFee.ts @@ -0,0 +1,105 @@ +/** + * Route: Polygon USDC → Base USDC via Relay.link (no swap) + * Function: bridge (simple bridge entrypoint) + * Fee: preFee — FEE_BPS of inputAmount USDC deducted before bridge + * + * Bridge amount is pre-encoded in Relay deposit calldata. + * Uses router.bridge() rather than performExecution / performActions. + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/relay/usdc.bridge.preFee.ts + */ +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, +} from '../config'; +import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; +import { getWalletErc20Balance } from '../utils/erc20'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { ZERO_BYTES32, type BridgeData, type FeeData, type InputData } from '../utils/contractTypes'; +import { fetchRelayQuoteV2, parseRelayQuote } from '../utils/relayLinkQuote'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterErc20Balance } from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; + +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); + +async function main(): Promise { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) { + throw new Error('PRIVATE_KEY env var required'); + } + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const inputToken = TOKENS.USDC_POLYGON_CIRCLE; + const { balance: walletBalance } = await getWalletErc20Balance(inputToken, signerAddress, provider); + if (walletBalance === 0n) { + throw new Error(`Signer ${signerAddress} has zero Circle native USDC on Polygon`); + } + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error('Balance too small'); + + const feeAmount = bpsOf(inputAmount, FEE_BPS); + const bridgeAmount = inputAmount - feeAmount; + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`USDC balance: ${ethers.formatUnits(walletBalance, 6)}`); + console.log(`Pre-bridge fee: ${ethers.formatUnits(feeAmount, 6)} USDC (${FEE_BPS} bps)`); + console.log(`Net to bridge: ${ethers.formatUnits(bridgeAmount, 6)}`); + + console.log('Fetching Relay.link quote...'); + const quote = await fetchRelayQuoteV2({ + routerAddress: ROUTER_POLYGON, + recipient: signerAddress, + originChainId: CHAIN_IDS.POLYGON, + destinationChainId: CHAIN_IDS.BASE, + originCurrency: TOKENS.USDC_POLYGON_CIRCLE, + destinationCurrency: TOKENS.USDC_BASE, + amount: bridgeAmount, + }); + const { relaySpender, depositTarget, depositData } = parseRelayQuote(quote); + console.log(`Relay spender: ${relaySpender}`); + console.log(`Deposit target: ${depositTarget}`); + + await ensureRouterErc20Balance(signer, inputToken, ROUTER_POLYGON); + + const bridgeApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + inputToken, + relaySpender, + bridgeAmount, + ); + + const input: InputData = { user: signerAddress, inputToken, inputAmount }; + const fee: FeeData = { receiver: signerAddress, amount: feeAmount }; + const bridgeData: BridgeData = { target: depositTarget, approvalSpender: bridgeApprovalSpender, value: 0n }; + + const routerIface = new ethers.Interface(ROUTER_ABI); + const execCalldata = routerIface.encodeFunctionData('bridge', [ZERO_BYTES32, input, fee, bridgeData, depositData]); + + await ensureAllowanceForAllowanceHolder(signer, inputToken, inputAmount); + + console.log('Sending AllowanceHolder.exec → router.bridge...'); + const receipt = await execViaAH(signer, ROUTER_POLYGON, inputToken, inputAmount, ROUTER_POLYGON, execCalldata); + + logTxnSummary('Polygon USDC → Base USDC — Relay — bridge preFee', CHAIN_IDS.POLYGON, receipt); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/relay/usdc.performExecution.preFee.ts b/scripts/e2e/relay/usdc.performExecution.preFee.ts new file mode 100644 index 0000000..e5ed43d --- /dev/null +++ b/scripts/e2e/relay/usdc.performExecution.preFee.ts @@ -0,0 +1,102 @@ +/** + * Route: Polygon USDC → Base USDC via Relay.link (no swap) + * Function: bridge + * Fee: preFee — FEE_BPS of inputAmount USDC deducted before bridge + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/relay/usdc.performExecution.preFee.ts + */ +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, +} from '../config'; +import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; +import { getWalletErc20Balance } from '../utils/erc20'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { ZERO_BYTES32, bridgeArgs, type BridgeData, type FeeData, type InputData } from '../utils/contractTypes'; +import { fetchRelayQuoteV2, parseRelayQuote } from '../utils/relayLinkQuote'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterErc20Balance } from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; + +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); + +async function main(): Promise { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) { + throw new Error('PRIVATE_KEY env var required'); + } + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const inputToken = TOKENS.USDC_POLYGON_CIRCLE; + const { balance: walletBalance } = await getWalletErc20Balance(inputToken, signerAddress, provider); + if (walletBalance === 0n) { + throw new Error(`Signer ${signerAddress} has zero Circle native USDC on Polygon`); + } + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error('Balance too small'); + + const feeAmount = bpsOf(inputAmount, FEE_BPS); + const bridgeAmount = inputAmount - feeAmount; + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`USDC balance: ${ethers.formatUnits(walletBalance, 6)}`); + console.log(`Pre-bridge fee: ${ethers.formatUnits(feeAmount, 6)} USDC (${FEE_BPS} bps)`); + console.log(`Net to bridge: ${ethers.formatUnits(bridgeAmount, 6)}`); + + console.log('Fetching Relay.link quote...'); + const quote = await fetchRelayQuoteV2({ + routerAddress: ROUTER_POLYGON, + recipient: signerAddress, + originChainId: CHAIN_IDS.POLYGON, + destinationChainId: CHAIN_IDS.BASE, + originCurrency: TOKENS.USDC_POLYGON_CIRCLE, + destinationCurrency: TOKENS.USDC_BASE, + amount: bridgeAmount, + }); + const { relaySpender, depositTarget, depositData } = parseRelayQuote(quote); + console.log(`Relay spender: ${relaySpender}`); + console.log(`Deposit target: ${depositTarget}`); + + await ensureRouterErc20Balance(signer, inputToken, ROUTER_POLYGON); + + const input: InputData = { user: signerAddress, inputToken, inputAmount }; + const fee: FeeData = { receiver: signerAddress, amount: feeAmount }; + const bridgeApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + inputToken, + relaySpender, + bridgeAmount, + ); + + const bridgeData: BridgeData = { target: depositTarget, approvalSpender: bridgeApprovalSpender, value: 0n }; + + const routerIface = new ethers.Interface(ROUTER_ABI); + const execCalldata = routerIface.encodeFunctionData('bridge', bridgeArgs(ZERO_BYTES32, input, fee, bridgeData, depositData)); + + await ensureAllowanceForAllowanceHolder(signer, inputToken, inputAmount); + + console.log('Sending AllowanceHolder.exec → router.bridge...'); + const receipt = await execViaAH(signer, ROUTER_POLYGON, inputToken, inputAmount, ROUTER_POLYGON, execCalldata); + + logTxnSummary('Polygon USDC → Base USDC — Relay — bridge preFee', CHAIN_IDS.POLYGON, receipt); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/relay/usdc.performModularExecution.preFee.ts b/scripts/e2e/relay/usdc.performModularExecution.preFee.ts new file mode 100644 index 0000000..9dc70e5 --- /dev/null +++ b/scripts/e2e/relay/usdc.performModularExecution.preFee.ts @@ -0,0 +1,107 @@ +/** + * Route: Polygon USDC → Base USDC via Relay.link (no swap) + * Function: performActions (modular) + * Fee: preFee — FEE_BPS of inputAmount USDC deducted before bridge + * + * Modular action sequence: + * [0] AH.transferFrom(USDC, signer, router, inputAmount) + * [1] USDC.transfer(signer, feeAmount) — preFee out + * [2] USDC.approve(relaySpender, bridgeAmount) + * [3] call(depositTarget, depositData) — Relay bridge + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/relay/usdc.performActions.preFee.ts + */ +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + ALLOWANCE_HOLDER, +} from '../config'; +import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; +import { encodeTransfer, getWalletErc20Balance } from '../utils/erc20'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { ModularActionsBuilder } from '../utils/modularActionsBuilder/index'; +import { ZERO_BYTES32 } from '../utils/contractTypes'; +import { fetchRelayQuoteV2, parseRelayQuote } from '../utils/relayLinkQuote'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterErc20Balance } from '../utils/reproducibility'; +import { modularApproveIfNeeded } from '../utils/routerAllowance'; + +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); + +async function main(): Promise { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) { + throw new Error('PRIVATE_KEY env var required'); + } + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const inputToken = TOKENS.USDC_POLYGON_CIRCLE; + const { balance: walletBalance } = await getWalletErc20Balance(inputToken, signerAddress, provider); + if (walletBalance === 0n) { + throw new Error(`Signer ${signerAddress} has zero Circle native USDC on Polygon`); + } + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error('Balance too small'); + + const feeAmount = bpsOf(inputAmount, FEE_BPS); + const bridgeAmount = inputAmount - feeAmount; + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`USDC balance: ${ethers.formatUnits(walletBalance, 6)}`); + console.log(`Pre-bridge fee: ${ethers.formatUnits(feeAmount, 6)} USDC (${FEE_BPS} bps)`); + console.log(`Net to bridge: ${ethers.formatUnits(bridgeAmount, 6)}`); + + console.log('Fetching Relay.link quote...'); + const quote = await fetchRelayQuoteV2({ + routerAddress: ROUTER_POLYGON, + recipient: signerAddress, + originChainId: CHAIN_IDS.POLYGON, + destinationChainId: CHAIN_IDS.BASE, + originCurrency: TOKENS.USDC_POLYGON_CIRCLE, + destinationCurrency: TOKENS.USDC_BASE, + amount: bridgeAmount, + }); + const { relaySpender, depositTarget, depositData } = parseRelayQuote(quote); + console.log(`Relay spender: ${relaySpender}`); + console.log(`Deposit target: ${depositTarget}`); + + await ensureRouterErc20Balance(signer, inputToken, ROUTER_POLYGON); + + const ahIface = new ethers.Interface([ + 'function transferFrom(address token, address owner, address recipient, uint256 amount)', + ]); + const exec = new ModularActionsBuilder(); + exec.call(ALLOWANCE_HOLDER, ahIface.encodeFunctionData('transferFrom', [inputToken, signerAddress, ROUTER_POLYGON, inputAmount])); + exec.call(inputToken, encodeTransfer(signerAddress, feeAmount)); + await modularApproveIfNeeded(exec, provider, ROUTER_POLYGON, inputToken, relaySpender, bridgeAmount, bridgeAmount); + exec.call(depositTarget, depositData); + + const routerIface = new ethers.Interface(ROUTER_ABI); + const execCalldata = routerIface.encodeFunctionData('performActions', [ZERO_BYTES32, exec.toActions()]); + + await ensureAllowanceForAllowanceHolder(signer, inputToken, inputAmount); + + console.log('Sending AllowanceHolder.exec → router.performActions...'); + const receipt = await execViaAH(signer, ROUTER_POLYGON, inputToken, inputAmount, ROUTER_POLYGON, execCalldata); + + logTxnSummary('Polygon USDC → Base USDC — Relay — performActions preFee', CHAIN_IDS.POLYGON, receipt); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/stargate/arbUsdcBaseEth.performExecution.postFee.ts b/scripts/e2e/stargate/arbUsdcBaseEth.performExecution.postFee.ts new file mode 100644 index 0000000..81e4f8e --- /dev/null +++ b/scripts/e2e/stargate/arbUsdcBaseEth.performExecution.postFee.ts @@ -0,0 +1,196 @@ +/** + * Route: Arbitrum USDC → ETH (OpenOcean) → Base ETH (Stargate Native ETH Pool) + * Function: swapAndBridge + * Fee: postFee — FEE_BPS of estimatedOut ETH deducted after swap + * + * BRIDGE_VALUE_FLAG set: router forwards finalETH + nativeFeeWithBuffer as msg.value to Stargate. + * BRIDGE_AMOUNT_POSITION_FLAG set: router splices finalETH into amountLD at runtime. + * Stargate receives the exact actual post-swap, post-fee ETH as amountLD. + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/stargate/arbUsdcBaseEth.performExecution.postFee.ts + */ +import axios from 'axios'; +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, + NATIVE_TOKEN_ADDRESS, + STARGATE_NATIVE_ARB, + BASE_LZ_EID, + STARGATE_AMOUNT_LD_OFFSET, +} from '../config'; +import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; +import { getWalletErc20Balance } from '../utils/erc20'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { + BRIDGE_VALUE_FLAG, + POST_FEE_FLAG, + ZERO_ADDRESS, + ZERO_BYTES32, + bridgeAmountPositionFlag, + swapAndBridgeArgs, +} from '../utils/contractTypes'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterErc20Balance, ensureRouterNativeBalance } from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; + +const ROUTER_ARB = routerAddressForChain(CHAIN_IDS.ARBITRUM); +const FLAGS = POST_FEE_FLAG | BRIDGE_VALUE_FLAG | bridgeAmountPositionFlag(STARGATE_AMOUNT_LD_OFFSET); + +const STARGATE_ABI = [ + 'function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)', + 'function quoteOFT(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) external view returns (tuple(uint256 minAmountLD, uint256 maxAmountLD) oftLimit, tuple(int256 feeAmountLD, string description)[] oftFeeDetails, tuple(uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt)', + 'function send(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee, address refundAddress) external payable', +]; +const STARGATE_IFACE = new ethers.Interface(STARGATE_ABI); + +interface OoQuoteResponse { + data: { to: string; data: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.USDC_ARB, + outTokenAddress: NATIVE_TOKEN_ADDRESS, + amount: ethers.formatUnits(inputAmount, 6), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_ARB, + account: ROUTER_ARB, + gasPrice: '1', + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.ARBITRUM}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + }; +} + +async function fetchStargateQuote( + provider: ethers.JsonRpcProvider, + bridgeAmountLD: bigint, + recipient: string, +): Promise<{ nativeFee: bigint; nativeFeeWithBuffer: bigint; amountReceivedLD: bigint }> { + const contract = new ethers.Contract(STARGATE_NATIVE_ARB, STARGATE_ABI, provider); + const to32 = ethers.zeroPadValue(recipient, 32); + const sendParam = { dstEid: BASE_LZ_EID, to: to32, amountLD: bridgeAmountLD, minAmountLD: 0n, extraOptions: '0x', composeMsg: '0x', oftCmd: '0x' }; + const [fee, oft] = await Promise.all([contract.quoteSend(sendParam, false), contract.quoteOFT(sendParam)]); + const nativeFee = fee.nativeFee as bigint; + return { + nativeFee, + nativeFeeWithBuffer: (nativeFee * 105n) / 100n, + amountReceivedLD: oft.oftReceipt.amountReceivedLD as bigint, + }; +} + +function buildStargateCalldata(nativeFee: bigint, recipient: string, amountLD: bigint): string { + return STARGATE_IFACE.encodeFunctionData('send', [ + { dstEid: BASE_LZ_EID, to: ethers.zeroPadValue(recipient, 32), amountLD, minAmountLD: 0n, extraOptions: '0x', composeMsg: '0x', oftCmd: '0x' }, + { nativeFee, lzTokenFee: 0n }, + recipient, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error('PRIVATE_KEY env var required'); + + const provider = new ethers.JsonRpcProvider(RPC.ARBITRUM); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance(TOKENS.USDC_ARB, signerAddress, provider); + if (walletBalance === 0n) throw new Error(`Signer ${signerAddress} has zero USDC on Arbitrum`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error('Balance too small'); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_ARB}`); + console.log(`USDC balance: ${ethers.formatUnits(walletBalance, 6)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log('Fetching OpenOcean quote (USDC → ETH on Arbitrum)...'); + const { ooRouter, swapData, estimatedOut, minAmountOut } = await fetchOpenOceanQuote(inputAmount); + const feeAmount = bpsOf(estimatedOut, FEE_BPS); + const estimatedBridgeAmount = estimatedOut - feeAmount; + console.log(` OO router: ${ooRouter}`); + console.log(` Est. ETH: ${ethers.formatEther(estimatedOut)}`); + console.log(` Post-fee: ${ethers.formatEther(feeAmount)} ETH (${FEE_BPS} bps)`); + console.log(` Min ETH: ${ethers.formatEther(minAmountOut)}`); + + console.log('Fetching Stargate quote (Arbitrum → Base native pool)...'); + const { nativeFeeWithBuffer, amountReceivedLD } = await fetchStargateQuote(provider, estimatedBridgeAmount, signerAddress); + console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} ETH`); + console.log(` Est. received: ${ethers.formatEther(amountReceivedLD)} ETH`); + + // estimatedBridgeAmount is a placeholder; router splices the actual finalETH at runtime + const amountLD = estimatedBridgeAmount; + + await ensureRouterErc20Balance(signer, TOKENS.USDC_ARB, ROUTER_ARB); + await ensureRouterNativeBalance(signer, ROUTER_ARB); + + const swapApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_ARB, + TOKENS.USDC_ARB, + ooRouter, + inputAmount, + ); + + const stargateData = buildStargateCalldata(nativeFeeWithBuffer, signerAddress, amountLD); + + const callData = routerIface.encodeFunctionData( + 'swapAndBridge', + swapAndBridgeArgs( + ZERO_BYTES32, + FLAGS, + { user: signerAddress, inputToken: TOKENS.USDC_ARB, inputAmount }, + { receiver: signerAddress, amount: feeAmount }, + { + target: ooRouter, + approvalSpender: swapApprovalSpender, + outputToken: NATIVE_TOKEN_ADDRESS, + value: 0n, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + swapData, + { target: STARGATE_NATIVE_ARB, approvalSpender: ZERO_ADDRESS, value: nativeFeeWithBuffer }, + stargateData, + ), + ); + + await ensureAllowanceForAllowanceHolder(signer, TOKENS.USDC_ARB, inputAmount); + const receipt = await execViaAH(signer, ROUTER_ARB, TOKENS.USDC_ARB, inputAmount, ROUTER_ARB, callData, nativeFeeWithBuffer); + + logTxnSummary( + 'Arbitrum USDC → ETH (OO) → Base ETH (Stargate native) — performExecution postFee', + CHAIN_IDS.ARBITRUM, + receipt, + ); + + console.log('\nETH arrives on Base once LZ delivers the message.'); +} + +main().catch((err) => { console.error(err); process.exit(1); }); diff --git a/scripts/e2e/stargate/arbUsdcBaseEth.performModularExecution.postFee.ts b/scripts/e2e/stargate/arbUsdcBaseEth.performModularExecution.postFee.ts new file mode 100644 index 0000000..aa0b93c --- /dev/null +++ b/scripts/e2e/stargate/arbUsdcBaseEth.performModularExecution.postFee.ts @@ -0,0 +1,178 @@ +/** + * Route: Arbitrum USDC → ETH (OpenOcean) → Base ETH (Stargate Native ETH Pool) + * Function: performActions (modular) + * Fee: postFee — FEE_BPS of estimatedOut ETH sent to signer after swap + * + * Modular action sequence: + * [0] AH.transferFrom(USDC, signer, router, inputAmount) + * [1] USDC.approve(ooRouter, inputAmount) + * [2] call(ooRouter, swapData) — USDC → ETH lands in router + * [3] nativeCall(signer, '0x', feeAmount) — ETH fee out + * [4] nativeCall(Stargate, sendData, bridgeValue) — value = amountLD + nativeFeeWithBuffer + * + * amountLD = minAmountOut - feeAmount - nativeFeeWithBuffer (pre-encoded, surplus stays in router). + * bridgeValue = minAmountOut - feeAmount (amountLD + nativeFeeWithBuffer). + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/stargate/arbUsdcBaseEth.performActions.postFee.ts + */ +import axios from 'axios'; +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, + ALLOWANCE_HOLDER, + NATIVE_TOKEN_ADDRESS, + STARGATE_NATIVE_ARB, + BASE_LZ_EID, +} from '../config'; +import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; +import { getWalletErc20Balance } from '../utils/erc20'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { ModularActionsBuilder } from '../utils/modularActionsBuilder/index'; +import { ZERO_BYTES32 } from '../utils/contractTypes'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterErc20Balance, ensureRouterNativeBalance } from '../utils/reproducibility'; +import { modularApproveIfNeeded } from '../utils/routerAllowance'; + +const ROUTER_ARB = routerAddressForChain(CHAIN_IDS.ARBITRUM); + +const STARGATE_ABI = [ + 'function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)', + 'function quoteOFT(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) external view returns (tuple(uint256 minAmountLD, uint256 maxAmountLD) oftLimit, tuple(int256 feeAmountLD, string description)[] oftFeeDetails, tuple(uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt)', + 'function send(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee, address refundAddress) external payable', +]; +const STARGATE_IFACE = new ethers.Interface(STARGATE_ABI); + +interface OoQuoteResponse { + data: { to: string; data: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.USDC_ARB, + outTokenAddress: NATIVE_TOKEN_ADDRESS, + amount: ethers.formatUnits(inputAmount, 6), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_ARB, + account: ROUTER_ARB, + gasPrice: '1', + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.ARBITRUM}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + }; +} + +async function fetchStargateQuote( + provider: ethers.JsonRpcProvider, + bridgeAmountLD: bigint, + recipient: string, +): Promise<{ nativeFeeWithBuffer: bigint; amountReceivedLD: bigint }> { + const contract = new ethers.Contract(STARGATE_NATIVE_ARB, STARGATE_ABI, provider); + const to32 = ethers.zeroPadValue(recipient, 32); + const sendParam = { dstEid: BASE_LZ_EID, to: to32, amountLD: bridgeAmountLD, minAmountLD: 0n, extraOptions: '0x', composeMsg: '0x', oftCmd: '0x' }; + const [fee, oft] = await Promise.all([contract.quoteSend(sendParam, false), contract.quoteOFT(sendParam)]); + return { + nativeFeeWithBuffer: ((fee.nativeFee as bigint) * 105n) / 100n, + amountReceivedLD: oft.oftReceipt.amountReceivedLD as bigint, + }; +} + +function buildStargateCalldata(nativeFee: bigint, amountLD: bigint, recipient: string): string { + return STARGATE_IFACE.encodeFunctionData('send', [ + { dstEid: BASE_LZ_EID, to: ethers.zeroPadValue(recipient, 32), amountLD, minAmountLD: 0n, extraOptions: '0x', composeMsg: '0x', oftCmd: '0x' }, + { nativeFee, lzTokenFee: 0n }, + recipient, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error('PRIVATE_KEY env var required'); + + const provider = new ethers.JsonRpcProvider(RPC.ARBITRUM); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance(TOKENS.USDC_ARB, signerAddress, provider); + if (walletBalance === 0n) throw new Error(`Signer ${signerAddress} has zero USDC on Arbitrum`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error('Balance too small'); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_ARB}`); + console.log(`USDC balance: ${ethers.formatUnits(walletBalance, 6)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log('Fetching OpenOcean quote (USDC → ETH on Arbitrum)...'); + const { ooRouter, swapData, estimatedOut, minAmountOut } = await fetchOpenOceanQuote(inputAmount); + const feeAmount = bpsOf(estimatedOut, FEE_BPS); + const estimatedBridgeAmount = estimatedOut - feeAmount; + console.log(` OO router: ${ooRouter}`); + console.log(` Est. ETH: ${ethers.formatEther(estimatedOut)}`); + console.log(` Post-fee: ${ethers.formatEther(feeAmount)} ETH (${FEE_BPS} bps)`); + console.log(` Min ETH: ${ethers.formatEther(minAmountOut)}`); + + console.log('Fetching Stargate quote (Arbitrum → Base native pool)...'); + const { nativeFeeWithBuffer, amountReceivedLD } = await fetchStargateQuote(provider, estimatedBridgeAmount, signerAddress); + console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} ETH`); + console.log(` Est. received: ${ethers.formatEther(amountReceivedLD)} ETH`); + + const amountLD = minAmountOut - feeAmount - nativeFeeWithBuffer; + if (amountLD <= 0n) throw new Error('minAmountOut too small to cover fee + nativeFee'); + // bridgeValue = amountLD + nativeFeeWithBuffer = minAmountOut - feeAmount + const bridgeValue = minAmountOut - feeAmount; + + await ensureRouterErc20Balance(signer, TOKENS.USDC_ARB, ROUTER_ARB); + await ensureRouterNativeBalance(signer, ROUTER_ARB); + + const stargateData = buildStargateCalldata(nativeFeeWithBuffer, amountLD, signerAddress); + + const ahIface = new ethers.Interface([ + 'function transferFrom(address token, address owner, address recipient, uint256 amount)', + ]); + const exec = new ModularActionsBuilder(); + exec.call(ALLOWANCE_HOLDER, ahIface.encodeFunctionData('transferFrom', [TOKENS.USDC_ARB, signerAddress, ROUTER_ARB, inputAmount])); + await modularApproveIfNeeded(exec, provider, ROUTER_ARB, TOKENS.USDC_ARB, ooRouter, inputAmount, inputAmount); + exec.call(ooRouter, swapData); + exec.nativeCall(signerAddress, '0x', feeAmount); + exec.nativeCall(STARGATE_NATIVE_ARB, stargateData, bridgeValue); + + const callData = routerIface.encodeFunctionData('performActions', [ZERO_BYTES32, exec.toActions()]); + + await ensureAllowanceForAllowanceHolder(signer, TOKENS.USDC_ARB, inputAmount); + const receipt = await execViaAH(signer, ROUTER_ARB, TOKENS.USDC_ARB, inputAmount, ROUTER_ARB, callData, nativeFeeWithBuffer); + + logTxnSummary( + 'Arbitrum USDC → ETH (OO) → Base ETH (Stargate native) — performActions postFee', + CHAIN_IDS.ARBITRUM, + receipt, + ); + + console.log('\nETH arrives on Base once LZ delivers the message.'); +} + +main().catch((err) => { console.error(err); process.exit(1); }); diff --git a/scripts/e2e/stargate/baseUsdcArbEth.performExecution.postFee.ts b/scripts/e2e/stargate/baseUsdcArbEth.performExecution.postFee.ts new file mode 100644 index 0000000..05d0c3f --- /dev/null +++ b/scripts/e2e/stargate/baseUsdcArbEth.performExecution.postFee.ts @@ -0,0 +1,270 @@ +/** + * Route: Base USDC → native ETH (OpenOcean) → Arbitrum ETH (Stargate Native Pool on Base, LayerZero v2) + * Flags: post-fee (fee taken from ETH output after swap), output read from swap returndata word 0 + * bridge-value + bridge-amount-position flags: router splices finalETH into amountLD and + * forwards finalETH + nativeFeeWithBuffer as msg.value to Stargate + * + * Post-fee (bit0=1): feeAmount = FEE_BPS of estimatedOut ETH, deducted from swap output. + * Returndata (bit1=0): final ETH amount is read from word 0 of the swap call returndata. + * BridgeValue (bit2=1): router forwards finalETH + nativeFeeWithBuffer as msg.value to Stargate. + * BridgeAmountPosition (bit3=1): router splices finalETH into amountLD at STARGATE_AMOUNT_LD_OFFSET. + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/stargate/baseUsdcArbEth.performExecution.postFee.ts + */ +import axios from "axios"; +import { ethers } from "ethers"; +import * as dotenv from "dotenv"; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + NATIVE_TOKEN_ADDRESS, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, + STARGATE_NATIVE_BASE, + ARBITRUM_LZ_EID, +} from "../config"; +import { + execViaAH, + ensureAllowanceForAllowanceHolder, +} from "../utils/allowanceHolder"; +import { getWalletErc20Balance } from "../utils/erc20"; +import { ROUTER_ABI } from "../utils/routerAbi"; +import { + ZERO_BYTES32, + BRIDGE_VALUE_FLAG, + ZERO_ADDRESS, + bridgeAmountPositionFlag, + swapAndBridgeArgs, +} from "../utils/contractTypes"; +import { STARGATE_AMOUNT_LD_OFFSET } from "../config"; +import { logTxnSummary } from "../utils/txnLogSummary"; +import { + ensureRouterErc20Balance, + ensureRouterNativeBalance, +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; + +// post-fee (0x01) | bridge-value (0x04) | bridge-amount-position (0x08 + offset): splice finalETH into amountLD + forward with nativeFee +const FLAGS = 0x01n | BRIDGE_VALUE_FLAG | bridgeAmountPositionFlag(STARGATE_AMOUNT_LD_OFFSET); +const ROUTER_BASE = routerAddressForChain(CHAIN_IDS.BASE); + +const STARGATE_ABI = [ + "function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)", + "function quoteOFT(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) external view returns (tuple(uint256 minAmountLD, uint256 maxAmountLD) oftLimit, tuple(int256 feeAmountLD, string description)[] oftFeeDetails, tuple(uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt)", + "function send(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee, address refundAddress) external payable", +]; + +const STARGATE_IFACE = new ethers.Interface(STARGATE_ABI); + +interface OoQuoteResponse { + data: { to: string; data: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.USDC_BASE, + outTokenAddress: NATIVE_TOKEN_ADDRESS, + amount: ethers.formatUnits(inputAmount, 6), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_BASE, + account: ROUTER_BASE, + gasPrice: "1", + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.BASE}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + }; +} + +async function fetchStargateQuote( + provider: ethers.JsonRpcProvider, + bridgeAmountLD: bigint, + recipient: string +): Promise<{ nativeFee: bigint; amountReceivedLD: bigint }> { + const contract = new ethers.Contract( + STARGATE_NATIVE_BASE, + STARGATE_ABI, + provider + ); + const to32 = ethers.zeroPadValue(recipient, 32); + const sendParam = { + dstEid: ARBITRUM_LZ_EID, + to: to32, + amountLD: bridgeAmountLD, + minAmountLD: 0n, + extraOptions: "0x", + composeMsg: "0x", + oftCmd: "0x", + }; + const [fee, oft] = await Promise.all([ + contract.quoteSend(sendParam, false), + contract.quoteOFT(sendParam), + ]); + return { + nativeFee: fee.nativeFee as bigint, + amountReceivedLD: oft.oftReceipt.amountReceivedLD as bigint, + }; +} + +function buildStargateCalldata( + nativeFee: bigint, + recipient: string, + amountLD: bigint +): string { + return STARGATE_IFACE.encodeFunctionData("send", [ + { + dstEid: ARBITRUM_LZ_EID, + to: ethers.zeroPadValue(recipient, 32), + amountLD, + minAmountLD: 0n, + extraOptions: "0x", + composeMsg: "0x", + oftCmd: "0x", + }, + { nativeFee, lzTokenFee: 0n }, + recipient, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error("PRIVATE_KEY env var required"); + + const provider = new ethers.JsonRpcProvider(RPC.BASE); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.USDC_BASE, + signerAddress, + provider + ); + if (walletBalance === 0n) + throw new Error(`Signer ${signerAddress} has zero USDC on Base`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error("Balance too small"); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_BASE}`); + console.log( + `Flags: 0x${FLAGS.toString( + 16 + )} (post-fee, returndata, bridge-value)` + ); + console.log(`USDC balance: ${ethers.formatUnits(walletBalance, 6)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log("Fetching OpenOcean quote (USDC → ETH)..."); + const { ooRouter, swapData, estimatedOut, minAmountOut } = + await fetchOpenOceanQuote(inputAmount); + + // post-fee: deduct from estimated ETH output after swap + const feeAmount = bpsOf(estimatedOut, FEE_BPS); + console.log(` OO router: ${ooRouter}`); + console.log(` Est. ETH: ${ethers.formatEther(estimatedOut)}`); + console.log( + ` Post-fee: ${ethers.formatEther(feeAmount)} ETH (${FEE_BPS} bps)` + ); + console.log(` Min ETH: ${ethers.formatEther(minAmountOut)}`); + + console.log("Fetching Stargate quote (Base native pool → Arbitrum)..."); + const bridgeEstimate = estimatedOut - feeAmount; + const { nativeFee, amountReceivedLD } = await fetchStargateQuote( + provider, + bridgeEstimate, + signerAddress + ); + const nativeFeeWithBuffer = (nativeFee * 105n) / 100n; + console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} ETH`); + console.log(` Est. received: ${ethers.formatEther(amountReceivedLD)} ETH`); + + // bridgeEstimate is a placeholder for amountLD; router splices the actual finalETH at runtime + const amountLD = bridgeEstimate; + + await ensureRouterErc20Balance(signer, TOKENS.USDC_BASE, ROUTER_BASE); + await ensureRouterNativeBalance(signer, ROUTER_BASE); + + const swapApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_BASE, + TOKENS.USDC_BASE, + ooRouter, + inputAmount, + ); + + const stargateData = buildStargateCalldata( + nativeFeeWithBuffer, + signerAddress, + amountLD + ); + + const callData = routerIface.encodeFunctionData("swapAndBridge", swapAndBridgeArgs( + ZERO_BYTES32, + FLAGS, + { + user: signerAddress, + inputToken: TOKENS.USDC_BASE, + inputAmount: inputAmount, + }, + { receiver: signerAddress, amount: feeAmount }, + { + target: ooRouter, + approvalSpender: swapApprovalSpender, + outputToken: NATIVE_TOKEN_ADDRESS, + value: 0n, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + swapData, + { + target: STARGATE_NATIVE_BASE, + approvalSpender: ZERO_ADDRESS, + value: nativeFeeWithBuffer, + }, + stargateData, + )); + + await ensureAllowanceForAllowanceHolder(signer, TOKENS.USDC_BASE, inputAmount); + const receipt = await execViaAH( + signer, + ROUTER_BASE, + TOKENS.USDC_BASE, + inputAmount, + ROUTER_BASE, + callData, + nativeFeeWithBuffer + ); + + logTxnSummary( + `Base USDC → Arbitrum ETH (Stargate) — swapAndBridge postFee/returndata`, + CHAIN_IDS.BASE, + receipt + ); + + console.log("\nETH arrives on Arbitrum once LZ delivers the message."); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/stargate/baseUsdcArbEth.performModularExecution.postFee.ts b/scripts/e2e/stargate/baseUsdcArbEth.performModularExecution.postFee.ts new file mode 100644 index 0000000..7521bcb --- /dev/null +++ b/scripts/e2e/stargate/baseUsdcArbEth.performModularExecution.postFee.ts @@ -0,0 +1,174 @@ +/** + * Route: Base USDC → ETH (OpenOcean) → Arbitrum ETH (Stargate Native ETH Pool) + * Function: performActions (modular) + * Fee: postFee — FEE_BPS of estimatedOut ETH sent to signer after swap + * + * Modular action sequence: + * [0] AH.transferFrom(USDC, signer, router, inputAmount) + * [1] USDC.approve(ooRouter, inputAmount) + * [2] call(ooRouter, swapData) — USDC → ETH lands in router + * [3] nativeCall(signer, '0x', feeAmount) + * [4] nativeCall(Stargate, sendData, bridgeValue) — value = amountLD + nativeFeeWithBuffer + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/stargate/baseUsdcArbEth.performActions.postFee.ts + */ +import axios from 'axios'; +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, + ALLOWANCE_HOLDER, + NATIVE_TOKEN_ADDRESS, + STARGATE_NATIVE_BASE, + ARBITRUM_LZ_EID, +} from '../config'; +import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; +import { getWalletErc20Balance } from '../utils/erc20'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { ModularActionsBuilder } from '../utils/modularActionsBuilder/index'; +import { ZERO_BYTES32 } from '../utils/contractTypes'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterErc20Balance, ensureRouterNativeBalance } from '../utils/reproducibility'; +import { modularApproveIfNeeded } from '../utils/routerAllowance'; + +const ROUTER_BASE = routerAddressForChain(CHAIN_IDS.BASE); + +const STARGATE_ABI = [ + 'function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)', + 'function quoteOFT(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) external view returns (tuple(uint256 minAmountLD, uint256 maxAmountLD) oftLimit, tuple(int256 feeAmountLD, string description)[] oftFeeDetails, tuple(uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt)', + 'function send(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee, address refundAddress) external payable', +]; +const STARGATE_IFACE = new ethers.Interface(STARGATE_ABI); + +interface OoQuoteResponse { + data: { to: string; data: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.USDC_BASE, + outTokenAddress: NATIVE_TOKEN_ADDRESS, + amount: ethers.formatUnits(inputAmount, 6), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_BASE, + account: ROUTER_BASE, + gasPrice: '1', + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.BASE}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + }; +} + +async function fetchStargateQuote( + provider: ethers.JsonRpcProvider, + bridgeAmountLD: bigint, + recipient: string, +): Promise<{ nativeFeeWithBuffer: bigint; amountReceivedLD: bigint }> { + const contract = new ethers.Contract(STARGATE_NATIVE_BASE, STARGATE_ABI, provider); + const to32 = ethers.zeroPadValue(recipient, 32); + const sendParam = { dstEid: ARBITRUM_LZ_EID, to: to32, amountLD: bridgeAmountLD, minAmountLD: 0n, extraOptions: '0x', composeMsg: '0x', oftCmd: '0x' }; + const [fee, oft] = await Promise.all([contract.quoteSend(sendParam, false), contract.quoteOFT(sendParam)]); + return { + nativeFeeWithBuffer: ((fee.nativeFee as bigint) * 105n) / 100n, + amountReceivedLD: oft.oftReceipt.amountReceivedLD as bigint, + }; +} + +function buildStargateCalldata(nativeFee: bigint, amountLD: bigint, recipient: string): string { + return STARGATE_IFACE.encodeFunctionData('send', [ + { dstEid: ARBITRUM_LZ_EID, to: ethers.zeroPadValue(recipient, 32), amountLD, minAmountLD: 0n, extraOptions: '0x', composeMsg: '0x', oftCmd: '0x' }, + { nativeFee, lzTokenFee: 0n }, + recipient, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error('PRIVATE_KEY env var required'); + + const provider = new ethers.JsonRpcProvider(RPC.BASE); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance(TOKENS.USDC_BASE, signerAddress, provider); + if (walletBalance === 0n) throw new Error(`Signer ${signerAddress} has zero USDC on Base`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error('Balance too small'); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_BASE}`); + console.log(`USDC balance: ${ethers.formatUnits(walletBalance, 6)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log('Fetching OpenOcean quote (USDC → ETH on Base)...'); + const { ooRouter, swapData, estimatedOut, minAmountOut } = await fetchOpenOceanQuote(inputAmount); + const feeAmount = bpsOf(estimatedOut, FEE_BPS); + const estimatedBridgeAmount = estimatedOut - feeAmount; + console.log(` OO router: ${ooRouter}`); + console.log(` Est. ETH: ${ethers.formatEther(estimatedOut)}`); + console.log(` Post-fee: ${ethers.formatEther(feeAmount)} ETH (${FEE_BPS} bps)`); + console.log(` Min ETH: ${ethers.formatEther(minAmountOut)}`); + + console.log('Fetching Stargate quote (Base → Arbitrum native pool)...'); + const { nativeFeeWithBuffer, amountReceivedLD } = await fetchStargateQuote(provider, estimatedBridgeAmount, signerAddress); + console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} ETH`); + console.log(` Est. received: ${ethers.formatEther(amountReceivedLD)} ETH`); + + const amountLD = minAmountOut - feeAmount - nativeFeeWithBuffer; + if (amountLD <= 0n) throw new Error('minAmountOut too small to cover fee + nativeFee'); + const bridgeValue = minAmountOut - feeAmount; + + await ensureRouterErc20Balance(signer, TOKENS.USDC_BASE, ROUTER_BASE); + await ensureRouterNativeBalance(signer, ROUTER_BASE); + + const stargateData = buildStargateCalldata(nativeFeeWithBuffer, amountLD, signerAddress); + + const ahIface = new ethers.Interface([ + 'function transferFrom(address token, address owner, address recipient, uint256 amount)', + ]); + const exec = new ModularActionsBuilder(); + exec.call(ALLOWANCE_HOLDER, ahIface.encodeFunctionData('transferFrom', [TOKENS.USDC_BASE, signerAddress, ROUTER_BASE, inputAmount])); + await modularApproveIfNeeded(exec, provider, ROUTER_BASE, TOKENS.USDC_BASE, ooRouter, inputAmount, inputAmount); + exec.call(ooRouter, swapData); + exec.nativeCall(signerAddress, '0x', feeAmount); + exec.nativeCall(STARGATE_NATIVE_BASE, stargateData, bridgeValue); + + const callData = routerIface.encodeFunctionData('performActions', [ZERO_BYTES32, exec.toActions()]); + + await ensureAllowanceForAllowanceHolder(signer, TOKENS.USDC_BASE, inputAmount); + const receipt = await execViaAH(signer, ROUTER_BASE, TOKENS.USDC_BASE, inputAmount, ROUTER_BASE, callData, nativeFeeWithBuffer); + + logTxnSummary( + 'Base USDC → ETH (OO) → Arbitrum ETH (Stargate native) — performActions postFee', + CHAIN_IDS.BASE, + receipt, + ); + + console.log('\nETH arrives on Arbitrum once LZ delivers the message.'); +} + +main().catch((err) => { console.error(err); process.exit(1); }); diff --git a/scripts/e2e/stargate/polygonPolUsdt0Arb.performExecution.postFee.ts b/scripts/e2e/stargate/polygonPolUsdt0Arb.performExecution.postFee.ts new file mode 100644 index 0000000..87ca5eb --- /dev/null +++ b/scripts/e2e/stargate/polygonPolUsdt0Arb.performExecution.postFee.ts @@ -0,0 +1,236 @@ +/** + * Route: Polygon POL (native) → USDT0 (OpenOcean) → Arbitrum USDT0 (LZ OFT Adapter) + * Function: swapAndBridge + * Fee: postFee — FEE_BPS of estimatedOut USDT0 deducted after swap + * + * Input is native POL; msg.value = ooSwapNativeWei + nativeFeeWithBuffer. + * swap.value = POL forwarded to OO router; bridge.value = nativeFeeWithBuffer (LZ fee). + * Bridge amount position flag splices actual post-fee USDT0 balance at byte 196. + * + * For native-input cases this script must be run with sufficient POL balance. + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/stargate/polygonPolUsdt0Arb.performExecution.postFee.ts + */ +import axios from 'axios'; +import { ethers, parseEther } from 'ethers'; +import * as dotenv from 'dotenv'; +import { Options } from '@layerzerolabs/lz-v2-utilities'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, + NATIVE_TOKEN_ADDRESS, + USDT0_OFT_ADAPTER_POLYGON, + ARBITRUM_LZ_EID, + STARGATE_AMOUNT_LD_OFFSET, +} from '../config'; +import { execViaAH } from '../utils/allowanceHolder'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { + POST_FEE_FLAG, + ZERO_ADDRESS, + ZERO_BYTES32, + bridgeAmountPositionFlag, + swapAndBridgeArgs, +} from '../utils/contractTypes'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterErc20Balance, ensureRouterNativeBalance } from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; + +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); +const FLAGS = POST_FEE_FLAG | bridgeAmountPositionFlag(STARGATE_AMOUNT_LD_OFFSET); +const LZ_EXTRA_OPTIONS = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toHex(); +const NATIVE_INPUT_GAS_RESERVE = parseEther('0.01'); +const NATIVE_INPUT_GAS_LIMIT_ESTIMATE = 2_000_000n; + +const OFT_ABI = [ + 'function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)', + 'function quoteOFT(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) external view returns (tuple(uint256 minAmountLD, uint256 maxAmountLD) oftLimit, tuple(int256 feeAmountLD, string description)[] oftFeeDetails, tuple(uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt)', + 'function send(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee, address refundAddress) external payable', +]; +const OFT_IFACE = new ethers.Interface(OFT_ABI); + +interface OoQuoteResponse { + data: { to: string; data: string; value?: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmountWei: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; + nativeSwapWei: bigint; +}> { + const params: Record = { + inTokenAddress: NATIVE_TOKEN_ADDRESS, + outTokenAddress: TOKENS.USDT0_POLYGON, + amount: ethers.formatEther(inputAmountWei), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_POLYGON, + account: ROUTER_POLYGON, + gasPrice: '1', + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.POLYGON}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + nativeSwapWei: q.value !== undefined && q.value !== '' ? BigInt(q.value) : 0n, + }; +} + +async function fetchOftQuote( + provider: ethers.JsonRpcProvider, + bridgeAmountLD: bigint, + recipient: string, +): Promise<{ nativeFeeWithBuffer: bigint; amountReceivedLD: bigint }> { + const contract = new ethers.Contract(USDT0_OFT_ADAPTER_POLYGON, OFT_ABI, provider); + const to32 = ethers.zeroPadValue(recipient, 32); + const sendParam = { dstEid: ARBITRUM_LZ_EID, to: to32, amountLD: bridgeAmountLD, minAmountLD: 0n, extraOptions: LZ_EXTRA_OPTIONS, composeMsg: '0x', oftCmd: '0x' }; + const [fee, oft] = await Promise.all([contract.quoteSend(sendParam, false), contract.quoteOFT(sendParam)]); + return { + nativeFeeWithBuffer: ((fee.nativeFee as bigint) * 105n) / 100n, + amountReceivedLD: oft.oftReceipt.amountReceivedLD as bigint, + }; +} + +function buildOftSendCalldata(nativeFee: bigint, recipient: string): string { + return OFT_IFACE.encodeFunctionData('send', [ + { dstEid: ARBITRUM_LZ_EID, to: ethers.zeroPadValue(recipient, 32), amountLD: 0n, minAmountLD: 0n, extraOptions: LZ_EXTRA_OPTIONS, composeMsg: '0x', oftCmd: '0x' }, + { nativeFee, lzTokenFee: 0n }, + recipient, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error('PRIVATE_KEY env var required'); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const rawBalance = await provider.getBalance(signerAddress); + if (rawBalance <= NATIVE_INPUT_GAS_RESERVE) { + throw new Error(`Signer ${signerAddress} POL balance (${ethers.formatEther(rawBalance)}) below reserve`); + } + + const feeData = await provider.getFeeData(); + const maxFeePerGas = feeData.maxFeePerGas ?? feeData.gasPrice ?? 500_000_000n; + const gasReserve = maxFeePerGas * NATIVE_INPUT_GAS_LIMIT_ESTIMATE; + console.log(` Gas reserve: ${ethers.formatEther(gasReserve)} POL`); + + // Start with full usable balance; capped below if lz fee eats too much + let inputAmountWei = rawBalance - NATIVE_INPUT_GAS_RESERVE - 20n; + if (inputAmountWei <= 0n) throw new Error('POL balance too small'); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`POL balance: ${ethers.formatEther(rawBalance)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + let ooRouter = ''; + let swapData = ''; + let nativeSwapWei = 0n; + let feeAmount = 0n; + let estimatedBridgeAmount = 0n; + let minAmountOut = 0n; + let nativeFeeWithBuffer = 0n; + let amountReceivedLD = 0n; + + // Re-quote loop: cap inputAmountWei if balance can't cover lz fee + gas reserve + for (let iter = 0; iter < 6; iter++) { + console.log('Fetching OpenOcean quote (POL → USDT0)...'); + const q = await fetchOpenOceanQuote(inputAmountWei); + ooRouter = q.ooRouter; + swapData = q.swapData; + nativeSwapWei = q.nativeSwapWei; + feeAmount = bpsOf(q.estimatedOut, FEE_BPS); + estimatedBridgeAmount = q.estimatedOut - feeAmount; + minAmountOut = q.minAmountOut; + console.log(` OO router: ${ooRouter}`); + console.log(` Est. USDT0: ${ethers.formatUnits(q.estimatedOut, 6)}`); + console.log(` Post-fee: ${ethers.formatUnits(feeAmount, 6)} USDT0`); + + console.log('Fetching OFT quote (Polygon → Arbitrum)...'); + ({ nativeFeeWithBuffer, amountReceivedLD } = await fetchOftQuote(provider, estimatedBridgeAmount, signerAddress)); + console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} POL`); + console.log(` Est. received: ${ethers.formatUnits(amountReceivedLD, 6)} USDT0`); + + const maxAffordable = rawBalance - nativeFeeWithBuffer - gasReserve; + if (maxAffordable <= 0n) { + throw new Error(`POL balance cannot cover lz fee (${ethers.formatEther(nativeFeeWithBuffer)}) + gas reserve`); + } + if (inputAmountWei <= maxAffordable) { + break; + } + console.warn(` Capping swap input from ${ethers.formatEther(inputAmountWei)} to ${ethers.formatEther(maxAffordable)} POL`); + inputAmountWei = maxAffordable; + } + + await ensureRouterErc20Balance(signer, TOKENS.USDT0_POLYGON, ROUTER_POLYGON); + await ensureRouterNativeBalance(signer, ROUTER_POLYGON); + + const bridgeApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + TOKENS.USDT0_POLYGON, + USDT0_OFT_ADAPTER_POLYGON, + estimatedBridgeAmount, + ); + + const rawOoWei = nativeSwapWei > 0n ? nativeSwapWei : inputAmountWei; + const polOrEthToOo = rawOoWei <= inputAmountWei ? rawOoWei : inputAmountWei; + + const oftSendData = buildOftSendCalldata(nativeFeeWithBuffer, signerAddress); + + const txValue = inputAmountWei + nativeFeeWithBuffer; + const callData = routerIface.encodeFunctionData( + 'swapAndBridge', + swapAndBridgeArgs( + ZERO_BYTES32, + FLAGS, + { user: signerAddress, inputToken: NATIVE_TOKEN_ADDRESS, inputAmount: inputAmountWei }, + { receiver: signerAddress, amount: feeAmount }, + { + target: ooRouter, + approvalSpender: ZERO_ADDRESS, + outputToken: TOKENS.USDT0_POLYGON, + value: polOrEthToOo, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + swapData, + { target: USDT0_OFT_ADAPTER_POLYGON, approvalSpender: bridgeApprovalSpender, value: nativeFeeWithBuffer }, + oftSendData, + ), + ); + + // Native input — no ERC-20 allowance needed for AH; pass NATIVE_TOKEN_ADDRESS + const receipt = await execViaAH(signer, ROUTER_POLYGON, NATIVE_TOKEN_ADDRESS, inputAmountWei, ROUTER_POLYGON, callData, txValue); + + logTxnSummary( + 'Polygon POL → USDT0 (OO) → Arbitrum USDT0 (OFT) — performExecution postFee', + CHAIN_IDS.POLYGON, + receipt, + ); + + console.log('\nUSDT0 arrives on Arbitrum once LZ delivers the message.'); + + void amountReceivedLD; +} + +main().catch((err) => { console.error(err); process.exit(1); }); diff --git a/scripts/e2e/stargate/polygonPolUsdt0Arb.performModularExecution.postFee.ts b/scripts/e2e/stargate/polygonPolUsdt0Arb.performModularExecution.postFee.ts new file mode 100644 index 0000000..d46436b --- /dev/null +++ b/scripts/e2e/stargate/polygonPolUsdt0Arb.performModularExecution.postFee.ts @@ -0,0 +1,218 @@ +/** + * Route: Polygon POL (native) → USDT0 (OpenOcean) → Arbitrum USDT0 (LZ OFT Adapter) + * Function: performActions (modular) + * Fee: postFee — FEE_BPS of estimatedOut USDT0 transferred to signer after swap + * + * Input is native POL; msg.value = ooSwapNativeWei + nativeFeeWithBuffer. + * + * Modular action sequence: + * [0] nativeCall(ooRouter, swapData, polOrEthToOo) — POL → USDT0 lands in router + * [1] USDT0.transfer(signer, feeAmount) + * [2] USDT0.approve(adapter, MaxUint256) + * [3] STATICCALL USDT0.balanceOf(router) + * [4] nativeCall(adapter, oftSendData, nativeFeeWithBuffer) — splicePayloadWord(196) from [3] + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/stargate/polygonPolUsdt0Arb.performActions.postFee.ts + */ +import axios from 'axios'; +import { ethers, parseEther } from 'ethers'; +import * as dotenv from 'dotenv'; +import { Options } from '@layerzerolabs/lz-v2-utilities'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, + NATIVE_TOKEN_ADDRESS, + USDT0_OFT_ADAPTER_POLYGON, + ARBITRUM_LZ_EID, + STARGATE_AMOUNT_LD_OFFSET, +} from '../config'; +import { execViaAH } from '../utils/allowanceHolder'; +import { encodeTransfer, encodeBalanceOf } from '../utils/erc20'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { ModularActionsBuilder } from '../utils/modularActionsBuilder/index'; +import { ZERO_BYTES32 } from '../utils/contractTypes'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterErc20Balance, ensureRouterNativeBalance } from '../utils/reproducibility'; +import { modularApproveIfNeeded } from '../utils/routerAllowance'; + +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); +const LZ_EXTRA_OPTIONS = Options.newOptions().addExecutorLzReceiveOption(65000, 0).toHex(); +const NATIVE_INPUT_GAS_RESERVE = parseEther('0.01'); +const NATIVE_INPUT_GAS_LIMIT_ESTIMATE = 2_000_000n; + +const OFT_ABI = [ + 'function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)', + 'function quoteOFT(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) external view returns (tuple(uint256 minAmountLD, uint256 maxAmountLD) oftLimit, tuple(int256 feeAmountLD, string description)[] oftFeeDetails, tuple(uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt)', + 'function send(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee, address refundAddress) external payable', +]; +const OFT_IFACE = new ethers.Interface(OFT_ABI); + +interface OoQuoteResponse { + data: { to: string; data: string; value?: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmountWei: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; + nativeSwapWei: bigint; +}> { + const params: Record = { + inTokenAddress: NATIVE_TOKEN_ADDRESS, + outTokenAddress: TOKENS.USDT0_POLYGON, + amount: ethers.formatEther(inputAmountWei), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_POLYGON, + account: ROUTER_POLYGON, + gasPrice: '1', + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.POLYGON}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + nativeSwapWei: q.value !== undefined && q.value !== '' ? BigInt(q.value) : 0n, + }; +} + +async function fetchOftQuote( + provider: ethers.JsonRpcProvider, + bridgeAmountLD: bigint, + recipient: string, +): Promise<{ nativeFeeWithBuffer: bigint; amountReceivedLD: bigint }> { + const contract = new ethers.Contract(USDT0_OFT_ADAPTER_POLYGON, OFT_ABI, provider); + const to32 = ethers.zeroPadValue(recipient, 32); + const sendParam = { dstEid: ARBITRUM_LZ_EID, to: to32, amountLD: bridgeAmountLD, minAmountLD: 0n, extraOptions: LZ_EXTRA_OPTIONS, composeMsg: '0x', oftCmd: '0x' }; + const [fee, oft] = await Promise.all([contract.quoteSend(sendParam, false), contract.quoteOFT(sendParam)]); + return { + nativeFeeWithBuffer: ((fee.nativeFee as bigint) * 105n) / 100n, + amountReceivedLD: oft.oftReceipt.amountReceivedLD as bigint, + }; +} + +function buildOftSendCalldata(nativeFee: bigint, recipient: string): string { + return OFT_IFACE.encodeFunctionData('send', [ + { dstEid: ARBITRUM_LZ_EID, to: ethers.zeroPadValue(recipient, 32), amountLD: 0n, minAmountLD: 0n, extraOptions: LZ_EXTRA_OPTIONS, composeMsg: '0x', oftCmd: '0x' }, + { nativeFee, lzTokenFee: 0n }, + recipient, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error('PRIVATE_KEY env var required'); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const rawBalance = await provider.getBalance(signerAddress); + if (rawBalance <= NATIVE_INPUT_GAS_RESERVE) { + throw new Error(`Signer ${signerAddress} POL balance (${ethers.formatEther(rawBalance)}) below reserve`); + } + + const feeData = await provider.getFeeData(); + const maxFeePerGas = feeData.maxFeePerGas ?? feeData.gasPrice ?? 500_000_000n; + const gasReserve = maxFeePerGas * NATIVE_INPUT_GAS_LIMIT_ESTIMATE; + console.log(` Gas reserve: ${ethers.formatEther(gasReserve)} POL`); + + let inputAmountWei = rawBalance - NATIVE_INPUT_GAS_RESERVE - 20n; + if (inputAmountWei <= 0n) throw new Error('POL balance too small'); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`POL balance: ${ethers.formatEther(rawBalance)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + let ooRouter = ''; + let swapData = ''; + let nativeSwapWei = 0n; + let feeAmount = 0n; + let estimatedBridgeAmount = 0n; + let nativeFeeWithBuffer = 0n; + let amountReceivedLD = 0n; + + for (let iter = 0; iter < 6; iter++) { + console.log('Fetching OpenOcean quote (POL → USDT0)...'); + const q = await fetchOpenOceanQuote(inputAmountWei); + ooRouter = q.ooRouter; + swapData = q.swapData; + nativeSwapWei = q.nativeSwapWei; + feeAmount = bpsOf(q.estimatedOut, FEE_BPS); + estimatedBridgeAmount = q.estimatedOut - feeAmount; + console.log(` OO router: ${ooRouter}`); + console.log(` Est. USDT0: ${ethers.formatUnits(q.estimatedOut, 6)}`); + console.log(` Post-fee: ${ethers.formatUnits(feeAmount, 6)} USDT0`); + + console.log('Fetching OFT quote (Polygon → Arbitrum)...'); + ({ nativeFeeWithBuffer, amountReceivedLD } = await fetchOftQuote(provider, estimatedBridgeAmount, signerAddress)); + console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} POL`); + console.log(` Est. received: ${ethers.formatUnits(amountReceivedLD, 6)} USDT0`); + + const maxAffordable = rawBalance - nativeFeeWithBuffer - gasReserve; + if (maxAffordable <= 0n) { + throw new Error(`POL balance cannot cover lz fee (${ethers.formatEther(nativeFeeWithBuffer)}) + gas reserve`); + } + if (inputAmountWei <= maxAffordable) { + break; + } + console.warn(` Capping swap input from ${ethers.formatEther(inputAmountWei)} to ${ethers.formatEther(maxAffordable)} POL`); + inputAmountWei = maxAffordable; + } + + await ensureRouterErc20Balance(signer, TOKENS.USDT0_POLYGON, ROUTER_POLYGON); + await ensureRouterNativeBalance(signer, ROUTER_POLYGON); + + const rawOoWei = nativeSwapWei > 0n ? nativeSwapWei : inputAmountWei; + const polOrEthToOo = rawOoWei <= inputAmountWei ? rawOoWei : inputAmountWei; + + const oftSendData = buildOftSendCalldata(nativeFeeWithBuffer, signerAddress); + + const exec = new ModularActionsBuilder(); + exec.nativeCall(ooRouter, swapData, polOrEthToOo); + exec.call(TOKENS.USDT0_POLYGON, encodeTransfer(signerAddress, feeAmount)); + await modularApproveIfNeeded( + exec, + provider, + ROUTER_POLYGON, + TOKENS.USDT0_POLYGON, + USDT0_OFT_ADAPTER_POLYGON, + estimatedBridgeAmount, + ethers.MaxUint256, + ); + const usdt0Balance = exec.staticCall(TOKENS.USDT0_POLYGON, encodeBalanceOf(ROUTER_POLYGON)); + exec.nativeCall(USDT0_OFT_ADAPTER_POLYGON, oftSendData, nativeFeeWithBuffer) + .splicePayloadWord(BigInt(STARGATE_AMOUNT_LD_OFFSET), usdt0Balance.returnWord()); + + const txValue = inputAmountWei + nativeFeeWithBuffer; + const callData = routerIface.encodeFunctionData('performActions', [ZERO_BYTES32, exec.toActions()]); + + const receipt = await execViaAH(signer, ROUTER_POLYGON, NATIVE_TOKEN_ADDRESS, inputAmountWei, ROUTER_POLYGON, callData, txValue); + + logTxnSummary( + 'Polygon POL → USDT0 (OO) → Arbitrum USDT0 (OFT) — performActions postFee', + CHAIN_IDS.POLYGON, + receipt, + ); + + console.log('\nUSDT0 arrives on Arbitrum once LZ delivers the message.'); + + void amountReceivedLD; +} + +main().catch((err) => { console.error(err); process.exit(1); }); diff --git a/scripts/e2e/stargate/polygonUsdcBase.bridge.preFee.ts b/scripts/e2e/stargate/polygonUsdcBase.bridge.preFee.ts new file mode 100644 index 0000000..c2f6c2b --- /dev/null +++ b/scripts/e2e/stargate/polygonUsdcBase.bridge.preFee.ts @@ -0,0 +1,166 @@ +/** + * Route: Polygon USDC → Base USDC (Stargate USDC Pool, no swap) + * Function: bridge (simple bridge entrypoint) + * Fee: preFee — FEE_BPS of inputAmount USDC deducted before bridge + * + * Bridge amount is pre-encoded in Stargate send() calldata (no splice needed). + * bridge.value = nativeFeeWithBuffer forwarded as LZ msg.value. + * Uses router.bridge() rather than performExecution / performActions. + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/stargate/polygonUsdcBase.bridge.preFee.ts + */ +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + STARGATE_USDC_POLYGON, + BASE_LZ_EID, +} from '../config'; +import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; +import { getWalletErc20Balance } from '../utils/erc20'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { ZERO_BYTES32, type BridgeData, type FeeData, type InputData } from '../utils/contractTypes'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterErc20Balance } from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; + +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); + +const STARGATE_ABI = [ + 'function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)', + 'function quoteOFT(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) external view returns (tuple(uint256 minAmountLD, uint256 maxAmountLD) oftLimit, tuple(int256 feeAmountLD, string description)[] oftFeeDetails, tuple(uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt)', + 'function send(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee, address refundAddress) external payable', +]; +const STARGATE_IFACE = new ethers.Interface(STARGATE_ABI); + +async function fetchStargateQuote( + provider: ethers.JsonRpcProvider, + bridgeAmountLD: bigint, + recipient: string, +): Promise<{ nativeFeeWithBuffer: bigint; amountReceivedLD: bigint }> { + const contract = new ethers.Contract(STARGATE_USDC_POLYGON, STARGATE_ABI, provider); + const to32 = ethers.zeroPadValue(recipient, 32); + const sendParam = { + dstEid: BASE_LZ_EID, + to: to32, + amountLD: bridgeAmountLD, + minAmountLD: 0n, + extraOptions: '0x', + composeMsg: '0x', + oftCmd: '0x', + }; + const [fee, oft] = await Promise.all([ + contract.quoteSend(sendParam, false), + contract.quoteOFT(sendParam), + ]); + return { + nativeFeeWithBuffer: ((fee.nativeFee as bigint) * 105n) / 100n, + amountReceivedLD: oft.oftReceipt.amountReceivedLD as bigint, + }; +} + +/** + * Builds Stargate send() calldata with the exact bridgeAmount pre-encoded (no 0 placeholder). + * Used by router.bridge() which has no splice mechanism. + */ +function buildStargateCalldata(bridgeAmount: bigint, nativeFee: bigint, recipient: string): string { + return STARGATE_IFACE.encodeFunctionData('send', [ + { + dstEid: BASE_LZ_EID, + to: ethers.zeroPadValue(recipient, 32), + amountLD: bridgeAmount, + minAmountLD: 0n, + extraOptions: '0x', + composeMsg: '0x', + oftCmd: '0x', + }, + { nativeFee, lzTokenFee: 0n }, + recipient, + ]); +} + +async function main(): Promise { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) { + throw new Error('PRIVATE_KEY env var required'); + } + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const inputToken = TOKENS.USDC_POLYGON_CIRCLE; + const { balance: walletBalance } = await getWalletErc20Balance(inputToken, signerAddress, provider); + if (walletBalance === 0n) { + throw new Error(`Signer ${signerAddress} has zero USDC on Polygon`); + } + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error('Balance too small'); + + const feeAmount = bpsOf(inputAmount, FEE_BPS); + const bridgeAmount = inputAmount - feeAmount; + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`USDC balance: ${ethers.formatUnits(walletBalance, 6)}`); + console.log(`Pre-bridge fee: ${ethers.formatUnits(feeAmount, 6)} USDC (${FEE_BPS} bps)`); + console.log(`Net to bridge: ${ethers.formatUnits(bridgeAmount, 6)}`); + + console.log('Fetching Stargate quote (Polygon → Base USDC pool)...'); + const { nativeFeeWithBuffer, amountReceivedLD } = await fetchStargateQuote(provider, bridgeAmount, signerAddress); + console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} POL`); + console.log(` Est. received: ${ethers.formatUnits(amountReceivedLD, 6)} USDC`); + + const sendData = buildStargateCalldata(bridgeAmount, nativeFeeWithBuffer, signerAddress); + + const input: InputData = { user: signerAddress, inputToken, inputAmount }; + const fee: FeeData = { receiver: signerAddress, amount: feeAmount }; + const bridgeApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + inputToken, + STARGATE_USDC_POLYGON, + bridgeAmount, + ); + + const bridgeData: BridgeData = { + target: STARGATE_USDC_POLYGON, + approvalSpender: bridgeApprovalSpender, + value: nativeFeeWithBuffer, + }; + + const routerIface = new ethers.Interface(ROUTER_ABI); + const execCalldata = routerIface.encodeFunctionData('bridge', [ZERO_BYTES32, input, fee, bridgeData, sendData]); + + await ensureRouterErc20Balance(signer, inputToken, ROUTER_POLYGON); + await ensureAllowanceForAllowanceHolder(signer, inputToken, inputAmount); + + console.log('Sending AllowanceHolder.exec → router.bridge...'); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + inputToken, + inputAmount, + ROUTER_POLYGON, + execCalldata, + nativeFeeWithBuffer, + ); + + logTxnSummary('Polygon USDC → Base USDC (Stargate USDC pool) — bridge preFee', CHAIN_IDS.POLYGON, receipt); + + console.log('\nUSDC arrives on Base once LZ delivers the message.'); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/stargate/polygonUsdcBase.performExecution.postFee.ts b/scripts/e2e/stargate/polygonUsdcBase.performExecution.postFee.ts new file mode 100644 index 0000000..0009435 --- /dev/null +++ b/scripts/e2e/stargate/polygonUsdcBase.performExecution.postFee.ts @@ -0,0 +1,166 @@ +/** + * Route: Polygon USDC → Base USDC (Stargate USDC Pool, no swap) + * Function: bridge (simple bridge entrypoint) + * Fee: preFee — FEE_BPS of inputAmount USDC deducted before bridge + * + * Bridge amount is pre-encoded in Stargate send() calldata (no splice needed). + * bridge.value = nativeFeeWithBuffer forwarded as LZ msg.value. + * Uses router.bridge() rather than performExecution / performActions. + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/stargate/polygonUsdcBase.performExecution.postFee.ts + */ +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + STARGATE_USDC_POLYGON, + BASE_LZ_EID, +} from '../config'; +import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; +import { getWalletErc20Balance } from '../utils/erc20'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { ZERO_BYTES32, type BridgeData, type FeeData, type InputData } from '../utils/contractTypes'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterErc20Balance } from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; + +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); + +const STARGATE_ABI = [ + 'function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)', + 'function quoteOFT(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) external view returns (tuple(uint256 minAmountLD, uint256 maxAmountLD) oftLimit, tuple(int256 feeAmountLD, string description)[] oftFeeDetails, tuple(uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt)', + 'function send(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee, address refundAddress) external payable', +]; +const STARGATE_IFACE = new ethers.Interface(STARGATE_ABI); + +async function fetchStargateQuote( + provider: ethers.JsonRpcProvider, + bridgeAmountLD: bigint, + recipient: string, +): Promise<{ nativeFeeWithBuffer: bigint; amountReceivedLD: bigint }> { + const contract = new ethers.Contract(STARGATE_USDC_POLYGON, STARGATE_ABI, provider); + const to32 = ethers.zeroPadValue(recipient, 32); + const sendParam = { + dstEid: BASE_LZ_EID, + to: to32, + amountLD: bridgeAmountLD, + minAmountLD: 0n, + extraOptions: '0x', + composeMsg: '0x', + oftCmd: '0x', + }; + const [fee, oft] = await Promise.all([ + contract.quoteSend(sendParam, false), + contract.quoteOFT(sendParam), + ]); + return { + nativeFeeWithBuffer: ((fee.nativeFee as bigint) * 105n) / 100n, + amountReceivedLD: oft.oftReceipt.amountReceivedLD as bigint, + }; +} + +/** + * Builds Stargate send() calldata with the exact bridgeAmount pre-encoded (no 0 placeholder). + * Used by router.bridge() which has no splice mechanism. + */ +function buildStargateCalldata(bridgeAmount: bigint, nativeFee: bigint, recipient: string): string { + return STARGATE_IFACE.encodeFunctionData('send', [ + { + dstEid: BASE_LZ_EID, + to: ethers.zeroPadValue(recipient, 32), + amountLD: bridgeAmount, + minAmountLD: 0n, + extraOptions: '0x', + composeMsg: '0x', + oftCmd: '0x', + }, + { nativeFee, lzTokenFee: 0n }, + recipient, + ]); +} + +async function main(): Promise { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) { + throw new Error('PRIVATE_KEY env var required'); + } + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const inputToken = TOKENS.USDC_POLYGON_CIRCLE; + const { balance: walletBalance } = await getWalletErc20Balance(inputToken, signerAddress, provider); + if (walletBalance === 0n) { + throw new Error(`Signer ${signerAddress} has zero USDC on Polygon`); + } + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error('Balance too small'); + + const feeAmount = bpsOf(inputAmount, FEE_BPS); + const bridgeAmount = inputAmount - feeAmount; + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`USDC balance: ${ethers.formatUnits(walletBalance, 6)}`); + console.log(`Pre-bridge fee: ${ethers.formatUnits(feeAmount, 6)} USDC (${FEE_BPS} bps)`); + console.log(`Net to bridge: ${ethers.formatUnits(bridgeAmount, 6)}`); + + console.log('Fetching Stargate quote (Polygon → Base USDC pool)...'); + const { nativeFeeWithBuffer, amountReceivedLD } = await fetchStargateQuote(provider, bridgeAmount, signerAddress); + console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} POL`); + console.log(` Est. received: ${ethers.formatUnits(amountReceivedLD, 6)} USDC`); + + const sendData = buildStargateCalldata(bridgeAmount, nativeFeeWithBuffer, signerAddress); + + const input: InputData = { user: signerAddress, inputToken, inputAmount }; + const fee: FeeData = { receiver: signerAddress, amount: feeAmount }; + const bridgeApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + inputToken, + STARGATE_USDC_POLYGON, + bridgeAmount, + ); + + const bridgeData: BridgeData = { + target: STARGATE_USDC_POLYGON, + approvalSpender: bridgeApprovalSpender, + value: nativeFeeWithBuffer, + }; + + const routerIface = new ethers.Interface(ROUTER_ABI); + const execCalldata = routerIface.encodeFunctionData('bridge', [ZERO_BYTES32, input, fee, bridgeData, sendData]); + + await ensureRouterErc20Balance(signer, inputToken, ROUTER_POLYGON); + await ensureAllowanceForAllowanceHolder(signer, inputToken, inputAmount); + + console.log('Sending AllowanceHolder.exec → router.bridge...'); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + inputToken, + inputAmount, + ROUTER_POLYGON, + execCalldata, + nativeFeeWithBuffer, + ); + + logTxnSummary('Polygon USDC → Base USDC (Stargate USDC pool) — bridge preFee', CHAIN_IDS.POLYGON, receipt); + + console.log('\nUSDC arrives on Base once LZ delivers the message.'); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/stargate/polygonUsdcBase.performModularExecution.postFee.ts b/scripts/e2e/stargate/polygonUsdcBase.performModularExecution.postFee.ts new file mode 100644 index 0000000..ebb355a --- /dev/null +++ b/scripts/e2e/stargate/polygonUsdcBase.performModularExecution.postFee.ts @@ -0,0 +1,141 @@ +/** + * Route: Polygon USDC → Base USDC (Stargate USDC Pool, no swap) + * Function: performActions (modular) + * Fee: postFee — FEE_BPS of inputAmount USDC transferred to signer; staticCall balance spliced + * into Stargate amountLD at STARGATE_AMOUNT_LD_OFFSET (byte 196). + * + * Modular action sequence: + * [0] AH.transferFrom(USDC, signer, router, inputAmount) + * [1] USDC.transfer(signer, feeAmount) + * [2] USDC.approve(stargatePool, MaxUint256) + * [3] STATICCALL USDC.balanceOf(router) + * [4] nativeCall(Stargate, sendData, nativeFeeWithBuffer) — splicePayloadWord(196) from [3] + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/stargate/polygonUsdcBase.performActions.postFee.ts + */ +import { ethers } from 'ethers'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + ALLOWANCE_HOLDER, + STARGATE_USDC_POLYGON, + BASE_LZ_EID, + STARGATE_AMOUNT_LD_OFFSET, +} from '../config'; +import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; +import { encodeTransfer, encodeBalanceOf, getWalletErc20Balance } from '../utils/erc20'; +import { ROUTER_ABI } from '../utils/routerAbi'; +import { ModularActionsBuilder } from '../utils/modularActionsBuilder/index'; +import { ZERO_BYTES32 } from '../utils/contractTypes'; +import { logTxnSummary } from '../utils/txnLogSummary'; +import { ensureRouterErc20Balance } from '../utils/reproducibility'; +import { modularApproveIfNeeded } from '../utils/routerAllowance'; + +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); + +const STARGATE_ABI = [ + 'function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)', + 'function quoteOFT(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) external view returns (tuple(uint256 minAmountLD, uint256 maxAmountLD) oftLimit, tuple(int256 feeAmountLD, string description)[] oftFeeDetails, tuple(uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt)', + 'function send(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee, address refundAddress) external payable', +]; +const STARGATE_IFACE = new ethers.Interface(STARGATE_ABI); + +async function fetchStargateQuote( + provider: ethers.JsonRpcProvider, + bridgeAmountLD: bigint, + recipient: string, +): Promise<{ nativeFeeWithBuffer: bigint; amountReceivedLD: bigint }> { + const contract = new ethers.Contract(STARGATE_USDC_POLYGON, STARGATE_ABI, provider); + const to32 = ethers.zeroPadValue(recipient, 32); + const sendParam = { dstEid: BASE_LZ_EID, to: to32, amountLD: bridgeAmountLD, minAmountLD: 0n, extraOptions: '0x', composeMsg: '0x', oftCmd: '0x' }; + const [fee, oft] = await Promise.all([contract.quoteSend(sendParam, false), contract.quoteOFT(sendParam)]); + return { + nativeFeeWithBuffer: ((fee.nativeFee as bigint) * 105n) / 100n, + amountReceivedLD: oft.oftReceipt.amountReceivedLD as bigint, + }; +} + +function buildStargateCalldata(nativeFee: bigint, recipient: string): string { + return STARGATE_IFACE.encodeFunctionData('send', [ + { dstEid: BASE_LZ_EID, to: ethers.zeroPadValue(recipient, 32), amountLD: 0n, minAmountLD: 0n, extraOptions: '0x', composeMsg: '0x', oftCmd: '0x' }, + { nativeFee, lzTokenFee: 0n }, + recipient, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error('PRIVATE_KEY env var required'); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance(TOKENS.USDC_POLYGON_CIRCLE, signerAddress, provider); + if (walletBalance === 0n) throw new Error(`Signer ${signerAddress} has zero USDC on Polygon`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error('Balance too small'); + + const feeAmount = bpsOf(inputAmount, FEE_BPS); + const estimatedBridgeAmount = inputAmount - feeAmount; + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`USDC balance: ${ethers.formatUnits(walletBalance, 6)}`); + console.log(`Post-fee: ${ethers.formatUnits(feeAmount, 6)} USDC (${FEE_BPS} bps)`); + console.log(`Est. bridge: ${ethers.formatUnits(estimatedBridgeAmount, 6)} USDC`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log('Fetching Stargate quote (Polygon → Base USDC pool)...'); + const { nativeFeeWithBuffer, amountReceivedLD } = await fetchStargateQuote(provider, estimatedBridgeAmount, signerAddress); + console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} POL`); + console.log(` Est. received: ${ethers.formatUnits(amountReceivedLD, 6)} USDC`); + + await ensureRouterErc20Balance(signer, TOKENS.USDC_POLYGON_CIRCLE, ROUTER_POLYGON); + + const stargateData = buildStargateCalldata(nativeFeeWithBuffer, signerAddress); + + const ahIface = new ethers.Interface([ + 'function transferFrom(address token, address owner, address recipient, uint256 amount)', + ]); + const exec = new ModularActionsBuilder(); + exec.call(ALLOWANCE_HOLDER, ahIface.encodeFunctionData('transferFrom', [TOKENS.USDC_POLYGON_CIRCLE, signerAddress, ROUTER_POLYGON, inputAmount])); + exec.call(TOKENS.USDC_POLYGON_CIRCLE, encodeTransfer(signerAddress, feeAmount)); + await modularApproveIfNeeded( + exec, + provider, + ROUTER_POLYGON, + TOKENS.USDC_POLYGON_CIRCLE, + STARGATE_USDC_POLYGON, + estimatedBridgeAmount, + ethers.MaxUint256, + ); + const usdcBalance = exec.staticCall(TOKENS.USDC_POLYGON_CIRCLE, encodeBalanceOf(ROUTER_POLYGON)); + exec.nativeCall(STARGATE_USDC_POLYGON, stargateData, nativeFeeWithBuffer) + .splicePayloadWord(BigInt(STARGATE_AMOUNT_LD_OFFSET), usdcBalance.returnWord()); + + const callData = routerIface.encodeFunctionData('performActions', [ZERO_BYTES32, exec.toActions()]); + + await ensureAllowanceForAllowanceHolder(signer, TOKENS.USDC_POLYGON_CIRCLE, inputAmount); + const receipt = await execViaAH(signer, ROUTER_POLYGON, TOKENS.USDC_POLYGON_CIRCLE, inputAmount, ROUTER_POLYGON, callData, nativeFeeWithBuffer); + + logTxnSummary( + 'Polygon USDC → Base USDC (Stargate USDC pool) — performActions postFee', + CHAIN_IDS.POLYGON, + receipt, + ); + + console.log('\nUSDC arrives on Base once LZ delivers the message.'); +} + +main().catch((err) => { console.error(err); process.exit(1); }); diff --git a/scripts/e2e/stargate/swapAndBridge.postFee.balanceOf.ts b/scripts/e2e/stargate/swapAndBridge.postFee.balanceOf.ts new file mode 100644 index 0000000..8be7ccb --- /dev/null +++ b/scripts/e2e/stargate/swapAndBridge.postFee.balanceOf.ts @@ -0,0 +1,269 @@ +/** + * Route: Base USDC → native ETH (OpenOcean) → Arbitrum ETH (Stargate Native Pool on Base, LayerZero v2) + * Flags: post-fee (fee taken from ETH output after swap), output measured as ETH balanceOf delta + * bridge-value + bridge-amount-position flags: router splices finalETH into amountLD and + * forwards finalETH + nativeFeeWithBuffer as msg.value to Stargate + * + * Post-fee (bit0=1): feeAmount = FEE_BPS of estimatedOut ETH, deducted from swap output. + * BalanceOf (bit1=1): final ETH amount is measured as router ETH balance change (not returndata). + * BridgeValue (bit2=1): router forwards finalETH + nativeFeeWithBuffer as msg.value to Stargate. + * BridgeAmountPosition (bit3=1): router splices finalETH into amountLD at STARGATE_AMOUNT_LD_OFFSET. + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/stargate/swapAndBridge.postFee.balanceOf.ts + */ +import axios from "axios"; +import { ethers } from "ethers"; +import * as dotenv from "dotenv"; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + NATIVE_TOKEN_ADDRESS, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, + STARGATE_NATIVE_BASE, + ARBITRUM_LZ_EID, +} from "../config"; +import { + execViaAH, + ensureAllowanceForAllowanceHolder, +} from "../utils/allowanceHolder"; +import { getWalletErc20Balance } from "../utils/erc20"; +import { ROUTER_ABI } from "../utils/routerAbi"; +import { + ZERO_BYTES32, + BRIDGE_VALUE_FLAG, + ZERO_ADDRESS, + bridgeAmountPositionFlag, + swapAndBridgeArgs, +} from "../utils/contractTypes"; +import { STARGATE_AMOUNT_LD_OFFSET } from "../config"; +import { logTxnSummary } from "../utils/txnLogSummary"; +import { + ensureRouterErc20Balance, + ensureRouterNativeBalance, +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; + +// post-fee (0x01) | balance-of (0x02) | bridge-value (0x04) | bridge-amount-position (0x08 + offset) +const FLAGS = 0x03n | BRIDGE_VALUE_FLAG | bridgeAmountPositionFlag(STARGATE_AMOUNT_LD_OFFSET); +const ROUTER_BASE = routerAddressForChain(CHAIN_IDS.BASE); + +const STARGATE_ABI = [ + "function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)", + "function quoteOFT(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) external view returns (tuple(uint256 minAmountLD, uint256 maxAmountLD) oftLimit, tuple(int256 feeAmountLD, string description)[] oftFeeDetails, tuple(uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt)", + "function send(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee, address refundAddress) external payable", +]; + +const STARGATE_IFACE = new ethers.Interface(STARGATE_ABI); + +interface OoQuoteResponse { + data: { to: string; data: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.USDC_BASE, + outTokenAddress: NATIVE_TOKEN_ADDRESS, + amount: ethers.formatUnits(inputAmount, 6), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_BASE, + account: ROUTER_BASE, + gasPrice: "1", + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.BASE}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + }; +} + +async function fetchStargateQuote( + provider: ethers.JsonRpcProvider, + bridgeAmountLD: bigint, + recipient: string +): Promise<{ nativeFee: bigint; amountReceivedLD: bigint }> { + const contract = new ethers.Contract( + STARGATE_NATIVE_BASE, + STARGATE_ABI, + provider + ); + const to32 = ethers.zeroPadValue(recipient, 32); + const sendParam = { + dstEid: ARBITRUM_LZ_EID, + to: to32, + amountLD: bridgeAmountLD, + minAmountLD: 0n, + extraOptions: "0x", + composeMsg: "0x", + oftCmd: "0x", + }; + const [fee, oft] = await Promise.all([ + contract.quoteSend(sendParam, false), + contract.quoteOFT(sendParam), + ]); + return { + nativeFee: fee.nativeFee as bigint, + amountReceivedLD: oft.oftReceipt.amountReceivedLD as bigint, + }; +} + +function buildStargateCalldata( + nativeFee: bigint, + recipient: string, + amountLD: bigint +): string { + return STARGATE_IFACE.encodeFunctionData("send", [ + { + dstEid: ARBITRUM_LZ_EID, + to: ethers.zeroPadValue(recipient, 32), + amountLD, + minAmountLD: 0n, + extraOptions: "0x", + composeMsg: "0x", + oftCmd: "0x", + }, + { nativeFee, lzTokenFee: 0n }, + recipient, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error("PRIVATE_KEY env var required"); + + const provider = new ethers.JsonRpcProvider(RPC.BASE); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.USDC_BASE, + signerAddress, + provider + ); + if (walletBalance === 0n) + throw new Error(`Signer ${signerAddress} has zero USDC on Base`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error("Balance too small"); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_BASE}`); + console.log( + `Flags: 0x${FLAGS.toString(16)} (post-fee, balanceOf, bridge-value)` + ); + console.log(`USDC balance: ${ethers.formatUnits(walletBalance, 6)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log("Fetching OpenOcean quote (USDC → ETH)..."); + const { ooRouter, swapData, estimatedOut, minAmountOut } = + await fetchOpenOceanQuote(inputAmount); + + // post-fee: deduct from estimated ETH output after swap + const feeAmount = bpsOf(estimatedOut, FEE_BPS); + const bridgeEstimate = estimatedOut - feeAmount; + console.log(` OO router: ${ooRouter}`); + console.log(` Est. ETH: ${ethers.formatEther(estimatedOut)}`); + console.log( + ` Post-fee: ${ethers.formatEther(feeAmount)} ETH (${FEE_BPS} bps)` + ); + console.log(` Bridge est: ${ethers.formatEther(bridgeEstimate)}`); + console.log(` Min ETH: ${ethers.formatEther(minAmountOut)}`); + + console.log("Fetching Stargate quote (Base native pool → Arbitrum)..."); + const { nativeFee, amountReceivedLD } = await fetchStargateQuote( + provider, + bridgeEstimate, + signerAddress + ); + const nativeFeeWithBuffer = (nativeFee * 105n) / 100n; + console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} ETH`); + console.log(` Est. received: ${ethers.formatEther(amountReceivedLD)} ETH`); + + // bridgeEstimate is a placeholder for amountLD; router splices the actual finalETH at runtime + const amountLD = bridgeEstimate; + + await ensureRouterErc20Balance(signer, TOKENS.USDC_BASE, ROUTER_BASE); + await ensureRouterNativeBalance(signer, ROUTER_BASE); + + const swapApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_BASE, + TOKENS.USDC_BASE, + ooRouter, + inputAmount, + ); + + const stargateData = buildStargateCalldata( + nativeFeeWithBuffer, + signerAddress, + amountLD + ); + + const callData = routerIface.encodeFunctionData("swapAndBridge", swapAndBridgeArgs( + ZERO_BYTES32, + FLAGS, + { + user: signerAddress, + inputToken: TOKENS.USDC_BASE, + inputAmount: inputAmount, + }, + { receiver: signerAddress, amount: feeAmount }, + { + target: ooRouter, + approvalSpender: swapApprovalSpender, + outputToken: NATIVE_TOKEN_ADDRESS, + value: 0n, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + swapData, + { + target: STARGATE_NATIVE_BASE, + approvalSpender: ZERO_ADDRESS, + value: nativeFeeWithBuffer, + }, + stargateData, + )); + + await ensureAllowanceForAllowanceHolder(signer, TOKENS.USDC_BASE, inputAmount); + const receipt = await execViaAH( + signer, + ROUTER_BASE, + TOKENS.USDC_BASE, + inputAmount, + ROUTER_BASE, + callData, + nativeFeeWithBuffer + ); + + logTxnSummary( + `Base USDC → Arbitrum ETH (Stargate) — swapAndBridge postFee/balanceOf`, + CHAIN_IDS.BASE, + receipt + ); + + console.log("\nETH arrives on Arbitrum once LZ delivers the message."); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/stargate/swapAndBridge.postFee.returndata.ts b/scripts/e2e/stargate/swapAndBridge.postFee.returndata.ts new file mode 100644 index 0000000..ba1c483 --- /dev/null +++ b/scripts/e2e/stargate/swapAndBridge.postFee.returndata.ts @@ -0,0 +1,270 @@ +/** + * Route: Base USDC → native ETH (OpenOcean) → Arbitrum ETH (Stargate Native Pool on Base, LayerZero v2) + * Flags: post-fee (fee taken from ETH output after swap), output read from swap returndata word 0 + * bridge-value + bridge-amount-position flags: router splices finalETH into amountLD and + * forwards finalETH + nativeFeeWithBuffer as msg.value to Stargate + * + * Post-fee (bit0=1): feeAmount = FEE_BPS of estimatedOut ETH, deducted from swap output. + * Returndata (bit1=0): final ETH amount is read from word 0 of the swap call returndata. + * BridgeValue (bit2=1): router forwards finalETH + nativeFeeWithBuffer as msg.value to Stargate. + * BridgeAmountPosition (bit3=1): router splices finalETH into amountLD at STARGATE_AMOUNT_LD_OFFSET. + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/stargate/swapAndBridge.postFee.returndata.ts + */ +import axios from "axios"; +import { ethers } from "ethers"; +import * as dotenv from "dotenv"; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + NATIVE_TOKEN_ADDRESS, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, + STARGATE_NATIVE_BASE, + ARBITRUM_LZ_EID, +} from "../config"; +import { + execViaAH, + ensureAllowanceForAllowanceHolder, +} from "../utils/allowanceHolder"; +import { getWalletErc20Balance } from "../utils/erc20"; +import { ROUTER_ABI } from "../utils/routerAbi"; +import { + ZERO_BYTES32, + BRIDGE_VALUE_FLAG, + ZERO_ADDRESS, + bridgeAmountPositionFlag, + swapAndBridgeArgs, +} from "../utils/contractTypes"; +import { STARGATE_AMOUNT_LD_OFFSET } from "../config"; +import { logTxnSummary } from "../utils/txnLogSummary"; +import { + ensureRouterErc20Balance, + ensureRouterNativeBalance, +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; + +// post-fee (0x01) | bridge-value (0x04) | bridge-amount-position (0x08 + offset): splice finalETH into amountLD + forward with nativeFee +const FLAGS = 0x01n | BRIDGE_VALUE_FLAG | bridgeAmountPositionFlag(STARGATE_AMOUNT_LD_OFFSET); +const ROUTER_BASE = routerAddressForChain(CHAIN_IDS.BASE); + +const STARGATE_ABI = [ + "function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)", + "function quoteOFT(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) external view returns (tuple(uint256 minAmountLD, uint256 maxAmountLD) oftLimit, tuple(int256 feeAmountLD, string description)[] oftFeeDetails, tuple(uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt)", + "function send(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee, address refundAddress) external payable", +]; + +const STARGATE_IFACE = new ethers.Interface(STARGATE_ABI); + +interface OoQuoteResponse { + data: { to: string; data: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.USDC_BASE, + outTokenAddress: NATIVE_TOKEN_ADDRESS, + amount: ethers.formatUnits(inputAmount, 6), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_BASE, + account: ROUTER_BASE, + gasPrice: "1", + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.BASE}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + }; +} + +async function fetchStargateQuote( + provider: ethers.JsonRpcProvider, + bridgeAmountLD: bigint, + recipient: string +): Promise<{ nativeFee: bigint; amountReceivedLD: bigint }> { + const contract = new ethers.Contract( + STARGATE_NATIVE_BASE, + STARGATE_ABI, + provider + ); + const to32 = ethers.zeroPadValue(recipient, 32); + const sendParam = { + dstEid: ARBITRUM_LZ_EID, + to: to32, + amountLD: bridgeAmountLD, + minAmountLD: 0n, + extraOptions: "0x", + composeMsg: "0x", + oftCmd: "0x", + }; + const [fee, oft] = await Promise.all([ + contract.quoteSend(sendParam, false), + contract.quoteOFT(sendParam), + ]); + return { + nativeFee: fee.nativeFee as bigint, + amountReceivedLD: oft.oftReceipt.amountReceivedLD as bigint, + }; +} + +function buildStargateCalldata( + nativeFee: bigint, + recipient: string, + amountLD: bigint +): string { + return STARGATE_IFACE.encodeFunctionData("send", [ + { + dstEid: ARBITRUM_LZ_EID, + to: ethers.zeroPadValue(recipient, 32), + amountLD, + minAmountLD: 0n, + extraOptions: "0x", + composeMsg: "0x", + oftCmd: "0x", + }, + { nativeFee, lzTokenFee: 0n }, + recipient, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error("PRIVATE_KEY env var required"); + + const provider = new ethers.JsonRpcProvider(RPC.BASE); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.USDC_BASE, + signerAddress, + provider + ); + if (walletBalance === 0n) + throw new Error(`Signer ${signerAddress} has zero USDC on Base`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error("Balance too small"); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_BASE}`); + console.log( + `Flags: 0x${FLAGS.toString( + 16 + )} (post-fee, returndata, bridge-value)` + ); + console.log(`USDC balance: ${ethers.formatUnits(walletBalance, 6)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log("Fetching OpenOcean quote (USDC → ETH)..."); + const { ooRouter, swapData, estimatedOut, minAmountOut } = + await fetchOpenOceanQuote(inputAmount); + + // post-fee: deduct from estimated ETH output after swap + const feeAmount = bpsOf(estimatedOut, FEE_BPS); + console.log(` OO router: ${ooRouter}`); + console.log(` Est. ETH: ${ethers.formatEther(estimatedOut)}`); + console.log( + ` Post-fee: ${ethers.formatEther(feeAmount)} ETH (${FEE_BPS} bps)` + ); + console.log(` Min ETH: ${ethers.formatEther(minAmountOut)}`); + + console.log("Fetching Stargate quote (Base native pool → Arbitrum)..."); + const bridgeEstimate = estimatedOut - feeAmount; + const { nativeFee, amountReceivedLD } = await fetchStargateQuote( + provider, + bridgeEstimate, + signerAddress + ); + const nativeFeeWithBuffer = (nativeFee * 105n) / 100n; + console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} ETH`); + console.log(` Est. received: ${ethers.formatEther(amountReceivedLD)} ETH`); + + // bridgeEstimate is a placeholder for amountLD; router splices the actual finalETH at runtime + const amountLD = bridgeEstimate; + + await ensureRouterErc20Balance(signer, TOKENS.USDC_BASE, ROUTER_BASE); + await ensureRouterNativeBalance(signer, ROUTER_BASE); + + const swapApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_BASE, + TOKENS.USDC_BASE, + ooRouter, + inputAmount, + ); + + const stargateData = buildStargateCalldata( + nativeFeeWithBuffer, + signerAddress, + amountLD + ); + + const callData = routerIface.encodeFunctionData("swapAndBridge", swapAndBridgeArgs( + ZERO_BYTES32, + FLAGS, + { + user: signerAddress, + inputToken: TOKENS.USDC_BASE, + inputAmount: inputAmount, + }, + { receiver: signerAddress, amount: feeAmount }, + { + target: ooRouter, + approvalSpender: swapApprovalSpender, + outputToken: NATIVE_TOKEN_ADDRESS, + value: 0n, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + swapData, + { + target: STARGATE_NATIVE_BASE, + approvalSpender: ZERO_ADDRESS, + value: nativeFeeWithBuffer, + }, + stargateData, + )); + + await ensureAllowanceForAllowanceHolder(signer, TOKENS.USDC_BASE, inputAmount); + const receipt = await execViaAH( + signer, + ROUTER_BASE, + TOKENS.USDC_BASE, + inputAmount, + ROUTER_BASE, + callData, + nativeFeeWithBuffer + ); + + logTxnSummary( + `Base USDC → Arbitrum ETH (Stargate) — swapAndBridge postFee/returndata`, + CHAIN_IDS.BASE, + receipt + ); + + console.log("\nETH arrives on Arbitrum once LZ delivers the message."); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/stargate/swapAndBridge.preFee.balanceOf.ts b/scripts/e2e/stargate/swapAndBridge.preFee.balanceOf.ts new file mode 100644 index 0000000..462ed18 --- /dev/null +++ b/scripts/e2e/stargate/swapAndBridge.preFee.balanceOf.ts @@ -0,0 +1,269 @@ +/** + * Route: Base USDC → native ETH (OpenOcean) → Arbitrum ETH (Stargate Native Pool on Base, LayerZero v2) + * Flags: pre-fee (fee taken from USDC input before swap), output measured as ETH balanceOf delta + * bridge-value + bridge-amount-position flags: router splices finalETH into amountLD and + * forwards finalETH + nativeFeeWithBuffer as msg.value to Stargate + * + * Pre-fee (bit0=0): feeAmount = FEE_BPS of inputAmount USDC, deducted before the swap. + * BalanceOf (bit1=1): final ETH amount is measured as router ETH balance change (not returndata). + * BridgeValue (bit2=1): router forwards finalETH + nativeFeeWithBuffer as msg.value to Stargate. + * BridgeAmountPosition (bit3=1): router splices finalETH into amountLD at STARGATE_AMOUNT_LD_OFFSET. + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/stargate/swapAndBridge.preFee.balanceOf.ts + */ +import axios from "axios"; +import { ethers } from "ethers"; +import * as dotenv from "dotenv"; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + NATIVE_TOKEN_ADDRESS, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, + STARGATE_NATIVE_BASE, + ARBITRUM_LZ_EID, +} from "../config"; +import { + execViaAH, + ensureAllowanceForAllowanceHolder, +} from "../utils/allowanceHolder"; +import { getWalletErc20Balance } from "../utils/erc20"; +import { ROUTER_ABI } from "../utils/routerAbi"; +import { + ZERO_BYTES32, + BRIDGE_VALUE_FLAG, + ZERO_ADDRESS, + bridgeAmountPositionFlag, + swapAndBridgeArgs, +} from "../utils/contractTypes"; +import { STARGATE_AMOUNT_LD_OFFSET } from "../config"; +import { logTxnSummary } from "../utils/txnLogSummary"; +import { + ensureRouterErc20Balance, + ensureRouterNativeBalance, +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; + +// pre-fee (0x00) | balance-of (0x02) | bridge-value (0x04) | bridge-amount-position (0x08 + offset) +const FLAGS = 0x02n | BRIDGE_VALUE_FLAG | bridgeAmountPositionFlag(STARGATE_AMOUNT_LD_OFFSET); +const ROUTER_BASE = routerAddressForChain(CHAIN_IDS.BASE); + +const STARGATE_ABI = [ + "function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)", + "function quoteOFT(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) external view returns (tuple(uint256 minAmountLD, uint256 maxAmountLD) oftLimit, tuple(int256 feeAmountLD, string description)[] oftFeeDetails, tuple(uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt)", + "function send(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee, address refundAddress) external payable", +]; + +const STARGATE_IFACE = new ethers.Interface(STARGATE_ABI); + +interface OoQuoteResponse { + data: { to: string; data: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.USDC_BASE, + outTokenAddress: NATIVE_TOKEN_ADDRESS, + amount: ethers.formatUnits(inputAmount, 6), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_BASE, + account: ROUTER_BASE, + gasPrice: "1", + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.BASE}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + }; +} + +async function fetchStargateQuote( + provider: ethers.JsonRpcProvider, + bridgeAmountLD: bigint, + recipient: string +): Promise<{ nativeFee: bigint; amountReceivedLD: bigint }> { + const contract = new ethers.Contract( + STARGATE_NATIVE_BASE, + STARGATE_ABI, + provider + ); + const to32 = ethers.zeroPadValue(recipient, 32); + const sendParam = { + dstEid: ARBITRUM_LZ_EID, + to: to32, + amountLD: bridgeAmountLD, + minAmountLD: 0n, + extraOptions: "0x", + composeMsg: "0x", + oftCmd: "0x", + }; + const [fee, oft] = await Promise.all([ + contract.quoteSend(sendParam, false), + contract.quoteOFT(sendParam), + ]); + return { + nativeFee: fee.nativeFee as bigint, + amountReceivedLD: oft.oftReceipt.amountReceivedLD as bigint, + }; +} + +function buildStargateCalldata( + nativeFee: bigint, + recipient: string, + amountLD: bigint +): string { + return STARGATE_IFACE.encodeFunctionData("send", [ + { + dstEid: ARBITRUM_LZ_EID, + to: ethers.zeroPadValue(recipient, 32), + amountLD, + minAmountLD: 0n, + extraOptions: "0x", + composeMsg: "0x", + oftCmd: "0x", + }, + { nativeFee, lzTokenFee: 0n }, + recipient, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error("PRIVATE_KEY env var required"); + + const provider = new ethers.JsonRpcProvider(RPC.BASE); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.USDC_BASE, + signerAddress, + provider + ); + if (walletBalance === 0n) + throw new Error(`Signer ${signerAddress} has zero USDC on Base`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error("Balance too small"); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_BASE}`); + console.log( + `Flags: 0x${FLAGS.toString(16)} (pre-fee, balanceOf, bridge-value)` + ); + console.log(`USDC balance: ${ethers.formatUnits(walletBalance, 6)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + // pre-fee: deduct from input USDC before the swap + const feeAmount = bpsOf(inputAmount, FEE_BPS); + const swapInput = inputAmount - feeAmount; + + console.log("Fetching OpenOcean quote (USDC → ETH)..."); + const { ooRouter, swapData, estimatedOut, minAmountOut } = + await fetchOpenOceanQuote(swapInput); + console.log(` OO router: ${ooRouter}`); + console.log( + ` Pre-fee: ${ethers.formatUnits(feeAmount, 6)} USDC (${FEE_BPS} bps)` + ); + console.log(` Swap input: ${ethers.formatUnits(swapInput, 6)} USDC`); + console.log(` Est. ETH: ${ethers.formatEther(estimatedOut)}`); + console.log(` Min ETH: ${ethers.formatEther(minAmountOut)}`); + + console.log("Fetching Stargate quote (Base native pool → Arbitrum)..."); + const { nativeFee, amountReceivedLD } = await fetchStargateQuote( + provider, + estimatedOut, + signerAddress + ); + const nativeFeeWithBuffer = (nativeFee * 105n) / 100n; + console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} ETH`); + console.log(` Est. received: ${ethers.formatEther(amountReceivedLD)} ETH`); + + // estimatedOut is a placeholder for amountLD; router splices the actual finalETH at runtime + const amountLD = estimatedOut; + + await ensureRouterErc20Balance(signer, TOKENS.USDC_BASE, ROUTER_BASE); + await ensureRouterNativeBalance(signer, ROUTER_BASE); + + const swapApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_BASE, + TOKENS.USDC_BASE, + ooRouter, + swapInput, + ); + + const stargateData = buildStargateCalldata( + nativeFeeWithBuffer, + signerAddress, + amountLD + ); + + const callData = routerIface.encodeFunctionData("swapAndBridge", swapAndBridgeArgs( + ZERO_BYTES32, + FLAGS, + { + user: signerAddress, + inputToken: TOKENS.USDC_BASE, + inputAmount: inputAmount, + }, + { receiver: signerAddress, amount: feeAmount }, + { + target: ooRouter, + approvalSpender: swapApprovalSpender, + outputToken: NATIVE_TOKEN_ADDRESS, + value: 0n, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + swapData, + { + target: STARGATE_NATIVE_BASE, + approvalSpender: ZERO_ADDRESS, + value: nativeFeeWithBuffer, + }, + stargateData, + )); + + await ensureAllowanceForAllowanceHolder(signer, TOKENS.USDC_BASE, inputAmount); + const receipt = await execViaAH( + signer, + ROUTER_BASE, + TOKENS.USDC_BASE, + inputAmount, + ROUTER_BASE, + callData, + nativeFeeWithBuffer + ); + + logTxnSummary( + `Base USDC → Arbitrum ETH (Stargate) — swapAndBridge preFee/balanceOf`, + CHAIN_IDS.BASE, + receipt + ); + + console.log("\nETH arrives on Arbitrum once LZ delivers the message."); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/stargate/swapAndBridge.preFee.returndata.ts b/scripts/e2e/stargate/swapAndBridge.preFee.returndata.ts new file mode 100644 index 0000000..971437f --- /dev/null +++ b/scripts/e2e/stargate/swapAndBridge.preFee.returndata.ts @@ -0,0 +1,269 @@ +/** + * Route: Base USDC → native ETH (OpenOcean) → Arbitrum ETH (Stargate Native Pool on Base, LayerZero v2) + * Flags: pre-fee (fee taken from USDC input before swap), output read from swap returndata word 0 + * bridge-value + bridge-amount-position flags: router splices finalETH into amountLD and + * forwards finalETH + nativeFeeWithBuffer as msg.value to Stargate + * + * Pre-fee (bit0=0): feeAmount = FEE_BPS of inputAmount USDC, deducted before the swap. + * Returndata (bit1=0): final ETH amount is read from word 0 of the swap call returndata. + * BridgeValue (bit2=1): router forwards finalETH + nativeFeeWithBuffer as msg.value to Stargate. + * BridgeAmountPosition (bit3=1): router splices finalETH into amountLD at STARGATE_AMOUNT_LD_OFFSET. + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/stargate/swapAndBridge.preFee.returndata.ts + */ +import axios from "axios"; +import { ethers } from "ethers"; +import * as dotenv from "dotenv"; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + NATIVE_TOKEN_ADDRESS, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, + STARGATE_NATIVE_BASE, + ARBITRUM_LZ_EID, +} from "../config"; +import { + execViaAH, + ensureAllowanceForAllowanceHolder, +} from "../utils/allowanceHolder"; +import { getWalletErc20Balance } from "../utils/erc20"; +import { ROUTER_ABI } from "../utils/routerAbi"; +import { + ZERO_BYTES32, + BRIDGE_VALUE_FLAG, + ZERO_ADDRESS, + bridgeAmountPositionFlag, + swapAndBridgeArgs, +} from "../utils/contractTypes"; +import { STARGATE_AMOUNT_LD_OFFSET } from "../config"; +import { logTxnSummary } from "../utils/txnLogSummary"; +import { + ensureRouterErc20Balance, + ensureRouterNativeBalance, +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; + +// pre-fee (0x00) | bridge-value (0x04) | bridge-amount-position (0x08 + offset): splice finalETH into amountLD + forward with nativeFee +const FLAGS = BRIDGE_VALUE_FLAG | bridgeAmountPositionFlag(STARGATE_AMOUNT_LD_OFFSET); +const ROUTER_BASE = routerAddressForChain(CHAIN_IDS.BASE); + +const STARGATE_ABI = [ + "function quoteSend(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, bool payInLzToken) external view returns (tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee)", + "function quoteOFT(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) external view returns (tuple(uint256 minAmountLD, uint256 maxAmountLD) oftLimit, tuple(int256 feeAmountLD, string description)[] oftFeeDetails, tuple(uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt)", + "function send(tuple(uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, tuple(uint256 nativeFee, uint256 lzTokenFee) messagingFee, address refundAddress) external payable", +]; + +const STARGATE_IFACE = new ethers.Interface(STARGATE_ABI); + +interface OoQuoteResponse { + data: { to: string; data: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.USDC_BASE, + outTokenAddress: NATIVE_TOKEN_ADDRESS, + amount: ethers.formatUnits(inputAmount, 6), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_BASE, + account: ROUTER_BASE, + gasPrice: "1", + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.BASE}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + }; +} + +async function fetchStargateQuote( + provider: ethers.JsonRpcProvider, + bridgeAmountLD: bigint, + recipient: string +): Promise<{ nativeFee: bigint; amountReceivedLD: bigint }> { + const contract = new ethers.Contract( + STARGATE_NATIVE_BASE, + STARGATE_ABI, + provider + ); + const to32 = ethers.zeroPadValue(recipient, 32); + const sendParam = { + dstEid: ARBITRUM_LZ_EID, + to: to32, + amountLD: bridgeAmountLD, + minAmountLD: 0n, + extraOptions: "0x", + composeMsg: "0x", + oftCmd: "0x", + }; + const [fee, oft] = await Promise.all([ + contract.quoteSend(sendParam, false), + contract.quoteOFT(sendParam), + ]); + return { + nativeFee: fee.nativeFee as bigint, + amountReceivedLD: oft.oftReceipt.amountReceivedLD as bigint, + }; +} + +function buildStargateCalldata( + nativeFee: bigint, + recipient: string, + amountLD: bigint +): string { + return STARGATE_IFACE.encodeFunctionData("send", [ + { + dstEid: ARBITRUM_LZ_EID, + to: ethers.zeroPadValue(recipient, 32), + amountLD, + minAmountLD: 0n, + extraOptions: "0x", + composeMsg: "0x", + oftCmd: "0x", + }, + { nativeFee, lzTokenFee: 0n }, + recipient, + ]); +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error("PRIVATE_KEY env var required"); + + const provider = new ethers.JsonRpcProvider(RPC.BASE); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.USDC_BASE, + signerAddress, + provider + ); + if (walletBalance === 0n) + throw new Error(`Signer ${signerAddress} has zero USDC on Base`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error("Balance too small"); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_BASE}`); + console.log( + `Flags: 0x${FLAGS.toString(16)} (pre-fee, returndata, bridge-value)` + ); + console.log(`USDC balance: ${ethers.formatUnits(walletBalance, 6)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + // pre-fee: deduct from input USDC before the swap + const feeAmount = bpsOf(inputAmount, FEE_BPS); + const swapInput = inputAmount - feeAmount; + + console.log("Fetching OpenOcean quote (USDC → ETH)..."); + const { ooRouter, swapData, estimatedOut, minAmountOut } = + await fetchOpenOceanQuote(swapInput); + console.log(` OO router: ${ooRouter}`); + console.log( + ` Pre-fee: ${ethers.formatUnits(feeAmount, 6)} USDC (${FEE_BPS} bps)` + ); + console.log(` Swap input: ${ethers.formatUnits(swapInput, 6)} USDC`); + console.log(` Est. ETH: ${ethers.formatEther(estimatedOut)}`); + console.log(` Min ETH: ${ethers.formatEther(minAmountOut)}`); + + console.log("Fetching Stargate quote (Base native pool → Arbitrum)..."); + const { nativeFee, amountReceivedLD } = await fetchStargateQuote( + provider, + estimatedOut, + signerAddress + ); + const nativeFeeWithBuffer = (nativeFee * 105n) / 100n; + console.log(` nativeFee+5%: ${ethers.formatEther(nativeFeeWithBuffer)} ETH`); + console.log(` Est. received: ${ethers.formatEther(amountReceivedLD)} ETH`); + + // estimatedOut is a placeholder for amountLD; router splices the actual finalETH at runtime + const amountLD = estimatedOut; + + await ensureRouterErc20Balance(signer, TOKENS.USDC_BASE, ROUTER_BASE); + await ensureRouterNativeBalance(signer, ROUTER_BASE); + + const swapApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_BASE, + TOKENS.USDC_BASE, + ooRouter, + swapInput, + ); + + const stargateData = buildStargateCalldata( + nativeFeeWithBuffer, + signerAddress, + amountLD + ); + + const callData = routerIface.encodeFunctionData("swapAndBridge", swapAndBridgeArgs( + ZERO_BYTES32, + FLAGS, + { + user: signerAddress, + inputToken: TOKENS.USDC_BASE, + inputAmount: inputAmount, + }, + { receiver: signerAddress, amount: feeAmount }, + { + target: ooRouter, + approvalSpender: swapApprovalSpender, + outputToken: NATIVE_TOKEN_ADDRESS, + value: 0n, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + swapData, + { + target: STARGATE_NATIVE_BASE, + approvalSpender: ZERO_ADDRESS, + value: nativeFeeWithBuffer, + }, + stargateData, + )); + + await ensureAllowanceForAllowanceHolder(signer, TOKENS.USDC_BASE, inputAmount); + const receipt = await execViaAH( + signer, + ROUTER_BASE, + TOKENS.USDC_BASE, + inputAmount, + ROUTER_BASE, + callData, + nativeFeeWithBuffer + ); + + logTxnSummary( + `Base USDC → Arbitrum ETH (Stargate) — swapAndBridge preFee/returndata`, + CHAIN_IDS.BASE, + receipt + ); + + console.log("\nETH arrives on Arbitrum once LZ delivers the message."); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/swap/kyberswap.postFee.balanceOf.ts b/scripts/e2e/swap/kyberswap.postFee.balanceOf.ts new file mode 100644 index 0000000..e1a0380 --- /dev/null +++ b/scripts/e2e/swap/kyberswap.postFee.balanceOf.ts @@ -0,0 +1,234 @@ +/** + * Route: Polygon AAVE → USDC (KyberSwap) — standalone swap, no bridge + * Flags: post-fee (fee taken from USDC output after swap), output measured as USDC balanceOf delta + * + * Post-fee (bit0=1): feeAmount = FEE_BPS of estimatedOut USDC, deducted from swap output. + * BalanceOf (bit1=1): final USDC amount is measured as router USDC balance change (not returndata). + * + * returnData mode is not available for KyberSwap — it routes output directly to recipient. + * + * USDC lands in signer's wallet on Polygon. + * + * Usage: + * PRIVATE_KEY=0x... KYBERSWAP_API_KEY=... ts-node scripts/e2e/swap/kyberswap.postFee.balanceOf.ts + */ +import axios from "axios"; +import { ethers } from "ethers"; +import * as dotenv from "dotenv"; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + KYBERSWAP_API_KEY, + SWAP_SLIPPAGE_BPS, +} from "../config"; +import { + execViaAH, + ensureAllowanceForAllowanceHolder, +} from "../utils/allowanceHolder"; +import { getWalletErc20Balance } from "../utils/erc20"; +import { ROUTER_ABI } from "../utils/routerAbi"; +import { ZERO_BYTES32, swapArgs } from "../utils/contractTypes"; +import { logTxnSummary } from "../utils/txnLogSummary"; +import { + ensureRouterErc20Balance, +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; + +// post-fee (0x01) | balance-of (0x02) +const FLAGS = 0x03n; +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); +const KYBERSWAP_BASE_URL = "https://aggregator-api.kyberswap.com"; +const KYBERSWAP_CHAIN = "polygon"; + +interface KyberRouteData { + routeSummary: object; + routerAddress: string; +} + +interface KyberBuildData { + routerAddress: string; + amountOut: string; + data: string; + transactionValue: string; +} + +/** + * Fetches a KyberSwap route quote then builds executable calldata. + * Sets sender and recipient both to the router so output lands in the router + * for balanceOf delta measurement and post-fee deduction. + */ +async function fetchKyberSwapQuote( + amountIn: bigint, + routerAddress: string, +): Promise<{ + ksRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; + value: bigint; +}> { + const headers: Record = {}; + if (KYBERSWAP_API_KEY) { + headers["x-client-id"] = KYBERSWAP_API_KEY; + } + + const routeUrl = + `${KYBERSWAP_BASE_URL}/${KYBERSWAP_CHAIN}/api/v1/routes` + + `?tokenIn=${TOKENS.AAVE_POLYGON}&tokenOut=${TOKENS.USDC_POLYGON_CIRCLE}` + + `&amountIn=${amountIn.toString()}&excludedSources=bebop&gasInclude=true`; + + const routeResp = await axios.get<{ code: number; data: KyberRouteData }>( + routeUrl, + { headers }, + ); + if (!routeResp.data?.data?.routeSummary) { + throw new Error( + `KyberSwap routes call failed: ${JSON.stringify(routeResp.data)}`, + ); + } + const { routeSummary } = routeResp.data.data; + + const buildUrl = `${KYBERSWAP_BASE_URL}/${KYBERSWAP_CHAIN}/api/v1/route/build`; + const buildResp = await axios.post<{ code: number; data: KyberBuildData }>( + buildUrl, + { + routeSummary, + sender: routerAddress, + recipient: routerAddress, + slippageTolerance: SWAP_SLIPPAGE_BPS, + }, + { headers }, + ); + if (!buildResp.data?.data?.data) { + throw new Error( + `KyberSwap build call failed: ${JSON.stringify(buildResp.data)}`, + ); + } + + const { routerAddress: ksRouter, amountOut, data, transactionValue } = + buildResp.data.data; + const estimatedOut = BigInt(amountOut); + + return { + ksRouter, + swapData: data, + estimatedOut, + minAmountOut: + (estimatedOut * (10000n - BigInt(SWAP_SLIPPAGE_BPS))) / 10000n, + value: BigInt(transactionValue ?? "0"), + }; +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error("PRIVATE_KEY env var required"); + if (!KYBERSWAP_API_KEY) { + console.warn( + "KYBERSWAP_API_KEY not set — unauthenticated requests may be rate-limited", + ); + } + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.AAVE_POLYGON, + signerAddress, + provider, + ); + if (walletBalance === 0n) { + throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + } + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error("Balance too small"); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`Flags: 0x${FLAGS.toString(16)} (post-fee, balanceOf)`); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log("Fetching KyberSwap quote (AAVE → USDC)..."); + const { ksRouter, swapData, estimatedOut, minAmountOut, value } = + await fetchKyberSwapQuote(inputAmount, ROUTER_POLYGON); + + // post-fee: deduct from estimated USDC output after swap + const feeAmount = bpsOf(estimatedOut, FEE_BPS); + console.log(` KS router: ${ksRouter}`); + console.log(` Est. USDC: ${ethers.formatUnits(estimatedOut, 6)}`); + console.log( + ` Post-fee: ${ethers.formatUnits(feeAmount, 6)} USDC (${FEE_BPS} bps)`, + ); + console.log(` Min USDC: ${ethers.formatUnits(minAmountOut, 6)}`); + + await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); + await ensureRouterErc20Balance( + signer, + TOKENS.USDC_POLYGON_CIRCLE, + ROUTER_POLYGON, + ); + + const swapApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + ksRouter, + inputAmount, + ); + + const callData = routerIface.encodeFunctionData("swap", swapArgs( + ZERO_BYTES32, + FLAGS, + { + user: signerAddress, + inputToken: TOKENS.AAVE_POLYGON, + inputAmount: inputAmount, + }, + { receiver: signerAddress, amount: feeAmount }, + { + target: ksRouter, + approvalSpender: swapApprovalSpender, + outputToken: TOKENS.USDC_POLYGON_CIRCLE, + value, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + swapData, + signerAddress, + )); + + await ensureAllowanceForAllowanceHolder( + signer, + TOKENS.AAVE_POLYGON, + inputAmount, + ); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + inputAmount, + ROUTER_POLYGON, + callData, + ); + + logTxnSummary( + `Polygon AAVE → USDC — kyberswap postFee/balanceOf`, + CHAIN_IDS.POLYGON, + receipt, + ); + console.log(`\nUSDC is now in signer's wallet on Polygon.`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/swap/kyberswap.postFee.returndata.ts b/scripts/e2e/swap/kyberswap.postFee.returndata.ts new file mode 100644 index 0000000..5deafac --- /dev/null +++ b/scripts/e2e/swap/kyberswap.postFee.returndata.ts @@ -0,0 +1,229 @@ +/** + * Route: Polygon AAVE → USDC (KyberSwap) — standalone swap, no bridge + * Flags: post-fee (fee taken from USDC output after swap), output read from swap returndata word 0 + * + * Post-fee (bit0=1): feeAmount = FEE_BPS of estimatedOut USDC, deducted from swap output. + * Returndata (bit1=0): final USDC amount is read from word 0 of the swap call returndata. + * + * Build uses sender = recipient = router so gross USDC stays on the router until post-fee forward + * (same shape as the balanceOf post-fee script; only the output-measurement flag differs). + * + * USDC lands in signer's wallet on Polygon. + * + * Usage: + * PRIVATE_KEY=0x... KYBERSWAP_API_KEY=... ts-node scripts/e2e/swap/kyberswap.postFee.returndata.ts + */ +import axios from "axios"; +import { ethers } from "ethers"; +import * as dotenv from "dotenv"; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + KYBERSWAP_API_KEY, + SWAP_SLIPPAGE_BPS, +} from "../config"; +import { + execViaAH, + ensureAllowanceForAllowanceHolder, +} from "../utils/allowanceHolder"; +import { getWalletErc20Balance } from "../utils/erc20"; +import { ROUTER_ABI } from "../utils/routerAbi"; +import { ZERO_BYTES32, swapArgs } from "../utils/contractTypes"; +import { logTxnSummary } from "../utils/txnLogSummary"; +import { + ensureRouterErc20Balance, +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; + +// post-fee (0x01) | returndata (bit1=0 ⇒ no 0x02) +const FLAGS = 0x01n; +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); +const KYBERSWAP_BASE_URL = "https://aggregator-api.kyberswap.com"; +const KYBERSWAP_CHAIN = "polygon"; + +interface KyberRouteData { + routeSummary: object; + routerAddress: string; +} + +interface KyberBuildData { + routerAddress: string; + amountOut: string; + data: string; + transactionValue: string; +} + +/** + * Fetches a KyberSwap route quote then builds executable calldata. + * Post-fee path: both sender and recipient are the router so output settles on-contract before fee. + */ +async function fetchKyberSwapQuote( + amountIn: bigint, + routerAddress: string, +): Promise<{ + ksRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; + value: bigint; +}> { + const headers: Record = {}; + if (KYBERSWAP_API_KEY) { + headers["x-client-id"] = KYBERSWAP_API_KEY; + } + + const routeUrl = + `${KYBERSWAP_BASE_URL}/${KYBERSWAP_CHAIN}/api/v1/routes` + + `?tokenIn=${TOKENS.AAVE_POLYGON}&tokenOut=${TOKENS.USDC_POLYGON_CIRCLE}` + + `&amountIn=${amountIn.toString()}&excludedSources=bebop&gasInclude=true`; + + const routeResp = await axios.get<{ code: number; data: KyberRouteData }>( + routeUrl, + { headers }, + ); + if (!routeResp.data?.data?.routeSummary) { + throw new Error( + `KyberSwap routes call failed: ${JSON.stringify(routeResp.data)}`, + ); + } + const { routeSummary } = routeResp.data.data; + + const buildUrl = `${KYBERSWAP_BASE_URL}/${KYBERSWAP_CHAIN}/api/v1/route/build`; + const buildResp = await axios.post<{ code: number; data: KyberBuildData }>( + buildUrl, + { + routeSummary, + sender: routerAddress, + recipient: routerAddress, + slippageTolerance: SWAP_SLIPPAGE_BPS, + }, + { headers }, + ); + if (!buildResp.data?.data?.data) { + throw new Error( + `KyberSwap build call failed: ${JSON.stringify(buildResp.data)}`, + ); + } + + const { routerAddress: ksRouter, amountOut, data, transactionValue } = + buildResp.data.data; + const estimatedOut = BigInt(amountOut); + + return { + ksRouter, + swapData: data, + estimatedOut, + minAmountOut: + (estimatedOut * (10000n - BigInt(SWAP_SLIPPAGE_BPS))) / 10000n, + value: BigInt(transactionValue ?? "0"), + }; +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error("PRIVATE_KEY env var required"); + if (!KYBERSWAP_API_KEY) { + console.warn( + "KYBERSWAP_API_KEY not set — unauthenticated requests may be rate-limited", + ); + } + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.AAVE_POLYGON, + signerAddress, + provider, + ); + if (walletBalance === 0n) { + throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + } + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error("Balance too small"); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`Flags: 0x${FLAGS.toString(16)} (post-fee, returndata)`); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log("Fetching KyberSwap quote (AAVE → USDC)..."); + const { ksRouter, swapData, estimatedOut, minAmountOut, value } = + await fetchKyberSwapQuote(inputAmount, ROUTER_POLYGON); + + // post-fee: deduct from estimated USDC output after swap + const feeAmount = bpsOf(estimatedOut, FEE_BPS); + console.log(` KS router: ${ksRouter}`); + console.log(` Est. USDC: ${ethers.formatUnits(estimatedOut, 6)}`); + console.log( + ` Post-fee: ${ethers.formatUnits(feeAmount, 6)} USDC (${FEE_BPS} bps)`, + ); + console.log(` Min USDC: ${ethers.formatUnits(minAmountOut, 6)}`); + + await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); + + const swapApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + ksRouter, + inputAmount, + ); + + const callData = routerIface.encodeFunctionData("swap", swapArgs( + ZERO_BYTES32, + FLAGS, + { + user: signerAddress, + inputToken: TOKENS.AAVE_POLYGON, + inputAmount: inputAmount, + }, + { receiver: signerAddress, amount: feeAmount }, + { + target: ksRouter, + approvalSpender: swapApprovalSpender, + outputToken: TOKENS.USDC_POLYGON_CIRCLE, + value, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + swapData, + signerAddress, + )); + + await ensureAllowanceForAllowanceHolder( + signer, + TOKENS.AAVE_POLYGON, + inputAmount, + ); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + inputAmount, + ROUTER_POLYGON, + callData, + ); + + logTxnSummary( + `Polygon AAVE → USDC — kyberswap postFee/returndata`, + CHAIN_IDS.POLYGON, + receipt, + ); + console.log(`\nUSDC is now in signer's wallet on Polygon.`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/swap/kyberswap.preFee.balanceOf.ts b/scripts/e2e/swap/kyberswap.preFee.balanceOf.ts new file mode 100644 index 0000000..fbd0787 --- /dev/null +++ b/scripts/e2e/swap/kyberswap.preFee.balanceOf.ts @@ -0,0 +1,240 @@ +/** + * Route: Polygon AAVE → USDC (KyberSwap) — standalone swap, no bridge + * Flags: pre-fee (fee taken from AAVE input before swap), output measured as USDC balanceOf delta + * + * Pre-fee (bit0=0): feeAmount = FEE_BPS of inputAmount AAVE, deducted before the swap. + * BalanceOf (bit1=1): final USDC amount is measured as router USDC balance change (not returndata). + * + * KyberSwap build calldata encodes exact input amounts, so the quote is for swapInput + * (inputAmount − preFeeAmount) to match the router's approval amount at execution time. + * returnData mode is not available for KyberSwap — it routes output directly to recipient. + * + * USDC lands in signer's wallet on Polygon. + * + * Usage: + * PRIVATE_KEY=0x... KYBERSWAP_API_KEY=... ts-node scripts/e2e/swap/kyberswap.preFee.balanceOf.ts + */ +import axios from "axios"; +import { ethers } from "ethers"; +import * as dotenv from "dotenv"; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + KYBERSWAP_API_KEY, + SWAP_SLIPPAGE_BPS, +} from "../config"; +import { + execViaAH, + ensureAllowanceForAllowanceHolder, +} from "../utils/allowanceHolder"; +import { getWalletErc20Balance } from "../utils/erc20"; +import { ROUTER_ABI } from "../utils/routerAbi"; +import { ZERO_BYTES32, swapArgs } from "../utils/contractTypes"; +import { logTxnSummary } from "../utils/txnLogSummary"; +import { + ensureRouterErc20Balance, +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; + +// pre-fee (0x00) | balance-of (0x02) +const FLAGS = 0x02n; +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); +const KYBERSWAP_BASE_URL = "https://aggregator-api.kyberswap.com"; +const KYBERSWAP_CHAIN = "polygon"; + +interface KyberRouteData { + routeSummary: object; + routerAddress: string; +} + +interface KyberBuildData { + routerAddress: string; + amountOut: string; + data: string; + transactionValue: string; +} + +/** + * Fetches a KyberSwap route quote then builds executable calldata. + * Sets sender and recipient both to the router so output lands in the router + * for balanceOf delta measurement. + */ +async function fetchKyberSwapQuote( + amountIn: bigint, + routerAddress: string, +): Promise<{ + ksRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; + value: bigint; +}> { + const headers: Record = {}; + if (KYBERSWAP_API_KEY) { + headers["x-client-id"] = KYBERSWAP_API_KEY; + } + + const routeUrl = + `${KYBERSWAP_BASE_URL}/${KYBERSWAP_CHAIN}/api/v1/routes` + + `?tokenIn=${TOKENS.AAVE_POLYGON}&tokenOut=${TOKENS.USDC_POLYGON_CIRCLE}` + + `&amountIn=${amountIn.toString()}&excludedSources=bebop&gasInclude=true`; + + const routeResp = await axios.get<{ code: number; data: KyberRouteData }>( + routeUrl, + { headers }, + ); + if (!routeResp.data?.data?.routeSummary) { + throw new Error( + `KyberSwap routes call failed: ${JSON.stringify(routeResp.data)}`, + ); + } + const { routeSummary } = routeResp.data.data; + + const buildUrl = `${KYBERSWAP_BASE_URL}/${KYBERSWAP_CHAIN}/api/v1/route/build`; + const buildResp = await axios.post<{ code: number; data: KyberBuildData }>( + buildUrl, + { + routeSummary, + sender: routerAddress, + recipient: routerAddress, + slippageTolerance: SWAP_SLIPPAGE_BPS, + }, + { headers }, + ); + if (!buildResp.data?.data?.data) { + throw new Error( + `KyberSwap build call failed: ${JSON.stringify(buildResp.data)}`, + ); + } + + const { routerAddress: ksRouter, amountOut, data, transactionValue } = + buildResp.data.data; + const estimatedOut = BigInt(amountOut); + + return { + ksRouter, + swapData: data, + estimatedOut, + minAmountOut: + (estimatedOut * (10000n - BigInt(SWAP_SLIPPAGE_BPS))) / 10000n, + value: BigInt(transactionValue ?? "0"), + }; +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error("PRIVATE_KEY env var required"); + if (!KYBERSWAP_API_KEY) { + console.warn( + "KYBERSWAP_API_KEY not set — unauthenticated requests may be rate-limited", + ); + } + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.AAVE_POLYGON, + signerAddress, + provider, + ); + if (walletBalance === 0n) { + throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + } + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error("Balance too small"); + + // pre-fee: deduct from input AAVE before the swap + const feeAmount = bpsOf(inputAmount, FEE_BPS); + // quote for the reduced swap input so KyberSwap calldata encodes the correct amount + const swapInput = inputAmount - feeAmount; + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`Flags: 0x${FLAGS.toString(16)} (pre-fee, balanceOf)`); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + console.log( + `Pre-fee: ${ethers.formatUnits(feeAmount, 18)} AAVE (${FEE_BPS} bps)`, + ); + console.log(`Swap input: ${ethers.formatUnits(swapInput, 18)} AAVE`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log("Fetching KyberSwap quote (AAVE → USDC)..."); + const { ksRouter, swapData, estimatedOut, minAmountOut, value } = + await fetchKyberSwapQuote(swapInput, ROUTER_POLYGON); + + console.log(` KS router: ${ksRouter}`); + console.log(` Est. USDC: ${ethers.formatUnits(estimatedOut, 6)}`); + console.log(` Min USDC: ${ethers.formatUnits(minAmountOut, 6)}`); + + await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); + await ensureRouterErc20Balance( + signer, + TOKENS.USDC_POLYGON_CIRCLE, + ROUTER_POLYGON, + ); + + const swapApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + ksRouter, + swapInput, + ); + + const callData = routerIface.encodeFunctionData("swap", swapArgs( + ZERO_BYTES32, + FLAGS, + { + user: signerAddress, + inputToken: TOKENS.AAVE_POLYGON, + inputAmount: inputAmount, + }, + { receiver: signerAddress, amount: feeAmount }, + { + target: ksRouter, + approvalSpender: swapApprovalSpender, + outputToken: TOKENS.USDC_POLYGON_CIRCLE, + value, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + swapData, + signerAddress, + )); + + await ensureAllowanceForAllowanceHolder( + signer, + TOKENS.AAVE_POLYGON, + inputAmount, + ); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + inputAmount, + ROUTER_POLYGON, + callData, + ); + + logTxnSummary( + `Polygon AAVE → USDC — kyberswap preFee/balanceOf`, + CHAIN_IDS.POLYGON, + receipt, + ); + console.log(`\nUSDC is now in signer's wallet on Polygon.`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/swap/kyberswap.preFee.returndata.ts b/scripts/e2e/swap/kyberswap.preFee.returndata.ts new file mode 100644 index 0000000..c9b84e4 --- /dev/null +++ b/scripts/e2e/swap/kyberswap.preFee.returndata.ts @@ -0,0 +1,236 @@ +/** + * Route: Polygon AAVE → USDC (KyberSwap) — standalone swap, no bridge + * Flags: pre-fee (fee taken from AAVE input before swap), output read from swap returndata word 0 + * + * Pre-fee (bit0=0): feeAmount = FEE_BPS of inputAmount AAVE, deducted before the swap. + * Returndata (bit1=0): final USDC amount is read from word 0 of the swap call returndata. + * + * Pre-fee + returndata: swap output must go to `receiver` (signer); the router decodes amount from + * returndata. Quote uses swapInput = inputAmount − fee so calldata matches the post-fee swap size. + * + * Kyber build: sender = router (executor), recipient = user (net output destination). + * + * USDC lands in signer's wallet on Polygon. + * + * Usage: + * PRIVATE_KEY=0x... KYBERSWAP_API_KEY=... ts-node scripts/e2e/swap/kyberswap.preFee.returndata.ts + */ +import axios from "axios"; +import { ethers } from "ethers"; +import * as dotenv from "dotenv"; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + KYBERSWAP_API_KEY, + SWAP_SLIPPAGE_BPS, +} from "../config"; +import { + execViaAH, + ensureAllowanceForAllowanceHolder, +} from "../utils/allowanceHolder"; +import { getWalletErc20Balance } from "../utils/erc20"; +import { ROUTER_ABI } from "../utils/routerAbi"; +import { ZERO_BYTES32, swapArgs } from "../utils/contractTypes"; +import { logTxnSummary } from "../utils/txnLogSummary"; +import { + ensureRouterErc20Balance, +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; + +// pre-fee (0x00) | returndata (bit1=0 ⇒ no 0x02) +const FLAGS = 0x00n; +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); +const KYBERSWAP_BASE_URL = "https://aggregator-api.kyberswap.com"; +const KYBERSWAP_CHAIN = "polygon"; + +interface KyberRouteData { + routeSummary: object; + routerAddress: string; +} + +interface KyberBuildData { + routerAddress: string; + amountOut: string; + data: string; + transactionValue: string; +} + +/** + * Fetches a KyberSwap route quote then builds executable calldata. + * Pre-fee returndata: router executes; tokens are sent to `outputRecipient` (user). + */ +async function fetchKyberSwapQuote( + amountIn: bigint, + routerAddress: string, + outputRecipient: string, +): Promise<{ + ksRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; + value: bigint; +}> { + const headers: Record = {}; + if (KYBERSWAP_API_KEY) { + headers["x-client-id"] = KYBERSWAP_API_KEY; + } + + const routeUrl = + `${KYBERSWAP_BASE_URL}/${KYBERSWAP_CHAIN}/api/v1/routes` + + `?tokenIn=${TOKENS.AAVE_POLYGON}&tokenOut=${TOKENS.USDC_POLYGON_CIRCLE}` + + `&amountIn=${amountIn.toString()}&excludedSources=bebop&gasInclude=true`; + + const routeResp = await axios.get<{ code: number; data: KyberRouteData }>( + routeUrl, + { headers }, + ); + if (!routeResp.data?.data?.routeSummary) { + throw new Error( + `KyberSwap routes call failed: ${JSON.stringify(routeResp.data)}`, + ); + } + const { routeSummary } = routeResp.data.data; + + const buildUrl = `${KYBERSWAP_BASE_URL}/${KYBERSWAP_CHAIN}/api/v1/route/build`; + const buildResp = await axios.post<{ code: number; data: KyberBuildData }>( + buildUrl, + { + routeSummary, + sender: routerAddress, + recipient: outputRecipient, + slippageTolerance: SWAP_SLIPPAGE_BPS, + }, + { headers }, + ); + if (!buildResp.data?.data?.data) { + throw new Error( + `KyberSwap build call failed: ${JSON.stringify(buildResp.data)}`, + ); + } + + const { routerAddress: ksRouter, amountOut, data, transactionValue } = + buildResp.data.data; + const estimatedOut = BigInt(amountOut); + + return { + ksRouter, + swapData: data, + estimatedOut, + minAmountOut: + (estimatedOut * (10000n - BigInt(SWAP_SLIPPAGE_BPS))) / 10000n, + value: BigInt(transactionValue ?? "0"), + }; +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error("PRIVATE_KEY env var required"); + if (!KYBERSWAP_API_KEY) { + console.warn( + "KYBERSWAP_API_KEY not set — unauthenticated requests may be rate-limited", + ); + } + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.AAVE_POLYGON, + signerAddress, + provider, + ); + if (walletBalance === 0n) { + throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + } + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error("Balance too small"); + + // pre-fee: deduct from input AAVE before the swap + const feeAmount = bpsOf(inputAmount, FEE_BPS); + // quote for the reduced swap input so KyberSwap calldata encodes the correct amount + const swapInput = inputAmount - feeAmount; + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`Flags: 0x${FLAGS.toString(16)} (pre-fee, returndata)`); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + console.log( + `Pre-fee: ${ethers.formatUnits(feeAmount, 18)} AAVE (${FEE_BPS} bps)`, + ); + console.log(`Swap input: ${ethers.formatUnits(swapInput, 18)} AAVE`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log("Fetching KyberSwap quote (AAVE → USDC)..."); + const { ksRouter, swapData, estimatedOut, minAmountOut, value } = + await fetchKyberSwapQuote(swapInput, ROUTER_POLYGON, signerAddress); + + console.log(` KS router: ${ksRouter}`); + console.log(` Est. USDC: ${ethers.formatUnits(estimatedOut, 6)}`); + console.log(` Min USDC: ${ethers.formatUnits(minAmountOut, 6)}`); + + await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); + + const swapApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + ksRouter, + swapInput, + ); + + const callData = routerIface.encodeFunctionData("swap", swapArgs( + ZERO_BYTES32, + FLAGS, + { + user: signerAddress, + inputToken: TOKENS.AAVE_POLYGON, + inputAmount: inputAmount, + }, + { receiver: signerAddress, amount: feeAmount }, + { + target: ksRouter, + approvalSpender: swapApprovalSpender, + outputToken: TOKENS.USDC_POLYGON_CIRCLE, + value, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + swapData, + signerAddress, + )); + + await ensureAllowanceForAllowanceHolder( + signer, + TOKENS.AAVE_POLYGON, + inputAmount, + ); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + inputAmount, + ROUTER_POLYGON, + callData, + ); + + logTxnSummary( + `Polygon AAVE → USDC — kyberswap preFee/returndata`, + CHAIN_IDS.POLYGON, + receipt, + ); + console.log(`\nUSDC is now in signer's wallet on Polygon.`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/swap/swap.postFee.balanceOf.ts b/scripts/e2e/swap/swap.postFee.balanceOf.ts new file mode 100644 index 0000000..70a71af --- /dev/null +++ b/scripts/e2e/swap/swap.postFee.balanceOf.ts @@ -0,0 +1,177 @@ +/** + * Route: Polygon AAVE → USDC (OpenOcean) — standalone swap, no bridge + * Flags: post-fee (fee taken from USDC output after swap), output measured as USDC balanceOf delta + * + * Post-fee (bit0=1): feeAmount = FEE_BPS of estimatedOut USDC, deducted from swap output. + * BalanceOf (bit1=1): final USDC amount is measured as router USDC balance change (not returndata). + * + * USDC lands in signer's wallet on Polygon. + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/swap/swap.postFee.balanceOf.ts + */ +import axios from "axios"; +import { ethers } from "ethers"; +import * as dotenv from "dotenv"; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, +} from "../config"; +import { + execViaAH, + ensureAllowanceForAllowanceHolder, +} from "../utils/allowanceHolder"; +import { getWalletErc20Balance } from "../utils/erc20"; +import { ROUTER_ABI } from "../utils/routerAbi"; +import { ZERO_BYTES32, swapArgs } from "../utils/contractTypes"; +import { logTxnSummary } from "../utils/txnLogSummary"; +import { + ensureRouterErc20Balance, +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; + +// post-fee (0x01) | balance-of (0x02) +const FLAGS = 0x03n; +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); + +interface OoQuoteResponse { + data: { to: string; data: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.AAVE_POLYGON, + outTokenAddress: TOKENS.USDC_POLYGON_CIRCLE, + amount: ethers.formatUnits(inputAmount, 18), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_POLYGON, + account: ROUTER_POLYGON, + gasPrice: "1", + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.POLYGON}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + }; +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error("PRIVATE_KEY env var required"); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.AAVE_POLYGON, + signerAddress, + provider + ); + if (walletBalance === 0n) + throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error("Balance too small"); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`Flags: 0x${FLAGS.toString(16)} (post-fee, balanceOf)`); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log("Fetching OpenOcean quote (AAVE → USDC)..."); + const { ooRouter, swapData, estimatedOut, minAmountOut } = + await fetchOpenOceanQuote(inputAmount); + + // post-fee: deduct from estimated USDC output after swap + const feeAmount = bpsOf(estimatedOut, FEE_BPS); + console.log(` OO router: ${ooRouter}`); + console.log(` Est. USDC: ${ethers.formatUnits(estimatedOut, 6)}`); + console.log( + ` Post-fee: ${ethers.formatUnits(feeAmount, 6)} USDC (${FEE_BPS} bps)` + ); + console.log(` Min USDC: ${ethers.formatUnits(minAmountOut, 6)}`); + + await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); + await ensureRouterErc20Balance( + signer, + TOKENS.USDC_POLYGON_CIRCLE, + ROUTER_POLYGON + ); + + const swapApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + ooRouter, + inputAmount, + ); + + const callData = routerIface.encodeFunctionData("swap", swapArgs( + ZERO_BYTES32, + FLAGS, + { + user: signerAddress, + inputToken: TOKENS.AAVE_POLYGON, + inputAmount: inputAmount, + }, + { receiver: signerAddress, amount: feeAmount }, + { + target: ooRouter, + approvalSpender: swapApprovalSpender, + outputToken: TOKENS.USDC_POLYGON_CIRCLE, + value: 0n, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + swapData, + signerAddress, + )); + + await ensureAllowanceForAllowanceHolder( + signer, + TOKENS.AAVE_POLYGON, + inputAmount + ); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + inputAmount, + ROUTER_POLYGON, + callData + ); + + logTxnSummary( + `Polygon AAVE → USDC — swap postFee/balanceOf`, + CHAIN_IDS.POLYGON, + receipt + ); + + console.log(`\nUSDC is now in signer's wallet on Polygon.`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/swap/swap.postFee.returndata.ts b/scripts/e2e/swap/swap.postFee.returndata.ts new file mode 100644 index 0000000..7fde5db --- /dev/null +++ b/scripts/e2e/swap/swap.postFee.returndata.ts @@ -0,0 +1,172 @@ +/** + * Route: Polygon AAVE → USDC (OpenOcean) — standalone swap, no bridge + * Flags: post-fee (fee taken from USDC output after swap), output read from swap returndata word 0 + * + * Post-fee (bit0=1): feeAmount = FEE_BPS of estimatedOut USDC, deducted from swap output. + * Returndata (bit1=0): final USDC amount is read from word 0 of the swap call returndata. + * + * USDC lands in signer's wallet on Polygon. + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/swap/swap.postFee.returndata.ts + */ +import axios from "axios"; +import { ethers } from "ethers"; +import * as dotenv from "dotenv"; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, +} from "../config"; +import { + execViaAH, + ensureAllowanceForAllowanceHolder, +} from "../utils/allowanceHolder"; +import { getWalletErc20Balance } from "../utils/erc20"; +import { ROUTER_ABI } from "../utils/routerAbi"; +import { ZERO_BYTES32, swapArgs } from "../utils/contractTypes"; +import { logTxnSummary } from "../utils/txnLogSummary"; +import { + ensureRouterErc20Balance, +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; + +// post-fee (0x01) | returndata (0x00) +const FLAGS = 0x01n; +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); + +interface OoQuoteResponse { + data: { to: string; data: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.AAVE_POLYGON, + outTokenAddress: TOKENS.USDC_POLYGON_CIRCLE, + amount: ethers.formatUnits(inputAmount, 18), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_POLYGON, + account: ROUTER_POLYGON, + gasPrice: "1", + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.POLYGON}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + }; +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error("PRIVATE_KEY env var required"); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.AAVE_POLYGON, + signerAddress, + provider + ); + if (walletBalance === 0n) + throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error("Balance too small"); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`Flags: 0x${FLAGS.toString(16)} (post-fee, returndata)`); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log("Fetching OpenOcean quote (AAVE → USDC)..."); + const { ooRouter, swapData, estimatedOut, minAmountOut } = + await fetchOpenOceanQuote(inputAmount); + + // post-fee: deduct from estimated USDC output after swap + const feeAmount = bpsOf(estimatedOut, FEE_BPS); + console.log(` OO router: ${ooRouter}`); + console.log(` Est. USDC: ${ethers.formatUnits(estimatedOut, 6)}`); + console.log( + ` Post-fee: ${ethers.formatUnits(feeAmount, 6)} USDC (${FEE_BPS} bps)` + ); + console.log(` Min USDC: ${ethers.formatUnits(minAmountOut, 6)}`); + + await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); + + const swapApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + ooRouter, + inputAmount, + ); + + const callData = routerIface.encodeFunctionData("swap", swapArgs( + ZERO_BYTES32, + FLAGS, + { + user: signerAddress, + inputToken: TOKENS.AAVE_POLYGON, + inputAmount: inputAmount, + }, + { receiver: signerAddress, amount: feeAmount }, + { + target: ooRouter, + approvalSpender: swapApprovalSpender, + outputToken: TOKENS.USDC_POLYGON_CIRCLE, + value: 0n, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + swapData, + signerAddress, + )); + + await ensureAllowanceForAllowanceHolder( + signer, + TOKENS.AAVE_POLYGON, + inputAmount + ); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + inputAmount, + ROUTER_POLYGON, + callData + ); + + logTxnSummary( + `Polygon AAVE → USDC — swap postFee/returndata`, + CHAIN_IDS.POLYGON, + receipt + ); + + console.log(`\nUSDC is now in signer's wallet on Polygon.`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/swap/swap.preFee.balanceOf.ts b/scripts/e2e/swap/swap.preFee.balanceOf.ts new file mode 100644 index 0000000..d557b50 --- /dev/null +++ b/scripts/e2e/swap/swap.preFee.balanceOf.ts @@ -0,0 +1,177 @@ +/** + * Route: Polygon AAVE → USDC (OpenOcean) — standalone swap, no bridge + * Flags: pre-fee (fee taken from AAVE input before swap), output measured as USDC balanceOf delta + * + * Pre-fee (bit0=0): feeAmount = FEE_BPS of inputAmount AAVE, deducted before the swap. + * BalanceOf (bit1=1): final USDC amount is measured as router USDC balance change (not returndata). + * + * USDC lands in signer's wallet on Polygon. + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/swap/swap.preFee.balanceOf.ts + */ +import axios from "axios"; +import { ethers } from "ethers"; +import * as dotenv from "dotenv"; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, +} from "../config"; +import { + execViaAH, + ensureAllowanceForAllowanceHolder, +} from "../utils/allowanceHolder"; +import { getWalletErc20Balance } from "../utils/erc20"; +import { ROUTER_ABI } from "../utils/routerAbi"; +import { ZERO_BYTES32, swapArgs } from "../utils/contractTypes"; +import { logTxnSummary } from "../utils/txnLogSummary"; +import { + ensureRouterErc20Balance, +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; + +// pre-fee (0x00) | balance-of (0x02) +const FLAGS = 0x02n; +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); + +interface OoQuoteResponse { + data: { to: string; data: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.AAVE_POLYGON, + outTokenAddress: TOKENS.USDC_POLYGON_CIRCLE, + amount: ethers.formatUnits(inputAmount, 18), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_POLYGON, + account: ROUTER_POLYGON, + gasPrice: "1", + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.POLYGON}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + }; +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error("PRIVATE_KEY env var required"); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.AAVE_POLYGON, + signerAddress, + provider + ); + if (walletBalance === 0n) + throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error("Balance too small"); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`Flags: 0x${FLAGS.toString(16)} (pre-fee, balanceOf)`); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log("Fetching OpenOcean quote (AAVE → USDC)..."); + const { ooRouter, swapData, estimatedOut, minAmountOut } = + await fetchOpenOceanQuote(inputAmount); + + // pre-fee: deduct from input AAVE before the swap + const feeAmount = bpsOf(inputAmount, FEE_BPS); + console.log(` OO router: ${ooRouter}`); + console.log( + ` Pre-fee: ${ethers.formatUnits(feeAmount, 18)} AAVE (${FEE_BPS} bps)` + ); + console.log(` Est. USDC: ${ethers.formatUnits(estimatedOut, 6)}`); + console.log(` Min USDC: ${ethers.formatUnits(minAmountOut, 6)}`); + + await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); + await ensureRouterErc20Balance( + signer, + TOKENS.USDC_POLYGON_CIRCLE, + ROUTER_POLYGON + ); + + const swapApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + ooRouter, + inputAmount - feeAmount, + ); + + const callData = routerIface.encodeFunctionData("swap", swapArgs( + ZERO_BYTES32, + FLAGS, + { + user: signerAddress, + inputToken: TOKENS.AAVE_POLYGON, + inputAmount: inputAmount, + }, + { receiver: signerAddress, amount: feeAmount }, + { + target: ooRouter, + approvalSpender: swapApprovalSpender, + outputToken: TOKENS.USDC_POLYGON_CIRCLE, + value: 0n, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + swapData, + signerAddress, + )); + + await ensureAllowanceForAllowanceHolder( + signer, + TOKENS.AAVE_POLYGON, + inputAmount + ); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + inputAmount, + ROUTER_POLYGON, + callData + ); + + logTxnSummary( + `Polygon AAVE → USDC — swap preFee/balanceOf`, + CHAIN_IDS.POLYGON, + receipt + ); + + console.log(`\nUSDC is now in signer's wallet on Polygon.`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/swap/swap.preFee.returndata.ts b/scripts/e2e/swap/swap.preFee.returndata.ts new file mode 100644 index 0000000..094a548 --- /dev/null +++ b/scripts/e2e/swap/swap.preFee.returndata.ts @@ -0,0 +1,172 @@ +/** + * Route: Polygon AAVE → USDC (OpenOcean) — standalone swap, no bridge + * Flags: pre-fee (fee taken from AAVE input before swap), output read from swap returndata word 0 + * + * Pre-fee (bit0=0): feeAmount = FEE_BPS of inputAmount AAVE, deducted before the swap. + * Returndata (bit1=0): final USDC amount is read from word 0 of the swap call returndata. + * + * USDC lands in signer's wallet on Polygon. + * + * Usage: + * PRIVATE_KEY=0x... ts-node scripts/e2e/swap/swap.preFee.returndata.ts + */ +import axios from "axios"; +import { ethers } from "ethers"; +import * as dotenv from "dotenv"; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + OPEN_OCEAN_API_KEY, + OO_SLIPPAGE_PERCENT, +} from "../config"; +import { + execViaAH, + ensureAllowanceForAllowanceHolder, +} from "../utils/allowanceHolder"; +import { getWalletErc20Balance } from "../utils/erc20"; +import { ROUTER_ABI } from "../utils/routerAbi"; +import { ZERO_BYTES32, swapArgs } from "../utils/contractTypes"; +import { logTxnSummary } from "../utils/txnLogSummary"; +import { + ensureRouterErc20Balance, +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; + +// pre-fee (0x00) | returndata (0x00) +const FLAGS = 0x00n; +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); + +interface OoQuoteResponse { + data: { to: string; data: string; outAmount: string; minOutAmount: string }; +} + +async function fetchOpenOceanQuote(inputAmount: bigint): Promise<{ + ooRouter: string; + swapData: string; + estimatedOut: bigint; + minAmountOut: bigint; +}> { + const params: Record = { + inTokenAddress: TOKENS.AAVE_POLYGON, + outTokenAddress: TOKENS.USDC_POLYGON_CIRCLE, + amount: ethers.formatUnits(inputAmount, 18), + slippage: OO_SLIPPAGE_PERCENT, + sender: ROUTER_POLYGON, + account: ROUTER_POLYGON, + gasPrice: "1", + }; + if (OPEN_OCEAN_API_KEY) params.apikey = OPEN_OCEAN_API_KEY; + const url = `https://open-api.openocean.finance/v3/${CHAIN_IDS.POLYGON}/swap_quote`; + const response = await axios.get(url, { params }); + const q = response.data.data; + return { + ooRouter: q.to, + swapData: q.data, + estimatedOut: BigInt(q.outAmount), + minAmountOut: BigInt(q.minOutAmount), + }; +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error("PRIVATE_KEY env var required"); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.AAVE_POLYGON, + signerAddress, + provider + ); + if (walletBalance === 0n) + throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error("Balance too small"); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`Flags: 0x${FLAGS.toString(16)} (pre-fee, returndata)`); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log("Fetching OpenOcean quote (AAVE → USDC)..."); + const { ooRouter, swapData, estimatedOut, minAmountOut } = + await fetchOpenOceanQuote(inputAmount); + + // pre-fee: deduct from input AAVE before the swap + const feeAmount = bpsOf(inputAmount, FEE_BPS); + console.log(` OO router: ${ooRouter}`); + console.log( + ` Pre-fee: ${ethers.formatUnits(feeAmount, 18)} AAVE (${FEE_BPS} bps)` + ); + console.log(` Est. USDC: ${ethers.formatUnits(estimatedOut, 6)}`); + console.log(` Min USDC: ${ethers.formatUnits(minAmountOut, 6)}`); + + await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); + + const swapApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + ooRouter, + inputAmount - feeAmount, + ); + + const callData = routerIface.encodeFunctionData("swap", swapArgs( + ZERO_BYTES32, + FLAGS, + { + user: signerAddress, + inputToken: TOKENS.AAVE_POLYGON, + inputAmount: inputAmount, + }, + { receiver: signerAddress, amount: feeAmount }, + { + target: ooRouter, + approvalSpender: swapApprovalSpender, + outputToken: TOKENS.USDC_POLYGON_CIRCLE, + value: 0n, + minOutput: minAmountOut, + returnDataWordOffset: 0n, + }, + swapData, + signerAddress, + )); + + await ensureAllowanceForAllowanceHolder( + signer, + TOKENS.AAVE_POLYGON, + inputAmount + ); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + inputAmount, + ROUTER_POLYGON, + callData + ); + + logTxnSummary( + `Polygon AAVE → USDC — swap preFee/returndata`, + CHAIN_IDS.POLYGON, + receipt + ); + + console.log(`\nUSDC is now in signer's wallet on Polygon.`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/swap/zerox.postFee.balanceOf.ts b/scripts/e2e/swap/zerox.postFee.balanceOf.ts new file mode 100644 index 0000000..7137d81 --- /dev/null +++ b/scripts/e2e/swap/zerox.postFee.balanceOf.ts @@ -0,0 +1,230 @@ +/** + * Route: Polygon AAVE → USDC (0x v2 AllowanceHolder) — standalone swap, no bridge + * Flags: post-fee (fee taken from USDC output after swap), output measured as USDC balanceOf delta + * + * Post-fee (bit0=1): feeAmount = FEE_BPS of estimatedOut USDC, deducted from swap output. + * BalanceOf (bit1=1): final USDC amount is measured as router USDC balance change (not returndata). + * + * 0x v2 uses the AllowanceHolder contract (0x000…1fF3) as both the approval target and the swap + * call target. taker=router (router holds the tokens and makes the AH call), recipient=router + * (output lands in router for post-fee deduction and balanceOf delta measurement). + * + * USDC lands in signer's wallet on Polygon. + * + * Usage: + * PRIVATE_KEY=0x... ZEROX_API_KEY=... ts-node scripts/e2e/swap/zerox.postFee.balanceOf.ts + */ +import axios from "axios"; +import { ethers } from "ethers"; +import * as dotenv from "dotenv"; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + ZEROX_API_KEY, + SWAP_SLIPPAGE_BPS, + ALLOWANCE_HOLDER, +} from "../config"; +import { + execViaAH, + ensureAllowanceForAllowanceHolder, +} from "../utils/allowanceHolder"; +import { getWalletErc20Balance } from "../utils/erc20"; +import { ROUTER_ABI } from "../utils/routerAbi"; +import { ZERO_BYTES32, swapArgs } from "../utils/contractTypes"; +import { logTxnSummary } from "../utils/txnLogSummary"; +import { + ensureRouterErc20Balance, +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; + +// post-fee (0x01) | balance-of (0x02) +const FLAGS = 0x03n; +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); +const ZEROX_BASE_URL = "https://api.0x.org"; + +interface ZeroXTransaction { + to: string; + data: string; + value: string; + gas: string; + gasPrice: string; +} + +interface ZeroXQuoteResponse { + transaction: ZeroXTransaction; + buyAmount: string; + minBuyAmount: string; + issues?: { + balance?: { token: string; actual: string; expected: string }; + allowance?: { actual: string; spender: string }; + simulationIncomplete?: boolean; + }; +} + +/** + * Fetches a 0x v2 AllowanceHolder swap quote. + * taker and recipient are both set to the router so the router executes the call + * and receives the output for post-fee deduction and balanceOf delta measurement. + * + * Balance/allowance issues in the response are expected at quote time (the router + * will have the tokens at execution time) and are ignored. + */ +async function fetchZeroXQuote( + sellAmount: bigint, + taker: string, + recipient: string, + txOrigin: string, +): Promise<{ + swapTarget: string; + swapData: string; + buyAmount: bigint; + minBuyAmount: bigint; + value: bigint; +}> { + if (!ZEROX_API_KEY) { + throw new Error("ZEROX_API_KEY env var required"); + } + + const url = + `${ZEROX_BASE_URL}/swap/allowance-holder/quote` + + `?chainId=${CHAIN_IDS.POLYGON}` + + `&buyToken=${TOKENS.USDC_POLYGON_CIRCLE}` + + `&sellToken=${TOKENS.AAVE_POLYGON}` + + `&sellAmount=${sellAmount.toString()}` + + `&taker=${taker}` + + `&recipient=${recipient}` + + `&txOrigin=${txOrigin}` + + `&slippageBps=${SWAP_SLIPPAGE_BPS}`; + + const resp = await axios.get(url, { + headers: { + "0x-api-key": ZEROX_API_KEY, + "0x-version": "v2", + }, + }); + + if (!resp.data?.transaction) { + throw new Error(`0x quote call failed: ${JSON.stringify(resp.data)}`); + } + + const { transaction, buyAmount, minBuyAmount } = resp.data; + return { + swapTarget: transaction.to, + swapData: transaction.data, + buyAmount: BigInt(buyAmount), + minBuyAmount: BigInt(minBuyAmount), + value: BigInt(transaction.value ?? "0"), + }; +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error("PRIVATE_KEY env var required"); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.AAVE_POLYGON, + signerAddress, + provider, + ); + if (walletBalance === 0n) { + throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + } + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error("Balance too small"); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`Flags: 0x${FLAGS.toString(16)} (post-fee, balanceOf)`); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log("Fetching 0x quote (AAVE → USDC)..."); + const { swapTarget, swapData, buyAmount, minBuyAmount, value } = + await fetchZeroXQuote(inputAmount, ROUTER_POLYGON, ROUTER_POLYGON, signerAddress); + + // post-fee: deduct from estimated USDC output after swap + const feeAmount = bpsOf(buyAmount, FEE_BPS); + console.log(` 0x target: ${swapTarget}`); + console.log(` Est. USDC: ${ethers.formatUnits(buyAmount, 6)}`); + console.log( + ` Post-fee: ${ethers.formatUnits(feeAmount, 6)} USDC (${FEE_BPS} bps)`, + ); + console.log(` Min USDC: ${ethers.formatUnits(minBuyAmount, 6)}`); + + // The 0x AllowanceHolder is the approval spender; swapTarget should equal ALLOWANCE_HOLDER + const approvalSpender = ALLOWANCE_HOLDER; + + await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); + await ensureRouterErc20Balance( + signer, + TOKENS.USDC_POLYGON_CIRCLE, + ROUTER_POLYGON, + ); + + const swapApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + approvalSpender, + inputAmount, + ); + + const callData = routerIface.encodeFunctionData("swap", swapArgs( + ZERO_BYTES32, + FLAGS, + { + user: signerAddress, + inputToken: TOKENS.AAVE_POLYGON, + inputAmount: inputAmount, + }, + { receiver: signerAddress, amount: feeAmount }, + { + target: swapTarget, + approvalSpender: swapApprovalSpender, + outputToken: TOKENS.USDC_POLYGON_CIRCLE, + value, + minOutput: minBuyAmount, + returnDataWordOffset: 0n, + }, + swapData, + signerAddress, + )); + + await ensureAllowanceForAllowanceHolder( + signer, + TOKENS.AAVE_POLYGON, + inputAmount, + ); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + inputAmount, + ROUTER_POLYGON, + callData, + ); + + logTxnSummary( + `Polygon AAVE → USDC — 0x postFee/balanceOf`, + CHAIN_IDS.POLYGON, + receipt, + ); + console.log(`\nUSDC is now in signer's wallet on Polygon.`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/swap/zerox.postFee.returndata.ts b/scripts/e2e/swap/zerox.postFee.returndata.ts new file mode 100644 index 0000000..8ec6e73 --- /dev/null +++ b/scripts/e2e/swap/zerox.postFee.returndata.ts @@ -0,0 +1,219 @@ +/** + * Route: Polygon AAVE → USDC (0x v2 AllowanceHolder) — standalone swap, no bridge + * Flags: post-fee (fee taken from USDC output after swap), output read from swap returndata word 0 + * + * Post-fee (bit0=1): feeAmount = FEE_BPS of estimatedOut USDC, deducted from swap output. + * Returndata (bit1=0): final USDC amount is read from word 0 of the swap call returndata. + * + * 0x: taker=router, recipient=router so gross USDC stays on the router for post-fee settle + * (same quote shape as balanceOf post-fee; only the output-measurement flag differs). + * + * USDC lands in signer's wallet on Polygon. + * + * Usage: + * PRIVATE_KEY=0x... ZEROX_API_KEY=... ts-node scripts/e2e/swap/zerox.postFee.returndata.ts + */ +import axios from "axios"; +import { ethers } from "ethers"; +import * as dotenv from "dotenv"; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + ZEROX_API_KEY, + SWAP_SLIPPAGE_BPS, + ALLOWANCE_HOLDER, +} from "../config"; +import { + execViaAH, + ensureAllowanceForAllowanceHolder, +} from "../utils/allowanceHolder"; +import { getWalletErc20Balance } from "../utils/erc20"; +import { ROUTER_ABI } from "../utils/routerAbi"; +import { ZERO_BYTES32, swapArgs } from "../utils/contractTypes"; +import { logTxnSummary } from "../utils/txnLogSummary"; +import { + ensureRouterErc20Balance, +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; + +// post-fee (0x01) | returndata (no 0x02) +const FLAGS = 0x01n; +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); +const ZEROX_BASE_URL = "https://api.0x.org"; + +interface ZeroXTransaction { + to: string; + data: string; + value: string; + gas: string; + gasPrice: string; +} + +interface ZeroXQuoteResponse { + transaction: ZeroXTransaction; + buyAmount: string; + minBuyAmount: string; + issues?: { + balance?: { token: string; actual: string; expected: string }; + allowance?: { actual: string; spender: string }; + simulationIncomplete?: boolean; + }; +} + +/** + * Fetches a 0x v2 AllowanceHolder swap quote. + * taker and recipient are the router so execution and settlement stay on-contract for post-fee. + */ +async function fetchZeroXQuote( + sellAmount: bigint, + taker: string, + recipient: string, + txOrigin: string, +): Promise<{ + swapTarget: string; + swapData: string; + buyAmount: bigint; + minBuyAmount: bigint; + value: bigint; +}> { + if (!ZEROX_API_KEY) { + throw new Error("ZEROX_API_KEY env var required"); + } + + const url = + `${ZEROX_BASE_URL}/swap/allowance-holder/quote` + + `?chainId=${CHAIN_IDS.POLYGON}` + + `&buyToken=${TOKENS.USDC_POLYGON_CIRCLE}` + + `&sellToken=${TOKENS.AAVE_POLYGON}` + + `&sellAmount=${sellAmount.toString()}` + + `&taker=${taker}` + + `&recipient=${recipient}` + + `&txOrigin=${txOrigin}` + + `&slippageBps=${SWAP_SLIPPAGE_BPS}`; + + const resp = await axios.get(url, { + headers: { + "0x-api-key": ZEROX_API_KEY, + "0x-version": "v2", + }, + }); + + if (!resp.data?.transaction) { + throw new Error(`0x quote call failed: ${JSON.stringify(resp.data)}`); + } + + const { transaction, buyAmount, minBuyAmount } = resp.data; + return { + swapTarget: transaction.to, + swapData: transaction.data, + buyAmount: BigInt(buyAmount), + minBuyAmount: BigInt(minBuyAmount), + value: BigInt(transaction.value ?? "0"), + }; +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error("PRIVATE_KEY env var required"); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.AAVE_POLYGON, + signerAddress, + provider, + ); + if (walletBalance === 0n) { + throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + } + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error("Balance too small"); + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`Flags: 0x${FLAGS.toString(16)} (post-fee, returndata)`); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log("Fetching 0x quote (AAVE → USDC)..."); + const { swapTarget, swapData, buyAmount, minBuyAmount, value } = + await fetchZeroXQuote(inputAmount, ROUTER_POLYGON, ROUTER_POLYGON, signerAddress); + + // post-fee: deduct from estimated USDC output after swap + const feeAmount = bpsOf(buyAmount, FEE_BPS); + console.log(` 0x target: ${swapTarget}`); + console.log(` Est. USDC: ${ethers.formatUnits(buyAmount, 6)}`); + console.log( + ` Post-fee: ${ethers.formatUnits(feeAmount, 6)} USDC (${FEE_BPS} bps)`, + ); + console.log(` Min USDC: ${ethers.formatUnits(minBuyAmount, 6)}`); + + const approvalSpender = ALLOWANCE_HOLDER; + + await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); + + const swapApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + approvalSpender, + inputAmount, + ); + + const callData = routerIface.encodeFunctionData("swap", swapArgs( + ZERO_BYTES32, + FLAGS, + { + user: signerAddress, + inputToken: TOKENS.AAVE_POLYGON, + inputAmount: inputAmount, + }, + { receiver: signerAddress, amount: feeAmount }, + { + target: swapTarget, + approvalSpender: swapApprovalSpender, + outputToken: TOKENS.USDC_POLYGON_CIRCLE, + value, + minOutput: minBuyAmount, + returnDataWordOffset: 0n, + }, + swapData, + signerAddress, + )); + + await ensureAllowanceForAllowanceHolder( + signer, + TOKENS.AAVE_POLYGON, + inputAmount, + ); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + inputAmount, + ROUTER_POLYGON, + callData, + ); + + logTxnSummary( + `Polygon AAVE → USDC — 0x postFee/returndata`, + CHAIN_IDS.POLYGON, + receipt, + ); + console.log(`\nUSDC is now in signer's wallet on Polygon.`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/swap/zerox.preFee.balanceOf.ts b/scripts/e2e/swap/zerox.preFee.balanceOf.ts new file mode 100644 index 0000000..d3ec6f8 --- /dev/null +++ b/scripts/e2e/swap/zerox.preFee.balanceOf.ts @@ -0,0 +1,236 @@ +/** + * Route: Polygon AAVE → USDC (0x v2 AllowanceHolder) — standalone swap, no bridge + * Flags: pre-fee (fee taken from AAVE input before swap), output measured as USDC balanceOf delta + * + * Pre-fee (bit0=0): feeAmount = FEE_BPS of inputAmount AAVE, deducted before the swap. + * BalanceOf (bit1=1): final USDC amount is measured as router USDC balance change (not returndata). + * + * 0x v2 uses the AllowanceHolder contract (0x000…1fF3) as both the approval target and the swap + * call target. taker=router (router holds the tokens and makes the AH call), recipient=router + * (output lands in router for balanceOf delta measurement). + * + * The quote is for swapInput (inputAmount − preFeeAmount) so the 0x calldata matches the + * router's approval amount at execution time. + * + * USDC lands in signer's wallet on Polygon. + * + * Usage: + * PRIVATE_KEY=0x... ZEROX_API_KEY=... ts-node scripts/e2e/swap/zerox.preFee.balanceOf.ts + */ +import axios from "axios"; +import { ethers } from "ethers"; +import * as dotenv from "dotenv"; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + ZEROX_API_KEY, + SWAP_SLIPPAGE_BPS, + ALLOWANCE_HOLDER, +} from "../config"; +import { + execViaAH, + ensureAllowanceForAllowanceHolder, +} from "../utils/allowanceHolder"; +import { getWalletErc20Balance } from "../utils/erc20"; +import { ROUTER_ABI } from "../utils/routerAbi"; +import { ZERO_BYTES32, swapArgs } from "../utils/contractTypes"; +import { logTxnSummary } from "../utils/txnLogSummary"; +import { + ensureRouterErc20Balance, +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; + +// pre-fee (0x00) | balance-of (0x02) +const FLAGS = 0x02n; +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); +const ZEROX_BASE_URL = "https://api.0x.org"; + +interface ZeroXTransaction { + to: string; + data: string; + value: string; + gas: string; + gasPrice: string; +} + +interface ZeroXQuoteResponse { + transaction: ZeroXTransaction; + buyAmount: string; + minBuyAmount: string; + issues?: { + balance?: { token: string; actual: string; expected: string }; + allowance?: { actual: string; spender: string }; + simulationIncomplete?: boolean; + }; +} + +/** + * Fetches a 0x v2 AllowanceHolder swap quote. + * taker and recipient are both set to the router so the router executes the call + * and receives the output for balanceOf delta measurement. + * + * Balance/allowance issues in the response are expected at quote time (the router + * will have the tokens at execution time) and are ignored. + */ +async function fetchZeroXQuote( + sellAmount: bigint, + taker: string, + recipient: string, + txOrigin: string, +): Promise<{ + swapTarget: string; + swapData: string; + buyAmount: bigint; + minBuyAmount: bigint; + value: bigint; +}> { + if (!ZEROX_API_KEY) { + throw new Error("ZEROX_API_KEY env var required"); + } + + const url = + `${ZEROX_BASE_URL}/swap/allowance-holder/quote` + + `?chainId=${CHAIN_IDS.POLYGON}` + + `&buyToken=${TOKENS.USDC_POLYGON_CIRCLE}` + + `&sellToken=${TOKENS.AAVE_POLYGON}` + + `&sellAmount=${sellAmount.toString()}` + + `&taker=${taker}` + + `&recipient=${recipient}` + + `&txOrigin=${txOrigin}` + + `&slippageBps=${SWAP_SLIPPAGE_BPS}`; + + const resp = await axios.get(url, { + headers: { + "0x-api-key": ZEROX_API_KEY, + "0x-version": "v2", + }, + }); + + if (!resp.data?.transaction) { + throw new Error(`0x quote call failed: ${JSON.stringify(resp.data)}`); + } + + const { transaction, buyAmount, minBuyAmount } = resp.data; + return { + swapTarget: transaction.to, + swapData: transaction.data, + buyAmount: BigInt(buyAmount), + minBuyAmount: BigInt(minBuyAmount), + value: BigInt(transaction.value ?? "0"), + }; +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error("PRIVATE_KEY env var required"); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.AAVE_POLYGON, + signerAddress, + provider, + ); + if (walletBalance === 0n) { + throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + } + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error("Balance too small"); + + // pre-fee: deduct from input AAVE before the swap + const feeAmount = bpsOf(inputAmount, FEE_BPS); + // quote for the reduced swap input so 0x calldata encodes the correct sell amount + const swapInput = inputAmount - feeAmount; + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`Flags: 0x${FLAGS.toString(16)} (pre-fee, balanceOf)`); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + console.log( + `Pre-fee: ${ethers.formatUnits(feeAmount, 18)} AAVE (${FEE_BPS} bps)`, + ); + console.log(`Swap input: ${ethers.formatUnits(swapInput, 18)} AAVE`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log("Fetching 0x quote (AAVE → USDC)..."); + const { swapTarget, swapData, buyAmount, minBuyAmount, value } = + await fetchZeroXQuote(swapInput, ROUTER_POLYGON, ROUTER_POLYGON, signerAddress); + + console.log(` 0x target: ${swapTarget}`); + console.log(` Est. USDC: ${ethers.formatUnits(buyAmount, 6)}`); + console.log(` Min USDC: ${ethers.formatUnits(minBuyAmount, 6)}`); + + // The 0x AllowanceHolder is the approval spender; swapTarget should equal ALLOWANCE_HOLDER + const approvalSpender = ALLOWANCE_HOLDER; + await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); + await ensureRouterErc20Balance( + signer, + TOKENS.USDC_POLYGON_CIRCLE, + ROUTER_POLYGON, + ); + + const swapApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + approvalSpender, + swapInput, + ); + + const callData = routerIface.encodeFunctionData("swap", swapArgs( + ZERO_BYTES32, + FLAGS, + { + user: signerAddress, + inputToken: TOKENS.AAVE_POLYGON, + inputAmount: inputAmount, + }, + { receiver: signerAddress, amount: feeAmount }, + { + target: swapTarget, + approvalSpender: swapApprovalSpender, + outputToken: TOKENS.USDC_POLYGON_CIRCLE, + value, + minOutput: minBuyAmount, + returnDataWordOffset: 0n, + }, + swapData, + signerAddress, + )); + + await ensureAllowanceForAllowanceHolder( + signer, + TOKENS.AAVE_POLYGON, + inputAmount, + ); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + inputAmount, + ROUTER_POLYGON, + callData, + ); + + logTxnSummary( + `Polygon AAVE → USDC — 0x preFee/balanceOf`, + CHAIN_IDS.POLYGON, + receipt, + ); + console.log(`\nUSDC is now in signer's wallet on Polygon.`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/swap/zerox.preFee.returndata.ts b/scripts/e2e/swap/zerox.preFee.returndata.ts new file mode 100644 index 0000000..b955005 --- /dev/null +++ b/scripts/e2e/swap/zerox.preFee.returndata.ts @@ -0,0 +1,227 @@ +/** + * Route: Polygon AAVE → USDC (0x v2 AllowanceHolder) — standalone swap, no bridge + * Flags: pre-fee (fee taken from AAVE input before swap), output read from swap returndata word 0 + * + * Pre-fee (bit0=0): feeAmount = FEE_BPS of inputAmount AAVE, deducted before the swap. + * Returndata (bit1=0): final USDC amount is read from word 0 of the swap call returndata. + * + * 0x: taker=router (AllowanceHolder entry), recipient=signer so output USDC goes to the user + * while the router decodes `filledAmount` / return data per `returnDataWordOffset`. + * + * Quote uses swapInput (inputAmount − preFeeAmount) so calldata matches execution. + * + * USDC lands in signer's wallet on Polygon. + * + * Usage: + * PRIVATE_KEY=0x... ZEROX_API_KEY=... ts-node scripts/e2e/swap/zerox.preFee.returndata.ts + */ +import axios from "axios"; +import { ethers } from "ethers"; +import * as dotenv from "dotenv"; +dotenv.config(); + +import { + CHAIN_IDS, + routerAddressForChain, + TOKENS, + FEE_BPS, + bpsOf, + RPC, + ZEROX_API_KEY, + SWAP_SLIPPAGE_BPS, + ALLOWANCE_HOLDER, +} from "../config"; +import { + execViaAH, + ensureAllowanceForAllowanceHolder, +} from "../utils/allowanceHolder"; +import { getWalletErc20Balance } from "../utils/erc20"; +import { ROUTER_ABI } from "../utils/routerAbi"; +import { ZERO_BYTES32, swapArgs } from "../utils/contractTypes"; +import { logTxnSummary } from "../utils/txnLogSummary"; +import { + ensureRouterErc20Balance, +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; + +// pre-fee (0x00) | returndata (no 0x02) +const FLAGS = 0x00n; +const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); +const ZEROX_BASE_URL = "https://api.0x.org"; + +interface ZeroXTransaction { + to: string; + data: string; + value: string; + gas: string; + gasPrice: string; +} + +interface ZeroXQuoteResponse { + transaction: ZeroXTransaction; + buyAmount: string; + minBuyAmount: string; + issues?: { + balance?: { token: string; actual: string; expected: string }; + allowance?: { actual: string; spender: string }; + simulationIncomplete?: boolean; + }; +} + +/** + * Fetches a 0x v2 AllowanceHolder swap quote. + * taker=router, recipient=user so bought USDC is delivered to the user (pre-fee + returndata). + */ +async function fetchZeroXQuote( + sellAmount: bigint, + taker: string, + recipient: string, + txOrigin: string, +): Promise<{ + swapTarget: string; + swapData: string; + buyAmount: bigint; + minBuyAmount: bigint; + value: bigint; +}> { + if (!ZEROX_API_KEY) { + throw new Error("ZEROX_API_KEY env var required"); + } + + const url = + `${ZEROX_BASE_URL}/swap/allowance-holder/quote` + + `?chainId=${CHAIN_IDS.POLYGON}` + + `&buyToken=${TOKENS.USDC_POLYGON_CIRCLE}` + + `&sellToken=${TOKENS.AAVE_POLYGON}` + + `&sellAmount=${sellAmount.toString()}` + + `&taker=${taker}` + + `&recipient=${recipient}` + + `&txOrigin=${txOrigin}` + + `&slippageBps=${SWAP_SLIPPAGE_BPS}`; + + const resp = await axios.get(url, { + headers: { + "0x-api-key": ZEROX_API_KEY, + "0x-version": "v2", + }, + }); + + if (!resp.data?.transaction) { + throw new Error(`0x quote call failed: ${JSON.stringify(resp.data)}`); + } + + const { transaction, buyAmount, minBuyAmount } = resp.data; + return { + swapTarget: transaction.to, + swapData: transaction.data, + buyAmount: BigInt(buyAmount), + minBuyAmount: BigInt(minBuyAmount), + value: BigInt(transaction.value ?? "0"), + }; +} + +async function main() { + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) throw new Error("PRIVATE_KEY env var required"); + + const provider = new ethers.JsonRpcProvider(RPC.POLYGON); + const signer = new ethers.Wallet(privateKey, provider); + const signerAddress = await signer.getAddress(); + + const { balance: walletBalance } = await getWalletErc20Balance( + TOKENS.AAVE_POLYGON, + signerAddress, + provider, + ); + if (walletBalance === 0n) { + throw new Error(`Signer ${signerAddress} has zero AAVE on Polygon`); + } + + const inputAmount = walletBalance - 20n; + if (inputAmount === 0n) throw new Error("Balance too small"); + + // pre-fee: deduct from input AAVE before the swap + const feeAmount = bpsOf(inputAmount, FEE_BPS); + const swapInput = inputAmount - feeAmount; + + console.log(`Signer: ${signerAddress}`); + console.log(`Router: ${ROUTER_POLYGON}`); + console.log(`Flags: 0x${FLAGS.toString(16)} (pre-fee, returndata)`); + console.log(`AAVE balance: ${ethers.formatUnits(walletBalance, 18)}`); + console.log( + `Pre-fee: ${ethers.formatUnits(feeAmount, 18)} AAVE (${FEE_BPS} bps)`, + ); + console.log(`Swap input: ${ethers.formatUnits(swapInput, 18)} AAVE`); + + const routerIface = new ethers.Interface(ROUTER_ABI); + + console.log("Fetching 0x quote (AAVE → USDC)..."); + const { swapTarget, swapData, buyAmount, minBuyAmount, value } = + await fetchZeroXQuote( + swapInput, + ROUTER_POLYGON, + signerAddress, + signerAddress, + ); + + console.log(` 0x target: ${swapTarget}`); + console.log(` Est. USDC: ${ethers.formatUnits(buyAmount, 6)}`); + console.log(` Min USDC: ${ethers.formatUnits(minBuyAmount, 6)}`); + const approvalSpender = ALLOWANCE_HOLDER; + await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); + + const swapApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + approvalSpender, + swapInput, + ); + + const callData = routerIface.encodeFunctionData("swap", swapArgs( + ZERO_BYTES32, + FLAGS, + { + user: signerAddress, + inputToken: TOKENS.AAVE_POLYGON, + inputAmount: inputAmount, + }, + { receiver: signerAddress, amount: feeAmount }, + { + target: swapTarget, + approvalSpender: swapApprovalSpender, + outputToken: TOKENS.USDC_POLYGON_CIRCLE, + value, + minOutput: minBuyAmount, + returnDataWordOffset: 0n, + }, + swapData, + signerAddress, + )); + + await ensureAllowanceForAllowanceHolder( + signer, + TOKENS.AAVE_POLYGON, + inputAmount, + ); + const receipt = await execViaAH( + signer, + ROUTER_POLYGON, + TOKENS.AAVE_POLYGON, + inputAmount, + ROUTER_POLYGON, + callData, + ); + + logTxnSummary( + `Polygon AAVE → USDC — 0x preFee/returndata`, + CHAIN_IDS.POLYGON, + receipt, + ); + console.log(`\nUSDC is now in signer's wallet on Polygon.`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/e2e/utils/allowanceHolder.ts b/scripts/e2e/utils/allowanceHolder.ts new file mode 100644 index 0000000..aab6a1e --- /dev/null +++ b/scripts/e2e/utils/allowanceHolder.ts @@ -0,0 +1,138 @@ +/** + * AllowanceHolder helpers. + * + * The AH.exec() flow in a single transaction: + * 1. User calls AllowanceHolder.exec(operator, token, amount, target, data, { value }) + * 2. AH grants transient allowance: operator may pull `amount` of `token` from msg.sender + * 3. AH calls target(data) forwarding msg.value + * 4. Inside target: AllowanceHolder.transferFrom(token, msg.sender_original, recipient, amount) + * pulls the tokens using the transient allowance (cleared after the call) + * + * The router's _pullFromUser uses the same AH.transferFrom to move tokens in. + */ +import { ethers, MaxUint256, Signer } from 'ethers'; +import { ALLOWANCE_HOLDER } from '../config'; +import { getErc20Contract } from './erc20'; + +/** + * Minimal ABI fragment for AllowanceHolder — only the exec function we call. + * + * IMPORTANT: the amount parameter MUST be uint256 (not uint160). AllowanceHolder + * is entirely implemented in its fallback() function, which dispatches by comparing + * the incoming 4-byte selector against IAllowanceHolder.exec.selector. That selector + * is keccak256("exec(address,address,uint256,address,bytes)")[0:4] = 0x2213bc0b. + * Using uint160 would produce a different selector and the call would revert. + * + * Full ABI reference: https://docs.0x.org/docs/core-concepts/contracts#allowanceholder-recommended + */ +export const ALLOWANCE_HOLDER_ABI = [ + 'function exec(address operator, address token, uint256 amount, address target, bytes calldata data) external payable returns (bytes memory result)', +] as const; + +/** + * Returns an ethers Contract instance for AllowanceHolder connected to the + * given signer. + */ +export function getAllowanceHolderContract(signer: Signer): ethers.Contract { + return new ethers.Contract(ALLOWANCE_HOLDER, ALLOWANCE_HOLDER_ABI, signer); +} + +/** + * Reads `token.allowance(owner, AllowanceHolder)` and, if it is below + * `requiredAmount`, submits `approve(AllowanceHolder, MaxUint256)`. + * + * AH.exec pulls FROM the user's wallet via ephemeral allowance keyed by operator; + * the user must have a persistent ERC20 approval to AH first. + */ +export async function ensureAllowanceForAllowanceHolder( + signer: Signer, + token: string, + requiredAmount: bigint, +): Promise { + const owner = await signer.getAddress(); + const erc20 = getErc20Contract(token, signer); + const allowanceRaw = await erc20.allowance(owner, ALLOWANCE_HOLDER); + const allowance = + typeof allowanceRaw === 'bigint' ? allowanceRaw : BigInt(allowanceRaw.toString()); + + if (allowance >= requiredAmount) { + console.log( + `AllowanceHolder allowance OK (${allowance.toString()} >= ${requiredAmount.toString()})`, + ); + return; + } + + console.log( + `Approving AllowanceHolder: allowance ${allowance.toString()} < required ${requiredAmount.toString()}, sending approve...`, + ); + const tx = await erc20.approve(ALLOWANCE_HOLDER, MaxUint256); + console.log(`approve tx sent: ${tx.hash}`); + const receipt = await tx.wait(); + if (!receipt || receipt.status !== 1) { + throw new Error(`ERC20 approve to AllowanceHolder failed: ${tx.hash}`); + } + console.log('AllowanceHolder approval confirmed'); +} + +/** + * Builds and sends an AllowanceHolder.exec() transaction. + * + * @param signer - The EOA signing and paying for the tx (= the "user") + * @param operator - The contract that will pull funds (our router) + * @param token - ERC-20 token to grant ephemeral allowance for + * @param amount - Exact amount the operator is allowed to pull + * @param target - Contract to call after granting the allowance (our router) + * @param callData - Encoded function call on `target` + * @param txValue - Optional ETH to forward with the call (for native-token flows) + */ +export async function execViaAH( + signer: Signer, + operator: string, + token: string, + amount: bigint, + target: string, + callData: string, + txValue?: bigint, +): Promise { + const ah = getAllowanceHolderContract(signer); + + const tx = await ah.exec(operator, token, amount, target, callData, { + value: txValue ?? 0n, + }); + + console.log(`AllowanceHolder.exec tx sent: ${tx.hash}`); + const receipt = await tx.wait(); + if (!receipt || receipt.status !== 1) { + throw new Error(`Transaction failed: ${tx.hash}`); + } + console.log(`Transaction confirmed in block ${receipt.blockNumber}`); + return receipt; +} + +/** + * Sends a transaction directly to `target` (the router) without routing through + * AllowanceHolder. Use this when the input token is native ETH/POL — the router's + * `_pullFromUser` path for native tokens only checks `msg.value >= amount` and does + * NOT enforce `_msgSender() == user` nor call `AH.transferFrom`. For modular + * execution (`performActions`) there is no `_pullFromUser` at all. + * + * @param signer - EOA signing and paying for the tx + * @param target - Router contract address + * @param callData - Encoded router entrypoint calldata (`swap`, `bridge`, `performActions`, etc.) + * @param txValue - ETH to forward (inputAmount + nativeFeeWithBuffer for native input) + */ +export async function execDirect( + signer: Signer, + target: string, + callData: string, + txValue: bigint, +): Promise { + const tx = await signer.sendTransaction({ to: target, data: callData, value: txValue }); + console.log(`Direct router tx sent: ${tx.hash}`); + const receipt = await tx.wait(); + if (!receipt || receipt.status !== 1) { + throw new Error(`Transaction failed: ${tx.hash}`); + } + console.log(`Transaction confirmed in block ${receipt.blockNumber}`); + return receipt; +} diff --git a/scripts/e2e/utils/contractTypes.ts b/scripts/e2e/utils/contractTypes.ts new file mode 100644 index 0000000..9bf07c2 --- /dev/null +++ b/scripts/e2e/utils/contractTypes.ts @@ -0,0 +1,105 @@ +/** + * TypeScript interfaces mirroring OpenRouter Solidity structs. + * Field names and order must match the compiler ABI encoding. + */ + +export interface InputData { + user: string; + inputToken: string; + inputAmount: bigint; +} + +export interface FeeData { + receiver: string; + amount: bigint; +} + +export interface SwapData { + target: string; + approvalSpender: string; + outputToken: string; + value: bigint; + minOutput: bigint; + returnDataWordOffset: bigint; +} + +export interface BridgeData { + target: string; + approvalSpender: string; + value: bigint; +} + +export const POST_FEE_FLAG = 0x01n; +export const BALANCE_FLAG = 0x02n; +export const BRIDGE_VALUE_FLAG = 0x04n; +export const BRIDGE_AMOUNT_POSITION_FLAG = 0x08n; +export const BRIDGE_AMOUNT_POSITION_SHIFT = 16n; +export const MAX_BRIDGE_AMOUNT_POSITION = 0xffffn; + +/** 32-byte zero; use as `quoteId` when scripts do not assign a correlation id. */ +export const ZERO_BYTES32 = + '0x0000000000000000000000000000000000000000000000000000000000000000' as const; + +export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; + +/** Convenience: empty fee (no fee taken) */ +export const NO_FEE: FeeData = { receiver: ZERO_ADDRESS, amount: 0n }; + +export function bridgeAmountPositionFlag(position: bigint | number): bigint { + const positionBigInt = BigInt(position); + if (positionBigInt < 0n || positionBigInt > MAX_BRIDGE_AMOUNT_POSITION) { + throw new Error(`bridge amount position exceeds uint16: ${positionBigInt}`); + } + return BRIDGE_AMOUNT_POSITION_FLAG | (positionBigInt << BRIDGE_AMOUNT_POSITION_SHIFT); +} + +export function swapArgs( + quoteId: string, + flags: bigint, + input: InputData, + fee: FeeData, + swapData: SwapData, + swapCallData: string, + receiver: string, +): readonly [string, bigint, InputData, FeeData, SwapData, string, string] { + return [quoteId, flags, input, fee, swapData, swapCallData, receiver] as const; +} + +export function swapAndBridgeArgs( + quoteId: string, + flags: bigint, + input: InputData, + fee: FeeData, + swapData: SwapData, + swapCallData: string, + bridgeData: BridgeData, + bridgeCallData: string, +): readonly [ + string, + bigint, + InputData, + FeeData, + SwapData, + string, + BridgeData, + string, +] { + return [quoteId, flags, input, fee, swapData, swapCallData, bridgeData, bridgeCallData] as const; +} + +export function bridgeArgs( + quoteId: string, + input: InputData, + fee: FeeData, + bridgeData: BridgeData, + bridgeCallData: string, +): readonly [string, InputData, FeeData, BridgeData, string] { + return [quoteId, input, fee, bridgeData, bridgeCallData] as const; +} + +export function performActionsArgs( + quoteId: string, + actions: { actionInfo: bigint | string; data: string; splices: (bigint | string)[] }[], +): readonly [string, typeof actions] { + return [quoteId, actions] as const; +} diff --git a/scripts/e2e/utils/erc20.ts b/scripts/e2e/utils/erc20.ts new file mode 100644 index 0000000..87484af --- /dev/null +++ b/scripts/e2e/utils/erc20.ts @@ -0,0 +1,88 @@ +/** + * ERC-20 calldata encoding helpers used when building modular Action arrays. + * These produce raw encoded bytes rather than making any actual calls, so they + * work for both building action.data fields and for direct contract calls. + */ +import { ethers } from 'ethers'; + +const ERC20_IFACE = new ethers.Interface([ + 'function approve(address spender, uint256 amount) external returns (bool)', + 'function transfer(address recipient, uint256 amount) external returns (bool)', + 'function balanceOf(address account) external view returns (uint256)', + 'function allowance(address owner, address spender) external view returns (uint256)', +]); + +/** + * Encodes ERC-20 approve(spender, amount) calldata. + */ +export function encodeApprove(spender: string, amount: bigint): string { + return ERC20_IFACE.encodeFunctionData('approve', [spender, amount]); +} + +/** + * Encodes ERC-20 transfer(recipient, amount) calldata. + */ +export function encodeTransfer(recipient: string, amount: bigint): string { + return ERC20_IFACE.encodeFunctionData('transfer', [recipient, amount]); +} + +/** + * Encodes ERC-20 balanceOf(account) calldata for use in a STATICCALL action. + * The return value is a 32-byte ABI-encoded uint256 — can be spliced directly + * into a subsequent action's calldata. + */ +export function encodeBalanceOf(account: string): string { + return ERC20_IFACE.encodeFunctionData('balanceOf', [account]); +} + +/** + * Returns an ethers Contract instance for a standard ERC-20 token (read-only). + * Pass a provider or signer as the second argument. + */ +export function getErc20Contract(tokenAddress: string, providerOrSigner: ethers.Provider | ethers.Signer): ethers.Contract { + return new ethers.Contract( + tokenAddress, + [ + 'function approve(address spender, uint256 amount) external returns (bool)', + 'function transfer(address recipient, uint256 amount) external returns (bool)', + 'function allowance(address owner, address spender) external view returns (uint256)', + 'function balanceOf(address account) external view returns (uint256)', + 'function decimals() external view returns (uint8)', + ], + providerOrSigner, + ); +} + +/** + * Reads an ERC-20 balance and decimals for `owner`, similar to bungee-sandbox + * `getTokenBalance` patterns (fund the signer, then swap/bridge whole balance). + */ +export async function getWalletErc20Balance( + tokenAddress: string, + owner: string, + provider: ethers.Provider, +): Promise<{ balance: bigint; decimals: number }> { + const token = getErc20Contract(tokenAddress, provider); + const [balanceRaw, decimalsRaw] = await Promise.all([ + token.balanceOf(owner), + token.decimals(), + ]); + const balance = typeof balanceRaw === 'bigint' ? balanceRaw : BigInt(balanceRaw.toString()); + + return { balance, decimals: Number(decimalsRaw) }; +} + +/** + * Convenience: approve a spender with a real provider transaction. + */ +export async function approveErc20( + tokenAddress: string, + spender: string, + amount: bigint, + signer: ethers.Signer, +): Promise { + const token = getErc20Contract(tokenAddress, signer); + const tx = await token.approve(spender, amount); + await tx.wait(); + console.log(`Approved ${spender} for ${amount} of ${tokenAddress}`); +} diff --git a/scripts/e2e/utils/modularActionsBuilder/README.md b/scripts/e2e/utils/modularActionsBuilder/README.md new file mode 100644 index 0000000..3768bc6 --- /dev/null +++ b/scripts/e2e/utils/modularActionsBuilder/README.md @@ -0,0 +1,105 @@ +# Modular Actions Builder + +Dependency-free helper for formatting packed `performActions(Action[])` +payloads from provider SDK/API calldata. + +```js +const { ModularActionsBuilder } = require("./modularActionsBuilder/index"); + +const exec = new ModularActionsBuilder({ + routeId: "openocean-stargate-native", + chainId: 42161, +}); + +exec.call(USDC, approveCalldata).as("approve"); +exec.call(OPENOCEAN_EXCHANGE_V2, openOceanSwapCalldata).as("swap"); + +exec + .staticCall(MATH_MANIPULATOR, percentCalldataWithZeroAmount) + .as("routeFee") + .spliceArg(0, exec.ref("swap").returnWord()); + +exec + .nativeCall(FEE_RECIPIENT) + .as("feeTransfer") + .valueFrom(exec.ref("routeFee").returnWord()); + +const calldata = exec.toCalldata(); +``` + +`toActions()` returns the packed modular `Action[]` ABI shape: + +```js +[ + { + actionInfo, // packed callType | storeResult << 8 | target << 16 + data, + splices, // uint256[] packed as sourceActionIndex | srcOffset << 64 | dstOffset << 128 | length << 192 + }, +]; +``` + +Splice sources are marked as `storeResult` automatically. For an action whose returndata should be returned but is not used by a splice, call `.storeResult()` on the handle or pass `storeResult: true` to `action(...)`. + +## Offset Helpers + +- `spliceArg(argIndex, source)` writes a 32-byte source into a normal ABI calldata argument. It maps `argIndex` to `4 + argIndex * 32`. +- `valueFrom(source)` writes a 32-byte source into the leading value word used by `CALL_WITH_NATIVE`. +- `splicePayloadWord(payloadOffset, source)` writes into the payload of a `CALL_WITH_NATIVE`. It automatically adds the 32-byte value prefix. +- `splicePayload(payloadOffset, source, length)` does the same for non-word slices. +- `patchWord(dstOffset, source)` writes directly to an absolute calldata offset. + +## Across Shape + +```js +exec.call(ARBITRUM_USDC, approveOpenOcean).as("approve"); +exec.call(OPENOCEAN_EXCHANGE_V2, openOceanSwapCalldata).as("swap"); + +exec + .staticCall(ACROSS_AMOUNT_MANIPULATOR, deriveOutputAmountWithZeroInput) + .as("acrossOutputAmount") + .spliceArg(0, exec.ref("swap").returnWord()); + +exec.call(ARBITRUM_WETH, approveAcross).as("approveAcross"); + +exec + .call(ACROSS_SPOKE_POOL, acrossDepositWithZeroAmounts) + .as("acrossDeposit") + .patchWord(132, exec.ref("swap").returnWord()) + .patchWord(164, exec.ref("acrossOutputAmount").returnWord()); +``` + +## Stargate Native Shape + +```js +exec.call(ARBITRUM_USDC, approveOpenOcean).as("approve"); +exec.call(OPENOCEAN_EXCHANGE_V2, openOceanSwapCalldata).as("swap"); + +exec + .staticCall(MATH_MANIPULATOR, percentCalldataWithZeroAmount) + .as("routeFee") + .spliceArg(0, exec.ref("swap").returnWord()); + +exec.nativeCall(FEE_RECIPIENT).as("feeTransfer").valueFrom(exec.ref("routeFee").returnWord()); + +exec + .staticCall(MATH_MANIPULATOR, subtractWithZeroArgs) + .as("postFeeAmount") + .spliceArg(0, exec.ref("swap").returnWord()) + .spliceArg(1, exec.ref("routeFee").returnWord()); + +exec + .staticCall(MATH_MANIPULATOR, subtractNativeFeeFromZeroAmount) + .as("bridgeAmount") + .spliceArg(0, exec.ref("postFeeAmount").returnWord()); + +exec + .nativeCall(STARGATE_NATIVE_WRAPPER, stargateSendCalldata) + .as("stargate") + .valueFrom(exec.ref("postFeeAmount").returnWord()) + .splicePayloadWord(STARGATE_AMOUNT_OFFSET, exec.ref("bridgeAmount").returnWord()); +``` + +Use `toActions()` when the caller already has an ABI encoder for the packed modular action tuple. Use +`toLogicalActions()` for the readable builder shape. Use `toCalldata()` when you need raw +`performActions(Action[])` calldata. diff --git a/scripts/e2e/utils/modularActionsBuilder/index.d.ts b/scripts/e2e/utils/modularActionsBuilder/index.d.ts new file mode 100644 index 0000000..22a492e --- /dev/null +++ b/scripts/e2e/utils/modularActionsBuilder/index.d.ts @@ -0,0 +1,108 @@ +export type Hex = string; +export type Address = Hex; +export type BigNumberish = bigint | number | string; + +export interface ExecutionContext { + [key: string]: unknown; +} + +export interface ReturnSource { + sourceActionIndex: number; + srcOffset: number; + length: number; +} + +export interface Splice { + sourceActionIndex: BigNumberish; + srcOffset: BigNumberish; + dstOffset: BigNumberish; + length: BigNumberish; +} + +export interface LogicalAction { + callType: number; + target: Address; + data: Hex; + storeResult: boolean; + splices: Splice[]; +} + +export interface ModularAction { + actionInfo: BigNumberish; + data: Hex; + splices: BigNumberish[]; +} + +export type Action = LogicalAction; + +export declare const PERFORM_ACTIONS_SELECTOR: "0x197aa51e"; +/** @deprecated Use PERFORM_ACTIONS_SELECTOR */ +export declare const PERFORM_MODULAR_EXECUTION_SELECTOR: "0x197aa51e"; + +export declare const CallType: Readonly<{ + CALL: 0; + STATICCALL: 1; + CALL_WITH_NATIVE: 2; +}>; + +export declare const Offset: Readonly<{ + selectorArg(argIndex: BigNumberish): number; + nativePayload(payloadOffset: BigNumberish): number; +}>; + +export declare class ModularActionsBuilder { + context: ExecutionContext; + constructor(context?: ExecutionContext); + call(target: Address, data: Hex): ActionHandle; + staticCall(target: Address, data: Hex): ActionHandle; + callWithNative(target: Address, payload?: Hex, value?: BigNumberish): ActionHandle; + nativeCall(target: Address, payload?: Hex, value?: BigNumberish): ActionHandle; + action(action: { + callType: BigNumberish; + target: Address; + data?: Hex; + splices?: Splice[]; + storeResult?: boolean; + }): ActionHandle; + ref(labelOrIndex: string | BigNumberish): ActionRef; + actionAt(index: BigNumberish): LogicalAction; + toActions(): ModularAction[]; + toLogicalActions(): LogicalAction[]; + toJSON(): unknown; + toCalldata(): Hex; +} + +export declare class ActionHandle { + readonly execution: ModularActionsBuilder; + readonly index: number; + as(label: string): this; + label(label: string): this; + ref(): ActionRef; + return(offset?: BigNumberish, length?: BigNumberish): ReturnSource; + returnWord(offset?: BigNumberish): ReturnSource; + splice(source: ReturnSource, dstOffset: BigNumberish, length?: BigNumberish): this; + spliceWord(dstOffset: BigNumberish, source: ReturnSource): this; + spliceArg(argIndex: BigNumberish, source: ReturnSource): this; + spliceNativeValue(source: ReturnSource): this; + valueFrom(source: ReturnSource): this; + splicePayloadWord(payloadOffset: BigNumberish, source: ReturnSource): this; + splicePayload(payloadOffset: BigNumberish, source: ReturnSource, length?: BigNumberish): this; + patchWord(dstOffset: BigNumberish, source: ReturnSource): this; + storeResult(value?: boolean): this; +} + +export declare class ActionRef { + readonly index: number; + readonly label?: string; + return(srcOffset?: BigNumberish, length?: BigNumberish): ReturnSource; + returnWord(srcOffset?: BigNumberish): ReturnSource; +} + +export declare function concatHex(values: Hex[]): Hex; +export declare function encodePerformActionsArgs(actions: Array): Hex; +/** @deprecated Use encodePerformActionsArgs */ +export declare function encodePerformModularExecutionArgs(actions: Array): Hex; +export declare function encodeWord(value: BigNumberish): Hex; +export declare function packActionInfo(action: Pick): bigint; +export declare function packSpliceInfo(splice: Splice): bigint; +export declare function toModularAction(action: LogicalAction): ModularAction; diff --git a/scripts/e2e/utils/modularActionsBuilder/index.js b/scripts/e2e/utils/modularActionsBuilder/index.js new file mode 100644 index 0000000..4c52612 --- /dev/null +++ b/scripts/e2e/utils/modularActionsBuilder/index.js @@ -0,0 +1,455 @@ +"use strict"; + +const PERFORM_ACTIONS_SELECTOR = "0x197aa51e"; +/** @deprecated Use PERFORM_ACTIONS_SELECTOR */ +const PERFORM_MODULAR_EXECUTION_SELECTOR = PERFORM_ACTIONS_SELECTOR; +const WORD_BYTES = 32; +const WORD_HEX_CHARS = WORD_BYTES * 2; +const UINT256_MAX = (1n << 256n) - 1n; +const UINT64_MAX = (1n << 64n) - 1n; + +const CallType = Object.freeze({ + CALL: 0, + STATICCALL: 1, + CALL_WITH_NATIVE: 2, +}); + +const Offset = Object.freeze({ + selectorArg: (argIndex) => 4 + WORD_BYTES * checkedIndex(argIndex, "argIndex"), + nativePayload: (payloadOffset) => WORD_BYTES + checkedIndex(payloadOffset, "payloadOffset"), +}); + +class ModularActionsBuilder { + constructor(context = {}) { + this.context = { ...context }; + this._actions = []; + this._labels = new Map(); + } + + call(target, data) { + return this.action({ callType: CallType.CALL, target, data }); + } + + staticCall(target, data) { + return this.action({ callType: CallType.STATICCALL, target, data }); + } + + callWithNative(target, payload = "0x", value = 0n) { + return this.action({ + callType: CallType.CALL_WITH_NATIVE, + target, + data: concatHex([encodeWord(value), payload]), + }); + } + + nativeCall(target, payload = "0x", value = 0n) { + return this.callWithNative(target, payload, value); + } + + action({ callType, target, data = "0x", splices = [], storeResult = false }) { + const actionIndex = this._actions.length; + const action = { + callType: checkedCallType(callType), + target: normalizeAddress(target), + data: normalizeHex(data, "data"), + storeResult: Boolean(storeResult), + splices: splices.map((splice, index) => normalizeSplice(splice, `splices[${index}]`)), + }; + for (const splice of action.splices) { + validateSpliceForAction(actionIndex, action, splice); + this._actions[splice.sourceActionIndex].storeResult = true; + } + this._actions.push(action); + return new ActionHandle(this, this._actions.length - 1); + } + + ref(labelOrIndex) { + if (typeof labelOrIndex === "string") { + if (!this._labels.has(labelOrIndex)) { + throw new Error(`Unknown action label: ${labelOrIndex}`); + } + return new ActionRef(this._labels.get(labelOrIndex), labelOrIndex); + } + return new ActionRef(checkedIndex(labelOrIndex, "actionIndex")); + } + + actionAt(index) { + const checked = checkedIndex(index, "actionIndex"); + const action = this._actions[checked]; + if (!action) throw new Error(`Unknown action index: ${checked}`); + return action; + } + + toActions() { + this._markSpliceSources(); + return this._actions.map(toModularAction); + } + + toLogicalActions() { + this._markSpliceSources(); + return this._actions.map(cloneAction); + } + + toJSON() { + this._markSpliceSources(); + return this._actions.map((action) => ({ + callType: action.callType, + target: action.target, + data: action.data, + storeResult: action.storeResult, + actionInfo: packActionInfo(action).toString(), + splices: action.splices.map((splice) => ({ + sourceActionIndex: String(splice.sourceActionIndex), + srcOffset: String(splice.srcOffset), + dstOffset: String(splice.dstOffset), + length: String(splice.length), + spliceInfo: packSpliceInfo(splice).toString(), + })), + })); + } + + toCalldata() { + this._markSpliceSources(); + return concatHex([PERFORM_ACTIONS_SELECTOR, encodePerformActionsArgs(this._actions)]); + } + + _label(index, label) { + if (!label || typeof label !== "string") throw new Error("Action label must be a non-empty string"); + if (this._labels.has(label)) throw new Error(`Duplicate action label: ${label}`); + this._labels.set(label, index); + return new ActionRef(index, label); + } + + _splice(index, splice) { + const action = this.actionAt(index); + const normalized = normalizeSplice(splice, "splice"); + validateSpliceForAction(index, action, normalized); + this._actions[normalized.sourceActionIndex].storeResult = true; + action.splices.push(normalized); + } + + _markSpliceSources() { + for (const action of this._actions) { + for (const splice of action.splices) { + this._actions[splice.sourceActionIndex].storeResult = true; + } + } + } +} + +class ActionHandle { + constructor(execution, index) { + this.execution = execution; + this.index = index; + } + + as(label) { + this.execution._label(this.index, label); + return this; + } + + label(label) { + return this.as(label); + } + + ref() { + return new ActionRef(this.index); + } + + return(offset = 0, length = WORD_BYTES) { + return this.ref().return(offset, length); + } + + returnWord(offset = 0) { + return this.ref().returnWord(offset); + } + + splice(source, dstOffset, length = source.length) { + this.execution._splice(this.index, { + sourceActionIndex: source.sourceActionIndex, + srcOffset: source.srcOffset, + dstOffset, + length, + }); + return this; + } + + spliceWord(dstOffset, source) { + return this.splice(source, dstOffset, WORD_BYTES); + } + + spliceArg(argIndex, source) { + return this.spliceWord(Offset.selectorArg(argIndex), source); + } + + spliceNativeValue(source) { + return this.spliceWord(0, source); + } + + valueFrom(source) { + return this.spliceNativeValue(source); + } + + splicePayloadWord(payloadOffset, source) { + return this.spliceWord(Offset.nativePayload(payloadOffset), source); + } + + splicePayload(payloadOffset, source, length = source.length) { + return this.splice(source, Offset.nativePayload(payloadOffset), length); + } + + patchWord(dstOffset, source) { + return this.spliceWord(dstOffset, source); + } + + storeResult(value = true) { + this.execution.actionAt(this.index).storeResult = Boolean(value); + return this; + } +} + +class ActionRef { + constructor(index, label) { + this.index = index; + this.label = label; + } + + return(srcOffset = 0, length = WORD_BYTES) { + return { + sourceActionIndex: this.index, + srcOffset: checkedIndex(srcOffset, "srcOffset"), + length: checkedIndex(length, "length"), + }; + } + + returnWord(srcOffset = 0) { + return this.return(srcOffset, WORD_BYTES); + } +} + +function encodePerformActionsArgs(actions) { + return concatHex([encodeWord(WORD_BYTES), encodeActionArray(prepareActionsForEncoding(actions))]); +} + +/** @deprecated Use encodePerformActionsArgs */ +function encodePerformModularExecutionArgs(actions) { + return encodePerformActionsArgs(actions); +} + +function encodeActionArray(actions) { + const encodedActions = actions.map(encodeActionTuple); + let nextOffset = WORD_BYTES * actions.length; + const offsets = []; + for (const encodedAction of encodedActions) { + offsets.push(encodeWord(nextOffset)); + nextOffset += hexByteLength(encodedAction); + } + return concatHex([encodeWord(actions.length), ...offsets, ...encodedActions]); +} + +function encodeActionTuple(action) { + const packedAction = isPackedAction(action) ? normalizePackedAction(action) : toModularAction(action); + const encodedData = encodeBytes(packedAction.data); + const encodedSplices = encodeUint256Array(packedAction.splices); + const dataOffset = WORD_BYTES * 3; + const splicesOffset = dataOffset + hexByteLength(encodedData); + + return concatHex([ + encodeWord(packedAction.actionInfo), + encodeWord(dataOffset), + encodeWord(splicesOffset), + encodedData, + encodedSplices, + ]); +} + +function encodeUint256Array(values) { + return concatHex([encodeWord(values.length), ...values.map(encodeWord)]); +} + +function encodeBytes(value) { + const hex = strip0x(normalizeHex(value, "bytes")); + const byteLength = hex.length / 2; + const paddedLength = Math.ceil(byteLength / WORD_BYTES) * WORD_HEX_CHARS; + return `0x${strip0x(encodeWord(byteLength))}${hex.padEnd(paddedLength, "0")}`; +} + +function encodeWord(value) { + const bigint = toBigInt(value); + if (bigint < 0n || bigint > UINT256_MAX) throw new Error(`uint256 out of range: ${value}`); + return `0x${bigint.toString(16).padStart(WORD_HEX_CHARS, "0")}`; +} + +function concatHex(values) { + return `0x${values.map((value) => strip0x(normalizeHex(value, "hex"))).join("")}`; +} + +function normalizeSplice(splice, label) { + if (!splice || typeof splice !== "object") throw new Error(`${label} must be an object`); + const length = checkedIndex(splice.length, `${label}.length`); + if (length === 0) throw new Error(`${label}.length must be greater than zero`); + const normalized = { + sourceActionIndex: checkedIndex(splice.sourceActionIndex, `${label}.sourceActionIndex`), + srcOffset: checkedIndex(splice.srcOffset, `${label}.srcOffset`), + dstOffset: checkedIndex(splice.dstOffset, `${label}.dstOffset`), + length, + }; + packSpliceInfo(normalized); + return normalized; +} + +function validateSpliceForAction(actionIndex, action, splice) { + if (splice.sourceActionIndex >= actionIndex) { + throw new Error(`Invalid future splice: action ${actionIndex} cannot read action ${splice.sourceActionIndex}`); + } + if (splice.dstOffset + splice.length > hexByteLength(action.data)) { + throw new Error( + `Splice destination exceeds action ${actionIndex} data length: ${splice.dstOffset} + ${splice.length}`, + ); + } +} + +function checkedCallType(callType) { + const value = checkedIndex(callType, "callType"); + if (![CallType.CALL, CallType.STATICCALL, CallType.CALL_WITH_NATIVE].includes(value)) { + throw new Error(`Unsupported callType: ${callType}`); + } + return value; +} + +function checkedIndex(value, label) { + const bigint = toBigInt(value); + if (bigint < 0n || bigint > BigInt(Number.MAX_SAFE_INTEGER)) { + throw new Error(`${label} must fit in a safe non-negative integer`); + } + return Number(bigint); +} + +function toBigInt(value) { + if (typeof value === "bigint") return value; + if (typeof value === "number") { + if (!Number.isInteger(value)) throw new Error(`Expected integer, got ${value}`); + return BigInt(value); + } + if (typeof value === "string") { + if (value.startsWith("0x") || value.startsWith("0X")) return BigInt(value); + return BigInt(value); + } + throw new Error(`Expected bigint, number, or numeric string, got ${typeof value}`); +} + +function normalizeAddress(value) { + const hex = strip0x(normalizeHex(value, "address")); + if (hex.length !== 40) throw new Error(`Invalid address length: ${value}`); + return `0x${hex.toLowerCase()}`; +} + +function normalizeHex(value, label) { + if (typeof value !== "string") throw new Error(`${label} must be a hex string`); + if (!/^0x[0-9a-fA-F]*$/.test(value)) throw new Error(`${label} must be 0x-prefixed hex`); + if (value.length % 2 !== 0) throw new Error(`${label} must contain whole bytes`); + return value.toLowerCase(); +} + +function strip0x(value) { + return value.startsWith("0x") || value.startsWith("0X") ? value.slice(2) : value; +} + +function hexByteLength(value) { + return strip0x(normalizeHex(value, "hex")).length / 2; +} + +function cloneAction(action) { + return { + callType: action.callType, + target: action.target, + data: action.data, + storeResult: action.storeResult, + splices: action.splices.map((splice) => ({ ...splice })), + }; +} + +function prepareActionsForEncoding(actions) { + const prepared = actions.map((action) => (isPackedAction(action) ? action : cloneAction(action))); + for (const action of prepared) { + if (isPackedAction(action)) continue; + for (const splice of action.splices) { + if (!prepared[splice.sourceActionIndex] || isPackedAction(prepared[splice.sourceActionIndex])) continue; + prepared[splice.sourceActionIndex].storeResult = true; + } + } + return prepared; +} + +function toModularAction(action) { + return { + actionInfo: packActionInfo(action).toString(), + data: action.data, + splices: action.splices.map((splice) => packSpliceInfo(splice).toString()), + }; +} + +function isPackedAction(action) { + return action && Object.prototype.hasOwnProperty.call(action, "actionInfo"); +} + +function normalizePackedAction(action) { + return { + actionInfo: encodeWord(action.actionInfo), + data: normalizeHex(action.data, "data"), + splices: (action.splices || []).map((splice, index) => encodeWordField(splice, `splices[${index}]`)), + }; +} + +function encodeWordField(value, label) { + try { + return encodeWord(value); + } catch (error) { + throw new Error(`${label}: ${error.message}`); + } +} + +function packActionInfo(action) { + return ( + BigInt(action.callType) | + (action.storeResult ? 1n << 8n : 0n) | + (addressToBigInt(action.target) << 16n) + ); +} + +function packSpliceInfo(splice) { + const sourceActionIndex = checkedUint64(splice.sourceActionIndex, "splice.sourceActionIndex"); + const srcOffset = checkedUint64(splice.srcOffset, "splice.srcOffset"); + const dstOffset = checkedUint64(splice.dstOffset, "splice.dstOffset"); + const length = checkedUint64(splice.length, "splice.length"); + return sourceActionIndex | (srcOffset << 64n) | (dstOffset << 128n) | (length << 192n); +} + +function checkedUint64(value, label) { + const bigint = toBigInt(value); + if (bigint < 0n || bigint > UINT64_MAX) { + throw new Error(`${label} must fit in uint64`); + } + return bigint; +} + +function addressToBigInt(value) { + return BigInt(normalizeAddress(value)); +} + +module.exports = { + ActionHandle, + ActionRef, + CallType, + PERFORM_ACTIONS_SELECTOR, + PERFORM_MODULAR_EXECUTION_SELECTOR, + Offset, + ModularActionsBuilder, + concatHex, + encodePerformActionsArgs, + encodePerformModularExecutionArgs, + encodeWord, + packActionInfo, + packSpliceInfo, + toModularAction, +}; diff --git a/scripts/e2e/utils/relayLinkQuote.ts b/scripts/e2e/utils/relayLinkQuote.ts new file mode 100644 index 0000000..39f3ff7 --- /dev/null +++ b/scripts/e2e/utils/relayLinkQuote.ts @@ -0,0 +1,88 @@ +/** + * Shared Relay.link quote/v2 fetch + approve/deposit parsing (used by e2e scripts). + */ +import axios from 'axios'; +import { ethers } from 'ethers'; + +import { RELAY_API_KEY } from '../config'; + +export interface RelayQuoteResponse { + steps: RelayStep[]; +} + +interface RelayStep { + items: Array<{ + data: { + to?: string; + data?: string; + }; + }>; +} + +export async function fetchRelayQuoteV2(params: { + routerAddress: string; + recipient: string; + originChainId: number; + destinationChainId: number; + originCurrency: string; + destinationCurrency: string; + amount: bigint; +}): Promise { + const headers: Record = { + 'Content-Type': 'application/json', + }; + if (RELAY_API_KEY) { + headers['x-api-key'] = RELAY_API_KEY; + } + + const body = { + user: params.routerAddress, + recipient: params.recipient, + originChainId: params.originChainId, + destinationChainId: params.destinationChainId, + originCurrency: params.originCurrency, + destinationCurrency: params.destinationCurrency, + tradeType: 'EXACT_INPUT', + amount: params.amount.toString(), + }; + + const response = await axios.post( + 'https://api.relay.link/quote/v2', + body, + { headers }, + ); + return response.data; +} + +export function parseRelayQuote(quote: RelayQuoteResponse): { + relaySpender: string; + depositTarget: string; + depositData: string; +} { + const approveIface = new ethers.Interface([ + 'function approve(address spender, uint256 amount) external returns (bool)', + ]); + + const approveStep = quote.steps[0]; + const approveDataHex = approveStep.items[0].data.data ?? ''; + let relaySpender: string; + try { + relaySpender = ethers.getAddress( + approveIface.decodeFunctionData('approve', approveDataHex)[0], + ); + } catch { + const normalized = approveDataHex.startsWith('0x') ? approveDataHex.slice(2) : approveDataHex; + if (normalized.length < 8 + 64) { + throw new Error('Relay approve step calldata too short for fallback spender parse'); + } + const spender40 = normalized.slice(8 + 24, 8 + 24 + 40); + relaySpender = ethers.getAddress('0x' + spender40); + } + + const depositStep = quote.steps[1]; + const depositItem = depositStep.items[0].data; + const depositTarget = depositItem.to ?? ''; + const depositData = depositItem.data ?? '0x'; + + return { relaySpender, depositTarget, depositData }; +} diff --git a/scripts/e2e/utils/reproducibility.ts b/scripts/e2e/utils/reproducibility.ts new file mode 100644 index 0000000..62e49da --- /dev/null +++ b/scripts/e2e/utils/reproducibility.ts @@ -0,0 +1,66 @@ +/** + * State-prep helpers for reproducible on-chain gas-cost tests. + * + * Callers must pass the deployed open-router address from config (`routerAddressForChain`, etc.), + * never Relay `depositTarget`, CCTP `tokenMessenger`, or other external calldata targets. + * + * Before each test leg these ensure: + * 1. The router holds ≥ 20 wei of every token whose balance slot will be written. + * + * Router→spender approvals are handled per-script via `routerAllowance.ts` (check allowance, + * then set `approvalSpender` or modular `approve` only when insufficient). The contract also + * approves inside `swap` / `bridge` / `swapAndBridge` when `approvalSpender` is non-zero. + * + * Seeding balance slots to non-zero means subsequent SSTORE writes cost ~2 900 gas + * (non-zero → non-zero) rather than ~20 000 gas (zero → non-zero), giving + * consistent gas readings across repeated runs. + */ +import { ethers } from 'ethers'; +import { getErc20Contract } from './erc20'; + +const SEED_WEI = 20n; + +/** + * Transfers {@link SEED_WEI} of `token` from `signer` to the deployed open router only + * when that router already holds zero — never to Relay/deposit/spender contracts. + */ +export async function ensureRouterErc20Balance( + signer: ethers.Wallet, + token: string, + openRouterAddress: string, +): Promise { + const openRouter = ethers.getAddress(openRouterAddress); + const tokenResolved = ethers.getAddress(token); + const tokenRo = getErc20Contract(tokenResolved, signer.provider!); + const bal = BigInt(await tokenRo.balanceOf(openRouter)); + if (bal > 0n) { + return; + } + + console.log( + ` [state-prep] open router ${openRouter} token ${tokenResolved} balance=0 — signer transfer ${SEED_WEI} wei to open router only`, + ); + const tx = await getErc20Contract(tokenResolved, signer).transfer(openRouter, SEED_WEI); + await tx.wait(); +} + +/** + * Sends {@link SEED_WEI} of native currency from `signer` to the open router when its + * balance is zero; skipped when already non-zero. + */ +export async function ensureRouterNativeBalance( + signer: ethers.Wallet, + openRouterAddress: string, +): Promise { + const openRouter = ethers.getAddress(openRouterAddress); + const bal = await signer.provider!.getBalance(openRouter); + if (bal > 0n) { + return; + } + + console.log( + ` [state-prep] open router ${openRouter} native balance=0 — signer sending ${SEED_WEI} wei to open router only`, + ); + const tx = await signer.sendTransaction({ to: openRouter, value: SEED_WEI }); + await tx.wait(); +} diff --git a/scripts/e2e/utils/routerAbi.ts b/scripts/e2e/utils/routerAbi.ts new file mode 100644 index 0000000..7e2e702 --- /dev/null +++ b/scripts/e2e/utils/routerAbi.ts @@ -0,0 +1,39 @@ +/** + * ABI fragments for OpenRouter entrypoints used by e2e scripts. + * Struct field order must match the Solidity definitions. + */ +export const ROUTER_ABI = [ + `function performActions( + bytes32 quoteId, + (uint256 actionInfo, bytes data, uint256[] splices)[] actions + ) external payable`, + + `function swap( + bytes32 quoteId, + uint256 flags, + (address user, address inputToken, uint256 inputAmount) input, + (address receiver, uint256 amount) fee, + (address target, address approvalSpender, address outputToken, uint256 value, uint256 minOutput, uint256 returnDataWordOffset) swapData, + bytes swapCallData, + address receiver + ) external payable returns (uint256)`, + + `function swapAndBridge( + bytes32 quoteId, + uint256 flags, + (address user, address inputToken, uint256 inputAmount) input, + (address receiver, uint256 amount) fee, + (address target, address approvalSpender, address outputToken, uint256 value, uint256 minOutput, uint256 returnDataWordOffset) swapData, + bytes swapCallData, + (address target, address approvalSpender, uint256 value) bridgeData, + bytes bridgeCallData + ) external payable`, + + `function bridge( + bytes32 quoteId, + (address user, address inputToken, uint256 inputAmount) input, + (address receiver, uint256 amount) fee, + (address target, address approvalSpender, uint256 value) bridgeData, + bytes bridgeCallData + ) external payable`, +] as const; diff --git a/scripts/e2e/utils/routerAllowance.ts b/scripts/e2e/utils/routerAllowance.ts new file mode 100644 index 0000000..15c382b --- /dev/null +++ b/scripts/e2e/utils/routerAllowance.ts @@ -0,0 +1,110 @@ +/** + * Router ERC-20 allowance helpers for e2e scripts. + * + * `OpenRouter` only calls `approve` when `approvalSpender != 0` and + * `requiredAmount > allowance(router, spender)`. Scripts mirror that: check on-chain + * allowance first, omit modular approve actions when sufficient, and pass + * `ZERO_ADDRESS` as `approvalSpender` on `swap` / `bridge` / `swapAndBridge` when not needed. + */ +import { ethers } from 'ethers'; + +import { NATIVE_TOKEN_ADDRESS } from '../config'; +import { ZERO_ADDRESS } from './contractTypes'; +import { encodeApprove, getErc20Contract } from './erc20'; + +export interface ModularActionsExec { + call(target: string, data: string): unknown; +} + +/** + * Reads `token.allowance(router, spender)`. + */ +export async function readRouterAllowance( + provider: ethers.Provider, + routerAddress: string, + tokenAddress: string, + spenderAddress: string, +): Promise { + const router = ethers.getAddress(routerAddress); + const token = ethers.getAddress(tokenAddress); + const spender = ethers.getAddress(spenderAddress); + const erc20 = getErc20Contract(token, provider); + const allowanceRaw = await erc20.allowance(router, spender); + return typeof allowanceRaw === 'bigint' ? allowanceRaw : BigInt(allowanceRaw.toString()); +} + +/** + * Matches contract logic: approval is skipped when `allowance >= requiredAmount`. + */ +export function routerAllowanceSufficient(allowance: bigint, requiredAmount: bigint): boolean { + return allowance >= requiredAmount; +} + +function isNativeToken(tokenAddress: string): boolean { + return ethers.getAddress(tokenAddress) === ethers.getAddress(NATIVE_TOKEN_ADDRESS); +} + +function isZeroSpender(spenderAddress: string): boolean { + return ethers.getAddress(spenderAddress) === ethers.getAddress(ZERO_ADDRESS); +} + +/** + * Returns `spender` for `SwapData` / `BridgeData` when the router must approve, else `ZERO_ADDRESS`. + */ +export async function resolveApprovalSpender( + provider: ethers.Provider, + routerAddress: string, + tokenAddress: string, + spenderAddress: string, + requiredAmount: bigint, +): Promise { + if (isNativeToken(tokenAddress) || isZeroSpender(spenderAddress)) { + return ZERO_ADDRESS; + } + + const allowance = await readRouterAllowance(provider, routerAddress, tokenAddress, spenderAddress); + if (routerAllowanceSufficient(allowance, requiredAmount)) { + console.log( + ` [allowance] sufficient: token=${tokenAddress} spender=${spenderAddress} allowance=${allowance} required=${requiredAmount} → approvalSpender=0`, + ); + return ZERO_ADDRESS; + } + + console.log( + ` [allowance] insufficient: token=${tokenAddress} spender=${spenderAddress} allowance=${allowance} required=${requiredAmount} → approvalSpender set`, + ); + return ethers.getAddress(spenderAddress); +} + +/** + * Appends a modular `approve` action only when router allowance is below `requiredAmount`. + * + * @returns true when an approve action was added. + */ +export async function modularApproveIfNeeded( + exec: ModularActionsExec, + provider: ethers.Provider, + routerAddress: string, + tokenAddress: string, + spenderAddress: string, + requiredAmount: bigint, + approveAmount: bigint = ethers.MaxUint256, +): Promise { + if (isNativeToken(tokenAddress) || isZeroSpender(spenderAddress)) { + return false; + } + + const allowance = await readRouterAllowance(provider, routerAddress, tokenAddress, spenderAddress); + if (routerAllowanceSufficient(allowance, requiredAmount)) { + console.log( + ` [allowance] skipping modular approve: token=${tokenAddress} spender=${spenderAddress} allowance=${allowance} required=${requiredAmount}`, + ); + return false; + } + + console.log( + ` [allowance] modular approve: token=${tokenAddress} spender=${spenderAddress} amount=${approveAmount}`, + ); + exec.call(tokenAddress, encodeApprove(spenderAddress, approveAmount)); + return true; +} diff --git a/scripts/e2e/utils/sleep.ts b/scripts/e2e/utils/sleep.ts new file mode 100644 index 0000000..ae67cc7 --- /dev/null +++ b/scripts/e2e/utils/sleep.ts @@ -0,0 +1,5 @@ +export function sleep(ms: number): Promise { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} diff --git a/scripts/e2e/utils/txnLogSummary.ts b/scripts/e2e/utils/txnLogSummary.ts new file mode 100644 index 0000000..7f1fa2d --- /dev/null +++ b/scripts/e2e/utils/txnLogSummary.ts @@ -0,0 +1,24 @@ +import type { TransactionReceipt } from 'ethers'; + +import { BLOCK_EXPLORER_TX_PREFIX } from '../config'; + +export function explorerTxUrl(chainId: number, txHash: string): string { + const prefix = BLOCK_EXPLORER_TX_PREFIX[chainId]; + + if (!prefix) { + return txHash; + } + + return `${prefix}${txHash}`; +} + +export function logTxnSummary( + headline: string, + chainId: number, + receipt: TransactionReceipt, +): void { + console.log(''); + console.log(headline); + console.log(explorerTxUrl(chainId, receipt.hash)); + console.log(`Gas: ${receipt.gasUsed.toLocaleString('en-US')}`); +} diff --git a/src/OpenRouter.sol b/src/OpenRouter.sol new file mode 100644 index 0000000..d9aa58d --- /dev/null +++ b/src/OpenRouter.sol @@ -0,0 +1,771 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.34; + +import {SafeTransferLib} from "solady/src/utils/SafeTransferLib.sol"; + +import {IERC20} from "./common/interfaces/IERC20.sol"; +import {AccessControl} from "./common/utils/AccessControl.sol"; +import {AllowanceHolderContext} from "./common/allowance/AllowanceHolderContext.sol"; +import {ALLOWANCE_HOLDER} from "./common/interfaces/IAllowanceHolder.sol"; +import {BytesSpliceLib} from "./common/lib/BytesSpliceLib.sol"; +import {CurrencyLib} from "./common/lib/CurrencyLib.sol"; +import {RescueFundsLib} from "./common/lib/RescueFundsLib.sol"; +import {RESCUE_ROLE} from "./common/AccessRoles.sol"; + +/// @title OpenRouter +/// @notice Pull → optional fee → swap/bridge execution without backend signature verification. +/// Fund safety rests on AllowanceHolder's transient allowance scoping (operator + owner + token): +/// only the user whose address was passed to `AllowanceHolder.exec` can authorise a pull of +/// their own funds. The `_msgSender() == user` check in `_pullFromUser` enforces this. +contract OpenRouter is AccessControl, AllowanceHolderContext { + using SafeTransferLib for address; + + // ========================================================================= + // Structs + // ========================================================================= + + struct InputData { + address user; + address inputToken; + uint256 inputAmount; + } + + struct FeeData { + address receiver; + uint256 amount; + } + + struct SwapData { + address target; + address approvalSpender; + address outputToken; + uint256 value; + uint256 minOutput; + uint256 returnDataWordOffset; + } + + struct BridgeData { + address target; + address approvalSpender; + uint256 value; + } + + enum CallType { + CALL, + STATICCALL, + CALL_WITH_NATIVE + } + + struct Action { + /// @dev Packed call metadata. Decode with masks/shifts below; encode with + /// `callType | (storeResult ? 1 << 8 : 0) | (uint160(target) << 16)`. + /// + /// Bit layout (least significant bits first): + /// bits 255..160 : reserved (0) + /// bits 159..16 : target address (uint160, left-aligned in this field) + /// bit 8 : storeResult — when set, returndata is saved to `results[i]` + /// even on success so later actions can splice from it + /// bits 7..3 : reserved (0) + /// bits 2..0 : CallType — CALL (0), STATICCALL (1), CALL_WITH_NATIVE (2) + /// + /// CALL_WITH_NATIVE: first 32 bytes of `data` are forwarded as `msg.value`; + /// the remaining bytes are the call payload. + uint256 actionInfo; + /// @dev Calldata passed to the target. Splices from `splices[]` overwrite byte + /// ranges in a mutable memory copy before the external call runs. + bytes data; + /// @dev Packed splice descriptors applied to `data` before the call. + /// Each entry is one `uint256` with four uint64 fields (see layout below). + /// Encode with `packSpliceInfo` in `scripts/e2e/utils/modularActionsBuilder/index.js`. + /// + /// Per-entry bit layout (least significant bits first): + /// bits 255..192 : length — number of bytes to copy (must be > 0) + /// bits 191..128 : dstOffset — byte offset into this action's `data` payload + /// (skips the bytes-array length word; for CALL_WITH_NATIVE, + /// offset 0 is the value word, offset 32 is payload start) + /// bits 127..64 : srcOffset — byte offset into `results[sourceActionIndex]` + /// payload (same length-prefix convention) + /// bits 63..0 : sourceActionIndex — index of a prior action (< current index) + /// + /// Packing formula: + /// sourceActionIndex | (srcOffset << 64) | (dstOffset << 128) | (length << 192) + /// + /// The source action must have bit 8 set in `actionInfo` (storeResult); the JS + /// builder sets this automatically when a splice references that action. + uint256[] splices; + } + + // ========================================================================= + // Flags (swap / swapAndBridge) + // ========================================================================= + // + // Instead of bool parameters, one uint256 packs independent switches without adding + // ABI range checks or extra words for standalone bools. + // + // Bit layout (least significant bits); test with `(flags & MASK) != 0`: + // bits 255..32 : reserved (0) + // bits 31..16 : bridge amount word byte offset, uint16, used only when bit 3 is set + // bits 15..4 : reserved (0) + // bit 3 : BRIDGE_AMOUNT_POSITION_FLAG_BIT_MASK (0x08) — splice finalAmount into bridge calldata + // bit 2 : BRIDGE_VALUE_FLAG_BIT_MASK (0x04) — bridge msg.value: bridge.value alone vs finalAmount + bridge.value + // bit 1 : BALANCE_FLAG_BIT_MASK (0x02) — swap output: returndata vs balance delta + // bit 0 : POST_FEE_FLAG_BIT_MASK (0x01) — swap fee: pre- vs post-swap + // + // Combined values for flags: + // + // flags binary (low byte) postFee? balance-of output? bridge value? + // ───── ────────────────── ──────── ────────────────── ───────────── + // 0x00 00000000 no returndata word bridge.value + // 0x01 00000001 yes returndata word bridge.value + // 0x02 00000010 no balance delta on outputToken bridge.value + // 0x03 00000011 yes balance delta on outputToken bridge.value + // 0x04 00000100 no returndata word finalAmount + bridge.value + // + // POST_FEE_FLAG_BIT_MASK selects bit 0 — fee timing + // 0000 — pre-swap fee: pull → deduct fee from input token → swap remainder + // 0001 — post-swap fee: pull → swap full input → deduct fee from output token (after minOutput check on swap result) + // + // BALANCE_FLAG_BIT_MASK selects bit 1 — swap output sizing + // 0000 — returnData as swap output: decode returned amount from call returndata at `swapData.returnDataWordOffset` + // 0010 — balanceOf() delta as swap output: snapshot outputToken balance before call, measure (after − before) as output + // + // BRIDGE_VALUE_FLAG_BIT_MASK selects bit 2 — bridge native value source + // 0000 — bridge.value as msg.value: forward `bridge.value` as msg.value + // 0100 — finalAmount + bridge.value as msg.value: forward `finalAmount + bridge.value` as msg.value (bridge.value carries static addend, e.g. LZ nativeFee) + // + // BRIDGE_AMOUNT_POSITION_FLAG_BIT_MASK selects bit 3 — bridge calldata amount splicing. + // 0000 — no bridge calldata modification + // 1000 — bridge calldata modification: splice finalAmount at uint16(flags >> BRIDGE_AMOUNT_POSITION_SHIFT) + // + + /// @dev Bit mask 0x01: post-swap fee path when `(flags & mask) != 0`; clear = pre-swap fee from input token. + uint256 internal constant POST_FEE_FLAG_BIT_MASK = 0x01; + + /// @dev Bit mask 0x02: measure swap output by balance delta when `(flags & mask) != 0`; clear = returndata word. + uint256 internal constant BALANCE_FLAG_BIT_MASK = 0x02; + + /// @dev Bit mask 0x04: `finalAmount + bridge.value` is forwarded as msg.value (bridge.value acts as a static addend, e.g. LZ nativeFee). + uint256 internal constant BRIDGE_VALUE_FLAG_BIT_MASK = 0x04; + + /// @dev Bit mask 0x08: splice finalAmount into bridge calldata at the uint16 position packed in flags. + uint256 internal constant BRIDGE_AMOUNT_POSITION_FLAG_BIT_MASK = 0x08; + + /// @dev Shift for the packed uint16 bridge amount position. + uint256 internal constant BRIDGE_AMOUNT_POSITION_SHIFT = 16; + + /// @dev Mask for the packed uint16 bridge amount position after shifting. + uint256 internal constant BRIDGE_AMOUNT_POSITION_MASK = 0xffff; + + // ========================================================================= + // Errors + // ========================================================================= + + error SwapOutputInsufficient(); + error InvalidExecution(); + error CallerNotSignedUser(); + error InsufficientMsgValue(); + error FutureSplice(uint256 actionIndex, uint256 sourceActionIndex); + error SpliceOutOfBounds(uint256 actionIndex, uint256 spliceIndex); + error CallFailed(uint256 actionIndex, bytes returndata); + error MissingNativeValue(uint256 actionIndex); + error ReturnDataOutOfBounds(); + + // ========================================================================= + // Events + // ========================================================================= + + event RequestExecuted(bytes32 indexed quoteId); + + // ========================================================================= + // Constructor + // ========================================================================= + + /** + * @notice Deploys the router and grants `RESCUE_ROLE` to `_owner`. + * @param _owner Initial contract owner and rescue-role holder. + */ + constructor(address _owner) AccessControl(_owner) { + _grantRole(RESCUE_ROLE, _owner); + } + + /// @notice Accepts native ETH forwarded with bridge/swap calls. + receive() external payable {} + + // ========================================================================= + // External functions + // ========================================================================= + + /** + * @notice Perform swap with optional pre/post fee. + * @param quoteId Caller-defined correlation id logged in `RequestExecuted`. + * @param flags Packed flags + * @param input User, input token, and pull amount. + * @dev For pre-fee / no-fee: the swap router must + * be instructed (via `swapCallData`) to send tokens directly to `receiver`; the contract never holds the output. + * For post-fee: tokens land at this contract, fee is deducted, net is forwarded to `receiver`. + * @param fee Fee collection info: receiver and amount. Set `amount` to 0 to skip fee collection. + * @param swapData Swap target, spender, output token, value, `minOutput`, and returndata offset. + * @param swapCallData Calldata forwarded to `swapData.target`. + * @param receiver Address that ultimately receives the swap output (net of any post-swap fee). + * @return finalAmount Gross swap output sent to receiver after any post-swap fee + * @dev `minOutput` is the minimum gross amount coming out of the swap (before any output-token fee). It is enforced immediately after `_execSwap`, then post-swap fee (if any) is collected. + * Pre-fee paths take the input-side fee before the swap; `minOutput` still guards the swap outcome. + */ + function swap( + bytes32 quoteId, + uint256 flags, + InputData calldata input, + FeeData calldata fee, + SwapData calldata swapData, + bytes calldata swapCallData, + address receiver + ) external payable returns (uint256 finalAmount) { + if ( + input.user == address(0) || input.inputToken == address(0) || swapData.target == address(0) + || receiver == address(0) + ) { + revert InvalidExecution(); + } + + // Parse flags + bool postFee = fee.amount != 0 && ((flags & POST_FEE_FLAG_BIT_MASK) != 0); + bool useBalanceOf = ((flags & BALANCE_FLAG_BIT_MASK) != 0); + + { + // Pull funds from user via AllowanceHolder + _pullFromUser(input.inputToken, input.user, input.inputAmount); + + // Collect pre-swap fee + uint256 swapInput = input.inputAmount; + if (fee.amount != 0 && !postFee) { + uint256 feeAmount = fee.amount; + CurrencyLib.transfer(input.inputToken, fee.receiver, feeAmount); + unchecked { + swapInput -= feeAmount; + } + } + + // Approve spender + if ( + // check spender & token + swapData.approvalSpender != address(0) && input.inputToken != CurrencyLib.NATIVE_TOKEN_ADDRESS && + // check current allowance + swapInput > IERC20(input.inputToken).allowance(address(this), swapData.approvalSpender) + ) { + // approve max allowance + SafeTransferLib.safeApproveWithRetry(input.inputToken, swapData.approvalSpender, type(uint256).max); + } + } + + /// @dev Pre-fee / no-fee: swap calldata encodes `receiver` as the output recipient; tokens never touch this contract. + /// @dev Post-fee: swap output lands at this contract so the fee can be deducted before forwarding. + address outputReceiver = postFee ? address(this) : receiver; + + // Execute swap + finalAmount = _execSwap(swapData, swapCallData, useBalanceOf, outputReceiver); + if (finalAmount < swapData.minOutput) revert SwapOutputInsufficient(); + + if (postFee) { + // Collect post-swap fee + uint256 feeAmount = fee.amount; + CurrencyLib.transfer(swapData.outputToken, fee.receiver, feeAmount); + unchecked { + finalAmount -= feeAmount; + } + + // Transfer net output to receiver + CurrencyLib.transfer(swapData.outputToken, receiver, finalAmount); + } + + // Pre-fee / no-fee: tokens were sent directly to `receiver` by the swap router; nothing to transfer + + emit RequestExecuted(quoteId); + } + + /** + * @notice Perform swap and bridge with optional pre/post swap fee. + * @param quoteId Caller-defined correlation id logged in `RequestExecuted`. + * @param flags Packed flags + * @param input User, input token, and pull amount. + * @param fee Fee collection info: receiver and amount. Set `amount` to 0 to skip fee collection. + * @param swapData Swap target, spender, output token, value, `minOutput`, and returndata offset. + * @param swapCallData Calldata forwarded to `swapData.target`. + * @param bridgeData Bridge target, approval spender, and static `msg.value` addend. + * @param bridgeCallData Bridge calldata; optionally spliced with swap output per `flags`. + * @dev Same `minOutput` rule as `swap`: validated on gross `_execSwap` output, then optional output fee applies. + */ + function swapAndBridge( + bytes32 quoteId, + uint256 flags, + InputData calldata input, + FeeData calldata fee, + SwapData calldata swapData, + bytes calldata swapCallData, + BridgeData calldata bridgeData, + bytes calldata bridgeCallData + ) external payable { + if ( + bridgeData.target == address(0) || input.user == address(0) || input.inputToken == address(0) + || swapData.target == address(0) + ) { + revert InvalidExecution(); + } + + // Execute swap before bridge + uint256 finalAmount = _swapBeforeBridge(flags, input, fee, swapData, swapCallData); + + // Execute bridge + _execBridge(swapData.outputToken, finalAmount, flags, bridgeData, bridgeCallData); + + emit RequestExecuted(quoteId); + } + + /** + * @notice Perform bridge with optional pre-bridge fee. + * @param quoteId Caller-defined correlation id logged in `RequestExecuted`. + * @param input User, input token, and pull amount. + * @param fee Fee collection info: receiver and amount. Set `amount` to 0 to skip fee collection. + * @param bridgeData Bridge target, approval spender, and `msg.value` for the bridge call. + * @param bridgeCallData Calldata forwarded to `bridgeData.target` (amount must be baked in by the caller). + * @dev Because no swap is involved, `finalAmount = inputAmount - feeAmount` is fully knowable by the caller before signing. + * The caller must therefore bake the correct amount directly into `bridgeCallData` and set `bridgeData.value` to the desired `msg.value` for the bridge call. + * No runtime calldata splicing is performed. The caller MUST route through `AllowanceHolder.exec` for ERC-20 inputs so that `_msgSender()` resolves to `input.user`. + */ + function bridge( + bytes32 quoteId, + InputData calldata input, + FeeData calldata fee, + BridgeData calldata bridgeData, + bytes calldata bridgeCallData + ) external payable { + if (bridgeData.target == address(0) || input.user == address(0) || input.inputToken == address(0)) { + revert InvalidExecution(); + } + + // Pull funds from user via AllowanceHolder + _pullFromUser(input.inputToken, input.user, input.inputAmount); + + // Collect pre-bridge fee + uint256 feeAmount = fee.amount; + if (feeAmount != 0) { + CurrencyLib.transfer(input.inputToken, fee.receiver, feeAmount); + } + + uint256 netAmount; + unchecked { + netAmount = input.inputAmount - feeAmount; + } + + // Approve bridge spender + if ( + // check spender && token + bridgeData.approvalSpender != address(0) && input.inputToken != CurrencyLib.NATIVE_TOKEN_ADDRESS && + // check current allowance + netAmount > IERC20(input.inputToken).allowance(address(this), bridgeData.approvalSpender) + ) { + // approve max allowance + SafeTransferLib.safeApproveWithRetry(input.inputToken, bridgeData.approvalSpender, type(uint256).max); + } + + // Execute bridge + _execCallCalldata(bridgeData.target, bridgeData.value, bridgeCallData, false); + + emit RequestExecuted(quoteId); + } + + /** + * @notice Runs a sequence of generic actions with optional returndata splicing between steps. + * @param quoteId Caller-defined correlation id logged in `RequestExecuted`. + * @param actions Ordered actions; each may splice bytes from a prior action's returndata into its calldata. + */ + function performActions(bytes32 quoteId, Action[] calldata actions) external payable { + _performActions(actions); + + emit RequestExecuted(quoteId); + } + + // ========================================================================= + // Internal functions + // ========================================================================= + + // ------------------------------------- + // swapAndBridge internal functions + // ------------------------------------- + + /** + * @dev Pull, optional pre/post swap fee, and swap for `swapAndBridge`. Swap output always remains at `address(this)` for bridging. + * @param flags Fee timing and swap output measurement flags (same as `swap`). + * @param input User, input token, and pull amount. + * @param fee Fee receiver and amount; `amount == 0` skips fee collection. + * @param swapData Swap target, spender, output token, value, `minOutput`, and returndata offset. + * @param swapCallData Calldata forwarded to `swapData.target`. + * @return finalAmount Swap output net of any post-swap fee, ready for `_execBridge`. + */ + function _swapBeforeBridge( + uint256 flags, + InputData calldata input, + FeeData calldata fee, + SwapData calldata swapData, + bytes calldata swapCallData + ) internal returns (uint256 finalAmount) { + // Pull funds from user via AllowanceHolder + _pullFromUser(input.inputToken, input.user, input.inputAmount); + + bool postFee; + { + // Collect pre-swap fee + uint256 feeAmount = fee.amount; + postFee = feeAmount != 0 && ((flags & POST_FEE_FLAG_BIT_MASK) != 0); + uint256 swapInput = input.inputAmount; + + if (feeAmount != 0 && !postFee) { + CurrencyLib.transfer(input.inputToken, fee.receiver, feeAmount); + unchecked { + swapInput -= feeAmount; + } + } + + // Approve swap spender + if ( + // check spender & token + swapData.approvalSpender != address(0) && input.inputToken != CurrencyLib.NATIVE_TOKEN_ADDRESS && + // check current allowance + swapInput > IERC20(input.inputToken).allowance(address(this), swapData.approvalSpender) + ) { + // approve max allowance + SafeTransferLib.safeApproveWithRetry(input.inputToken, swapData.approvalSpender, type(uint256).max); + } + } + + // Execute swap + /// @dev Swap output always lands at this contract regardless of fee timing — tokens must be here for bridging. + bool useBalanceOf = ((flags & BALANCE_FLAG_BIT_MASK) != 0); + finalAmount = _execSwap(swapData, swapCallData, useBalanceOf, address(this)); + if (finalAmount < swapData.minOutput) revert SwapOutputInsufficient(); + + // Collect post-swap fee + if (postFee) { + uint256 feeAmount = fee.amount; + CurrencyLib.transfer(swapData.outputToken, fee.receiver, feeAmount); + unchecked { + finalAmount -= feeAmount; + } + } + } + + /** + * @dev Splice `amount` into bridge calldata when flagged, approve the bridge spender, and call the bridge target. + * @param token ERC-20 bridged (or native sentinel); used for approval only. + * @param amount Post-swap token amount spliced into calldata and/or forwarded as `msg.value`. + * @param flags Bridge splice position, `msg.value` composition, and related bit flags. + * @param bridgeData Bridge target, approval spender, and static `msg.value` addend. + * @param bridgeCallData Base bridge calldata; copied to memory when splicing is required. + */ + function _execBridge( + address token, + uint256 amount, + uint256 flags, + BridgeData calldata bridgeData, + bytes calldata bridgeCallData + ) internal { + bytes memory _bridgeCallData = bridgeCallData; + + // Modify bridge calldata if splicing is required + if (flags & BRIDGE_AMOUNT_POSITION_FLAG_BIT_MASK != 0) { + uint256 position = flags >> BRIDGE_AMOUNT_POSITION_SHIFT & BRIDGE_AMOUNT_POSITION_MASK; + BytesSpliceLib.spliceWord({data: _bridgeCallData, position: position, word: amount}); + } + + // Approve bridge spender + if ( + // check spender & token + bridgeData.approvalSpender != address(0) && token != CurrencyLib.NATIVE_TOKEN_ADDRESS && + // check current allowance + amount > IERC20(token).allowance(address(this), bridgeData.approvalSpender) + ) { + // approve max allowance + SafeTransferLib.safeApproveWithRetry(token, bridgeData.approvalSpender, type(uint256).max); + } + + // Parse and set bridge value flag + uint256 bridgeValue = ((flags & BRIDGE_VALUE_FLAG_BIT_MASK) != 0) ? amount + bridgeData.value : bridgeData.value; + + // Execute bridge call + _execCall(bridgeData.target, bridgeValue, _bridgeCallData); + } + + // -------------------------------------- + // performActions internal functions + // -------------------------------------- + + /** + * @dev Executes `actions` in order, applying returndata splices before each call. + * @dev See `Action` for `actionInfo` and `splices[]` bit layouts. + * @param actions Ordered list of actions to run. + */ + function _performActions(Action[] calldata actions) internal { + uint256 actionsLength = actions.length; + bytes[] memory results = new bytes[](actionsLength); + + for (uint256 i; i < actionsLength;) { + Action calldata action = actions[i]; + bytes memory callData = action.data; + + // Patch callData with slices of prior action returndata. + uint256 splicesLength = action.splices.length; + for (uint256 j; j < splicesLength;) { + uint256 spliceInfo = action.splices[j]; + uint256 sourceActionIndex = uint64(spliceInfo); // first 64 bits: index of the prior action to read returndata from. + if (sourceActionIndex >= i) revert FutureSplice(i, sourceActionIndex); + + uint256 srcOffset = uint64(spliceInfo >> 64); // Next 64 bits: byte offset into source returndata + uint256 dstOffset = uint64(spliceInfo >> 128); // Next 64 bits: byte offset into next action's data + uint256 length = spliceInfo >> 192; // Top 64 bits: number of bytes to copy + + // Fetch source action returndata + bytes memory source = results[sourceActionIndex]; + if (srcOffset + length > source.length || dstOffset + length > callData.length) { + revert SpliceOutOfBounds(i, j); + } + + assembly ("memory-safe") { + // copy `length` bytes from `source returndata starting from `srcOffset` to `callData` starting from `dstOffset` + mcopy(add(add(callData, 0x20), dstOffset), add(add(source, 0x20), srcOffset), length) + } + + unchecked { + ++j; + } + } + + // Parse actionInfo + bool success; + uint256 actionInfo = action.actionInfo; + bool storeResult = (actionInfo & 0xff00) != 0; // Bit 8: persist returndata if set + uint256 callType = actionInfo & 0xff; // Bits 0–7: specify CallType + address target = address(uint160(actionInfo >> 16)); // Bits 16+: target address + + if (callType == uint256(CallType.STATICCALL)) { + assembly ("memory-safe") { + // staticcall without copying return data by default + success := staticcall(gas(), target, add(callData, 0x20), mload(callData), 0, 0) + } + } else if (callType == uint256(CallType.CALL_WITH_NATIVE)) { + if (callData.length < 32) revert MissingNativeValue(i); + uint256 callValue; + uint256 payloadLength = callData.length - 32; + assembly ("memory-safe") { + // regular call with value forwarded without copying return data by default + callValue := mload(add(callData, 0x20)) // CALL_WITH_NATIVE prepends a 32-byte wei amount before the actual calldata payload. + success := call(gas(), target, callValue, add(callData, 0x40), payloadLength, 0, 0) // skips first two bytes to reach actuall calldata + } + } else { + assembly ("memory-safe") { + // regular call with zero value forwarded without copying return data by default + success := call(gas(), target, 0, add(callData, 0x20), mload(callData), 0, 0) + } + } + + // Capture returndata on failure (for revert reason) or when explicitly requested. + if (!success || storeResult) { + bytes memory ret; + assembly ("memory-safe") { + // prep return / revert data + let returnDataSize := returndatasize() + ret := mload(0x40) + mstore(ret, returnDataSize) // write length prefix on free-mem pointer + returndatacopy(add(ret, 0x20), 0, returnDataSize) // copy returndata after length + mstore(0x40, and(add(add(add(ret, 0x20), returnDataSize), 0x1f), not(0x1f))) // Advance free pointer to next 32-byte boundary: (ret + 0x20 + size + 31) and clear last 5 bits with not(0x1f) + } + // if any call was failed, revert with the returndata + if (!success) revert CallFailed(i, ret); + + // else, save returndata to results array + results[i] = ret; + } + unchecked { + ++i; + } + } + } + + // ------------------------------- + // Common internal functions + // ------------------------------- + + /** + * @dev Pulls `amount` of `token` from `user` into this contract. + * For ERC20: enforces `_msgSender() == user` (caller must have routed through `AllowanceHolder.exec`) and calls AH.transferFrom via assembly. + * AH selector: transferFrom(address,address,address,uint256) = 0x15dacbea. + * For native ETH: ETH must already be present as msg.value; verify sufficient value was forwarded. + * @param token Input token or `CurrencyLib.NATIVE_TOKEN_ADDRESS`. + * @param user Owner whose AllowanceHolder-scoped allowance is consumed. + * @param amount Tokens or wei to pull. + */ + function _pullFromUser(address token, address user, uint256 amount) internal { + // Check input value if native token + if (token == CurrencyLib.NATIVE_TOKEN_ADDRESS) { + if (msg.value < amount) { + revert InsufficientMsgValue(); + } + return; + } + + // Check caller is user + if (_msgSender() != user) revert CallerNotSignedUser(); + + // Call AllowanceHolder.transferFrom() + address allowanceHolder = address(ALLOWANCE_HOLDER); + assembly ("memory-safe") { + // Manually ABI-encode AllowanceHolder.transferFrom(address token, address owner, address recipient, uint256 amount) + // selector 0x15dacbea. Calldata is 0x84 (132) bytes and starts at ptr+0x1c (see last mstore below). + // + // The `shl(0x60, addr)` trick left-aligns a 20-byte address in a 32-byte word: the high 20 bytes + // hold the address and the trailing 12 bytes are zero, which simultaneously encodes the address AND + // provides the ABI zero-padding for the *next* field — so each shifted mstore clears the following + // field's padding without a separate write. + // + // Calldata layout relative to ptr+0x1c: + // [0..3] selector (0x15dacbea) + // [4..35] token (12-byte pad + 20-byte address) + // [36..67] owner/user (12-byte pad + 20-byte address) + // [68..99] recipient (12-byte pad + 20-byte address = address(this)) + // [100..131] amount (uint256) + let ptr := mload(0x40) + mstore(add(0x80, ptr), amount) // calldata[100..131]: amount (uint256, right-aligned) + mstore(add(0x60, ptr), address()) // calldata[68..99]: recipient = this contract (right-aligned, high 12 bytes are zero padding) + mstore(add(0x4c, ptr), shl(0x60, user)) // calldata[48..67]: user address; trailing 12 zero bytes fill calldata[68..79] (recipient padding) + // `shl(0x60)` (96-bit), NOT `shl(0xa0)` (160-bit): 0xa0 here is literal 160, which + // shifts the 20-byte address out of place and corrupts the calldata token. Same as 0x-settler `Permit2Payment._allowanceHolderTransferFrom`. + mstore(add(0x2c, ptr), shl(0x60, token)) // calldata[16..35]: token address; trailing 12 zero bytes fill calldata[36..47] (user padding) + mstore(add(0x0c, ptr), 0x15dacbea000000000000000000000000) // selector at calldata[0..3]; 12 zero bytes fill calldata[4..15] (token padding); calldata begins at ptr+0x1c + + if iszero(call(gas(), allowanceHolder, 0x00, add(0x1c, ptr), 0x84, 0x00, 0x00)) { + // if call did not succeed, revert with the revert returndata + let p := mload(0x40) + returndatacopy(p, 0x00, returndatasize()) + revert(p, returndatasize()) + } + } + } + + /** + * @dev Executes the swap call and returns the output amount. + * `useBalanceOf=true`: measure output as (balance after − balance before) at `outputReceiver`. + * `useBalanceOf=false`: decode output from returndata at `swapData.returnDataWordOffset`. + * `outputReceiver` must be `address(this)` when tokens are expected at the contract (post-swap fee path, bridge path) + * or the end user when the router sends directly to them. + * @param swapData Swap target, value, output token, and returndata layout. + * @param swapCallData Calldata forwarded to `swapData.target`. + * @param useBalanceOf When true, use balance delta instead of returndata decoding. + * @param outputReceiver Account whose output-token balance is measured or credited. + * @return finalAmount Gross swap output amount. + */ + function _execSwap( + SwapData calldata swapData, + bytes calldata swapCallData, + bool useBalanceOf, + address outputReceiver + ) internal returns (uint256 finalAmount) { + if (useBalanceOf) { + // Measure output as (balance after − balance before) at `outputReceiver` + uint256 before = CurrencyLib.balanceOf(swapData.outputToken, outputReceiver); + _execCallCalldata(swapData.target, swapData.value, swapCallData, false); + finalAmount = CurrencyLib.balanceOf(swapData.outputToken, outputReceiver) - before; + } else { + // Decode output from returndata + bytes memory ret = _execCallCalldata(swapData.target, swapData.value, swapCallData, true); + finalAmount = _decodeReturnWord(ret, swapData.returnDataWordOffset); + } + } + + /** + * @dev Low-level `call` with bubbled revert data on failure. + * @param target Call recipient. + * @param value Wei forwarded with the call. + * @param data ABI-encoded calldata in memory. + */ + function _execCall(address target, uint256 value, bytes memory data) internal { + bool success; + assembly ("memory-safe") { + success := call(gas(), target, value, add(data, 0x20), mload(data), 0, 0) + } + + if (!success) { + bytes memory ret; + assembly ("memory-safe") { + // prep and return revert data + let returnDataSize := returndatasize() + ret := mload(0x40) + mstore(ret, returnDataSize) // write length prefix on free-mem pointer + returndatacopy(add(ret, 0x20), 0, returnDataSize) // copy returndata after length + mstore(0x40, and(add(add(add(ret, 0x20), returnDataSize), 0x1f), not(0x1f))) // bump free pointer + revert(add(ret, 0x20), mload(ret)) // bubbles up the original revert payload + } + } + } + + /** + * @dev Low-level `call` using calldata copied to memory; optionally captures returndata. + * @dev Helps cheaper external calls avoiding early copy of calldata to memory. + * @param target Call recipient. + * @param value Wei forwarded with the call. + * @param data Calldata slice forwarded to `target`. + * @param storeResult When true, copy returndata into memory even on success. + * @return ret Returndata when `storeResult` is true or the call reverts (revert bubbles). + */ + function _execCallCalldata(address target, uint256 value, bytes calldata data, bool storeResult) + internal + returns (bytes memory ret) + { + bool success; + assembly ("memory-safe") { + let ptr := mload(0x40) + calldatacopy(ptr, data.offset, data.length) // copy calldata slice to fresh memory (avoids redundant memory alloc) + mstore(0x40, and(add(add(ptr, data.length), 0x1f), not(0x1f))) // advance free pointer to next 32-byte boundary + success := call(gas(), target, value, ptr, data.length, 0, 0) + } + + if (!success || storeResult) { + assembly ("memory-safe") { + // prep and return revert data + let returnDataSize := returndatasize() + ret := mload(0x40) + mstore(ret, returnDataSize) // write length prefix on free-mem pointer + returndatacopy(add(ret, 0x20), 0, returnDataSize) // copy returndata after length + mstore(0x40, and(add(add(add(ret, 0x20), returnDataSize), 0x1f), not(0x1f))) // bump free pointer + } + if (!success) { + assembly ("memory-safe") { + revert(add(ret, 0x20), mload(ret)) // bubble up the raw revert payload + } + } + } + } + + /** + * @dev Reads the 32-byte word at `wordOffset` from ABI-encoded `ret` (word index, not byte offset). + * @param ret Return blob from a prior call. + * @param wordOffset Zero-based index of the 32-byte word to load. + * @return word Decoded amount or value at that offset. + */ + function _decodeReturnWord(bytes memory ret, uint256 wordOffset) internal pure returns (uint256 word) { + uint256 offset = wordOffset * 32; + if (offset + 32 > ret.length) revert ReturnDataOutOfBounds(); + + assembly ("memory-safe") { + // read the word at the offset from return data + word := mload(add(add(ret, 0x20), offset)) + } + } + + /** + * @notice Rescues funds from the contract if they are locked by mistake. + * @param token The address of the token contract. + * @param rescueTo The address where rescued tokens need to be sent. + * @param amount The amount of tokens to be rescued. + */ + function rescueFunds(address token, address rescueTo, uint256 amount) external onlyRole(RESCUE_ROLE) { + RescueFundsLib.rescueFunds(token, rescueTo, amount); + } +} diff --git a/src/common/AccessRoles.sol b/src/common/AccessRoles.sol new file mode 100644 index 0000000..0039d01 --- /dev/null +++ b/src/common/AccessRoles.sol @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.34; + +bytes32 constant RESCUE_ROLE = keccak256("RESCUE_ROLE"); diff --git a/src/common/OpenRouterAuthBase.sol b/src/common/OpenRouterAuthBase.sol index dbf416b..db7316b 100644 --- a/src/common/OpenRouterAuthBase.sol +++ b/src/common/OpenRouterAuthBase.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity =0.8.25; +pragma solidity 0.8.34; import {Ownable} from "./utils/Ownable.sol"; import {AuthenticationLib} from "./lib/AuthenticationLib.sol"; diff --git a/src/common/allowance/AllowanceHolderContext.sol b/src/common/allowance/AllowanceHolderContext.sol index 2ab3f2b..34ed2db 100644 --- a/src/common/allowance/AllowanceHolderContext.sol +++ b/src/common/allowance/AllowanceHolderContext.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity =0.8.25; +pragma solidity 0.8.34; import {ALLOWANCE_HOLDER} from "../interfaces/IAllowanceHolder.sol"; diff --git a/src/common/interfaces/IAllowanceHolder.sol b/src/common/interfaces/IAllowanceHolder.sol index a941f77..1ec809f 100644 --- a/src/common/interfaces/IAllowanceHolder.sol +++ b/src/common/interfaces/IAllowanceHolder.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity =0.8.25; +pragma solidity 0.8.34; // @dev Mainnet AllowanceHolder address. Same address is used for every chain // on which 0x deploys it via the canonical CREATE2 deployer. See: diff --git a/src/common/interfaces/IERC20.sol b/src/common/interfaces/IERC20.sol new file mode 100644 index 0000000..d4ea45e --- /dev/null +++ b/src/common/interfaces/IERC20.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.34; + +interface IERC20 { + function allowance(address owner, address spender) external view returns (uint256); +} diff --git a/src/common/lib/AuthenticationLib.sol b/src/common/lib/AuthenticationLib.sol index d1bfdde..0a65cbd 100644 --- a/src/common/lib/AuthenticationLib.sol +++ b/src/common/lib/AuthenticationLib.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity =0.8.25; +pragma solidity 0.8.34; /// @title AuthenticationLib /// @notice Personal-sign style signature recovery, ported from diff --git a/src/common/lib/BytesSpliceLib.sol b/src/common/lib/BytesSpliceLib.sol index 8426d28..e094de6 100644 --- a/src/common/lib/BytesSpliceLib.sol +++ b/src/common/lib/BytesSpliceLib.sol @@ -1,9 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity =0.8.25; +pragma solidity 0.8.34; /// @title BytesSpliceLib -/// @notice Generalisation of the in-place calldata patching used in -/// GenericStakedRoute and BungeeApproveAndBridge. Supports patching +/// @notice Generalisation of the in-place calldata patching. Supports patching /// either a single 32-byte word (for `uint256` amount fields) or an /// arbitrary length copy from one bytes blob to another. library BytesSpliceLib { @@ -13,7 +12,6 @@ library BytesSpliceLib { error SplicePositionOutOfBounds(); /// @notice Overwrites a 32-byte word at `position` in `data` with `word`. - /// @dev Mirrors the GenericStakedRoute amount patching pattern. function spliceWord(bytes memory data, uint256 position, uint256 word) internal pure { // Bounds check: position + 32 must fit in data if (position + 32 > data.length) { diff --git a/src/common/lib/CurrencyLib.sol b/src/common/lib/CurrencyLib.sol index d6df584..56ca7e0 100644 --- a/src/common/lib/CurrencyLib.sol +++ b/src/common/lib/CurrencyLib.sol @@ -1,10 +1,11 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity =0.8.25; +pragma solidity 0.8.34; import {SafeTransferLib} from "solady/src/utils/SafeTransferLib.sol"; error TransferFailed(); +// @audit Audited before by Hexens: https://github.com/SocketDotTech/audits/blob/main/Bungee/12-2024%20-%20Bungee%20Protocol%20-%20Hexens.pdf /// @title CurrencyLib /// @notice Token transfer + balance helpers that treat the canonical native /// pseudo-token (`0xEee...EEe`) the same way as the marketplace's diff --git a/src/common/lib/RescueFundsLib.sol b/src/common/lib/RescueFundsLib.sol new file mode 100644 index 0000000..22c4423 --- /dev/null +++ b/src/common/lib/RescueFundsLib.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.34; + +// @audit Audited before by Zellic: https://github.com/SocketDotTech/audits/blob/main/Socket-DL/07-2023%20-%20Data%20Layer%20-%20Zellic.pdf +import {SafeTransferLib} from "solady/src/utils/SafeTransferLib.sol"; + +error ZeroAddress(); + +/// @title RescueFundsLib +/// @notice Pull tokens or native ETH from the calling contract to a recipient. +library RescueFundsLib { + address public constant ETH_ADDRESS = address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE); + + error InvalidTokenAddress(); + + /// @param token_ ERC20 token or `ETH_ADDRESS` for native balance. + /// @param rescueTo_ Recipient; must not be zero. + /// @param amount_ Amount to transfer out of `address(this)`. + function rescueFunds(address token_, address rescueTo_, uint256 amount_) internal { + if (rescueTo_ == address(0)) { + revert ZeroAddress(); + } + + if (token_ == ETH_ADDRESS) { + SafeTransferLib.safeTransferETH(rescueTo_, amount_); + } else { + if (token_.code.length == 0) { + revert InvalidTokenAddress(); + } + SafeTransferLib.safeTransfer(token_, rescueTo_, amount_); + } + } +} diff --git a/src/common/utils/AccessControl.sol b/src/common/utils/AccessControl.sol new file mode 100644 index 0000000..51e3f72 --- /dev/null +++ b/src/common/utils/AccessControl.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.34; + +import {Ownable} from "./Ownable.sol"; + +// @audit Audited before by Zellic: https://github.com/SocketDotTech/audits/blob/main/Socket-DL/07-2023%20-%20Data%20Layer%20-%20Zellic.pdf +abstract contract AccessControl is Ownable { + mapping(bytes32 => mapping(address => bool)) private _permits; + + event RoleGranted(bytes32 indexed role, address indexed grantee); + event RoleRevoked(bytes32 indexed role, address indexed revokee); + + error NoPermit(bytes32 role); + + constructor(address owner_) Ownable(owner_) {} + + modifier onlyRole(bytes32 role) { + if (!_permits[role][msg.sender]) revert NoPermit(role); + _; + } + + function grantRole(bytes32 role_, address grantee_) external virtual onlyOwner { + _grantRole(role_, grantee_); + } + + function revokeRole(bytes32 role_, address revokee_) external virtual onlyOwner { + _revokeRole(role_, revokee_); + } + + function hasRole(bytes32 role_, address address_) public view returns (bool) { + return _hasRole(role_, address_); + } + + function _grantRole(bytes32 role_, address grantee_) internal { + _permits[role_][grantee_] = true; + emit RoleGranted(role_, grantee_); + } + + function _revokeRole(bytes32 role_, address revokee_) internal { + _permits[role_][revokee_] = false; + emit RoleRevoked(role_, revokee_); + } + + function _hasRole(bytes32 role_, address address_) internal view returns (bool) { + return _permits[role_][address_]; + } +} diff --git a/src/common/utils/Ownable.sol b/src/common/utils/Ownable.sol index f03d76f..a7c7f17 100644 --- a/src/common/utils/Ownable.sol +++ b/src/common/utils/Ownable.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity =0.8.25; +pragma solidity 0.8.34; +// @audit Audited before by Zellic: https://github.com/SocketDotTech/audits/blob/main/Socket-DL/07-2023%20-%20Data%20Layer%20-%20Zellic.pdf /// @title Ownable /// @notice Two-step ownership transfer, ported from /// marketplace/src/utils/Ownable.sol. Simpler than OpenZeppelin's diff --git a/src/dummyRouter.sol b/src/dummyRouter.sol deleted file mode 100644 index 043f24d..0000000 --- a/src/dummyRouter.sol +++ /dev/null @@ -1,61 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity =0.8.25; - -contract DummyRouter { - - enum CallType { - CALL, - STATICCALL - } - - struct Splice { - uint256 sourceActionIndex; // which previous return data to read from - uint256 srcOffset; // offset inside previous returndata - uint256 dstOffset; // offset inside current calldata - uint256 length; // bytes to copy - } - - struct Action { - CallType callType; - address target; - uint256 value; - bytes data; - Splice[] splices; - } - - function execute(Action[] calldata actions) external payable returns (bytes[] memory results) { - results = new bytes[](actions.length); - - for (uint256 i = 0; i < actions.length; i++) { - bytes memory callData = actions[i].data; - - // Patch this action's calldata using earlier action results. - for (uint256 j = 0; j < actions[i].splices.length; j++) { - Splice calldata s = actions[i].splices[j]; - - bytes memory source = results[s.sourceActionIndex]; - - _copyBytes({ - src: source, - dst: callData, - srcOffset: s.srcOffset, - dstOffset: s.dstOffset, - length: s.length - }); - } - - bool success; - bytes memory ret; - - if (actions[i].callType == CallType.STATICCALL) { - (success, ret) = actions[i].target.staticcall(callData); - } else { - (success, ret) = actions[i].target.call{value: actions[i].value}(callData); - } - - if (!success) revert CallFailed(i, ret); - - results[i] = ret; - } - } -} diff --git a/src/manipulators/AcrossERC20AmountManipulator.sol b/src/manipulators/AcrossERC20AmountManipulator.sol index 783e32b..9df80dc 100644 --- a/src/manipulators/AcrossERC20AmountManipulator.sol +++ b/src/manipulators/AcrossERC20AmountManipulator.sol @@ -1,60 +1,20 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.19; +pragma solidity 0.8.34; -import {ERC20} from "solady/src/tokens/ERC20.sol"; - -/// @title AcrossERC20AmountManipulator -/// @notice Stateless helper for OpenRouter-style batches that need Across deposit amounts after swap and fee transfer +/// @notice Computes the Across output amount that must be spliced into SpokePool.deposit calldata. contract AcrossERC20AmountManipulator { - error InvalidAddress(); error BridgeFeeExceedsInputAmount(); error DecimalDiffTooLarge(); uint256 internal constant MAX_SAFE_DECIMAL_DIFF = 77; - /// @notice Reads the current ERC20 balance and derives Across input/output amounts. - /// @dev Intended to be called after swap and fee-transfer actions have completed. - /// Returndata layout is two ABI words: - /// - offset 0: inputAmount - /// - offset 32: outputAmount - /// @param token ERC20 token to bridge. - /// @param balanceHolder Address whose post-fee balance should be used as Across inputAmount. - /// @param bridgeFee Fee to subtract before deriving outputAmount, denominated in input token decimals. - /// @param inputTokenDecimals Decimals of the source/input token. - /// @param outputTokenDecimals Decimals of the destination/output token. - function acrossAmounts( - address token, - address balanceHolder, - uint256 bridgeFee, - uint256 inputTokenDecimals, - uint256 outputTokenDecimals - ) external view returns (uint256 inputAmount, uint256 outputAmount) { - if (token == address(0) || balanceHolder == address(0)) revert InvalidAddress(); - - inputAmount = ERC20(token).balanceOf(balanceHolder); - outputAmount = deriveOutputAmount(inputAmount, bridgeFee, inputTokenDecimals, outputTokenDecimals); - } - - /// @notice Derives Across input/output amounts from a caller-provided input amount. - /// @dev Use this when a previous OpenRouter action already returned the final post-fee amount. - function acrossAmountsFromInput( - uint256 inputAmount, - uint256 bridgeFee, - uint256 inputTokenDecimals, - uint256 outputTokenDecimals - ) external pure returns (uint256, uint256 outputAmount) { - outputAmount = deriveOutputAmount(inputAmount, bridgeFee, inputTokenDecimals, outputTokenDecimals); - return (inputAmount, outputAmount); - } - - /// @notice Derives Across outputAmount from a runtime inputAmount. /// @dev bridgeFee is denominated in input token decimals. function deriveOutputAmount( uint256 inputAmount, uint256 bridgeFee, uint256 inputTokenDecimals, uint256 outputTokenDecimals - ) public pure returns (uint256 outputAmount) { + ) external pure returns (uint256 outputAmount) { if (bridgeFee > inputAmount) revert BridgeFeeExceedsInputAmount(); uint256 amountAfterFee = inputAmount - bridgeFee; diff --git a/src/manipulators/MathManipulator.sol b/src/manipulators/MathManipulator.sol new file mode 100644 index 0000000..7879cd5 --- /dev/null +++ b/src/manipulators/MathManipulator.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.34; + +/// @notice Generic arithmetic helpers for router calldata splicing. +contract MathManipulator { + uint256 internal constant BPS_DENOMINATOR = 10_000; + + function add(uint256 a, uint256 b) external pure returns (uint256) { + return a + b; + } + + function subtract(uint256 a, uint256 b) external pure returns (uint256) { + return a - b; + } + + function percent(uint256 amount, uint256 bps) external pure returns (uint256) { + return amount * bps / BPS_DENOMINATOR; + } +} diff --git a/src/minimal/BungeeOpenRouterMinimal.sol b/src/minimal/BungeeOpenRouterMinimal.sol deleted file mode 100644 index 86c1bc5..0000000 --- a/src/minimal/BungeeOpenRouterMinimal.sol +++ /dev/null @@ -1,99 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity =0.8.25; - -import {OpenRouterAuthBase} from "../common/OpenRouterAuthBase.sol"; - -/// @title BungeeOpenRouterMinimal (v3, modular w/o splicing) -/// @notice Smallest possible signed-action runner. Identical surface to -/// `BungeeOpenRouterModular` minus the splice mechanism: each -/// `Action` is dispatched standalone via `CALL`, `DELEGATECALL`, or -/// `STATICCALL`, and there is no plumbing of returndata into the -/// next action's calldata. -/// -/// This relies on the assumption that whenever a step needs the -/// "real" amount produced by a previous step (typical for swap-then- -/// bridge flows), the next step's target can re-read that amount -/// itself - usually by calling `balanceOf(this)` at runtime, which -/// is exactly what `BaseRouterSingleOutput`-style pre/post balance -/// deltas do already. -/// -/// @dev Same signing scheme as the other variants: personal_sign over -/// keccak256(abi.encode(chainid, this, exec)). Caller cannot reorder -/// or retarget actions; only re-submission patterns are restricted. -contract BungeeOpenRouterMinimal is OpenRouterAuthBase { - enum CallType { - CALL, - DELEGATECALL, - STATICCALL - } - - struct Action { - CallType callType; - address target; - uint256 value; // forwarded ETH; must be zero for non-CALL types - bytes data; - } - - struct Execution { - Action[] actions; - uint256 nonce; - uint256 deadline; - } - - error ValueOnNonCall(); - error EmptyExecution(); - error UnknownCallType(); - - constructor(address _owner, address _openRouterSigner) OpenRouterAuthBase(_owner, _openRouterSigner) {} - - receive() external payable {} - - function performExecution(Execution calldata exec, bytes calldata signature) external payable virtual { - bytes32 digest = keccak256(abi.encode(block.chainid, address(this), exec)); - _verifyAndConsume(digest, exec.nonce, exec.deadline, signature); - _performActions(exec.actions); - } - - /// @notice Internal action loop, exposed to subclasses. - function _performActions(Action[] calldata actions) internal { - uint256 actionsLen = actions.length; - if (actionsLen == 0) { - revert EmptyExecution(); - } - - for (uint256 i = 0; i < actionsLen;) { - Action calldata a = actions[i]; - _performAction(a.callType, a.target, a.value, a.data); - unchecked { - ++i; - } - } - } - - /// @notice Dispatches a single action; bubbles any revert. - function _performAction(CallType callType, address target, uint256 value, bytes memory data) internal virtual { - bool ok; - bytes memory ret; - if (callType == CallType.CALL) { - (ok, ret) = target.call{value: value}(data); - } else if (callType == CallType.DELEGATECALL) { - if (value != 0) { - revert ValueOnNonCall(); - } - (ok, ret) = target.delegatecall(data); - } else if (callType == CallType.STATICCALL) { - if (value != 0) { - revert ValueOnNonCall(); - } - (ok, ret) = target.staticcall(data); - } else { - revert UnknownCallType(); - } - - if (!ok) { - assembly ("memory-safe") { - revert(add(ret, 0x20), mload(ret)) - } - } - } -} diff --git a/src/minimal/BungeeOpenRouterMinimalAH.sol b/src/minimal/BungeeOpenRouterMinimalAH.sol deleted file mode 100644 index fbd0401..0000000 --- a/src/minimal/BungeeOpenRouterMinimalAH.sol +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity =0.8.25; - -import {BungeeOpenRouterMinimal} from "./BungeeOpenRouterMinimal.sol"; -import {AllowanceHolderContext} from "../common/allowance/AllowanceHolderContext.sol"; - -/// @title BungeeOpenRouterMinimalAH -/// @notice AllowanceHolder variant of `BungeeOpenRouterMinimal`. Adds the -/// confused-deputy `balanceOf` shim and a user-bound entrypoint that -/// pins the signed payload to a specific `signedUser` (the AH.exec -/// caller). Apart from that, the action loop is identical to v3. -contract BungeeOpenRouterMinimalAH is BungeeOpenRouterMinimal, AllowanceHolderContext { - error CallerNotSignedUser(); - - constructor(address _owner, address _openRouterSigner) - BungeeOpenRouterMinimal(_owner, _openRouterSigner) - {} - - /// @notice AllowanceHolder-aware entrypoint. Same role as - /// `BungeeOpenRouterModularAH.performExecutionAH` - prevents a - /// signed payload meant for user A from being submitted via user - /// B's AllowanceHolder.exec to grief user A's nonce. - function performExecutionAH(Execution calldata exec, address signedUser, bytes calldata signature) external payable { - if (_msgSender() != signedUser) { - revert CallerNotSignedUser(); - } - bytes32 digest = keccak256(abi.encode(block.chainid, address(this), signedUser, exec)); - _verifyAndConsume(digest, exec.nonce, exec.deadline, signature); - _performActions(exec.actions); - } -} diff --git a/src/modular/BungeeOpenRouterModular.sol b/src/modular/BungeeOpenRouterModular.sol deleted file mode 100644 index 14983bc..0000000 --- a/src/modular/BungeeOpenRouterModular.sol +++ /dev/null @@ -1,144 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity =0.8.25; - -import {OpenRouterAuthBase} from "../common/OpenRouterAuthBase.sol"; -import {BytesSpliceLib} from "../common/lib/BytesSpliceLib.sol"; - -/// @title BungeeOpenRouterModular (v2, modular + returndata splicing) -/// @notice Lightweight, generic open-router. Only signature verification is -/// hard-wired into the contract; every other step (token pull, pre- -/// swap fee, swap, post-swap fee, bridge call) is just an `Action` -/// executed via `CALL`, `DELEGATECALL`, or `STATICCALL`. -/// -/// To plumb the *output of a previous step into the input calldata -/// of the next*, each `Action` carries a list of `Splice`s. Each -/// splice copies a slice of the previous action's returndata into a -/// specific byte offset of this action's calldata. This generalises -/// the single-position `mstore` patching used in `GenericStakedRoute` -/// and `BungeeApproveAndBridge` to multiple positions of any length. -/// -/// @dev The base calldata for every action comes from the caller (and is -/// therefore covered by the signature). Splices only mutate parts of -/// that base calldata - they cannot replace it wholesale, so even if -/// one of the actions returns adversarial bytes, an attacker can only -/// move signed amount-shaped data, not redirect the call target or -/// alter unrelated fields. -contract BungeeOpenRouterModular is OpenRouterAuthBase { - enum CallType { - CALL, - DELEGATECALL, - STATICCALL - } - - /// @notice Describes a single byte-range copy from the previous action's - /// returndata into this action's calldata. - struct Splice { - uint256 srcOffset; // offset within the previous returndata - uint256 dstOffset; // offset within this action's `data` - uint256 length; // number of bytes to copy - } - - /// @notice One step in the execution pipeline. - struct Action { - CallType callType; - address target; - uint256 value; // forwarded ETH; must be zero for non-CALL types - bytes data; // mutable in memory: splices may patch parts of it - Splice[] splices; // applied BEFORE this action runs - } - - struct Execution { - Action[] actions; - uint256 nonce; - uint256 deadline; - } - - error ValueOnNonCall(); - error EmptyExecution(); - error UnknownCallType(); - - constructor(address _owner, address _openRouterSigner) OpenRouterAuthBase(_owner, _openRouterSigner) {} - - receive() external payable {} - - /// @notice Executes a signed sequence of actions. - /// @dev The signed digest binds chainId, this contract, and the entire - /// action set, so the caller cannot reorder, retarget, or strip - /// splices from any action. - function performExecution(Execution calldata exec, bytes calldata signature) external payable virtual { - bytes32 digest = keccak256(abi.encode(block.chainid, address(this), exec)); - _verifyAndConsume(digest, exec.nonce, exec.deadline, signature); - _performActions(exec.actions); - } - - /// @notice Internal executor for the action loop. Split out so variants - /// (e.g. the AllowanceHolder variant) can add bindings on top of - /// the base signature check without duplicating the loop. - function _performActions(Action[] calldata actions) internal { - uint256 actionsLen = actions.length; - if (actionsLen == 0) { - revert EmptyExecution(); - } - - bytes memory prevReturn; // empty for the first action; splices on action 0 are illegal - for (uint256 i = 0; i < actionsLen;) { - Action calldata a = actions[i]; - - // Copy the action's data into memory so we can splice it in-place. - bytes memory data = a.data; - - // Apply splices: copy slices from prevReturn into data. - uint256 spLen = a.splices.length; - for (uint256 j = 0; j < spLen;) { - Splice calldata sp = a.splices[j]; - BytesSpliceLib.spliceBytes({ - dst: data, // this action's calldata (base is signed; patched before dispatch) - dstOffset: sp.dstOffset, // write `length` bytes into `dst` starting here - src: prevReturn, // read from the previous action's returndata - srcOffset: sp.srcOffset, // copy slice starting at this offset in `src` - length: sp.length // number of bytes to copy (overwrites same span in `dst`) - }); - unchecked { - ++j; - } - } - - prevReturn = _performAction(a.callType, a.target, a.value, data); - - unchecked { - ++i; - } - } - } - - /// @notice Dispatches a single action and returns its returndata. Reverts - /// are bubbled with the underlying revert data. - function _performAction(CallType callType, address target, uint256 value, bytes memory data) - internal - virtual - returns (bytes memory ret) - { - bool ok; - if (callType == CallType.CALL) { - (ok, ret) = target.call{value: value}(data); - } else if (callType == CallType.DELEGATECALL) { - if (value != 0) { - revert ValueOnNonCall(); - } - (ok, ret) = target.delegatecall(data); - } else if (callType == CallType.STATICCALL) { - if (value != 0) { - revert ValueOnNonCall(); - } - (ok, ret) = target.staticcall(data); - } else { - revert UnknownCallType(); - } - - if (!ok) { - assembly ("memory-safe") { - revert(add(ret, 0x20), mload(ret)) - } - } - } -} diff --git a/src/modular/BungeeOpenRouterModularAH.sol b/src/modular/BungeeOpenRouterModularAH.sol deleted file mode 100644 index e0f37cb..0000000 --- a/src/modular/BungeeOpenRouterModularAH.sol +++ /dev/null @@ -1,45 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity =0.8.25; - -import {BungeeOpenRouterModular} from "./BungeeOpenRouterModular.sol"; -import {AllowanceHolderContext} from "../common/allowance/AllowanceHolderContext.sol"; - -/// @title BungeeOpenRouterModularAH -/// @notice AllowanceHolder variant of `BungeeOpenRouterModular`. The actual -/// AllowanceHolder pull is just one of the modular `Action`s (a -/// `CALL` to `ALLOWANCE_HOLDER` with `transferFrom(token, user, this, -/// amount)` calldata), so this contract adds very little on top of -/// the base modular contract: -/// -/// - `AllowanceHolderContext` for the dummy `balanceOf` shim that -/// passes AllowanceHolder's confused-deputy probe. -/// - A new `performExecutionAH` entrypoint that takes an explicit -/// `signedUser` argument, includes it in the signed digest, and -/// enforces `_msgSender() == signedUser`. This stops a malicious -/// actor from wrapping someone else's signed payload inside their -/// own `AllowanceHolder.exec` to grief their nonce. -/// -/// @dev Even without the explicit `signedUser` check the AllowanceHolder -/// allowance scoping (`operator + owner + token`) prevents actual -/// fund theft - any pull whose `owner` differs from the AH.exec -/// caller will revert. The `signedUser` binding is purely to avoid -/// someone else burning a signed-but-unsubmitted payload. -contract BungeeOpenRouterModularAH is BungeeOpenRouterModular, AllowanceHolderContext { - error CallerNotSignedUser(); - - constructor(address _owner, address _openRouterSigner) - BungeeOpenRouterModular(_owner, _openRouterSigner) - {} - - /// @notice AllowanceHolder-aware entrypoint. Bind the signed payload to a - /// specific user so it can only be submitted via that user's - /// `AllowanceHolder.exec` call. - function performExecutionAH(Execution calldata exec, address signedUser, bytes calldata signature) external payable { - if (_msgSender() != signedUser) { - revert CallerNotSignedUser(); - } - bytes32 digest = keccak256(abi.encode(block.chainid, address(this), signedUser, exec)); - _verifyAndConsume(digest, exec.nonce, exec.deadline, signature); - _performActions(exec.actions); - } -} diff --git a/src/monolithic/BungeeOpenRouter.sol b/src/monolithic/BungeeOpenRouter.sol deleted file mode 100644 index df0ac22..0000000 --- a/src/monolithic/BungeeOpenRouter.sol +++ /dev/null @@ -1,190 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity =0.8.25; - -import {SafeTransferLib} from "solady/src/utils/SafeTransferLib.sol"; - -import {OpenRouterAuthBase} from "../common/OpenRouterAuthBase.sol"; -import {BytesSpliceLib} from "../common/lib/BytesSpliceLib.sol"; -import {CurrencyLib} from "../common/lib/CurrencyLib.sol"; - -/// @title BungeeOpenRouter (v1, monolithic) -/// @notice Monolithic, opinionated open-router: pulls ERC20 funds from a user -/// via standard ERC20 `transferFrom`, optionally takes a pre-swap fee, -/// optionally performs a swap, optionally takes a post-swap fee, then -/// executes a single arbitrary bridge call where the final amount is -/// spliced into the bridge calldata at a list of byte positions. -/// -/// This version is the easiest to reason about because every step is -/// laid out explicitly. The trade-off is rigidity - if a route needs -/// a different ordering or a multi-call bridge interaction, see the -/// modular variants (`BungeeOpenRouterModular`, `BungeeOpenRouterMinimal`). -/// -/// @dev Authentication is matched to `Solver` / `StakedRouterReceiver`: -/// - personal_sign + ecrecover via `AuthenticationLib` -/// - single-use nonces marked with the same assembly pattern -/// - signed digest binds `block.chainid` and `address(this)` so that a -/// payload meant for one deployment cannot be replayed elsewhere. -/// - the user, input token + amount, both fee transfers, the swap, -/// and the bridge calldata are ALL part of the signed payload, so a -/// malicious caller cannot redirect funds. -contract BungeeOpenRouter is OpenRouterAuthBase { - // marked virtual so AllowanceHolder variants can override the pull step - // without duplicating the rest of the body. - using SafeTransferLib for address; - - /// @notice Who is sending funds and how much. - struct InputData { - address user; - address inputToken; - uint256 inputAmount; - } - - /// @notice Optional fee taken in the input token before a swap, or in the - /// bridge token when there is no swap. Set `receiver` to address(0) - /// and `amount` to 0 to skip. - struct FeeData { - address receiver; - uint256 amount; - } - - /// @notice Optional swap step. Set `target` to address(0) to skip entirely. - struct SwapData { - address target; - address approvalSpender; // 0 to skip ERC20 approval - address outputToken; // token measured for balance delta - uint256 value; // ETH forwarded to the swap target - uint256 minOutput; // minimum balance delta; reverts if not met - bytes data; - } - - /// @notice Mandatory bridge call. `amountPositions` lists every byte offset - /// in `data` where the final amount (post-fees) must be written - /// before dispatching the call. - struct BridgeData { - address target; - address approvalSpender; // 0 to skip ERC20 approval - uint256 value; // ETH forwarded to the bridge target - bytes data; - uint256[] amountPositions; - } - - /// @notice Full signed payload for one execution. - /// @dev Signed via personal_sign over keccak256(abi.encode(chainid, this, exec)). - struct Execution { - InputData input; - FeeData preFee; // taken in inputToken before swap - SwapData swap; - FeeData postFee; // taken in finalToken after swap - BridgeData bridge; - uint256 nonce; - uint256 deadline; - } - - error SwapOutputInsufficient(); - error InsufficientFunds(); - error InvalidExecution(); - - constructor(address _owner, address _openRouterSigner) OpenRouterAuthBase(_owner, _openRouterSigner) {} - - receive() external payable {} - - /// @notice Executes the signed payload end-to-end. - /// @dev Anyone can call this; the security boundary is the signature. - function performExecution(Execution calldata exec, bytes calldata signature) external payable { - bytes32 digest = keccak256(abi.encode(block.chainid, address(this), exec)); - _verifyAndConsume(digest, exec.nonce, exec.deadline, signature); - - if (exec.bridge.target == address(0) || exec.input.user == address(0) || exec.input.inputToken == address(0)) { - revert InvalidExecution(); - } - - // 1. pull funds from user; ERC20 transferFrom on the base contract, - // AllowanceHolder transferFrom on the AH variant. - _pullFromUser(exec.input.inputToken, exec.input.user, exec.input.inputAmount); - - // 2. optional pre-swap fee in input token - if (exec.preFee.amount != 0) { - CurrencyLib.transfer(exec.input.inputToken, exec.preFee.receiver, exec.preFee.amount); - } - - // 3. optional swap, accounted via balance delta - address finalToken; - uint256 finalAmount; - if (exec.swap.target != address(0)) { - (finalToken, finalAmount) = _performSwap(exec); - } else { - // no swap path: input minus pre-fee is what we have on-hand - if (exec.preFee.amount > exec.input.inputAmount) { - revert InsufficientFunds(); - } - finalToken = exec.input.inputToken; - unchecked { - finalAmount = exec.input.inputAmount - exec.preFee.amount; - } - } - - // 4. optional post-swap fee in final token - if (exec.postFee.amount != 0) { - if (exec.postFee.amount > finalAmount) { - revert InsufficientFunds(); - } - CurrencyLib.transfer(finalToken, exec.postFee.receiver, exec.postFee.amount); - unchecked { - finalAmount -= exec.postFee.amount; - } - } - - // 5. patch bridge calldata with final amount at every signed position - bytes memory bridgeData = exec.bridge.data; - BytesSpliceLib.spliceWords({data: bridgeData, positions: exec.bridge.amountPositions, word: finalAmount}); - - // 6. optional approval to the bridge spender (no-op if same as target via permit / native) - if (exec.bridge.approvalSpender != address(0) && finalToken != CurrencyLib.NATIVE_TOKEN_ADDRESS) { - SafeTransferLib.safeApproveWithRetry(finalToken, exec.bridge.approvalSpender, finalAmount); - } - - // 7. dispatch the bridge call, bubbling any revert - _performAction(exec.bridge.target, exec.bridge.value, bridgeData); - } - - /// @notice Hook for pulling `amount` of `token` from `user` into this - /// contract. Default uses ERC20 transferFrom; the AllowanceHolder - /// variant overrides this to call AllowanceHolder. - function _pullFromUser(address token, address user, uint256 amount) internal virtual { - SafeTransferLib.safeTransferFrom(token, user, address(this), amount); - } - - /// @dev Split out so the main `performExecution` body stays under the - /// marketplace "≤ 100 lines / SRP" guideline. - function _performSwap(Execution calldata exec) internal returns (address finalToken, uint256 finalAmount) { - // Snapshot pre-swap balance of the swap output token on this contract. - uint256 preBalance = CurrencyLib.balanceOf(exec.swap.outputToken, address(this)); - - // Approve swap router to pull the input token if it expects an allowance. - if (exec.swap.approvalSpender != address(0) && exec.input.inputToken != CurrencyLib.NATIVE_TOKEN_ADDRESS) { - // amount available for swap = inputAmount - preFee - uint256 swapInput; - unchecked { - swapInput = exec.input.inputAmount - exec.preFee.amount; - } - SafeTransferLib.safeApproveWithRetry(exec.input.inputToken, exec.swap.approvalSpender, swapInput); - } - - _performAction(exec.swap.target, exec.swap.value, exec.swap.data); - - uint256 postBalance = CurrencyLib.balanceOf(exec.swap.outputToken, address(this)); - if (postBalance < preBalance) { - revert SwapOutputInsufficient(); - } - uint256 delta; - unchecked { - delta = postBalance - preBalance; - } - if (delta < exec.swap.minOutput) { - revert SwapOutputInsufficient(); - } - - finalToken = exec.swap.outputToken; - finalAmount = delta; - } -} diff --git a/src/monolithic/BungeeOpenRouterAH.sol b/src/monolithic/BungeeOpenRouterAH.sol deleted file mode 100644 index 3340b53..0000000 --- a/src/monolithic/BungeeOpenRouterAH.sol +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity =0.8.25; - -import {BungeeOpenRouter} from "./BungeeOpenRouter.sol"; -import {AllowanceHolderContext} from "../common/allowance/AllowanceHolderContext.sol"; -import {ALLOWANCE_HOLDER} from "../common/interfaces/IAllowanceHolder.sol"; - -/// @title BungeeOpenRouterAH -/// @notice AllowanceHolder variant of `BungeeOpenRouter`. Identical flow, -/// except that user funds are pulled via 0x's AllowanceHolder -/// (transient-storage allowance) rather than a persistent ERC20 -/// allowance to this contract. -/// -/// Expected flow: -/// 1. user (off-chain) approves AllowanceHolder for `inputToken`. -/// 2. backend signer signs the same `Execution` payload as v1. -/// 3. user calls `AllowanceHolder.exec(operator=this, inputToken, -/// inputAmount, target=this, callData=this.execute(...))`. -/// 4. AllowanceHolder writes a transient allowance and forwards the -/// call to this contract with the user's address appended to -/// calldata (ERC-2771 style). -/// 5. this contract verifies the signature, then calls -/// `AllowanceHolder.transferFrom(inputToken, user, address(this), -/// inputAmount)` to pull the funds. -/// 6. remaining steps are identical to v1. -/// -/// @dev We enforce `_msgSender() == exec.user` so the AllowanceHolder -/// ephemeral allowance (keyed by `operator + owner + token`) actually -/// belongs to the user named in the signed payload. -contract BungeeOpenRouterAH is BungeeOpenRouter, AllowanceHolderContext { - error CallerNotSignedUser(); - - constructor(address _owner, address _openRouterSigner) BungeeOpenRouter(_owner, _openRouterSigner) {} - - /// @notice Override the v1 fund-pull hook to use AllowanceHolder. - /// @dev Assembly path mirrors `0x-settler/src/core/Permit2Payment.sol` - /// `_allowanceHolderTransferFrom`. AllowanceHolder's `transferFrom` - /// either reverts or returns true, so we don't bother decoding the - /// return value. - function _pullFromUser(address token, address user, uint256 amount) internal override { - // The signed user MUST equal the original AllowanceHolder.exec caller, - // because AllowanceHolder writes the transient allowance for - // (operator=this, owner=msg.sender_to_AH, token). - if (_msgSender() != user) { - revert CallerNotSignedUser(); - } - - address allowanceHolder = address(ALLOWANCE_HOLDER); - // Build calldata for: AllowanceHolder.transferFrom(token, user, address(this), amount) - // Selector: 0x15dacbea = bytes4(keccak256("transferFrom(address,address,address,uint256)")) - assembly ("memory-safe") { - let ptr := mload(0x40) - mstore(add(0x80, ptr), amount) - mstore(add(0x60, ptr), address()) - mstore(add(0x4c, ptr), shl(0x60, user)) // clears recipient padding - mstore(add(0x2c, ptr), shl(0xa0, token)) // clears owner padding - mstore(add(0x0c, ptr), 0x15dacbea000000000000000000000000) // selector + token padding - - if iszero(call(gas(), allowanceHolder, 0x00, add(0x1c, ptr), 0x84, 0x00, 0x00)) { - let p := mload(0x40) - returndatacopy(p, 0x00, returndatasize()) - revert(p, returndatasize()) - } - } - } -} diff --git a/test/combined/OpenRouterV2UncheckedBridge.t.sol b/test/combined/OpenRouterV2UncheckedBridge.t.sol new file mode 100644 index 0000000..e6c5f81 --- /dev/null +++ b/test/combined/OpenRouterV2UncheckedBridge.t.sol @@ -0,0 +1,193 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.34; + +import {OpenRouter as Router} from "../../src/OpenRouter.sol"; +import {OpenRouterV2UncheckedTestBase} from "./OpenRouterV2UncheckedTestBase.sol"; + +contract OpenRouterV2UncheckedBridgeTest is OpenRouterV2UncheckedTestBase { + function test_bridge_erc20() public { + _deal(address(inputToken), USER, INPUT_AMOUNT); + _approveInputToken(INPUT_AMOUNT); + + _assertTokenBalances( + address(inputToken), + Balances({ + user: INPUT_AMOUNT, + router: 0, + swapTarget: 0, + bridgeTarget: 0, + receiver: 0, + feeRecipient: 0, + allowanceHolder: 0, + testContract: 0 + }), + "input token initial" + ); + + _execBridge( + address(inputToken), + INPUT_AMOUNT, + 0, + Router.FeeData({receiver: address(0), amount: 0}), + _bridgeData(address(inputToken), 0), + _bridgeCallData(address(inputToken), INPUT_AMOUNT) + ); + + _assertTokenBalances( + address(inputToken), + Balances({ + user: 0, + router: 0, + swapTarget: 0, + bridgeTarget: INPUT_AMOUNT, + receiver: 0, + feeRecipient: 0, + allowanceHolder: 0, + testContract: 0 + }), + "input token final" + ); + assertEq(bridgeTarget.receivedToken(), address(inputToken)); + assertEq(bridgeTarget.receivedAmount(), INPUT_AMOUNT); + } + + function test_bridge_native() public { + vm.deal(USER, INPUT_AMOUNT); + uint256 testContractBalance = address(this).balance; + + _assertTokenBalances( + NATIVE_TOKEN, + Balances({ + user: INPUT_AMOUNT, + router: 0, + swapTarget: 0, + bridgeTarget: 0, + receiver: 0, + feeRecipient: 0, + allowanceHolder: 0, + testContract: testContractBalance + }), + "native initial" + ); + + _execBridge( + NATIVE_TOKEN, + INPUT_AMOUNT, + INPUT_AMOUNT, + Router.FeeData({receiver: address(0), amount: 0}), + _bridgeData(NATIVE_TOKEN, INPUT_AMOUNT), + _bridgeCallData(NATIVE_TOKEN, INPUT_AMOUNT) + ); + + _assertTokenBalances( + NATIVE_TOKEN, + Balances({ + user: 0, + router: 0, + swapTarget: 0, + bridgeTarget: INPUT_AMOUNT, + receiver: 0, + feeRecipient: 0, + allowanceHolder: 0, + testContract: testContractBalance + }), + "native final" + ); + assertEq(bridgeTarget.receivedToken(), NATIVE_TOKEN); + assertEq(bridgeTarget.receivedAmount(), INPUT_AMOUNT); + } + + function test_bridge_withErc20Fee() public { + uint256 bridgeAmount = INPUT_AMOUNT - FEE_AMOUNT; + _deal(address(inputToken), USER, INPUT_AMOUNT); + _approveInputToken(INPUT_AMOUNT); + + _assertTokenBalances( + address(inputToken), + Balances({ + user: INPUT_AMOUNT, + router: 0, + swapTarget: 0, + bridgeTarget: 0, + receiver: 0, + feeRecipient: 0, + allowanceHolder: 0, + testContract: 0 + }), + "input token initial" + ); + + _execBridge( + address(inputToken), + INPUT_AMOUNT, + 0, + Router.FeeData({receiver: FEE_RECIPIENT, amount: FEE_AMOUNT}), + _bridgeData(address(inputToken), 0), + _bridgeCallData(address(inputToken), bridgeAmount) + ); + + _assertTokenBalances( + address(inputToken), + Balances({ + user: 0, + router: 0, + swapTarget: 0, + bridgeTarget: bridgeAmount, + receiver: 0, + feeRecipient: FEE_AMOUNT, + allowanceHolder: 0, + testContract: 0 + }), + "input token final" + ); + assertEq(bridgeTarget.receivedToken(), address(inputToken)); + assertEq(bridgeTarget.receivedAmount(), bridgeAmount); + } + + function test_bridge_withNativeFee() public { + uint256 bridgeAmount = INPUT_AMOUNT - FEE_AMOUNT; + vm.deal(USER, INPUT_AMOUNT); + uint256 testContractBalance = address(this).balance; + + _assertTokenBalances( + NATIVE_TOKEN, + Balances({ + user: INPUT_AMOUNT, + router: 0, + swapTarget: 0, + bridgeTarget: 0, + receiver: 0, + feeRecipient: 0, + allowanceHolder: 0, + testContract: testContractBalance + }), + "native initial" + ); + + _execBridge( + NATIVE_TOKEN, + INPUT_AMOUNT, + INPUT_AMOUNT, + Router.FeeData({receiver: FEE_RECIPIENT, amount: FEE_AMOUNT}), + _bridgeData(NATIVE_TOKEN, bridgeAmount), + _bridgeCallData(NATIVE_TOKEN, bridgeAmount) + ); + + _assertTokenBalances( + NATIVE_TOKEN, + Balances({ + user: 0, + router: 0, + swapTarget: 0, + bridgeTarget: bridgeAmount, + receiver: 0, + feeRecipient: FEE_AMOUNT, + allowanceHolder: 0, + testContract: testContractBalance + }), + "native final" + ); + assertEq(bridgeTarget.receivedToken(), NATIVE_TOKEN); + assertEq(bridgeTarget.receivedAmount(), bridgeAmount); + } +} diff --git a/test/combined/OpenRouterV2UncheckedSwap.t.sol b/test/combined/OpenRouterV2UncheckedSwap.t.sol new file mode 100644 index 0000000..b36aca1 --- /dev/null +++ b/test/combined/OpenRouterV2UncheckedSwap.t.sol @@ -0,0 +1,303 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.34; + +import {OpenRouter as Router} from "../../src/OpenRouter.sol"; +import {OpenRouterV2UncheckedTestBase} from "./OpenRouterV2UncheckedTestBase.sol"; + +contract OpenRouterV2UncheckedSwapTest is OpenRouterV2UncheckedTestBase { + function test_swapWithReturnData() public { + _deal(address(inputToken), USER, INPUT_AMOUNT); + _deal(address(outputToken), address(swapTarget), SWAP_OUTPUT_AMOUNT); + _approveInputToken(INPUT_AMOUNT); + + _assertERC20Balances(address(inputToken), INPUT_AMOUNT, 0, 0, 0, "before input token"); + _assertERC20Balances(address(outputToken), 0, SWAP_OUTPUT_AMOUNT, 0, 0, "before output token"); + _assertNativeBalances(0, 0, 0, 0, "before native"); + + uint256 finalAmount = _execSwap( + SwapParams({ + input: address(inputToken), + inputAmount: INPUT_AMOUNT, + value: 0, + receiver: RECEIVER, + flags: 0, + fee: _feeData(0), + swapData: _swapData(address(inputToken), address(outputToken), SWAP_OUTPUT_AMOUNT, true), + swapCallData: _swapCallData( + address(inputToken), address(outputToken), INPUT_AMOUNT, SWAP_OUTPUT_AMOUNT, RECEIVER + ) + }) + ); + + assertEq(finalAmount, SWAP_OUTPUT_AMOUNT, "final amount"); + + _assertERC20Balances(address(inputToken), 0, INPUT_AMOUNT, 0, 0, "after input token"); + _assertERC20Balances(address(outputToken), 0, 0, SWAP_OUTPUT_AMOUNT, 0, "after output token"); + _assertNativeBalances(0, 0, 0, 0, "after native"); + _assertSwapInput(address(inputToken), INPUT_AMOUNT); + } + + function test_swapWithoutReturnDataUsesBalanceDelta() public { + _deal(address(inputToken), USER, INPUT_AMOUNT); + _deal(address(outputToken), address(swapTarget), SWAP_OUTPUT_AMOUNT); + _approveInputToken(INPUT_AMOUNT); + + _assertERC20Balances(address(inputToken), INPUT_AMOUNT, 0, 0, 0, "before input token"); + _assertERC20Balances(address(outputToken), 0, SWAP_OUTPUT_AMOUNT, 0, 0, "before output token"); + _assertNativeBalances(0, 0, 0, 0, "before native"); + + uint256 finalAmount = _execSwap( + SwapParams({ + input: address(inputToken), + inputAmount: INPUT_AMOUNT, + value: 0, + receiver: RECEIVER, + flags: BALANCE_FLAG_BIT_MASK, + fee: _feeData(0), + swapData: _swapData(address(inputToken), address(outputToken), SWAP_OUTPUT_AMOUNT, false), + swapCallData: _swapNoReturnCallData( + address(inputToken), address(outputToken), INPUT_AMOUNT, SWAP_OUTPUT_AMOUNT, RECEIVER + ) + }) + ); + + assertEq(finalAmount, SWAP_OUTPUT_AMOUNT, "final amount"); + + _assertERC20Balances(address(inputToken), 0, INPUT_AMOUNT, 0, 0, "after input token"); + _assertERC20Balances(address(outputToken), 0, 0, SWAP_OUTPUT_AMOUNT, 0, "after output token"); + _assertNativeBalances(0, 0, 0, 0, "after native"); + _assertSwapInput(address(inputToken), INPUT_AMOUNT); + } + + function test_swapERC20ToNative() public { + _deal(address(inputToken), USER, INPUT_AMOUNT); + _deal(NATIVE_TOKEN, address(swapTarget), SWAP_OUTPUT_AMOUNT); + _approveInputToken(INPUT_AMOUNT); + + _assertERC20Balances(address(inputToken), INPUT_AMOUNT, 0, 0, 0, "before input token"); + _assertNativeBalances(0, SWAP_OUTPUT_AMOUNT, 0, 0, "before native"); + + uint256 finalAmount = _execSwap( + SwapParams({ + input: address(inputToken), + inputAmount: INPUT_AMOUNT, + value: 0, + receiver: RECEIVER, + flags: 0, + fee: _feeData(0), + swapData: _swapData(address(inputToken), NATIVE_TOKEN, SWAP_OUTPUT_AMOUNT, true), + swapCallData: _swapCallData(address(inputToken), NATIVE_TOKEN, INPUT_AMOUNT, SWAP_OUTPUT_AMOUNT, RECEIVER) + }) + ); + + assertEq(finalAmount, SWAP_OUTPUT_AMOUNT, "final amount"); + + _assertERC20Balances(address(inputToken), 0, INPUT_AMOUNT, 0, 0, "after input token"); + _assertNativeBalances(0, 0, SWAP_OUTPUT_AMOUNT, 0, "after native"); + + _assertSwapInput(address(inputToken), INPUT_AMOUNT); + } + + function test_swapNativeToERC20() public { + _deal(NATIVE_TOKEN, USER, INPUT_AMOUNT); + _deal(address(outputToken), address(swapTarget), SWAP_OUTPUT_AMOUNT); + + _assertNativeBalances(INPUT_AMOUNT, 0, 0, 0, "before native"); + _assertERC20Balances(address(outputToken), 0, SWAP_OUTPUT_AMOUNT, 0, 0, "before output token"); + + uint256 finalAmount = _execSwap( + SwapParams({ + input: NATIVE_TOKEN, + inputAmount: INPUT_AMOUNT, + value: INPUT_AMOUNT, + receiver: RECEIVER, + flags: 0, + fee: _feeData(0), + swapData: _swapData(NATIVE_TOKEN, address(outputToken), SWAP_OUTPUT_AMOUNT, true), + swapCallData: _swapCallData(NATIVE_TOKEN, address(outputToken), INPUT_AMOUNT, SWAP_OUTPUT_AMOUNT, RECEIVER) + }) + ); + + assertEq(finalAmount, SWAP_OUTPUT_AMOUNT, "final amount"); + + _assertNativeBalances(0, INPUT_AMOUNT, 0, 0, "after native"); + _assertERC20Balances(address(outputToken), 0, 0, SWAP_OUTPUT_AMOUNT, 0, "after output token"); + + _assertSwapInput(NATIVE_TOKEN, INPUT_AMOUNT); + } + + function test_swapERC20ToERC20() public { + test_swapWithReturnData(); + } + + function test_prefeeSwapWithNativeFee() public { + uint256 swapInput = INPUT_AMOUNT - FEE_AMOUNT; + + _deal(NATIVE_TOKEN, USER, INPUT_AMOUNT); + _deal(address(outputToken), address(swapTarget), SWAP_OUTPUT_AMOUNT); + + _assertNativeBalances(INPUT_AMOUNT, 0, 0, 0, "before native"); + _assertERC20Balances(address(outputToken), 0, SWAP_OUTPUT_AMOUNT, 0, 0, "before output token"); + + uint256 finalAmount = _execSwap( + SwapParams({ + input: NATIVE_TOKEN, + inputAmount: INPUT_AMOUNT, + value: INPUT_AMOUNT, + receiver: RECEIVER, + flags: 0, + fee: _feeData(FEE_AMOUNT), + swapData: _swapDataWithValue(NATIVE_TOKEN, address(outputToken), SWAP_OUTPUT_AMOUNT, swapInput), + swapCallData: _swapCallData(NATIVE_TOKEN, address(outputToken), swapInput, SWAP_OUTPUT_AMOUNT, RECEIVER) + }) + ); + + assertEq(finalAmount, SWAP_OUTPUT_AMOUNT, "final amount"); + + _assertNativeBalances(0, swapInput, 0, FEE_AMOUNT, "after native"); + _assertERC20Balances(address(outputToken), 0, 0, SWAP_OUTPUT_AMOUNT, 0, "after output token"); + + _assertSwapInput(NATIVE_TOKEN, swapInput); + } + + function test_prefeeSwapWithERC20Fee() public { + uint256 swapInput = INPUT_AMOUNT - FEE_AMOUNT; + + _deal(address(inputToken), USER, INPUT_AMOUNT); + _deal(address(outputToken), address(swapTarget), SWAP_OUTPUT_AMOUNT); + _approveInputToken(INPUT_AMOUNT); + + _assertERC20Balances(address(inputToken), INPUT_AMOUNT, 0, 0, 0, "before input token"); + _assertERC20Balances(address(outputToken), 0, SWAP_OUTPUT_AMOUNT, 0, 0, "before output token"); + _assertNativeBalances(0, 0, 0, 0, "before native"); + + uint256 finalAmount = _execSwap( + SwapParams({ + input: address(inputToken), + inputAmount: INPUT_AMOUNT, + value: 0, + receiver: RECEIVER, + flags: 0, + fee: _feeData(FEE_AMOUNT), + swapData: _swapData(address(inputToken), address(outputToken), SWAP_OUTPUT_AMOUNT, true), + swapCallData: _swapCallData( + address(inputToken), address(outputToken), swapInput, SWAP_OUTPUT_AMOUNT, RECEIVER + ) + }) + ); + + assertEq(finalAmount, SWAP_OUTPUT_AMOUNT, "final amount"); + + _assertERC20Balances(address(inputToken), 0, swapInput, 0, FEE_AMOUNT, "after input token"); + _assertERC20Balances(address(outputToken), 0, 0, SWAP_OUTPUT_AMOUNT, 0, "after output token"); + _assertNativeBalances(0, 0, 0, 0, "after native"); + _assertSwapInput(address(inputToken), swapInput); + } + + function test_postfeeSwapWithNativeFee() public { + _deal(address(inputToken), USER, INPUT_AMOUNT); + _deal(NATIVE_TOKEN, address(swapTarget), SWAP_OUTPUT_AMOUNT); + _approveInputToken(INPUT_AMOUNT); + + _assertERC20Balances(address(inputToken), INPUT_AMOUNT, 0, 0, 0, "before input token"); + _assertNativeBalances(0, SWAP_OUTPUT_AMOUNT, 0, 0, "before native"); + + uint256 finalAmount = _execSwap( + SwapParams({ + input: address(inputToken), + inputAmount: INPUT_AMOUNT, + value: 0, + receiver: RECEIVER, + flags: FEE_FLAG_BIT_MASK, + fee: _feeData(FEE_AMOUNT), + swapData: _swapData(address(inputToken), NATIVE_TOKEN, SWAP_OUTPUT_AMOUNT, true), + swapCallData: _swapCallData( + address(inputToken), NATIVE_TOKEN, INPUT_AMOUNT, SWAP_OUTPUT_AMOUNT, address(router) + ) + }) + ); + + assertEq(finalAmount, SWAP_OUTPUT_AMOUNT - FEE_AMOUNT, "final amount"); + + _assertERC20Balances(address(inputToken), 0, INPUT_AMOUNT, 0, 0, "after input token"); + _assertNativeBalances(0, 0, SWAP_OUTPUT_AMOUNT - FEE_AMOUNT, FEE_AMOUNT, "after native"); + + _assertSwapInput(address(inputToken), INPUT_AMOUNT); + } + + function test_postfeeSwapWithERC20Fee() public { + _deal(NATIVE_TOKEN, USER, INPUT_AMOUNT); + _deal(address(outputToken), address(swapTarget), SWAP_OUTPUT_AMOUNT); + + _assertNativeBalances(INPUT_AMOUNT, 0, 0, 0, "before native"); + _assertERC20Balances(address(outputToken), 0, SWAP_OUTPUT_AMOUNT, 0, 0, "before output token"); + + uint256 finalAmount = _execSwap( + SwapParams({ + input: NATIVE_TOKEN, + inputAmount: INPUT_AMOUNT, + value: INPUT_AMOUNT, + receiver: RECEIVER, + flags: FEE_FLAG_BIT_MASK, + fee: _feeData(FEE_AMOUNT), + swapData: _swapData(NATIVE_TOKEN, address(outputToken), SWAP_OUTPUT_AMOUNT, true), + swapCallData: _swapCallData( + NATIVE_TOKEN, address(outputToken), INPUT_AMOUNT, SWAP_OUTPUT_AMOUNT, address(router) + ) + }) + ); + + assertEq(finalAmount, SWAP_OUTPUT_AMOUNT - FEE_AMOUNT, "final amount"); + + _assertNativeBalances(0, INPUT_AMOUNT, 0, 0, "after native"); + _assertERC20Balances( + address(outputToken), 0, 0, SWAP_OUTPUT_AMOUNT - FEE_AMOUNT, FEE_AMOUNT, "after output token" + ); + + _assertSwapInput(NATIVE_TOKEN, INPUT_AMOUNT); + } + + function _feeData(uint256 amount) private pure returns (Router.FeeData memory) { + return Router.FeeData({receiver: FEE_RECIPIENT, amount: amount}); + } + + function _emptyNativeBalances() private view returns (Balances memory balances) { + balances.testContract = address(this).balance; + } + + function _assertERC20Balances( + address token, + uint256 user, + uint256 swapTargetBalance, + uint256 receiver, + uint256 feeRecipient, + string memory label + ) private view { + Balances memory balances = _emptyBalances(); + balances.user = user; + balances.swapTarget = swapTargetBalance; + balances.receiver = receiver; + balances.feeRecipient = feeRecipient; + _assertTokenBalances(token, balances, label); + } + + function _assertNativeBalances( + uint256 user, + uint256 swapTargetBalance, + uint256 receiver, + uint256 feeRecipient, + string memory label + ) private view { + Balances memory balances = _emptyNativeBalances(); + balances.user = user; + balances.swapTarget = swapTargetBalance; + balances.receiver = receiver; + balances.feeRecipient = feeRecipient; + _assertTokenBalances(NATIVE_TOKEN, balances, label); + } + + function _assertSwapInput(address input, uint256 amount) private view { + assertEq(swapTarget.storedInputToken(), input, "swap input token"); + assertEq(swapTarget.storedInputAmount(), amount, "swap input amount"); + } +} diff --git a/test/combined/OpenRouterV2UncheckedSwapAndBridge.t.sol b/test/combined/OpenRouterV2UncheckedSwapAndBridge.t.sol new file mode 100644 index 0000000..3eddd22 --- /dev/null +++ b/test/combined/OpenRouterV2UncheckedSwapAndBridge.t.sol @@ -0,0 +1,207 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.34; + +import {OpenRouter as Router} from "../../src/OpenRouter.sol"; +import {OpenRouterV2UncheckedTestBase} from "./OpenRouterV2UncheckedTestBase.sol"; + +contract OpenRouterV2UncheckedSwapAndBridgeTest is OpenRouterV2UncheckedTestBase { + enum FeeMode { + None, + Pre, + Post + } + + struct Scenario { + address input; + address output; + FeeMode feeMode; + bool balanceDelta; + uint256 swapInput; + uint256 bridgeAmount; + } + + function test_swapAndBridge_noFee_erc20ToNative() public { + _runSwapAndBridge(address(inputToken), NATIVE_TOKEN, FeeMode.None, false); + } + + function test_swapAndBridge_noFee_nativeToErc20() public { + _runSwapAndBridge(NATIVE_TOKEN, address(outputToken), FeeMode.None, false); + } + + function test_swapAndBridge_noFee_erc20ToErc20() public { + _runSwapAndBridge(address(inputToken), address(outputToken), FeeMode.None, false); + } + + function test_swapAndBridge_prefee_erc20ToNative() public { + _runSwapAndBridge(address(inputToken), NATIVE_TOKEN, FeeMode.Pre, false); + } + + function test_swapAndBridge_prefee_nativeToErc20() public { + _runSwapAndBridge(NATIVE_TOKEN, address(outputToken), FeeMode.Pre, false); + } + + function test_swapAndBridge_prefee_erc20ToErc20() public { + _runSwapAndBridge(address(inputToken), address(outputToken), FeeMode.Pre, false); + } + + function test_swapAndBridge_postfee_erc20ToNative() public { + _runSwapAndBridge(address(inputToken), NATIVE_TOKEN, FeeMode.Post, false); + } + + function test_swapAndBridge_postfee_nativeToErc20() public { + _runSwapAndBridge(NATIVE_TOKEN, address(outputToken), FeeMode.Post, false); + } + + function test_swapAndBridge_postfee_erc20ToErc20() public { + _runSwapAndBridge(address(inputToken), address(outputToken), FeeMode.Post, false); + } + + function test_swapAndBridge_balanceDelta_noFee_erc20ToNative() public { + _runSwapAndBridge(address(inputToken), NATIVE_TOKEN, FeeMode.None, true); + } + + function test_swapAndBridge_balanceDelta_noFee_nativeToErc20() public { + _runSwapAndBridge(NATIVE_TOKEN, address(outputToken), FeeMode.None, true); + } + + function test_swapAndBridge_balanceDelta_noFee_erc20ToErc20() public { + _runSwapAndBridge(address(inputToken), address(outputToken), FeeMode.None, true); + } + + function test_swapAndBridge_balanceDelta_prefee_erc20ToNative() public { + _runSwapAndBridge(address(inputToken), NATIVE_TOKEN, FeeMode.Pre, true); + } + + function test_swapAndBridge_balanceDelta_prefee_nativeToErc20() public { + _runSwapAndBridge(NATIVE_TOKEN, address(outputToken), FeeMode.Pre, true); + } + + function test_swapAndBridge_balanceDelta_prefee_erc20ToErc20() public { + _runSwapAndBridge(address(inputToken), address(outputToken), FeeMode.Pre, true); + } + + function test_swapAndBridge_balanceDelta_postfee_erc20ToNative() public { + _runSwapAndBridge(address(inputToken), NATIVE_TOKEN, FeeMode.Post, true); + } + + function test_swapAndBridge_balanceDelta_postfee_nativeToErc20() public { + _runSwapAndBridge(NATIVE_TOKEN, address(outputToken), FeeMode.Post, true); + } + + function test_swapAndBridge_balanceDelta_postfee_erc20ToErc20() public { + _runSwapAndBridge(address(inputToken), address(outputToken), FeeMode.Post, true); + } + + function _runSwapAndBridge(address input, address output, FeeMode feeMode, bool balanceDelta) internal { + Scenario memory scenario = _scenario(input, output, feeMode, balanceDelta); + + _fundSwapAndBridge(scenario.input, scenario.output); + if (scenario.input != NATIVE_TOKEN) _approveInputToken(INPUT_AMOUNT); + + _assertSwapAndBridgeInitial(scenario.input, scenario.output); + _executeSwapAndBridge(scenario); + _assertSwapAndBridgeFinal(scenario); + + assertEq(swapTarget.storedInputToken(), scenario.input); + assertEq(swapTarget.storedInputAmount(), scenario.swapInput); + assertEq(bridgeTarget.receivedToken(), scenario.output); + assertEq(bridgeTarget.receivedAmount(), scenario.bridgeAmount); + } + + function _scenario(address input, address output, FeeMode feeMode, bool balanceDelta) + internal + pure + returns (Scenario memory scenario) + { + scenario.input = input; + scenario.output = output; + scenario.feeMode = feeMode; + scenario.balanceDelta = balanceDelta; + scenario.swapInput = _swapInput(feeMode); + scenario.bridgeAmount = _bridgeAmount(feeMode); + } + + function _executeSwapAndBridge(Scenario memory scenario) internal { + _execThroughAllowanceHolder( + scenario.input, + INPUT_AMOUNT, + scenario.input == NATIVE_TOKEN ? INPUT_AMOUNT : 0, + _swapAndBridgeCallData(scenario) + ); + } + + function _swapAndBridgeCallData(Scenario memory scenario) internal view returns (bytes memory) { + return abi.encodeCall( + router.swapAndBridge, + ( + keccak256("swap-and-bridge"), + _flags(scenario.output, scenario.feeMode, scenario.balanceDelta), + Router.InputData({user: USER, inputToken: scenario.input, inputAmount: INPUT_AMOUNT}), + _fee(scenario.feeMode), + _swapDataWithValue( + scenario.input, + scenario.output, + SWAP_OUTPUT_AMOUNT, + scenario.input == NATIVE_TOKEN ? scenario.swapInput : 0 + ), + _swapCallData(scenario), + _bridgeData(scenario.output, 0), + _bridgeCallData(scenario.output, 0) + ) + ); + } + + function _swapCallData(Scenario memory scenario) internal view returns (bytes memory) { + if (scenario.balanceDelta) { + return _swapNoReturnCallData( + scenario.input, scenario.output, scenario.swapInput, SWAP_OUTPUT_AMOUNT, address(router) + ); + } + return _swapCallData(scenario.input, scenario.output, scenario.swapInput, SWAP_OUTPUT_AMOUNT, address(router)); + } + + function _fundSwapAndBridge(address input, address output) internal { + _deal(input, USER, INPUT_AMOUNT); + _deal(output, address(swapTarget), SWAP_OUTPUT_AMOUNT); + } + + function _assertSwapAndBridgeInitial(address input, address output) internal view { + Balances memory inputBalances = _emptyBalancesFor(input); + inputBalances.user = INPUT_AMOUNT; + _assertTokenBalances(input, inputBalances, "input initial"); + Balances memory outputBalances = _emptyBalancesFor(output); + outputBalances.swapTarget = SWAP_OUTPUT_AMOUNT; + _assertTokenBalances(output, outputBalances, "output initial"); + } + + function _assertSwapAndBridgeFinal(Scenario memory scenario) internal view { + Balances memory inputBalances = _emptyBalancesFor(scenario.input); + inputBalances.swapTarget = scenario.swapInput; + inputBalances.feeRecipient = scenario.feeMode == FeeMode.Pre ? FEE_AMOUNT : 0; + _assertTokenBalances(scenario.input, inputBalances, "input final"); + Balances memory outputBalances = _emptyBalancesFor(scenario.output); + outputBalances.bridgeTarget = scenario.bridgeAmount; + outputBalances.feeRecipient = scenario.feeMode == FeeMode.Post ? FEE_AMOUNT : 0; + _assertTokenBalances(scenario.output, outputBalances, "output final"); + } + + function _swapInput(FeeMode feeMode) internal pure returns (uint256) { + return feeMode == FeeMode.Pre ? INPUT_AMOUNT - FEE_AMOUNT : INPUT_AMOUNT; + } + + function _bridgeAmount(FeeMode feeMode) internal pure returns (uint256) { + return feeMode == FeeMode.Post ? SWAP_OUTPUT_AMOUNT - FEE_AMOUNT : SWAP_OUTPUT_AMOUNT; + } + + function _flags(address output, FeeMode feeMode, bool balanceDelta) internal pure returns (uint256) { + uint256 flags = balanceDelta ? BALANCE_FLAG_BIT_MASK : 0; + if (output == NATIVE_TOKEN) flags |= BRIDGE_VALUE_FLAG_BIT_MASK; + if (feeMode == FeeMode.Post) flags |= FEE_FLAG_BIT_MASK; + return _bridgeAmountSpliceFlags(flags); + } + + function _fee(FeeMode feeMode) internal pure returns (Router.FeeData memory) { + if (feeMode == FeeMode.None) return Router.FeeData({receiver: address(0), amount: 0}); + return Router.FeeData({receiver: FEE_RECIPIENT, amount: FEE_AMOUNT}); + } +} diff --git a/test/combined/OpenRouterV2UncheckedTestBase.sol b/test/combined/OpenRouterV2UncheckedTestBase.sol new file mode 100644 index 0000000..1b3fda1 --- /dev/null +++ b/test/combined/OpenRouterV2UncheckedTestBase.sol @@ -0,0 +1,376 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.34; + +import {Test} from "forge-std/Test.sol"; +import {ERC20} from "solady/src/tokens/ERC20.sol"; + +import {OpenRouter as Router} from "../../src/OpenRouter.sol"; +import {ALLOWANCE_HOLDER, IAllowanceHolder} from "../../src/common/interfaces/IAllowanceHolder.sol"; + +abstract contract OpenRouterV2UncheckedTestBase is Test { + uint256 internal constant FEE_FLAG_BIT_MASK = 0x01; + uint256 internal constant BALANCE_FLAG_BIT_MASK = 0x02; + uint256 internal constant BRIDGE_VALUE_FLAG_BIT_MASK = 0x04; + uint256 internal constant BRIDGE_AMOUNT_POSITION_FLAG_BIT_MASK = 0x08; + uint256 internal constant BRIDGE_AMOUNT_POSITION_SHIFT = 16; + uint256 internal constant BRIDGE_AMOUNT_CALLDATA_OFFSET = 36; + + address internal constant NATIVE_TOKEN = address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE); + address internal constant USER = address(0xA11CE); + address internal constant RECEIVER = address(0xB0B); + address internal constant FEE_RECIPIENT = address(0xFEE); + + uint256 internal constant INPUT_AMOUNT = 100 ether; + uint256 internal constant SWAP_OUTPUT_AMOUNT = 175 ether; + uint256 internal constant FEE_AMOUNT = 7 ether; + + Router internal router; + MockERC20 internal inputToken; + MockERC20 internal outputToken; + MockSwap internal swapTarget; + MockBridge internal bridgeTarget; + + struct Balances { + uint256 user; + uint256 router; + uint256 swapTarget; + uint256 bridgeTarget; + uint256 receiver; + uint256 feeRecipient; + uint256 allowanceHolder; + uint256 testContract; + } + + struct SwapParams { + address input; + uint256 inputAmount; + uint256 value; + address receiver; + uint256 flags; + Router.FeeData fee; + Router.SwapData swapData; + bytes swapCallData; + } + + struct SwapAndBridgeParams { + address input; + uint256 inputAmount; + uint256 value; + uint256 flags; + Router.FeeData fee; + Router.SwapData swapData; + bytes swapCallData; + Router.BridgeData bridgeData; + bytes bridgeCallData; + } + + function setUp() public virtual { + vm.etch(address(ALLOWANCE_HOLDER), address(new MockAllowanceHolder()).code); + + router = new Router(address(this)); + inputToken = new MockERC20("Input Token", "IN"); + outputToken = new MockERC20("Output Token", "OUT"); + swapTarget = new MockSwap(); + bridgeTarget = new MockBridge(); + + vm.label(address(router), "router"); + vm.label(address(inputToken), "inputToken"); + vm.label(address(outputToken), "outputToken"); + vm.label(address(swapTarget), "swapTarget"); + vm.label(address(bridgeTarget), "bridgeTarget"); + vm.label(address(ALLOWANCE_HOLDER), "allowanceHolder"); + vm.label(USER, "user"); + vm.label(RECEIVER, "receiver"); + vm.label(FEE_RECIPIENT, "feeRecipient"); + } + + function _approveInputToken(uint256 amount) internal { + vm.prank(USER); + inputToken.approve(address(ALLOWANCE_HOLDER), amount); + } + + function _execThroughAllowanceHolder(address token, uint256 amount, uint256 value, bytes memory data) + internal + returns (bytes memory result) + { + vm.prank(USER); + result = IAllowanceHolder(address(ALLOWANCE_HOLDER)).exec{value: value}( + address(router), token, amount, payable(address(router)), data + ); + } + + function _execSwap(SwapParams memory params) internal returns (uint256 finalAmount) { + bytes memory result = _execThroughAllowanceHolder( + params.input, + params.inputAmount, + params.value, + abi.encodeCall( + router.swap, + ( + keccak256("swap"), + params.flags, + Router.InputData({user: USER, inputToken: params.input, inputAmount: params.inputAmount}), + params.fee, + params.swapData, + params.swapCallData, + params.receiver + ) + ) + ); + finalAmount = abi.decode(result, (uint256)); + } + + function _execBridge( + address input, + uint256 inputAmount, + uint256 value, + Router.FeeData memory fee, + Router.BridgeData memory bridgeData, + bytes memory bridgeCallData + ) internal { + _execThroughAllowanceHolder( + input, + inputAmount, + value, + abi.encodeCall( + router.bridge, + ( + keccak256("bridge"), + Router.InputData({user: USER, inputToken: input, inputAmount: inputAmount}), + fee, + bridgeData, + bridgeCallData + ) + ) + ); + } + + function _execSwapAndBridge(SwapAndBridgeParams memory params) internal { + _execThroughAllowanceHolder( + params.input, + params.inputAmount, + params.value, + abi.encodeCall( + router.swapAndBridge, + ( + keccak256("swap-and-bridge"), + params.flags, + Router.InputData({user: USER, inputToken: params.input, inputAmount: params.inputAmount}), + params.fee, + params.swapData, + params.swapCallData, + params.bridgeData, + params.bridgeCallData + ) + ) + ); + } + + function _swapData(address input, address output, uint256 outputAmount, bool useReturnData) + internal + view + returns (Router.SwapData memory) + { + return Router.SwapData({ + target: address(swapTarget), + approvalSpender: input == NATIVE_TOKEN ? address(0) : address(swapTarget), + outputToken: output, + value: input == NATIVE_TOKEN ? INPUT_AMOUNT : 0, + minOutput: outputAmount, + returnDataWordOffset: useReturnData ? 0 : 0 + }); + } + + function _swapDataWithValue(address input, address output, uint256 outputAmount, uint256 value) + internal + view + returns (Router.SwapData memory) + { + return Router.SwapData({ + target: address(swapTarget), + approvalSpender: input == NATIVE_TOKEN ? address(0) : address(swapTarget), + outputToken: output, + value: value, + minOutput: outputAmount, + returnDataWordOffset: 0 + }); + } + + function _bridgeData(address token, uint256 value) internal view returns (Router.BridgeData memory) { + return Router.BridgeData({ + target: address(bridgeTarget), + approvalSpender: token == NATIVE_TOKEN ? address(0) : address(bridgeTarget), + value: value + }); + } + + function _swapCallData(address input, address output, uint256 inputAmount, uint256 outputAmount, address receiver) + internal + pure + returns (bytes memory) + { + return abi.encodeCall(MockSwap.swap, (input, output, inputAmount, outputAmount, receiver)); + } + + function _swapNoReturnCallData( + address input, + address output, + uint256 inputAmount, + uint256 outputAmount, + address receiver + ) internal pure returns (bytes memory) { + return abi.encodeCall(MockSwap.swapNoReturn, (input, output, inputAmount, outputAmount, receiver)); + } + + function _bridgeCallData(address token, uint256 amount) internal pure returns (bytes memory) { + return abi.encodeCall(MockBridge.bridge, (token, amount)); + } + + function _bridgeAmountSpliceFlags(uint256 baseFlags) internal pure returns (uint256) { + return baseFlags | BRIDGE_AMOUNT_POSITION_FLAG_BIT_MASK + | (BRIDGE_AMOUNT_CALLDATA_OFFSET << BRIDGE_AMOUNT_POSITION_SHIFT); + } + + function _assertTokenBalances(address token, Balances memory expected, string memory label) internal view { + assertEq(_balanceOf(token, USER), expected.user, string.concat(label, ": user")); + assertEq(_balanceOf(token, address(router)), expected.router, string.concat(label, ": router")); + assertEq(_balanceOf(token, address(swapTarget)), expected.swapTarget, string.concat(label, ": swap")); + assertEq(_balanceOf(token, address(bridgeTarget)), expected.bridgeTarget, string.concat(label, ": bridge")); + assertEq(_balanceOf(token, RECEIVER), expected.receiver, string.concat(label, ": receiver")); + assertEq(_balanceOf(token, FEE_RECIPIENT), expected.feeRecipient, string.concat(label, ": fee recipient")); + assertEq( + _balanceOf(token, address(ALLOWANCE_HOLDER)), + expected.allowanceHolder, + string.concat(label, ": allowance holder") + ); + assertEq(_balanceOf(token, address(this)), expected.testContract, string.concat(label, ": test contract")); + } + + function _balanceOf(address token, address account) internal view returns (uint256) { + if (token == NATIVE_TOKEN) return account.balance; + return ERC20(token).balanceOf(account); + } + + function _emptyBalances() internal pure returns (Balances memory balances) {} + + function _emptyBalancesFor(address token) internal view returns (Balances memory balances) { + if (token == NATIVE_TOKEN) balances.testContract = address(this).balance; + } + + function _deal(address token, address account, uint256 amount) internal { + if (token == NATIVE_TOKEN) { + vm.deal(account, amount); + } else { + MockERC20(token).mint(account, amount); + } + } +} + +contract MockAllowanceHolder { + function exec(address, address, uint256, address payable target, bytes calldata data) + external + payable + returns (bytes memory result) + { + (bool success, bytes memory returndata) = target.call{value: msg.value}(bytes.concat(data, bytes20(msg.sender))); + if (!success) { + assembly ("memory-safe") { + revert(add(returndata, 0x20), mload(returndata)) + } + } + return returndata; + } + + function transferFrom(address token, address owner, address recipient, uint256 amount) external returns (bool) { + require(ERC20(token).transferFrom(owner, recipient, amount), "MockAllowanceHolder: transfer failed"); + return true; + } +} + +contract MockSwap { + address public storedInputToken; + uint256 public storedInputAmount; + + receive() external payable {} + + function swap(address inputToken, address outputToken, uint256 inputAmount, uint256 outputAmount, address receiver) + external + payable + returns (uint256) + { + _swap(inputToken, outputToken, inputAmount, outputAmount, receiver); + return outputAmount; + } + + function swapNoReturn( + address inputToken, + address outputToken, + uint256 inputAmount, + uint256 outputAmount, + address receiver + ) external payable { + _swap(inputToken, outputToken, inputAmount, outputAmount, receiver); + } + + function _swap(address inputToken, address outputToken, uint256 inputAmount, uint256 outputAmount, address receiver) + internal + { + storedInputToken = inputToken; + storedInputAmount += inputAmount; + + if (inputToken == address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE)) { + require(msg.value == inputAmount, "MockSwap: bad native input"); + } else { + require(msg.value == 0, "MockSwap: unexpected value"); + require(ERC20(inputToken).transferFrom(msg.sender, address(this), inputAmount), "MockSwap: input failed"); + } + + if (outputToken == address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE)) { + (bool success,) = receiver.call{value: outputAmount}(""); + require(success, "MockSwap: native output failed"); + } else { + require(ERC20(outputToken).transfer(receiver, outputAmount), "MockSwap: output failed"); + } + } +} + +contract MockBridge { + address public receivedToken; + uint256 public receivedAmount; + + receive() external payable {} + + function bridge(address token, uint256 amount) external payable { + receivedToken = token; + receivedAmount += amount; + + if (token == address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE)) { + require(msg.value == amount, "MockBridge: bad native amount"); + } else { + require(msg.value == 0, "MockBridge: unexpected value"); + require(ERC20(token).transferFrom(msg.sender, address(this), amount), "MockBridge: transfer failed"); + } + } +} + +contract MockERC20 is ERC20 { + string private _name; + string private _symbol; + + constructor(string memory name_, string memory symbol_) { + _name = name_; + _symbol = symbol_; + } + + function name() public view override returns (string memory) { + return _name; + } + + function symbol() public view override returns (string memory) { + return _symbol; + } + + function mint(address to, uint256 amount) external { + _mint(to, amount); + } +} diff --git a/test/poc/OneInchCctpOpenRouterPoC.t.sol b/test/poc/OneInchCctpOpenRouterPoC.t.sol new file mode 100644 index 0000000..d5f939e --- /dev/null +++ b/test/poc/OneInchCctpOpenRouterPoC.t.sol @@ -0,0 +1,332 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.34; + +import {Test} from "forge-std/Test.sol"; +import {ERC20} from "solady/src/tokens/ERC20.sol"; + +import {OpenRouter as Router} from "../../src/OpenRouter.sol"; +import {ALLOWANCE_HOLDER, IAllowanceHolder} from "../../src/common/interfaces/IAllowanceHolder.sol"; + +interface ITokenMessengerV2 { + function depositForBurn( + uint256 amount, + uint32 destinationDomain, + bytes32 mintRecipient, + address burnToken, + bytes32 destinationCaller, + uint256 maxFee, + uint32 minFinalityThreshold + ) external; +} + +// ref tx 0x3ce8e42b6a0b1f8dbc2f2872deb4c74f3b24d6814b2466134129ead30c2ca1de +contract OneInchCctpOpenRouterPoCTest is Test { + address internal constant ONEINCH_SWAP_TARGET = 0x6352a56caadC4F1E25CD6c75970Fa768A3304e64; + address internal constant CCTP_TOKEN_MESSENGER_V2 = 0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d; + address internal constant FIXTURE_ROUTER = 0x3a23F943181408EAC424116Af7b7790c94Cb97a5; + address internal constant FIXTURE_RECIPIENT = 0xB0BBff6311B7F245761A7846d3Ce7B1b100C1836; + address internal constant FEE_RECIPIENT = 0xc91E5068968ACAEC9C8E7C056390d9e3CB34f7FC; + address internal constant POLYGON_AAVE = 0xD6DF932A45C0f255f85145f286eA0b292B21C90B; + address internal constant POLYGON_USDC = 0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359; + + uint256 internal constant FORK_BLOCK_NUMBER = 86_816_149; + uint256 internal constant FORK_BLOCK_TIMESTAMP = 0x6a0451c3; + uint256 internal constant SWAP_INPUT_AAVE = 0x2d169fe80174000; + uint256 internal constant EXPECTED_SWAP_OUTPUT_USDC = 0x132b02c; + uint256 internal constant ROUTE_FEE_USDC = 0x7530; + uint256 internal constant EXPECTED_CCTP_BURN_AMOUNT = 0x1323afc; + uint256 internal constant REFERENCE_GATEWAY_REMAINING_AAVE_ALLOWANCE = 0x564150ddc57; + uint256 internal constant REFERENCE_GATEWAY_INITIAL_AAVE_ALLOWANCE = + SWAP_INPUT_AAVE + REFERENCE_GATEWAY_REMAINING_AAVE_ALLOWANCE; + uint32 internal constant BASE_CCTP_DOMAIN = 6; + uint256 internal constant CCTP_MAX_FEE = 0x2710; + uint32 internal constant CCTP_MIN_FINALITY_THRESHOLD = 1000; + + string internal constant SOCKET_GATEWAY_REFERENCE_CALLDATA_PREFIX = + "0x000001ad4db9cf6a00000000000000000000000000000000000000000000000000000000000001a60000000000000000000000000000000000000000000000000000000000000120000000000000000000000000b0bbff6311b7f245761A7846d3Ce7B1b100C1836000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000021050000000000000000000000000000000000000000000000000000000000007530000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003e8000000000000000000000000000000000000000000000000000000000000271000000000000000000000000000000000000000000000000000000000000013e4ee8f0b86000000000000000000000000d6df932a45c0f255f85145f286ea0b292b21c90b0000000000000000000000003c499c542cef5e3811e1192ce70d8cc03d5c335900000000000000000000000000000000000000000000000002d169fe80174000000000000000000000000000000000000000000000000000000000000000271000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000001304"; + string internal constant SOCKET_GATEWAY_REFERENCE_CALLDATA_SUFFIX = + "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; + string internal constant ONEINCH_SWAP_CALLDATA = + "0x90411a320000000000000000000000001e82ad8a12068a85fcb96368463b434e77b21201000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001c0000000000000000000000000d6df932a45c0f255f85145f286ea0b292b21c90b0000000000000000000000003c499c542cef5e3811e1192ce70d8cc03d5c33590000000000000000000000001e82ad8a12068a85fcb96368463b434e77b212010000000000000000000000003a23f943181408eac424116af7b7790c94cb97a500000000000000000000000000000000000000000000000002d169fe801740000000000000000000000000000000000000000000000000000000000001140299000000000000000000000000000000000000000000000000000000000132b02c000000000000000000000000000000000000000000000000000000000000000200000000000000000000000038c7720238a2c123814aaf1a3d0e31e0093af04600000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000034000000000000000000000000000000000000000000000000000000000000008a00000000000000000000000000000000000000000000000000000000000000aa00000000000000000000000000000000000000000000000000000000000000da00000000000000000000000000000000000000000000000000000000000000ec000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000064eb5625d9000000000000000000000000d6df932a45c0f255f85145f286ea0b292b21c90b000000000000000000000000000000000022d473030f116ddee9f6b43ac78ba300000000000000000000000000000000000000000000000002d169fe8017400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000022d473030f116ddee9f6b43ac78ba3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000008487517c45000000000000000000000000d6df932a45c0f255f85145f286ea0b292b21c90b0000000000000000000000001095692a6237d83c6a72f3f5efedb9a670c4922300000000000000000000000000000000000000000000000002d169fe801740000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001095692a6237d83c6a72f3f5efedb9a670c4922300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000004a424856bc300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000003c0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000003070b0e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000002a000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000d6df932a45c0f255f85145f286ea0b292b21c90b000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000002d169fe8017400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000bb8000000000000000000000000000000000000000000000000000000000000003c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000d6df932a45c0f255f85145f286ea0b292b21c90b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001e82ad8a12068a85fcb96368463b434e77b21201000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001449f86542200000000000000000000000000000000000000000000000000000000000010100000000000000000000000000000000100000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000004d0e30db00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000002449f8654220000000000000000000000000d500b1d8e8ef31e21c99d1db9a6444d3adf127000000000000000000000000000000001000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000004400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000104e5b07cdb000000000000000000000000b6e57ed85c4c9dbfef2a68711e9d6f36c56e0fcb000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000001e82ad8a12068a85fcb96368463b434e77b2120100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000002e0d500b1d8e8ef31e21c99d1db9a6444d3adf12700001f43c499c542cef5e3811e1192ce70d8cc03d5c33590000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000648a6a1e850000000000000000000000003c499c542cef5e3811e1192ce70d8cc03d5c3359000000000000000000000000922164bbbd36acf9e854acbbf32facc949fcaeef000300000000000000000000000000000000000000000000000000000132c7bb00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001a49f8654220000000000000000000000003c499c542cef5e3811e1192ce70d8cc03d5c335900000000000000000000000000000001000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000004400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000064d1660f990000000000000000000000003c499c542cef5e3811e1192ce70d8cc03d5c33590000000000000000000000003a23f943181408eac424116af7b7790c94cb97a500000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; + + function test_oneInchSwapCctpBridge_polygonFork() public { + string memory rpcUrl = vm.envOr("POLYGON_RPC", string("")); + if (bytes(rpcUrl).length != 0) { + uint256 forkBlock = vm.envOr("POLYGON_FORK_BLOCK", FORK_BLOCK_NUMBER); + vm.createSelectFork(rpcUrl, forkBlock); + vm.warp(FORK_BLOCK_TIMESTAMP); + } + + Router router = _routerAtFixtureAddress(); + if (bytes(rpcUrl).length == 0) { + emit log("Set POLYGON_RPC to execute this fork PoC."); + return; + } + + uint256 inputAmount = vm.envOr("POC_AAVE_AMOUNT", SWAP_INPUT_AAVE); + + deal(POLYGON_AAVE, FIXTURE_RECIPIENT, inputAmount); + deal(POLYGON_AAVE, address(router), 0); + deal(POLYGON_USDC, address(router), 0); + + vm.prank(FIXTURE_RECIPIENT); + ERC20(POLYGON_AAVE).approve(address(ALLOWANCE_HOLDER), inputAmount); + + uint256 feeRecipientUsdcBefore = ERC20(POLYGON_USDC).balanceOf(FEE_RECIPIENT); + uint256 usdcSupplyBefore = ERC20(POLYGON_USDC).totalSupply(); + + Router.Action[] memory actions = _buildActions(inputAmount, vm.parseBytes(ONEINCH_SWAP_CALLDATA)); + + vm.prank(FIXTURE_RECIPIENT); + uint256 gasBeforeExecute = gasleft(); + IAllowanceHolder(address(ALLOWANCE_HOLDER)).exec( + address(router), + POLYGON_AAVE, + inputAmount, + payable(address(router)), + abi.encodeCall(router.performActions, (keccak256("one-inch-cctp-modular"), actions)) + ); + uint256 executeGasUsed = gasBeforeExecute - gasleft(); + emit log_named_uint("AllowanceHolder.exec -> router.performActions gas used", executeGasUsed); + + _assertPocResult(router, feeRecipientUsdcBefore, usdcSupplyBefore); + } + + function test_oneInchSwapCctpBridgeSwapAndBridge_polygonFork() public { + string memory rpcUrl = vm.envOr("POLYGON_RPC", string("")); + if (bytes(rpcUrl).length != 0) { + uint256 forkBlock = vm.envOr("POLYGON_FORK_BLOCK", FORK_BLOCK_NUMBER); + vm.createSelectFork(rpcUrl, forkBlock); + vm.warp(FORK_BLOCK_TIMESTAMP); + } + + Router router = _routerAtFixtureAddress(); + if (bytes(rpcUrl).length == 0) { + emit log("Set POLYGON_RPC to execute this fork PoC."); + return; + } + + uint256 inputAmount = vm.envOr("POC_AAVE_AMOUNT", SWAP_INPUT_AAVE); + + deal(POLYGON_AAVE, FIXTURE_RECIPIENT, inputAmount); + deal(POLYGON_AAVE, address(router), 0); + deal(POLYGON_USDC, address(router), 0); + + vm.prank(FIXTURE_RECIPIENT); + ERC20(POLYGON_AAVE).approve(address(ALLOWANCE_HOLDER), inputAmount); + + uint256 feeRecipientUsdcBefore = ERC20(POLYGON_USDC).balanceOf(FEE_RECIPIENT); + uint256 usdcSupplyBefore = ERC20(POLYGON_USDC).totalSupply(); + + bytes memory routerCallData = _swapAndBridgeCallData(inputAmount, vm.parseBytes(ONEINCH_SWAP_CALLDATA)); + + vm.prank(FIXTURE_RECIPIENT); + uint256 gasBeforeExecute = gasleft(); + IAllowanceHolder(address(ALLOWANCE_HOLDER)).exec( + address(router), POLYGON_AAVE, inputAmount, payable(address(router)), routerCallData + ); + uint256 executeGasUsed = gasBeforeExecute - gasleft(); + emit log_named_uint("AllowanceHolder.exec -> router.swapAndBridge gas used", executeGasUsed); + + _assertPocResult(router, feeRecipientUsdcBefore, usdcSupplyBefore); + } + + function test_oneInchSwapCctpBridgeSocketGatewayReference_polygonFork() public { + string memory rpcUrl = vm.envOr("POLYGON_RPC", string("")); + if (bytes(rpcUrl).length != 0) { + uint256 forkBlock = vm.envOr("POLYGON_FORK_BLOCK", FORK_BLOCK_NUMBER); + vm.createSelectFork(rpcUrl, forkBlock); + vm.warp(FORK_BLOCK_TIMESTAMP); + } + + if (bytes(rpcUrl).length == 0) { + emit log("Set POLYGON_RPC to execute this fork PoC."); + return; + } + + uint256 inputAmount = vm.envOr("POC_AAVE_AMOUNT", SWAP_INPUT_AAVE); + assertEq(inputAmount, SWAP_INPUT_AAVE, "reference gateway calldata fixes the input amount"); + + deal(POLYGON_AAVE, FIXTURE_RECIPIENT, inputAmount); + deal(POLYGON_AAVE, FIXTURE_ROUTER, 0); + deal(POLYGON_USDC, FIXTURE_ROUTER, 0); + + vm.prank(FIXTURE_RECIPIENT); + ERC20(POLYGON_AAVE).approve(FIXTURE_ROUTER, REFERENCE_GATEWAY_INITIAL_AAVE_ALLOWANCE); + + uint256 feeRecipientUsdcBefore = ERC20(POLYGON_USDC).balanceOf(FEE_RECIPIENT); + uint256 usdcSupplyBefore = ERC20(POLYGON_USDC).totalSupply(); + + bytes memory referenceCalldata = _referenceSocketGatewayCalldata(); + + vm.prank(FIXTURE_RECIPIENT, FIXTURE_RECIPIENT); + uint256 gasBeforeExecute = gasleft(); + (bool success, bytes memory returndata) = FIXTURE_ROUTER.call(referenceCalldata); + uint256 executeGasUsed = gasBeforeExecute - gasleft(); + if (!success) { + assembly ("memory-safe") { + revert(add(returndata, 0x20), mload(returndata)) + } + } + emit log_named_uint("SocketGateway reference call gas used", executeGasUsed); + + _assertSocketGatewayPocResult(feeRecipientUsdcBefore, usdcSupplyBefore); + } + + function _buildActions(uint256 inputAmount, bytes memory swapCalldata) + internal + pure + returns (Router.Action[] memory actions) + { + actions = new Router.Action[](7); + actions[0] = _action( + Router.CallType.CALL, + address(ALLOWANCE_HOLDER), + abi.encodeWithSelector( + IAllowanceHolder.transferFrom.selector, POLYGON_AAVE, FIXTURE_RECIPIENT, FIXTURE_ROUTER, inputAmount + ), + new uint256[](0), + false + ); + + actions[1] = _action( + Router.CallType.CALL, + POLYGON_AAVE, + abi.encodeWithSelector(ERC20.approve.selector, ONEINCH_SWAP_TARGET, inputAmount), + new uint256[](0), + false + ); + + actions[2] = _action(Router.CallType.CALL, ONEINCH_SWAP_TARGET, swapCalldata, new uint256[](0), true); + + actions[3] = _action( + Router.CallType.CALL, + POLYGON_USDC, + abi.encodeWithSelector(ERC20.transfer.selector, FEE_RECIPIENT, ROUTE_FEE_USDC), + new uint256[](0), + false + ); + + actions[4] = _action( + Router.CallType.CALL, + POLYGON_USDC, + abi.encodeWithSelector(ERC20.approve.selector, CCTP_TOKEN_MESSENGER_V2, type(uint256).max), + new uint256[](0), + false + ); + + actions[5] = _action( + Router.CallType.STATICCALL, + POLYGON_USDC, + abi.encodeWithSelector(ERC20.balanceOf.selector, FIXTURE_ROUTER), + new uint256[](0), + true + ); + + uint256[] memory depositSplices = new uint256[](1); + depositSplices[0] = _splice(5, 0, 4, 32); + actions[6] = _action( + Router.CallType.CALL, CCTP_TOKEN_MESSENGER_V2, _emptyDepositForBurnCalldata(), depositSplices, false + ); + } + + function _swapAndBridgeCallData(uint256 inputAmount, bytes memory swapCalldata) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + Router.swapAndBridge.selector, + keccak256("one-inch-cctp-swap-and-bridge"), + uint256(0x01 | 0x08 | (uint256(4) << 16)), + Router.InputData({user: FIXTURE_RECIPIENT, inputToken: POLYGON_AAVE, inputAmount: inputAmount}), + Router.FeeData({receiver: FEE_RECIPIENT, amount: ROUTE_FEE_USDC}), + Router.SwapData({ + target: ONEINCH_SWAP_TARGET, + approvalSpender: ONEINCH_SWAP_TARGET, + outputToken: POLYGON_USDC, + value: 0, + minOutput: EXPECTED_SWAP_OUTPUT_USDC, + returnDataWordOffset: 0 + }), + swapCalldata, + Router.BridgeData({target: CCTP_TOKEN_MESSENGER_V2, approvalSpender: CCTP_TOKEN_MESSENGER_V2, value: 0}), + _emptyDepositForBurnCalldata() + ); + } + + function _assertPocResult(Router router, uint256 feeRecipientUsdcBefore, uint256 usdcSupplyBefore) internal view { + assertEq(ERC20(POLYGON_USDC).balanceOf(FEE_RECIPIENT) - feeRecipientUsdcBefore, ROUTE_FEE_USDC); + assertEq(ERC20(POLYGON_USDC).totalSupply(), usdcSupplyBefore - EXPECTED_CCTP_BURN_AMOUNT); + assertEq(ERC20(POLYGON_AAVE).balanceOf(FIXTURE_RECIPIENT), 0); + assertEq(ERC20(POLYGON_AAVE).balanceOf(address(router)), 0); + assertEq(ERC20(POLYGON_USDC).balanceOf(address(router)), 0); + } + + function _assertSocketGatewayPocResult(uint256 feeRecipientUsdcBefore, uint256 usdcSupplyBefore) internal view { + assertEq( + ERC20(POLYGON_AAVE).allowance(FIXTURE_RECIPIENT, FIXTURE_ROUTER), REFERENCE_GATEWAY_REMAINING_AAVE_ALLOWANCE + ); + assertEq(ERC20(POLYGON_USDC).balanceOf(FEE_RECIPIENT) - feeRecipientUsdcBefore, ROUTE_FEE_USDC); + assertEq(ERC20(POLYGON_USDC).totalSupply(), usdcSupplyBefore - EXPECTED_CCTP_BURN_AMOUNT); + assertEq(ERC20(POLYGON_AAVE).balanceOf(FIXTURE_RECIPIENT), 0); + assertEq(ERC20(POLYGON_AAVE).balanceOf(FIXTURE_ROUTER), 0); + assertEq(ERC20(POLYGON_USDC).balanceOf(FIXTURE_ROUTER), 0); + } + + function _routerAtFixtureAddress() internal returns (Router router) { + Router implementation = new Router(address(this)); + vm.etch(FIXTURE_ROUTER, address(implementation).code); + return Router(payable(FIXTURE_ROUTER)); + } + + function _emptyDepositForBurnCalldata() internal pure returns (bytes memory) { + return abi.encodeCall( + ITokenMessengerV2.depositForBurn, + ( + uint256(0), + BASE_CCTP_DOMAIN, + _toBytes32(FIXTURE_RECIPIENT), + POLYGON_USDC, + bytes32(0), + CCTP_MAX_FEE, + CCTP_MIN_FINALITY_THRESHOLD + ) + ); + } + + function _referenceSocketGatewayCalldata() internal pure returns (bytes memory) { + return bytes.concat( + vm.parseBytes(SOCKET_GATEWAY_REFERENCE_CALLDATA_PREFIX), + vm.parseBytes(ONEINCH_SWAP_CALLDATA), + vm.parseBytes(SOCKET_GATEWAY_REFERENCE_CALLDATA_SUFFIX) + ); + } + + function _action( + Router.CallType callType, + address target, + bytes memory data, + uint256[] memory splices, + bool storeResult + ) internal pure returns (Router.Action memory) { + return Router.Action({actionInfo: _actionInfo(callType, target, storeResult), data: data, splices: splices}); + } + + function _actionInfo(Router.CallType callType, address target, bool storeResult) internal pure returns (uint256) { + return uint256(uint8(callType)) | (storeResult ? uint256(1) << 8 : 0) | (uint256(uint160(target)) << 16); + } + + function _splice(uint64 sourceActionIndex, uint64 srcOffset, uint64 dstOffset, uint64 length) + internal + pure + returns (uint256) + { + return uint256(sourceActionIndex) | (uint256(srcOffset) << 64) | (uint256(dstOffset) << 128) + | (uint256(length) << 192); + } + + function _toBytes32(address addr) internal pure returns (bytes32) { + return bytes32(uint256(uint160(addr))); + } +} diff --git a/test/poc/OpenOceanAcrossOpenRouterPoC.t.sol b/test/poc/OpenOceanAcrossOpenRouterPoC.t.sol new file mode 100644 index 0000000..57522fd --- /dev/null +++ b/test/poc/OpenOceanAcrossOpenRouterPoC.t.sol @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.34; + +import {Test} from "forge-std/Test.sol"; +import {ERC20} from "solady/src/tokens/ERC20.sol"; + +import {OpenRouter as Router} from "../../src/OpenRouter.sol"; +import {AcrossERC20AmountManipulator} from "../../src/manipulators/AcrossERC20AmountManipulator.sol"; + +interface ISpokePool { + function deposit( + bytes32 depositor, + bytes32 recipient, + bytes32 inputToken, + bytes32 outputToken, + uint256 inputAmount, + uint256 outputAmount, + uint256 destinationChainId, + bytes32 exclusiveRelayer, + uint32 quoteTimestamp, + uint32 fillDeadline, + uint32 exclusivityParameter, + bytes calldata message + ) external payable; +} +// ref tx 0xc0ba134856d0151eebfeb67aabe0eb12db248974f4d78b9d358a6d46dcaa9700 + +contract OpenOceanAcrossOpenRouterPoCTest is Test { + address internal constant OPENOCEAN_EXCHANGE_V2 = 0x6352a56caadC4F1E25CD6c75970Fa768A3304e64; + address internal constant ACROSS_ARBITRUM_SPOKE_POOL = 0xe35e9842fceaCA96570B734083f4a58e8F7C5f2A; + address internal constant FIXTURE_ROUTER = 0x3a23F943181408EAC424116Af7b7790c94Cb97a5; + address internal constant FIXTURE_RECIPIENT = 0xB0BBff6311B7F245761A7846d3Ce7B1b100C1836; + address internal constant ARBITRUM_WETH = 0x82aF49447D8a07e3bd95BD0d56f35241523fBab1; + address internal constant ARBITRUM_USDC = 0xaf88d065e77c8cC2239327C5EDb3A432268e5831; + address internal constant BASE_WETH = 0x4200000000000000000000000000000000000006; + uint256 internal constant BASE_CHAIN_ID = 8453; + uint256 internal constant FORK_BLOCK_NUMBER = 461_716_058; + uint256 internal constant FORK_BLOCK_TIMESTAMP = 0x6a01d6d1; + uint256 internal constant SWAP_INPUT_USDC = 0x1640325; + uint256 internal constant DEFAULT_ACROSS_BRIDGE_FEE = 1; + + string internal constant OPENOCEAN_SWAP_CALLDATA = + "0x0a9704d5000000000000000000000000b100a5b2591dd099040a5ab76efe682a6d8a48a2000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001a0000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e583100000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab1000000000000000000000000b100a5b2591dd099040a5ab76efe682a6d8a48a20000000000000000000000003a23f943181408eac424116af7b7790c94cb97a50000000000000000000000000000000000000000000000000000000001640325000000000000000000000000000000000000000000000000002002d5154237f3000000000000000000000000000000000000000000000000000000000000000200000000000000000000000038c7720238a2c123814aaf1a3d0e31e0093af04600000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000220000000000000000000000000000000000000000000000000000000000000034000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000104e5b07cdb0000000000000000000000007fcdc35463e3770c2fb992716cd070b63540b94700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001640325000000000000000000000000b100a5b2591dd099040a5ab76efe682a6d8a48a200000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000002eaf88d065e77c8cc2239327c5edb3a432268e583100006482af49447d8a07e3bd95bd0d56f35241523fbab100000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000648a6a1e8500000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab1000000000000000000000000922164bbbd36acf9e854acbbf32facc949fcaeef00020000000000000000000000000000000000000000000000239364a56cb36600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001a49f86542200000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab100000000000000000000000000000001000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000004400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000064d1660f9900000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab10000000000000000000000003a23f943181408eac424116af7b7790c94cb97a500000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; + + function test_openOceanSwapManipulatorAcrossDeposit_arbitrumFork() public { + string memory rpcUrl = vm.envOr("ARBITRUM_RPC", string("")); + if (bytes(rpcUrl).length != 0) { + uint256 forkBlock = vm.envOr("ARBITRUM_FORK_BLOCK", FORK_BLOCK_NUMBER); + vm.createSelectFork(rpcUrl, forkBlock); + vm.warp(FORK_BLOCK_TIMESTAMP); + } + + Router router = _routerAtFixtureAddress(); + AcrossERC20AmountManipulator manipulator = new AcrossERC20AmountManipulator(); + if (bytes(rpcUrl).length == 0) { + emit log("Set ARBITRUM_RPC to execute this fork PoC."); + return; + } + + uint256 inputAmount = vm.envOr("POC_USDC_AMOUNT", SWAP_INPUT_USDC); + uint256 bridgeFee = vm.envOr("POC_ACROSS_BRIDGE_FEE", DEFAULT_ACROSS_BRIDGE_FEE); + + deal(ARBITRUM_USDC, address(router), inputAmount); + deal(ARBITRUM_WETH, address(router), 0); + + Router.Action[] memory actions = + _buildActions(manipulator, inputAmount, bridgeFee, vm.parseBytes(OPENOCEAN_SWAP_CALLDATA)); + uint256 spokePoolWethBefore = ERC20(ARBITRUM_WETH).balanceOf(ACROSS_ARBITRUM_SPOKE_POOL); + + uint256 gasBeforeExecute = gasleft(); + router.performActions(keccak256("open-ocean-across-modular"), actions); + uint256 executeGasUsed = gasBeforeExecute - gasleft(); + emit log_named_uint("router.performActions gas used", executeGasUsed); + + _assertPocResult(router, bridgeFee, spokePoolWethBefore); + } + + function _buildActions( + AcrossERC20AmountManipulator manipulator, + uint256 inputAmount, + uint256 bridgeFee, + bytes memory swapCalldata + ) internal view returns (Router.Action[] memory actions) { + actions = new Router.Action[](5); + actions[0] = _action( + Router.CallType.CALL, + ARBITRUM_USDC, + abi.encodeWithSelector(ERC20.approve.selector, OPENOCEAN_EXCHANGE_V2, inputAmount), + new uint256[](0), + false + ); + + actions[1] = _action(Router.CallType.CALL, OPENOCEAN_EXCHANGE_V2, swapCalldata, new uint256[](0), true); + + uint256[] memory outputAmountSplices = new uint256[](1); + outputAmountSplices[0] = _splice(1, 0, 4, 32); + + actions[2] = _action( + Router.CallType.STATICCALL, + address(manipulator), + abi.encodeCall( + AcrossERC20AmountManipulator.deriveOutputAmount, (uint256(0), bridgeFee, uint256(18), uint256(18)) + ), + outputAmountSplices, + true + ); + + actions[3] = _action( + Router.CallType.CALL, + ARBITRUM_WETH, + abi.encodeWithSelector(ERC20.approve.selector, ACROSS_ARBITRUM_SPOKE_POOL, type(uint256).max), + new uint256[](0), + false + ); + + uint256[] memory depositSplices = new uint256[](2); + depositSplices[0] = _splice(1, 0, 132, 32); + depositSplices[1] = _splice(2, 0, 164, 32); + + actions[4] = _action( + Router.CallType.CALL, ACROSS_ARBITRUM_SPOKE_POOL, _emptyAcrossDepositCalldata(), depositSplices, false + ); + } + + function _assertPocResult(Router router, uint256 bridgeFee, uint256 spokePoolWethBefore) internal view { + uint256 actualInputAmount = ERC20(ARBITRUM_WETH).balanceOf(ACROSS_ARBITRUM_SPOKE_POOL) - spokePoolWethBefore; + + assertEq(ERC20(ARBITRUM_USDC).balanceOf(address(router)), 0); + assertEq(ERC20(ARBITRUM_WETH).balanceOf(address(router)), 0); + assertGt(actualInputAmount, bridgeFee); + } + + function _routerAtFixtureAddress() internal returns (Router router) { + Router implementation = new Router(address(this)); + vm.etch(FIXTURE_ROUTER, address(implementation).code); + return Router(payable(FIXTURE_ROUTER)); + } + + function _emptyAcrossDepositCalldata() internal view returns (bytes memory) { + return abi.encodeWithSelector( + ISpokePool.deposit.selector, + _toBytes32(FIXTURE_RECIPIENT), + _toBytes32(FIXTURE_RECIPIENT), + _toBytes32(ARBITRUM_WETH), + _toBytes32(BASE_WETH), + uint256(0), + uint256(0), + BASE_CHAIN_ID, + bytes32(0), + uint32(block.timestamp), + uint32(block.timestamp + 3 hours), + uint32(0), + "" + ); + } + + function _action( + Router.CallType callType, + address target, + bytes memory data, + uint256[] memory splices, + bool storeResult + ) internal pure returns (Router.Action memory) { + return Router.Action({actionInfo: _actionInfo(callType, target, storeResult), data: data, splices: splices}); + } + + function _actionInfo(Router.CallType callType, address target, bool storeResult) internal pure returns (uint256) { + return uint256(uint8(callType)) | (storeResult ? uint256(1) << 8 : 0) | (uint256(uint160(target)) << 16); + } + + function _splice(uint64 sourceActionIndex, uint64 srcOffset, uint64 dstOffset, uint64 length) + internal + pure + returns (uint256) + { + return uint256(sourceActionIndex) | (uint256(srcOffset) << 64) | (uint256(dstOffset) << 128) + | (uint256(length) << 192); + } + + function _toBytes32(address addr) internal pure returns (bytes32) { + return bytes32(uint256(uint160(addr))); + } +} diff --git a/test/poc/OpenOceanStargateNativeOpenRouterPoC.t.sol b/test/poc/OpenOceanStargateNativeOpenRouterPoC.t.sol new file mode 100644 index 0000000..f9ae56a --- /dev/null +++ b/test/poc/OpenOceanStargateNativeOpenRouterPoC.t.sol @@ -0,0 +1,301 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.34; + +import {Test} from "forge-std/Test.sol"; +import {ERC20} from "solady/src/tokens/ERC20.sol"; + +import {OpenRouter as Router} from "../../src/OpenRouter.sol"; +import {MathManipulator} from "../../src/manipulators/MathManipulator.sol"; + +interface IOpenOceanExchangeV2 { + struct SwapDescription { + address srcToken; + address dstToken; + address srcReceiver; + address dstReceiver; + uint256 amount; + uint256 minReturnAmount; + uint256 flags; + address referrer; + bytes permit; + } + + struct CallDescription { + uint256 target; + uint256 gasLimit; + uint256 value; + bytes data; + } +} + +interface IStargateNative { + struct SendParam { + uint32 dstEid; + bytes32 to; + uint256 amountLD; + uint256 minAmountLD; + bytes extraOptions; + bytes composeMsg; + bytes oftCmd; + } + + struct MessagingFee { + uint256 nativeFee; + uint256 lzTokenFee; + } + + function send(SendParam calldata sendParam, MessagingFee calldata fee, address refundAddress) external payable; +} + +// ref tx 0xef65dc3323cd757c5e3a1a872b99beff6e71f0a80b1a2a6d280d2f2458f3cbaf +contract OpenOceanStargateNativeOpenRouterPoCTest is Test { + bytes4 internal constant OPENOCEAN_SWAP_SELECTOR = 0x0a9704d5; + address internal constant OPENOCEAN_EXCHANGE_V2 = 0x6352a56caadC4F1E25CD6c75970Fa768A3304e64; + address internal constant OPENOCEAN_CALLER = 0xB100a5B2591Dd099040a5ab76EFe682A6D8a48a2; + address internal constant OPENOCEAN_REFERRER = 0x38c7720238a2C123814aaF1A3D0e31E0093aF046; + address internal constant STARGATE_NATIVE_WRAPPER = 0xA45B5130f36CDcA45667738e2a258AB09f4A5f7F; + address internal constant FIXTURE_ROUTER = 0x3a23F943181408EAC424116Af7b7790c94Cb97a5; + address internal constant FIXTURE_RECIPIENT = 0xB0BBff6311B7F245761A7846d3Ce7B1b100C1836; + address internal constant FEE_RECIPIENT = 0x0079a23EDEA601190EdF1cda05c8Af3fEA2f2d9F; + address internal constant ARBITRUM_WETH = 0x82aF49447D8a07e3bd95BD0d56f35241523fBab1; + address internal constant ARBITRUM_USDC = 0xaf88d065e77c8cC2239327C5EDb3A432268e5831; + address internal constant NATIVE_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + uint256 internal constant FORK_BLOCK_NUMBER = 461_745_499; + uint256 internal constant FORK_BLOCK_TIMESTAMP = 0x6a01f38b; + uint256 internal constant SWAP_INPUT_USDC = 0x1312d00; + uint256 internal constant OPENOCEAN_MIN_RETURN = 0x1b91a33e163bdf; + uint256 internal constant OPENOCEAN_FLAGS = 2; + uint256 internal constant STARGATE_NATIVE_FEE = 0x1603e90a5fe0; + uint256 internal constant ROUTE_FEE_BPS = 100; + + uint32 internal constant BASE_ENDPOINT_ID = 30_184; + uint256 internal constant STARGATE_AMOUNT_OFFSET = 196; + uint256 internal constant CALL_WITH_NATIVE_PAYLOAD_OFFSET = 32; + + function test_openOceanSwapStargateNativeBridge_arbitrumFork() public { + string memory rpcUrl = vm.envOr("ARBITRUM_RPC", string("")); + if (bytes(rpcUrl).length != 0) { + uint256 forkBlock = vm.envOr("ARBITRUM_FORK_BLOCK", FORK_BLOCK_NUMBER); + vm.createSelectFork(rpcUrl, forkBlock); + vm.warp(FORK_BLOCK_TIMESTAMP); + } + + Router router = _routerAtFixtureAddress(); + MathManipulator manipulator = new MathManipulator(); + if (bytes(rpcUrl).length == 0) { + emit log("Set ARBITRUM_RPC to execute this fork PoC."); + return; + } + + uint256 inputAmount = vm.envOr("POC_USDC_AMOUNT", SWAP_INPUT_USDC); + uint256 nativeFee = vm.envOr("POC_STARGATE_NATIVE_FEE", STARGATE_NATIVE_FEE); + + deal(ARBITRUM_USDC, address(router), inputAmount); + uint256 initialNativeBalance = address(router).balance; + uint256 initialFeeRecipientBalance = FEE_RECIPIENT.balance; + uint256 initialWethBalance = ERC20(ARBITRUM_WETH).balanceOf(address(router)); + + Router.Action[] memory actions = _buildActions( + manipulator, inputAmount, nativeFee, _openOceanSwapCalldata(inputAmount), _stargateCalldata(nativeFee) + ); + + uint256 gasBeforeExecute = gasleft(); + router.performActions(keccak256("open-ocean-stargate-native-modular"), actions); + uint256 executeGasUsed = gasBeforeExecute - gasleft(); + emit log_named_uint("router.performActions gas used", executeGasUsed); + + _assertPocResult(router, nativeFee, initialNativeBalance, initialFeeRecipientBalance, initialWethBalance); + } + + function _openOceanSwapCalldata(uint256 inputAmount) internal pure returns (bytes memory) { + return abi.encodeWithSelector( + OPENOCEAN_SWAP_SELECTOR, + OPENOCEAN_CALLER, + IOpenOceanExchangeV2.SwapDescription({ + srcToken: ARBITRUM_USDC, + dstToken: NATIVE_TOKEN, + srcReceiver: OPENOCEAN_CALLER, + dstReceiver: FIXTURE_ROUTER, + amount: inputAmount, + minReturnAmount: OPENOCEAN_MIN_RETURN, + flags: OPENOCEAN_FLAGS, + referrer: OPENOCEAN_REFERRER, + permit: "" + }), + _openOceanCalls() + ); + } + + function _stargateCalldata(uint256 nativeFee) internal pure returns (bytes memory) { + return abi.encodeCall( + IStargateNative.send, + ( + IStargateNative.SendParam({ + dstEid: BASE_ENDPOINT_ID, + to: _toBytes32(FIXTURE_RECIPIENT), + amountLD: 0, + minAmountLD: 0, + extraOptions: "", + composeMsg: "", + oftCmd: "" + }), + IStargateNative.MessagingFee({nativeFee: nativeFee, lzTokenFee: 0}), + FIXTURE_RECIPIENT + ) + ); + } + + function _openOceanCalls() internal pure returns (IOpenOceanExchangeV2.CallDescription[] memory calls) { + calls = new IOpenOceanExchangeV2.CallDescription[](6); + calls[0] = IOpenOceanExchangeV2.CallDescription({ + target: 0, + gasLimit: 0, + value: 0, + data: hex"e5b07cdb0000000000000000000000007fcdc35463e3770c2fb992716cd070b63540b9470000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000112a880000000000000000000000000b100a5b2591dd099040a5ab76efe682a6d8a48a200000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000002eaf88d065e77c8cc2239327c5edb3a432268e583100006482af49447d8a07e3bd95bd0d56f35241523fbab1000003000000000000000000000000000000000000" + }); + calls[1] = IOpenOceanExchangeV2.CallDescription({ + target: 0, + gasLimit: 0, + value: 0, + data: hex"9f865422000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e583100000000000000000000000000000001000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000004400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000064d1660f99000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e5831000000000000000000000000b7236b927e03542ac3be0a054f2bea8868af9508000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000" + }); + calls[2] = IOpenOceanExchangeV2.CallDescription({ + target: uint256(uint160(0xb7236B927e03542AC3bE0A054F2bEa8868AF9508)), + gasLimit: 0, + value: 0, + data: hex"53c059a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b100a5b2591dd099040a5ab76efe682a6d8a48a2" + }); + calls[3] = IOpenOceanExchangeV2.CallDescription({ + target: 0, + gasLimit: 0, + value: 0, + data: hex"9f86542200000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab100000000000000000000000000000001000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000400000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000242e1a7d4d000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000" + }); + calls[4] = IOpenOceanExchangeV2.CallDescription({ + target: 0, + gasLimit: 0, + value: 0, + data: hex"8a6a1e85000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee000000000000000000000000922164bbbd36acf9e854acbbf32facc949fcaeef000000000000000000000000000000000000000000000000001ea1d1d3352615" + }); + calls[5] = IOpenOceanExchangeV2.CallDescription({ + target: 0, + gasLimit: 0, + value: 0, + data: hex"9f865422000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee00000000000000000000000000000001000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000004400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000064d1660f99000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000003a23f943181408eac424116af7b7790c94cb97a5000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000" + }); + } + + function _buildActions( + MathManipulator manipulator, + uint256 inputAmount, + uint256 nativeFee, + bytes memory swapCalldata, + bytes memory stargateCalldata + ) internal pure returns (Router.Action[] memory actions) { + actions = new Router.Action[](7); + actions[0] = _action( + Router.CallType.CALL, + ARBITRUM_USDC, + abi.encodeWithSelector(ERC20.approve.selector, OPENOCEAN_EXCHANGE_V2, inputAmount), + new uint256[](0), + false + ); + + actions[1] = _action(Router.CallType.CALL, OPENOCEAN_EXCHANGE_V2, swapCalldata, new uint256[](0), true); + + uint256[] memory feeSplices = new uint256[](1); + feeSplices[0] = _splice(1, 0, 4, 32); + actions[2] = _action( + Router.CallType.STATICCALL, + address(manipulator), + abi.encodeCall(MathManipulator.percent, (uint256(0), ROUTE_FEE_BPS)), + feeSplices, + true + ); + + uint256[] memory feeTransferSplices = new uint256[](1); + feeTransferSplices[0] = _splice(2, 0, 0, 32); + actions[3] = _action( + Router.CallType.CALL_WITH_NATIVE, FEE_RECIPIENT, abi.encodePacked(uint256(0)), feeTransferSplices, false + ); + + uint256[] memory postFeeSplices = new uint256[](2); + postFeeSplices[0] = _splice(1, 0, 4, 32); + postFeeSplices[1] = _splice(2, 0, 36, 32); + actions[4] = _action( + Router.CallType.STATICCALL, + address(manipulator), + abi.encodeCall(MathManipulator.subtract, (uint256(0), uint256(0))), + postFeeSplices, + true + ); + + uint256[] memory bridgeAmountSplices = new uint256[](1); + bridgeAmountSplices[0] = _splice(4, 0, 4, 32); + actions[5] = _action( + Router.CallType.STATICCALL, + address(manipulator), + abi.encodeCall(MathManipulator.subtract, (uint256(0), nativeFee)), + bridgeAmountSplices, + true + ); + + uint256[] memory stargateSplices = new uint256[](2); + stargateSplices[0] = _splice(4, 0, 0, 32); + stargateSplices[1] = _splice(5, 0, uint64(CALL_WITH_NATIVE_PAYLOAD_OFFSET + STARGATE_AMOUNT_OFFSET), 32); + actions[6] = _action( + Router.CallType.CALL_WITH_NATIVE, + STARGATE_NATIVE_WRAPPER, + abi.encodePacked(uint256(0), stargateCalldata), + stargateSplices, + false + ); + } + + function _assertPocResult( + Router router, + uint256 nativeFee, + uint256 initialNativeBalance, + uint256 initialFeeRecipientBalance, + uint256 initialWethBalance + ) internal view { + assertGt(FEE_RECIPIENT.balance - initialFeeRecipientBalance, 0); + assertEq(ERC20(ARBITRUM_USDC).balanceOf(address(router)), 0); + assertEq(ERC20(ARBITRUM_WETH).balanceOf(address(router)), initialWethBalance); + assertLt(address(router).balance - initialNativeBalance, nativeFee); + } + + function _routerAtFixtureAddress() internal returns (Router router) { + Router implementation = new Router(address(this)); + vm.etch(FIXTURE_ROUTER, address(implementation).code); + return Router(payable(FIXTURE_ROUTER)); + } + + function _action( + Router.CallType callType, + address target, + bytes memory data, + uint256[] memory splices, + bool storeResult + ) internal pure returns (Router.Action memory) { + return Router.Action({actionInfo: _actionInfo(callType, target, storeResult), data: data, splices: splices}); + } + + function _actionInfo(Router.CallType callType, address target, bool storeResult) internal pure returns (uint256) { + return uint256(uint8(callType)) | (storeResult ? uint256(1) << 8 : 0) | (uint256(uint160(target)) << 16); + } + + function _splice(uint64 sourceActionIndex, uint64 srcOffset, uint64 dstOffset, uint64 length) + internal + pure + returns (uint256) + { + return uint256(sourceActionIndex) | (uint256(srcOffset) << 64) | (uint256(dstOffset) << 128) + | (uint256(length) << 192); + } + + function _toBytes32(address addr) internal pure returns (bytes32) { + return bytes32(uint256(uint160(addr))); + } +} diff --git a/test/poc/OpenRouterAllowanceHolderFork.t.sol b/test/poc/OpenRouterAllowanceHolderFork.t.sol new file mode 100644 index 0000000..abde3fe --- /dev/null +++ b/test/poc/OpenRouterAllowanceHolderFork.t.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.34; + +import {Test} from "forge-std/Test.sol"; +import {ERC20} from "solady/src/tokens/ERC20.sol"; + +import {OpenRouter as Router} from "../../src/OpenRouter.sol"; +import {ALLOWANCE_HOLDER, IAllowanceHolder} from "../../src/common/interfaces/IAllowanceHolder.sol"; + +/// @dev No-op bridge target so `router.bridge` can complete after the pull. +contract NoopBridgeTarget { + function ping() external {} +} + +/// @notice Polygon fork: user funds + AH approval, entry via AllowanceHolder.exec, OpenRouter pulls via `_pullFromUser`. +contract OpenRouterAllowanceHolderForkTest is Test { + address internal constant POLYGON_USDC = 0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359; + uint256 internal constant POLYGON_FORK_BLOCK = 86_816_149; + uint256 internal constant INPUT_AMOUNT = 100e6; + + address internal user; + + function setUp() public { + user = makeAddr("ahForkUser"); + } + + function test_fork_openRouter_bridge_pullsFromUserViaAllowanceHolder() public { + string memory rpcUrl = vm.envOr("POLYGON_RPC", string("")); + if (bytes(rpcUrl).length == 0) { + emit log("Set POLYGON_RPC to run this fork test."); + return; + } + + uint256 forkBlock = vm.envOr("POLYGON_FORK_BLOCK", POLYGON_FORK_BLOCK); + vm.createSelectFork(rpcUrl, forkBlock); + + Router router = new Router(address(this)); + NoopBridgeTarget noopBridge = new NoopBridgeTarget(); + + deal(POLYGON_USDC, user, INPUT_AMOUNT); + assertEq(ERC20(POLYGON_USDC).balanceOf(address(router)), 0, "router must not be pre-funded"); + + vm.prank(user); + ERC20(POLYGON_USDC).approve(address(ALLOWANCE_HOLDER), INPUT_AMOUNT); + + bytes memory routerCalldata = abi.encodeCall( + Router.bridge, + ( + keccak256("open-router-ah-fork"), + Router.InputData({user: user, inputToken: POLYGON_USDC, inputAmount: INPUT_AMOUNT}), + Router.FeeData({receiver: address(0), amount: 0}), + Router.BridgeData({target: address(noopBridge), approvalSpender: address(0), value: 0}), + abi.encodeCall(NoopBridgeTarget.ping, ()) + ) + ); + + // Runtime-only gas (excludes `new OpenRouter` / `new NoopBridgeTarget` above). + // Forge's per-test `gas:` figure still includes deployment; use this log for comparisons. + uint256 gasBeforeExec = gasleft(); + vm.prank(user); + IAllowanceHolder(address(ALLOWANCE_HOLDER)).exec( + address(router), POLYGON_USDC, INPUT_AMOUNT, payable(address(router)), routerCalldata + ); + uint256 runtimeGas = gasBeforeExec - gasleft(); + emit log_named_uint("runtime gas AH.exec -> router.bridge", runtimeGas); + + assertEq(ERC20(POLYGON_USDC).balanceOf(user), 0, "user balance"); + assertEq(ERC20(POLYGON_USDC).balanceOf(address(router)), INPUT_AMOUNT, "router pulled via AH"); + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..6257b56 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "es2020", + "module": "commonjs", + "esModuleInterop": true, + "strict": true, + "resolveJsonModule": true, + "outDir": "dist" + }, + "include": ["scripts/**/*", "hardhat.config.ts"], + "files": ["hardhat.config.ts"] +}