Skip to content

Commit 65e49d3

Browse files
authored
Implement simple bridge algo (#17)
This implements a basic algo for figuring the amount to bridge from each chain. It loops through account balances on each chain and greedily attempts to bridge until the total amount needed is reached.
1 parent 9fa905d commit 65e49d3

10 files changed

+269
-67
lines changed

.github/workflows/gas-snapshot.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ on:
55
pull_request:
66

77
env:
8-
FOUNDRY_PROFILE: ci
8+
FOUNDRY_PROFILE: ir
99

1010
jobs:
1111
snapshot-gas:
@@ -29,7 +29,7 @@ jobs:
2929
run: |
3030
set -euo pipefail
3131
# grep -E '^test' -- skip over test results, just get diffs
32-
FOUNDRY_PROFILE=ir forge snapshot --diff \
32+
forge snapshot --diff \
3333
| grep -E '^test' \
3434
| tee .gas-snapshot.new
3535
env:

.github/workflows/lint.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ on:
55
pull_request:
66

77
env:
8-
FOUNDRY_PROFILE: ci
8+
FOUNDRY_PROFILE: ir
99

1010
jobs:
1111
check:

.github/workflows/test.yml

+3-3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ on:
55
pull_request:
66

77
env:
8-
FOUNDRY_PROFILE: ci
8+
FOUNDRY_PROFILE: ir
99

1010
jobs:
1111
check:
@@ -30,12 +30,12 @@ jobs:
3030
- name: Run Forge build
3131
run: |
3232
forge --version
33-
FOUNDRY_PROFILE=ir forge build --sizes
33+
forge build --sizes
3434
id: build
3535

3636
- name: Run Forge tests
3737
run: |
38-
FOUNDRY_PROFILE=ir forge test
38+
forge test
3939
id: test
4040
env:
4141
NODE_PROVIDER_BYPASS_KEY: ${{ secrets.NODE_PROVIDER_BYPASS_KEY }}

.github/workflows/vendoza.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ on:
55
pull_request:
66

77
env:
8-
FOUNDRY_PROFILE: ci
8+
FOUNDRY_PROFILE: ir
99

1010
jobs:
1111
check:

src/builder/Accounts.sol

+9
Original file line numberDiff line numberDiff line change
@@ -88,4 +88,13 @@ library Accounts {
8888
}
8989
return totalBalance;
9090
}
91+
92+
function getBalanceOnChain(string memory assetSymbol, uint256 chainId, ChainAccounts[] memory chainAccountsList)
93+
internal
94+
pure
95+
returns (uint256)
96+
{
97+
AssetPositions memory positions = findAssetPositions(assetSymbol, chainId, chainAccountsList);
98+
return sumBalances(positions);
99+
}
91100
}

src/builder/Actions.sol

