Skip to content

Commit 063f455

Browse files
committed
Add bridge aggregation functionality
1 parent 213248a commit 063f455

File tree

12 files changed

+533
-110
lines changed

12 files changed

+533
-110
lines changed

MIXINS.md

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
# Available Mixins
2-
32
Currently, only the following internal-type mixins are available:
4-
53
## buildCrossChainTransaction
64
Mixins that affect the logic for building a cross-chain exchange transaction
7-
85
| Name | Data Type | Description |
96
|--------------------------------|-----------------------------|------------------------------------------------------------------------------------|
107
| nativeAmountFinalized | BigNumber | The amount of the native currency required for the transaction has been determined |
@@ -14,51 +11,41 @@ Mixins that affect the logic for building a cross-chain exchange transaction
1411
| approveTransactionRequest | ExecutorCallData | An approve transaction request has been obtained |
1512
| outputAmountsCorrected | [Amount, Amount] | The quota’s output values have been finalized |
1613
| quotaComputationFinalized | ExchangeQuota | The quota processing is complete |
17-
| stargateSendV2CallData | string | Encoded data has been obtained to send the payload through Stargate |
14+
| bridgeTransactionCallData | string | Encoded data has been obtained to send the payload through selected bridge |
15+
| bridgeTransactionNativeAmount | string | Native amount has been obtained for the bridge transaction |
1816
| tokenTransferCallDataFinalized | Promise\<string\> \| string | Token transfer calldata has been obtained |
19-
2017
## computeQuotaExecutionGasUsage
2118
Mixins that affect the logic for calculating the gas required to execute a quota
22-
2319
| Name | Data Type | Description |
2420
|----------------------------------|-----------|------------------------------------------------------------------------------------------|
25-
| stargateSwapMessageGasUsage | number | The amount of gas needed to send a message and then perform an exchange through Stargate |
26-
| stargateHollowMessageGasUsage | number | The amount of gas needed to send a message without a subsequent exchange |
21+
| bridgeSwapMessageGasUsage | number | The amount of gas needed to send a message and then perform an exchange through Stargate |
22+
| bridgeHollowMessageGasUsage | number | The amount of gas needed to send a message without a subsequent exchange |
2723
| wrapTransactionGasUsage | number | The amount of gas that will be used to wrap the native token |
2824
| unwrapTransactionGasUsage | number | The amount of gas that will be used to unwrap the native token |
2925
| multiStepExchangeWrapperGasUsage | number | The amount of gas needed to wrap the execution of multi-step exchanges |
3026
| finalMultiplier | number | The multiplier for the used gas |
31-
3227
## computeOnchainTradeGasUsage
3328
Mixins that affect the logic for calculating the gas required for an exchange within a single network
34-
3529
| Name | Data Type | Description |
3630
|-----------------------|-----------|-----------------------------------------------------------------------------------|
3731
| uniswapV3StepGasUsage | number | The amount of gas spent on an exchange step through a UniswapV3-type pool |
3832
| uniswapV2StepGasUsage | number | The amount of gas spent on an exchange step through a UniswapV2-type pool |
3933
| receiveNativeGasUsage | number | The amount of gas needed to receive the native token at the end of the exchange |
4034
| routeInitialGasUsage | number | The amount of gas that will be consumed by additional exchange contract functions |
41-
4235
## fetchRoute
4336
Mixins that affect the logic of retrieving and subsequently processing routes
44-
4537
| Name | Data Type | Description |
4638
|-----------------------------|------------------|-------------------------------------------------------------------|
4739
| receivedFinalizedRoute | SimulatedRoute | Route filtering is complete |
4840
| wrapUnwrapVirtualRouteBuilt | SimulatedRoute | A virtual route has been built for the wrap or unwrap transaction |
49-
5041
## createSingleChainTransaction
5142
Mixins that affect the process of building an exchange transaction within a single network
52-
5343
| Name | Data Type | Description |
5444
|--------------------------------|-----------------------------|--------------------------------------------------------------------------|
5545
| singleChainQuotaBuilt | ExchangeQuota | The construction of the single-network exchange quota has been completed |
5646
| tokenTransferCallDataFinalized | Promise\<string\> \| string | Token transfer calldata has been obtained |
57-
58-
5947
## createSingleChainWrapUnwrapTransaction
6048
Mixins that affect the process of building a wrap or unwrap transaction
61-
6249
| Name | Data Type | Description |
6350
|------------|---------------|-------------------------------------------|
6451
| quotaBuilt | ExchangeQuota | The quota construction has been completed |

