@bananapus/suckers-v6 0.0.40 → 0.0.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/foundry.toml +1 -0
- package/package.json +2 -2
- package/src/JBSwapCCIPSucker.sol +60 -37
- package/src/libraries/JBSwapPoolLib.sol +34 -2
package/foundry.toml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bananapus/suckers-v6",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.42",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@arbitrum/nitro-contracts": "3.2.0",
|
|
33
|
-
"@bananapus/core-v6": "^0.0.
|
|
33
|
+
"@bananapus/core-v6": "^0.0.48",
|
|
34
34
|
"@bananapus/permission-ids-v6": "^0.0.25",
|
|
35
35
|
"@chainlink/contracts-ccip": "1.6.4",
|
|
36
36
|
"@chainlink/local": "0.2.7",
|
package/src/JBSwapCCIPSucker.sol
CHANGED
|
@@ -91,8 +91,11 @@ contract JBSwapCCIPSucker is JBCCIPSucker, IUnlockCallback, IUniswapV3SwapCallba
|
|
|
91
91
|
error JBSwapCCIPSucker_InvalidBridgeToken(address bridgeToken, address wrappedNativeToken);
|
|
92
92
|
error JBSwapCCIPSucker_NoPendingSwap(address localToken, uint64 nonce, bool retrySwapLocked);
|
|
93
93
|
error JBSwapCCIPSucker_OnlySelf(address caller, address expected);
|
|
94
|
+
error JBSwapCCIPSucker_PositiveRootWithoutDelivery(uint256 rootAmount);
|
|
94
95
|
error JBSwapCCIPSucker_SwapFailed(address tokenIn, address tokenOut, uint256 amountIn);
|
|
95
96
|
error JBSwapCCIPSucker_SwapPending(uint64 nonce);
|
|
97
|
+
error JBSwapCCIPSucker_UnexpectedDeliveredTokens(uint256 count);
|
|
98
|
+
error JBSwapCCIPSucker_WrongDeliveredToken(address delivered, address expected);
|
|
96
99
|
|
|
97
100
|
//*********************************************************************//
|
|
98
101
|
// ------------------------------ events ----------------------------- //
|
|
@@ -278,22 +281,58 @@ contract JBSwapCCIPSucker is JBCCIPSucker, IUnlockCallback, IUniswapV3SwapCallba
|
|
|
278
281
|
abi.decode(payload, (JBMessageRoot, uint256, uint256));
|
|
279
282
|
|
|
280
283
|
address localToken = _toAddress(root.token);
|
|
284
|
+
uint64 nonce = root.remoteRoot.nonce;
|
|
285
|
+
uint256 leafTotal = root.amount;
|
|
281
286
|
uint256 localAmount;
|
|
282
287
|
bool swapFailed;
|
|
288
|
+
// Cache the single delivered entry once so subsequent branches reuse it without re-indexing
|
|
289
|
+
// calldata. `deliveredAmount > 0` later implies a delivery was present.
|
|
290
|
+
address deliveredToken;
|
|
291
|
+
uint256 deliveredAmount;
|
|
292
|
+
{
|
|
293
|
+
// Send-side guarantees: at most one entry in `destTokenAmounts` (length 0 for zero-value
|
|
294
|
+
// batches, length 1 for value-bearing batches), and when present the delivered token is
|
|
295
|
+
// `BRIDGE_TOKEN`. Refuse anything that deviates so a peer compromise or a malformed CCIP
|
|
296
|
+
// delivery cannot register positive root accounting against zero or wrong-token backing.
|
|
297
|
+
uint256 deliveryCount = any2EvmMessage.destTokenAmounts.length;
|
|
298
|
+
if (deliveryCount > 1) {
|
|
299
|
+
revert JBSwapCCIPSucker_UnexpectedDeliveredTokens(deliveryCount);
|
|
300
|
+
}
|
|
301
|
+
if (deliveryCount == 0) {
|
|
302
|
+
if (leafTotal > 0) revert JBSwapCCIPSucker_PositiveRootWithoutDelivery(leafTotal);
|
|
303
|
+
} else {
|
|
304
|
+
Client.EVMTokenAmount calldata delivered = any2EvmMessage.destTokenAmounts[0];
|
|
305
|
+
deliveredToken = delivered.token;
|
|
306
|
+
deliveredAmount = delivered.amount;
|
|
307
|
+
if (deliveredToken != address(BRIDGE_TOKEN)) {
|
|
308
|
+
revert JBSwapCCIPSucker_WrongDeliveredToken({
|
|
309
|
+
delivered: deliveredToken, expected: address(BRIDGE_TOKEN)
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
// Zero delivery alongside a positive root is structurally indistinguishable from
|
|
313
|
+
// "no delivery + positive root" — both leave the local sucker with nothing to back
|
|
314
|
+
// the leaves the root advertises. Reject so a peer cannot mint a claimable rate
|
|
315
|
+
// that records `leafTotal=N, localTotal=0` and lets later claims withdraw against
|
|
316
|
+
// unrelated balance.
|
|
317
|
+
if (leafTotal > 0 && deliveredAmount == 0) {
|
|
318
|
+
revert JBSwapCCIPSucker_PositiveRootWithoutDelivery(leafTotal);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
283
322
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
if (localToken == address(BRIDGE_TOKEN) || localToken ==
|
|
323
|
+
// After the validation block above, `deliveredToken != address(0)` iff a delivery was present,
|
|
324
|
+
// because the invariants ensure it equals `BRIDGE_TOKEN` (a non-zero ERC-20) whenever there is one.
|
|
325
|
+
if (deliveredToken != address(0)) {
|
|
326
|
+
if (localToken == address(BRIDGE_TOKEN) || localToken == deliveredToken) {
|
|
288
327
|
// No swap needed — bridge token IS the local token.
|
|
289
|
-
localAmount =
|
|
328
|
+
localAmount = deliveredAmount;
|
|
290
329
|
} else {
|
|
291
330
|
// Swap bridge token -> local token via best V3/V4 pool.
|
|
292
331
|
// Wrapped in try-catch so a swap failure doesn't revert the entire CCIP message
|
|
293
332
|
// (which would leave tokens stuck in the OffRamp). On failure, bridge tokens are
|
|
294
333
|
// stored for later retry via `retrySwap` (written below, after nonce validation).
|
|
295
334
|
try this.executeSwapExternal({
|
|
296
|
-
tokenIn:
|
|
335
|
+
tokenIn: deliveredToken, tokenOut: localToken, amount: deliveredAmount
|
|
297
336
|
}) returns (
|
|
298
337
|
uint256 swapped
|
|
299
338
|
) {
|
|
@@ -321,28 +360,23 @@ contract JBSwapCCIPSucker is JBCCIPSucker, IUnlockCallback, IUniswapV3SwapCallba
|
|
|
321
360
|
// Detect "already seen" without extra storage: a nonce has been processed if it has
|
|
322
361
|
// either a batch range (batchEnd > 0) or a conversion rate / pending swap recorded.
|
|
323
362
|
if (
|
|
324
|
-
_batchEndOf[localToken][
|
|
325
|
-
&&
|
|
326
|
-
&& pendingSwapOf[localToken][root.remoteRoot.nonce].leafTotal == 0
|
|
363
|
+
_batchEndOf[localToken][nonce] == 0 && _conversionRateOf[localToken][nonce].leafTotal == 0
|
|
364
|
+
&& pendingSwapOf[localToken][nonce].leafTotal == 0
|
|
327
365
|
) {
|
|
328
366
|
// Record the batch range so _findNonceForLeafIndex can resolve leaf ownership
|
|
329
367
|
// independently of nonce ordering. Each nonce is self-describing: [start, end).
|
|
330
368
|
if (batchEnd > 0) {
|
|
331
|
-
_batchStartOf[localToken][
|
|
332
|
-
_batchEndOf[localToken][
|
|
333
|
-
if (
|
|
334
|
-
_highestReceivedNonce[localToken] =
|
|
369
|
+
_batchStartOf[localToken][nonce] = batchStart;
|
|
370
|
+
_batchEndOf[localToken][nonce] = batchEnd;
|
|
371
|
+
if (nonce > _highestReceivedNonce[localToken]) {
|
|
372
|
+
_highestReceivedNonce[localToken] = nonce;
|
|
335
373
|
}
|
|
336
374
|
}
|
|
337
375
|
|
|
338
376
|
// Store pendingSwapOf for failed swaps now that nonce is validated.
|
|
339
377
|
if (swapFailed) {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
bridgeToken: failedTokenAmount.token,
|
|
343
|
-
bridgeAmount: failedTokenAmount.amount,
|
|
344
|
-
leafTotal: root.amount
|
|
345
|
-
});
|
|
378
|
+
pendingSwapOf[localToken][nonce] =
|
|
379
|
+
PendingSwap({bridgeToken: deliveredToken, bridgeAmount: deliveredAmount, leafTotal: leafTotal});
|
|
346
380
|
}
|
|
347
381
|
|
|
348
382
|
// Zero-output swap guard: When a swap succeeds but returns zero local tokens, the
|
|
@@ -354,25 +388,14 @@ contract JBSwapCCIPSucker is JBCCIPSucker, IUnlockCallback, IUniswapV3SwapCallba
|
|
|
354
388
|
// Route zero-output swaps into `pendingSwapOf` so the swap can be retried via
|
|
355
389
|
// `retrySwap` once pool conditions improve. Only store the conversion rate when
|
|
356
390
|
// the swap produced a positive local amount.
|
|
357
|
-
if (
|
|
358
|
-
if (localAmount == 0 &&
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
// normally — there is nothing to retry.
|
|
363
|
-
if (zeroSwapTokenAmount.amount > 0) {
|
|
364
|
-
pendingSwapOf[localToken][root.remoteRoot.nonce] = PendingSwap({
|
|
365
|
-
bridgeToken: zeroSwapTokenAmount.token,
|
|
366
|
-
bridgeAmount: zeroSwapTokenAmount.amount,
|
|
367
|
-
leafTotal: root.amount
|
|
368
|
-
});
|
|
369
|
-
} else {
|
|
370
|
-
_conversionRateOf[localToken][root.remoteRoot.nonce] =
|
|
371
|
-
ConversionRate({leafTotal: root.amount, localTotal: 0});
|
|
372
|
-
}
|
|
391
|
+
if (leafTotal > 0 && !swapFailed) {
|
|
392
|
+
if (localAmount == 0 && deliveredAmount > 0) {
|
|
393
|
+
pendingSwapOf[localToken][nonce] = PendingSwap({
|
|
394
|
+
bridgeToken: deliveredToken, bridgeAmount: deliveredAmount, leafTotal: leafTotal
|
|
395
|
+
});
|
|
373
396
|
} else {
|
|
374
|
-
_conversionRateOf[localToken][
|
|
375
|
-
ConversionRate({leafTotal:
|
|
397
|
+
_conversionRateOf[localToken][nonce] =
|
|
398
|
+
ConversionRate({leafTotal: leafTotal, localTotal: localAmount});
|
|
376
399
|
}
|
|
377
400
|
}
|
|
378
401
|
}
|
|
@@ -51,6 +51,7 @@ library JBSwapPoolLib {
|
|
|
51
51
|
error JBSwapPoolLib_InsufficientTwapHistory(address pool, uint256 availableWindow, uint256 requiredWindow);
|
|
52
52
|
error JBSwapPoolLib_NoLiquidity(address pool, PoolId poolId);
|
|
53
53
|
error JBSwapPoolLib_NoPool(address tokenIn, address tokenOut);
|
|
54
|
+
error JBSwapPoolLib_PartialFill(uint256 consumed, uint256 requested);
|
|
54
55
|
error JBSwapPoolLib_SlippageExceeded(uint256 amountOut, uint256 minAmountOut);
|
|
55
56
|
|
|
56
57
|
//*********************************************************************//
|
|
@@ -132,6 +133,7 @@ library JBSwapPoolLib {
|
|
|
132
133
|
config: config,
|
|
133
134
|
key: v4Key,
|
|
134
135
|
normalizedTokenIn: normalizedIn,
|
|
136
|
+
originalTokenIn: tokenIn,
|
|
135
137
|
amount: amount,
|
|
136
138
|
minAmountOut: minAmountOut
|
|
137
139
|
});
|
|
@@ -142,6 +144,7 @@ library JBSwapPoolLib {
|
|
|
142
144
|
key: v4Key,
|
|
143
145
|
normalizedTokenIn: normalizedIn,
|
|
144
146
|
normalizedTokenOut: normalizedOut,
|
|
147
|
+
originalTokenIn: tokenIn,
|
|
145
148
|
amount: amount
|
|
146
149
|
});
|
|
147
150
|
}
|
|
@@ -893,6 +896,7 @@ library JBSwapPoolLib {
|
|
|
893
896
|
PoolKey memory key,
|
|
894
897
|
address normalizedTokenIn,
|
|
895
898
|
address normalizedTokenOut,
|
|
899
|
+
address originalTokenIn,
|
|
896
900
|
uint256 amount
|
|
897
901
|
)
|
|
898
902
|
internal
|
|
@@ -909,7 +913,12 @@ library JBSwapPoolLib {
|
|
|
909
913
|
|
|
910
914
|
// Execute the swap through the V4 PoolManager.
|
|
911
915
|
amountOut = _executeV4Swap({
|
|
912
|
-
config: config,
|
|
916
|
+
config: config,
|
|
917
|
+
key: key,
|
|
918
|
+
normalizedTokenIn: normalizedTokenIn,
|
|
919
|
+
originalTokenIn: originalTokenIn,
|
|
920
|
+
amount: amount,
|
|
921
|
+
minAmountOut: minOut
|
|
913
922
|
});
|
|
914
923
|
}
|
|
915
924
|
|
|
@@ -981,6 +990,18 @@ library JBSwapPoolLib {
|
|
|
981
990
|
data: abi.encode(originalTokenIn, normalizedTokenIn, normalizedTokenOut)
|
|
982
991
|
});
|
|
983
992
|
|
|
993
|
+
// Reject partial fills: when the V3 pool's price limit is hit before the full input is
|
|
994
|
+
// consumed, the pool returns only the consumed portion of `amount` and the caller is left
|
|
995
|
+
// holding the unconsumed remainder. The sucker's accounting assumes the full bridge amount
|
|
996
|
+
// was either swapped or made retryable, so a silent partial fill strands input tokens and
|
|
997
|
+
// breaks cross-chain solvency. Revert so the caller can either retry with a smaller size
|
|
998
|
+
// or wait for liquidity to return.
|
|
999
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
1000
|
+
uint256 consumedAmount = uint256(zeroForOne ? amount0 : amount1);
|
|
1001
|
+
if (consumedAmount < amount) {
|
|
1002
|
+
revert JBSwapPoolLib_PartialFill({consumed: consumedAmount, requested: amount});
|
|
1003
|
+
}
|
|
1004
|
+
|
|
984
1005
|
// Extract the output amount from the signed delta (negative = tokens received).
|
|
985
1006
|
amountOut = uint256(-(zeroForOne ? amount1 : amount0));
|
|
986
1007
|
|
|
@@ -1001,6 +1022,7 @@ library JBSwapPoolLib {
|
|
|
1001
1022
|
SwapConfig memory config,
|
|
1002
1023
|
PoolKey memory key,
|
|
1003
1024
|
address normalizedTokenIn,
|
|
1025
|
+
address originalTokenIn,
|
|
1004
1026
|
uint256 amount,
|
|
1005
1027
|
uint256 minAmountOut
|
|
1006
1028
|
)
|
|
@@ -1013,6 +1035,16 @@ library JBSwapPoolLib {
|
|
|
1013
1035
|
// Determine swap direction based on currency ordering in the pool key.
|
|
1014
1036
|
bool zeroForOne = Currency.unwrap(key.currency0) == v4In;
|
|
1015
1037
|
|
|
1038
|
+
// Tell the unlock callback whether to consume any wrapped-native-token balance the caller may hold.
|
|
1039
|
+
// The pool's input side is native iff the swap input came from the wrapped-native ERC-20 (not the
|
|
1040
|
+
// NATIVE_TOKEN sentinel). If the caller's input was already native (NATIVE_TOKEN sentinel), the caller
|
|
1041
|
+
// holds raw ETH for THIS swap; any wrapped balance it holds is for unrelated reasons (e.g., backing other
|
|
1042
|
+
// claims) and must not be consumed here.
|
|
1043
|
+
address callbackWrappedNativeToken;
|
|
1044
|
+
if (v4In == address(0) && originalTokenIn != JBConstants.NATIVE_TOKEN) {
|
|
1045
|
+
callbackWrappedNativeToken = config.wrappedNativeToken;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1016
1048
|
// Build the encoded unlock data in a scoped block to avoid stack-too-deep.
|
|
1017
1049
|
bytes memory unlockData;
|
|
1018
1050
|
{
|
|
@@ -1027,7 +1059,7 @@ library JBSwapPoolLib {
|
|
|
1027
1059
|
int256 exactInputAmount = -int256(amount);
|
|
1028
1060
|
|
|
1029
1061
|
unlockData = abi.encode(
|
|
1030
|
-
key, zeroForOne, exactInputAmount, sqrtPriceLimitX96, minAmountOut,
|
|
1062
|
+
key, zeroForOne, exactInputAmount, sqrtPriceLimitX96, minAmountOut, callbackWrappedNativeToken
|
|
1031
1063
|
);
|
|
1032
1064
|
}
|
|
1033
1065
|
|