@bananapus/suckers-v6 0.0.47 → 0.0.49

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.
@@ -175,19 +175,15 @@ contract JBSwapCCIPSucker is JBCCIPSucker, IUnlockCallback, IUniswapV3SwapCallba
175
175
  /// @custom:param nonce The CCIP nonce identifying the batch.
176
176
  mapping(address token => mapping(uint64 nonce => ConversionRate)) internal _conversionRateOf;
177
177
 
178
- /// @notice Highest nonce received so far per token. Used as upper bound for nonce iteration.
179
- /// @custom:param token The local token address.
180
- mapping(address token => uint64) internal _highestReceivedNonce;
181
-
182
178
  /// @notice Count of populated batch nonces per token. Appended exactly once per batch in
183
179
  /// `ccipReceive`, so it equals the number of received batches independent of CCIP ordering.
184
180
  /// @custom:param token The local token address.
185
181
  mapping(address token => uint64) internal _populatedNonceCount;
186
182
 
187
- /// @notice Populated batch nonces per token, indexed by insertion order. Enables the
188
- /// empty-midpoint fallback in `_findNonceForLeafIndex` to walk only the K populated nonces
189
- /// (O(K) worst case) instead of the full [lo, hi] nonce span (O(N) worst case under sparse
190
- /// adversarial patterns).
183
+ /// @notice Populated batch nonces per token, indexed by insertion order.
184
+ /// @dev `_findNonceForLeafIndex` walks this list directly. That bounds lookup by the number of
185
+ /// received batches, not by the highest nonce, so sparse or out-of-order CCIP delivery cannot
186
+ /// force the claim path to scan empty nonce slots.
191
187
  /// @custom:param token The local token address.
192
188
  /// @custom:param index The insertion index in [0, _populatedNonceCount[token]).
193
189
  mapping(address token => mapping(uint64 index => uint64 nonce)) internal _populatedNonceByIndex;
@@ -201,6 +197,10 @@ contract JBSwapCCIPSucker is JBCCIPSucker, IUnlockCallback, IUniswapV3SwapCallba
201
197
  // ------------------- transient stored properties ------------------- //
202
198
  //*********************************************************************//
203
199
 
200
+ /// @dev Reentrancy guard for the initial `ccipReceive` swap. Prevents claims from consuming newly received
201
+ /// swap output before the batch's conversion rate has been recorded.
202
+ bool transient _ccipReceiveSwapLocked;
203
+
204
204
  /// @notice Leaf index + 1 of the claim currently in progress (set by the `claim` override).
205
205
  /// @dev Transient storage — auto-resets to 0 each transaction, saving ~9,800 gas per claim vs SSTORE.
206
206
  /// Value 0 means no active claim (bypass scaling); non-zero means leafIndex = value - 1.
@@ -344,13 +344,16 @@ contract JBSwapCCIPSucker is JBCCIPSucker, IUnlockCallback, IUniswapV3SwapCallba
344
344
  // Wrapped in try-catch so a swap failure doesn't revert the entire CCIP message
345
345
  // (which would leave tokens stuck in the OffRamp). On failure, bridge tokens are
346
346
  // stored for later retry via `retrySwap` (written below, after nonce validation).
347
+ _ccipReceiveSwapLocked = true;
347
348
  try this.executeSwapExternal({
348
349
  tokenIn: deliveredToken, tokenOut: localToken, amount: deliveredAmount
349
350
  }) returns (
350
351
  uint256 swapped
351
352
  ) {
353
+ _ccipReceiveSwapLocked = false;
352
354
  localAmount = swapped;
353
355
  } catch {
356
+ _ccipReceiveSwapLocked = false;
354
357
  swapFailed = true;
355
358
  // localAmount stays 0 — pendingSwapOf and conversion rate are written
356
359
  // below, after fromRemote validates the nonce.
@@ -401,14 +404,6 @@ contract JBSwapCCIPSucker is JBCCIPSucker, IUnlockCallback, IUniswapV3SwapCallba
401
404
  unchecked {
402
405
  _populatedNonceCount[localToken] = priorCount + 1;
403
406
  }
404
-
405
- // Track the highest nonce ever observed for this token. Read by
406
- // `_findNonceForLeafIndex` as the binary-search upper bound. Out-of-order
407
- // delivery keeps this monotonic — we only advance it when the new nonce is
408
- // strictly higher than the prior maximum.
409
- if (nonce > _highestReceivedNonce[localToken]) {
410
- _highestReceivedNonce[localToken] = nonce;
411
- }
412
407
  }
