Skip to content

Commit 17bca16

Browse files
committed
[simplex,sdk]: add Uniswap V4 funding venue and unified FundingVenue interface
1 parent 7f32793 commit 17bca16

File tree

19 files changed

+2592
-154
lines changed

19 files changed

+2592
-154
lines changed

sdk/packages/sdk/src/configs/ChainConfigService.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,18 @@ export class ChainConfigService {
127127
return (this.getConfig(chain)?.addresses.UniswapV4Quoter ?? "0x") as HexString
128128
}
129129

130+
getUniswapV4PositionManagerAddress(chain: string): HexString {
131+
return (this.getConfig(chain)?.addresses.UniswapV4PositionManager ?? "0x") as HexString
132+
}
133+
134+
getUniswapV4PoolManagerAddress(chain: string): HexString {
135+
return (this.getConfig(chain)?.addresses.UniswapV4PoolManager ?? "0x") as HexString
136+
}
137+
138+
getUniswapV4StateViewAddress(chain: string): HexString {
139+
return (this.getConfig(chain)?.addresses.UniswapV4StateView ?? "0x") as HexString
140+
}
141+
130142
getPermit2Address(chain: string): HexString {
131143
return (this.getConfig(chain)?.addresses.Permit2 ?? "0x") as HexString
132144
}

