Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
c7d0c1c
feat: simple bridge function
sebastiantf May 15, 2026
e85dba5
feat: simple bridge relay test
sebastiantf May 15, 2026
e8e1074
feat: approval, balance dust during gas tests
sebastiantf May 15, 2026
17dc785
feat: introduce flags for fee and balance handling in swap functions
sebastiantf May 15, 2026
0685308
fix: swap output check after swap
sebastiantf May 15, 2026
c45a025
feat: calldata optimisations
arthcp May 15, 2026
5bed333
Merge commit '06853084fee1a7a21e1ac178c57432a99ad71f3e' into refactor…
arthcp May 15, 2026
82faec8
feat: requestHash event
sebastiantf May 15, 2026
00e3801
refactor: tests
sebastiantf May 15, 2026
08d78aa
refactor: tests
sebastiantf May 18, 2026
d24bb5a
fix: swap receiver
sebastiantf May 18, 2026
22c39b6
fix: sum amount + bd.value
sebastiantf May 18, 2026
2dc0080
fix: comments
sebastiantf May 18, 2026
c88af75
test: fix stargate tests
sebastiantf May 18, 2026
29ab099
fix: stargate tests
sebastiantf May 18, 2026
362b8be
feat: kyberswap, 0x swap scripts, fix swap scripts
sebastiantf May 18, 2026
1313cc4
feat: rescueFunds
sebastiantf May 18, 2026
c58077b
refactor: remove monolithic exec code
sebastiantf May 19, 2026
85a7d2b
feat: slither
sebastiantf May 19, 2026
9775932
fix: fork tests
arthcp May 19, 2026
3da9283
feat: mock tests
arthcp May 19, 2026
783c885
chore: format
arthcp May 19, 2026
01eb83b
refactor: renames, refactors, reorders
sebastiantf May 19, 2026
9a19a3c
build: change solc version
sebastiantf May 19, 2026
419f396
feat: balance and return data variants
arthcp May 19, 2026
578ef44
refactor: reorder function params
sebastiantf May 19, 2026
d84d648
refactor: remove old contracts, rename router, move to src root
sebastiantf May 19, 2026
b3cf268
refactor: comments
sebastiantf May 19, 2026
4eb6ab2
refactor: remove performActions return
sebastiantf May 19, 2026
e7a81b5
refactor: check and set max approval
sebastiantf May 19, 2026
5307f1b
feat: agent docs
arthcp May 19, 2026
fac7a9c
Merge remote-tracking branch 'origin/refactor/monolithic' into tests
arthcp May 19, 2026
cd64732
Merge pull request #5 from SocketDotTech/tests
arthcp May 19, 2026
8c3f4f8
fix: tests
sebastiantf May 19, 2026
2c69a6f
Merge branch 'refactor/monolithic' into refactor/infinite-approval
sebastiantf May 19, 2026
5a2b25a
test: set approvalSpender zero if enough allowance
sebastiantf May 19, 2026
b51be89
ci: remove fmt
sebastiantf May 19, 2026
d05a66b
ci: run on push to main only
sebastiantf May 19, 2026
d313ed2
refactor: rename contract name
sebastiantf May 19, 2026
02494d1
docs: update
sebastiantf May 19, 2026
2859af9
Merge pull request #6 from SocketDotTech/refactor/infinite-approval
sebastiantf May 19, 2026
7ba76fe
feat: create3
sebastiantf May 20, 2026
4a7b732
chore: comments
sebastiantf May 20, 2026
e2a09a5
test: allowance holder pull fork gas test
sebastiantf May 20, 2026
bce867e
chore: comments, function renames
sebastiantf May 25, 2026
c1c4b4a
chore: prev. audit comments
sebastiantf May 25, 2026
220c7a7
fix: prev. audit comment
sebastiantf May 25, 2026
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
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Project Context

For OpenRouter contract work read files which are relevant for the task. general context - `OPENROUTER_CONTEXT.md`
assumptions - `OPENROUTER_ASSUMPTIONS.md` first.