413
408
 
414
409
  // Store pendingSwapOf for failed swaps now that nonce is validated.
@@ -529,7 +524,7 @@ contract JBSwapCCIPSucker is JBCCIPSucker, IUnlockCallback, IUniswapV3SwapCallba
529
524
  /// @param claimData The claim data containing the leaf and proof.
530
525
  function claim(JBClaim calldata claimData) public override {
531
526
  // Block claims during retrySwap to prevent zero-backed minting via reentrancy.
532
- if (_retrySwapLocked) revert JBSwapCCIPSucker_SwapPending(0);
527
+ if (_retrySwapLocked || _ccipReceiveSwapLocked) revert JBSwapCCIPSucker_SwapPending(0);
533
528
  _currentClaimLeafIndex = claimData.leaf.index + 1;
534
529
  super.claim(claimData);
535
530
  // Clear stale transient context to prevent leaking into same-tx emergency exits.
@@ -573,7 +568,7 @@ contract JBSwapCCIPSucker is JBCCIPSucker, IUnlockCallback, IUniswapV3SwapCallba
573
568
  }
574
569
 
575
570
  /// @notice Override to swap local tokens into bridge tokens before CCIP bridging.
576
- /// @dev Does NOT modify `sucker_message.amount` — keeps the original leaf-denomination total so the
571
+ /// @dev Does NOT modify `suckerMessage.amount` — keeps the original leaf-denomination total so the
577
572
  /// receiving chain can use it (along with the actual delivered amount) to compute the proportional
578
573
  /// scaling factor for individual claims.
579
574
  /// Delegates CCIP message construction to JBCCIPLib (via DELEGATECALL) to reduce bytecode.
@@ -582,7 +577,7 @@ contract JBSwapCCIPSucker is JBCCIPSucker, IUnlockCallback, IUniswapV3SwapCallba
582
577
  /// @param token The local token to bridge.
583
578
  /// @param amount The amount of local tokens to bridge.
584
579
  /// @param remoteToken The remote token configuration (including minGas).
585
- /// @param sucker_message The merkle root message to send to the remote chain.
580
+ /// @param suckerMessage The merkle root message to send to the remote chain.
586
581
  // forge-lint: disable-next-line(mixed-case-function)
587
582
  function _sendRootOverAMB(
588
583
  uint256 transportPayment,
@@ -590,7 +585,7 @@ contract JBSwapCCIPSucker is JBCCIPSucker, IUnlockCallback, IUniswapV3SwapCallba
590
585
  address token,
591
586
  uint256 amount,
592
587
  JBRemoteToken memory remoteToken,
593
- JBMessageRoot memory sucker_message
588
+ JBMessageRoot memory suckerMessage
594
589
  )
595
590
  internal
596
591
  override
@@ -620,13 +615,13 @@ contract JBSwapCCIPSucker is JBCCIPSucker, IUnlockCallback, IUniswapV3SwapCallba
620
615
  BRIDGE_TOKEN.forceApprove({spender: address(CCIP_ROUTER), value: bridgeAmount});
621
616
  }
622
617
 
623
- // NOTE: sucker_message.amount stays as the original leaf-denomination total.
618
+ // NOTE: suckerMessage.amount stays as the original leaf-denomination total.
624
619
  // Encode batch range [batchStart, batchEnd) so the receiver can resolve leaf ownership
625
620
  // per-nonce without requiring contiguous nonce delivery.
626
621
  uint256 batchStart = _lastSentCount[token];
627
622
  uint256 batchEnd = index + 1;
628
623
  _lastSentCount[token] = batchEnd;
629
- encodedPayload = abi.encode(_CCIP_MSG_TYPE_ROOT, abi.encode(sucker_message, batchStart, batchEnd));
624
+ encodedPayload = abi.encode(_CCIP_MSG_TYPE_ROOT, abi.encode(suckerMessage, batchStart, batchEnd));
630
625
  }
631
626
 
