Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 2 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ permissions: {}

on:
push:
branches:
- main
pull_request:
workflow_dispatch:

Expand All @@ -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

Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
332 changes: 149 additions & 183 deletions OPENROUTER.md

Large diffs are not rendered by default.

14 changes: 7 additions & 7 deletions OPENROUTER_ASSUMPTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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:

Expand All @@ -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:

Expand Down Expand Up @@ -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:

Expand Down Expand Up @@ -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.
12 changes: 6 additions & 6 deletions OPENROUTER_CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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`.
Expand Down Expand Up @@ -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.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/**
* Deployment script for BungeeOpenRouter.
* Deployment script for OpenRouter.
*
* Usage:
* npx hardhat run scripts/deploy/deployBungeeOpenRouter.ts --network <network>
* npx hardhat run scripts/deploy/deployOpenRouter.ts --network <network>
*
* Required env vars:
* DEPLOYER_PRIVATE_KEY — deployer wallet private key
Expand All @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion scripts/e2e/approveViaModular.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
14 changes: 11 additions & 3 deletions scripts/e2e/arbitrum/performExecution.postFee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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',
Expand All @@ -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,
Expand Down
8 changes: 4 additions & 4 deletions scripts/e2e/arbitrum/performModularExecution.postFee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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);
Expand Down
14 changes: 11 additions & 3 deletions scripts/e2e/cctp/bridge.preFee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -93,15 +94,22 @@ async function main(): Promise<void> {
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...');
Expand Down
24 changes: 19 additions & 5 deletions scripts/e2e/cctp/performExecution.postFee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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',
Expand All @@ -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,
),
);
Expand Down
14 changes: 11 additions & 3 deletions scripts/e2e/cctp/performExecution.preFee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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));
Expand Down
Loading