@bananapus/suckers-v6 0.0.38 → 0.0.40

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.38",
3
+ "version": "0.0.40",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -31,7 +31,7 @@
31
31
  "dependencies": {
32
32
  "@arbitrum/nitro-contracts": "3.2.0",
33
33
  "@bananapus/core-v6": "^0.0.44",
34
- "@bananapus/permission-ids-v6": "^0.0.23",
34
+ "@bananapus/permission-ids-v6": "^0.0.25",
35
35
  "@chainlink/contracts-ccip": "1.6.4",
36
36
  "@chainlink/local": "0.2.7",
37
37
  "@openzeppelin/contracts": "5.6.1",
@@ -497,6 +497,13 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
497
497
  // default peer symmetry assumption will not hold.
498
498
  salt = keccak256(abi.encode(sender, salt));
499
499
 
500
+ // Cache the project owner so the explicit-peer gate can check against the original authority, not a
501
+ // delegated operator. The default same-address peering invariant (peer == 0 or peer == address(this))
502
+ // does not need this stronger gate, but a non-symmetric explicit peer authorizes that arbitrary address
503
+ // to deliver outbox roots and mint project tokens — so it must require a permission strictly broader
504
+ // than ops automation's `DEPLOY_SUCKERS`.
505
+ address projectOwner = PROJECTS.ownerOf(projectId);
506
+
500
507
  // Iterate through the configurations and deploy the suckers.
501
508
  for (uint256 i; i < configurations.length;) {
502
509
  // Get the configuration being iterated over.
@@ -507,6 +514,18 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
507
514
  revert JBSuckerRegistry_InvalidDeployer({deployer: configuration.deployer});
508
515
  }
509
516
 
517
+ // If the configuration specifies a non-symmetric explicit peer, require the additional
518
+ // `SET_SUCKER_PEER` permission. Default peering (peer == 0 or peer == bytes32 of address(this)) is
519
+ // unaffected. Without this gate, a delegated operator with only `DEPLOY_SUCKERS` could register an
520
+ // attacker peer and use the resulting sucker's mint authority to deliver fabricated outbox roots.
521
+ // forge-lint: disable-next-line(unsafe-typecast)
522
+ bytes32 selfPeer = bytes32(uint256(uint160(address(this))));
523
+ if (configuration.peer != bytes32(0) && configuration.peer != selfPeer) {
524
+ _requirePermissionFrom({
525
+ account: projectOwner, projectId: projectId, permissionId: JBPermissionIds.SET_SUCKER_PEER
526
+ });
527
+ }
528
+
510
529
  // Create the sucker.
511
530
  IJBSucker sucker = configuration.deployer
512
531
  .createForSender({localProjectId: projectId, salt: salt, peer: configuration.peer});
@@ -224,7 +224,7 @@ contract JBSwapCCIPSucker is JBCCIPSucker, IUnlockCallback, IUniswapV3SwapCallba
224
224
  POOL_MANAGER = swapDeployer.poolManager();
225
225
  V3_FACTORY = swapDeployer.v3Factory();
226
226
  UNIV4_HOOK = swapDeployer.univ4Hook();
227
- WRAPPED_NATIVE_TOKEN = IWrappedNativeToken(swapDeployer.weth());
227
+ WRAPPED_NATIVE_TOKEN = IWrappedNativeToken(swapDeployer.wrappedNativeToken());
228
228
 
229
229
  if (address(BRIDGE_TOKEN) == address(0)) {
230
230
  revert JBSwapCCIPSucker_InvalidBridgeToken({
@@ -306,20 +306,25 @@ contract JBSwapCCIPSucker is JBCCIPSucker, IUnlockCallback, IUniswapV3SwapCallba
306
306
  }
307
307
  }
308
308
 
309
- // Capture the inbox nonce before fromRemote to detect whether the root was accepted.
310
- uint64 inboxNonceBefore = _inboxOf[localToken].nonce;
311
-
312
309
  // Store the inbox merkle root for later claims.
313
310
  // Must be called BEFORE writing batch metadata and conversion rates so that stale
314
311
  // (duplicate/replayed) roots that fromRemote silently rejects do not overwrite
315
312
  // metadata from the original accepted delivery.
316
313
  this.fromRemote(root);
317
314
 
318
- // Only write batch metadata and conversion rates if fromRemote accepted this root.
319
- // fromRemote updates _inboxOf[localToken].nonce when the root's nonce is strictly
320
- // greater than the current inbox nonce; stale roots are silently rejected.
321
- // We detect acceptance by checking if the nonce actually changed.
322
- if (_inboxOf[localToken].nonce > inboxNonceBefore) {
315
+ // Write batch metadata if this nonce hasn't been seen before.
316
+ // Decoupled from nonce advancement to support out-of-order CCIP delivery:
317
+ // if nonce 2 arrives before nonce 1, fromRemote only advances the inbox for nonce 2,
318
+ // but we still need to record nonce 1's batch metadata when it arrives later.
319
+ // The Merkle tree is append-only, so nonce 1's leaves are provable against nonce 2's root.
320
+ //
321
+ // Detect "already seen" without extra storage: a nonce has been processed if it has
322
+ // either a batch range (batchEnd > 0) or a conversion rate / pending swap recorded.
323
+ if (
324
+ _batchEndOf[localToken][root.remoteRoot.nonce] == 0
325
+ && _conversionRateOf[localToken][root.remoteRoot.nonce].leafTotal == 0
326
+ && pendingSwapOf[localToken][root.remoteRoot.nonce].leafTotal == 0
327
+ ) {
323
328
  // Record the batch range so _findNonceForLeafIndex can resolve leaf ownership
324
329
  // independently of nonce ordering. Each nonce is self-describing: [start, end).
325
330
  if (batchEnd > 0) {
@@ -605,7 +610,7 @@ contract JBSwapCCIPSucker is JBCCIPSucker, IUnlockCallback, IUniswapV3SwapCallba
605
610
  v3Factory: V3_FACTORY,
606
611
  poolManager: POOL_MANAGER,
607
612
  univ4Hook: UNIV4_HOOK,
608
- weth: address(WRAPPED_NATIVE_TOKEN)
613
+ wrappedNativeToken: address(WRAPPED_NATIVE_TOKEN)
609
614
  }),
610
615
  tokenIn: tokenIn,
611
616
  tokenOut: tokenOut,
@@ -42,7 +42,7 @@ contract JBSwapCCIPSuckerDeployer is JBCCIPSuckerDeployer, IJBSwapCCIPSuckerDepl
42
42
  address public univ4Hook;
43
43
 
44
44
  /// @notice The ERC-20 wrapper address for the chain's native token (e.g. WETH on Ethereum).
45
- address public weth;
45
+ address public wrappedNativeToken;
46
46
 
47
47
  //*********************************************************************//
48
48
  // ---------------------------- constructor -------------------------- //
@@ -72,13 +72,13 @@ contract JBSwapCCIPSuckerDeployer is JBCCIPSuckerDeployer, IJBSwapCCIPSuckerDepl
72
72
  /// @param _poolManager The Uniswap V4 PoolManager (can be address(0) if V4 unavailable).
73
73
  /// @param _v3Factory The Uniswap V3 factory (can be address(0) if V3 unavailable).
74
74
  /// @param _univ4Hook The V4 hook for pool discovery (optional, address(0) if none).
75
- /// @param _weth The ERC-20 wrapper address for the chain's native token (e.g. WETH on Ethereum).
75
+ /// @param _wrappedNativeToken The ERC-20 wrapper address for the chain's native token (e.g. WETH on Ethereum).
76
76
  function setSwapConstants(
77
77
  IERC20 _bridgeToken,
78
78
  IPoolManager _poolManager,
79
79
  IUniswapV3Factory _v3Factory,
80
80
  address _univ4Hook,
81
- address _weth
81
+ address _wrappedNativeToken
82
82
  )
83
83
  external
84
84
  {
@@ -110,6 +110,6 @@ contract JBSwapCCIPSuckerDeployer is JBCCIPSuckerDeployer, IJBSwapCCIPSuckerDepl
110
110
  univ4Hook = _univ4Hook;
111
111
 
112
112
  // Store the wrapped native token address.
113
- weth = _weth;
113
+ wrappedNativeToken = _wrappedNativeToken;
114
114
  }
115
115
  }
@@ -24,5 +24,5 @@ interface IJBSwapCCIPSuckerDeployer is IJBCCIPSuckerDeployer {
24
24
 
25
25
  /// @notice The ERC-20 wrapper address for the chain's native token (e.g. WETH on Ethereum). Used for V3 native
26
26
  /// swaps.
27
- function weth() external view returns (address);
27
+ function wrappedNativeToken() external view returns (address);
28
28
  }
@@ -130,11 +130,11 @@ library JBSuckerLib {
130
130
  projectId: projectId,
131
131
  pricingCurrency: tokenCurrency,
132
132
  unitCurrency: JBCurrencyIds.ETH,
133
- decimals: _ETH_DECIMALS
133
+ decimals: dec
134
134
  }) returns (
135
135
  uint256 price
136
136
  ) {
137
- ethBalance += mulDiv({x: bal, y: price, denominator: 10 ** dec});
137
+ ethBalance += mulDiv({x: bal, y: 10 ** _ETH_DECIMALS, denominator: price});
138
138
  } catch {}
139
139
  }
