@bananapus/suckers-v6 0.0.57 → 0.0.58

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bananapus/suckers-v6",
3
- "version": "0.0.57",
3
+ "version": "0.0.58",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -26,7 +26,7 @@
26
26
  },
27
27
  "dependencies": {
28
28
  "@arbitrum/nitro-contracts": "3.2.0",
29
- "@bananapus/core-v6": "^0.0.60",
29
+ "@bananapus/core-v6": "^0.0.66",
30
30
  "@bananapus/permission-ids-v6": "^0.0.27",
31
31
  "@chainlink/contracts-ccip": "1.6.4",
32
32
  "@chainlink/local": "0.2.7",
@@ -16,7 +16,7 @@ contract JBBaseSucker is JBOptimismSucker {
16
16
  // ---------------------------- constructor -------------------------- //
17
17
  //*********************************************************************//
18
18
 
19
- /// @param deployer A contract that deploys the clones for this contracts.
19
+ /// @param deployer A contract that deploys clones of this contract.
20
20
  /// @param directory A contract storing directories of terminals and controllers for each project.
21
21
  /// @param permissions A contract storing permissions.
22
22
  /// @param prices The price oracle used to convert peer-chain balances and surplus.
@@ -35,7 +35,7 @@ contract JBCeloSucker is JBOptimismSucker {
35
35
  // ---------------------------- constructor -------------------------- //
36
36
  //*********************************************************************//
37
37
 
38
- /// @param deployer A contract that deploys the clones for this contracts.
38
+ /// @param deployer A contract that deploys clones of this contract.
39
39
  /// @param directory A contract storing directories of terminals and controllers for each project.
40
40
  /// @param permissions A contract storing permissions.
41
41
  /// @param prices The price oracle used to convert peer-chain balances and surplus.
@@ -135,7 +135,7 @@ contract JBCeloSucker is JBOptimismSucker {
135
135
  {
136
136
  index; // Silence unused parameter warning (not needed for Celo bridge).
137
137
 
138
- // Revert if there's a `msg.value`. The OP bridge does not expect to be paid.
138
+ // Revert if there's a `msg.value`. The Celo bridge does not expect to be paid.
139
139
  if (transportPayment != 0) {
140
140
  revert JBSucker_UnexpectedMsgValue({value: transportPayment});
141
141
  }
@@ -36,7 +36,7 @@ contract JBOptimismSucker is JBSucker, IJBOptimismSucker {
36
36
  // ---------------------------- constructor -------------------------- //
37
37
  //*********************************************************************//
38
38
 
39
- /// @param deployer A contract that deploys the clones for this contracts.
39
+ /// @param deployer A contract that deploys clones of this contract.
40
40
  /// @param directory A contract storing directories of terminals and controllers for each project.
41
41
  /// @param permissions A contract storing permissions.
42
42
  /// @param prices The price oracle used to convert peer-chain balances and surplus.
package/src/JBSucker.sol CHANGED
@@ -95,12 +95,10 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
95
95
  // ------------------------- public constants ------------------------ //
96
96
  //*********************************************************************//
97
97
 
98
- /// @notice A reasonable minimum gas limit for a basic cross-chain call. The minimum amount of gas required to call
99
- /// the `fromRemote` (successfully/safely) on the remote chain.
98
+ /// @notice A reasonable minimum gas limit for a basic cross-chain call to `fromRemote` on the remote chain.
100
99
  uint32 public constant override MESSENGER_BASE_GAS_LIMIT = 300_000;
101
100
 
102
- /// @notice A reasonable minimum gas limit used when bridging ERC-20s. The minimum amount of gas required to
103
- /// (successfully/safely) perform a transfer on the remote chain.
101
+ /// @notice A reasonable minimum gas limit for performing an ERC-20 transfer on the remote chain.
104
102
  uint32 public constant override MESSENGER_ERC20_MIN_GAS_LIMIT = 200_000;
105
103
 
106
104
  /// @notice The message format version. Used to reject incompatible messages from remote chains.
@@ -601,7 +599,7 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
601
599
  account: _ownerOf(_projectId), projectId: _projectId, permissionId: JBPermissionIds.SET_SUCKER_DEPRECATION
602
600
  });
603
601
 
604
- // This is the earliest time for when the sucker can be considered deprecated.
602
+ // This is the earliest time the sucker can be considered deprecated.
605
603
  // There is a mandatory delay to allow for remaining messages to be received.
606
604
  // This should be called on both sides of the suckers, preferably with a matching timestamp.
607
605
  uint256 nextEarliestDeprecationTime = block.timestamp + _maxMessagingDelay();
@@ -804,7 +802,7 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
804
802
  return JBSuckerState.ENABLED;
805
803
  }
806
804
 
807
- // The sucker will soon be considered deprecated, this functions only as a warning to users.
805
+ // The sucker is close to deprecation; this state only warns users.
808
806
  // Deprecation state is intentionally time-based.
809
807
  // forge-lint: disable-next-line(block-timestamp)
810
808
  if (block.timestamp < _deprecatedAfter - _maxMessagingDelay()) {
@@ -960,7 +958,7 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
960
958
  }
961
959
  }
962
960
 
963
- /// @notice The action(s) to perform after a user has succesfully proven their claim.
961
+ /// @notice Actions to perform after a user has successfully proven their claim.
964
962
  /// @param terminalToken The terminal token to suck.
965
963
  /// @param terminalTokenAmount The amount of terminal tokens.
966
964
  /// @param projectTokenAmount The amount of project tokens.
@@ -1204,10 +1202,8 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
1204
1202
  }
1205
1203
 
1206
1204
  /// @notice Send the outbox root for the specified token to the remote peer.
1207
- /// @dev The call may have a `transportPayment` for bridging native tokens. Require it to be `0` if it is not
1208
- /// needed. Make sure if a value being paid to the bridge is expected to revert if the given value is `0`.
1209
- /// @param transportPayment the amount of `msg.value` that is going to get paid for sending this message. (usually
1210
- /// derived from `msg.value`)
1205
+ /// @dev Some bridges require a nonzero `transportPayment`; zero-cost bridges must reject nonzero values.
1206
+ /// @param transportPayment The amount of `msg.value` paid to the transport for this message.
1211
1207
  /// @param token The terminal token to bridge the merkle tree of.
1212
1208
  /// @param remoteToken The remote token which the `token` is mapped to.
1213
1209
  function _sendRoot(uint256 transportPayment, address token, JBRemoteToken memory remoteToken) internal virtual {
@@ -1324,8 +1320,7 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
1324
1320
  // Register the leaf as executed to prevent double-spending.
1325
1321
  _executedFor[terminalToken].set(index);
1326
1322
 
1327
- // Calculate the root based on the leaf, the branch, and the index.
1328
- // Compare to the current root, Revert if they do not match.
1323
+ // Calculate the root and compare it to the current inbox root.
1329
1324
  _validateBranchRoot({
1330
1325
  expectedRoot: _inboxOf[terminalToken].root,
1331
1326
  projectTokenCount: projectTokenCount,
@@ -1371,7 +1366,7 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
1371
1366
  index: index
1372
1367
  });
1373
1368
 
1374
- // Compare to the current root, Revert if they do not match.
1369
+ // Revert if the computed root does not match the expected inbox root.
1375
1370
  if (root != expectedRoot) {
1376
1371
  revert JBSucker_InvalidProof({root: root, inboxRoot: expectedRoot});
1377
1372
  }
@@ -1453,8 +1448,7 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
1453
1448
  _executedFor[emergencyExitAddress].set(index);
1454
1449
  }
1455
1450
 
1456
- // Calculate the root based on the leaf, the branch, and the index.
1457
- // Compare to the current root, Revert if they do not match.
1451
+ // Calculate the root and compare it to the current outbox root.
1458
1452
  _validateBranchRoot({
1459
1453
  expectedRoot: _computeOutboxRoot(_outboxOf[terminalToken].tree),
1460
1454
  projectTokenCount: projectTokenCount,
@@ -35,6 +35,7 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
35
35
  error JBSuckerRegistry_InvalidDeployer(IJBSuckerDeployer deployer);
36
36
  error JBSuckerRegistry_SuckerDoesNotBelongToProject(uint256 projectId, address sucker);
37
37
  error JBSuckerRegistry_SuckerIsNotDeprecated(address sucker, JBSuckerState suckerState);
38
+ error JBSuckerRegistry_ZeroPeerChainId(address sucker);
38
39
 
39
40
  //*********************************************************************//
40
41
  // ------------------------- public constants ------------------------ //
@@ -142,8 +143,9 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
142
143
  (, uint256 val) = _suckersOf[projectId].tryGet(allSuckers[i]);
143
144
  if (val == _SUCKER_EXISTS) {
144
145
  IJBSucker sucker = IJBSucker(allSuckers[i]);
145
- pairs[j] =
146
- JBSuckersPair({local: address(sucker), remote: sucker.peer(), remoteChainId: sucker.peerChainId()});
146
+ pairs[j] = JBSuckersPair({
147
+ local: address(sucker), remote: sucker.peer(), remoteChainId: _peerChainIdOf(sucker)
148
+ });
147
149
  unchecked {
148
150
  ++j;
149
151
  }
@@ -220,7 +222,7 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
220
222
  try IJBSucker(allSuckers[i]).peerChainBalanceOf({decimals: decimals, currency: currency}) returns (
221
223
  JBDenominatedAmount memory amt
222
224
  ) {
223
- uint256 chainId = IJBSucker(allSuckers[i]).peerChainId();
225
+ uint256 chainId = _peerChainIdOf(IJBSucker(allSuckers[i]));
224
226
  scratch.chainCount = _recordPeerValue({
225
227
  scratch: scratch,
226
228
  chainId: chainId,
@@ -276,7 +278,7 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
276
278
  try IJBSucker(allSuckers[i]).peerChainSurplusOf({decimals: decimals, currency: currency}) returns (
277
279
  JBDenominatedAmount memory amt
278
280
  ) {
279
- uint256 chainId = IJBSucker(allSuckers[i]).peerChainId();
281
+ uint256 chainId = _peerChainIdOf(IJBSucker(allSuckers[i]));
280
282
  scratch.chainCount = _recordPeerValue({
281
283
  scratch: scratch,
282
284
  chainId: chainId,
@@ -319,7 +321,7 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
319
321
  // Include both active and deprecated suckers in aggregate economic views.
320
322
  if (val == _SUCKER_EXISTS || val == _SUCKER_DEPRECATED) {
321
323
  try IJBSucker(allSuckers[i]).peerChainTotalSupply() returns (uint256 supply) {
322
- uint256 chainId = IJBSucker(allSuckers[i]).peerChainId();
324
+ uint256 chainId = _peerChainIdOf(IJBSucker(allSuckers[i]));
323
325
  scratch.chainCount = _recordPeerValue({
324
326
  scratch: scratch,
325
327
  chainId: chainId,
@@ -377,6 +379,14 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
377
379
  scratch.hasActiveValue = new bool[](len);
378
380
  }
379
381
 
382
+ /// @notice Reads a sucker's peer chain ID, reverting if the sucker cannot identify a real peer chain.
383
+ /// @param sucker The sucker to query.
384
+ /// @return chainId The non-zero peer chain ID.
385
+ function _peerChainIdOf(IJBSucker sucker) internal view returns (uint256 chainId) {
386
+ chainId = sucker.peerChainId();
387
+ if (chainId == 0) revert JBSuckerRegistry_ZeroPeerChainId({sucker: address(sucker)});
388
+ }
389
+
380
390
  /// @notice Records a project-scoped peer-chain aggregate value.
381
391
  /// @dev Callers pass scratch arrays sized from `_suckersOf[projectId].keys()`, so entries are already scoped to
382
392
  /// the project being aggregated. For each peer chain, active suckers replace deprecated suckers; deprecated
@@ -544,6 +554,7 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
544
554
  // Create the sucker.
545
555
  IJBSucker sucker = configuration.deployer
546
556
  .createForSender({localProjectId: projectId, salt: salt, peer: configuration.peer});
557
+ _peerChainIdOf(sucker);
547
558
  suckers[i] = address(sucker);
548
559
 
549
560
  // Store the sucker as being deployed for this project.
@@ -88,6 +88,7 @@ contract JBSwapCCIPSucker is JBCCIPSucker, IUnlockCallback, IUniswapV3SwapCallba
88
88
 
89
89
  error JBSwapCCIPSucker_BatchNotReceived(uint64 nonce);
90
90
  error JBSwapCCIPSucker_CallerNotPoolManager(address caller);
91
+ error JBSwapCCIPSucker_DuplicateBatch(uint64 nonce);
91
92
  error JBSwapCCIPSucker_InvalidBridgeToken(address bridgeToken, address wrappedNativeToken);
92
93
  error JBSwapCCIPSucker_NoPendingSwap(address localToken, uint64 nonce, bool retrySwapLocked);
93
94
  error JBSwapCCIPSucker_OnlySelf(address caller, address expected);
@@ -336,6 +337,19 @@ contract JBSwapCCIPSucker is JBCCIPSucker, IUnlockCallback, IUniswapV3SwapCallba
336
337
  }
337
338
  }
338
339
 
340
+ // Detect an already-processed batch before the swap path. The inbox nonce alone cannot be used here:
341
+ // CCIP can deliver nonce 2 before nonce 1, and nonce 1 still needs its self-described batch metadata.
342
+ if (
343
+ _batchEndOf[localToken][nonce] != 0 || _conversionRateOf[localToken][nonce].leafTotal != 0
344
+ || pendingSwapOf[localToken][nonce].bridgeAmount != 0
345
+ ) {
346
+ if (deliveredAmount != 0) {
347
+ revert JBSwapCCIPSucker_DuplicateBatch({nonce: nonce});
348
+ }
349
+
350
+ return;
351
+ }
352
+
339
353
  // After the validation block above, `deliveredToken != address(0)` iff a delivery was present,
340
354
  // because the invariants ensure it equals `BRIDGE_TOKEN` (a non-zero ERC-20) whenever there is one.
341
355
  if (deliveredToken != address(0)) {
@@ -378,61 +392,54 @@ contract JBSwapCCIPSucker is JBCCIPSucker, IUnlockCallback, IUniswapV3SwapCallba
378
392
  //
379
393
  // Detect "already seen" without extra storage: a nonce has been processed if it has
380
394
  // either a batch range (batchEnd > 0) or a conversion rate / pending swap recorded.
381
- if (
382
- _batchEndOf[localToken][nonce] == 0 && _conversionRateOf[localToken][nonce].leafTotal == 0
383
- && pendingSwapOf[localToken][nonce].leafTotal == 0
384
- ) {
385
- // Record the batch range so _findNonceForLeafIndex can resolve leaf ownership
386
- // independently of nonce ordering. Each nonce is self-describing: [start, end).
387
- if (batchEnd > 0) {
388
- // Record this batch's half-open leaf range `[batchStart, batchEnd)`. Self-
389
- // describing per-nonce — no implicit chain across nonces — so out-of-order
390
- // delivery can still resolve a leaf to its batch.
391
- _batchStartOf[localToken][nonce] = batchStart;
392
- _batchEndOf[localToken][nonce] = batchEnd;
393
-
394
- // Append `nonce` to the populated-nonce list for this token. The outer
395
- // `_batchEndOf == 0 && _conversionRateOf == 0 && pendingSwapOf == 0` guard
396
- // fires at most once per (token, nonce), so each populated nonce is appended
397
- // exactly once the array stays duplicate-free without extra checks.
398
- //
399
- // Reading `_populatedNonceCount[localToken]` first into a local lets us write
400
- // the new slot and the new count in a single read-modify-write pair (one
401
- // SLOAD, two SSTOREs to distinct slots). The `unchecked` increment is safe:
402
- // `priorCount` is bounded by the total number of populated nonces, which is
403
- // upper-bounded by the CCIP nonce space (`uint64`) — overflow requires more
404
- // batches than `uint64.max`, which the inbox can never produce.
405
- uint64 priorCount = _populatedNonceCount[localToken];
406
- _populatedNonceByIndex[localToken][priorCount] = nonce;
407
- unchecked {
408
- _populatedNonceCount[localToken] = priorCount + 1;
409
- }
395
+ // Record the batch range so _findNonceForLeafIndex can resolve leaf ownership
396
+ // independently of nonce ordering. Each nonce is self-describing: [start, end).
397
+ if (batchEnd > 0) {
398
+ // Record this batch's half-open leaf range `[batchStart, batchEnd)`. Self-
399
+ // describing per-nonce no implicit chain across nonces so out-of-order
400
+ // delivery can still resolve a leaf to its batch.
401
+ _batchStartOf[localToken][nonce] = batchStart;
402
+ _batchEndOf[localToken][nonce] = batchEnd;
403
+
404
+ // Append `nonce` to the populated-nonce list for this token. The duplicate guard
405
+ // above fires at most once per (token, nonce), so each populated nonce is appended
406
+ // exactly once — the array stays duplicate-free without extra checks.
407
+ //
408
+ // Reading `_populatedNonceCount[localToken]` first into a local lets us write
409
+ // the new slot and the new count in a single read-modify-write pair (one
410
+ // SLOAD, two SSTOREs to distinct slots). The `unchecked` increment is safe:
411
+ // `priorCount` is bounded by the total number of populated nonces, which is
412
+ // upper-bounded by the CCIP nonce space (`uint64`) — overflow requires more
413
+ // batches than `uint64.max`, which the inbox can never produce.
414
+ uint64 priorCount = _populatedNonceCount[localToken];
415
+ _populatedNonceByIndex[localToken][priorCount] = nonce;
416
+ unchecked {
417
+ _populatedNonceCount[localToken] = priorCount + 1;
410
418
  }
419
+ }
420
+
421
+ // Store pendingSwapOf for failed swaps now that nonce is validated.
422
+ if (swapFailed) {
423
+ pendingSwapOf[localToken][nonce] =
424
+ PendingSwap({bridgeToken: deliveredToken, bridgeAmount: deliveredAmount, leafTotal: leafTotal});
425
+ }
411
426
 
412
- // Store pendingSwapOf for failed swaps now that nonce is validated.
413
- if (swapFailed) {
427
+ // Zero-output swap guard: When a swap succeeds but returns zero local tokens, the
428
+ // batch must NOT be marked claimable. Without this guard, `_addToBalance` would see
429
+ // `pendingSwapOf.bridgeAmount == 0` (no pending swap stored) and allow claims to
430
+ // proceed — minting the full bridged project-token amount while adding zero terminal
431
+ // backing, breaking cross-chain solvency.
432
+ //
433
+ // Route zero-output swaps into `pendingSwapOf` so the swap can be retried via
434
+ // `retrySwap` once pool conditions improve. Only store the conversion rate when
435
+ // the swap produced a positive local amount.
436
+ if (leafTotal > 0 && !swapFailed) {
437
+ if (localAmount == 0 && deliveredAmount > 0) {
414
438
  pendingSwapOf[localToken][nonce] =
415
439
  PendingSwap({bridgeToken: deliveredToken, bridgeAmount: deliveredAmount, leafTotal: leafTotal});
416
- }
417
-
418
- // Zero-output swap guard: When a swap succeeds but returns zero local tokens, the
419
- // batch must NOT be marked claimable. Without this guard, `_addToBalance` would see
420
- // `pendingSwapOf.bridgeAmount == 0` (no pending swap stored) and allow claims to
421
- // proceed — minting the full bridged project-token amount while adding zero terminal
422
- // backing, breaking cross-chain solvency.
423
- //
424
- // Route zero-output swaps into `pendingSwapOf` so the swap can be retried via
425
- // `retrySwap` once pool conditions improve. Only store the conversion rate when
426
- // the swap produced a positive local amount.
427
- if (leafTotal > 0 && !swapFailed) {
428
- if (localAmount == 0 && deliveredAmount > 0) {
429
- pendingSwapOf[localToken][nonce] = PendingSwap({
430
- bridgeToken: deliveredToken, bridgeAmount: deliveredAmount, leafTotal: leafTotal
431
- });
432
- } else {
433
- _conversionRateOf[localToken][nonce] =
434
- ConversionRate({leafTotal: leafTotal, localTotal: localAmount});
435
- }
440
+ } else {
441
+ _conversionRateOf[localToken][nonce] =
442
+ ConversionRate({leafTotal: leafTotal, localTotal: localAmount});
436
443
  }
437
444
  }
438
445
  } else {