Main ship target is `src/combined/OpenRouterV2Unchecked.sol`. If its ABI changes, update the backend encoders in `bungee-backend/src/modules/dex/utils.ts` and `bungee-backend/src/modules/router/utils/directQuotesOpenRouter.ts`.
367 changes: 190 additions & 177 deletions OPENROUTER.md

Large diffs are not rendered by default.

248 changes: 248 additions & 0 deletions OPENROUTER_ASSUMPTIONS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
# OpenRouter Assumptions

Last reviewed: 2026-05-19.

Scope: `src/combined/OpenRouterV2Unchecked.sol`.

This document captures the assumptions that make the unchecked OpenRouter safe to operate. Many of these are business and integration assumptions, not guarantees enforced by the contract.

## Source Of Truth

`OpenRouterV2Unchecked` intentionally removes backend signature verification, nonces, and deadlines. Public entrypoints can be called by anyone.

Current checked-in public surface:

- `swap(...)`
- `swapAndBridge(...)`
- `bridge(...)`
- `performActions()(...)`
- `rescueFunds(...)`

`OPENROUTER_CONTEXT.md` and `scripts/e2e/utils/routerAbi.ts` may mention `performExecution(...)`; verify against the Solidity file before relying on that ABI.

## Enforcement Classes

Use this distinction when reviewing any route or integration:

- On-chain enforced: checked directly by the router.
- Operationally enforced: must be true because frontend, backend, deploy config, or runbooks enforce it.
- Policy assumption: not enforced by code. If it becomes false, the unchecked router can become unsafe.

## Critical Business Assumptions

### Router Never Holds Durable Funds

The router may temporarily hold funds during one transaction, but it should not end routes with meaningful token or native balances.

Failure mode: `performActions()` lets any caller make the router call arbitrary contracts. If the router holds ERC20s, native ETH, bridged refunds, swap dust, rebates, or protocol refunds, a public caller can move or approve those assets through modular actions before owner rescue.

Operational requirements:

- Do not use the router as a treasury, escrow, settlement account, refund address, or fee vault.
- Route calldata should send final assets to the user, bridge, or fee recipient in the same transaction.
- Monitor router token/native balances and treat non-zero balances as an incident or stuck-funds condition.
- Owner rescue is an operational recovery tool, not a security boundary.

### Users Never Directly Approve The Router

Users must not give persistent ERC20, Permit2, ERC721, ERC1155, or protocol-specific approvals directly to the router.

Failure mode: if a user directly approves the router, any caller can use `performActions()` to make the router call `transferFrom`, `approve`, or equivalent privileged token functions against that user allowance.

Operational requirements:

- User ERC20 approvals should go to 0x AllowanceHolder, not OpenRouter.
- UI copy and wallet flows must never ask users to approve OpenRouter directly.
- Monitoring should flag direct allowances from users to the router.
- If a direct approval is discovered, revoke it before treating that user as safe.

### Router Has No Privileged Role On Other Contracts

No external contract should treat OpenRouter as a privileged actor unless every public caller is allowed to exercise that privilege.

Failure mode: if another contract has `onlyRouter`, allowlists the router, grants it minter/burner/pauser/admin/operator/bridge-agent permissions, or keys permissions off `msg.sender == router`, any caller can exercise that role through modular execution.

Operational requirements:

- Do not grant OpenRouter roles in bridges, vaults, tokens, staking systems, receivers, relayers, or settlement contracts.
- Do not whitelist OpenRouter in downstream contracts as a trusted caller unless the called operation is safe for arbitrary public callers.
- Review new integrations for hidden trust checks against `msg.sender`.

### Router Is Not A User-Intent Authority

The unchecked router does not prove that a route reflects user intent. It only executes calldata.

Failure mode: a malicious UI or compromised backend can make the user call `AllowanceHolder.exec` with calldata that pays an attacker, charges an arbitrary fee, bridges to a wrong recipient, or approves a malicious spender.

Operational requirements:

- The frontend/backend must validate recipients, fee receivers, fee amounts, swap targets, bridge targets, approval spenders, destination chain/domain, bridge min amounts, and refund addresses before presenting a transaction.
- Wallet simulation and transaction review should show the actual route effects where possible.
- `requestHash` is only an event correlation id. It does not enforce uniqueness, replay protection, or user consent.

