diff --git a/.env.example b/.env.example index 528f41e..74fa06b 100644 --- a/.env.example +++ b/.env.example @@ -6,7 +6,6 @@ PRIVATE_KEY= # Constructor arguments OWNER_ADDRESS= -OPEN_ROUTER_SIGNER_ADDRESS= # only needed for BungeeOpenRouterV2 (not Unchecked) # External API keys RELAY_API_KEY= # optional, relay.link x-api-key header 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/AGENTS.md b/AGENTS.md index f714adb..7e55c30 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,4 +3,4 @@ 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/BungeeOpenRouterV2Unchecked.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`. +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 ced6eec..17b94a4 100644 --- a/OPENROUTER.md +++ b/OPENROUTER.md @@ -1,272 +1,238 @@ -# 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) +--- -**File:** [`src/modular/BungeeOpenRouterModular.sol`](src/modular/BungeeOpenRouterModular.sol). AllowanceHolder variant: [`src/modular/BungeeOpenRouterModularAH.sol`](src/modular/BungeeOpenRouterModularAH.sol). +## Packed `flags` (structured routes) -This version removes all domain-specific knowledge from the contract. The only signed payload is a list of `Action`s: +One `uint256` packs switches for `swap` and `swapAndBridge` (not used by `bridge` or `performActions`): -```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 -} +| 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. | -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 -} -``` +Common combinations: -The loop is: +| `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` | -``` -prevReturn = empty bytes -for each action: - apply all splices (copy ranges from prevReturn into action.data) - dispatch the call - prevReturn = returndata from this call -``` +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`). -**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. +--- -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. +## Generic routes — `performActions` -Under the hood, the copy uses `mcopy` (Cancun, EIP-5656): +For flows that need extra hops, manipulator contracts, or multiple splices into one calldata blob, use the modular path: ```solidity -// BytesSpliceLib.spliceBytes -assembly ("memory-safe") { - mcopy( - add(add(dst, 0x20), dstOffset), - add(add(src, 0x20), srcOffset), - length - ) +struct Action { + uint256 actionInfo; // packed call metadata + bytes data; + uint256[] splices; // packed splice descriptors } + +enum CallType { CALL, STATICCALL, CALL_WITH_NATIVE } ``` -Both source and destination offsets are bounds-checked before the copy; zero-length splices are rejected. +### `actionInfo` layout -**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. +```text +bits 0–7 : CallType (CALL = 0, STATICCALL = 1, CALL_WITH_NATIVE = 2) +bit 8 : store returndata for later splices +bits 16+ : target address (uint160, shifted left 16) +``` -**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. +### `splices[]` entry layout -**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. +Each `splices[j]` is one `uint256`: -**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. +```text +sourceActionIndex | (srcOffset << 64) | (dstOffset << 128) | (length << 192) +``` ---- +Before action `i` runs, each splice copies `length` bytes from `results[sourceActionIndex]` at `srcOffset` into this action’s `data` at `dstOffset` (via `mcopy`). `sourceActionIndex` must be **strictly less than** `i` or the call reverts with `FutureSplice`. -## v3 — BungeeOpenRouterMinimal (generic actions, no splicing) +`CALL_WITH_NATIVE`: first 32 bytes of `data` are `msg.value`; remaining bytes are calldata. -**File:** [`src/minimal/BungeeOpenRouterMinimal.sol`](src/minimal/BungeeOpenRouterMinimal.sol). AllowanceHolder variant: [`src/minimal/BungeeOpenRouterMinimalAH.sol`](src/minimal/BungeeOpenRouterMinimalAH.sol). +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 version is the stripped-down sibling of v2. The `Action` struct has no `splices` field: +--- -```solidity -struct Action { - CallType callType; - address target; - uint256 value; - bytes data; // used exactly as signed; never mutated -} -``` +## Internal helpers (shared behavior) -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. +- **`_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. -``` -for each action: - dispatch the call (no splice step) - discard returndata -``` +Approvals use Solady `safeApproveWithRetry` to `type(uint256).max` only when current allowance is below the needed amount. -**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. +--- -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. +## Choosing structured vs generic -**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. +| 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`). | -**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`. +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. --- -## Choosing between them +## Security model (summary) -The three versions exist on a spectrum from "the contract knows everything" to "the contract knows nothing except who signed". +| 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 | -**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. - -**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. +`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 - -All live under `src/common/`. +## Shared libraries (`src/common/`) -**`OpenRouterAuthBase.sol`** — abstract base all three inherit. Owns the signer address, the nonce mapping, and `_verifyAndConsume`. +| 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 | -**`lib/AuthenticationLib.sol`** — personal_sign recovery (`\x19Ethereum Signed Message:\n32` + ecrecover). Matches `marketplace/src/lib/AuthenticationLib.sol` exactly. +`OpenRouterAuthBase` and signed-router variants are **not** used by this contract. -**`lib/CurrencyLib.sol`** — wraps Solady `SafeTransferLib` with a native token shortcut (address `0xEee...EEe`), identical in spirit to the marketplace `CurrencyLib`. - -**`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). - -**`allowance/AllowanceHolderContext.sol`**, **`interfaces/IAllowanceHolder.sol`** — imported only by the `*AH` contracts in each variant folder. +--- +## Backend and tests +ABI encoders (update if the Solidity ABI changes): +- `bungee-backend/src/modules/dex/utils.ts` — `swap`, AllowanceHolder `exec` +- `bungee-backend/src/modules/router/utils/directQuotesOpenRouter.ts` — `bridge`, `swapAndBridge` -0. AllowanceHolder -1. OpenRouter -> Fee Transfer -> -2. OpenRouter (modify input) -> Swap execution -> OpenRouter (modify input) -> -3. AcrossManipulator -> OpenRouter (modify input) -> -4. SpokePool +Tests: -0. AllowanceHolder -1. OpenRouter -> Fee Transfer -> -2. OpenRouter (modify input) -> Swap execution -> OpenRouter (modify input) -> -3. AcrossRouter(amount, AcrossBridgeData) -> (modify SpokePool input with output ) -> SpokePool +- `test/combined/OpenRouterV2Unchecked*.t.sol` — unit tests against `src/OpenRouter.sol` +- `test/poc/*OpenRouterPoC.t.sol` — fork PoCs using `performActions` + manipulators -0. AllowanceHolder -1. AcrossRouter - should have all the fee, swap, bridge code in this \ No newline at end of file +Deploy: `scripts/deploy/deployOpenRouter.ts` (`constructor(address _owner)` grants `RESCUE_ROLE`). diff --git a/OPENROUTER_ASSUMPTIONS.md b/OPENROUTER_ASSUMPTIONS.md index 7ba02a6..98bff2a 100644 --- a/OPENROUTER_ASSUMPTIONS.md +++ b/OPENROUTER_ASSUMPTIONS.md @@ -2,20 +2,20 @@ Last reviewed: 2026-05-19. -Scope: `src/combined/BungeeOpenRouterV2Unchecked.sol`. +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 -`BungeeOpenRouterV2Unchecked` intentionally removes backend signature verification, nonces, and deadlines. Public entrypoints can be called by anyone. +`OpenRouterV2Unchecked` intentionally removes backend signature verification, nonces, and deadlines. Public entrypoints can be called by anyone. Current checked-in public surface: - `swap(...)` - `swapAndBridge(...)` - `bridge(...)` -- `performModularExecution(...)` +- `performActions()(...)` - `rescueFunds(...)` `OPENROUTER_CONTEXT.md` and `scripts/e2e/utils/routerAbi.ts` may mention `performExecution(...)`; verify against the Solidity file before relying on that ABI. @@ -34,7 +34,7 @@ Use this distinction when reviewing any route or integration: The router may temporarily hold funds during one transaction, but it should not end routes with meaningful token or native balances. -Failure mode: `performModularExecution` 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. +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: @@ -47,7 +47,7 @@ Operational requirements: 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 `performModularExecution` to make the router call `transferFrom`, `approve`, or equivalent privileged token functions against that user allowance. +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: @@ -194,7 +194,7 @@ Failure modes: ## Modular Execution Assumptions -`performModularExecution` is the broadest surface. It makes the router a public generic call executor. +`performActions()` is the broadest surface. It makes the router a public generic call executor. Assumptions: @@ -245,4 +245,4 @@ Before enabling a route or integration, confirm: - 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 `BungeeOpenRouterV2Unchecked` as-is. Add access control, use a signed variant, or remove the downstream privilege/funds/allowance that makes the public call surface dangerous. +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 index 94a4248..2736eb4 100644 --- a/OPENROUTER_CONTEXT.md +++ b/OPENROUTER_CONTEXT.md @@ -4,13 +4,13 @@ Last researched: 2026-05-18. Main ship target: -- `src/combined/BungeeOpenRouterV2Unchecked.sol` +- `src/combined/OpenRouterV2Unchecked.sol` -Use `src/combined/BungeeOpenRouterV2.sol` as the signed sibling/reference, but the backend branch researched here targets the unchecked ABI. +Use `src/combined/OpenRouterV2.sol` as the signed sibling/reference, but the backend branch researched here targets the unchecked ABI. ## V2Unchecked Surface -`BungeeOpenRouterV2Unchecked` 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`. +`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: @@ -29,12 +29,12 @@ External entrypoints: - `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`. -- `performModularExecution(bytes32 requestHash, Action[] actions)` +- `performActions()(bytes32 requestHash, Action[] actions)` - Generic action loop with packed action metadata and packed splices. ## Flags -Flag constants in `BungeeOpenRouterV2Unchecked.sol`: +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`. @@ -105,4 +105,4 @@ If the Solidity ABI changes, update those hard-coded ABI strings first. Direct D - `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 `BungeeOpenRouterV2Unchecked` needs an operational access-control decision; the contract itself has no signature or nonce checks. +- Production use of `OpenRouterV2Unchecked` needs an operational access-control decision; the contract itself has no signature or nonce checks. diff --git a/package.json b/package.json index e3000a6..b1a1749 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,8 @@ "private": true, "scripts": { "compile": "hardhat compile", - "deploy": "hardhat run scripts/deploy/deployBungeeOpenRouter.ts --network", - "deploy:v2": "hardhat run scripts/deploy/deployBungeeOpenRouterV2.ts --network", + "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" }, diff --git a/scripts/deploy/deployBungeeOpenRouter.ts b/scripts/deploy/deployOpenRouter.ts similarity index 74% rename from scripts/deploy/deployBungeeOpenRouter.ts rename to scripts/deploy/deployOpenRouter.ts index d5db918..6afcb60 100644 --- a/scripts/deploy/deployBungeeOpenRouter.ts +++ b/scripts/deploy/deployOpenRouter.ts @@ -1,8 +1,8 @@ /** - * Deployment script for BungeeOpenRouter. + * Deployment script for OpenRouter. * * Usage: - * npx hardhat run scripts/deploy/deployBungeeOpenRouter.ts --network + * npx hardhat run scripts/deploy/deployOpenRouter.ts --network * * Required env vars: * DEPLOYER_PRIVATE_KEY — deployer wallet private key @@ -23,15 +23,15 @@ async function main() { console.log('Network: ', networkName); console.log(''); - console.log('Deploying BungeeOpenRouter...'); - const factory = await ethers.getContractFactory('BungeeOpenRouter'); + console.log('Deploying OpenRouter...'); + const factory = await ethers.getContractFactory('OpenRouter'); const router = await factory.deploy(owner); await router.waitForDeployment(); const routerAddress = await router.getAddress(); - console.log('BungeeOpenRouter deployed to:', routerAddress); + console.log('OpenRouter deployed to:', routerAddress); console.log('\n=== Deployment Summary ==='); - console.log(`BungeeOpenRouter: ${routerAddress}`); + console.log(`OpenRouter: ${routerAddress}`); const chainId = (await ethers.provider.getNetwork()).chainId; if (chainId !== 31337n) { diff --git a/scripts/e2e/approveViaModular.ts b/scripts/e2e/approveViaModular.ts index 8a62bdb..891a7e2 100644 --- a/scripts/e2e/approveViaModular.ts +++ b/scripts/e2e/approveViaModular.ts @@ -2,7 +2,7 @@ * Script — Call ERC-20 approve(spender, amount) through the router using * `performActions(Action[])`. * - * DISABLED by default: `BungeeOpenRouter` now sets max allowance inside + * 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. diff --git a/scripts/e2e/arbitrum/performExecution.postFee.ts b/scripts/e2e/arbitrum/performExecution.postFee.ts index 4c009f1..3e97d1e 100644 --- a/scripts/e2e/arbitrum/performExecution.postFee.ts +++ b/scripts/e2e/arbitrum/performExecution.postFee.ts @@ -36,7 +36,8 @@ import { swapAndBridgeArgs, } from '../utils/contractTypes'; import { logTxnSummary } from '../utils/txnLogSummary'; -import { ensureRouterErc20Balance, ensureRouterNativeBalance, ensureRouterApproval } from '../utils/reproducibility'; +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); @@ -108,7 +109,14 @@ async function main() { await ensureRouterErc20Balance(signer, TOKENS.AAVE_ETH, ROUTER_ETH); await ensureRouterNativeBalance(signer, ROUTER_ETH); - await ensureRouterApproval(signer, ROUTER_ETH, TOKENS.AAVE_ETH, ooRouter); + + const swapApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_ETH, + TOKENS.AAVE_ETH, + ooRouter, + inputAmount, + ); const callData = routerIface.encodeFunctionData( 'swapAndBridge', @@ -119,7 +127,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, - approvalSpender: ooRouter, + approvalSpender: swapApprovalSpender, outputToken: NATIVE_TOKEN_ADDRESS, value: 0n, minOutput: minAmountOut, diff --git a/scripts/e2e/arbitrum/performModularExecution.postFee.ts b/scripts/e2e/arbitrum/performModularExecution.postFee.ts index 78a0dcf..069e29d 100644 --- a/scripts/e2e/arbitrum/performModularExecution.postFee.ts +++ b/scripts/e2e/arbitrum/performModularExecution.postFee.ts @@ -34,12 +34,13 @@ import { NATIVE_TOKEN_ADDRESS, } from '../config'; import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; -import { encodeApprove, getWalletErc20Balance } from '../utils/erc20'; +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, ensureRouterApproval } from '../utils/reproducibility'; +import { ensureRouterErc20Balance, ensureRouterNativeBalance } from '../utils/reproducibility'; +import { modularApproveIfNeeded } from '../utils/routerAllowance'; const ROUTER_ETH = routerAddressForChain(CHAIN_IDS.ETHEREUM); @@ -133,14 +134,13 @@ async function main() { await ensureRouterErc20Balance(signer, TOKENS.AAVE_ETH, ROUTER_ETH); await ensureRouterNativeBalance(signer, ROUTER_ETH); - await ensureRouterApproval(signer, ROUTER_ETH, TOKENS.AAVE_ETH, ooRouter); 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])); - exec.call(TOKENS.AAVE_ETH, encodeApprove(ooRouter, 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); diff --git a/scripts/e2e/cctp/bridge.preFee.ts b/scripts/e2e/cctp/bridge.preFee.ts index ce85682..bbac9a1 100644 --- a/scripts/e2e/cctp/bridge.preFee.ts +++ b/scripts/e2e/cctp/bridge.preFee.ts @@ -27,7 +27,8 @@ 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, ensureRouterApproval } from '../utils/reproducibility'; +import { ensureRouterErc20Balance } from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); @@ -93,15 +94,22 @@ async function main(): Promise { 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: polyCctp.tokenMessenger, value: 0n }; + 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 ensureRouterApproval(signer, ROUTER_POLYGON, inputToken, polyCctp.tokenMessenger); await ensureAllowanceForAllowanceHolder(signer, inputToken, inputAmount); console.log('Sending AllowanceHolder.exec → router.bridge...'); diff --git a/scripts/e2e/cctp/performExecution.postFee.ts b/scripts/e2e/cctp/performExecution.postFee.ts index 645346a..36d0a50 100644 --- a/scripts/e2e/cctp/performExecution.postFee.ts +++ b/scripts/e2e/cctp/performExecution.postFee.ts @@ -32,7 +32,8 @@ import { swapAndBridgeArgs, } from '../utils/contractTypes'; import { logTxnSummary } from '../utils/txnLogSummary'; -import { ensureRouterErc20Balance, ensureRouterApproval } from '../utils/reproducibility'; +import { ensureRouterErc20Balance } from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; const FLAGS = POST_FEE_FLAG | bridgeAmountPositionFlag(4); const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); @@ -121,8 +122,21 @@ async function main() { await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); await ensureRouterErc20Balance(signer, TOKENS.USDC_POLYGON_CIRCLE, ROUTER_POLYGON); - await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, ooRouter); - await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.USDC_POLYGON_CIRCLE, polyCctp.tokenMessenger); + + 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', @@ -133,14 +147,14 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, - approvalSpender: ooRouter, + approvalSpender: swapApprovalSpender, outputToken: TOKENS.USDC_POLYGON_CIRCLE, value: 0n, minOutput: minAmountOut, returnDataWordOffset: 0n, }, swapData, - { target: polyCctp.tokenMessenger, approvalSpender: polyCctp.tokenMessenger, value: 0n }, + { target: polyCctp.tokenMessenger, approvalSpender: bridgeApprovalSpender, value: 0n }, depositForBurnData, ), ); diff --git a/scripts/e2e/cctp/performExecution.preFee.ts b/scripts/e2e/cctp/performExecution.preFee.ts index 5065564..cc53631 100644 --- a/scripts/e2e/cctp/performExecution.preFee.ts +++ b/scripts/e2e/cctp/performExecution.preFee.ts @@ -30,7 +30,8 @@ import { type InputData, } from '../utils/contractTypes'; import { logTxnSummary } from '../utils/txnLogSummary'; -import { ensureRouterErc20Balance, ensureRouterApproval } from '../utils/reproducibility'; +import { ensureRouterErc20Balance } from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); @@ -88,14 +89,21 @@ async function main() { 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: polyCctp.tokenMessenger, + approvalSpender: bridgeApprovalSpender, value: 0n, }; await ensureRouterErc20Balance(signer, TOKENS.USDC_POLYGON_CIRCLE, ROUTER_POLYGON); - await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.USDC_POLYGON_CIRCLE, polyCctp.tokenMessenger); const routerIface = new ethers.Interface(ROUTER_ABI); const callData = routerIface.encodeFunctionData('bridge', bridgeArgs(ZERO_BYTES32, input, fee, bridgeData, depositForBurnData)); diff --git a/scripts/e2e/cctp/performModularExecution.postFee.ts b/scripts/e2e/cctp/performModularExecution.postFee.ts index 7d37635..c040a37 100644 --- a/scripts/e2e/cctp/performModularExecution.postFee.ts +++ b/scripts/e2e/cctp/performModularExecution.postFee.ts @@ -33,12 +33,13 @@ import { ALLOWANCE_HOLDER, } from '../config'; import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; -import { encodeApprove, encodeTransfer, encodeBalanceOf, getWalletErc20Balance } from '../utils/erc20'; +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, ensureRouterApproval } from '../utils/reproducibility'; +import { ensureRouterErc20Balance } from '../utils/reproducibility'; +import { modularApproveIfNeeded } from '../utils/routerAllowance'; const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); @@ -126,18 +127,24 @@ async function main() { await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); await ensureRouterErc20Balance(signer, TOKENS.USDC_POLYGON_CIRCLE, ROUTER_POLYGON); - await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, ooRouter); - await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.USDC_POLYGON_CIRCLE, polyCctp.tokenMessenger); 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])); - exec.call(TOKENS.AAVE_POLYGON, encodeApprove(ooRouter, 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)); - exec.call(TOKENS.USDC_POLYGON_CIRCLE, encodeApprove(polyCctp.tokenMessenger, ethers.MaxUint256)); + 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()); diff --git a/scripts/e2e/cctp/performModularExecution.preFee.ts b/scripts/e2e/cctp/performModularExecution.preFee.ts index 89582a2..163391f 100644 --- a/scripts/e2e/cctp/performModularExecution.preFee.ts +++ b/scripts/e2e/cctp/performModularExecution.preFee.ts @@ -28,12 +28,13 @@ import { ALLOWANCE_HOLDER, } from '../config'; import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; -import { encodeApprove, encodeTransfer, encodeBalanceOf, getWalletErc20Balance } from '../utils/erc20'; +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, ensureRouterApproval } from '../utils/reproducibility'; +import { ensureRouterErc20Balance } from '../utils/reproducibility'; +import { modularApproveIfNeeded } from '../utils/routerAllowance'; const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); @@ -71,6 +72,7 @@ async function main() { 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}`); @@ -85,7 +87,6 @@ async function main() { const depositForBurnData = buildDepositForBurnCalldata(signerAddress, polyCctp.usdcAddress, baseCctp.cctpDomain); await ensureRouterErc20Balance(signer, TOKENS.USDC_POLYGON_CIRCLE, ROUTER_POLYGON); - await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.USDC_POLYGON_CIRCLE, polyCctp.tokenMessenger); const ahIface = new ethers.Interface([ 'function transferFrom(address token, address owner, address recipient, uint256 amount)', @@ -93,7 +94,15 @@ async function main() { 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)); - exec.call(TOKENS.USDC_POLYGON_CIRCLE, encodeApprove(polyCctp.tokenMessenger, ethers.MaxUint256)); + 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()); diff --git a/scripts/e2e/cctp/swapAndBridge.postFee.balanceOf.ts b/scripts/e2e/cctp/swapAndBridge.postFee.balanceOf.ts index 1a7fe75..243e0a7 100644 --- a/scripts/e2e/cctp/swapAndBridge.postFee.balanceOf.ts +++ b/scripts/e2e/cctp/swapAndBridge.postFee.balanceOf.ts @@ -35,8 +35,8 @@ import { ZERO_BYTES32, bridgeAmountPositionFlag, swapAndBridgeArgs } from "../ut import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, - ensureRouterApproval, -} from "../utils/reproducibility"; +} 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); @@ -149,17 +149,20 @@ async function main() { TOKENS.USDC_POLYGON_CIRCLE, ROUTER_POLYGON ); - await ensureRouterApproval( - signer, + + const swapApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, - ooRouter + ooRouter, + inputAmount, ); - await ensureRouterApproval( - signer, + const bridgeApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.USDC_POLYGON_CIRCLE, - polyCctp.tokenMessenger + polyCctp.tokenMessenger, + estimatedOut - feeAmount, ); const callData = routerIface.encodeFunctionData("swapAndBridge", swapAndBridgeArgs( @@ -173,7 +176,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, - approvalSpender: ooRouter, + approvalSpender: swapApprovalSpender, outputToken: TOKENS.USDC_POLYGON_CIRCLE, value: 0n, minOutput: minAmountOut, @@ -182,7 +185,7 @@ async function main() { swapData, { target: polyCctp.tokenMessenger, - approvalSpender: polyCctp.tokenMessenger, + approvalSpender: bridgeApprovalSpender, value: 0n, }, depositForBurnData, diff --git a/scripts/e2e/cctp/swapAndBridge.postFee.returndata.kyberswap.ts b/scripts/e2e/cctp/swapAndBridge.postFee.returndata.kyberswap.ts index 3a8abf6..0d9c5b0 100644 --- a/scripts/e2e/cctp/swapAndBridge.postFee.returndata.kyberswap.ts +++ b/scripts/e2e/cctp/swapAndBridge.postFee.returndata.kyberswap.ts @@ -35,8 +35,8 @@ import { ZERO_BYTES32, bridgeAmountPositionFlag, swapAndBridgeArgs } from "../ut import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, - ensureRouterApproval, -} from "../utils/reproducibility"; +} 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); @@ -203,17 +203,20 @@ async function main() { TOKENS.USDC_POLYGON_CIRCLE, ROUTER_POLYGON ); - await ensureRouterApproval( - signer, + + const swapApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, - ksRouter + ksRouter, + inputAmount, ); - await ensureRouterApproval( - signer, + const bridgeApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.USDC_POLYGON_CIRCLE, - polyCctp.tokenMessenger + polyCctp.tokenMessenger, + estimatedOut - feeAmount, ); const callData = routerIface.encodeFunctionData("swapAndBridge", swapAndBridgeArgs( @@ -227,7 +230,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: ksRouter, - approvalSpender: ksRouter, + approvalSpender: swapApprovalSpender, outputToken: TOKENS.USDC_POLYGON_CIRCLE, value, minOutput: minAmountOut, @@ -236,7 +239,7 @@ async function main() { swapData, { target: polyCctp.tokenMessenger, - approvalSpender: polyCctp.tokenMessenger, + approvalSpender: bridgeApprovalSpender, value: 0n, }, depositForBurnData, diff --git a/scripts/e2e/cctp/swapAndBridge.postFee.returndata.ts b/scripts/e2e/cctp/swapAndBridge.postFee.returndata.ts index dffa1df..415e714 100644 --- a/scripts/e2e/cctp/swapAndBridge.postFee.returndata.ts +++ b/scripts/e2e/cctp/swapAndBridge.postFee.returndata.ts @@ -35,8 +35,8 @@ import { ZERO_BYTES32, bridgeAmountPositionFlag, swapAndBridgeArgs } from "../ut import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, - ensureRouterApproval, -} from "../utils/reproducibility"; +} 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); @@ -149,17 +149,20 @@ async function main() { TOKENS.USDC_POLYGON_CIRCLE, ROUTER_POLYGON ); - await ensureRouterApproval( - signer, + + const swapApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, - ooRouter + ooRouter, + inputAmount, ); - await ensureRouterApproval( - signer, + const bridgeApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.USDC_POLYGON_CIRCLE, - polyCctp.tokenMessenger + polyCctp.tokenMessenger, + estimatedOut - feeAmount, ); const callData = routerIface.encodeFunctionData("swapAndBridge", swapAndBridgeArgs( @@ -173,7 +176,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, - approvalSpender: ooRouter, + approvalSpender: swapApprovalSpender, outputToken: TOKENS.USDC_POLYGON_CIRCLE, value: 0n, minOutput: minAmountOut, @@ -182,7 +185,7 @@ async function main() { swapData, { target: polyCctp.tokenMessenger, - approvalSpender: polyCctp.tokenMessenger, + approvalSpender: bridgeApprovalSpender, value: 0n, }, depositForBurnData, diff --git a/scripts/e2e/cctp/swapAndBridge.preFee.balanceOf.ts b/scripts/e2e/cctp/swapAndBridge.preFee.balanceOf.ts index 7d0b75c..1e2449a 100644 --- a/scripts/e2e/cctp/swapAndBridge.preFee.balanceOf.ts +++ b/scripts/e2e/cctp/swapAndBridge.preFee.balanceOf.ts @@ -35,8 +35,8 @@ import { ZERO_BYTES32, bridgeAmountPositionFlag, swapAndBridgeArgs } from "../ut import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, - ensureRouterApproval, -} from "../utils/reproducibility"; +} 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); @@ -151,17 +151,20 @@ async function main() { TOKENS.USDC_POLYGON_CIRCLE, ROUTER_POLYGON ); - await ensureRouterApproval( - signer, + + const swapApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, - ooRouter + ooRouter, + swapInputAmount, ); - await ensureRouterApproval( - signer, + const bridgeApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.USDC_POLYGON_CIRCLE, - polyCctp.tokenMessenger + polyCctp.tokenMessenger, + minAmountOut, ); const callData = routerIface.encodeFunctionData("swapAndBridge", swapAndBridgeArgs( @@ -175,7 +178,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, - approvalSpender: ooRouter, + approvalSpender: swapApprovalSpender, outputToken: TOKENS.USDC_POLYGON_CIRCLE, value: 0n, minOutput: minAmountOut, @@ -184,7 +187,7 @@ async function main() { swapData, { target: polyCctp.tokenMessenger, - approvalSpender: polyCctp.tokenMessenger, + approvalSpender: bridgeApprovalSpender, value: 0n, }, depositForBurnData, diff --git a/scripts/e2e/cctp/swapAndBridge.preFee.returndata.ts b/scripts/e2e/cctp/swapAndBridge.preFee.returndata.ts index f9729ac..72050de 100644 --- a/scripts/e2e/cctp/swapAndBridge.preFee.returndata.ts +++ b/scripts/e2e/cctp/swapAndBridge.preFee.returndata.ts @@ -35,8 +35,8 @@ import { ZERO_BYTES32, bridgeAmountPositionFlag, swapAndBridgeArgs } from "../ut import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, - ensureRouterApproval, -} from "../utils/reproducibility"; +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; // pre-fee (0x00) | bridge amount at byte offset 4 (depositForBurn amount param) const FLAGS = bridgeAmountPositionFlag(4); @@ -151,17 +151,20 @@ async function main() { TOKENS.USDC_POLYGON_CIRCLE, ROUTER_POLYGON ); - await ensureRouterApproval( - signer, + + const swapApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, - ooRouter + ooRouter, + swapInputAmount, ); - await ensureRouterApproval( - signer, + const bridgeApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.USDC_POLYGON_CIRCLE, - polyCctp.tokenMessenger + polyCctp.tokenMessenger, + minAmountOut, ); const callData = routerIface.encodeFunctionData("swapAndBridge", swapAndBridgeArgs( @@ -175,7 +178,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, - approvalSpender: ooRouter, + approvalSpender: swapApprovalSpender, outputToken: TOKENS.USDC_POLYGON_CIRCLE, value: 0n, minOutput: minAmountOut, @@ -184,7 +187,7 @@ async function main() { swapData, { target: polyCctp.tokenMessenger, - approvalSpender: polyCctp.tokenMessenger, + approvalSpender: bridgeApprovalSpender, value: 0n, }, depositForBurnData, diff --git a/scripts/e2e/config.ts b/scripts/e2e/config.ts index 24f4dc6..5aca8d4 100644 --- a/scripts/e2e/config.ts +++ b/scripts/e2e/config.ts @@ -29,12 +29,12 @@ export const BLOCK_EXPLORER_TX_PREFIX: Record = { export const ALLOWANCE_HOLDER = '0x0000000000001fF3684f28c67538d4D072C22734'; /** - * Deployed `BungeeOpenRouterV2Unchecked` — one address per chain used by e2e scripts. + * 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]: '0x7894c2c93e8952867e2fA4C0778296fEE77074Ea', + [CHAIN_IDS.POLYGON]: '0x33654252CEA9c95220Aa1d434a3631d5c0843AA4', [CHAIN_IDS.ARBITRUM]: '0xe1788A0374EF5D4C35e62478FdB35F37CeE5B951', [CHAIN_IDS.BASE]: '0x91b536E79cd3607b593f3011937862609D608253', [CHAIN_IDS.ETHEREUM]: '0xeB5ae85Fe7e3E272Ac77fd316079589C0Ed91648', diff --git a/scripts/e2e/misc/routerUsdc.withdraw.modular.ts b/scripts/e2e/misc/routerUsdc.withdraw.modular.ts index 4df585d..6ea546f 100644 --- a/scripts/e2e/misc/routerUsdc.withdraw.modular.ts +++ b/scripts/e2e/misc/routerUsdc.withdraw.modular.ts @@ -1,5 +1,5 @@ /** - * Polygon: sweep USDC from `BungeeOpenRouter` to the tx sender using + * Polygon: sweep USDC from `OpenRouter` to the tx sender using * `performActions` only — no AllowanceHolder, no pull step. * * Actions: diff --git a/scripts/e2e/oft/bridge.preFee.ts b/scripts/e2e/oft/bridge.preFee.ts index 49a909f..756ed5f 100644 --- a/scripts/e2e/oft/bridge.preFee.ts +++ b/scripts/e2e/oft/bridge.preFee.ts @@ -30,7 +30,8 @@ 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, ensureRouterApproval } from '../utils/reproducibility'; +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(); @@ -125,9 +126,17 @@ async function main(): Promise { 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: USDT0_OFT_ADAPTER_POLYGON, + approvalSpender: bridgeApprovalSpender, value: nativeFeeWithBuffer, }; @@ -135,7 +144,6 @@ async function main(): Promise { const execCalldata = routerIface.encodeFunctionData('bridge', [ZERO_BYTES32, input, fee, bridgeData, sendData]); await ensureRouterErc20Balance(signer, inputToken, ROUTER_POLYGON); - await ensureRouterApproval(signer, ROUTER_POLYGON, inputToken, USDT0_OFT_ADAPTER_POLYGON); await ensureAllowanceForAllowanceHolder(signer, inputToken, inputAmount); console.log('Sending AllowanceHolder.exec → router.bridge...'); diff --git a/scripts/e2e/oft/performExecution.postFee.ts b/scripts/e2e/oft/performExecution.postFee.ts index 67c0bbb..b8e668f 100644 --- a/scripts/e2e/oft/performExecution.postFee.ts +++ b/scripts/e2e/oft/performExecution.postFee.ts @@ -37,8 +37,8 @@ import { ZERO_BYTES32, bridgeAmountPositionFlag, swapAndBridgeArgs } from "../ut import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, - ensureRouterApproval, -} from "../utils/reproducibility"; +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; // post-fee (0x01) | bridge amount at byte offset 196 (sendParam.amountLD) const FLAGS = 0x01n | bridgeAmountPositionFlag(196); @@ -191,17 +191,20 @@ async function main() { await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); await ensureRouterErc20Balance(signer, TOKENS.USDT0_POLYGON, ROUTER_POLYGON); - await ensureRouterApproval( - signer, + + const swapApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, - ooRouter + ooRouter, + inputAmount, ); - await ensureRouterApproval( - signer, + const bridgeApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.USDT0_POLYGON, - USDT0_OFT_ADAPTER_POLYGON + USDT0_OFT_ADAPTER_POLYGON, + bridgeAmount, ); const oftSendData = buildOftSendCalldata(nativeFeeWithBuffer, signerAddress); @@ -217,7 +220,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, - approvalSpender: ooRouter, + approvalSpender: swapApprovalSpender, outputToken: TOKENS.USDT0_POLYGON, value: 0n, minOutput: minAmountOut, @@ -226,7 +229,7 @@ async function main() { swapData, { target: USDT0_OFT_ADAPTER_POLYGON, - approvalSpender: USDT0_OFT_ADAPTER_POLYGON, + approvalSpender: bridgeApprovalSpender, value: nativeFeeWithBuffer, }, oftSendData, diff --git a/scripts/e2e/oft/performExecution.preFee.ts b/scripts/e2e/oft/performExecution.preFee.ts index ebb6ab8..cc16455 100644 --- a/scripts/e2e/oft/performExecution.preFee.ts +++ b/scripts/e2e/oft/performExecution.preFee.ts @@ -30,7 +30,8 @@ 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, ensureRouterApproval } from '../utils/reproducibility'; +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(); @@ -125,9 +126,17 @@ async function main(): Promise { 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: USDT0_OFT_ADAPTER_POLYGON, + approvalSpender: bridgeApprovalSpender, value: nativeFeeWithBuffer, }; @@ -135,7 +144,6 @@ async function main(): Promise { const execCalldata = routerIface.encodeFunctionData('bridge', [ZERO_BYTES32, input, fee, bridgeData, sendData]); await ensureRouterErc20Balance(signer, inputToken, ROUTER_POLYGON); - await ensureRouterApproval(signer, ROUTER_POLYGON, inputToken, USDT0_OFT_ADAPTER_POLYGON); await ensureAllowanceForAllowanceHolder(signer, inputToken, inputAmount); console.log('Sending AllowanceHolder.exec → router.bridge...'); diff --git a/scripts/e2e/oft/performModularExecution.postFee.ts b/scripts/e2e/oft/performModularExecution.postFee.ts index 74e7e59..be9538b 100644 --- a/scripts/e2e/oft/performModularExecution.postFee.ts +++ b/scripts/e2e/oft/performModularExecution.postFee.ts @@ -35,12 +35,13 @@ import { USDT0_OFT_ADAPTER_POLYGON, } from '../config'; import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; -import { encodeApprove, encodeTransfer, encodeBalanceOf, getWalletErc20Balance } from '../utils/erc20'; +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, ensureRouterApproval } from '../utils/reproducibility'; +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(); @@ -143,8 +144,6 @@ async function main() { await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); await ensureRouterErc20Balance(signer, TOKENS.USDT0_POLYGON, ROUTER_POLYGON); - await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, ooRouter); - await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.USDT0_POLYGON, USDT0_OFT_ADAPTER_POLYGON); const oftSendData = buildOftSendCalldata(nativeFeeWithBuffer, signerAddress); @@ -153,10 +152,18 @@ async function main() { ]); const exec = new ModularActionsBuilder(); exec.call(ALLOWANCE_HOLDER, ahIface.encodeFunctionData('transferFrom', [TOKENS.AAVE_POLYGON, signerAddress, ROUTER_POLYGON, inputAmount])); - exec.call(TOKENS.AAVE_POLYGON, encodeApprove(ooRouter, 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)); - exec.call(TOKENS.USDT0_POLYGON, encodeApprove(USDT0_OFT_ADAPTER_POLYGON, ethers.MaxUint256)); + 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()); diff --git a/scripts/e2e/oft/performModularExecution.preFee.ts b/scripts/e2e/oft/performModularExecution.preFee.ts index 4f02cdc..297226e 100644 --- a/scripts/e2e/oft/performModularExecution.preFee.ts +++ b/scripts/e2e/oft/performModularExecution.preFee.ts @@ -30,12 +30,13 @@ import { USDT0_OFT_ADAPTER_POLYGON, } from '../config'; import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; -import { encodeApprove, encodeTransfer, encodeBalanceOf, getWalletErc20Balance } from '../utils/erc20'; +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, ensureRouterApproval } from '../utils/reproducibility'; +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(); @@ -102,7 +103,6 @@ async function main() { console.log(` Est. received: ${ethers.formatUnits(amountReceivedLD, 6)} USDT0`); await ensureRouterErc20Balance(signer, TOKENS.USDT0_POLYGON, ROUTER_POLYGON); - await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.USDT0_POLYGON, USDT0_OFT_ADAPTER_POLYGON); const oftSendData = buildOftSendCalldata(nativeFeeWithBuffer, signerAddress); @@ -112,7 +112,15 @@ async function main() { 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)); - exec.call(TOKENS.USDT0_POLYGON, encodeApprove(USDT0_OFT_ADAPTER_POLYGON, ethers.MaxUint256)); + 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()); diff --git a/scripts/e2e/oft/swapAndBridge.postFee.balanceOf.ts b/scripts/e2e/oft/swapAndBridge.postFee.balanceOf.ts index d865f0e..96f4944 100644 --- a/scripts/e2e/oft/swapAndBridge.postFee.balanceOf.ts +++ b/scripts/e2e/oft/swapAndBridge.postFee.balanceOf.ts @@ -37,8 +37,8 @@ import { ZERO_BYTES32, bridgeAmountPositionFlag, swapAndBridgeArgs } from "../ut import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, - ensureRouterApproval, -} from "../utils/reproducibility"; +} 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); @@ -191,17 +191,20 @@ async function main() { await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); await ensureRouterErc20Balance(signer, TOKENS.USDT0_POLYGON, ROUTER_POLYGON); - await ensureRouterApproval( - signer, + + const swapApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, - ooRouter + ooRouter, + inputAmount, ); - await ensureRouterApproval( - signer, + const bridgeApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.USDT0_POLYGON, - USDT0_OFT_ADAPTER_POLYGON + USDT0_OFT_ADAPTER_POLYGON, + bridgeAmount, ); const oftSendData = buildOftSendCalldata(nativeFeeWithBuffer, signerAddress); @@ -217,7 +220,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, - approvalSpender: ooRouter, + approvalSpender: swapApprovalSpender, outputToken: TOKENS.USDT0_POLYGON, value: 0n, minOutput: minAmountOut, @@ -226,7 +229,7 @@ async function main() { swapData, { target: USDT0_OFT_ADAPTER_POLYGON, - approvalSpender: USDT0_OFT_ADAPTER_POLYGON, + approvalSpender: bridgeApprovalSpender, value: nativeFeeWithBuffer, }, oftSendData, diff --git a/scripts/e2e/oft/swapAndBridge.postFee.returndata.ts b/scripts/e2e/oft/swapAndBridge.postFee.returndata.ts index fad5ebb..4980ec7 100644 --- a/scripts/e2e/oft/swapAndBridge.postFee.returndata.ts +++ b/scripts/e2e/oft/swapAndBridge.postFee.returndata.ts @@ -37,8 +37,8 @@ import { ZERO_BYTES32, bridgeAmountPositionFlag, swapAndBridgeArgs } from "../ut import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, - ensureRouterApproval, -} from "../utils/reproducibility"; +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; // post-fee (0x01) | bridge amount at byte offset 196 (sendParam.amountLD) const FLAGS = 0x01n | bridgeAmountPositionFlag(196); @@ -191,17 +191,20 @@ async function main() { await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); await ensureRouterErc20Balance(signer, TOKENS.USDT0_POLYGON, ROUTER_POLYGON); - await ensureRouterApproval( - signer, + + const swapApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, - ooRouter + ooRouter, + inputAmount, ); - await ensureRouterApproval( - signer, + const bridgeApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.USDT0_POLYGON, - USDT0_OFT_ADAPTER_POLYGON + USDT0_OFT_ADAPTER_POLYGON, + bridgeAmount, ); const oftSendData = buildOftSendCalldata(nativeFeeWithBuffer, signerAddress); @@ -217,7 +220,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, - approvalSpender: ooRouter, + approvalSpender: swapApprovalSpender, outputToken: TOKENS.USDT0_POLYGON, value: 0n, minOutput: minAmountOut, @@ -226,7 +229,7 @@ async function main() { swapData, { target: USDT0_OFT_ADAPTER_POLYGON, - approvalSpender: USDT0_OFT_ADAPTER_POLYGON, + approvalSpender: bridgeApprovalSpender, value: nativeFeeWithBuffer, }, oftSendData, diff --git a/scripts/e2e/oft/swapAndBridge.preFee.balanceOf.ts b/scripts/e2e/oft/swapAndBridge.preFee.balanceOf.ts index 789858f..8415223 100644 --- a/scripts/e2e/oft/swapAndBridge.preFee.balanceOf.ts +++ b/scripts/e2e/oft/swapAndBridge.preFee.balanceOf.ts @@ -37,8 +37,8 @@ import { ZERO_BYTES32, bridgeAmountPositionFlag, swapAndBridgeArgs } from "../ut import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, - ensureRouterApproval, -} from "../utils/reproducibility"; +} 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); @@ -190,17 +190,20 @@ async function main() { await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); await ensureRouterErc20Balance(signer, TOKENS.USDT0_POLYGON, ROUTER_POLYGON); - await ensureRouterApproval( - signer, + + const swapApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, - ooRouter + ooRouter, + inputAmount - feeAmount, ); - await ensureRouterApproval( - signer, + const bridgeApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.USDT0_POLYGON, - USDT0_OFT_ADAPTER_POLYGON + USDT0_OFT_ADAPTER_POLYGON, + minAmountOut, ); const oftSendData = buildOftSendCalldata(nativeFeeWithBuffer, signerAddress); @@ -216,7 +219,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, - approvalSpender: ooRouter, + approvalSpender: swapApprovalSpender, outputToken: TOKENS.USDT0_POLYGON, value: 0n, minOutput: minAmountOut, @@ -225,7 +228,7 @@ async function main() { swapData, { target: USDT0_OFT_ADAPTER_POLYGON, - approvalSpender: USDT0_OFT_ADAPTER_POLYGON, + approvalSpender: bridgeApprovalSpender, value: nativeFeeWithBuffer, }, oftSendData, diff --git a/scripts/e2e/oft/swapAndBridge.preFee.returndata.ts b/scripts/e2e/oft/swapAndBridge.preFee.returndata.ts index e8a60ae..5e5dcb7 100644 --- a/scripts/e2e/oft/swapAndBridge.preFee.returndata.ts +++ b/scripts/e2e/oft/swapAndBridge.preFee.returndata.ts @@ -37,8 +37,8 @@ import { ZERO_BYTES32, bridgeAmountPositionFlag, swapAndBridgeArgs } from "../ut import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, - ensureRouterApproval, -} from "../utils/reproducibility"; +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; // pre-fee (0x00) | bridge amount at byte offset 196 (sendParam.amountLD) const FLAGS = bridgeAmountPositionFlag(196); @@ -190,17 +190,20 @@ async function main() { await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); await ensureRouterErc20Balance(signer, TOKENS.USDT0_POLYGON, ROUTER_POLYGON); - await ensureRouterApproval( - signer, + + const swapApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, - ooRouter + ooRouter, + inputAmount - feeAmount, ); - await ensureRouterApproval( - signer, + const bridgeApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.USDT0_POLYGON, - USDT0_OFT_ADAPTER_POLYGON + USDT0_OFT_ADAPTER_POLYGON, + minAmountOut, ); const oftSendData = buildOftSendCalldata(nativeFeeWithBuffer, signerAddress); @@ -216,7 +219,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, - approvalSpender: ooRouter, + approvalSpender: swapApprovalSpender, outputToken: TOKENS.USDT0_POLYGON, value: 0n, minOutput: minAmountOut, @@ -225,7 +228,7 @@ async function main() { swapData, { target: USDT0_OFT_ADAPTER_POLYGON, - approvalSpender: USDT0_OFT_ADAPTER_POLYGON, + approvalSpender: bridgeApprovalSpender, value: nativeFeeWithBuffer, }, oftSendData, diff --git a/scripts/e2e/relay/aave.bridge.preFee.ts b/scripts/e2e/relay/aave.bridge.preFee.ts index 2d90da3..73f4955 100644 --- a/scripts/e2e/relay/aave.bridge.preFee.ts +++ b/scripts/e2e/relay/aave.bridge.preFee.ts @@ -27,7 +27,8 @@ 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, ensureRouterApproval } from '../utils/reproducibility'; +import { ensureRouterErc20Balance } from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); @@ -74,11 +75,18 @@ async function main(): Promise { console.log(`Deposit target: ${depositTarget}`); await ensureRouterErc20Balance(signer, inputToken, ROUTER_POLYGON); - await ensureRouterApproval(signer, ROUTER_POLYGON, inputToken, relaySpender); + + 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: relaySpender, value: 0n }; + 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]); diff --git a/scripts/e2e/relay/aave.performExecution.preFee.ts b/scripts/e2e/relay/aave.performExecution.preFee.ts index 188ba82..70098a0 100644 --- a/scripts/e2e/relay/aave.performExecution.preFee.ts +++ b/scripts/e2e/relay/aave.performExecution.preFee.ts @@ -24,7 +24,8 @@ 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, ensureRouterApproval } from '../utils/reproducibility'; +import { ensureRouterErc20Balance } from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); @@ -71,11 +72,18 @@ async function main(): Promise { console.log(`Deposit target: ${depositTarget}`); await ensureRouterErc20Balance(signer, inputToken, ROUTER_POLYGON); - await ensureRouterApproval(signer, ROUTER_POLYGON, inputToken, relaySpender); const input: InputData = { user: signerAddress, inputToken, inputAmount }; const fee: FeeData = { receiver: signerAddress, amount: feeAmount }; - const bridgeData: BridgeData = { target: depositTarget, approvalSpender: relaySpender, value: 0n }; + 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)); diff --git a/scripts/e2e/relay/aave.performModularExecution.preFee.ts b/scripts/e2e/relay/aave.performModularExecution.preFee.ts index 42076a0..dd9207e 100644 --- a/scripts/e2e/relay/aave.performModularExecution.preFee.ts +++ b/scripts/e2e/relay/aave.performModularExecution.preFee.ts @@ -26,13 +26,14 @@ import { ALLOWANCE_HOLDER, } from '../config'; import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; -import { encodeApprove, encodeTransfer, getWalletErc20Balance } from '../utils/erc20'; +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, ensureRouterApproval } from '../utils/reproducibility'; +import { ensureRouterErc20Balance } from '../utils/reproducibility'; +import { modularApproveIfNeeded } from '../utils/routerAllowance'; const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); @@ -79,7 +80,6 @@ async function main(): Promise { console.log(`Deposit target: ${depositTarget}`); await ensureRouterErc20Balance(signer, inputToken, ROUTER_POLYGON); - await ensureRouterApproval(signer, ROUTER_POLYGON, inputToken, relaySpender); const ahIface = new ethers.Interface([ 'function transferFrom(address token, address owner, address recipient, uint256 amount)', @@ -87,7 +87,7 @@ async function main(): Promise { const exec = new ModularActionsBuilder(); exec.call(ALLOWANCE_HOLDER, ahIface.encodeFunctionData('transferFrom', [inputToken, signerAddress, ROUTER_POLYGON, inputAmount])); exec.call(inputToken, encodeTransfer(signerAddress, feeAmount)); - exec.call(inputToken, encodeApprove(relaySpender, bridgeAmount)); + await modularApproveIfNeeded(exec, provider, ROUTER_POLYGON, inputToken, relaySpender, bridgeAmount, bridgeAmount); exec.call(depositTarget, depositData); const routerIface = new ethers.Interface(ROUTER_ABI); diff --git a/scripts/e2e/relay/usdc.bridge.preFee.ts b/scripts/e2e/relay/usdc.bridge.preFee.ts index 438d915..2fc5cbb 100644 --- a/scripts/e2e/relay/usdc.bridge.preFee.ts +++ b/scripts/e2e/relay/usdc.bridge.preFee.ts @@ -27,7 +27,8 @@ 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, ensureRouterApproval } from '../utils/reproducibility'; +import { ensureRouterErc20Balance } from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); @@ -74,11 +75,18 @@ async function main(): Promise { console.log(`Deposit target: ${depositTarget}`); await ensureRouterErc20Balance(signer, inputToken, ROUTER_POLYGON); - await ensureRouterApproval(signer, ROUTER_POLYGON, inputToken, relaySpender); + + 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: relaySpender, value: 0n }; + 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]); diff --git a/scripts/e2e/relay/usdc.performExecution.preFee.ts b/scripts/e2e/relay/usdc.performExecution.preFee.ts index baf4aff..e5ed43d 100644 --- a/scripts/e2e/relay/usdc.performExecution.preFee.ts +++ b/scripts/e2e/relay/usdc.performExecution.preFee.ts @@ -24,7 +24,8 @@ 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, ensureRouterApproval } from '../utils/reproducibility'; +import { ensureRouterErc20Balance } from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); @@ -71,11 +72,18 @@ async function main(): Promise { console.log(`Deposit target: ${depositTarget}`); await ensureRouterErc20Balance(signer, inputToken, ROUTER_POLYGON); - await ensureRouterApproval(signer, ROUTER_POLYGON, inputToken, relaySpender); const input: InputData = { user: signerAddress, inputToken, inputAmount }; const fee: FeeData = { receiver: signerAddress, amount: feeAmount }; - const bridgeData: BridgeData = { target: depositTarget, approvalSpender: relaySpender, value: 0n }; + 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)); diff --git a/scripts/e2e/relay/usdc.performModularExecution.preFee.ts b/scripts/e2e/relay/usdc.performModularExecution.preFee.ts index d070ae0..9dc70e5 100644 --- a/scripts/e2e/relay/usdc.performModularExecution.preFee.ts +++ b/scripts/e2e/relay/usdc.performModularExecution.preFee.ts @@ -26,13 +26,14 @@ import { ALLOWANCE_HOLDER, } from '../config'; import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; -import { encodeApprove, encodeTransfer, getWalletErc20Balance } from '../utils/erc20'; +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, ensureRouterApproval } from '../utils/reproducibility'; +import { ensureRouterErc20Balance } from '../utils/reproducibility'; +import { modularApproveIfNeeded } from '../utils/routerAllowance'; const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); @@ -79,7 +80,6 @@ async function main(): Promise { console.log(`Deposit target: ${depositTarget}`); await ensureRouterErc20Balance(signer, inputToken, ROUTER_POLYGON); - await ensureRouterApproval(signer, ROUTER_POLYGON, inputToken, relaySpender); const ahIface = new ethers.Interface([ 'function transferFrom(address token, address owner, address recipient, uint256 amount)', @@ -87,7 +87,7 @@ async function main(): Promise { const exec = new ModularActionsBuilder(); exec.call(ALLOWANCE_HOLDER, ahIface.encodeFunctionData('transferFrom', [inputToken, signerAddress, ROUTER_POLYGON, inputAmount])); exec.call(inputToken, encodeTransfer(signerAddress, feeAmount)); - exec.call(inputToken, encodeApprove(relaySpender, bridgeAmount)); + await modularApproveIfNeeded(exec, provider, ROUTER_POLYGON, inputToken, relaySpender, bridgeAmount, bridgeAmount); exec.call(depositTarget, depositData); const routerIface = new ethers.Interface(ROUTER_ABI); diff --git a/scripts/e2e/stargate/arbUsdcBaseEth.performExecution.postFee.ts b/scripts/e2e/stargate/arbUsdcBaseEth.performExecution.postFee.ts index adaf4f6..81e4f8e 100644 --- a/scripts/e2e/stargate/arbUsdcBaseEth.performExecution.postFee.ts +++ b/scripts/e2e/stargate/arbUsdcBaseEth.performExecution.postFee.ts @@ -41,7 +41,8 @@ import { swapAndBridgeArgs, } from '../utils/contractTypes'; import { logTxnSummary } from '../utils/txnLogSummary'; -import { ensureRouterErc20Balance, ensureRouterNativeBalance, ensureRouterApproval } from '../utils/reproducibility'; +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); @@ -148,7 +149,14 @@ async function main() { await ensureRouterErc20Balance(signer, TOKENS.USDC_ARB, ROUTER_ARB); await ensureRouterNativeBalance(signer, ROUTER_ARB); - await ensureRouterApproval(signer, ROUTER_ARB, TOKENS.USDC_ARB, ooRouter); + + const swapApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_ARB, + TOKENS.USDC_ARB, + ooRouter, + inputAmount, + ); const stargateData = buildStargateCalldata(nativeFeeWithBuffer, signerAddress, amountLD); @@ -161,7 +169,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, - approvalSpender: ooRouter, + approvalSpender: swapApprovalSpender, outputToken: NATIVE_TOKEN_ADDRESS, value: 0n, minOutput: minAmountOut, diff --git a/scripts/e2e/stargate/arbUsdcBaseEth.performModularExecution.postFee.ts b/scripts/e2e/stargate/arbUsdcBaseEth.performModularExecution.postFee.ts index 2b8a109..aa0b93c 100644 --- a/scripts/e2e/stargate/arbUsdcBaseEth.performModularExecution.postFee.ts +++ b/scripts/e2e/stargate/arbUsdcBaseEth.performModularExecution.postFee.ts @@ -36,12 +36,13 @@ import { BASE_LZ_EID, } from '../config'; import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; -import { encodeApprove, getWalletErc20Balance } from '../utils/erc20'; +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, ensureRouterApproval } from '../utils/reproducibility'; +import { ensureRouterErc20Balance, ensureRouterNativeBalance } from '../utils/reproducibility'; +import { modularApproveIfNeeded } from '../utils/routerAllowance'; const ROUTER_ARB = routerAddressForChain(CHAIN_IDS.ARBITRUM); @@ -147,7 +148,6 @@ async function main() { await ensureRouterErc20Balance(signer, TOKENS.USDC_ARB, ROUTER_ARB); await ensureRouterNativeBalance(signer, ROUTER_ARB); - await ensureRouterApproval(signer, ROUTER_ARB, TOKENS.USDC_ARB, ooRouter); const stargateData = buildStargateCalldata(nativeFeeWithBuffer, amountLD, signerAddress); @@ -156,7 +156,7 @@ async function main() { ]); const exec = new ModularActionsBuilder(); exec.call(ALLOWANCE_HOLDER, ahIface.encodeFunctionData('transferFrom', [TOKENS.USDC_ARB, signerAddress, ROUTER_ARB, inputAmount])); - exec.call(TOKENS.USDC_ARB, encodeApprove(ooRouter, 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); diff --git a/scripts/e2e/stargate/baseUsdcArbEth.performExecution.postFee.ts b/scripts/e2e/stargate/baseUsdcArbEth.performExecution.postFee.ts index ed16e8c..05d0c3f 100644 --- a/scripts/e2e/stargate/baseUsdcArbEth.performExecution.postFee.ts +++ b/scripts/e2e/stargate/baseUsdcArbEth.performExecution.postFee.ts @@ -48,8 +48,8 @@ import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, ensureRouterNativeBalance, - ensureRouterApproval, -} from "../utils/reproducibility"; +} 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); @@ -203,7 +203,14 @@ async function main() { await ensureRouterErc20Balance(signer, TOKENS.USDC_BASE, ROUTER_BASE); await ensureRouterNativeBalance(signer, ROUTER_BASE); - await ensureRouterApproval(signer, ROUTER_BASE, TOKENS.USDC_BASE, ooRouter); + + const swapApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_BASE, + TOKENS.USDC_BASE, + ooRouter, + inputAmount, + ); const stargateData = buildStargateCalldata( nativeFeeWithBuffer, @@ -222,7 +229,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, - approvalSpender: ooRouter, + approvalSpender: swapApprovalSpender, outputToken: NATIVE_TOKEN_ADDRESS, value: 0n, minOutput: minAmountOut, diff --git a/scripts/e2e/stargate/baseUsdcArbEth.performModularExecution.postFee.ts b/scripts/e2e/stargate/baseUsdcArbEth.performModularExecution.postFee.ts index 7afc9c8..7521bcb 100644 --- a/scripts/e2e/stargate/baseUsdcArbEth.performModularExecution.postFee.ts +++ b/scripts/e2e/stargate/baseUsdcArbEth.performModularExecution.postFee.ts @@ -33,12 +33,13 @@ import { ARBITRUM_LZ_EID, } from '../config'; import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; -import { encodeApprove, getWalletErc20Balance } from '../utils/erc20'; +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, ensureRouterApproval } from '../utils/reproducibility'; +import { ensureRouterErc20Balance, ensureRouterNativeBalance } from '../utils/reproducibility'; +import { modularApproveIfNeeded } from '../utils/routerAllowance'; const ROUTER_BASE = routerAddressForChain(CHAIN_IDS.BASE); @@ -143,7 +144,6 @@ async function main() { await ensureRouterErc20Balance(signer, TOKENS.USDC_BASE, ROUTER_BASE); await ensureRouterNativeBalance(signer, ROUTER_BASE); - await ensureRouterApproval(signer, ROUTER_BASE, TOKENS.USDC_BASE, ooRouter); const stargateData = buildStargateCalldata(nativeFeeWithBuffer, amountLD, signerAddress); @@ -152,7 +152,7 @@ async function main() { ]); const exec = new ModularActionsBuilder(); exec.call(ALLOWANCE_HOLDER, ahIface.encodeFunctionData('transferFrom', [TOKENS.USDC_BASE, signerAddress, ROUTER_BASE, inputAmount])); - exec.call(TOKENS.USDC_BASE, encodeApprove(ooRouter, 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); diff --git a/scripts/e2e/stargate/polygonPolUsdt0Arb.performExecution.postFee.ts b/scripts/e2e/stargate/polygonPolUsdt0Arb.performExecution.postFee.ts index cd5fc38..87ca5eb 100644 --- a/scripts/e2e/stargate/polygonPolUsdt0Arb.performExecution.postFee.ts +++ b/scripts/e2e/stargate/polygonPolUsdt0Arb.performExecution.postFee.ts @@ -42,7 +42,8 @@ import { swapAndBridgeArgs, } from '../utils/contractTypes'; import { logTxnSummary } from '../utils/txnLogSummary'; -import { ensureRouterErc20Balance, ensureRouterNativeBalance, ensureRouterApproval } from '../utils/reproducibility'; +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); @@ -182,7 +183,14 @@ async function main() { await ensureRouterErc20Balance(signer, TOKENS.USDT0_POLYGON, ROUTER_POLYGON); await ensureRouterNativeBalance(signer, ROUTER_POLYGON); - await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.USDT0_POLYGON, USDT0_OFT_ADAPTER_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; @@ -206,7 +214,7 @@ async function main() { returnDataWordOffset: 0n, }, swapData, - { target: USDT0_OFT_ADAPTER_POLYGON, approvalSpender: USDT0_OFT_ADAPTER_POLYGON, value: nativeFeeWithBuffer }, + { target: USDT0_OFT_ADAPTER_POLYGON, approvalSpender: bridgeApprovalSpender, value: nativeFeeWithBuffer }, oftSendData, ), ); diff --git a/scripts/e2e/stargate/polygonPolUsdt0Arb.performModularExecution.postFee.ts b/scripts/e2e/stargate/polygonPolUsdt0Arb.performModularExecution.postFee.ts index b65902f..d46436b 100644 --- a/scripts/e2e/stargate/polygonPolUsdt0Arb.performModularExecution.postFee.ts +++ b/scripts/e2e/stargate/polygonPolUsdt0Arb.performModularExecution.postFee.ts @@ -36,12 +36,13 @@ import { STARGATE_AMOUNT_LD_OFFSET, } from '../config'; import { execViaAH } from '../utils/allowanceHolder'; -import { encodeApprove, encodeTransfer, encodeBalanceOf } from '../utils/erc20'; +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, ensureRouterApproval } from '../utils/reproducibility'; +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(); @@ -176,7 +177,6 @@ async function main() { await ensureRouterErc20Balance(signer, TOKENS.USDT0_POLYGON, ROUTER_POLYGON); await ensureRouterNativeBalance(signer, ROUTER_POLYGON); - await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.USDT0_POLYGON, USDT0_OFT_ADAPTER_POLYGON); const rawOoWei = nativeSwapWei > 0n ? nativeSwapWei : inputAmountWei; const polOrEthToOo = rawOoWei <= inputAmountWei ? rawOoWei : inputAmountWei; @@ -186,7 +186,15 @@ async function main() { const exec = new ModularActionsBuilder(); exec.nativeCall(ooRouter, swapData, polOrEthToOo); exec.call(TOKENS.USDT0_POLYGON, encodeTransfer(signerAddress, feeAmount)); - exec.call(TOKENS.USDT0_POLYGON, encodeApprove(USDT0_OFT_ADAPTER_POLYGON, ethers.MaxUint256)); + 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()); diff --git a/scripts/e2e/stargate/polygonUsdcBase.bridge.preFee.ts b/scripts/e2e/stargate/polygonUsdcBase.bridge.preFee.ts index 80b62be..c2f6c2b 100644 --- a/scripts/e2e/stargate/polygonUsdcBase.bridge.preFee.ts +++ b/scripts/e2e/stargate/polygonUsdcBase.bridge.preFee.ts @@ -29,7 +29,8 @@ 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, ensureRouterApproval } from '../utils/reproducibility'; +import { ensureRouterErc20Balance } from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); @@ -123,9 +124,17 @@ async function main(): Promise { 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: STARGATE_USDC_POLYGON, + approvalSpender: bridgeApprovalSpender, value: nativeFeeWithBuffer, }; @@ -133,7 +142,6 @@ async function main(): Promise { const execCalldata = routerIface.encodeFunctionData('bridge', [ZERO_BYTES32, input, fee, bridgeData, sendData]); await ensureRouterErc20Balance(signer, inputToken, ROUTER_POLYGON); - await ensureRouterApproval(signer, ROUTER_POLYGON, inputToken, STARGATE_USDC_POLYGON); await ensureAllowanceForAllowanceHolder(signer, inputToken, inputAmount); console.log('Sending AllowanceHolder.exec → router.bridge...'); diff --git a/scripts/e2e/stargate/polygonUsdcBase.performExecution.postFee.ts b/scripts/e2e/stargate/polygonUsdcBase.performExecution.postFee.ts index b4256dc..0009435 100644 --- a/scripts/e2e/stargate/polygonUsdcBase.performExecution.postFee.ts +++ b/scripts/e2e/stargate/polygonUsdcBase.performExecution.postFee.ts @@ -29,7 +29,8 @@ 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, ensureRouterApproval } from '../utils/reproducibility'; +import { ensureRouterErc20Balance } from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); @@ -123,9 +124,17 @@ async function main(): Promise { 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: STARGATE_USDC_POLYGON, + approvalSpender: bridgeApprovalSpender, value: nativeFeeWithBuffer, }; @@ -133,7 +142,6 @@ async function main(): Promise { const execCalldata = routerIface.encodeFunctionData('bridge', [ZERO_BYTES32, input, fee, bridgeData, sendData]); await ensureRouterErc20Balance(signer, inputToken, ROUTER_POLYGON); - await ensureRouterApproval(signer, ROUTER_POLYGON, inputToken, STARGATE_USDC_POLYGON); await ensureAllowanceForAllowanceHolder(signer, inputToken, inputAmount); console.log('Sending AllowanceHolder.exec → router.bridge...'); diff --git a/scripts/e2e/stargate/polygonUsdcBase.performModularExecution.postFee.ts b/scripts/e2e/stargate/polygonUsdcBase.performModularExecution.postFee.ts index 1a5e457..ebb355a 100644 --- a/scripts/e2e/stargate/polygonUsdcBase.performModularExecution.postFee.ts +++ b/scripts/e2e/stargate/polygonUsdcBase.performModularExecution.postFee.ts @@ -31,12 +31,13 @@ import { STARGATE_AMOUNT_LD_OFFSET, } from '../config'; import { execViaAH, ensureAllowanceForAllowanceHolder } from '../utils/allowanceHolder'; -import { encodeApprove, encodeTransfer, encodeBalanceOf, getWalletErc20Balance } from '../utils/erc20'; +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, ensureRouterApproval } from '../utils/reproducibility'; +import { ensureRouterErc20Balance } from '../utils/reproducibility'; +import { modularApproveIfNeeded } from '../utils/routerAllowance'; const ROUTER_POLYGON = routerAddressForChain(CHAIN_IDS.POLYGON); @@ -101,7 +102,6 @@ async function main() { console.log(` Est. received: ${ethers.formatUnits(amountReceivedLD, 6)} USDC`); await ensureRouterErc20Balance(signer, TOKENS.USDC_POLYGON_CIRCLE, ROUTER_POLYGON); - await ensureRouterApproval(signer, ROUTER_POLYGON, TOKENS.USDC_POLYGON_CIRCLE, STARGATE_USDC_POLYGON); const stargateData = buildStargateCalldata(nativeFeeWithBuffer, signerAddress); @@ -111,7 +111,15 @@ async function main() { 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)); - exec.call(TOKENS.USDC_POLYGON_CIRCLE, encodeApprove(STARGATE_USDC_POLYGON, ethers.MaxUint256)); + 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()); diff --git a/scripts/e2e/stargate/swapAndBridge.postFee.balanceOf.ts b/scripts/e2e/stargate/swapAndBridge.postFee.balanceOf.ts index 0b3a295..8be7ccb 100644 --- a/scripts/e2e/stargate/swapAndBridge.postFee.balanceOf.ts +++ b/scripts/e2e/stargate/swapAndBridge.postFee.balanceOf.ts @@ -48,8 +48,8 @@ import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, ensureRouterNativeBalance, - ensureRouterApproval, -} from "../utils/reproducibility"; +} 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); @@ -202,7 +202,14 @@ async function main() { await ensureRouterErc20Balance(signer, TOKENS.USDC_BASE, ROUTER_BASE); await ensureRouterNativeBalance(signer, ROUTER_BASE); - await ensureRouterApproval(signer, ROUTER_BASE, TOKENS.USDC_BASE, ooRouter); + + const swapApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_BASE, + TOKENS.USDC_BASE, + ooRouter, + inputAmount, + ); const stargateData = buildStargateCalldata( nativeFeeWithBuffer, @@ -221,7 +228,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, - approvalSpender: ooRouter, + approvalSpender: swapApprovalSpender, outputToken: NATIVE_TOKEN_ADDRESS, value: 0n, minOutput: minAmountOut, diff --git a/scripts/e2e/stargate/swapAndBridge.postFee.returndata.ts b/scripts/e2e/stargate/swapAndBridge.postFee.returndata.ts index 10f84e2..ba1c483 100644 --- a/scripts/e2e/stargate/swapAndBridge.postFee.returndata.ts +++ b/scripts/e2e/stargate/swapAndBridge.postFee.returndata.ts @@ -48,8 +48,8 @@ import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, ensureRouterNativeBalance, - ensureRouterApproval, -} from "../utils/reproducibility"; +} 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); @@ -203,7 +203,14 @@ async function main() { await ensureRouterErc20Balance(signer, TOKENS.USDC_BASE, ROUTER_BASE); await ensureRouterNativeBalance(signer, ROUTER_BASE); - await ensureRouterApproval(signer, ROUTER_BASE, TOKENS.USDC_BASE, ooRouter); + + const swapApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_BASE, + TOKENS.USDC_BASE, + ooRouter, + inputAmount, + ); const stargateData = buildStargateCalldata( nativeFeeWithBuffer, @@ -222,7 +229,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, - approvalSpender: ooRouter, + approvalSpender: swapApprovalSpender, outputToken: NATIVE_TOKEN_ADDRESS, value: 0n, minOutput: minAmountOut, diff --git a/scripts/e2e/stargate/swapAndBridge.preFee.balanceOf.ts b/scripts/e2e/stargate/swapAndBridge.preFee.balanceOf.ts index 00e5659..462ed18 100644 --- a/scripts/e2e/stargate/swapAndBridge.preFee.balanceOf.ts +++ b/scripts/e2e/stargate/swapAndBridge.preFee.balanceOf.ts @@ -48,8 +48,8 @@ import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, ensureRouterNativeBalance, - ensureRouterApproval, -} from "../utils/reproducibility"; +} 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); @@ -202,7 +202,14 @@ async function main() { await ensureRouterErc20Balance(signer, TOKENS.USDC_BASE, ROUTER_BASE); await ensureRouterNativeBalance(signer, ROUTER_BASE); - await ensureRouterApproval(signer, ROUTER_BASE, TOKENS.USDC_BASE, ooRouter); + + const swapApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_BASE, + TOKENS.USDC_BASE, + ooRouter, + swapInput, + ); const stargateData = buildStargateCalldata( nativeFeeWithBuffer, @@ -221,7 +228,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, - approvalSpender: ooRouter, + approvalSpender: swapApprovalSpender, outputToken: NATIVE_TOKEN_ADDRESS, value: 0n, minOutput: minAmountOut, diff --git a/scripts/e2e/stargate/swapAndBridge.preFee.returndata.ts b/scripts/e2e/stargate/swapAndBridge.preFee.returndata.ts index 2324ea5..971437f 100644 --- a/scripts/e2e/stargate/swapAndBridge.preFee.returndata.ts +++ b/scripts/e2e/stargate/swapAndBridge.preFee.returndata.ts @@ -48,8 +48,8 @@ import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, ensureRouterNativeBalance, - ensureRouterApproval, -} from "../utils/reproducibility"; +} 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); @@ -202,7 +202,14 @@ async function main() { await ensureRouterErc20Balance(signer, TOKENS.USDC_BASE, ROUTER_BASE); await ensureRouterNativeBalance(signer, ROUTER_BASE); - await ensureRouterApproval(signer, ROUTER_BASE, TOKENS.USDC_BASE, ooRouter); + + const swapApprovalSpender = await resolveApprovalSpender( + provider, + ROUTER_BASE, + TOKENS.USDC_BASE, + ooRouter, + swapInput, + ); const stargateData = buildStargateCalldata( nativeFeeWithBuffer, @@ -221,7 +228,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, - approvalSpender: ooRouter, + approvalSpender: swapApprovalSpender, outputToken: NATIVE_TOKEN_ADDRESS, value: 0n, minOutput: minAmountOut, diff --git a/scripts/e2e/swap/kyberswap.postFee.balanceOf.ts b/scripts/e2e/swap/kyberswap.postFee.balanceOf.ts index 570c08e..e1a0380 100644 --- a/scripts/e2e/swap/kyberswap.postFee.balanceOf.ts +++ b/scripts/e2e/swap/kyberswap.postFee.balanceOf.ts @@ -37,8 +37,8 @@ import { ZERO_BYTES32, swapArgs } from "../utils/contractTypes"; import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, - ensureRouterApproval, -} from "../utils/reproducibility"; +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; // post-fee (0x01) | balance-of (0x02) const FLAGS = 0x03n; @@ -176,11 +176,13 @@ async function main() { TOKENS.USDC_POLYGON_CIRCLE, ROUTER_POLYGON, ); - await ensureRouterApproval( - signer, + + const swapApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, ksRouter, + inputAmount, ); const callData = routerIface.encodeFunctionData("swap", swapArgs( @@ -194,7 +196,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: ksRouter, - approvalSpender: ksRouter, + approvalSpender: swapApprovalSpender, outputToken: TOKENS.USDC_POLYGON_CIRCLE, value, minOutput: minAmountOut, diff --git a/scripts/e2e/swap/kyberswap.postFee.returndata.ts b/scripts/e2e/swap/kyberswap.postFee.returndata.ts index 6da3af8..5deafac 100644 --- a/scripts/e2e/swap/kyberswap.postFee.returndata.ts +++ b/scripts/e2e/swap/kyberswap.postFee.returndata.ts @@ -38,8 +38,8 @@ import { ZERO_BYTES32, swapArgs } from "../utils/contractTypes"; import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, - ensureRouterApproval, -} from "../utils/reproducibility"; +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; // post-fee (0x01) | returndata (bit1=0 ⇒ no 0x02) const FLAGS = 0x01n; @@ -171,11 +171,13 @@ async function main() { console.log(` Min USDC: ${ethers.formatUnits(minAmountOut, 6)}`); await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); - await ensureRouterApproval( - signer, + + const swapApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, ksRouter, + inputAmount, ); const callData = routerIface.encodeFunctionData("swap", swapArgs( @@ -189,7 +191,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: ksRouter, - approvalSpender: ksRouter, + approvalSpender: swapApprovalSpender, outputToken: TOKENS.USDC_POLYGON_CIRCLE, value, minOutput: minAmountOut, diff --git a/scripts/e2e/swap/kyberswap.preFee.balanceOf.ts b/scripts/e2e/swap/kyberswap.preFee.balanceOf.ts index 9f9312d..fbd0787 100644 --- a/scripts/e2e/swap/kyberswap.preFee.balanceOf.ts +++ b/scripts/e2e/swap/kyberswap.preFee.balanceOf.ts @@ -39,8 +39,8 @@ import { ZERO_BYTES32, swapArgs } from "../utils/contractTypes"; import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, - ensureRouterApproval, -} from "../utils/reproducibility"; +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; // pre-fee (0x00) | balance-of (0x02) const FLAGS = 0x02n; @@ -182,11 +182,13 @@ async function main() { TOKENS.USDC_POLYGON_CIRCLE, ROUTER_POLYGON, ); - await ensureRouterApproval( - signer, + + const swapApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, ksRouter, + swapInput, ); const callData = routerIface.encodeFunctionData("swap", swapArgs( @@ -200,7 +202,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: ksRouter, - approvalSpender: ksRouter, + approvalSpender: swapApprovalSpender, outputToken: TOKENS.USDC_POLYGON_CIRCLE, value, minOutput: minAmountOut, diff --git a/scripts/e2e/swap/kyberswap.preFee.returndata.ts b/scripts/e2e/swap/kyberswap.preFee.returndata.ts index 59c9302..c9b84e4 100644 --- a/scripts/e2e/swap/kyberswap.preFee.returndata.ts +++ b/scripts/e2e/swap/kyberswap.preFee.returndata.ts @@ -40,8 +40,8 @@ import { ZERO_BYTES32, swapArgs } from "../utils/contractTypes"; import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, - ensureRouterApproval, -} from "../utils/reproducibility"; +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; // pre-fee (0x00) | returndata (bit1=0 ⇒ no 0x02) const FLAGS = 0x00n; @@ -178,11 +178,13 @@ async function main() { console.log(` Min USDC: ${ethers.formatUnits(minAmountOut, 6)}`); await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); - await ensureRouterApproval( - signer, + + const swapApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, ksRouter, + swapInput, ); const callData = routerIface.encodeFunctionData("swap", swapArgs( @@ -196,7 +198,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: ksRouter, - approvalSpender: ksRouter, + approvalSpender: swapApprovalSpender, outputToken: TOKENS.USDC_POLYGON_CIRCLE, value, minOutput: minAmountOut, diff --git a/scripts/e2e/swap/swap.postFee.balanceOf.ts b/scripts/e2e/swap/swap.postFee.balanceOf.ts index 06e8377..70a71af 100644 --- a/scripts/e2e/swap/swap.postFee.balanceOf.ts +++ b/scripts/e2e/swap/swap.postFee.balanceOf.ts @@ -35,8 +35,8 @@ import { ZERO_BYTES32, swapArgs } from "../utils/contractTypes"; import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, - ensureRouterApproval, -} from "../utils/reproducibility"; +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; // post-fee (0x01) | balance-of (0x02) const FLAGS = 0x03n; @@ -118,11 +118,13 @@ async function main() { TOKENS.USDC_POLYGON_CIRCLE, ROUTER_POLYGON ); - await ensureRouterApproval( - signer, + + const swapApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, - ooRouter + ooRouter, + inputAmount, ); const callData = routerIface.encodeFunctionData("swap", swapArgs( @@ -136,7 +138,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, - approvalSpender: ooRouter, + approvalSpender: swapApprovalSpender, outputToken: TOKENS.USDC_POLYGON_CIRCLE, value: 0n, minOutput: minAmountOut, diff --git a/scripts/e2e/swap/swap.postFee.returndata.ts b/scripts/e2e/swap/swap.postFee.returndata.ts index 9fa27fa..7fde5db 100644 --- a/scripts/e2e/swap/swap.postFee.returndata.ts +++ b/scripts/e2e/swap/swap.postFee.returndata.ts @@ -35,8 +35,8 @@ import { ZERO_BYTES32, swapArgs } from "../utils/contractTypes"; import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, - ensureRouterApproval, -} from "../utils/reproducibility"; +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; // post-fee (0x01) | returndata (0x00) const FLAGS = 0x01n; @@ -113,11 +113,13 @@ async function main() { console.log(` Min USDC: ${ethers.formatUnits(minAmountOut, 6)}`); await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); - await ensureRouterApproval( - signer, + + const swapApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, - ooRouter + ooRouter, + inputAmount, ); const callData = routerIface.encodeFunctionData("swap", swapArgs( @@ -131,7 +133,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, - approvalSpender: ooRouter, + approvalSpender: swapApprovalSpender, outputToken: TOKENS.USDC_POLYGON_CIRCLE, value: 0n, minOutput: minAmountOut, diff --git a/scripts/e2e/swap/swap.preFee.balanceOf.ts b/scripts/e2e/swap/swap.preFee.balanceOf.ts index 4e4c5c0..d557b50 100644 --- a/scripts/e2e/swap/swap.preFee.balanceOf.ts +++ b/scripts/e2e/swap/swap.preFee.balanceOf.ts @@ -35,8 +35,8 @@ import { ZERO_BYTES32, swapArgs } from "../utils/contractTypes"; import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, - ensureRouterApproval, -} from "../utils/reproducibility"; +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; // pre-fee (0x00) | balance-of (0x02) const FLAGS = 0x02n; @@ -118,11 +118,13 @@ async function main() { TOKENS.USDC_POLYGON_CIRCLE, ROUTER_POLYGON ); - await ensureRouterApproval( - signer, + + const swapApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, - ooRouter + ooRouter, + inputAmount - feeAmount, ); const callData = routerIface.encodeFunctionData("swap", swapArgs( @@ -136,7 +138,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, - approvalSpender: ooRouter, + approvalSpender: swapApprovalSpender, outputToken: TOKENS.USDC_POLYGON_CIRCLE, value: 0n, minOutput: minAmountOut, diff --git a/scripts/e2e/swap/swap.preFee.returndata.ts b/scripts/e2e/swap/swap.preFee.returndata.ts index bd38078..094a548 100644 --- a/scripts/e2e/swap/swap.preFee.returndata.ts +++ b/scripts/e2e/swap/swap.preFee.returndata.ts @@ -35,8 +35,8 @@ import { ZERO_BYTES32, swapArgs } from "../utils/contractTypes"; import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, - ensureRouterApproval, -} from "../utils/reproducibility"; +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; // pre-fee (0x00) | returndata (0x00) const FLAGS = 0x00n; @@ -113,11 +113,13 @@ async function main() { console.log(` Min USDC: ${ethers.formatUnits(minAmountOut, 6)}`); await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); - await ensureRouterApproval( - signer, + + const swapApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, - ooRouter + ooRouter, + inputAmount - feeAmount, ); const callData = routerIface.encodeFunctionData("swap", swapArgs( @@ -131,7 +133,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: ooRouter, - approvalSpender: ooRouter, + approvalSpender: swapApprovalSpender, outputToken: TOKENS.USDC_POLYGON_CIRCLE, value: 0n, minOutput: minAmountOut, diff --git a/scripts/e2e/swap/zerox.postFee.balanceOf.ts b/scripts/e2e/swap/zerox.postFee.balanceOf.ts index 0e1e7e0..7137d81 100644 --- a/scripts/e2e/swap/zerox.postFee.balanceOf.ts +++ b/scripts/e2e/swap/zerox.postFee.balanceOf.ts @@ -40,8 +40,8 @@ import { ZERO_BYTES32, swapArgs } from "../utils/contractTypes"; import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, - ensureRouterApproval, -} from "../utils/reproducibility"; +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; // post-fee (0x01) | balance-of (0x02) const FLAGS = 0x03n; @@ -172,11 +172,13 @@ async function main() { TOKENS.USDC_POLYGON_CIRCLE, ROUTER_POLYGON, ); - await ensureRouterApproval( - signer, + + const swapApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, approvalSpender, + inputAmount, ); const callData = routerIface.encodeFunctionData("swap", swapArgs( @@ -190,7 +192,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: swapTarget, - approvalSpender, + approvalSpender: swapApprovalSpender, outputToken: TOKENS.USDC_POLYGON_CIRCLE, value, minOutput: minBuyAmount, diff --git a/scripts/e2e/swap/zerox.postFee.returndata.ts b/scripts/e2e/swap/zerox.postFee.returndata.ts index 12253ef..8ec6e73 100644 --- a/scripts/e2e/swap/zerox.postFee.returndata.ts +++ b/scripts/e2e/swap/zerox.postFee.returndata.ts @@ -39,8 +39,8 @@ import { ZERO_BYTES32, swapArgs } from "../utils/contractTypes"; import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, - ensureRouterApproval, -} from "../utils/reproducibility"; +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; // post-fee (0x01) | returndata (no 0x02) const FLAGS = 0x01n; @@ -161,11 +161,13 @@ async function main() { const approvalSpender = ALLOWANCE_HOLDER; await ensureRouterErc20Balance(signer, TOKENS.AAVE_POLYGON, ROUTER_POLYGON); - await ensureRouterApproval( - signer, + + const swapApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, approvalSpender, + inputAmount, ); const callData = routerIface.encodeFunctionData("swap", swapArgs( @@ -179,7 +181,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: swapTarget, - approvalSpender, + approvalSpender: swapApprovalSpender, outputToken: TOKENS.USDC_POLYGON_CIRCLE, value, minOutput: minBuyAmount, diff --git a/scripts/e2e/swap/zerox.preFee.balanceOf.ts b/scripts/e2e/swap/zerox.preFee.balanceOf.ts index 75b18a1..d3ec6f8 100644 --- a/scripts/e2e/swap/zerox.preFee.balanceOf.ts +++ b/scripts/e2e/swap/zerox.preFee.balanceOf.ts @@ -43,8 +43,8 @@ import { ZERO_BYTES32, swapArgs } from "../utils/contractTypes"; import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, - ensureRouterApproval, -} from "../utils/reproducibility"; +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; // pre-fee (0x00) | balance-of (0x02) const FLAGS = 0x02n; @@ -172,18 +172,19 @@ async function main() { // 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, ); - await ensureRouterApproval( - signer, + + const swapApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, approvalSpender, + swapInput, ); const callData = routerIface.encodeFunctionData("swap", swapArgs( @@ -197,7 +198,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: swapTarget, - approvalSpender, + approvalSpender: swapApprovalSpender, outputToken: TOKENS.USDC_POLYGON_CIRCLE, value, minOutput: minBuyAmount, diff --git a/scripts/e2e/swap/zerox.preFee.returndata.ts b/scripts/e2e/swap/zerox.preFee.returndata.ts index dfa9057..b955005 100644 --- a/scripts/e2e/swap/zerox.preFee.returndata.ts +++ b/scripts/e2e/swap/zerox.preFee.returndata.ts @@ -41,8 +41,8 @@ import { ZERO_BYTES32, swapArgs } from "../utils/contractTypes"; import { logTxnSummary } from "../utils/txnLogSummary"; import { ensureRouterErc20Balance, - ensureRouterApproval, -} from "../utils/reproducibility"; +} from '../utils/reproducibility'; +import { resolveApprovalSpender } from '../utils/routerAllowance'; // pre-fee (0x00) | returndata (no 0x02) const FLAGS = 0x00n; @@ -167,15 +167,15 @@ async function main() { 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); - await ensureRouterApproval( - signer, + + const swapApprovalSpender = await resolveApprovalSpender( + provider, ROUTER_POLYGON, TOKENS.AAVE_POLYGON, approvalSpender, + swapInput, ); const callData = routerIface.encodeFunctionData("swap", swapArgs( @@ -189,7 +189,7 @@ async function main() { { receiver: signerAddress, amount: feeAmount }, { target: swapTarget, - approvalSpender, + approvalSpender: swapApprovalSpender, outputToken: TOKENS.USDC_POLYGON_CIRCLE, value, minOutput: minBuyAmount, diff --git a/scripts/e2e/utils/contractTypes.ts b/scripts/e2e/utils/contractTypes.ts index a55c4cb..9bf07c2 100644 --- a/scripts/e2e/utils/contractTypes.ts +++ b/scripts/e2e/utils/contractTypes.ts @@ -1,5 +1,5 @@ /** - * TypeScript interfaces mirroring BungeeOpenRouter Solidity structs. + * TypeScript interfaces mirroring OpenRouter Solidity structs. * Field names and order must match the compiler ABI encoding. */ diff --git a/scripts/e2e/utils/reproducibility.ts b/scripts/e2e/utils/reproducibility.ts index 6e34f9f..62e49da 100644 --- a/scripts/e2e/utils/reproducibility.ts +++ b/scripts/e2e/utils/reproducibility.ts @@ -7,10 +7,9 @@ * Before each test leg these ensure: * 1. The router holds ≥ 20 wei of every token whose balance slot will be written. * - * Router→spender ERC-20 approvals are NOT pre-seeded here. `BungeeOpenRouter` - * sets max allowance inside `swap`, `bridge`, and `swapAndBridge` when needed. - * Modular `performActions` legs may still include inline `approve` actions in the - * same transaction when testing raw modular flows. + * 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 @@ -65,18 +64,3 @@ export async function ensureRouterNativeBalance( const tx = await signer.sendTransaction({ to: openRouter, value: SEED_WEI }); await tx.wait(); } - -/** - * No-op: router→spender approvals are handled by the contract on `swap` / - * `bridge` / `swapAndBridge`. Kept so existing e2e scripts do not need rewrites. - * - * @deprecated Pre-approval via a separate `performActions` tx is intentionally disabled. - */ -export async function ensureRouterApproval( - _signer: ethers.Wallet, - _openRouterAddress: string, - _token: string, - _spender: string, -): Promise { - // Intentionally empty — see module header. -} diff --git a/scripts/e2e/utils/routerAbi.ts b/scripts/e2e/utils/routerAbi.ts index 03dc419..7e2e702 100644 --- a/scripts/e2e/utils/routerAbi.ts +++ b/scripts/e2e/utils/routerAbi.ts @@ -1,5 +1,5 @@ /** - * ABI fragments for BungeeOpenRouter entrypoints used by e2e scripts. + * ABI fragments for OpenRouter entrypoints used by e2e scripts. * Struct field order must match the Solidity definitions. */ export const ROUTER_ABI = [ 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/src/BungeeOpenRouter.sol b/src/OpenRouter.sol similarity index 93% rename from src/BungeeOpenRouter.sol rename to src/OpenRouter.sol index 7c2744f..858c00d 100644 --- a/src/BungeeOpenRouter.sol +++ b/src/OpenRouter.sol @@ -3,6 +3,7 @@ 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"; @@ -11,12 +12,12 @@ import {CurrencyLib} from "./common/lib/CurrencyLib.sol"; import {RescueFundsLib} from "./common/lib/RescueFundsLib.sol"; import {RESCUE_ROLE} from "./common/AccessRoles.sol"; -/// @title BungeeOpenRouter +/// @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 BungeeOpenRouter is AccessControl, AllowanceHolderContext { +contract OpenRouter is AccessControl, AllowanceHolderContext { using SafeTransferLib for address; // ========================================================================= @@ -211,9 +212,15 @@ contract BungeeOpenRouter is AccessControl, AllowanceHolderContext { } } - // Approve swap spender - if (swapData.approvalSpender != address(0) && input.inputToken != CurrencyLib.NATIVE_TOKEN_ADDRESS) { - SafeTransferLib.safeApproveWithRetry(input.inputToken, swapData.approvalSpender, swapInput); + // 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); } } @@ -311,13 +318,20 @@ contract BungeeOpenRouter is AccessControl, AllowanceHolderContext { CurrencyLib.transfer(input.inputToken, fee.receiver, feeAmount); } + uint256 netAmount; + unchecked { + netAmount = input.inputAmount - feeAmount; + } + // Approve bridge spender - if (bridgeData.approvalSpender != address(0) && input.inputToken != CurrencyLib.NATIVE_TOKEN_ADDRESS) { - uint256 netAmount; - unchecked { - netAmount = input.inputAmount - feeAmount; - } - SafeTransferLib.safeApproveWithRetry(input.inputToken, bridgeData.approvalSpender, netAmount); + 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 @@ -379,8 +393,14 @@ contract BungeeOpenRouter is AccessControl, AllowanceHolderContext { } // Approve swap spender - if (swapData.approvalSpender != address(0) && input.inputToken != CurrencyLib.NATIVE_TOKEN_ADDRESS) { - SafeTransferLib.safeApproveWithRetry(input.inputToken, swapData.approvalSpender, swapInput); + 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); } } @@ -424,8 +444,14 @@ contract BungeeOpenRouter is AccessControl, AllowanceHolderContext { } // Approve bridge spender - if (bridgeData.approvalSpender != address(0) && token != CurrencyLib.NATIVE_TOKEN_ADDRESS) { - SafeTransferLib.safeApproveWithRetry(token, bridgeData.approvalSpender, amount); + 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 @@ -568,10 +594,10 @@ contract BungeeOpenRouter is AccessControl, AllowanceHolderContext { } /** - * @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) + * @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`. 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/test/combined/BungeeOpenRouterV2UncheckedBridge.t.sol b/test/combined/OpenRouterV2UncheckedBridge.t.sol similarity index 95% rename from test/combined/BungeeOpenRouterV2UncheckedBridge.t.sol rename to test/combined/OpenRouterV2UncheckedBridge.t.sol index 50635d0..e6c5f81 100644 --- a/test/combined/BungeeOpenRouterV2UncheckedBridge.t.sol +++ b/test/combined/OpenRouterV2UncheckedBridge.t.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.34; -import {BungeeOpenRouter as Router} from "../../src/BungeeOpenRouter.sol"; -import {BungeeOpenRouterV2UncheckedTestBase} from "./BungeeOpenRouterV2UncheckedTestBase.sol"; +import {OpenRouter as Router} from "../../src/OpenRouter.sol"; +import {OpenRouterV2UncheckedTestBase} from "./OpenRouterV2UncheckedTestBase.sol"; -contract BungeeOpenRouterV2UncheckedBridgeTest is BungeeOpenRouterV2UncheckedTestBase { +contract OpenRouterV2UncheckedBridgeTest is OpenRouterV2UncheckedTestBase { function test_bridge_erc20() public { _deal(address(inputToken), USER, INPUT_AMOUNT); _approveInputToken(INPUT_AMOUNT); diff --git a/test/combined/BungeeOpenRouterV2UncheckedSwap.t.sol b/test/combined/OpenRouterV2UncheckedSwap.t.sol similarity index 97% rename from test/combined/BungeeOpenRouterV2UncheckedSwap.t.sol rename to test/combined/OpenRouterV2UncheckedSwap.t.sol index bf60bb4..b36aca1 100644 --- a/test/combined/BungeeOpenRouterV2UncheckedSwap.t.sol +++ b/test/combined/OpenRouterV2UncheckedSwap.t.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.34; -import {BungeeOpenRouter as Router} from "../../src/BungeeOpenRouter.sol"; -import {BungeeOpenRouterV2UncheckedTestBase} from "./BungeeOpenRouterV2UncheckedTestBase.sol"; +import {OpenRouter as Router} from "../../src/OpenRouter.sol"; +import {OpenRouterV2UncheckedTestBase} from "./OpenRouterV2UncheckedTestBase.sol"; -contract BungeeOpenRouterV2UncheckedSwapTest is BungeeOpenRouterV2UncheckedTestBase { +contract OpenRouterV2UncheckedSwapTest is OpenRouterV2UncheckedTestBase { function test_swapWithReturnData() public { _deal(address(inputToken), USER, INPUT_AMOUNT); _deal(address(outputToken), address(swapTarget), SWAP_OUTPUT_AMOUNT); diff --git a/test/combined/BungeeOpenRouterV2UncheckedSwapAndBridge.t.sol b/test/combined/OpenRouterV2UncheckedSwapAndBridge.t.sol similarity index 96% rename from test/combined/BungeeOpenRouterV2UncheckedSwapAndBridge.t.sol rename to test/combined/OpenRouterV2UncheckedSwapAndBridge.t.sol index f9376b8..3eddd22 100644 --- a/test/combined/BungeeOpenRouterV2UncheckedSwapAndBridge.t.sol +++ b/test/combined/OpenRouterV2UncheckedSwapAndBridge.t.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.34; -import {BungeeOpenRouter as Router} from "../../src/BungeeOpenRouter.sol"; -import {BungeeOpenRouterV2UncheckedTestBase} from "./BungeeOpenRouterV2UncheckedTestBase.sol"; +import {OpenRouter as Router} from "../../src/OpenRouter.sol"; +import {OpenRouterV2UncheckedTestBase} from "./OpenRouterV2UncheckedTestBase.sol"; -contract BungeeOpenRouterV2UncheckedSwapAndBridgeTest is BungeeOpenRouterV2UncheckedTestBase { +contract OpenRouterV2UncheckedSwapAndBridgeTest is OpenRouterV2UncheckedTestBase { enum FeeMode { None, Pre, diff --git a/test/combined/BungeeOpenRouterV2UncheckedTestBase.sol b/test/combined/OpenRouterV2UncheckedTestBase.sol similarity index 98% rename from test/combined/BungeeOpenRouterV2UncheckedTestBase.sol rename to test/combined/OpenRouterV2UncheckedTestBase.sol index 1704097..1b3fda1 100644 --- a/test/combined/BungeeOpenRouterV2UncheckedTestBase.sol +++ b/test/combined/OpenRouterV2UncheckedTestBase.sol @@ -4,10 +4,10 @@ pragma solidity 0.8.34; import {Test} from "forge-std/Test.sol"; import {ERC20} from "solady/src/tokens/ERC20.sol"; -import {BungeeOpenRouter as Router} from "../../src/BungeeOpenRouter.sol"; +import {OpenRouter as Router} from "../../src/OpenRouter.sol"; import {ALLOWANCE_HOLDER, IAllowanceHolder} from "../../src/common/interfaces/IAllowanceHolder.sol"; -abstract contract BungeeOpenRouterV2UncheckedTestBase is Test { +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; diff --git a/test/poc/OneInchCctpOpenRouterPoC.t.sol b/test/poc/OneInchCctpOpenRouterPoC.t.sol index 8065379..d5f939e 100644 --- a/test/poc/OneInchCctpOpenRouterPoC.t.sol +++ b/test/poc/OneInchCctpOpenRouterPoC.t.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.34; import {Test} from "forge-std/Test.sol"; import {ERC20} from "solady/src/tokens/ERC20.sol"; -import {BungeeOpenRouter as Router} from "../../src/BungeeOpenRouter.sol"; +import {OpenRouter as Router} from "../../src/OpenRouter.sol"; import {ALLOWANCE_HOLDER, IAllowanceHolder} from "../../src/common/interfaces/IAllowanceHolder.sol"; interface ITokenMessengerV2 { diff --git a/test/poc/OpenOceanAcrossOpenRouterPoC.t.sol b/test/poc/OpenOceanAcrossOpenRouterPoC.t.sol index 691ae10..57522fd 100644 --- a/test/poc/OpenOceanAcrossOpenRouterPoC.t.sol +++ b/test/poc/OpenOceanAcrossOpenRouterPoC.t.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.34; import {Test} from "forge-std/Test.sol"; import {ERC20} from "solady/src/tokens/ERC20.sol"; -import {BungeeOpenRouter as Router} from "../../src/BungeeOpenRouter.sol"; +import {OpenRouter as Router} from "../../src/OpenRouter.sol"; import {AcrossERC20AmountManipulator} from "../../src/manipulators/AcrossERC20AmountManipulator.sol"; interface ISpokePool { diff --git a/test/poc/OpenOceanStargateNativeOpenRouterPoC.t.sol b/test/poc/OpenOceanStargateNativeOpenRouterPoC.t.sol index 74b9545..f9ae56a 100644 --- a/test/poc/OpenOceanStargateNativeOpenRouterPoC.t.sol +++ b/test/poc/OpenOceanStargateNativeOpenRouterPoC.t.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.34; import {Test} from "forge-std/Test.sol"; import {ERC20} from "solady/src/tokens/ERC20.sol"; -import {BungeeOpenRouter as Router} from "../../src/BungeeOpenRouter.sol"; +import {OpenRouter as Router} from "../../src/OpenRouter.sol"; import {MathManipulator} from "../../src/manipulators/MathManipulator.sol"; interface IOpenOceanExchangeV2 {