src/abis/AcrossABI.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[{"inputs":[{"internalType":"address","name":"spokePool_","type":"address"},{"internalType":"address","name":"wrappedNative_","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"AcrossFacet_NotSpokePool","type":"error"},{"inputs":[],"name":"TransferHelper_ApproveError","type":"error"},{"inputs":[],"name":"TransferHelper_TransferError","type":"error"},{"inputs":[],"name":"TransferHelper_TransferFromError","type":"error"},{"inputs":[],"name":"TransientStorageFacetLibrary_InvalidSenderAddress","type":"error"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"bytes","name":"errorMessage","type":"bytes"}],"name":"CallFailed","type":"event"},{"inputs":[{"internalType":"address","name":"tokenSent","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"address","name":"","type":"address"},{"internalType":"bytes","name":"message","type":"bytes"}],"name":"handleV3AcrossMessage","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"address","name":"inputToken","type":"address"},{"internalType":"address","name":"outputToken","type":"address"},{"internalType":"uint256","name":"inputAmount","type":"uint256"},{"internalType":"uint256","name":"outputAmountPercent","type":"uint256"},{"internalType":"uint256","name":"destinationChainId","type":"uint256"},{"internalType":"address","name":"exclusiveRelayer","type":"address"},{"internalType":"uint32","name":"quoteTimestamp","type":"uint32"},{"internalType":"uint32","name":"fillDeadline","type":"uint32"},{"internalType":"uint32","name":"exclusivityDeadline","type":"uint32"},{"internalType":"bytes","name":"message","type":"bytes"}],"internalType":"struct IAcrossFacet.V3AcrossDepositParams","name":"acrossDepositParams","type":"tuple"}],"name":"sendAcrossDepositV3","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"spokePool","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"}]
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { Address, Amount } from "@safeblock/blockchain-utils"
2+
import { toUtf8Bytes } from "ethers"
3+
import { BridgeFaucet__factory } from "~/abis/types"
4+
import { contractAddresses, stargateNetworksMapping } from "~/config"
5+
import { SdkConfig } from "~/sdk"
6+
import acrossAggregationModule from "~/sdk/bridge-aggregation/modules/across"
7+
import stargateAggregationModule from "~/sdk/bridge-aggregation/modules/stargate"
8+
import { ExchangeUtils } from "~/sdk/exchange-utils"
9+
import SdkCore from "~/sdk/sdk-core"
10+
import SdkException, { SdkExceptionCode } from "~/sdk/sdk-exception"
11+
import { AggregationModuleRequestParams, AggregationResponse, ExchangeRequest, SimulatedRoute } from "~/types"
12+
13+
interface BridgingDetails {
14+
senderAddress: Address
15+
sourceChainRoute?: SimulatedRoute | null
16+
sourceNetworkSendAmount: Amount
17+
request: ExchangeRequest
18+
destinationChainRoute?: SimulatedRoute | null
19+
destinationNetworkCallData: string | null
20+
sdkConfig: SdkConfig
21+
}
22+
23+
export default async function aggregateBridges(sdk: SdkCore, options: BridgingDetails): Promise<AggregationResponse | SdkException> {
24+
const fromNetworkUSDC = contractAddresses.usdcParams(options.request.tokenIn.network)
25+
26+
const amountLD = !Address.equal(options.request.tokenIn.address, fromNetworkUSDC.address) ? "0" : Amount
27+
.select(options.sourceChainRoute?.amountIn!, options.sourceNetworkSendAmount)!.toString()
28+
29+
const receiverAddress = options.destinationNetworkCallData
30+
? contractAddresses.entryPoint(options.request.tokensOut[0].network, options.sdkConfig)
31+
: (options.request.destinationAddress || options.senderAddress || Address.zeroAddress).toString()
32+
33+
const gasLimit = (options.destinationNetworkCallData
34+
? (450_000 + (150_000 * (options.destinationChainRoute?.originalRouteSet.flat(1).length ?? 0))) : 0).toFixed(0)
35+
36+
options.sdkConfig.debugLogListener?.("BridgeAggregation: Sending request to bridge aggregator...")
37+
38+
const aggregationResult = await aggregate(sdk, {
39+
destinationAddress: (options.request.destinationAddress ?? options.senderAddress),
40+
userAddress: options.senderAddress,
41+
inputAmountRaw: Amount.select(options.sourceChainRoute?.amountsOut?.[0], options.sourceNetworkSendAmount)!.toString(),
42+
amountLD,
43+
sourceChainId: parseInt(options.request.tokenIn.network.chainId.toString()),
44+
destinationChainId: parseInt(options.request.tokensOut[0].network.chainId.toString()),
45+
inputToken: {
46+
address: Address.from(fromNetworkUSDC.address),
47+
decimals: fromNetworkUSDC.decimals,
48+
network: options.request.tokenIn.network
49+
},
50+
message: options.destinationNetworkCallData || "0x",
51+
receiverAddress,
52+
gasLimit
53+
})
54+
55+
if (aggregationResult instanceof SdkException) {
56+
options.sdkConfig.debugLogListener?.(`BridgeAggregation: Aggregator responded with error: ${ aggregationResult.message }`)
57+
options.sdkConfig.debugLogListener?.("BridgeAggregation: Aggregator not configured or not responded")
58+
options.sdkConfig.debugLogListener?.("BridgeAggregation: Using fallback internal computation logic")
59+
60+
const bridgeIface = BridgeFaucet__factory.createInterface()
61+
62+
const fallbackCalldata = bridgeIface.encodeFunctionData("sendStargateV2", [
63+
contractAddresses.stargateUSDCPool(options.request.tokenIn.network),
64+
stargateNetworksMapping(options.request.tokensOut[0].network),
65+
receiverAddress,
66+
gasLimit,
67+
options.destinationNetworkCallData || toUtf8Bytes("")
68+
])
69+
70+
options.sdkConfig.debugLogListener?.("BridgeAggregation: Stargate calldata computed")
71+
72+
const bridgeQuota = await ExchangeUtils.computeBridgeQuota(
73+
options.request,
74+
options.senderAddress,
75+
options.sourceNetworkSendAmount.toBigNumber().toFixed(0),
76+
options.destinationChainRoute?.originalRouteSet.flat(1).length ?? 0,
77+
options.destinationNetworkCallData,
78+
options.sdkConfig
79+
)
80+
81+
if (bridgeQuota instanceof SdkException) return bridgeQuota
82+
83+
options.sdkConfig.debugLogListener?.("BridgeAggregation: Stargate bridge quota computed")
84+
85+
return {
86+
valueToSend: Amount.from(bridgeQuota.valueToSend, 18, false),
87+
bridgeCallData: fallbackCalldata,
88+
bridgeName: "stargate"
89+
}
90+
}
91+
92+
options.sdkConfig.debugLogListener?.("BridgeAggregation: Aggregator responded with third party bridge "
93+
+ aggregationResult.label)
94+
95+
options.sdkConfig.debugLogListener?.("BridgeAggregation: Bridge transaction price impact "
96+
+ `is ${ aggregationResult.prices.impact }%`)
97+
98+
99+
return {
100+
valueToSend: Amount.from(aggregationResult.valueToSend, 18, false),
101+
bridgeCallData: aggregationResult.callData,
102+
bridgeName: aggregationResult.label
103+
}
104+
}
105+
106+
async function aggregate(sdk: SdkCore, options: AggregationModuleRequestParams) {
107+
const [across, stargate] = await Promise.all([
108+
acrossAggregationModule(sdk, options).catch((e: any) => {
109+
return new SdkException(e?.message || "Failed to process across bridge", SdkExceptionCode.InternalError)
110+
}),
111+
stargateAggregationModule(sdk, options).catch((e: any) => {
112+
return new SdkException(e?.message || "Failed to process stargate bridge", SdkExceptionCode.InternalError)
113+
})
114+
])
115+
116+
if (across instanceof SdkException && stargate instanceof SdkException) {
117+
return new SdkException(`Failed to process both bridges: [${ [across.message, stargate.message].join(", ") }]`,
118+
SdkExceptionCode.InternalError)
119+
}
120+
121+
if (across instanceof SdkException) return stargate
122+
if (stargate instanceof SdkException) return across
123+
124+
if (across.prices.impact > stargate.prices.impact) return stargate
125+
return across
126+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { Address, Amount, networksList } from "@safeblock/blockchain-utils"
2+
import BigNumber from "bignumber.js"
3+
import { AcrossABI__factory } from "~/abis/types"
4+
import { PriceStorageExtension } from "~/extensions"
5+
import { AcrossTokenDetails, SuggestedFeeApiResponse } from "~/sdk/bridge-aggregation/modules/across.types"
6+
import SdkCore from "~/sdk/sdk-core"
7+
import SdkException, { SdkExceptionCode } from "~/sdk/sdk-exception"
8+
import { AggregationModuleRequestParams, AggregationModuleResponse } from "~/types"
9+
10+
let tokenManifestCache: Map<string, AcrossTokenDetails[]> = new Map()
11+
12+
export default async function acrossAggregationModule(sdk: SdkCore, params: AggregationModuleRequestParams): Promise<SdkException | AggregationModuleResponse> {
13+
const urlParams = new URLSearchParams()
14+
15+
let acrossToken: AcrossTokenDetails | null = null
16+
17+
if (tokenManifestCache.has(params.sourceChainId.toString())) {
18+
acrossToken = (tokenManifestCache.get(params.sourceChainId.toString()) ?? []).find(t => t.originChainId === params.sourceChainId
19+
&& t.destinationChainId === params.destinationChainId
20+
&& Address.equal(t.originToken, params.inputToken.address)) ?? null
21+
}
22+
else {
23+
try {
24+
const tokensListURL = "https://raw.githubusercontent.com/safeblock-com/dex-content/refs/heads"
25+
+ `/main/aggregation/across/manifest/${ params.sourceChainId.toString() }.json`
26+
27+
const tokensListResponse = await fetch(tokensListURL)
28+
29+
if (!tokensListResponse.ok) return new SdkException("Failed to fetch Across tokens list", SdkExceptionCode.RoutesNotFound)
30+
31+
const tokensList = await tokensListResponse.json() as AcrossTokenDetails[]
32+
33+
tokenManifestCache.set(params.sourceChainId.toString(), tokensList)
34+
35+
acrossToken = (tokensList.find(t => t.originChainId === params.sourceChainId
36+
&& t.destinationChainId === params.destinationChainId
37+
&& Address.equal(t.originToken, params.inputToken.address)) ?? null
38+
)
39+
}
40+
catch {
41+
return new SdkException("Failed to parse Across tokens list", SdkExceptionCode.RoutesNotFound)
42+
}
43+
}
44+
if (!acrossToken) return new SdkException("No matching token found", SdkExceptionCode.RoutesNotFound)
45+
46+
const srcNet = Array.from(networksList).find(n => n.chainId.toString() === params.sourceChainId.toString())
47+
const dstNet = Array.from(networksList).find(n => n.chainId.toString() === params.destinationChainId.toString())
48+
49+
if (!srcNet || !dstNet) return new SdkException("Invalid network", SdkExceptionCode.InvalidRequest)
50+
51+
const priceStorage = sdk.extension(PriceStorageExtension)
52+
53+
const dstTokenPrice = priceStorage.getPrice({
54+
network: dstNet,
55+
address: Address.from(acrossToken.destinationToken),
56+
decimals: params.inputToken.decimals
57+
})
58+
59+
const srcTokenPrice = priceStorage.getPrice({
60+
network: srcNet,
61+
address: params.inputToken.address,
62+
decimals: params.inputToken.decimals
63+
})
64+
65+
if (!dstTokenPrice || !srcTokenPrice) return new SdkException("Failed to get prices", SdkExceptionCode.InternalError)
66+
67+
urlParams.set("inputToken", Address.equal(Address.zeroAddress, params.inputToken.address) ? Address.wrappedOf(srcNet).toString() : params.inputToken.address.toString())
68+
urlParams.set("outputToken", acrossToken.destinationToken)
69+
urlParams.set("originChainId", params.sourceChainId.toString())
70+
urlParams.set("destinationChainId", params.destinationChainId.toString())
71+
urlParams.set("amount", params.inputAmountRaw)
72+
73+
const baseURL = "https://app.across.to/api"
74+
75+
const suggestedFeesRequest = await fetch(`${ baseURL }/suggested-fees?${ urlParams.toString() }`)
76+
77+
if (!suggestedFeesRequest.ok) {
78+
const message = await suggestedFeesRequest.json()
79+
80+
return new SdkException(message?.message || "Failed to fetch suggested fees", SdkExceptionCode.InternalError)
81+
}
82+
83+
const suggestedFee = await suggestedFeesRequest.json() as SuggestedFeeApiResponse
84+
85+
const acrossContractIface = AcrossABI__factory.createInterface()
86+
87+
const callData = acrossContractIface.encodeFunctionData("sendAcrossDepositV3", [{
88+
recipient: params.receiverAddress,
89+
inputToken: params.inputToken.address.toString(),
90+
outputToken: acrossToken.destinationToken,
91+
inputAmount: params.inputAmountRaw,
92+
outputAmountPercent: BigInt(1e18) - BigInt(String(suggestedFee.totalRelayFee.pct)),
93+
destinationChainId: params.destinationChainId.toString(),
94+
exclusiveRelayer: suggestedFee.exclusiveRelayer,
95+
quoteTimestamp: suggestedFee.timestamp,
96+
fillDeadline: suggestedFee.fillDeadline,
97+
exclusivityDeadline: suggestedFee.exclusivityDeadline,
98+
message: params.message.length > 2 ? ("0x" + params.message.slice(130 + 128)) : params.message
99+
}])
100+
101+
const inputAmount = Amount.from(params.inputAmountRaw, params.inputToken.decimals, false)
102+
const outputAmount = Amount.from(suggestedFee.outputAmount, params.inputToken.decimals, false)
103+
104+
const inputAmountUSD = inputAmount.toReadableBigNumber().multipliedBy(srcTokenPrice.toReadableBigNumber())
105+
const outputAmountUSD = outputAmount.toReadableBigNumber().multipliedBy(dstTokenPrice.toReadableBigNumber())
106+
107+
return {
108+
callData,
109+
valueToSend: Amount.from(Address.equal(params.inputToken.address, Address.zeroAddress) ? params.inputAmountRaw : "0", 18, false),
110+
inputAmount: inputAmount,
111+
outputAmount: outputAmount,
112+
label: "across",
113+
prices: {
114+
input: inputAmountUSD.dp(5),
115+
output: outputAmountUSD.dp(5),
116+
impact: new BigNumber(100).minus(outputAmountUSD.dividedBy(inputAmountUSD).multipliedBy(100)).dp(5).toNumber()
117+
}
118+
}
119+
}

0 commit comments

Comments
 (0)