## Fund Pull Assumptions

### ERC20 Inputs Use AllowanceHolder

ERC20 input safety depends on 0x AllowanceHolder transient allowance scoping plus `_msgSender() == input.user`.

On-chain enforced:

- `_pullFromUser` reverts unless `_msgSender() == input.user` for ERC20 inputs.
- When called through AllowanceHolder, `_msgSender()` is decoded from the appended user address.

Operational assumptions:

- The user calls `AllowanceHolder.exec(operator, token, amount, target, data)`.
- `operator` is the router.
- `target` is the router.
- `token` and `amount` match the route input.
- The user has a persistent approval to AllowanceHolder, not to the router.

Failure modes:

- Direct ERC20 calls to the router fail because `_msgSender()` is not the user.
- Bad AH calldata can still execute a bad route if the user submits it.
- AH protects fund pulling for the route input, but it does not validate swap/bridge semantics.

### Native Inputs Are Not User-Bound

Native-token input routes only check that `msg.value >= inputAmount`.

Failure mode: `input.user` is not authenticated for native routes. Anyone can submit native routes if they provide the ETH. This is usually acceptable because the caller funds the transaction, but downstream analytics must not treat `input.user` as authenticated identity for native paths.

Operational requirements:

- Native route attribution should come from transaction signer / AH sender / product context, not only `input.user`.
- Excess `msg.value` is not automatically refunded by the router.

## Execution Assumptions

### External Targets Are Trusted Per Route

The router does not whitelist swap targets, bridge targets, approval spenders, manipulators, receivers, or fee recipients.

Failure modes:

- Malicious swap target can consume approved input and return misleading returndata.
- Malicious bridge target can consume approved output or native value.
- Malicious approval spender can use allowance after the route if allowance remains and the router later receives the same token.
- Malicious fee receiver can reject native fee transfers and revert the route.

Operational requirements:

- Backend/frontend must maintain target and spender allowlists or equivalent route validation.
- Approval spender should be the minimum necessary protocol spender.
- Prefer route patterns that leave no router balance and no meaningful residual allowance.

### Swap Output Measurement Matches The Aggregator

The router supports two output modes:

- Returndata mode: decode a 32-byte word at `swapData.returnDataWordOffset`.
- Balance-delta mode: measure `balanceOf(outputReceiver)` before and after the swap.

Failure modes:

- Returndata mode is unsafe if the target return word is not the actual output amount.
- Balance-delta mode is unsafe if unrelated balance changes occur during the call, or if the token has rebasing/fee-on-transfer behavior that breaks expected deltas.
- In standalone pre-fee/no-fee swaps, the swap calldata must send output directly to `receiver`; the router will not forward output afterward.
- In standalone post-fee swaps and all `swapAndBridge` paths, the swap output must land on the router.

Operational requirements:

- Choose output mode per aggregator and route.
- Verify `returnDataWordOffset` against the concrete swap target ABI.
- Verify output recipient encoded in `swapCallData` matches the router mode.
- Treat `minOutput` as gross swap output, not guaranteed net-to-user output after post-fee or bridge fees.

### Fee Semantics Are Caller-Defined

The router does not enforce fee policy.

Assumptions:

- Pre-fee amounts are denominated in the input token.
- Post-fee amounts are denominated in the output token.
- `fee.receiver` is trusted and product-approved.
- `fee.amount` is within product policy.

Failure modes:

- A malicious caller can set an arbitrary fee receiver and amount if the user submits the calldata.
- Post-fee is applied after gross `minOutput` validation, so net user proceeds can be lower than `minOutput`.

### Bridge Calldata Is Semantically Correct

The router does not understand bridge-specific fields.

Assumptions:

- Destination chain/domain is correct.
- Recipient is correct.
- Refund address is not the router unless intentionally safe.
- Bridge min amount / slippage fields are correct.
- Bridge fee quote and native fee buffer are current enough.
- Token and amount fields in calldata match the route.

Failure modes:

