Skip to content
Draft
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: 1 addition & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@
[submodule "sdks/universal-router-sdk/lib/universal-router"]
path = sdks/universal-router-sdk/lib/universal-router
url = https://github.com/Uniswap/universal-router
branch = oz-fixes
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
],
"resolutions": {
"@manypkg/cli@^0.19.2": "patch:@manypkg/cli@npm%3A0.19.2#./.yarn/patches/@manypkg-cli-npm-0.19.2-ea52ff91d4.patch",
"eslint-config-prettier": "^9.1.0"
"eslint-config-prettier": "^9.1.0",
"@uniswap/v4-sdk": "workspace:sdks/v4-sdk",
"@uniswap/sdk-core": "workspace:sdks/sdk-core"
}
}
2 changes: 1 addition & 1 deletion sdks/router-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"@uniswap/swap-router-contracts": "^1.3.0",
"@uniswap/v2-sdk": "^4.16.0",
"@uniswap/v3-sdk": "^3.26.0",
"@uniswap/v4-sdk": "^1.22.0"
"@uniswap/v4-sdk": "workspace:*"
},
"devDependencies": {
"@types/jest": "^24.0.25",
Expand Down
282 changes: 282 additions & 0 deletions sdks/universal-router-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,285 @@ Run forge integration tests
forge install
yarn test:forge
```

## Per-Hop Slippage Protection (V4 Routes)

Universal Router v2.1 adds granular slippage protection for multi-hop V4 swaps. Additionally to checking slippage at the end of a route, you can now verify that each individual hop doesn't exceed a maximum price limit.

### How It Works

For V4 multi-hop swaps, you can provide a `maxHopSlippage` array in your swap options:

```typescript
import { SwapRouter } from '@uniswap/universal-router-sdk'
import { BigNumber } from 'ethers'
import { Percent } from '@uniswap/sdk-core'

const swapOptions = {
slippageTolerance: new Percent(50, 10000), // 0.5% overall slippage
recipient: '0x...',
deadline: Math.floor(Date.now() / 1000) + 60 * 20,
// Optional: per-hop slippage protection for V4 routes
maxHopSlippage: [
BigNumber.from('1010000000000000000'), // Hop 0: max price 1.01 (1% slippage)
BigNumber.from('2500000000000000000000'), // Hop 1: max price 2500
]
}

const { calldata, value } = SwapRouter.swapCallParameters(trade, swapOptions)
```

### Price Calculation

The slippage is expressed as a **price** with 18 decimals of precision:
- **For Exact Input**: `price = amountIn * 1e18 / amountOut`
- **For Exact Output**: `price = amountIn * 1e18 / amountOut`

If the calculated price exceeds `maxHopSlippage[i]`, the transaction will revert with:
- `V4TooLittleReceivedPerHop` for exact input swaps
- `V4TooMuchRequestedPerHop` for exact output swaps

### Example: USDC → DAI → WETH

```typescript
// 2-hop swap: USDC → DAI → WETH
const swapOptions = {
slippageTolerance: new Percent(100, 10000), // 1% overall
recipient: userAddress,
deadline,
maxHopSlippage: [
BigNumber.from('1010000000000000000'), // Hop 0: USDC→DAI, max 1% slippage
BigNumber.from('2500000000000000000000'), // Hop 1: DAI→WETH, max price 2500 DAI/WETH
]
}
```

### Benefits

1. **MEV Protection**: Prevents sandwich attacks on individual hops
2. **Route Quality**: Ensures each segment of a multi-hop route meets expectations
3. **Granular Control**: Different slippage tolerance for different pairs in a route

### Backward Compatibility

- If `maxHopSlippage` is not provided or is an empty array, only overall slippage is checked (backward compatible)
- The feature only applies to V4 routes; V2 and V3 routes ignore this parameter
- Mixed routes with V4 sections will apply per-hop checks only to the V4 portions

## Signed Routes (Universal Router v2.1)

Universal Router v2.1 supports EIP712-signed route execution, enabling gasless transactions and intent-based trading.

**Important**: The SDK does not perform signing. It provides utilities to prepare EIP712 payloads and encode signed calldata. You sign with your own mechanism (wallet, KMS, hardware, etc.).

### Basic Flow

```typescript
import { SwapRouter, NONCE_SKIP_CHECK } from '@uniswap/universal-router-sdk'
import { Wallet } from '@ethersproject/wallet'

const wallet = new Wallet('0x...')
const chainId = 1
const routerAddress = '0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD'
const deadline = Math.floor(Date.now() / 1000) + 60 * 20

// 1. Generate regular swap calldata
const { calldata, value } = SwapRouter.swapCallParameters(trade, {
slippageTolerance: new Percent(50, 10000),
recipient: wallet.address,
deadline,
})

// 2. Get EIP712 payload to sign
const payload = SwapRouter.getExecuteSignedPayload(
calldata,
{
intent: '0x' + '0'.repeat(64), // Application-specific intent
data: '0x' + '0'.repeat(64), // Application-specific data
sender: wallet.address, // Or address(0) to skip sender verification
},
deadline,
chainId,
routerAddress
)

// 3. Sign externally (wallet/KMS/hardware)
const signature = await wallet._signTypedData(payload.domain, payload.types, payload.value)

// 4. Encode for executeSigned()
const { calldata: signedCalldata, value: signedValue } = SwapRouter.encodeExecuteSigned(
calldata,
signature,
{
intent: payload.value.intent,
data: payload.value.data,
sender: payload.value.sender,
nonce: payload.value.nonce, // Must match what was signed
},
deadline,
BigNumber.from(value)
)

// 5. Submit transaction
await wallet.sendTransaction({
to: routerAddress,
data: signedCalldata,
value: signedValue,
})
```

### Nonce Management

- **Random nonce (default)**: Omit `nonce` parameter - SDK generates random nonce
- **Skip nonce check**: Use `NONCE_SKIP_CHECK` sentinel to allow signature reuse
- **Custom nonce**: Provide your own nonce for ordering

```typescript
import { NONCE_SKIP_CHECK } from '@uniswap/universal-router-sdk'

// Reusable signature (no nonce check)
const payload = SwapRouter.getExecuteSignedPayload(
calldata,
{
intent: '0x...',
data: '0x...',
sender: '0x0000000000000000000000000000000000000000', // Skip sender verification too
nonce: NONCE_SKIP_CHECK, // Allow signature reuse
},
deadline,
chainId,
routerAddress
)
```

### Sender Verification

- **Verify sender**: Pass the actual sender address (e.g., `wallet.address`)
- **Skip verification**: Pass `'0x0000000000000000000000000000000000000000'`

The SDK automatically sets `verifySender` based on whether sender is address(0).

## Cross-Chain Bridging with Across (Universal Router v2.1)

Universal Router v2.1 integrates with Across Protocol V3 to enable seamless cross-chain bridging after swaps. This allows you to swap tokens on one chain and automatically bridge them to another chain in a single transaction.

### Basic Usage

```typescript
import { SwapRouter } from '@uniswap/universal-router-sdk'
import { BigNumber } from 'ethers'

// 1. Prepare your swap (e.g., USDC → WETH on mainnet)
const { calldata, value } = SwapRouter.swapCallParameters(
trade,
swapOptions,
[
{
// Bridge configuration
depositor: userAddress,
recipient: userAddress, // Recipient on destination chain
inputToken: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', // WETH mainnet
outputToken: '0x4200000000000000000000000000000000000006', // WETH optimism
inputAmount: BigNumber.from('1000000000000000000'), // 1 WETH
outputAmount: BigNumber.from('990000000000000000'), // 0.99 WETH (with fees)
destinationChainId: 10, // Optimism
exclusiveRelayer: '0x0000000000000000000000000000000000000000',
quoteTimestamp: Math.floor(Date.now() / 1000),
fillDeadline: Math.floor(Date.now() / 1000) + 3600,
exclusivityDeadline: 0,
message: '0x',
useNative: false,
}
]
)
```

### Swap + Bridge Example

```typescript
// Swap USDC to WETH, then bridge WETH to Optimism
const bridgeParams = {
depositor: userAddress,
recipient: userAddress, // Can be different address on destination
inputToken: WETH_MAINNET,
outputToken: WETH_OPTIMISM,
inputAmount: CONTRACT_BALANCE, // Use entire swap output
outputAmount: expectedOutputAmount,
destinationChainId: 10,
exclusiveRelayer: '0x0000000000000000000000000000000000000000',
quoteTimestamp: Math.floor(Date.now() / 1000),
fillDeadline: Math.floor(Date.now() / 1000) + 3600,
exclusivityDeadline: 0,
message: '0x', // Optional message to execute on destination
useNative: false, // Set to true to bridge native ETH
}

const { calldata, value } = SwapRouter.swapCallParameters(
trade,
swapOptions,
[bridgeParams] // Array of bridge operations
)
```

### Using CONTRACT_BALANCE

When bridging after a swap, you often don't know the exact output amount. Use `CONTRACT_BALANCE` to bridge the entire contract balance:

```typescript
import { CONTRACT_BALANCE } from '@uniswap/universal-router-sdk'

const bridgeParams = {
// ... other params
inputAmount: CONTRACT_BALANCE, // Bridge entire balance after swap
// ... other params
}
```

### Multiple Bridge Operations

You can perform multiple bridge operations after a swap:

```typescript
const { calldata, value } = SwapRouter.swapCallParameters(
trade,
swapOptions,
[
{
// Bridge 50% to Optimism
inputToken: WETH_MAINNET,
outputToken: WETH_OPTIMISM,
inputAmount: BigNumber.from('500000000000000000'),
destinationChainId: 10,
// ... other params
},
{
// Bridge remaining USDC to Arbitrum
inputToken: USDC_MAINNET,
outputToken: USDC_ARBITRUM,
inputAmount: CONTRACT_BALANCE,
destinationChainId: 42161,
// ... other params
}
]
)
```

### Native ETH Bridging

To bridge native ETH instead of WETH:

```typescript
const bridgeParams = {
inputToken: WETH_ADDRESS, // Must be WETH address
outputToken: WETH_ON_DESTINATION,
useNative: true, // Bridge as native ETH
// ... other params
}
```

### Important Notes

1. **Across Quote**: Bridge parameters (especially `outputAmount`, `quoteTimestamp`, `fillDeadline`) should come from the Across API quote
2. **Recipient Address**: Can be different from the sender, allowing cross-chain transfers to other addresses
3. **Message Passing**: The `message` field allows executing arbitrary calls on the destination chain
4. **Slippage**: The `outputAmount` already accounts for bridge fees and slippage
34 changes: 34 additions & 0 deletions sdks/universal-router-sdk/TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# TODO

## Universal Router v2.1 ABI Update

- [ ] **Update @uniswap/universal-router npm package**: The SDK currently uses `@uniswap/universal-router` version `2.0.0-beta.2` which doesn't include v2.1 features. Need to update to v2.1 once published.

**Current Issue**: The SDK cannot fully test signed routes functionality because:
- The npm package is v2.0 (doesn't have `executeSigned` function in ABI)
- The v2.1 contracts exist locally in `lib/universal-router/` but can't be built due to missing dependencies
- Manual encoding workaround is implemented using known function selector `0x5f8554bc`

**What Works**:
- ✅ `getExecuteSignedPayload()` - Generates EIP712 payloads
- ✅ `encodeExecuteSigned()` - Manually encodes the function call
- ⚠️ Tests are skipped until v2.1 ABI is available

**Action Required**: Update `package.json` when v2.1 is published:
```json
"@uniswap/universal-router": "^2.1.0" // Update from 2.0.0-beta.2
```

## Post-Release Tasks

- [ ] **Update v4-sdk dependency in universal-router-sdk**: Once the updated `@uniswap/v4-sdk` with per-hop slippage support is published to npm, change the dependency in `package.json` from `workspace:*` back to the published version (e.g., `^1.23.0` or whatever the new version is).

```json
"@uniswap/v4-sdk": "^1.23.0" // Update to published version
```

- [ ] **Update v4-sdk dependency in router-sdk**: Also update `sdks/router-sdk/package.json` to use the published version instead of `workspace:*`.

- [ ] **Remove yarn resolutions**: Remove the `"@uniswap/v4-sdk": "workspace:sdks/v4-sdk"` and `"@uniswap/sdk-core": "workspace:sdks/sdk-core"` entries from the root `package.json` resolutions field.

Currently using `workspace:*` and yarn resolutions for local development during v2.1 implementation.
3 changes: 3 additions & 0 deletions sdks/universal-router-sdk/foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@ fs_permissions = [{ access = "read", path = "./permit2/out/Permit2.sol/Permit2.j
src = "./test/forge"
via_ir = true
evm_version = "cancun"

[lint]
lint_on_build = false
2 changes: 1 addition & 1 deletion sdks/universal-router-sdk/lib/universal-router
Submodule universal-router updated 85 files
+10 −19 .github/workflows/deploy.yml
+8 −6 .github/workflows/forge.yml
+6 −4 .github/workflows/lint.yml
+7 −5 .github/workflows/test.yml
+4 −0 .gitignore
+1 −0 .gitmodules
+35 −4 contracts/UniversalRouter.sol
+36 −18 contracts/base/Dispatcher.sol
+96 −0 contracts/base/RouteSigner.sol
+52 −0 contracts/interfaces/IUniversalRouter.sol
+19 −0 contracts/interfaces/external/IV3SpokePool.sol
+5 −1 contracts/libraries/Commands.sol
+2 −2 contracts/libraries/Locker.sol
+2 −2 contracts/libraries/MaxInputAmount.sol
+58 −0 contracts/modules/ChainedActions.sol
+6 −4 contracts/modules/uniswap/v2/V2SwapRouter.sol
+8 −7 contracts/modules/uniswap/v3/V3SwapRouter.sol
+2 −0 contracts/types/RouterParameters.sol
+3 −3 deploy-addresses/arbitrum-goerli.json
+4 −3 deploy-addresses/arbitrum.json
+3 −3 deploy-addresses/avalanche.json
+1 −1 deploy-addresses/base-goerli.json
+2 −2 deploy-addresses/base-sepolia.json
+3 −2 deploy-addresses/base.json
+6 −0 deploy-addresses/blast.json
+4 −3 deploy-addresses/bsc.json
+3 −3 deploy-addresses/celo-alfajores.json
+3 −3 deploy-addresses/celo.json
+3 −3 deploy-addresses/goerli.json
+3 −0 deploy-addresses/ink.json
+4 −3 deploy-addresses/mainnet.json
+0 −1 deploy-addresses/op-sepolia.json
+3 −3 deploy-addresses/optimism-goerli.json
+4 −3 deploy-addresses/optimism.json
+3 −3 deploy-addresses/polygon-mumbai.json
+4 −3 deploy-addresses/polygon.json
+3 −2 deploy-addresses/sepolia.json
+4 −0 deploy-addresses/soneium.json
+1 −2 deploy-addresses/unichain-sepolia.json
+3 −0 deploy-addresses/unichain.json
+2 −1 deploy-addresses/worldchain.json
+3 −0 deploy-addresses/zora.json
+20 −0 foundry.lock
+5 −1 foundry.toml
+1 −1 lib/v4-periphery
+2 −2 package.json
+3 −2 script/DeployUniversalRouter.s.sol
+4 −3 script/deployParameters/DeployArbitrum.s.sol
+3 −2 script/deployParameters/DeployArbitrumGoerli.s.sol
+4 −3 script/deployParameters/DeployAvalanche.s.sol
+4 −3 script/deployParameters/DeployBSC.s.sol
+4 −3 script/deployParameters/DeployBase.s.sol
+3 −2 script/deployParameters/DeployBaseGoerli.s.sol
+4 −3 script/deployParameters/DeployBaseSepolia.s.sol
+4 −3 script/deployParameters/DeployBlast.s.sol
+3 −2 script/deployParameters/DeployCelo.s.sol
+3 −2 script/deployParameters/DeployCeloAlfajores.s.sol
+3 −2 script/deployParameters/DeployGoerli.s.sol
+22 −0 script/deployParameters/DeployInk.s.sol
+3 −2 script/deployParameters/DeployMainnet.s.sol
+5 −4 script/deployParameters/DeployOPSepolia.s.sol
+4 −3 script/deployParameters/DeployOptimism.s.sol
+3 −2 script/deployParameters/DeployOptimismGoerli.s.sol
+4 −3 script/deployParameters/DeployPolygon.s.sol
+3 −2 script/deployParameters/DeployPolygonMumbai.s.sol
+4 −3 script/deployParameters/DeploySepolia.s.sol
+24 −0 script/deployParameters/DeploySoneium.s.sol
+22 −0 script/deployParameters/DeployUnichain.s.sol
+4 −5 script/deployParameters/DeployUnichainSepolia.s.sol
+10 −9 script/deployParameters/DeployWorldchain.s.sol
+22 −0 script/deployParameters/DeployZora.s.sol
+651 −0 test/foundry-tests/RouteSigner.t.sol
+2 −1 test/foundry-tests/UniswapV2.t.sol
+2 −1 test/foundry-tests/UniversalRouter.t.sol
+164 −0 test/foundry-tests/external/ChainedActions.fork.t.sol
+2 −0 test/integration-tests/UniswapMixed.test.ts
+15 −0 test/integration-tests/UniswapV4.test.ts
+1 −1 test/integration-tests/gas-tests/__snapshots__/CheckOwnership.gas.test.ts.snap
+7 −7 test/integration-tests/gas-tests/__snapshots__/Payments.gas.test.ts.snap
+39 −39 test/integration-tests/gas-tests/__snapshots__/Uniswap.gas.test.ts.snap
+1 −1 test/integration-tests/gas-tests/__snapshots__/UniversalRouter.gas.test.ts.snap
+10 −10 test/integration-tests/gas-tests/__snapshots__/UniversalVSSwapRouter.gas.test.ts.snap
+8 −8 test/integration-tests/gas-tests/__snapshots__/V3ToV4Migration.gas.test.ts.snap
+1 −0 test/integration-tests/shared/deployUniversalRouter.ts
+6 −2 test/integration-tests/shared/v4Planner.ts
2 changes: 1 addition & 1 deletion sdks/universal-router-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"@uniswap/v2-sdk": "^4.16.0",
"@uniswap/v3-core": "1.0.0",
"@uniswap/v3-sdk": "^3.26.0",
"@uniswap/v4-sdk": "^1.22.0",
"@uniswap/v4-sdk": "workspace:*",
"bignumber.js": "^9.0.2",
"ethers": "^5.7.0"
},
Expand Down
2 changes: 1 addition & 1 deletion sdks/universal-router-sdk/remappings.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ forge-std/=lib/forge-std/src/
hardhat/=node_modules/@uniswap/universal-router/node_modules/hardhat/
permit2/=lib/permit2/
universal-router/=lib/universal-router/contracts
@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/
@openzeppelin/contracts/=lib/universal-router/lib/v4-periphery/lib/v4-core/lib/openzeppelin-contracts/contracts/
@uniswap/v3-periphery/=lib/universal-router/lib/v3-periphery/
@uniswap/v4-periphery/=lib/universal-router/lib/v4-periphery/
@uniswap/v4-core/=lib/universal-router/lib/v4-periphery/lib/v4-core/
Expand Down
24 changes: 24 additions & 0 deletions sdks/universal-router-sdk/src/entities/actions/across.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { BigNumberish } from 'ethers'

/**
* Parameters for Across V4 Deposit V3 command
* Used for cross-chain bridging via Across Protocol V3 SpokePool
*/
export type AcrossV4DepositV3Params = {
depositor: string // Credited depositor on origin chain
recipient: string // Destination recipient
inputToken: string // ERC20 on origin (WETH if bridging ETH)
outputToken: string // ERC20 on destination
inputAmount: BigNumberish // Amount to bridge (supports CONTRACT_BALANCE)
outputAmount: BigNumberish // Expected amount on destination
destinationChainId: number // Destination chain ID
exclusiveRelayer: string // 0x0 if no exclusivity
quoteTimestamp: number // uint32
fillDeadline: number // uint32
exclusivityDeadline: number // uint32
message: string // bytes - optional message data
useNative: boolean // If true, bridge native ETH (inputToken must be WETH)
}

// Export CONTRACT_BALANCE constant for convenience
export { CONTRACT_BALANCE } from '../../utils/constants'
Loading
Loading