sdk/packages/sdk/src/configs/chain.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,12 @@ export interface ChainConfigData {
125125
SolverAccount?: `0x${string}`
126126
/** Aerodrome (Solidly-style) router for LP removal / swaps on chains where Aerodrome is deployed */
127127
AerodromeRouter?: `0x${string}`
128+
/** Uniswap V4 PositionManager (canonical CREATE2 address) for LP position management */
129+
UniswapV4PositionManager?: `0x${string}`
130+
/** Uniswap V4 PoolManager (canonical CREATE2 address) for pool state reads via extsload */
131+
UniswapV4PoolManager?: `0x${string}`
132+
/** Uniswap V4 StateView (canonical CREATE2 address) for pool state reads via extsload */
133+
UniswapV4StateView?: `0x${string}`
128134
}
129135
rpcEnvKey?: string
130136
defaultRpcUrl?: string
@@ -286,6 +292,9 @@ export const chainConfigs: Record<number, ChainConfigData> = {
286292
UniversalRouter: "0x66a9893cc07d91d95644aedd05d03f95e1dba8af",
287293
UniswapV3Quoter: "0x61fFE014bA17989E743c5F6cB21bF9697530B21e",
288294
UniswapV4Quoter: "0x52f0e24d1c21c8a0cb1e5a5dd6198556bd9e1203",
295+
UniswapV4PositionManager: "0xbd216513d74c8cf14cf4747e6aaa6420ff64ee9e",
296+
UniswapV4PoolManager: "0x000000000004444c5dc75cB358380D2e3dE08A90",
297+
UniswapV4StateView: "0x7ffe42c4a5deea5b0fec41c94c136cf115597227",
289298
Calldispatcher: "0xc71251c8b3e7b02697a84363eef6dce8dfbdf333",
290299
Permit2: "0x000000000022D473030F116dDEE9F6B43aC78BA3",
291300
EntryPointV08: "0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108",
@@ -338,6 +347,9 @@ export const chainConfigs: Record<number, ChainConfigData> = {
338347
UniversalRouter: "0xd9C500DfF816a1Da21A48A732d3498Bf09dc9AEB",
339348
UniswapV3Quoter: "0xB048Bbc1Ee6b733FFfCFb9e9CeF7375518e25997",
340349
UniswapV4Quoter: "0xd0737C9762912dD34c3271197E362Aa736Df0926",
350+
UniswapV4PositionManager: "0x7a4a5c919ae2541aed11041a1aeee68f1287f95b",
351+
UniswapV4PoolManager: "0x28e2ea090877bf75740558f6bfb36a5ffee9e9df",
352+
UniswapV4StateView: "0xd13dd3d6e93f276fafc9db9e6bb47c1180aee0c4",
341353
Calldispatcher: "0xc71251c8b3e7b02697a84363eef6dce8dfbdf333",
342354
Permit2: "0x000000000022D473030F116dDEE9F6B43aC78BA3",
343355
EntryPointV08: "0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108",
@@ -392,6 +404,9 @@ export const chainConfigs: Record<number, ChainConfigData> = {
392404
UniversalRouter: "0xa51afafe0263b40edaef0df8781ea9aa03e381a3",
393405
UniswapV3Quoter: "0x61fFE014bA17989E743c5F6cB21bF9697530B21e",
394406
UniswapV4Quoter: "0x3972c00f7ed4885e145823eb7c655375d275a1c5",
407+
UniswapV4PositionManager: "0xd88f38f930b7952f2db2432cb002e7abbf3dd869",
408+
UniswapV4PoolManager: "0x360e68faccca8ca495c1b759fd9eee466db9fb32",
409+
UniswapV4StateView: "0x76fd297e2d437cd7f76d50f01afe6160f86e9990",
395410
Calldispatcher: "0xc71251c8b3e7b02697a84363eef6dce8dfbdf333",
396411
Permit2: "0x000000000022D473030F116dDEE9F6B43aC78BA3",
397412
EntryPointV08: "0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108",
@@ -450,6 +465,9 @@ export const chainConfigs: Record<number, ChainConfigData> = {
450465
Permit2: "0x000000000022D473030F116dDEE9F6B43aC78BA3",
451466
EntryPointV08: "0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108",
452467
AerodromeRouter: "0xcF77a3Ba9A5CA399B7c97c74d54e5b1Beb874E43",
468+
UniswapV4PositionManager: "0x7c5f5a4bbd8fd63184577525326123b519429bdc",
469+
UniswapV4PoolManager: "0x498581ff718922c3f8e6a244956af099b2652b2b",
470+
UniswapV4StateView: "0xa3c0c9b65bad0b08107aa264b0f3db444b867a71",
453471
// Usdt0Oft: Not available on Base
454472
},
455473
rpcEnvKey: "BASE_MAINNET",
@@ -501,6 +519,9 @@ export const chainConfigs: Record<number, ChainConfigData> = {
501519
UniversalRouter: "0x1095692a6237d83c6a72f3f5efedb9a670c49223",
502520
UniswapV3Quoter: "0x61fFE014bA17989E743c5F6cB21bF9697530B21e",
503521
UniswapV4Quoter: "0xb3d5c3dfc3a7aebff71895a7191796bffc2c81b9",
522+
UniswapV4PositionManager: "0x1ec2ebf4f37e7363fdfe3551602425af0b3ceef9",
523+
UniswapV4PoolManager: "0x67366782805870060151383f4bbff9dab53e5cd6",
524+
UniswapV4StateView: "0x5ea1bd7974c8a611cbab0bdcafcb1d9cc9b3ba5a",
504525
Calldispatcher: "0xc71251c8b3e7b02697a84363eef6dce8dfbdf333",
505526
Permit2: "0x000000000022D473030F116dDEE9F6B43aC78BA3",
506527
EntryPointV08: "0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108",
@@ -545,6 +566,9 @@ export const chainConfigs: Record<number, ChainConfigData> = {
545566
UniversalRouter: "0xef740bf23acae26f6492b10de645d6b98dc8eaf3",
546567
UniswapV3Quoter: "0x385a5cf5f83e99f7bb2852b6a19c3538b9fa7658",
547568
UniswapV4Quoter: "0x52f0e24d1c21c8a0cb1e5a5dd6198556bd9e1203",
569+
UniswapV4PositionManager: "0x4529a01c7a0410167c5740c487a8de60232617bf",
570+
UniswapV4PoolManager: "0x1f98400000000000000000000000000000000004",
571+
UniswapV4StateView: "0x86e8631a016f9068c3f085faf484ee3f5fdee8f2",
548572
Calldispatcher: "0xc71251c8b3e7b02697a84363eef6dce8dfbdf333",
549573
Permit2: "0x000000000022D473030F116dDEE9F6B43aC78BA3",
550574
EntryPointV08: "0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108",
@@ -651,6 +675,8 @@ export const chainConfigs: Record<number, ChainConfigData> = {
651675
Calldispatcher: "0xC71251c8b3e7B02697A84363Eef6DcE8DfBdF333",
652676
Permit2: "0x000000000022D473030F116dDEE9F6B43aC78BA3",
653677
EntryPointV08: "0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108",
678+
UniswapV4PositionManager: "0x3c3ea4b57a46241e54610e5f022e5c45859a1017",
679+
UniswapV4PoolManager: "0x9a13f98cb987694c9f086b1f5eb990eea8264ec3",
654680
},
655681
defaultRpcUrl: "https://mainnet.optimism.io",
656682
consensusStateId: "ETH0",
@@ -704,6 +730,8 @@ export const chainConfigs: Record<number, ChainConfigData> = {
704730
TokenGateway: "0xCe304770236f39F9911BfCC51afBdfF3b8635718",
705731
Host: "0x7F0165140D0f3251c8f6465e94E9d12C7FD40711",
706732
EntryPointV08: "0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108",
733+
UniswapV4PositionManager: "0x1b35d13a2e2528f192637f14b05f0dc0e7deb566",
734+
UniswapV4PoolManager: "0x360e68faccca8ca495c1b759fd9eee466db9fb32",
707735
},
708736
defaultRpcUrl: "https://rpc.soneium.org",
709737
consensusStateId: "ETH0",

sdk/packages/sdk/src/protocols/intents/GasEstimator.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ export class GasEstimator {
157157
if (this.ctx.bundlerUrl) {
158158
try {
159159
const callData = this.crypto.encodeERC7821Execute([
160+
...(params.prependCalls ?? []),
160161
{ target: intentGatewayV2Address, value: totalNativeValue, data: fillOrderCalldata },
161162
])
162163

sdk/packages/sdk/src/types/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1212,6 +1212,12 @@ export interface SubmitBidOptions {
12121212

12131213
export interface EstimateFillOrderParams {
12141214
order: Order
1215+
/**
1216+
* Optional ERC-7821 calls to prepend before the fillOrder call in the
1217+
* simulated UserOp. Used for funding calls (e.g. LP withdrawal) so the
1218+
* bundler estimates gas for the complete atomic batch.
1219+
*/
1220+
prependCalls?: ERC7821Call[]
12151221
/**
12161222
* Optional percentage to bump maxPriorityFeePerGas.
12171223
* This is added on top of the base gasPrice.

sdk/packages/simplex/package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,9 @@
5858
},
5959
"dependencies": {
6060
"@binance/wallet": "^16.0.0",
61+
"@bufbuild/protobuf": "^2.2.3",
6162
"@circle-fin/adapter-viem-v2": "1.4.0",
6263
"@circle-fin/bridge-kit": "1.5.0",
63-
"@bufbuild/protobuf": "^2.2.3",
6464
"@grpc/grpc-js": "^1.12.5",
6565
"@hyperbridge/sdk": "workspace:*",
6666
"@polkadot/api": "latest",
@@ -69,17 +69,20 @@
6969
"@safe-global/api-kit": "^3.0.1",
7070
"@safe-global/protocol-kit": "^6.0.4",
7171
"@safe-global/types-kit": "^2.0.1",
72+
"@uniswap/sdk-core": "^7.13.0",
73+
"@uniswap/v4-sdk": "^2.0.0",
7274
"async-mutex": "^0.5.0",
7375
"better-sqlite3": "^11.10.0",
7476
"ckb-mmr-wasm": "link:wasm-b",
7577
"commander": "^12.0.0",
7678
"decimal.js": "^10.5.0",
7779
"dotenv": "^16.4.7",
7880
"ethers": "^5.7.2",
81+
"jsbi": "^4.3.2",
7982
"p-queue": "^8.1.0",
8083
"pino": "^9.9.5",
81-
"prom-client": "^15.1.0",
8284
"pino-pretty": "^11.3.0",
85+
"prom-client": "^15.1.0",
8386
"scale-ts": "^1.6.1",
8487
"toml": "^3.0.0",
8588
"viem": "2.47.6"

sdk/packages/simplex/src/bin/simplex.ts

Lines changed: 40 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ import { parse } from "toml"
77
import { IntentFiller } from "@/core/filler"
88
import { BasicFiller } from "@/strategies/basic"
99
import { FXFiller } from "@/strategies/fx"
10-
import type { AerodromePoolConfig, OutputFundingConfig } from "@/funding/types"
10+
import type { AerodromePoolConfig, FundingVenue, UniswapV4PositionConfig } from "@/funding/types"
11+
import { AerodromeFundingPlanner } from "@/funding/aerodrome/AerodromeFundingPlanner"
12+
import { UniswapV4FundingPlanner } from "@/funding/uniswapV4/UniswapV4FundingPlanner"
1113
import { ConfirmationPolicy, FillerBpsPolicy, FillerPricePolicy } from "@/config/interpolated-curve"
12-
import { ChainConfig, Chains, FillerConfig, HexString, getConfigByStateMachineId } from "@hyperbridge/sdk"
14+
import { ChainConfig, FillerConfig, HexString } from "@hyperbridge/sdk"
1315
import {
1416
FillerConfigService,
1517
UserProvidedChainConfig,
@@ -75,6 +77,12 @@ interface AerodromePoolToml extends AerodromePoolConfig {
7577
chain: string
7678
}
7779

80+
/** TOML row for a Uniswap V4 position; only chain + tokenId needed. */
81+
interface UniswapV4PositionToml {
82+
chain: string
83+
tokenId: string // bigint as string in TOML
84+
}
85+
7886
interface FxStrategyConfig {
7987
type: "hyperfx"
8088
/**
@@ -101,11 +109,14 @@ interface FxStrategyConfig {
101109
exoticTokenAddresses: Record<string, HexString>
102110
/** Optional per-chain confirmation policies for cross-chain orders */
103111
confirmationPolicies?: Record<string, ChainConfirmationPolicy>
104-
/** Optional Aerodrome LP funding for destination-chain outputs */
112+
/** Optional on-chain liquidity funding for destination-chain outputs */
105113
outputFunding?: {
106114
aerodrome?: {
107115
pools?: AerodromePoolToml[]
108116
}
117+
uniswapV4?: {
118+
positions?: UniswapV4PositionToml[]
119+
}
109120
}
110121
}
111122

@@ -421,7 +432,7 @@ program
421432
"No confirmationPolicies configured for hyperfx strategy; cross-chain orders will be skipped",
422433
)
423434
}
424-
let outputFunding: OutputFundingConfig | undefined
435+
const fundingVenues: FundingVenue[] = []
425436
if (strategyConfig.outputFunding?.aerodrome?.pools?.length) {
426437
const poolsByChain: Record<string, AerodromePoolConfig[]> = {}
427438
for (const row of strategyConfig.outputFunding.aerodrome.pools) {
@@ -432,12 +443,22 @@ program
432443
gauge: row.gauge,
433444
})
434445
}
435-
436-
outputFunding = {
437-
aerodrome: {
438-
poolsByChain,
439-
},
446+
fundingVenues.push(
447+
new AerodromeFundingPlanner(chainClientManager, { poolsByChain }, configService),
448+
)
449+
}
450+
if (strategyConfig.outputFunding?.uniswapV4?.positions?.length) {
451+
const positionsByChain: Record<string, UniswapV4PositionConfig[]> = {}
452+
for (const row of strategyConfig.outputFunding.uniswapV4.positions) {
453+
const chain = row.chain
454+
if (!positionsByChain[chain]) positionsByChain[chain] = []
455+
positionsByChain[chain].push({
456+
tokenId: BigInt(row.tokenId),
457+
})
440458
}
459+
fundingVenues.push(
460+
new UniswapV4FundingPlanner(chainClientManager, { positionsByChain }, configService),
461+
)
441462
}
442463
return new FXFiller(
443464
runtimeSigner,
@@ -449,23 +470,23 @@ program
449470
strategyConfig.maxOrderUsd,
450471
strategyConfig.exoticTokenAddresses,
451472
fxConfirmationPolicy,
452-
outputFunding,
473+
fundingVenues,
453474
)
454475
}
455476
default:
456477
throw new Error(`Unknown strategy type: ${(strategyConfig as StrategyConfig).type}`)
457478
}
458479
})
459480

460-
// Initialise FXFiller strategies
481+
// Initialise FXFiller strategies (hydrate funding venue state)
461482
for (const strategy of strategies) {
462483
if (strategy instanceof FXFiller) {
463-
logger.info("Hydrating Aerodrome funding state...")
484+
logger.info("Hydrating funding venue state...")
464485
await strategy.initialise()
465486
}
466487
}
467488

468-
// Set up periodic Aerodrome state refresh (~12s)
489+
// Set up periodic funding state refresh (~12s)
469490
const FUNDING_REFRESH_INTERVAL_MS = 12_000
470491
const fundingRefreshTimers: ReturnType<typeof setInterval>[] = []
471492
for (const strategy of strategies) {
@@ -474,7 +495,7 @@ program
474495
try {
475496
await strategy.refreshFundingState()
476497
} catch (err) {
477-
logger.error({ err }, "Aerodrome funding state refresh failed")
498+
logger.error({ err }, "Funding state refresh failed")
478499
}
479500
}, FUNDING_REFRESH_INTERVAL_MS)
480501
fundingRefreshTimers.push(timer)
@@ -677,27 +698,11 @@ function validateConfig(config: FillerTomlConfig): void {
677698
}
678699

679700
if (strategy.type === "hyperfx") {
680-
if (strategy.outputFunding?.aerodrome) {
681-
if (strategy.outputFunding.aerodrome.pools?.length) {
682-
for (const pool of strategy.outputFunding.aerodrome.pools) {
683-
if (!pool.chain?.trim()) {
684-
throw new Error(
685-
"Each Aerodrome outputFunding pool must have a non-empty 'chain' (e.g. EVM-8453)",
686-
)
687-
}
688-
if (!pool.pair) {
689-
throw new Error("Each Aerodrome pool must include a 'pair' address")
690-
}
691-
const chainCfg = getConfigByStateMachineId(pool.chain as Chains)
692-
const aerodromeRouter = chainCfg?.addresses.AerodromeRouter
693-
const z = aerodromeRouter?.toLowerCase()
694-
if (!z || z === "0x" || z === "0x0000000000000000000000000000000000000000") {
695-
throw new Error(
696-
`Aerodrome pool ${pool.pair} uses chain ${pool.chain} but SDK chain config has no addresses.AerodromeRouter for that chain`,
697-
)
698-
}
699-
}
700-
}
701+
if (strategy.outputFunding?.aerodrome?.pools?.length) {
702+
AerodromeFundingPlanner.validateConfig(strategy.outputFunding.aerodrome.pools)
703+
}
704+
if (strategy.outputFunding?.uniswapV4?.positions?.length) {
705+
UniswapV4FundingPlanner.validateConfig(strategy.outputFunding.uniswapV4.positions)
701706
}
702707

703708
// Validate bid price curve

0 commit comments

Comments
 (0)