- `bridge()` performs no runtime amount splicing; the amount must already be encoded.
- `swapAndBridge()` can splice one 32-byte amount word only.
- The bridge-value flag forwards `finalAmount + bridgeData.value` as native value. It must only be used when the bridge expects the bridged asset itself as native value plus a static fee.
- Excess native fee behavior depends on the bridge target and refund address, not OpenRouter.

## Modular Execution Assumptions

`performActions()` is the broadest surface. It makes the router a public generic call executor.

Assumptions:

- The router has no durable funds.
- No user has directly approved the router.
- No external contract gives the router privileged rights.
- Each action target is safe for the router to call.
- Splice offsets and lengths are generated by trusted tooling.
- Actions that are splice sources store their returndata.

Failure modes:

- Any public caller can transfer, approve, or spend assets already held by the router.
- Any public caller can exercise downstream privileges granted to the router.
- `CALL_WITH_NATIVE` can spend native ETH already sitting in the router.
- Invalid `callType` values fall through to normal `CALL`; encoders must emit only known call types.
- Splices are bounds-checked but not semantically validated. A bad splice can write a valid but wrong bridge amount, recipient field, fee field, or payload word.

## Token Assumptions

Assumptions:

- ERC20s follow sane `transfer`, `transferFrom`, `approve`, and `balanceOf` behavior.
- The native token sentinel is exactly `0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE`.
- Tokens do not rebase or charge transfer fees in ways that invalidate route amounts, unless route tooling explicitly accounts for that.
- Approval reset/retry behavior in Solady `safeApproveWithRetry` is acceptable for the token.

Failure modes:

- Fee-on-transfer tokens can cause bridge approvals or calldata amounts to exceed actual received balances.
- Rebasing tokens can corrupt balance-delta output measurement.
- Non-standard tokens can revert, return false, or have allowance quirks.

## Operational Checklist

Before enabling a route or integration, confirm:

- Users approve AllowanceHolder only.
- The router has no direct user allowances.
- The router has no privileged roles on any touched contract.
- The router is not used as recipient, refund address, treasury, or settlement vault unless public draining is acceptable.
- Swap target, bridge target, approval spenders, manipulators, fee receiver, and receiver are validated.
- Swap output mode and `returnDataWordOffset` are correct for the aggregator.
- Standalone swap recipient is correct for pre-fee/no-fee versus post-fee mode.
- Bridge calldata encodes the correct recipient, destination, min amount, refund address, and fees.
- Bridge amount splice offset is correct for the exact calldata shape.
- Native `msg.value` covers input amount plus all downstream native call values.
- Excess native value and bridge refunds do not end up on the router.
- Monitoring exists for router balances, direct allowances to router, and unexpected downstream roles.

If any critical business assumption is false, do not rely on `OpenRouterV2Unchecked` as-is. Add access control, use a signed variant, or remove the downstream privilege/funds/allowance that makes the public call surface dangerous.
108 changes: 108 additions & 0 deletions OPENROUTER_CONTEXT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# OpenRouter Contract Context

Last researched: 2026-05-18.

Main ship target:

- `src/combined/OpenRouterV2Unchecked.sol`

Use `src/combined/OpenRouterV2.sol` as the signed sibling/reference, but the backend branch researched here targets the unchecked ABI.

## V2Unchecked Surface

`OpenRouterV2Unchecked` removes backend signature verification, nonce, and deadline fields. Fund safety for ERC20 inputs depends on 0x AllowanceHolder transient approvals plus `_msgSender() == input.user` in `_pullFromUser`.

External entrypoints:

- `performExecution(bytes32 requestHash, MonolithicExecution exec, bytes swapCallData, bytes bridgeCallData)`
- Pulls via AllowanceHolder.
- Optional pre-fee, optional swap, optional post-fee.
- Bridges with optional single amount-word splice controlled by flags.
- Bit 0 fee flag is ignored here; fee placement comes from `preFee` and `postFee`.
- `swap(bytes32 requestHash, InputData input, address receiver, uint256 flags, FeeData fee, SwapData swapData, bytes swapCallData)`
- Same-chain DEX path.
- Pre-fee/no-fee swaps can send output directly to `receiver`.
- Post-fee swaps send output to the router, then the router skims fee and forwards net.
- `swapAndBridge(bytes32 requestHash, InputData input, uint256 flags, FeeData fee, SwapData swapData, bytes swapCallData, BridgeData bridgeData, bytes bridgeCallData)`
- Swap output always lands on the router so it can be bridged.
- Supports runtime bridge amount splice and native bridge-value mode via flags.
- `bridge(bytes32 requestHash, InputData input, FeeData fee, BridgeData bridgeData, bytes bridgeCallData)`
- Direct bridge, no swap.
- No runtime splice; bridge amount must already be encoded in `bridgeCallData`.
- `performActions()(bytes32 requestHash, Action[] actions)`
- Generic action loop with packed action metadata and packed splices.