632
627
  {
@@ -703,94 +698,36 @@ contract JBSwapCCIPSucker is JBCCIPSucker, IUnlockCallback, IUniswapV3SwapCallba
703
698
  // ----------------------- internal views ---------------------------- //
704
699
  //*********************************************************************//
705
700
 
706
- /// @notice Find the nonce whose batch contains the given leaf index.
707
- /// @dev Binary search by nonce. Source-side `prepare()` appends batches in nonce order with each
708
- /// new batch's `batchStart` equal to the previous batch's `batchEnd`, so across populated
709
- /// destination slots `_batchStartOf` is strictly increasing in nonce the populated subset is
710
- /// monotonic even when CCIP delivery is out of order and leaves intermediate slots empty.
711
- /// Binary search exploits that monotonicity for O(log N) lookup.
712
- /// @dev Gap handling: when the midpoint slot is empty (CCIP out-of-order delivery or sparse
713
- /// attacker writes), the fallback walks `_populatedNonceByIndex` — the list of every nonce
714
- /// actually populated. That bounds the worst case at `O(K)` SLOADs, where `K` is the number
715
- /// of received batches, regardless of how sparse the populated set is inside `[1, maxNonce]`.
716
- /// The empty-slot scans that drove `O(N)` under the prior linear fallback are eliminated.
701
+ /// @notice Find the received nonce whose batch contains the given leaf index.
702
+ /// @dev Walks `_populatedNonceByIndex` instead of `[1, highestNonce]`. The populated list is the
703
+ /// only set that can contain a claimable batch, and it stays compact even when CCIP delivers
704
+ /// nonce 10 before nonce 2. This keeps lookup O(K) where K is received batches, avoids sparse
705
+ /// empty-slot scans, and keeps the deployable bytecode below the EIP-170 size limit.
717
706
  /// @param token The local token address.
718
707
  /// @param leafIndex The leaf index from the claim.
719
708
  /// @return The nonce of the batch containing this leaf, or 0 if no batches have been recorded.
720
709
  function _findNonceForLeafIndex(address token, uint256 leafIndex) internal view returns (uint64) {
721
- // `_highestReceivedNonce` upper-bounds any populated slot for this token; zero means
722
- // nothing has been received yet, so the leaf belongs to no batch.
723
- uint64 maxNonce = _highestReceivedNonce[token];
724
- if (maxNonce == 0) return 0;
725
-
726
- // Nonce 0 is reserved by inbox initialization and never holds a batch, so search [1, max].
727
- uint64 lo = 1;
728
- uint64 hi = maxNonce;
729
-
730
- // Wrap arithmetic in `unchecked` — `lo` and `hi` are always in `[1, maxNonce]` and the
731
- // edge-guards (`mid == lo` / `mid == hi` breaks) prevent the only ways `mid - 1` or
732
- // `mid + 1` could over/underflow. Skipping the compiler checks shaves enough bytecode to
733
- // stay under EIP-170 without changing semantics.
710
+ // No populated batches for this token means there is no conversion rate to apply. Preserve
711
+ // nonce 0 as the "unbatched" sentinel used by `_addToBalance`'s non-claim path.
712
+ uint64 count = _populatedNonceCount[token];
713
+ if (count == 0) return 0;
714
+
715
+ // Walk only nonces that actually received a batch. The array is insertion-ordered, not
716
+ // sorted, because CCIP can deliver batches out of nonce order; each entry still points to a
717
+ // self-contained `[batchStart, batchEnd)` range written before the append.
734
718
  unchecked {
735
- while (lo <= hi) {
736
- uint64 mid = lo + (hi - lo) / 2;
737
- // `batchEnd == 0` is the established sentinel for "no batch recorded" (see the
738
- // write guard in `ccipReceive`); a real batch always has `batchEnd > batchStart`.
739
- uint256 end = _batchEndOf[token][mid];
740
-
741
- // Empty midpoint from out-of-order CCIP delivery (the sender minted a higher nonce
742
- // than the inbox has yet received, leaving holes in `[1, maxNonce]`) or a sparse
743
- // pattern an attacker assembled by inflating `_highestReceivedNonce` without
744
- // populating intermediate slots. The earlier linear `[lo, hi]` scan walked every
745
- // empty slot, so worst-case cost was `O(maxNonce)` SLOADs — that's the residual
746
- // the audit flagged. Walk `_populatedNonceByIndex` instead: it's `K` entries (one
747
- // per received batch) and contains exactly the slots a real batch can cover.
748
- if (end == 0) {
749
- // Number of populated batch slots for this token — equal to the array length.
750
- // Maintained by `ccipReceive`'s append (one SSTORE per first-time receive).
751
- uint64 count = _populatedNonceCount[token];
752
-
753
- // Linear walk over the populated set. Bounded by `count`, not `maxNonce`, so
754
- // sparse adversarial patterns can't inflate this loop with empty slots.
755
- for (uint64 i; i < count; i++) {
756
- // Insertion-ordered nonce at index `i`. May be any value in `[1, maxNonce]`
757
- // because CCIP delivery is out-of-order; the array is not sorted by nonce.
758
- uint64 n = _populatedNonceByIndex[token][i];
759
-
760
- // Read end of this batch's leaf range. Always `> 0` here — we only push to
761
- // `_populatedNonceByIndex` after the corresponding `_batchEndOf` write.
762
- uint256 nEnd = _batchEndOf[token][n];
763
-
764
- // Coverage test: each batch's `[batchStart, batchEnd)` is non-overlapping
765
- // across populated nonces, so a hit is unique. The `[lo, hi]` window from
766
- // the binary search isn't applied — the binary-search invariant guarantees
767
- // the leaf can only live in a populated nonce, so an out-of-window match
768
- // would mean the binary search was already wrong; falling through to the
769
- // next iteration costs less than the extra two compares per iteration.
770
- if (leafIndex >= _batchStartOf[token][n] && leafIndex < nEnd) return n;
771
- }
772
-
773
- // No populated nonce contains `leafIndex` for this token. Break out of the
774
- // outer binary-search loop and fall through to the shared revert below.
775
- break;
776
- }
719
+ for (uint64 i; i < count; i++) {
720
+ uint64 nonce = _populatedNonceByIndex[token][i];
721
+ uint256 end = _batchEndOf[token][nonce];
777
722
 
778
- // Each batch covers [batchStart, batchEnd). Across populated nonces these ranges
779
- // are non-overlapping and strictly increasing, so the standard comparison applies.
780
- uint256 start = _batchStartOf[token][mid];
781
- if (leafIndex < start) {
782
- if (mid == lo) break; // Guard against `mid - 1` underflow at the lower edge.
783
- hi = mid - 1;
784
- } else if (leafIndex >= end) {
785
- if (mid == hi) break; // Mirror guard at the upper edge.
786
- lo = mid + 1;
787
- } else {
788
- return mid; // `start <= leafIndex < end`: leaf is inside this batch.
789
- }
723
+ // Ranges are non-overlapping across populated nonces. The first hit is therefore
724
+ // the unique conversion-rate batch for this claim leaf.
725
+ if (leafIndex >= _batchStartOf[token][nonce] && leafIndex < end) return nonce;
790
726
  }
791
727
  }
792
728
 
793
- // Window collapsed without a hit surface the same error the legacy linear scan used.
729
+ // Batches exist for the token, but none cover this leaf index; surface the same error used
730
+ // before the compact populated-nonce index was introduced.
794
731
  revert JBSwapCCIPSucker_BatchNotReceived({nonce: 0});
795
732
  }
796
733
  }
@@ -68,17 +68,17 @@ contract JBSwapCCIPSuckerDeployer is JBCCIPSuckerDeployer, IJBSwapCCIPSuckerDepl
68
68
  //*********************************************************************//
69
69
 
70
70
  /// @notice Configure the swap-specific constants. Can only be called once by the configurator.
71
- /// @param _bridgeToken The ERC-20 token used for CCIP bridging.
72
- /// @param _poolManager The Uniswap V4 PoolManager (can be address(0) if V4 unavailable).
73
- /// @param _v3Factory The Uniswap V3 factory (can be address(0) if V3 unavailable).
74
- /// @param _univ4Hook The V4 hook for pool discovery (optional, address(0) if none).
75
- /// @param _wrappedNativeToken The ERC-20 wrapper address for the chain's native token (e.g. WETH on Ethereum).
71
+ /// @param newBridgeToken The ERC-20 token used for CCIP bridging.
72
+ /// @param newPoolManager The Uniswap V4 PoolManager (can be address(0) if V4 unavailable).
73
+ /// @param newV3Factory The Uniswap V3 factory (can be address(0) if V3 unavailable).
74
+ /// @param newUniv4Hook The V4 hook for pool discovery (optional, address(0) if none).
75
+ /// @param newWrappedNativeToken The ERC-20 wrapper address for the chain's native token (e.g. WETH on Ethereum).
76
76
  function setSwapConstants(
77
- IERC20 _bridgeToken,
78
- IPoolManager _poolManager,
79
- IUniswapV3Factory _v3Factory,
80
- address _univ4Hook,
81
- address _wrappedNativeToken
77
+ IERC20 newBridgeToken,
78
+ IPoolManager newPoolManager,
79
+ IUniswapV3Factory newV3Factory,
80
+ address newUniv4Hook,
81
+ address newWrappedNativeToken
82
82
  )
83
83
  external
84
84
  {
@@ -93,23 +93,23 @@ contract JBSwapCCIPSuckerDeployer is JBCCIPSuckerDeployer, IJBSwapCCIPSuckerDepl
93
93
  }
94
94
 
95
95
  // Make sure the bridge token is not the zero address.
96
- if (address(_bridgeToken) == address(0)) {
97
- revert JBSwapCCIPSuckerDeployer_InvalidSwapConfig({bridgeToken: address(_bridgeToken)});
96
+ if (address(newBridgeToken) == address(0)) {
97
+ revert JBSwapCCIPSuckerDeployer_InvalidSwapConfig({bridgeToken: address(newBridgeToken)});
98
98
  }
99
99
 
100
100
  // Store the bridge token.
101
- bridgeToken = _bridgeToken;
101
+ bridgeToken = newBridgeToken;
102
102
 
103
103
  // Store the Uniswap V4 pool manager.
104
- poolManager = _poolManager;
104
+ poolManager = newPoolManager;
105
105
 
106
106
  // Store the Uniswap V3 factory.
107
- v3Factory = _v3Factory;
107
+ v3Factory = newV3Factory;
108
108
 
109
109
  // Store the Uniswap V4 hook address.
110
- univ4Hook = _univ4Hook;
110
+ univ4Hook = newUniv4Hook;
111
111
 
112
112
  // Store the wrapped native token address.
113
- wrappedNativeToken = _wrappedNativeToken;
113
+ wrappedNativeToken = newWrappedNativeToken;
114
114
  }
115
115
  }