+49-15
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ library Actions {
2424

2525
/* ===== Custom Errors ===== */
2626

27+
error BridgingUnsupportedForAsset();
2728
error InvalidAssetForBridge();
2829

2930
/* ===== Input Types ===== */
@@ -38,11 +39,11 @@ library Actions {
3839
uint256 blockTimestamp;
3940
}
4041

41-
struct BridgeUSDC {
42+
struct BridgeAsset {
4243
Accounts.ChainAccounts[] chainAccountsList;
4344
string assetSymbol;
4445
uint256 amount;
45-
uint256 originChainId;
46+
uint256 srcChainId;
4647
address sender;
4748
uint256 destinationChainId;
4849
address recipient;
@@ -83,37 +84,70 @@ library Actions {
8384
uint256 destinationChainId;
8485
}
8586

86-
function bridgeUSDC(BridgeUSDC memory bridge)
87+
function bridgeAsset(BridgeAsset memory bridge)
8788
internal
8889
pure
89-
returns (IQuarkWallet.QuarkOperation memory /*, QuarkAction memory*/ )
90+
returns (IQuarkWallet.QuarkOperation memory, Action memory)
91+
{
92+
if (Strings.stringEqIgnoreCase(bridge.assetSymbol, "USDC")) {
93+
return bridgeUSDC(bridge);
94+
} else {
95+
revert BridgingUnsupportedForAsset();
96+
}
97+
}
98+
99+
function bridgeUSDC(BridgeAsset memory bridge)
100+
internal
101+
pure
102+
returns (IQuarkWallet.QuarkOperation memory, Action memory)
90103
{
91104
if (!Strings.stringEqIgnoreCase(bridge.assetSymbol, "USDC")) {
92105
revert InvalidAssetForBridge();
93106
}
94107

95-
Accounts.ChainAccounts memory originChainAccounts =
96-
Accounts.findChainAccounts(bridge.originChainId, bridge.chainAccountsList);
108+
Accounts.ChainAccounts memory srcChainAccounts =
109+
Accounts.findChainAccounts(bridge.srcChainId, bridge.chainAccountsList);
97110

98-
Accounts.AssetPositions memory originUSDCPositions =
99-
Accounts.findAssetPositions("USDC", originChainAccounts.assetPositionsList);
111+
Accounts.AssetPositions memory srcUSDCPositions =
112+
Accounts.findAssetPositions("USDC", srcChainAccounts.assetPositionsList);
100113

101-
Accounts.QuarkState memory accountState =
102-
Accounts.findQuarkState(bridge.sender, originChainAccounts.quarkStates);
114+
Accounts.QuarkState memory accountState = Accounts.findQuarkState(bridge.sender, srcChainAccounts.quarkStates);
103115

104116
bytes[] memory scriptSources = new bytes[](1);
105117
scriptSources[0] = CCTP.bridgeScriptSource();
106118

107-
// CCTP bridge
108-
return IQuarkWallet.QuarkOperation({
119+
// Construct QuarkOperation
120+
IQuarkWallet.QuarkOperation memory quarkOperation = IQuarkWallet.QuarkOperation({
109121
nonce: accountState.quarkNextNonce,
110-
scriptAddress: CodeJarHelper.getCodeAddress(bridge.originChainId, scriptSources[0]),
122+
scriptAddress: CodeJarHelper.getCodeAddress(scriptSources[0]),
111123
scriptCalldata: CCTP.encodeBridgeUSDC(
112-
bridge.originChainId, bridge.destinationChainId, bridge.amount, bridge.recipient, originUSDCPositions.asset
124+
bridge.srcChainId, bridge.destinationChainId, bridge.amount, bridge.recipient, srcUSDCPositions.asset
113125
),
114126
scriptSources: scriptSources,
115127
expiry: bridge.blockTimestamp + BRIDGE_EXPIRY_BUFFER
116128
});
129+
130+
// Construct Action
131+
BridgeActionContext memory bridgeActionContext = BridgeActionContext({
132+
amount: bridge.amount,
133+
price: srcUSDCPositions.usdPrice,
134+
token: srcUSDCPositions.asset,
135+
chainId: bridge.srcChainId,
136+
recipient: bridge.recipient,
137+
destinationChainId: bridge.destinationChainId
138+
});
139+
Action memory action = Actions.Action({
140+
chainId: bridge.srcChainId,
141+
quarkAccount: bridge.sender,
142+
actionType: ACTION_TYPE_BRIDGE,
143+
actionContext: abi.encode(bridgeActionContext),
144+
paymentMethod: PAYMENT_METHOD_OFFCHAIN,
145+
// Null address for OFFCHAIN payment.
146+
paymentToken: address(0),
147+
paymentMaxCost: 0
148+
});
149+
150+
return (quarkOperation, action);
117151
}
118152

119153
// TODO: Handle paycall
@@ -149,7 +183,7 @@ library Actions {
149183
// Construct QuarkOperation
150184
IQuarkWallet.QuarkOperation memory quarkOperation = IQuarkWallet.QuarkOperation({
151185
nonce: accountState.quarkNextNonce,
152-
scriptAddress: CodeJarHelper.getCodeAddress(transfer.chainId, type(TransferActions).creationCode),
186+
scriptAddress: CodeJarHelper.getCodeAddress(type(TransferActions).creationCode),
153187
scriptCalldata: scriptCalldata,
154188
scriptSources: scriptSources,
155189
expiry: transfer.blockTimestamp + TRANSFER_EXPIRY_BUFFER

src/builder/BridgeRoutes.sol

+4-4
Original file line numberDiff line numberDiff line change
@@ -74,17 +74,17 @@ library CCTP {
7474
}
7575

7676
function encodeBridgeUSDC(
77-
uint256 originChainId,
78-
uint256 destChainId,
77+
uint256 srcChainId,
78+
uint256 dstChainId,
7979
uint256 amount,
8080
address recipient,
8181
address usdcAddress
8282
) internal pure returns (bytes memory) {
8383
return abi.encodeWithSelector(
8484
CCTPBridgeActions.bridgeUSDC.selector,
85-
knownBridge(originChainId),
85+
knownBridge(srcChainId),
8686
amount,
87-
knownDomainId(destChainId),
87+
knownDomainId(dstChainId),
8888
bytes32(uint256(uint160(recipient))),
8989
usdcAddress
9090
);

src/builder/CodeJarHelper.sol

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ library CodeJarHelper {
77
/// @notice The address for CodeJar on all chains
88
address constant CODE_JAR_ADDRESS = 0x2b68764bCfE9fCD8d5a30a281F141f69b69Ae3C8;
99

10-
function getCodeAddress(uint256 chainId, bytes memory code) public pure returns (address) {
10+
function getCodeAddress(bytes memory code) public pure returns (address) {
1111
return address(
1212
uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), CODE_JAR_ADDRESS, uint256(0), keccak256(code)))))
1313
);

src/builder/QuarkBuilder.sol

+69-35
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ contract QuarkBuilder {
1313
/* ===== Constants ===== */
1414

1515
string constant VERSION = "1.0.0";
16+
uint256 constant MAX_BRIDGE_ACTION = 1;
1617

1718
/* ===== Custom Errors ===== */
1819

@@ -21,6 +22,7 @@ contract QuarkBuilder {
2122
error InsufficientFunds();
2223
error InvalidInput();
2324
error MaxCostTooHigh();
25+
error TooManyBridgeOperations();
2426

2527
/* ===== Input Types ===== */
2628

@@ -93,42 +95,75 @@ contract QuarkBuilder {
9395
new IQuarkWallet.QuarkOperation[](chainAccountsList.length);
9496

9597
if (needsBridgedFunds(transferIntent, chainAccountsList)) {
96-
// TODO: actually enumerate chain accounts other than the destination chain,
97-
// and check balances and choose amounts to send and from which.
98-
//
99-
// for now: simplify!
100-
// only check 8453 (Base mainnet);
101-
// check every account;
102-
// sum the balances and if there's enough to cover the gap,
103-
// bridge from each account in arbitrary order of appearance
104-
// until there is enough.
105-
if (payment.isToken) {
106-
// wrap around paycall
107-
// TODO: need to embed price feed addresses for known tokens before we can do paycall.
108-
// ^^^ look up USDC price feeds for each supported chain?
109-
// we only need USDC/USD and only on chains 1 (mainnet) and 8453 (base mainnet).
110-
} else {
111-
quarkOperations[actionIndex++] = Actions.bridgeUSDC(
112-
Actions.BridgeUSDC({
113-
chainAccountsList: chainAccountsList,
114-
assetSymbol: transferIntent.assetSymbol,
115-
amount: transferIntent.amount,
116-
// where it comes from
117-
originChainId: 8453, // FIXME: originChainId
118-
sender: address(0), // FIXME: sender
119-
// where it goes
120-
destinationChainId: transferIntent.chainId,
121-
recipient: transferIntent.recipient,
122-
blockTimestamp: transferIntent.blockTimestamp
123-
})
124-
);
125-
// TODO: also append a Actions.Action to the actions array.
126-
// See: BridgeUSDC TODO for returning a Actions.Action.
98+
// Note: Assumes that the asset uses the same # of decimals on each chain
99+
uint256 balanceOnDstChain =
100+
Accounts.getBalanceOnChain(transferIntent.assetSymbol, transferIntent.chainId, chainAccountsList);
101+
uint256 amountLeftToBridge = transferIntent.amount - balanceOnDstChain;
102+
103+
uint256 bridgeActionCount = 0;
104+
// TODO: bridge routing logic (which bridge to prioritize, how many bridges?)
105+
// Iterate chainAccountList and find upto 2 chains that can provide enough fund
106+
// Backend can provide optimal routes by adjust the order in chainAccountList.
107+
for (uint256 i = 0; i < chainAccountsList.length; ++i) {
108+
if (amountLeftToBridge == 0) {
109+
break;
110+
}
111+
112+
Accounts.ChainAccounts memory srcChainAccounts = chainAccountsList[i];
113+
if (srcChainAccounts.chainId == transferIntent.chainId) {
114+
continue;
115+
}
116+
117+
if (
118+
!BridgeRoutes.canBridge(srcChainAccounts.chainId, transferIntent.chainId, transferIntent.assetSymbol)
119+
) {
120+
continue;
121+
}
122+
123+
Accounts.AssetPositions memory srcAssetPositions =
124+
Accounts.findAssetPositions(transferIntent.assetSymbol, srcChainAccounts.assetPositionsList);
125+
Accounts.AccountBalance[] memory srcAccountBalances = srcAssetPositions.accountBalances;
126+
// TODO: Make logic smarter. Currently, this uses a greedy algorithm.
127+
// e.g. Optimize by trying to bridge with the least amount of bridge operations
128+
for (uint256 j = 0; j < srcAccountBalances.length; ++j) {
129+
if (bridgeActionCount >= MAX_BRIDGE_ACTION) {
130+
revert TooManyBridgeOperations();
131+
}
132+
133+
uint256 amountToBridge = srcAccountBalances[j].balance >= amountLeftToBridge
134+
? amountLeftToBridge
135+
: srcAccountBalances[j].balance;
136+
amountLeftToBridge -= amountToBridge;
137+
138+
if (payment.isToken) {
139+
// TODO: wrap around paycall
140+
} else {
141+
(quarkOperations[actionIndex], actions[actionIndex]) = Actions.bridgeAsset(
142+
Actions.BridgeAsset({
143+
chainAccountsList: chainAccountsList,
144+
assetSymbol: transferIntent.assetSymbol,
145+
amount: amountToBridge,
146+
// where it comes from
147+
srcChainId: srcChainAccounts.chainId,
148+
sender: srcAccountBalances[j].account,
149+
// where it goes
150+
destinationChainId: transferIntent.chainId,
151+
recipient: transferIntent.sender,
152+
blockTimestamp: transferIntent.blockTimestamp
153+
})
154+
);
155+
actionIndex++;
156+
bridgeActionCount++;
157+
}
158+
}
159+
}
160+
161+
if (amountLeftToBridge > 0) {
162+
revert FundsUnavailable();
127163
}
128164
}
129165

130166
// Then, transferIntent `amount` of `assetSymbol` to `recipient`
131-
// TODO: construct action contexts
132167
if (payment.isToken) {
133168
// wrap around paycall
134169
} else {
@@ -221,9 +256,8 @@ contract QuarkBuilder {
221256
pure
222257
returns (bool)
223258
{
224-
Accounts.AssetPositions memory localPositions =
225-
Accounts.findAssetPositions(transferIntent.assetSymbol, transferIntent.chainId, chainAccountsList);
226-
return Accounts.sumBalances(localPositions) < transferIntent.amount;
259+
return Accounts.getBalanceOnChain(transferIntent.assetSymbol, transferIntent.chainId, chainAccountsList)
260+
< transferIntent.amount;
227261
}
228262

229263
// Assert that each chain has sufficient funds to cover the max cost for that chain.

0 commit comments

Comments
 (0)