## Flags

Flag constants in `OpenRouterV2Unchecked.sol`:

- `0x01` - post-swap fee for `swap` and `swapAndBridge`; clear means pre-fee from input. Ignored by `performExecution`.
- `0x02` - measure swap output by `balanceOf` delta; clear means decode return word at `SwapData.returnDataWordOffset`.
- `0x04` - bridge `msg.value = finalAmount + BridgeData.value`; used for native bridge assets.
- `0x08` - splice `finalAmount` into `bridgeCallData`.
- Bits `16..31` - byte offset for the bridge amount splice when `0x08` is set.

Backend constants live in both:

- `bungee-backend/src/modules/dex/dex.config.ts`
- `bungee-backend/src/modules/router/router.config.ts`

Keep those masks and deployed addresses in sync with this contract.

## Modular Packing

`Action.actionInfo` is packed as:

```text
uint8(callType) | (storeResult ? 1 << 8 : 0) | (uint160(target) << 16)
```

`Action.splices[]` entries are packed as:

```text
sourceActionIndex | (srcOffset << 64) | (dstOffset << 128) | (length << 192)
```

`CallType.CALL_WITH_NATIVE` treats the first 32 bytes of `action.data` as the call value and the remaining bytes as calldata. PoCs use this for native fee transfers and Stargate native sends.

## Current PoCs

- `test/poc/OpenOceanAcrossOpenRouterPoC.t.sol`
- Modular OpenOcean USDC -> WETH swap.
- `AcrossERC20AmountManipulator` derives the Across output amount.
- Splices swap output and derived output into `SpokePool.deposit`.
- `test/poc/OpenOceanStargateNativeOpenRouterPoC.t.sol`
- Modular OpenOcean USDC -> native ETH.
- `MathManipulator` derives fee, post-fee amount, and bridge amount.
- Uses `CALL_WITH_NATIVE` and splices Stargate `amountLD`.
- `test/poc/OneInchCctpOpenRouterPoC.t.sol`
- CCTP-oriented PoC.

Fork tests need RPC env vars and sometimes block pins. Example:

```bash
ARBITRUM_RPC=... ARBITRUM_FORK_BLOCK=461716058 forge test --match-path test/poc/OpenOceanAcrossOpenRouterPoC.t.sol -vv
```

## Backend ABI Expectations

The backend encodes the unchecked ABI in:

- `bungee-backend/src/modules/dex/utils.ts`
- `swap(...)`
- `AllowanceHolder.exec(...)`
- `bungee-backend/src/modules/router/utils/directQuotesOpenRouter.ts`
- `bridge(...)`
- `swapAndBridge(...)`
- `AllowanceHolder.exec(...)`

If the Solidity ABI changes, update those hard-coded ABI strings first. Direct DEX and direct bridge quote builders depend on them.

## Gotchas

- ERC20 inputs must be submitted through 0x AllowanceHolder, not directly to the router, or `_msgSender() == user` fails.
- Native input paths send ETH with the outer `AllowanceHolder.exec` call; no ERC20 pull happens.
- `bridge()` cannot splice runtime amounts. Use `swapAndBridge()` when bridge calldata needs the live swap output.
- `swapAndBridge()` uses balance-delta output measurement in backend builders today.
- `performExecution` and `swapAndBridge` share helpers but have different fee semantics.
- Production use of `OpenRouterV2Unchecked` needs an operational access-control decision; the contract itself has no signature or nonce checks.
Loading