140
140
  }
@@ -77,12 +77,13 @@ library JBSwapPoolLib {
77
77
  /// @custom:member v3Factory The Uniswap V3 factory used for pool discovery.
78
78
  /// @custom:member poolManager The Uniswap V4 pool manager used for V4 pool queries and swaps.
79
79
  /// @custom:member univ4Hook The address of the Uniswap V4 hook contract to search for hooked pools.
80
- /// @custom:member weth The address of the ERC-20 wrapper for the chain's native token (e.g. WETH on Ethereum).
80
+ /// @custom:member wrappedNativeToken The address of the ERC-20 wrapper for the chain's native token (e.g. WETH on
81
+ /// Ethereum).
81
82
  struct SwapConfig {
82
83
  IUniswapV3Factory v3Factory;
83
84
  IPoolManager poolManager;
84
85
  address univ4Hook;
85
- address weth;
86
+ address wrappedNativeToken;
86
87
  }
87
88
 
88
89
  //*********************************************************************//
@@ -109,8 +110,8 @@ library JBSwapPoolLib {
109
110
  returns (uint256 amountOut)
110
111
  {
111
112
  // Normalize NATIVE_TOKEN sentinel to the wrapped native token for pool lookups.
112
- address normalizedIn = _normalize({token: tokenIn, weth: config.weth});
113
- address normalizedOut = _normalize({token: tokenOut, weth: config.weth});
113
+ address normalizedIn = _normalize({token: tokenIn, wrappedNativeToken: config.wrappedNativeToken});
114
+ address normalizedOut = _normalize({token: tokenOut, wrappedNativeToken: config.wrappedNativeToken});
114
115
 
115
116
  // No swap needed if tokens are the same after normalization (e.g., NATIVE_TOKEN and wrapped native token).
116
117
  if (normalizedIn == normalizedOut) return amount;
@@ -167,15 +168,15 @@ library JBSwapPoolLib {
167
168
  }
168
169
  // V3 outputs wrapped native token for native pairs — unwrap to raw native token.
169
170
  if (tokenOut == JBConstants.NATIVE_TOKEN) {
170
- IWrappedNativeToken(config.weth).withdraw(amountOut);
171
+ IWrappedNativeToken(config.wrappedNativeToken).withdraw(amountOut);
171
172
  }
172
173
  }
173
174
 
174
175
  // V4 outputs native tokens for wrapped-native-paired pools. If the caller requested the wrapped form
175
176
  // (not NATIVE_TOKEN), wrap the received native tokens so the caller gets the token they expect.
176
- if (isV4 && tokenOut != JBConstants.NATIVE_TOKEN && normalizedOut == config.weth) {
177
+ if (isV4 && tokenOut != JBConstants.NATIVE_TOKEN && normalizedOut == config.wrappedNativeToken) {
177
178
  // Wrap through the configured wrapped native token contract.
178
- IWrappedNativeToken(config.weth).deposit{value: amountOut}();
179
+ IWrappedNativeToken(config.wrappedNativeToken).deposit{value: amountOut}();
179
180
  }
180
181
  }
181
182
 
@@ -193,7 +194,7 @@ library JBSwapPoolLib {
193
194
  int256 amountSpecified,
194
195
  uint160 sqrtPriceLimitX96,
195
196
  uint256 minAmountOut,
196
- address weth
197
+ address wrappedNativeToken
197
198
  ) = abi.decode(data, (PoolKey, bool, int256, uint160, uint256, address));
198
199
 
199
200
  uint256 amountIn;
@@ -237,8 +238,14 @@ library JBSwapPoolLib {
237
238
  // Settle input (pay what we owe to the PoolManager).
238
239
  Currency inputCurrency = zeroForOne ? key.currency0 : key.currency1;
239
240
  if (Currency.unwrap(inputCurrency) == address(0)) {
240
- // Native token: unwrap if needed, then settle by sending native value directly.
241
- if (weth != address(0)) IWrappedNativeToken(weth).withdraw(amountIn);
241
+ // Native token: unwrap wrapped native token if the caller holds it, then settle directly.
242
+ // The buyback hook holds wrapped native tokens (needs unwrap), while suckers hold raw ETH (skip unwrap).
243
+ if (wrappedNativeToken != address(0)) {
244
+ uint256 wrappedBalance = IERC20(wrappedNativeToken).balanceOf(address(this));
245
+ if (wrappedBalance >= amountIn) {
246
+ IWrappedNativeToken(wrappedNativeToken).withdraw(amountIn);
247
+ }
248
+ }
242
249
  poolManager.settle{value: amountIn}();
243
250
  } else {
244
251
  // ERC-20: sync the currency balance, transfer tokens, then settle.
@@ -440,8 +447,8 @@ library JBSwapPoolLib {
440
447
  address sorted0;
441
448
  address sorted1;
442
449
  {
443
- address v4In = normalizedTokenIn == config.weth ? address(0) : normalizedTokenIn;
444
- address v4Out = normalizedTokenOut == config.weth ? address(0) : normalizedTokenOut;
450
+ address v4In = normalizedTokenIn == config.wrappedNativeToken ? address(0) : normalizedTokenIn;
451
+ address v4Out = normalizedTokenOut == config.wrappedNativeToken ? address(0) : normalizedTokenOut;
445
452
 
446
453
  // Sort tokens to match the V4 currency ordering convention.
447
454
  (sorted0, sorted1) = v4In < v4Out ? (v4In, v4Out) : (v4Out, v4In);
@@ -668,8 +675,8 @@ library JBSwapPoolLib {
668
675
  minAmountOut = _quoteWithSlippage({
669
676
  amount: amount,
670
677
  liquidity: liquidity,
671
- tokenIn: normalizedTokenIn == config.weth ? address(0) : normalizedTokenIn,
672
- tokenOut: normalizedTokenOut == config.weth ? address(0) : normalizedTokenOut,
678
+ tokenIn: normalizedTokenIn == config.wrappedNativeToken ? address(0) : normalizedTokenIn,
679
+ tokenOut: normalizedTokenOut == config.wrappedNativeToken ? address(0) : normalizedTokenOut,
673
680
  tick: tick,
674
681
  poolFeeBps: feeBps
675
682
  });
@@ -1001,7 +1008,7 @@ library JBSwapPoolLib {
1001
1008
  returns (uint256 amountOut)
1002
1009
  {
1003
1010
  // Convert wrapped native token to address(0) for V4's native token convention.
1004
- address v4In = normalizedTokenIn == config.weth ? address(0) : normalizedTokenIn;
1011
+ address v4In = normalizedTokenIn == config.wrappedNativeToken ? address(0) : normalizedTokenIn;
1005
1012
 
1006
1013
  // Determine swap direction based on currency ordering in the pool key.
1007
1014
  bool zeroForOne = Currency.unwrap(key.currency0) == v4In;
@@ -1019,7 +1026,9 @@ library JBSwapPoolLib {
1019
1026
  // forge-lint: disable-next-line(unsafe-typecast)
1020
1027
  int256 exactInputAmount = -int256(amount);
1021
1028
 
1022
- unlockData = abi.encode(key, zeroForOne, exactInputAmount, sqrtPriceLimitX96, minAmountOut, config.weth);
1029
+ unlockData = abi.encode(
1030
+ key, zeroForOne, exactInputAmount, sqrtPriceLimitX96, minAmountOut, config.wrappedNativeToken
1031
+ );
1023
1032
  }
1024
1033
 
1025
1034
  // Unlock the PoolManager and encode the swap parameters for the callback.
@@ -1031,10 +1040,10 @@ library JBSwapPoolLib {
1031
1040
 
1032
1041
  /// @notice Normalize a token address, converting the NATIVE_TOKEN sentinel to the wrapped native token.
1033
1042
  /// @param token The token address to normalize.
1034
- /// @param weth The wrapped native token address on this chain.
1043
+ /// @param wrappedNativeToken The wrapped native token address on this chain.
1035
1044
  /// @return The normalized token address.
1036
- function _normalize(address token, address weth) internal pure returns (address) {
1037
- return token == JBConstants.NATIVE_TOKEN ? weth : token;
1045
+ function _normalize(address token, address wrappedNativeToken) internal pure returns (address) {
1046
+ return token == JBConstants.NATIVE_TOKEN ? wrappedNativeToken : token;
1038
1047
  }
1039
1048
 
1040
1049
  /// @notice Get the Uniswap V3 fee tier for a given index.