@@ -128,12 +128,6 @@ interface IJBSucker is IERC165 {
128
128
  /// @return chainId The remote chain ID.
129
129
  function peerChainId() external view returns (uint256 chainId);
130
130
 
131
- /// @notice The last known total token supply on the peer chain, updated each time a bridge message is received.
132
- /// @dev Used by data hooks to compute `effectiveTotalSupply = localSupply + sum(peerChainTotalSupply)` across all
133
- /// suckers, preventing cash out tax bypass on chains where a holder dominates the local supply.
134
- /// @return The peer chain's total supply.
135
- function peerChainTotalSupply() external view returns (uint256);
136
-
137
131
  /// @notice The aggregate peer chain balance, normalized to a desired currency and decimal precision using JBPrices.
138
132
  /// @dev The balance is stored as ETH-denominated (18 decimals) and converted to the requested currency/decimals
139
133
  /// using the local JBPrices oracle.
@@ -150,6 +144,12 @@ interface IJBSucker is IERC165 {
150
144
  /// @return A `JBDenominatedAmount` with the converted value.
151
145
  function peerChainSurplusOf(uint256 decimals, uint256 currency) external view returns (JBDenominatedAmount memory);
152
146
 
147
+ /// @notice The last known total token supply on the peer chain, updated each time a bridge message is received.
148
+ /// @dev Used by data hooks to compute `effectiveTotalSupply = localSupply + sum(peerChainTotalSupply)` across all
149
+ /// suckers, preventing cash out tax bypass on chains where a holder dominates the local supply.
150
+ /// @return The peer chain's total supply.
151
+ function peerChainTotalSupply() external view returns (uint256);
152
+
153
153
  /// @notice The ID of the project on the local chain that this sucker is associated with.
154
154
  /// @return The project ID.
155
155
  function projectId() external view returns (uint256);
@@ -159,6 +159,11 @@ interface IJBSucker is IERC165 {
159
159
  /// @return The remote token info.
160
160
  function remoteTokenFor(address token) external view returns (JBRemoteToken memory);
161
161
 
162
+ /// @notice The freshness key of the latest accepted peer-chain economic snapshot.
163
+ /// @dev Higher values are fresher. The key is source-chain monotonic, not a value magnitude.
164
+ /// @return The latest peer-chain snapshot freshness key.
165
+ function snapshotTimestamp() external view returns (uint256);
166
+
162
167
  /// @notice The current deprecation state of this sucker.
163
168
  /// @return The sucker state.
164
169
  function state() external view returns (JBSuckerState);
@@ -77,13 +77,15 @@ interface IJBSuckerRegistry {
77
77
  function suckersOf(uint256 projectId) external view returns (address[] memory);
78
78
 
79
79
  /// @notice The cumulative total supply across all remote peer chains for a project.
80
- /// @dev Sums `peerChainTotalSupply` from each active sucker. Silently skips suckers that revert.
80
+ /// @dev Dedupes same-peer active suckers by freshest snapshot, then sums peer-chain values. Silently skips suckers
81
+ /// that revert.
81
82
  /// @param projectId The ID of the project.
82
83
  /// @return totalSupply The combined peer chain total supply.
83
84
  function remoteTotalSupplyOf(uint256 projectId) external view returns (uint256 totalSupply);
84
85
 
85
86
  /// @notice The cumulative balance across all remote peer chains for a project, denominated in a given currency.
86
- /// @dev Sums `peerChainBalanceOf` from each active sucker. Silently skips suckers that revert.
87
+ /// @dev Dedupes same-peer active suckers by freshest snapshot, then sums peer-chain values. Silently skips suckers
88
+ /// that revert.
87
89
  /// @param projectId The ID of the project.
88
90
  /// @param decimals The decimal precision for the returned value.
89
91
  /// @param currency The currency to normalize to.
@@ -98,7 +100,8 @@ interface IJBSuckerRegistry {
98
100
  returns (uint256 balance);
99
101
 
100
102
  /// @notice The cumulative surplus across all remote peer chains for a project, denominated in a given currency.
101
- /// @dev Sums `peerChainSurplusOf` from each active sucker. Silently skips suckers that revert.
103
+ /// @dev Dedupes same-peer active suckers by freshest snapshot, then sums peer-chain values. Silently skips suckers
104
+ /// that revert.
102
105
  /// @param projectId The ID of the project.
103
106
  /// @param decimals The decimal precision for the returned value.
104
107
  /// @param currency The currency to normalize to.
@@ -4,18 +4,18 @@ pragma solidity ^0.8.0;
4
4
  /// @notice Interface for Arbitrum L1 gateways to query the exact calldata they construct for retryable tickets.
5
5
  interface IL1ArbitrumGateway {
6
6
  /// @notice Returns the calldata that the gateway will submit as a retryable ticket to the Inbox.
7
- /// @param _token The L1 token address.
8
- /// @param _from The sender on L1.
9
- /// @param _to The recipient on L2.
10
- /// @param _amount The amount of tokens to bridge.
11
- /// @param _data Additional data (forwarded to the L2 gateway).
7
+ /// @param token The L1 token address.
8
+ /// @param from The sender on L1.
9
+ /// @param to The recipient on L2.
10
+ /// @param amount The amount of tokens to bridge.
11
+ /// @param data Additional data (forwarded to the L2 gateway).
12
12
  /// @return The ABI-encoded calldata for `finalizeInboundTransfer` on the L2 gateway.
13
13
  function getOutboundCalldata(
14
- address _token,
15
- address _from,
16
- address _to,
17
- uint256 _amount,
18
- bytes calldata _data
14
+ address token,
15
+ address from,
16
+ address to,
17
+ uint256 amount,
18
+ bytes calldata data
19
19
  )
20
20
  external
21
21
  view
@@ -133,6 +133,70 @@ library CCIPHelper {
133
133
  /// @notice The wrapped native token address for Tempo mod chain.
134
134
  address public constant TEMPO_MOD_WETH = 0xbb2D3310E4232085d432A2e04b2Ac09c46F634E4;
135
135
 
136
+ /// @notice Returns the LINK token address for a given chain ID.
137
+ /// @param chainId The EVM chain ID to look up.
138
+ /// @return link The LINK token address.
139
+ function linkOfChain(uint256 chainId) public pure returns (address link) {
140
+ if (chainId == ETH_ID) {
141
+ return ETH_LINK;
142
+ } else if (chainId == OP_ID) {
143
+ return OP_LINK;
144
+ } else if (chainId == ARB_ID) {
145
+ return ARB_LINK;
146
+ } else if (chainId == BASE_ID) {
147
+ return BASE_LINK;
148
+ } else if (chainId == ETH_SEP_ID) {
149
+ return ETH_SEP_LINK;
150
+ } else if (chainId == OP_SEP_ID) {
151
+ return OP_SEP_LINK;
152
+ } else if (chainId == ARB_SEP_ID) {
153
+ return ARB_SEP_LINK;
154
+ } else if (chainId == BASE_SEP_ID) {
155
+ return BASE_SEP_LINK;
156
+ } else if (chainId == TEMPO_ID) {
157
+ return TEMPO_LINK;
158
+ } else if (chainId == TEMPO_MOD_ID) {
159
+ return TEMPO_MOD_LINK;
160
+ } else {
161
+ revert CCIPHelper_UnsupportedChain({chainId: chainId});
162
+ }
163
+ }
164
+
165
+ /// @notice Returns the wrapped native token address for a given chain ID.
166
+ /// @param chainId The EVM chain ID to look up.
167
+ /// @return weth The wrapped native token address.
168
+ function wethOfChain(uint256 chainId) public pure returns (address weth) {
169
+ if (chainId == ETH_ID) {
170
+ return ETH_WETH;
171
+ } else if (chainId == OP_ID) {
172
+ return OP_WETH;
173
+ } else if (chainId == ARB_ID) {
174
+ return ARB_WETH;
175
+ } else if (chainId == POLY_ID) {
176
+ return POLY_WETH;
177
+ } else if (chainId == AVA_ID) {
178
+ return AVA_WETH;
179
+ } else if (chainId == BNB_ID) {
180
+ return BNB_WETH;
181
+ } else if (chainId == BASE_ID) {
182
+ return BASE_WETH;
183
+ } else if (chainId == ETH_SEP_ID) {
184
+ return ETH_SEP_WETH;
185
+ } else if (chainId == ARB_SEP_ID) {
186
+ return ARB_SEP_WETH;
187
+ } else if (chainId == OP_SEP_ID) {
188
+ return OP_SEP_WETH;
189
+ } else if (chainId == BASE_SEP_ID) {
190
+ return BASE_SEP_WETH;
191
+ } else if (chainId == TEMPO_ID) {
192
+ return TEMPO_WETH;
193
+ } else if (chainId == TEMPO_MOD_ID) {
194
+ return TEMPO_MOD_WETH;
195
+ } else {
196
+ revert CCIPHelper_UnsupportedChain({chainId: chainId});
197
+ }
198
+ }
199
+
136
200
  /// @notice Returns the CCIP router address for a given chain ID.
137
201
  /// @param chainId The EVM chain ID to look up.
138
202
  /// @return router The CCIP router address.
@@ -202,68 +266,4 @@ library CCIPHelper {
202
266
  revert CCIPHelper_UnsupportedChain({chainId: chainId});
203
267
  }
204
268
  }
205
-
206
- /// @notice Returns the wrapped native token address for a given chain ID.
207
- /// @param chainId The EVM chain ID to look up.
208
- /// @return weth The wrapped native token address.
209
- function wethOfChain(uint256 chainId) public pure returns (address weth) {
210
- if (chainId == ETH_ID) {
211
- return ETH_WETH;
212
- } else if (chainId == OP_ID) {
213
- return OP_WETH;
214
- } else if (chainId == ARB_ID) {
215
- return ARB_WETH;
216
- } else if (chainId == POLY_ID) {
217
- return POLY_WETH;
218
- } else if (chainId == AVA_ID) {
219
- return AVA_WETH;
220
- } else if (chainId == BNB_ID) {
221
- return BNB_WETH;
222
- } else if (chainId == BASE_ID) {
223
- return BASE_WETH;
224
- } else if (chainId == ETH_SEP_ID) {
225
- return ETH_SEP_WETH;
226
- } else if (chainId == ARB_SEP_ID) {
227
- return ARB_SEP_WETH;
228
- } else if (chainId == OP_SEP_ID) {
229
- return OP_SEP_WETH;
230
- } else if (chainId == BASE_SEP_ID) {
231
- return BASE_SEP_WETH;
232
- } else if (chainId == TEMPO_ID) {
233
- return TEMPO_WETH;
234
- } else if (chainId == TEMPO_MOD_ID) {
235
- return TEMPO_MOD_WETH;
236
- } else {
237
- revert CCIPHelper_UnsupportedChain({chainId: chainId});
238
- }
239
- }
240
-
241
- /// @notice Returns the LINK token address for a given chain ID.
242
- /// @param chainId The EVM chain ID to look up.
243
- /// @return link The LINK token address.
244
- function linkOfChain(uint256 chainId) public pure returns (address link) {
245
- if (chainId == ETH_ID) {
246
- return ETH_LINK;
247
- } else if (chainId == OP_ID) {
248
- return OP_LINK;
249
- } else if (chainId == ARB_ID) {
250
- return ARB_LINK;
251
- } else if (chainId == BASE_ID) {
252
- return BASE_LINK;
253
- } else if (chainId == ETH_SEP_ID) {
254
- return ETH_SEP_LINK;
255
- } else if (chainId == OP_SEP_ID) {
256
- return OP_SEP_LINK;
257
- } else if (chainId == ARB_SEP_ID) {
258
- return ARB_SEP_LINK;
259
- } else if (chainId == BASE_SEP_ID) {
260
- return BASE_SEP_LINK;
261
- } else if (chainId == TEMPO_ID) {
262
- return TEMPO_LINK;
263
- } else if (chainId == TEMPO_MOD_ID) {
264
- return TEMPO_MOD_LINK;
265
- } else {
266
- revert CCIPHelper_UnsupportedChain({chainId: chainId});
267
- }
268
- }
269
269
  }
@@ -133,10 +133,15 @@ library JBCCIPLib {
133
133
  SafeERC20.forceApprove({token: IERC20(feeToken), spender: router, value: totalApproval});
134
134
 
135
135
  ICCIPRouter(router).ccipSend({destinationChainSelector: chainSel, message: message});
136
+
137
+ _clearTokenAmountApprovals({router: router, tokenAmounts: tokenAmounts});
138
+ SafeERC20.forceApprove({token: IERC20(feeToken), spender: router, value: 0});
136
139
  } else {
137
140
  // Native ETH fee path.
138
141
  ICCIPRouter(router).ccipSend{value: fees}({destinationChainSelector: chainSel, message: message});
139
142
 
143
+ _clearTokenAmountApprovals({router: router, tokenAmounts: tokenAmounts});
144
+
140
145
  // Calculate the excess transport payment to refund.
141
146
  refundAmount = transportPayment - fees;
142
147
 
@@ -189,4 +194,17 @@ library JBCCIPLib {
189
194
  // ABI-decode the type and payload from the raw data.
190
195
  (messageType, payload) = abi.decode(data, (uint8, bytes));
191
196
  }
197
+
198
+ //*********************************************************************//
199
+ // ----------------------- private helpers --------------------------- //
200
+ //*********************************************************************//
201
+
202
+ /// @notice Clear token approvals granted to the CCIP router for the current message.
203
+ /// @param router The CCIP router whose allowances should be revoked.
204
+ /// @param tokenAmounts The bridged token amounts approved for this message.
205
+ function _clearTokenAmountApprovals(address router, Client.EVMTokenAmount[] memory tokenAmounts) private {
206
+ for (uint256 i; i < tokenAmounts.length; i++) {
207
+ SafeERC20.forceApprove({token: IERC20(tokenAmounts[i].token), spender: router, value: 0});
208
+ }
209
+ }
192
210
  }
@@ -35,7 +35,7 @@ library JBRelayBeneficiary {
35
35
  returns (address effectiveBeneficiary)
36
36
  {
37
37
  // Only trust relay metadata when the payer is a registered sucker for this project.
38
- if (!registry.isSuckerOf(projectId, payer)) {
38
+ if (!registry.isSuckerOf({projectId: projectId, addr: payer})) {
39
39
  return beneficiary;
40
40
  }
41
41