Skip to content

Commit afe7f28

Browse files
authored
Add Across bridge support to the QuarkBuilder (#100)
This PR adds support for bridging assets using Across to the QuarkBuilder. This is the first time we are using an FFI in the QuarkBuilder, so any downstream clients using this version of the builder will need to implement the FFI at the reserved address (`address constant ACROSS_FFI_ADDRESS = address(0xFF1000)`).
1 parent f3e00c4 commit afe7f28

20 files changed

+575
-70
lines changed

src/builder/BridgeRoutes.sol

Lines changed: 102 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,25 @@
11
// SPDX-License-Identifier: BSD-3-Clause
22
pragma solidity ^0.8.27;
33

4-
import {QuarkBuilder} from "./QuarkBuilder.sol";
5-
import {CCTPBridgeActions} from "../BridgeScripts.sol";
4+
import {AcrossActions} from "src/AcrossScripts.sol";
5+
import {CCTPBridgeActions} from "src/BridgeScripts.sol";
6+
import {Errors} from "src/builder/Errors.sol";
7+
import {QuarkBuilder} from "src/builder/QuarkBuilder.sol";
68

7-
import "./Strings.sol";
9+
import "src/builder/Strings.sol";
810

911
library BridgeRoutes {
1012
function canBridge(uint256 srcChainId, uint256 dstChainId, string memory assetSymbol)
1113
internal
1214
pure
1315
returns (bool)
1416
{
15-
return CCTP.canBridge(srcChainId, dstChainId, assetSymbol);
17+
return
18+
CCTP.canBridge(srcChainId, dstChainId, assetSymbol) || Across.canBridge(srcChainId, dstChainId, assetSymbol);
1619
}
1720
}
1821

1922
library CCTP {
20-
error NoKnownBridge(string bridgeType, uint256 srcChainId);
2123
error NoKnownDomainId(string bridgeType, uint256 dstChainId);
2224

2325
struct CCTPChain {
@@ -76,7 +78,7 @@ library CCTP {
7678
if (chain.bridge != address(0)) {
7779
return chain.bridge;
7880
} else {
79-
revert NoKnownBridge("CCTP", srcChainId);
81+
revert Errors.NoKnownBridge("CCTP", srcChainId);
8082
}
8183
}
8284

@@ -101,3 +103,97 @@ library CCTP {
101103
);
102104
}
103105
}
106+
107+
library Across {
108+
struct AcrossChain {
109+
uint256 chainId;
110+
address bridge; // SpokePool contract
111+
}
112+
113+
/// @notice The buffer to subtrace from the quote timestamp to ensure it isn't some time in
114+
/// the future, which would cause the Across SpokePool contract to revert
115+
uint32 public constant QUOTE_TIMESTAMP_BUFFER = 30 seconds;
116+
117+
/// @notice The amount of time that the bridge action has to be filled before timing out
118+
uint256 public constant FILL_DEADLINE_BUFFER = 10 minutes;
119+
120+
function knownChains() internal pure returns (AcrossChain[] memory) {
121+
AcrossChain[] memory chains = new AcrossChain[](4);
122+
// Mainnet
123+
chains[0] = AcrossChain({chainId: 1, bridge: 0x5c7BCd6E7De5423a257D81B442095A1a6ced35C5});
124+
// Base
125+
chains[1] = AcrossChain({chainId: 8453, bridge: 0x09aea4b2242abC8bb4BB78D537A67a245A7bEC64});
126+
// Sepolia
127+
chains[2] = AcrossChain({chainId: 11155111, bridge: 0x5ef6C01E11889d86803e0B23e3cB3F9E9d97B662});
128+
// Base Sepolia
129+
chains[3] = AcrossChain({chainId: 84532, bridge: 0x82B564983aE7274c86695917BBf8C99ECb6F0F8F});
130+
return chains;
131+
}
132+
133+
function knownChain(uint256 chainId) internal pure returns (AcrossChain memory found) {
134+
AcrossChain[] memory acrossChains = knownChains();
135+
for (uint256 i = 0; i < acrossChains.length; ++i) {
136+
if (acrossChains[i].chainId == chainId) {
137+
return found = acrossChains[i];
138+
}
139+
}
140+
}
141+
142+
function canBridge(uint256 srcChainId, uint256 dstChainId, string memory assetSymbol)
143+
internal
144+
pure
145+
returns (bool)
146+
{
147+
return knownChain(srcChainId).bridge != address(0) && knownChain(dstChainId).chainId == dstChainId
148+
&& (
149+
Strings.stringEqIgnoreCase(assetSymbol, "USDC") || Strings.stringEqIgnoreCase(assetSymbol, "WETH")
150+
|| Strings.stringEqIgnoreCase(assetSymbol, "ETH")
151+
);
152+
}
153+
154+
function knownBridge(uint256 srcChainId) internal pure returns (address) {
155+
AcrossChain memory chain = knownChain(srcChainId);
156+
if (chain.bridge != address(0)) {
157+
return chain.bridge;
158+
} else {
159+
revert Errors.NoKnownBridge("Across", srcChainId);
160+
}
161+
}
162+
163+
function bridgeScriptSource() internal pure returns (bytes memory) {
164+
return type(AcrossActions).creationCode;
165+
}
166+
167+
function encodeBridgeAction(
168+
uint256 srcChainId,
169+
uint256 dstChainId,
170+
address inputToken,
171+
address outputToken,
172+
uint256 inputAmount,
173+
uint256 outputAmount,
174+
address recipient,
175+
uint256 blockTimestamp,
176+
bool useNativeToken
177+
) internal pure returns (bytes memory) {
178+
return abi.encodeCall(
179+
AcrossActions.depositV3,
180+
(
181+
knownBridge(srcChainId), // spokePool
182+
// TODO: Should this be account, instead of recipient?
183+
recipient, // depositor
184+
recipient, // recipient
185+
inputToken, // inputToken
186+
outputToken, // outputToken
187+
inputAmount, // inputAmount
188+
outputAmount, // outputAmount
189+
dstChainId, // destinationChainId
190+
address(0), // exclusiveRelayer
191+
uint32(blockTimestamp) - QUOTE_TIMESTAMP_BUFFER, // quoteTimestamp
192+
uint32(blockTimestamp + FILL_DEADLINE_BUFFER), // fillDeadline
193+
0, // exclusivityDeadline
194+
new bytes(0), // message
195+
useNativeToken // useNativeToken
196+
)
197+
);
198+
}
199+
}

src/builder/Errors.sol

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ pragma solidity 0.8.27;
44
/// Library of shared errors used across Quark Builder files
55
library Errors {
66
error BadData();
7+
error NoKnownBridge(string bridgeType, uint256 srcChainId);
78
}

src/builder/FFI.sol

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// SPDX-License-Identifier: BSD-3-Clause
2+
pragma solidity ^0.8.27;
3+
4+
import {IAcrossFFI} from "src/interfaces/IAcrossFFI.sol";
5+
6+
/**
7+
* @title Foreign Function Interface (FFI) Helper
8+
* @notice Defines the addresses of reserved FFIs and methods for calling them
9+
*/
10+
library FFI {
11+
/// FFI Addresses (starts from 0xFF1000, FFI with 1000 reserved addresses)
12+
/// 0xFF1000-0xFF1009 are reserved for framework-level FFIs like console log
13+
address constant ACROSS_FFI_ADDRESS = address(0xFF1010);
14+
15+
function requestAcrossQuote(
16+
address inputToken,
17+
address outputToken,
18+
uint256 srcChain,
19+
uint256 dstChain,
20+
uint256 amount
21+
) internal pure returns (uint256 gasFee, uint256 variableFeePct) {
22+
// Make FFI call to fetch a quote from Across API
23+
return IAcrossFFI(ACROSS_FFI_ADDRESS).requestAcrossQuote(inputToken, outputToken, srcChain, dstChain, amount);
24+
}
25+
}

src/builder/HashMap.sol

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,12 +92,12 @@ library HashMap {
9292
return remove(map, abi.encode(key));
9393
}
9494

95-
function getUint256(Map memory map, uint256 key) internal pure returns (uint256) {
96-
return abi.decode(get(map, abi.encode(key)), (uint256));
95+
function getUint256(Map memory map, bytes memory key) internal pure returns (uint256) {
96+
return abi.decode(get(map, key), (uint256));
9797
}
9898

99-
function putUint256(Map memory map, uint256 key, uint256 value) internal pure returns (Map memory) {
100-
return put(map, abi.encode(key), abi.encode(value));
99+
function putUint256(Map memory map, bytes memory key, uint256 value) internal pure returns (Map memory) {
100+
return put(map, key, abi.encode(value));
101101
}
102102

103103
function keysUint256(Map memory map) internal pure returns (uint256[] memory) {

src/builder/QuarkBuilderBase.sol

Lines changed: 50 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {PaymentInfo} from "src/builder/PaymentInfo.sol";
1616
import {TokenWrapper} from "src/builder/TokenWrapper.sol";
1717
import {QuarkOperationHelper} from "src/builder/QuarkOperationHelper.sol";
1818
import {List} from "src/builder/List.sol";
19+
import {HashMap} from "src/builder/HashMap.sol";
1920

2021
contract QuarkBuilderBase {
2122
/* ===== Output Types ===== */
@@ -36,7 +37,7 @@ contract QuarkBuilderBase {
3637

3738
/* ===== Constants ===== */
3839

39-
string constant VERSION = "0.1.2";
40+
string constant VERSION = "0.2.0";
4041

4142
/* ===== Custom Errors ===== */
4243

@@ -103,40 +104,33 @@ contract QuarkBuilderBase {
103104
// Flag to check if the assetSymbolOut (used/supplied/transferred out) is the same as the payment token
104105
bool paymentTokenIsPartOfAssetSymbolOuts = false;
105106

107+
// Track the amount of each asset that will be bridged to the destination chain
108+
HashMap.Map memory assetsBridged = HashMap.newMap();
109+
106110
for (uint256 i = 0; i < actionIntent.assetSymbolOuts.length; ++i) {
111+
string memory assetSymbolOut = actionIntent.assetSymbolOuts[i];
107112
assertFundsAvailable(
108-
actionIntent.chainId,
109-
actionIntent.assetSymbolOuts[i],
110-
actionIntent.amountOuts[i],
111-
chainAccountsList,
112-
payment
113+
actionIntent.chainId, assetSymbolOut, actionIntent.amountOuts[i], chainAccountsList, payment
113114
);
114115
// Check if the assetSymbolOut is the same as the payment token
115-
if (Strings.stringEqIgnoreCase(actionIntent.assetSymbolOuts[i], payment.currency)) {
116+
if (Strings.stringEqIgnoreCase(assetSymbolOut, payment.currency)) {
116117
paymentTokenIsPartOfAssetSymbolOuts = true;
117118
}
118119

119120
if (
120121
needsBridgedFunds(
121-
actionIntent.assetSymbolOuts[i],
122-
actionIntent.amountOuts[i],
123-
actionIntent.chainId,
124-
chainAccountsList,
125-
payment
122+
assetSymbolOut, actionIntent.amountOuts[i], actionIntent.chainId, chainAccountsList, payment
126123
)
127124
) {
128125
if (actionIntent.bridgeEnabled) {
129126
uint256 amountNeededOnDst = actionIntent.amountOuts[i];
130-
if (
131-
payment.isToken && Strings.stringEqIgnoreCase(payment.currency, actionIntent.assetSymbolOuts[i])
132-
) {
127+
if (payment.isToken && Strings.stringEqIgnoreCase(payment.currency, assetSymbolOut)) {
133128
amountNeededOnDst += PaymentInfo.findMaxCost(payment, actionIntent.chainId);
134129
}
135-
136130
(IQuarkWallet.QuarkOperation[] memory bridgeQuarkOperations, Actions.Action[] memory bridgeActions)
137131
= Actions.constructBridgeOperations(
138132
Actions.BridgeOperationInfo({
139-
assetSymbol: actionIntent.assetSymbolOuts[i],
133+
assetSymbol: assetSymbolOut,
140134
amountNeededOnDst: amountNeededOnDst,
141135
dstChainId: actionIntent.chainId,
142136
recipient: actionIntent.actor,
@@ -147,22 +141,31 @@ contract QuarkBuilderBase {
147141
payment
148142
);
149143

144+
// Track how much is actually bridged for each asset
145+
if (HashMap.contains(assetsBridged, abi.encode(assetSymbolOut))) {
146+
uint256 existingAmountBridged = HashMap.getUint256(assetsBridged, abi.encode(assetSymbolOut));
147+
HashMap.putUint256(
148+
assetsBridged, abi.encode(assetSymbolOut), existingAmountBridged + amountNeededOnDst
149+
);
150+
} else {
151+
HashMap.putUint256(assetsBridged, abi.encode(assetSymbolOut), amountNeededOnDst);
152+
}
153+
150154
for (uint256 j = 0; j < bridgeQuarkOperations.length; ++j) {
151155
List.addQuarkOperation(quarkOperations, bridgeQuarkOperations[j]);
152156
List.addAction(actions, bridgeActions[j]);
153157
}
154158
} else {
155-
uint256 balanceOnChain = getBalanceOnChain(
156-
actionIntent.assetSymbolOuts[i], actionIntent.chainId, chainAccountsList, payment
157-
);
159+
uint256 balanceOnChain =
160+
getBalanceOnChain(assetSymbolOut, actionIntent.chainId, chainAccountsList, payment);
158161
uint256 amountNeededOnChain = getAmountNeededOnChain(
159-
actionIntent.assetSymbolOuts[i], actionIntent.amountOuts[i], actionIntent.chainId, payment
162+
assetSymbolOut, actionIntent.amountOuts[i], actionIntent.chainId, payment
160163
);
161164
uint256 maxCostOnChain =
162165
payment.isToken ? PaymentInfo.findMaxCost(payment, actionIntent.chainId) : 0;
163166
uint256 availableAssetBalance =
164167
balanceOnChain >= maxCostOnChain ? balanceOnChain - maxCostOnChain : 0;
165-
revert FundsUnavailable(actionIntent.assetSymbolOuts[i], amountNeededOnChain, availableAssetBalance);
168+
revert FundsUnavailable(assetSymbolOut, amountNeededOnChain, availableAssetBalance);
166169
}
167170
}
168171
}
@@ -196,6 +199,16 @@ contract QuarkBuilderBase {
196199
payment
197200
);
198201

202+
// Track how much is actually bridged for the payment asset
203+
if (HashMap.contains(assetsBridged, abi.encode(payment.currency))) {
204+
uint256 existingAmountBridged = HashMap.getUint256(assetsBridged, abi.encode(payment.currency));
205+
HashMap.putUint256(
206+
assetsBridged, abi.encode(payment.currency), existingAmountBridged + maxCostOnDstChain
207+
);
208+
} else {
209+
HashMap.putUint256(assetsBridged, abi.encode(payment.currency), maxCostOnDstChain);
210+
}
211+
199212
for (uint256 i = 0; i < bridgeQuarkOperations.length; ++i) {
200213
List.addQuarkOperation(quarkOperations, bridgeQuarkOperations[i]);
201214
List.addAction(actions, bridgeActions[i]);
@@ -212,13 +225,18 @@ contract QuarkBuilderBase {
212225

213226
if (actionIntent.autoWrapperEnabled) {
214227
for (uint256 i = 0; i < actionIntent.assetSymbolOuts.length; ++i) {
228+
string memory assetSymbolOut = actionIntent.assetSymbolOuts[i];
229+
uint256 supplementalBalance = HashMap.contains(assetsBridged, abi.encode(assetSymbolOut))
230+
? HashMap.getUint256(assetsBridged, abi.encode(assetSymbolOut))
231+
: 0;
215232
checkAndInsertWrapOrUnwrapAction({
216233
actions: actions,
217234
quarkOperations: quarkOperations,
218235
chainAccountsList: chainAccountsList,
219236
payment: payment,
220-
assetSymbol: actionIntent.assetSymbolOuts[i],
237+
assetSymbol: assetSymbolOut,
221238
amount: actionIntent.amountOuts[i],
239+
supplementalBalance: supplementalBalance,
222240
chainId: actionIntent.chainId,
223241
account: actionIntent.actor,
224242
blockTimestamp: actionIntent.blockTimestamp,
@@ -373,6 +391,10 @@ contract QuarkBuilderBase {
373391
/**
374392
* @dev If there is not enough of the asset to cover the amount and the asset has a counterpart asset,
375393
* insert a wrap/unwrap action to cover the gap in amount.
394+
*
395+
* `supplementalBalance` param describes an amount of the token that might have been received in the course
396+
* of an action (for example, bridging to the destination chain), which would therefore not be present in
397+
* `chainAccountsList` but could be used to cover action costs.
376398
*/
377399
function checkAndInsertWrapOrUnwrapAction(
378400
List.DynamicArray memory actions,
@@ -381,13 +403,15 @@ contract QuarkBuilderBase {
381403
PaymentInfo.Payment memory payment,
382404
string memory assetSymbol,
383405
uint256 amount,
406+
uint256 supplementalBalance,
384407
uint256 chainId,
385408
address account,
386409
uint256 blockTimestamp,
387410
bool useQuotecall
388411
) internal pure {
389412
// Check if inserting wrapOrUnwrap action is necessary
390-
uint256 assetBalanceOnChain = Accounts.getBalanceOnChain(assetSymbol, chainId, chainAccountsList);
413+
uint256 assetBalanceOnChain =
414+
Accounts.getBalanceOnChain(assetSymbol, chainId, chainAccountsList) + supplementalBalance;
391415
if (assetBalanceOnChain < amount && TokenWrapper.hasWrapperContract(chainId, assetSymbol)) {
392416
// If the asset has a wrapper counterpart, wrap/unwrap the token to cover the transferIntent amount
393417
string memory counterpartSymbol = TokenWrapper.getWrapperCounterpartSymbol(chainId, assetSymbol);
@@ -468,7 +492,7 @@ contract QuarkBuilderBase {
468492
if (bridgeActionContext.token == bridgeActions[i].paymentToken) {
469493
// If the payment token is the transfer token and this is the target chain, we need to account for the transfer amount
470494
// If its bridge step, check if user has enough balance to cover the bridge amount
471-
if (paymentAssetBalanceOnChain < bridgeActions[i].paymentMaxCost + bridgeActionContext.amount) {
495+
if (paymentAssetBalanceOnChain < bridgeActions[i].paymentMaxCost + bridgeActionContext.inputAmount) {
472496
revert MaxCostTooHigh();
473497
}
474498
} else {
@@ -479,7 +503,7 @@ contract QuarkBuilderBase {
479503
}
480504

481505
if (Strings.stringEqIgnoreCase(bridgeActionContext.assetSymbol, paymentTokenSymbol)) {
482-
paymentTokenBridgeAmount += bridgeActionContext.amount;
506+
paymentTokenBridgeAmount += bridgeActionContext.outputAmount;
483507
}
484508
}
485509

0 commit comments

Comments
 (0)