@bananapus/suckers-v6 0.0.71 → 0.0.73

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.
@@ -1,735 +0,0 @@
1
- // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.28;
3
-
4
- // Core JB imports.
5
- import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
6
- import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
7
- import {IJBTokens} from "@bananapus/core-v6/src/interfaces/IJBTokens.sol";
8
- // CCIP imports.
9
- import {Client} from "@chainlink/contracts-ccip/contracts/libraries/Client.sol";
10
-
11
- // OpenZeppelin imports.
12
- import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
13
- import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
14
-
15
- // Uniswap V3 imports.
16
- import {IUniswapV3Factory} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol";
17
- import {IUniswapV3SwapCallback} from "@uniswap/v3-core/contracts/interfaces/callback/IUniswapV3SwapCallback.sol";
18
-
19
- // Uniswap V4 imports.
20
- import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
21
- import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol";
22
-
23
- // Local: contracts.
24
- import {JBCCIPSucker} from "./JBCCIPSucker.sol";
25
-
26
- // Local: deployers.
27
- import {JBSwapCCIPSuckerDeployer} from "./deployers/JBSwapCCIPSuckerDeployer.sol";
28
-
29
- // Local: interfaces (alphabetized).
30
- import {IJBSuckerRegistry} from "./interfaces/IJBSuckerRegistry.sol";
31
- import {IJBSwapCCIPSuckerDeployer} from "./interfaces/IJBSwapCCIPSuckerDeployer.sol";
32
- import {IWrappedNativeToken} from "./interfaces/IWrappedNativeToken.sol";
33
-
34
- // Local: libraries.
35
- import {CCIPHelper} from "./libraries/CCIPHelper.sol";
36
- import {JBCCIPLib} from "./libraries/JBCCIPLib.sol";
37
- import {JBSwapPoolLib} from "./libraries/JBSwapPoolLib.sol";
38
-
39
- // Local: structs (alphabetized).
40
- import {JBClaim} from "./structs/JBClaim.sol";
41
- import {JBConversionRate} from "./structs/JBConversionRate.sol";
42
- import {JBMessageRoot} from "./structs/JBMessageRoot.sol";
43
- import {JBPendingSwap} from "./structs/JBPendingSwap.sol";
44
- import {JBRemoteToken} from "./structs/JBRemoteToken.sol";
45
-
46
- /// @notice A `JBCCIPSucker` extension that swaps between local and bridge tokens using the best
47
- /// Uniswap V3 or V4 pool before/after CCIP bridging.
48
- /// @dev Enables cross-currency bridging: e.g., ETH on Ethereum <-> USDC on Tempo.
49
- /// Discovers the most liquid pool across V3 fee tiers and V4 pool configurations,
50
- /// then applies TWAP-based quoting with sigmoid slippage protection.
51
- ///
52
- /// **Cross-denomination claim scaling:** Because the merkle tree leaf amounts are denominated in the
53
- /// source chain's terminal token, the receiving chain must scale each claim proportionally. This contract
54
- /// stores an immutable conversion rate per nonce (one per received root batch). The `claim` override
55
- /// sets the leaf index context, and `_addToBalance` uses it to look up the correct nonce and rate:
56
- /// `scaledAmount = leafAmount * batchLocal / batchLeaf`. This is ordering-independent — out-of-order
57
- /// CCIP delivery cannot cause one batch's rate to be applied to another batch's claims.
58
- ///
59
- /// Flow (Ethereum -> Tempo, ETH -> USDC):
60
- /// prepare(ETH) -> burn project tokens, cash out ETH from terminal
61
- /// toRemote(ETH) -> swap ETH->USDC on best V3/V4 pool -> CCIP bridge USDC -> Tempo receives USDC
62
- ///
63
- /// Flow (Tempo -> Ethereum, USDC -> ETH):
64
- /// prepare(USDC) -> burn project tokens, cash out USDC from terminal
65
- /// toRemote(USDC) -> CCIP bridge USDC -> Ethereum receives USDC -> swap USDC->ETH on best V3/V4 pool
66
- ///
67
- /// **Gas limit configuration**: When mapping tokens for swap suckers via `mapToken`, set
68
- /// `JBTokenMapping.minGas` to at least 600,000. Combined with the base `MESSENGER_BASE_GAS_LIMIT`
69
- /// of 300,000, this provides ~900,000 gas for `ccipReceive` — sufficient for V3/V4 swap execution,
70
- /// TWAP oracle consultation, and slippage computation. Insufficient gas causes the CCIP message to
71
- /// fail on delivery, requiring manual re-execution via CCIP's ManualExecution mechanism.
72
- ///
73
- /// **Inbound swap resilience**: If a swap reverts during `ccipReceive` (due to insufficient
74
- /// liquidity, stale TWAP observations, or extreme price impact), the CCIP message still succeeds.
75
- /// The unswapped bridge tokens are stored in a `JBPendingSwap` and the merkle root is recorded
76
- /// normally. Claims for the affected batch are gated until `retrySwap` is called successfully.
77
- /// Anyone can call `retrySwap` once swap conditions improve.
78
- ///
79
- /// **No caller-controlled slippage**: Unlike the router terminal (where the caller spends their own
80
- /// funds and can accept any slippage), here the swap output determines the conversion rate for ALL
81
- /// claimers of the batch. Caller-controlled `minAmountOut` would allow sandwich attacks that lock
82
- /// in bad rates for everyone. All swaps (outbound and retry) use TWAP quoting exclusively.
83
- contract JBSwapCCIPSucker is JBCCIPSucker, IUnlockCallback, IUniswapV3SwapCallback {
84
- using SafeERC20 for IERC20;
85
-
86
- //*********************************************************************//
87
- // --------------------------- custom errors ------------------------- //
88
- //*********************************************************************//
89
-
90
- error JBSwapCCIPSucker_BatchNotReceived(uint64 nonce);
91
- error JBSwapCCIPSucker_CallerNotPoolManager(address caller);
92
- error JBSwapCCIPSucker_DuplicateBatch(uint64 nonce);
93
- error JBSwapCCIPSucker_InvalidBridgeToken(address bridgeToken, address wrappedNativeToken);
94
- error JBSwapCCIPSucker_NoPendingSwap(address localToken, uint64 nonce, bool retrySwapLocked);
95
- error JBSwapCCIPSucker_OnlySelf(address caller, address expected);
96
- error JBSwapCCIPSucker_PositiveRootWithoutDelivery(uint256 rootAmount);
97
- error JBSwapCCIPSucker_SwapFailed(address tokenIn, address tokenOut, uint256 amountIn);
98
- error JBSwapCCIPSucker_SwapPending(uint64 nonce);
99
- error JBSwapCCIPSucker_UnexpectedDeliveredTokens(uint256 count);
100
- error JBSwapCCIPSucker_WrongDeliveredToken(address delivered, address expected);
101
-
102
- //*********************************************************************//
103
- // ------------------------------ events ----------------------------- //
104
- //*********************************************************************//
105
-
106
- /// @notice Emitted when a previously failed inbound swap is successfully retried.
107
- /// @param localToken The local token that the bridge tokens were swapped into.
108
- /// @param nonce The nonce of the batch whose swap was retried.
109
- /// @param bridgeAmount The amount of bridge tokens that were swapped.
110
- /// @param localAmount The amount of local tokens received from the retry swap.
111
- /// @param caller The address that retried the swap.
112
- event SwapRetried(
113
- address indexed localToken, uint64 indexed nonce, uint256 bridgeAmount, uint256 localAmount, address caller
114
- );
115
-
116
- //*********************************************************************//
117
- // --------------- public immutable stored properties ---------------- //
118
- //*********************************************************************//
119
-
120
- /// @notice The ERC-20 token used for CCIP bridging (e.g., USDC). Must exist on both chains.
121
- IERC20 public immutable BRIDGE_TOKEN;
122
-
123
- /// @notice The Uniswap V4 PoolManager. Can be address(0) if V4 is unavailable on this chain.
124
- IPoolManager public immutable POOL_MANAGER;
125
-
126
- /// @notice The Uniswap V4 hook address for pool discovery (optional).
127
- address public immutable UNIV4_HOOK;
128
-
129
- /// @notice The Uniswap V3 factory for pool discovery and callback verification. Can be address(0).
130
- IUniswapV3Factory public immutable V3_FACTORY;
131
-
132
- /// @notice The ERC-20 wrapper for the chain's native token (e.g. WETH on Ethereum, WCELO on Celo). Used for V3
133
- /// native swaps.
134
- IWrappedNativeToken public immutable WRAPPED_NATIVE_TOKEN;
135
-
136
- //*********************************************************************//
137
- // ------------------- internal stored properties -------------------- //
138
- //*********************************************************************//
139
-
140
- /// @notice Pending (failed) inbound swaps, keyed by local token and batch nonce.
141
- /// @dev Populated when `ccipReceive` swap fails; cleared when `retrySwap` succeeds.
142
- /// @custom:param localToken The local token the swap targets.
143
- /// @custom:param nonce The CCIP nonce identifying the batch.
144
- mapping(address localToken => mapping(uint64 nonce => JBPendingSwap)) public pendingSwapOf;
145
-
146
- /// @notice End leaf index (exclusive) for each received root batch, keyed by token and nonce.
147
- /// @custom:param token The local token address.
148
- /// @custom:param nonce The CCIP nonce identifying the batch.
149
- mapping(address token => mapping(uint64 nonce => uint256)) internal _batchEndOf;
150
-
151
- /// @notice Start leaf index for each received root batch, keyed by token and nonce.
152
- /// @dev Together with `_batchEndOf`, defines the half-open range [start, end) of leaf indices in each batch.
153
- /// Self-describing per nonce — no sequential dependency for out-of-order CCIP delivery.
154
- /// @custom:param token The local token address.
155
- /// @custom:param nonce The CCIP nonce identifying the batch.
156
- mapping(address token => mapping(uint64 nonce => uint256)) internal _batchStartOf;
157
-
158
- /// @notice Conversion rate for each received root batch, keyed by token and nonce.
159
- /// @custom:param token The local token address.
160
- /// @custom:param nonce The CCIP nonce identifying the batch.
161
- mapping(address token => mapping(uint64 nonce => JBConversionRate)) internal _conversionRateOf;
162
-
163
- /// @notice Count of populated batch nonces per token. Appended exactly once per batch in
164
- /// `ccipReceive`, so it equals the number of received batches independent of CCIP ordering.
165
- /// @custom:param token The local token address.
166
- mapping(address token => uint64) internal _populatedNonceCount;
167
-
168
- /// @notice Populated batch nonces per token, indexed by insertion order.
169
- /// @dev `_findNonceForLeafIndex` walks this list directly. That bounds lookup by the number of
170
- /// received batches, not by the highest nonce, so sparse or out-of-order CCIP delivery cannot
171
- /// force the claim path to scan empty nonce slots.
172
- /// @custom:param token The local token address.
173
- /// @custom:param index The insertion index in [0, _populatedNonceCount[token]).
174
- mapping(address token => mapping(uint64 index => uint64 nonce)) internal _populatedNonceByIndex;
175
-
176
- /// @notice Cumulative leaf count at the last `_sendRootOverAMB` call, per token.
177
- /// @dev Used on the sender side to derive the batch start index for the next send.
178
- /// @custom:param token The local token address.
179
- mapping(address token => uint256) internal _lastSentCount;
180
-
181
- //*********************************************************************//
182
- // ------------------- transient stored properties ------------------- //
183
- //*********************************************************************//
184
-
185
- /// @dev Reentrancy guard for the initial `ccipReceive` swap. Prevents claims from consuming newly received
186
- /// swap output before the batch's conversion rate has been recorded.
187
- bool transient _ccipReceiveSwapLocked;
188
-
189
- /// @notice Leaf index + 1 of the claim currently in progress (set by the `claim` override).
190
- /// @dev Transient storage — auto-resets to 0 each transaction, saving ~9,800 gas per claim vs SSTORE.
191
- /// Value 0 means no active claim (bypass scaling); non-zero means leafIndex = value - 1.
192
- uint256 transient _currentClaimLeafIndex;
193
-
194
- /// @dev Reentrancy guard for `retrySwap`. Prevents claims from executing during the swap window
195
- /// (between delete pendingSwapOf and writing the conversion rate), which would allow zero-backed
196
- /// minting. Also prevents re-entry into `retrySwap` itself. Transient — auto-resets each tx.
197
- bool transient _retrySwapLocked;
198
-
199
- //*********************************************************************//
200
- // ---------------------------- constructor -------------------------- //
201
- //*********************************************************************//
202
-
203
- /// @param deployer The deployer that stores chain-specific configuration.
204
- /// @param directory The directory of terminals and controllers for projects.
205
- /// @param permissions The permissions contract.
206
- /// @param tokens The contract that manages token minting and burning.
207
- /// @param feeProjectId The project ID that receives bridge fees.
208
- /// @param registry The sucker registry.
209
- /// @param trustedForwarder The trusted forwarder for ERC-2771 meta-transactions.
210
- constructor(
211
- JBSwapCCIPSuckerDeployer deployer,
212
- IJBDirectory directory,
213
- IJBPermissions permissions,
214
- IJBTokens tokens,
215
- uint256 feeProjectId,
216
- IJBSuckerRegistry registry,
217
- address trustedForwarder
218
- )
219
- JBCCIPSucker(deployer, directory, permissions, tokens, feeProjectId, registry, trustedForwarder)
220
- {
221
- IJBSwapCCIPSuckerDeployer swapDeployer = IJBSwapCCIPSuckerDeployer(address(deployer));
222
- BRIDGE_TOKEN = swapDeployer.bridgeToken();
223
- POOL_MANAGER = swapDeployer.poolManager();
224
- V3_FACTORY = swapDeployer.v3Factory();
225
- UNIV4_HOOK = swapDeployer.univ4Hook();
226
- WRAPPED_NATIVE_TOKEN = IWrappedNativeToken(swapDeployer.wrappedNativeToken());
227
-
228
- if (address(BRIDGE_TOKEN) == address(0)) {
229
- revert JBSwapCCIPSucker_InvalidBridgeToken({
230
- bridgeToken: address(BRIDGE_TOKEN), wrappedNativeToken: address(WRAPPED_NATIVE_TOKEN)
231
- });
232
- }
233
- // BRIDGE_TOKEN must not be the wrapped native token — wrapping and CCIP ERC-20 bridging conflict.
234
- if (address(BRIDGE_TOKEN) == address(WRAPPED_NATIVE_TOKEN) && address(WRAPPED_NATIVE_TOKEN) != address(0)) {
235
- revert JBSwapCCIPSucker_InvalidBridgeToken({
236
- bridgeToken: address(BRIDGE_TOKEN), wrappedNativeToken: address(WRAPPED_NATIVE_TOKEN)
237
- });
238
- }
239
- // NOTE: V3_FACTORY and POOL_MANAGER can both be address(0) on chains where the local terminal token
240
- // IS the bridge token (e.g., USDC on Tempo). No swap is ever needed in that case. If a swap IS attempted
241
- // without swap infra, _discoverPool / _executeSwap will revert at runtime with JBSwapCCIPSucker_NoPool.
242
- }
243
-
244
- //*********************************************************************//
245
- // ------------------------- receive / fallback ---------------------- //
246
- //*********************************************************************//
247
-
248
- /// @notice Allow this contract to receive native tokens (from V4 swaps, wrapped-native-token unwrap, and CCIP
249
- /// refunds).
250
- receive() external payable override {}
251
-
252
- //*********************************************************************//
253
- // --------------------- external transactions ----------------------- //
254
- //*********************************************************************//
255
-
256
- /// @notice Override CCIP receive to swap bridge tokens into local tokens and track denomination conversion.
257
- /// @dev Preserves the parent's typed message discrimination and adds swap logic for ROOT messages.
258
- /// For ROOT messages: swaps received bridge tokens to local tokens and tracks the leaf-to-local conversion ratio.
259
- /// @param any2EvmMessage The CCIP message received from the remote chain.
260
- function ccipReceive(Client.Any2EVMMessage calldata any2EvmMessage) external override {
261
- // Use msg.sender (not _msgSender()) to prevent ERC2771 spoofing.
262
- if (msg.sender != address(CCIP_ROUTER)) revert JBSucker_NotPeer(_toBytes32(msg.sender));
263
-
264
- address origin = abi.decode(any2EvmMessage.sender, (address));
265
-
266
- if (origin != _toAddress(peer()) || any2EvmMessage.sourceChainSelector != REMOTE_CHAIN_SELECTOR) {
267
- revert JBSucker_NotPeer({caller: _toBytes32(origin)});
268
- }
269
-
270
- // Decode the typed message: abi.encode(uint8 type, bytes payload).
271
- (uint8 messageType, bytes memory payload) = JBCCIPLib.decodeTypedMessage(any2EvmMessage.data);
272
-
273
- if (messageType == _CCIP_MSG_TYPE_ROOT) {
274
- // ROOT message — swap bridge tokens to local tokens before storing the merkle root.
275
- // Decode the root and batch range [batchStart, batchEnd).
276
- (JBMessageRoot memory root, uint256 batchStart, uint256 batchEnd) =
277
- abi.decode(payload, (JBMessageRoot, uint256, uint256));
278
-
279
- address localToken = _toAddress(root.token);
280
- uint64 nonce = root.remoteRoot.nonce;
281
- uint256 leafTotal = root.amount;
282
- uint256 localAmount;
283
- bool swapFailed;
284
- // Cache the single delivered entry once so subsequent branches reuse it without re-indexing
285
- // calldata. `deliveredAmount > 0` later implies a delivery was present.
286
- address deliveredToken;
287
- uint256 deliveredAmount;
288
- {
289
- // Send-side guarantees: at most one entry in `destTokenAmounts` (length 0 for zero-value
290
- // batches, length 1 for value-bearing batches), and when present the delivered token is
291
- // `BRIDGE_TOKEN`. Refuse anything that deviates so a peer compromise or a malformed CCIP
292
- // delivery cannot register positive root accounting against zero or wrong-token backing.
293
- uint256 deliveryCount = any2EvmMessage.destTokenAmounts.length;
294
- if (deliveryCount > 1) {
295
- revert JBSwapCCIPSucker_UnexpectedDeliveredTokens(deliveryCount);
296
- }
297
- if (deliveryCount == 0) {
298
- if (leafTotal > 0) revert JBSwapCCIPSucker_PositiveRootWithoutDelivery(leafTotal);
299
- } else {
300
- Client.EVMTokenAmount calldata delivered = any2EvmMessage.destTokenAmounts[0];
301
- deliveredToken = delivered.token;
302
- deliveredAmount = delivered.amount;
303
- if (deliveredToken != address(BRIDGE_TOKEN)) {
304
- revert JBSwapCCIPSucker_WrongDeliveredToken({
305
- delivered: deliveredToken, expected: address(BRIDGE_TOKEN)
306
- });
307
- }
308
- // Zero delivery alongside a positive root is structurally indistinguishable from
309
- // "no delivery + positive root" — both leave the local sucker with nothing to back
310
- // the leaves the root advertises. Reject so a peer cannot mint a claimable rate
311
- // that records `leafTotal=N, localTotal=0` and lets later claims withdraw against
312
- // unrelated balance.
313
- if (leafTotal > 0 && deliveredAmount == 0) {
314
- revert JBSwapCCIPSucker_PositiveRootWithoutDelivery(leafTotal);
315
- }
316
- }
317
- }
318
-
319
- // Detect an already-processed batch before the swap path. The inbox nonce alone cannot be used here:
320
- // CCIP can deliver nonce 2 before nonce 1, and nonce 1 still needs its self-described batch metadata.
321
- if (
322
- _batchEndOf[localToken][nonce] != 0 || _conversionRateOf[localToken][nonce].leafTotal != 0
323
- || pendingSwapOf[localToken][nonce].bridgeAmount != 0
324
- ) {
325
- if (deliveredAmount != 0) {
326
- revert JBSwapCCIPSucker_DuplicateBatch({nonce: nonce});
327
- }
328
-
329
- return;
330
- }
331
-
332
- // After the validation block above, `deliveredToken != address(0)` iff a delivery was present,
333
- // because the invariants ensure it equals `BRIDGE_TOKEN` (a non-zero ERC-20) whenever there is one.
334
- if (deliveredToken != address(0)) {
335
- if (localToken == address(BRIDGE_TOKEN) || localToken == deliveredToken) {
336
- // No swap needed — bridge token IS the local token.
337
- localAmount = deliveredAmount;
338
- } else {
339
- // Swap bridge token -> local token via best V3/V4 pool.
340
- // Wrapped in try-catch so a swap failure doesn't revert the entire CCIP message
341
- // (which would leave tokens stuck in the OffRamp). On failure, bridge tokens are
342
- // stored for later retry via `retrySwap` (written below, after nonce validation).
343
- _ccipReceiveSwapLocked = true;
344
- try this.executeSwapExternal({
345
- tokenIn: deliveredToken, tokenOut: localToken, amount: deliveredAmount
346
- }) returns (
347
- uint256 swapped
348
- ) {
349
- _ccipReceiveSwapLocked = false;
350
- localAmount = swapped;
351
- } catch {
352
- _ccipReceiveSwapLocked = false;
353
- swapFailed = true;
354
- // localAmount stays 0 — pendingSwapOf and conversion rate are written
355
- // below, after fromRemote validates the nonce.
356
- }
357
- }
358
- }
359
-
360
- // Store the inbox merkle root for later claims.
361
- // Must be called BEFORE writing batch metadata and conversion rates so that stale
362
- // (duplicate/replayed) roots that fromRemote silently rejects do not overwrite
363
- // metadata from the original accepted delivery.
364
- this.fromRemote(root);
365
-
366
- // Write batch metadata if this nonce hasn't been seen before.
367
- // Decoupled from nonce advancement to support out-of-order CCIP delivery:
368
- // if nonce 2 arrives before nonce 1, fromRemote only advances the inbox for nonce 2,
369
- // but we still need to record nonce 1's batch metadata when it arrives later.
370
- // The Merkle tree is append-only, so nonce 1's leaves are provable against nonce 2's root.
371
- //
372
- // Detect "already seen" without extra storage: a nonce has been processed if it has
373
- // either a batch range (batchEnd > 0) or a conversion rate / pending swap recorded.
374
- // Record the batch range so _findNonceForLeafIndex can resolve leaf ownership
375
- // independently of nonce ordering. Each nonce is self-describing: [start, end).
376
- //
377
- // Only record metadata for batches that carry value (`leafTotal > 0`). The base-sucker
378
- // `prepare` revert on zero `projectTokenCount` blocks the legitimate spam entry point,
379
- // but a compromised peer or a `projectTokenCount = 1`-style bypass could still ship a
380
- // zero-leaf root over the bridge. Skipping the metadata write here ensures such roots
381
- // cannot inflate `_populatedNonceByIndex` and tax future `_findNonceForLeafIndex`
382
- // walks. Roots with `leafTotal == 0` carry no claimable leaves, so there is nothing
383
- // for `_findNonceForLeafIndex` to resolve against them.
384
- if (batchEnd > 0 && leafTotal > 0) {
385
- // Record this batch's half-open leaf range `[batchStart, batchEnd)`. Self-
386
- // describing per-nonce — no implicit chain across nonces — so out-of-order
387
- // delivery can still resolve a leaf to its batch.
388
- _batchStartOf[localToken][nonce] = batchStart;
389
- _batchEndOf[localToken][nonce] = batchEnd;
390
-
391
- // Append `nonce` to the populated-nonce list for this token. The duplicate guard
392
- // above fires at most once per (token, nonce), so each populated nonce is appended
393
- // exactly once — the array stays duplicate-free without extra checks.
394
- //
395
- // Reading `_populatedNonceCount[localToken]` first into a local lets us write
396
- // the new slot and the new count in a single read-modify-write pair (one
397
- // SLOAD, two SSTOREs to distinct slots). The `unchecked` increment is safe:
398
- // `priorCount` is bounded by the total number of populated nonces, which is
399
- // upper-bounded by the CCIP nonce space (`uint64`) — overflow requires more
400
- // batches than `uint64.max`, which the inbox can never produce.
401
- uint64 priorCount = _populatedNonceCount[localToken];
402
- _populatedNonceByIndex[localToken][priorCount] = nonce;
403
- unchecked {
404
- _populatedNonceCount[localToken] = priorCount + 1;
405
- }
406
- }
407
-
408
- // Store pendingSwapOf for failed swaps now that nonce is validated.
409
- if (swapFailed) {
410
- pendingSwapOf[localToken][nonce] =
411
- JBPendingSwap({bridgeToken: deliveredToken, bridgeAmount: deliveredAmount, leafTotal: leafTotal});
412
- }
413
-
414
- // Zero-output swap guard: When a swap succeeds but returns zero local tokens, the
415
- // batch must NOT be marked claimable. Without this guard, `_addToBalance` would see
416
- // `pendingSwapOf.bridgeAmount == 0` (no pending swap stored) and allow claims to
417
- // proceed — minting the full bridged project-token amount while adding zero terminal
418
- // backing, breaking cross-chain solvency.
419
- //
420
- // Route zero-output swaps into `pendingSwapOf` so the swap can be retried via
421
- // `retrySwap` once pool conditions improve. Only store the conversion rate when
422
- // the swap produced a positive local amount.
423
- if (leafTotal > 0 && !swapFailed) {
424
- if (localAmount == 0 && deliveredAmount > 0) {
425
- pendingSwapOf[localToken][nonce] = JBPendingSwap({
426
- bridgeToken: deliveredToken, bridgeAmount: deliveredAmount, leafTotal: leafTotal
427
- });
428
- } else {
429
- _conversionRateOf[localToken][nonce] =
430
- JBConversionRate({leafTotal: leafTotal, localTotal: localAmount});
431
- }
432
- }
433
- } else {
434
- revert JBCCIPSucker_UnknownMessageType({messageType: messageType});
435
- }
436
- }
437
-
438
- /// @notice Uniswap V3 swap callback — delegates to JBSwapPoolLib (via DELEGATECALL) to reduce bytecode.
439
- /// @param amount0Delta The amount of token0 used for the swap.
440
- /// @param amount1Delta The amount of token1 used for the swap.
441
- /// @param data Encoded (originalTokenIn, normalizedTokenIn, normalizedTokenOut).
442
- function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata data) external override {
443
- JBSwapPoolLib.executeV3SwapCallback({
444
- v3Factory: V3_FACTORY, amount0Delta: amount0Delta, amount1Delta: amount1Delta, data: data
445
- });
446
- }
447
-
448
- /// @notice Uniswap V4 unlock callback — delegates to JBSwapPoolLib (via DELEGATECALL) to reduce bytecode.
449
- /// @param data Encoded swap parameters from PoolManager.unlock().
450
- /// @return Encoded output amount.
451
- function unlockCallback(bytes calldata data) external override returns (bytes memory) {
452
- if (msg.sender != address(POOL_MANAGER)) revert JBSwapCCIPSucker_CallerNotPoolManager(msg.sender);
453
- return JBSwapPoolLib.executeV4UnlockCallback({poolManager: POOL_MANAGER, data: data});
454
- }
455
-
456
- /// @notice External swap entry point callable ONLY by this contract. Exists so `ccipReceive` can
457
- /// wrap the swap in a try-catch (Solidity requires an external call target for try-catch).
458
- /// @param tokenIn The input token address.
459
- /// @param tokenOut The output token address.
460
- /// @param amount The amount of input tokens to swap.
461
- /// @return amountOut The amount of output tokens received.
462
- function executeSwapExternal(
463
- address tokenIn,
464
- address tokenOut,
465
- uint256 amount
466
- )
467
- external
468
- returns (uint256 amountOut)
469
- {
470
- if (msg.sender != address(this)) {
471
- revert JBSwapCCIPSucker_OnlySelf({caller: msg.sender, expected: address(this)});
472
- }
473
- return _executeSwap({tokenIn: tokenIn, tokenOut: tokenOut, amount: amount});
474
- }
475
-
476
- /// @notice Retry a previously failed inbound swap. Anyone can call this once swap conditions improve.
477
- /// @dev On success, updates the conversion rate so claims for this nonce's batch can proceed.
478
- /// Always uses TWAP quoting — no caller-provided `minAmountOut`. Unlike the router terminal (where the
479
- /// caller is spending their own funds and can accept any slippage they choose), here the swap output
480
- /// determines the conversion rate for ALL claimers of this batch. Allowing a caller-controlled minimum
481
- /// would let an attacker sandwich the swap with `minAmountOut = 1` and lock in a bad rate for everyone.
482
- /// @param localToken The local token that the bridge tokens should be swapped into.
483
- /// @param nonce The nonce of the batch whose swap failed.
484
- function retrySwap(address localToken, uint64 nonce) external {
485
- // Reentrancy guard: prevents re-entry into retrySwap AND prevents claims from executing
486
- // during the swap window (which would see the stale {leafTotal > 0, localTotal: 0} rate
487
- // and mint project tokens backed by zero terminal tokens).
488
- if (_retrySwapLocked) {
489
- revert JBSwapCCIPSucker_NoPendingSwap({
490
- localToken: localToken, nonce: nonce, retrySwapLocked: _retrySwapLocked
491
- });
492
- }
493
- _retrySwapLocked = true;
494
-
495
- JBPendingSwap memory pending = pendingSwapOf[localToken][nonce];
496
- if (pending.bridgeAmount == 0) {
497
- revert JBSwapCCIPSucker_NoPendingSwap({
498
- localToken: localToken, nonce: nonce, retrySwapLocked: _retrySwapLocked
499
- });
500
- }
501
-
502
- uint256 localAmount =
503
- _executeSwapOrRevert({tokenIn: pending.bridgeToken, tokenOut: localToken, amount: pending.bridgeAmount});
504
-
505
- // Update the conversion rate so claims can proceed, then clear the pending swap.
506
- _conversionRateOf[localToken][nonce] = JBConversionRate({leafTotal: pending.leafTotal, localTotal: localAmount});
507
- delete pendingSwapOf[localToken][nonce];
508
-
509
- _retrySwapLocked = false;
510
- emit SwapRetried({
511
- localToken: localToken,
512
- nonce: nonce,
513
- bridgeAmount: pending.bridgeAmount,
514
- localAmount: localAmount,
515
- caller: _msgSender()
516
- });
517
- }
518
-
519
- //*********************************************************************//
520
- // ----------------------- public transactions ----------------------- //
521
- //*********************************************************************//
522
-
523
- /// @notice Override single claim to set the leaf index context for `_addToBalance` scaling.
524
- /// @dev The batch `claim(JBClaim[])` calls this in a loop, so it works automatically.
525
- /// Stores leafIndex + 1 (0 = no active claim sentinel). No reset needed — transient storage auto-clears.
526
- /// @param claimData The claim data containing the leaf and proof.
527
- function claim(JBClaim calldata claimData) public override {
528
- // Block claims during retrySwap to prevent zero-backed minting via reentrancy.
529
- if (_retrySwapLocked || _ccipReceiveSwapLocked) revert JBSwapCCIPSucker_SwapPending(0);
530
- _currentClaimLeafIndex = claimData.leaf.index + 1;
531
- super.claim(claimData);
532
- // Clear stale transient context to prevent leaking into same-tx emergency exits.
533
- _currentClaimLeafIndex = 0;
534
- }
535
-
536
- //*********************************************************************//
537
- // ---------------------- internal transactions ---------------------- //
538
- //*********************************************************************//
539
-
540
- /// @notice Override to scale claim amounts from source-chain denomination to local-chain denomination.
541
- /// @dev When cross-currency bridging, merkle tree leaf amounts are in the source chain's terminal token
542
- /// denomination. This override converts them proportionally using the nonce-indexed conversion rate
543
- /// populated during `ccipReceive`.
544
- ///
545
- /// **Per-batch isolation**: Each received root stores an immutable conversion rate keyed by nonce.
546
- /// The `claim` override sets `_currentClaimLeafIndex`, which this function uses to look up the
547
- /// correct nonce (and thus the correct rate) for the claim. This prevents out-of-order CCIP
548
- /// delivery from applying the wrong batch's rate to a claim.
549
- /// @param token The local token address.
550
- /// @param amount The claim amount in source-chain denomination.
551
- /// @param cachedProjectId The project ID (cached for gas efficiency).
552
- function _addToBalance(address token, uint256 amount, uint256 cachedProjectId) internal override {
553
- if (_currentClaimLeafIndex != 0) {
554
- uint64 nonce = _findNonceForLeafIndex({token: token, leafIndex: _currentClaimLeafIndex - 1});
555
- if (nonce != 0) {
556
- // Gate on pending swaps — if a swap failed and hasn't been retried yet,
557
- // claims must wait. This check must come BEFORE the leafTotal gate so that
558
- // failed swaps (where _conversionRateOf was never written) still block claims.
559
- if (pendingSwapOf[token][nonce].bridgeAmount > 0) {
560
- revert JBSwapCCIPSucker_SwapPending({nonce: nonce});
561
- }
562
- JBConversionRate storage rate = _conversionRateOf[token][nonce];
563
- if (rate.leafTotal > 0) {
564
- amount = amount * rate.localTotal / rate.leafTotal;
565
- }
566
- }
567
- }
568
-
569
- super._addToBalance({token: token, amount: amount, cachedProjectId: cachedProjectId});
570
- }
571
-
572
- /// @notice Override to swap local tokens into bridge tokens before CCIP bridging.
573
- /// @dev Does NOT modify `suckerMessage.amount` — keeps the original leaf-denomination total so the
574
- /// receiving chain can use it (along with the actual delivered amount) to compute the proportional
575
- /// scaling factor for individual claims.
576
- /// Delegates CCIP message construction to JBCCIPLib (via DELEGATECALL) to reduce bytecode.
577
- /// @param transportPayment The ETH sent for CCIP fees.
578
- /// @param index The last leaf index in the current batch.
579
- /// @param token The local token to bridge.
580
- /// @param amount The amount of local tokens to bridge.
581
- /// @param remoteToken The remote token configuration (including minGas).
582
- /// @param suckerMessage The merkle root message to send to the remote chain.
583
- // forge-lint: disable-next-line(mixed-case-function)
584
- function _sendRootOverAMB(
585
- uint256 transportPayment,
586
- uint256 index,
587
- address token,
588
- uint256 amount,
589
- JBRemoteToken memory remoteToken,
590
- JBMessageRoot memory suckerMessage
591
- )
592
- internal
593
- override
594
- {
595
- Client.EVMTokenAmount[] memory tokenAmounts;
596
- bytes memory encodedPayload;
597
-
598
- {
599
- uint256 bridgeAmount;
600
- if (amount == 0) {
601
- tokenAmounts = new Client.EVMTokenAmount[](0);
602
- } else {
603
- address bridgeTokenAddr = address(BRIDGE_TOKEN);
604
-
605
- if (token == bridgeTokenAddr) {
606
- bridgeAmount = amount;
607
- } else {
608
- // Always use TWAP quoting — no caller-provided minAmountOut. Unlike the router terminal
609
- // (where the caller spends their own funds), here the swap output sets the conversion
610
- // rate for ALL claimers of the batch. Caller-controlled slippage would allow sandwich
611
- // attacks that lock in bad rates for everyone.
612
- bridgeAmount = _executeSwapOrRevert({tokenIn: token, tokenOut: bridgeTokenAddr, amount: amount});
613
- }
614
-
615
- tokenAmounts = new Client.EVMTokenAmount[](1);
616
- tokenAmounts[0] = Client.EVMTokenAmount({token: bridgeTokenAddr, amount: bridgeAmount});
617
- BRIDGE_TOKEN.forceApprove({spender: address(CCIP_ROUTER), value: bridgeAmount});
618
- }
619
-
620
- // NOTE: suckerMessage.amount stays as the original leaf-denomination total.
621
- // Encode batch range [batchStart, batchEnd) so the receiver can resolve leaf ownership
622
- // per-nonce without requiring contiguous nonce delivery.
623
- uint256 batchStart = _lastSentCount[token];
624
- uint256 batchEnd = index + 1;
625
- _lastSentCount[token] = batchEnd;
626
- encodedPayload = abi.encode(_CCIP_MSG_TYPE_ROOT, abi.encode(suckerMessage, batchStart, batchEnd));
627
- }
628
-
629
- {
630
- // Determine fee payment mode: native ETH or LINK token.
631
- address feeToken = transportPayment == 0 ? CCIPHelper.linkOfChain(block.chainid) : address(0);
632
-
633
- (bool refundFailed, uint256 refundAmount) = JBCCIPLib.sendCCIPMessage({
634
- ccipRouter: CCIP_ROUTER,
635
- remoteChainSelector: REMOTE_CHAIN_SELECTOR,
636
- peerAddress: _toAddress(peer()),
637
- transportPayment: transportPayment,
638
- feeToken: feeToken,
639
- feeTokenPayer: feeToken != address(0) ? _msgSender() : address(0),
640
- gasLimit: MESSENGER_BASE_GAS_LIMIT + remoteToken.minGas,
641
- encodedPayload: encodedPayload,
642
- tokenAmounts: tokenAmounts,
643
- refundRecipient: _msgSender()
644
- });
645
-
646
- if (refundFailed) {
647
- _retainTransportPaymentRefund({account: _msgSender(), amount: refundAmount});
648
- emit TransportPaymentRefundFailed({recipient: _msgSender(), amount: refundAmount, caller: _msgSender()});
649
- }
650
- }
651
- }
652
-
653
- //*********************************************************************//
654
- // ----------------------- internal helpers -------------------------- //
655
- //*********************************************************************//
656
-
657
- /// @notice Execute a swap between two tokens using the best available V3 or V4 pool.
658
- /// @dev Delegates pool discovery, TWAP quoting, and swap execution to JBSwapPoolLib (via DELEGATECALL).
659
- /// Swap callbacks (`uniswapV3SwapCallback`, `unlockCallback`) remain on this contract.
660
- /// Always uses TWAP quoting (minAmountOut = 0) — see contract NatSpec for rationale.
661
- /// @param tokenIn The input token (raw address, e.g., NATIVE_TOKEN sentinel for ETH).
662
- /// @param tokenOut The output token (raw address).
663
- /// @param amount The amount of input tokens to swap.
664
- /// @return amountOut The amount of output tokens received.
665
- function _executeSwap(address tokenIn, address tokenOut, uint256 amount) internal returns (uint256 amountOut) {
666
- return JBSwapPoolLib.executeSwap({
667
- config: JBSwapPoolLib.SwapConfig({
668
- v3Factory: V3_FACTORY,
669
- poolManager: POOL_MANAGER,
670
- univ4Hook: UNIV4_HOOK,
671
- wrappedNativeToken: address(WRAPPED_NATIVE_TOKEN)
672
- }),
673
- tokenIn: tokenIn,
674
- tokenOut: tokenOut,
675
- amount: amount,
676
- minAmountOut: 0
677
- });
678
- }
679
-
680
- /// @notice Execute a swap and revert if it produces no output.
681
- /// @param tokenIn The input token.
682
- /// @param tokenOut The output token.
683
- /// @param amount The input amount.
684
- /// @return amountOut The output amount.
685
- function _executeSwapOrRevert(
686
- address tokenIn,
687
- address tokenOut,
688
- uint256 amount
689
- )
690
- internal
691
- returns (uint256 amountOut)
692
- {
693
- amountOut = _executeSwap({tokenIn: tokenIn, tokenOut: tokenOut, amount: amount});
694
- if (amountOut == 0) {
695
- revert JBSwapCCIPSucker_SwapFailed({tokenIn: tokenIn, tokenOut: tokenOut, amountIn: amount});
696
- }
697
- }
698
-
699
- //*********************************************************************//
700
- // ----------------------- internal views ---------------------------- //
701
- //*********************************************************************//
702
-
703
- /// @notice Find the received nonce whose batch contains the given leaf index.
704
- /// @dev Walks `_populatedNonceByIndex` instead of `[1, highestNonce]`. The populated list is the
705
- /// only set that can contain a claimable batch, and it stays compact even when CCIP delivers
706
- /// nonce 10 before nonce 2. This keeps lookup O(K) where K is received batches, avoids sparse
707
- /// empty-slot scans, and keeps the deployable bytecode below the EIP-170 size limit.
708
- /// @param token The local token address.
709
- /// @param leafIndex The leaf index from the claim.
710
- /// @return The nonce of the batch containing this leaf, or 0 if no batches have been recorded.
711
- function _findNonceForLeafIndex(address token, uint256 leafIndex) internal view returns (uint64) {
712
- // No populated batches for this token means there is no conversion rate to apply. Preserve
713
- // nonce 0 as the "unbatched" sentinel used by `_addToBalance`'s non-claim path.
714
- uint64 count = _populatedNonceCount[token];
715
- if (count == 0) return 0;
716
-
717
- // Walk only nonces that actually received a batch. The array is insertion-ordered, not
718
- // sorted, because CCIP can deliver batches out of nonce order; each entry still points to a
719
- // self-contained `[batchStart, batchEnd)` range written before the append.
720
- unchecked {
721
- for (uint64 i; i < count; i++) {
722
- uint64 nonce = _populatedNonceByIndex[token][i];
723
- uint256 end = _batchEndOf[token][nonce];
724
-
725
- // Ranges are non-overlapping across populated nonces. The first hit is therefore
726
- // the unique conversion-rate batch for this claim leaf.
727
- if (leafIndex >= _batchStartOf[token][nonce] && leafIndex < end) return nonce;
728
- }
729
- }
730
-
731
- // Batches exist for the token, but none cover this leaf index; surface the same error used
732
- // before the compact populated-nonce index was introduced.
733
- revert JBSwapCCIPSucker_BatchNotReceived({nonce: 0});
734
- }
735
- }