@bananapus/suckers-v6 0.0.30 → 0.0.31
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/RISKS.md +8 -0
- package/package.json +1 -1
- package/src/JBArbitrumSucker.sol +3 -4
- package/src/JBSucker.sol +6 -6
- package/src/JBSwapCCIPSucker.sol +63 -43
- package/src/libraries/JBSwapPoolLib.sol +48 -33
- package/test/ForkArbitrum.t.sol +12 -12
- package/test/ForkCelo.t.sol +12 -12
- package/test/ForkClaimMainnet.t.sol +7 -122
- package/test/ForkMainnet.t.sol +7 -128
- package/test/ForkOPStack.t.sol +3 -68
- package/test/ForkSwapMainnet.t.sol +3 -87
- package/test/audit/{codex-PeerDeterminism.t.sol → PeerDeterminism.t.sol} +6 -6
- package/test/audit/{2026-04-22-codex-nemesis-RegistryPeerAuthBreak.t.sol → RegistryPeerAuthBreak.t.sol} +17 -17
- package/test/audit/{2026-04-24-codex-nemesis-RegistryPeerMismatch.t.sol → RegistryPeerMismatch.t.sol} +11 -11
- package/test/audit/StaleNonceMetadataOverwrite.t.sol +318 -0
- package/test/fork/OptimismSuckerFork.t.sol +6 -6
- package/test/helpers/SuckerForkHelpers.sol +126 -0
- package/test/unit/deployer.t.sol +6 -6
- package/test/unit/merkle.t.sol +27 -27
- package/test/unit/multi_chain_evolution.t.sol +8 -10
- /package/test/audit/{codex-CCIPLegacyFormatCompatibility.t.sol → CCIPLegacyFormatCompatibility.t.sol} +0 -0
- /package/test/audit/{codex-CCIPWrappedNativeMisunwrap.t.sol → CCIPWrappedNativeMisunwrap.t.sol} +0 -0
- /package/test/audit/{codex-nemesis-DeprecatedRemovalUndercount.t.sol → DeprecatedRemovalUndercount.t.sol} +0 -0
- /package/test/audit/{CertikAIScan.t.sol → DeprecationBoundaryAndMapToken.t.sol} +0 -0
- /package/test/audit/{codex-FeeLocking.t.sol → FeeLocking.t.sol} +0 -0
- /package/test/audit/{2026-04-24-codex-nemesis-FreshRound.t.sol → FreshRound.t.sol} +0 -0
- /package/test/audit/{codex-MapTokensEnableOnlyValueStuck.t.sol → MapTokensEnableOnlyValueStuck.t.sol} +0 -0
- /package/test/audit/{codex-PeerSnapshotDesync.t.sol → PeerSnapshotDesync.t.sol} +0 -0
- /package/test/audit/{2026-04-22-codex-nemesis-PeerTopologyAuthBreak.t.sol → PeerTopologyAuthBreak.t.sol} +0 -0
- /package/test/audit/{2026-04-21-codex-nemesis-RegistryStaleDeprecatedMaxSurplus.t.sol → RegistryStaleDeprecatedMaxSurplus.t.sol} +0 -0
- /package/test/audit/{2026-04-24-codex-nemesis-RegistryStaleMaxAggregation.t.sol → RegistryStaleMaxAggregation.t.sol} +0 -0
- /package/test/audit/{codex-SwapBatchRateMixing.t.sol → SwapBatchRateMixing.t.sol} +0 -0
- /package/test/audit/{codex-NemesisSwapQueueOrder.t.sol → SwapQueueOrder.t.sol} +0 -0
- /package/test/audit/{codex-SwapZeroAmountBatchGap.t.sol → SwapZeroAmountBatchGap.t.sol} +0 -0
- /package/test/audit/{codex-nemesis-SwapZeroLocalTotalUnbackedClaim.t.sol → SwapZeroLocalTotalUnbackedClaim.t.sol} +0 -0
- /package/test/audit/{codex-ToRemoteFeeIrrecoverable.t.sol → ToRemoteFeeIrrecoverable.t.sol} +0 -0
- /package/test/audit/{2026-04-25-codex-nemesis-TransientClaimContext.t.sol → TransientClaimContext.t.sol} +0 -0
- /package/test/audit/{2026-04-22-codex-nemesis-ZeroOutputRetryClaim.t.sol → ZeroOutputRetryClaim.t.sol} +0 -0
package/RISKS.md
CHANGED
|
@@ -151,3 +151,11 @@ When the only available Uniswap pool for a cross-denomination swap is a hookless
|
|
|
151
151
|
### 10.9 Zero-value `prepare()` is allowed
|
|
152
152
|
|
|
153
153
|
`prepare()` does not reject `projectTokenCount == 0`. A zero-value check would be trivially bypassed by passing `1` instead, so it provides no real protection against remap-window consumption. The cost to create a leaf with `projectTokenCount = 1` is negligible (1 wei of project tokens). The one-time remap window is protected by the token mapping's `enabled` flag and the outbox tree count, not by minimum deposit requirements.
|
|
154
|
+
|
|
155
|
+
### 10.10 Cross-chain currency uses standardized `JBCurrencyIds.ETH` (1), not local token addresses
|
|
156
|
+
|
|
157
|
+
Snapshot messages encode surplus and balance values using `JBCurrencyIds.ETH` (currency ID `1`) as the cross-chain currency identifier, not `uint32(uint160(JBConstants.NATIVE_TOKEN))` (currency ID `61166`). This is intentional: `NATIVE_TOKEN` (`0x000...EEEe`) is a local sentinel meaning "the native token on this chain," which may represent different assets on different networks (e.g., ETH on mainnet, MATIC on Polygon). A standardized semantic currency ID is required for cross-chain values to be comparable.
|
|
158
|
+
|
|
159
|
+
On the consuming side, contracts like `REVOwner` and `REVLoans` query sucker-reported values using `uint32(uint160(NATIVE_TOKEN))` — the local terminal convention. The `JBPrices` oracle in `JBSuckerLib.convertPeerValue` resolves the conversion between currency ID `1` (ETH) and currency ID `61166` (native token) when a price feed is registered for the pair. If no feed exists, `convertPeerValue` returns `0`, which is acceptable: it means the project has not configured cross-chain pricing for that token pair, and remote values are simply not factored into local calculations.
|
|
160
|
+
|
|
161
|
+
Projects that need accurate cross-chain surplus and supply accounting should register a price feed for the `1 ↔ 61166` pair via `JBPrices`. On chains where the native token is ETH, this is a 1:1 identity feed. On chains where the native token is not ETH, the feed should reflect the actual exchange rate.
|
package/package.json
CHANGED
package/src/JBArbitrumSucker.sol
CHANGED
|
@@ -206,8 +206,8 @@ contract JBArbitrumSucker is JBSucker, IJBArbitrumSucker {
|
|
|
206
206
|
// slither-disable-next-line calls-loop,unused-return
|
|
207
207
|
IArbL2GatewayRouter(address(GATEWAYROUTER))
|
|
208
208
|
.outboundTransfer({
|
|
209
|
-
|
|
210
|
-
|
|
209
|
+
l1Token: _toAddress(remoteToken.addr), to: peerAddress, amount: amount, data: bytes("")
|
|
210
|
+
});
|
|
211
211
|
} else {
|
|
212
212
|
// Otherwise, the token is the native token, and the amount will be sent as `msg.value`.
|
|
213
213
|
nativeValue = amount;
|
|
@@ -269,8 +269,7 @@ contract JBArbitrumSucker is JBSucker, IJBArbitrumSucker {
|
|
|
269
269
|
IL1ArbitrumGateway(gateway)
|
|
270
270
|
.getOutboundCalldata({
|
|
271
271
|
_token: token, _from: address(this), _to: _peerAddress(), _amount: amount, _data: bytes("")
|
|
272
|
-
})
|
|
273
|
-
.length;
|
|
272
|
+
}).length;
|
|
274
273
|
// slither-disable-next-line calls-loop
|
|
275
274
|
maxSubmissionCostERC20 = ARBINBOX.calculateRetryableSubmissionFee({
|
|
276
275
|
dataLength: outboundCalldataLength, baseFee: maxFeePerGas
|
package/src/JBSucker.sol
CHANGED
|
@@ -874,12 +874,12 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
874
874
|
// slither-disable-next-line calls-loop,unused-return
|
|
875
875
|
IJBController(address(DIRECTORY.controllerOf(cachedProjectId)))
|
|
876
876
|
.mintTokensOf({
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
877
|
+
projectId: cachedProjectId,
|
|
878
|
+
tokenCount: projectTokenAmount,
|
|
879
|
+
beneficiary: _toAddress(beneficiary),
|
|
880
|
+
memo: "",
|
|
881
|
+
useReservedPercent: false
|
|
882
|
+
});
|
|
883
883
|
}
|
|
884
884
|
|
|
885
885
|
/// @notice Inserts a new leaf into the outbox merkle tree for the specified `token`.
|
package/src/JBSwapCCIPSucker.sol
CHANGED
|
@@ -273,6 +273,7 @@ contract JBSwapCCIPSucker is JBCCIPSucker, IUnlockCallback, IUniswapV3SwapCallba
|
|
|
273
273
|
|
|
274
274
|
address localToken = _toAddress(root.token);
|
|
275
275
|
uint256 localAmount;
|
|
276
|
+
bool swapFailed;
|
|
276
277
|
|
|
277
278
|
if (any2EvmMessage.destTokenAmounts.length == 1) {
|
|
278
279
|
Client.EVMTokenAmount memory tokenAmount = any2EvmMessage.destTokenAmounts[0];
|
|
@@ -284,7 +285,7 @@ contract JBSwapCCIPSucker is JBCCIPSucker, IUnlockCallback, IUniswapV3SwapCallba
|
|
|
284
285
|
// Swap bridge token -> local token via best V3/V4 pool.
|
|
285
286
|
// Wrapped in try-catch so a swap failure doesn't revert the entire CCIP message
|
|
286
287
|
// (which would leave tokens stuck in the OffRamp). On failure, bridge tokens are
|
|
287
|
-
// stored for later retry via `retrySwap
|
|
288
|
+
// stored for later retry via `retrySwap` (written below, after nonce validation).
|
|
288
289
|
// slither-disable-next-line reentrancy-benign,reentrancy-events
|
|
289
290
|
try this.executeSwapExternal({
|
|
290
291
|
tokenIn: tokenAmount.token, tokenOut: localToken, amount: tokenAmount.amount
|
|
@@ -293,59 +294,78 @@ contract JBSwapCCIPSucker is JBCCIPSucker, IUnlockCallback, IUniswapV3SwapCallba
|
|
|
293
294
|
) {
|
|
294
295
|
localAmount = swapped;
|
|
295
296
|
} catch {
|
|
296
|
-
|
|
297
|
-
pendingSwapOf
|
|
298
|
-
|
|
299
|
-
});
|
|
300
|
-
// localAmount stays 0 — the conversion rate code below will set
|
|
301
|
-
// localTotal: 0, gating claims until retrySwap succeeds.
|
|
297
|
+
swapFailed = true;
|
|
298
|
+
// localAmount stays 0 — pendingSwapOf and conversion rate are written
|
|
299
|
+
// below, after fromRemote validates the nonce.
|
|
302
300
|
}
|
|
303
301
|
}
|
|
304
302
|
}
|
|
305
303
|
|
|
306
|
-
//
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
304
|
+
// Capture the inbox nonce before fromRemote to detect whether the root was accepted.
|
|
305
|
+
uint64 inboxNonceBefore = _inboxOf[localToken].nonce;
|
|
306
|
+
|
|
307
|
+
// Store the inbox merkle root for later claims.
|
|
308
|
+
// Must be called BEFORE writing batch metadata and conversion rates so that stale
|
|
309
|
+
// (duplicate/replayed) roots that fromRemote silently rejects do not overwrite
|
|
310
|
+
// metadata from the original accepted delivery.
|
|
311
|
+
this.fromRemote(root);
|
|
312
|
+
|
|
313
|
+
// Only write batch metadata and conversion rates if fromRemote accepted this root.
|
|
314
|
+
// fromRemote updates _inboxOf[localToken].nonce when the root's nonce is strictly
|
|
315
|
+
// greater than the current inbox nonce; stale roots are silently rejected.
|
|
316
|
+
// We detect acceptance by checking if the nonce actually changed.
|
|
317
|
+
if (_inboxOf[localToken].nonce > inboxNonceBefore) {
|
|
318
|
+
// Record the batch range so _findNonceForLeafIndex can resolve leaf ownership
|
|
319
|
+
// independently of nonce ordering. Each nonce is self-describing: [start, end).
|
|
320
|
+
if (batchEnd > 0) {
|
|
321
|
+
_batchStartOf[localToken][root.remoteRoot.nonce] = batchStart;
|
|
322
|
+
_batchEndOf[localToken][root.remoteRoot.nonce] = batchEnd;
|
|
323
|
+
if (root.remoteRoot.nonce > _highestReceivedNonce[localToken]) {
|
|
324
|
+
_highestReceivedNonce[localToken] = root.remoteRoot.nonce;
|
|
325
|
+
}
|
|
313
326
|
}
|
|
314
|
-
}
|
|
315
327
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
328
|
+
// Store pendingSwapOf for failed swaps now that nonce is validated.
|
|
329
|
+
if (swapFailed) {
|
|
330
|
+
Client.EVMTokenAmount memory failedTokenAmount = any2EvmMessage.destTokenAmounts[0];
|
|
331
|
+
pendingSwapOf[localToken][root.remoteRoot.nonce] = PendingSwap({
|
|
332
|
+
bridgeToken: failedTokenAmount.token,
|
|
333
|
+
bridgeAmount: failedTokenAmount.amount,
|
|
334
|
+
leafTotal: root.amount
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Zero-output swap guard: When a swap succeeds but returns zero local tokens, the
|
|
339
|
+
// batch must NOT be marked claimable. Without this guard, `_addToBalance` would see
|
|
340
|
+
// `pendingSwapOf.bridgeAmount == 0` (no pending swap stored) and allow claims to
|
|
341
|
+
// proceed — minting the full bridged project-token amount while adding zero terminal
|
|
342
|
+
// backing, breaking cross-chain solvency.
|
|
343
|
+
//
|
|
344
|
+
// Route zero-output swaps into `pendingSwapOf` so the swap can be retried via
|
|
345
|
+
// `retrySwap` once pool conditions improve. Only store the conversion rate when
|
|
346
|
+
// the swap produced a positive local amount.
|
|
347
|
+
if (root.amount > 0 && !swapFailed) {
|
|
348
|
+
if (localAmount == 0 && any2EvmMessage.destTokenAmounts.length == 1) {
|
|
349
|
+
Client.EVMTokenAmount memory zeroSwapTokenAmount = any2EvmMessage.destTokenAmounts[0];
|
|
350
|
+
// Only route to pending if there were actual bridge tokens delivered.
|
|
351
|
+
// If bridgeAmount is also 0 (zero-value batch), store the conversion rate
|
|
352
|
+
// normally — there is nothing to retry.
|
|
353
|
+
if (zeroSwapTokenAmount.amount > 0) {
|
|
354
|
+
pendingSwapOf[localToken][root.remoteRoot.nonce] = PendingSwap({
|
|
355
|
+
bridgeToken: zeroSwapTokenAmount.token,
|
|
356
|
+
bridgeAmount: zeroSwapTokenAmount.amount,
|
|
357
|
+
leafTotal: root.amount
|
|
358
|
+
});
|
|
359
|
+
} else {
|
|
360
|
+
_conversionRateOf[localToken][root.remoteRoot.nonce] =
|
|
361
|
+
ConversionRate({leafTotal: root.amount, localTotal: 0});
|
|
362
|
+
}
|
|
337
363
|
} else {
|
|
338
364
|
_conversionRateOf[localToken][root.remoteRoot.nonce] =
|
|
339
|
-
ConversionRate({leafTotal: root.amount, localTotal:
|
|
365
|
+
ConversionRate({leafTotal: root.amount, localTotal: localAmount});
|
|
340
366
|
}
|
|
341
|
-
} else {
|
|
342
|
-
_conversionRateOf[localToken][root.remoteRoot.nonce] =
|
|
343
|
-
ConversionRate({leafTotal: root.amount, localTotal: localAmount});
|
|
344
367
|
}
|
|
345
368
|
}
|
|
346
|
-
|
|
347
|
-
// Store the inbox merkle root for later claims.
|
|
348
|
-
this.fromRemote(root);
|
|
349
369
|
} else {
|
|
350
370
|
revert JBCCIPSucker_UnknownMessageType(messageType);
|
|
351
371
|
}
|
|
@@ -183,40 +183,50 @@ library JBSwapPoolLib {
|
|
|
183
183
|
/// @return Encoded output amount.
|
|
184
184
|
function executeV4UnlockCallback(IPoolManager poolManager, bytes calldata data) external returns (bytes memory) {
|
|
185
185
|
// Decode the swap parameters packed during _executeV4Swap.
|
|
186
|
-
(
|
|
187
|
-
|
|
186
|
+
(
|
|
187
|
+
PoolKey memory key,
|
|
188
|
+
bool zeroForOne,
|
|
189
|
+
int256 amountSpecified,
|
|
190
|
+
uint160 sqrtPriceLimitX96,
|
|
191
|
+
uint256 minAmountOut,
|
|
192
|
+
address weth
|
|
193
|
+
) = abi.decode(data, (PoolKey, bool, int256, uint160, uint256, address));
|
|
188
194
|
|
|
189
|
-
// Execute the swap through the V4 PoolManager.
|
|
190
|
-
BalanceDelta delta = poolManager.swap({
|
|
191
|
-
key: key,
|
|
192
|
-
params: SwapParams({
|
|
193
|
-
zeroForOne: zeroForOne, amountSpecified: amountSpecified, sqrtPriceLimitX96: sqrtPriceLimitX96
|
|
194
|
-
}),
|
|
195
|
-
hookData: ""
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
// V4 sign convention: negative = we owe (input), positive = we're owed (output).
|
|
199
|
-
int128 delta0 = delta.amount0();
|
|
200
|
-
int128 delta1 = delta.amount1();
|
|
201
195
|
uint256 amountIn;
|
|
202
196
|
uint256 amountOut;
|
|
203
197
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
198
|
+
{
|
|
199
|
+
// Execute the swap through the V4 PoolManager.
|
|
200
|
+
BalanceDelta delta = poolManager.swap({
|
|
201
|
+
key: key,
|
|
202
|
+
params: SwapParams({
|
|
203
|
+
zeroForOne: zeroForOne, amountSpecified: amountSpecified, sqrtPriceLimitX96: sqrtPriceLimitX96
|
|
204
|
+
}),
|
|
205
|
+
hookData: ""
|
|
206
|
+
});
|
|
212
207
|
|
|
213
|
-
|
|
214
|
-
|
|
208
|
+
// V4 sign convention: negative = we owe (input), positive = we're owed (output).
|
|
209
|
+
int128 delta0 = delta.amount0();
|
|
210
|
+
int128 delta1 = delta.amount1();
|
|
211
|
+
|
|
212
|
+
// Extract input and output amounts based on swap direction.
|
|
213
|
+
if (zeroForOne) {
|
|
214
|
+
amountIn = uint256(uint128(-delta0));
|
|
215
|
+
amountOut = uint256(uint128(delta1));
|
|
216
|
+
} else {
|
|
217
|
+
amountIn = uint256(uint128(-delta1));
|
|
218
|
+
amountOut = uint256(uint128(delta0));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Enforce the minimum output from the TWAP quote.
|
|
222
|
+
if (amountOut < minAmountOut) revert JBSwapPoolLib_SlippageExceeded(amountOut, minAmountOut);
|
|
223
|
+
}
|
|
215
224
|
|
|
216
225
|
// Settle input (pay what we owe to the PoolManager).
|
|
217
226
|
Currency inputCurrency = zeroForOne ? key.currency0 : key.currency1;
|
|
218
227
|
if (Currency.unwrap(inputCurrency) == address(0)) {
|
|
219
|
-
// Native ETH: settle by sending ETH value directly.
|
|
228
|
+
// Native ETH: unwrap WETH if needed, then settle by sending ETH value directly.
|
|
229
|
+
if (weth != address(0)) IWrappedNativeToken(weth).withdraw(amountIn);
|
|
220
230
|
// slither-disable-next-line unused-return,arbitrary-send-eth
|
|
221
231
|
poolManager.settle{value: amountIn}();
|
|
222
232
|
} else {
|
|
@@ -832,17 +842,22 @@ library JBSwapPoolLib {
|
|
|
832
842
|
// Determine swap direction based on currency ordering in the pool key.
|
|
833
843
|
bool zeroForOne = Currency.unwrap(key.currency0) == v4In;
|
|
834
844
|
|
|
835
|
-
//
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
845
|
+
// Build the encoded unlock data in a scoped block to avoid stack-too-deep.
|
|
846
|
+
bytes memory unlockData;
|
|
847
|
+
{
|
|
848
|
+
// Compute the sqrt price limit from the expected amounts.
|
|
849
|
+
uint160 sqrtPriceLimitX96 = JBSwapLib.sqrtPriceLimitFromAmounts({
|
|
850
|
+
amountIn: amount, minimumAmountOut: minAmountOut, zeroForOne: zeroForOne
|
|
851
|
+
});
|
|
839
852
|
|
|
840
|
-
|
|
841
|
-
|
|
853
|
+
// V4 uses negative amounts for exact-input swaps.
|
|
854
|
+
int256 exactInputAmount = -int256(amount);
|
|
855
|
+
|
|
856
|
+
unlockData = abi.encode(key, zeroForOne, exactInputAmount, sqrtPriceLimitX96, minAmountOut, config.weth);
|
|
857
|
+
}
|
|
842
858
|
|
|
843
859
|
// Unlock the PoolManager and encode the swap parameters for the callback.
|
|
844
|
-
bytes memory result =
|
|
845
|
-
config.poolManager.unlock(abi.encode(key, zeroForOne, exactInputAmount, sqrtPriceLimitX96, minAmountOut));
|
|
860
|
+
bytes memory result = config.poolManager.unlock(unlockData);
|
|
846
861
|
|
|
847
862
|
// Decode the output amount returned by the unlock callback.
|
|
848
863
|
amountOut = abi.decode(result, (uint256));
|
package/test/ForkArbitrum.t.sol
CHANGED
|
@@ -199,12 +199,12 @@ contract ForkArbitrumDeployerTest is TestBaseWorkflow, IERC721Receiver {
|
|
|
199
199
|
|
|
200
200
|
jbController()
|
|
201
201
|
.launchProjectFor({
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
202
|
+
owner: address(this),
|
|
203
|
+
projectUri: "arb-fork-test",
|
|
204
|
+
rulesetConfigurations: _rulesetConfigurations,
|
|
205
|
+
terminalConfigurations: _terminalConfigurations,
|
|
206
|
+
memo: ""
|
|
207
|
+
});
|
|
208
208
|
}
|
|
209
209
|
|
|
210
210
|
function onERC721Received(address, address, uint256, bytes calldata) external pure returns (bytes4) {
|
|
@@ -340,12 +340,12 @@ contract ForkArbitrumNativeTransferTest is TestBaseWorkflow {
|
|
|
340
340
|
|
|
341
341
|
jbController()
|
|
342
342
|
.launchProjectFor({
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
343
|
+
owner: multisig(),
|
|
344
|
+
projectUri: "arb-native-fork-test",
|
|
345
|
+
rulesetConfigurations: _rulesetConfigurations,
|
|
346
|
+
terminalConfigurations: _terminalConfigurations,
|
|
347
|
+
memo: ""
|
|
348
|
+
});
|
|
349
349
|
}
|
|
350
350
|
|
|
351
351
|
/// @notice Test native ETH transfer from L1 → Arbitrum L2 via the real Arbitrum inbox.
|
package/test/ForkCelo.t.sol
CHANGED
|
@@ -212,12 +212,12 @@ contract ForkCeloTest is TestBaseWorkflow {
|
|
|
212
212
|
|
|
213
213
|
jbController()
|
|
214
214
|
.launchProjectFor({
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
215
|
+
owner: multisig(),
|
|
216
|
+
projectUri: "celo-fork-test-native",
|
|
217
|
+
rulesetConfigurations: _rulesetConfigurations,
|
|
218
|
+
terminalConfigurations: _terminalConfigurations,
|
|
219
|
+
memo: ""
|
|
220
|
+
});
|
|
221
221
|
}
|
|
222
222
|
|
|
223
223
|
/// @notice Launch a project on Celo that accepts WETH (ETH as ERC-20).
|
|
@@ -259,12 +259,12 @@ contract ForkCeloTest is TestBaseWorkflow {
|
|
|
259
259
|
|
|
260
260
|
jbController()
|
|
261
261
|
.launchProjectFor({
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
262
|
+
owner: multisig(),
|
|
263
|
+
projectUri: "celo-fork-test-weth",
|
|
264
|
+
rulesetConfigurations: _rulesetConfigurations,
|
|
265
|
+
terminalConfigurations: _terminalConfigurations,
|
|
266
|
+
memo: ""
|
|
267
|
+
});
|
|
268
268
|
}
|
|
269
269
|
|
|
270
270
|
// ── Tests
|
|
@@ -3,6 +3,7 @@ pragma solidity 0.8.28;
|
|
|
3
3
|
|
|
4
4
|
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
5
5
|
import /* {*} from */ "@bananapus/core-v6/test/helpers/TestBaseWorkflow.sol";
|
|
6
|
+
import {SuckerForkHelpers} from "./helpers/SuckerForkHelpers.sol";
|
|
6
7
|
import {IJBSucker} from "../src/interfaces/IJBSucker.sol";
|
|
7
8
|
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
8
9
|
import {JBPermissionsData} from "@bananapus/core-v6/src/structs/JBPermissionsData.sol";
|
|
@@ -22,7 +23,7 @@ import {JBSucker} from "../src/JBSucker.sol";
|
|
|
22
23
|
import "forge-std/Test.sol";
|
|
23
24
|
import {JBCCIPSuckerDeployer} from "src/deployers/JBCCIPSuckerDeployer.sol";
|
|
24
25
|
import {JBCCIPSucker} from "../src/JBCCIPSucker.sol";
|
|
25
|
-
import {CCIPLocalSimulatorFork
|
|
26
|
+
import {CCIPLocalSimulatorFork} from "@chainlink/local/src/ccip/CCIPLocalSimulatorFork.sol";
|
|
26
27
|
import {CCIPHelper} from "../src/libraries/CCIPHelper.sol";
|
|
27
28
|
|
|
28
29
|
/// @dev Captured leaf data from InsertToOutboxTree event.
|
|
@@ -39,9 +40,8 @@ struct LeafData {
|
|
|
39
40
|
/// @dev Tests the full round-trip: pay → prepare → toRemote (L1) → manual ccipReceive → claim (L2).
|
|
40
41
|
/// Uses the dual-fork pattern from ForkMainnet.t.sol (real CCIP router on send side) combined with
|
|
41
42
|
/// the manual ccipReceive pattern from ForkSwapMainnet.t.sol (prank as router on receive side).
|
|
42
|
-
abstract contract CCIPSuckerClaimForkTestBase is
|
|
43
|
+
abstract contract CCIPSuckerClaimForkTestBase is SuckerForkHelpers {
|
|
43
44
|
CCIPLocalSimulatorFork ccipLocalSimulatorFork;
|
|
44
|
-
JBRulesetMetadata _metadata;
|
|
45
45
|
|
|
46
46
|
JBCCIPSuckerDeployer suckerDeployerL1;
|
|
47
47
|
JBCCIPSuckerDeployer suckerDeployerL2;
|
|
@@ -61,13 +61,6 @@ abstract contract CCIPSuckerClaimForkTestBase is TestBaseWorkflow {
|
|
|
61
61
|
function _l1ForkBlock() internal pure virtual returns (uint256);
|
|
62
62
|
function _l2ForkBlock() internal pure virtual returns (uint256);
|
|
63
63
|
|
|
64
|
-
// ── Token overrides (defaults: native token on both sides)
|
|
65
|
-
// ────────────────────────────────
|
|
66
|
-
|
|
67
|
-
function _terminalToken() internal view virtual returns (address) {
|
|
68
|
-
return JBConstants.NATIVE_TOKEN;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
64
|
function _remoteTerminalToken() internal view virtual returns (bytes32) {
|
|
72
65
|
return bytes32(uint256(uint160(JBConstants.NATIVE_TOKEN)));
|
|
73
66
|
}
|
|
@@ -77,83 +70,22 @@ abstract contract CCIPSuckerClaimForkTestBase is TestBaseWorkflow {
|
|
|
77
70
|
return true;
|
|
78
71
|
}
|
|
79
72
|
|
|
80
|
-
// ── LINK token addresses per mainnet chain
|
|
81
|
-
// ────────────────────────────────
|
|
82
|
-
|
|
83
|
-
function _linkTokenOf(uint256 chainId) internal pure returns (address) {
|
|
84
|
-
if (chainId == 1) return 0x514910771AF9Ca656af840dff83E8264EcF986CA;
|
|
85
|
-
if (chainId == 42_161) return 0xf97f4df75117a78c1A5a0DBb814Af92458539FB4;
|
|
86
|
-
if (chainId == 10) return 0x350a791Bfc2C21F9Ed5d10980Dad2e2638ffa7f6;
|
|
87
|
-
if (chainId == 8453) return 0x88Fb150BDc53A65fe94Dea0c9BA0a6dAf8C6e196;
|
|
88
|
-
if (chainId == 4217) return 0x15C03488B29e27d62BAf10E30b0c474bf60E0264;
|
|
89
|
-
revert("Unsupported chain for LINK token");
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// ── Register mainnet network details
|
|
93
|
-
// ──────────────────────────────────
|
|
94
|
-
|
|
95
|
-
function _registerMainnetDetails(uint256 chainId) internal {
|
|
96
|
-
Register.NetworkDetails memory details = Register.NetworkDetails({
|
|
97
|
-
chainSelector: CCIPHelper.selectorOfChain(chainId),
|
|
98
|
-
routerAddress: CCIPHelper.routerOfChain(chainId),
|
|
99
|
-
linkAddress: _linkTokenOf(chainId),
|
|
100
|
-
wrappedNativeAddress: CCIPHelper.wethOfChain(chainId),
|
|
101
|
-
ccipBnMAddress: address(0),
|
|
102
|
-
ccipLnMAddress: address(0),
|
|
103
|
-
rmnProxyAddress: address(0),
|
|
104
|
-
registryModuleOwnerCustomAddress: address(0),
|
|
105
|
-
tokenAdminRegistryAddress: address(0)
|
|
106
|
-
});
|
|
107
|
-
ccipLocalSimulatorFork.setNetworkDetails(chainId, details);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/// @dev Deploy mock ERC20 at the terminal token address if it has no code on the current fork.
|
|
111
|
-
function _ensureTerminalTokenExists() internal {
|
|
112
|
-
address token = _terminalToken();
|
|
113
|
-
if (token != JBConstants.NATIVE_TOKEN && token.code.length == 0) {
|
|
114
|
-
vm.etch(token, CCIPHelper.wethOfChain(block.chainid).code);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
73
|
// ── Setup
|
|
119
74
|
// ─────────────────────────────────────────────────────────────
|
|
120
75
|
|
|
121
76
|
function setUp() public override {
|
|
122
77
|
// ── L1
|
|
123
|
-
// ────────────────────────────────────────────────────────────
|
|
124
78
|
l1Fork = vm.createSelectFork(_l1RpcUrl(), _l1ForkBlock());
|
|
125
79
|
_ensureTerminalTokenExists();
|
|
126
80
|
|
|
127
81
|
ccipLocalSimulatorFork = new CCIPLocalSimulatorFork();
|
|
128
82
|
vm.makePersistent(address(ccipLocalSimulatorFork));
|
|
129
83
|
|
|
130
|
-
|
|
131
|
-
_registerMainnetDetails(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
_metadata = JBRulesetMetadata({
|
|
135
|
-
reservedPercent: JBConstants.MAX_RESERVED_PERCENT / 2,
|
|
136
|
-
cashOutTaxRate: 0,
|
|
137
|
-
baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
138
|
-
pausePay: false,
|
|
139
|
-
pauseCreditTransfers: false,
|
|
140
|
-
allowOwnerMinting: true,
|
|
141
|
-
allowSetCustomToken: false,
|
|
142
|
-
allowTerminalMigration: false,
|
|
143
|
-
allowSetTerminals: false,
|
|
144
|
-
allowSetController: false,
|
|
145
|
-
allowAddAccountingContext: true,
|
|
146
|
-
allowAddPriceFeed: true,
|
|
147
|
-
ownerMustSendPayouts: false,
|
|
148
|
-
holdFees: false,
|
|
149
|
-
useTotalSurplusForCashOuts: true,
|
|
150
|
-
useDataHookForPay: false,
|
|
151
|
-
useDataHookForCashOut: false,
|
|
152
|
-
dataHook: address(0),
|
|
153
|
-
metadata: 0
|
|
154
|
-
});
|
|
84
|
+
_registerMainnetDetails(ccipLocalSimulatorFork, _l1ChainId());
|
|
85
|
+
_registerMainnetDetails(ccipLocalSimulatorFork, _l2ChainId());
|
|
86
|
+
|
|
87
|
+
_initMetadata();
|
|
155
88
|
|
|
156
|
-
// Deploy full JB infrastructure on L1.
|
|
157
89
|
super.setUp();
|
|
158
90
|
vm.stopPrank();
|
|
159
91
|
|
|
@@ -196,11 +128,9 @@ abstract contract CCIPSuckerClaimForkTestBase is TestBaseWorkflow {
|
|
|
196
128
|
vm.stopPrank();
|
|
197
129
|
|
|
198
130
|
// ── L2
|
|
199
|
-
// ────────────────────────────────────────────────────────────
|
|
200
131
|
l2Fork = vm.createSelectFork(_l2RpcUrl(), _l2ForkBlock());
|
|
201
132
|
_ensureTerminalTokenExists();
|
|
202
133
|
|
|
203
|
-
// Deploy full JB infrastructure on L2.
|
|
204
134
|
super.setUp();
|
|
205
135
|
vm.stopPrank();
|
|
206
136
|
|
|
@@ -243,51 +173,6 @@ abstract contract CCIPSuckerClaimForkTestBase is TestBaseWorkflow {
|
|
|
243
173
|
vm.mockCall(address(0), abi.encodeCall(IJBSuckerRegistry.toRemoteFee, ()), abi.encode(uint256(0)));
|
|
244
174
|
}
|
|
245
175
|
|
|
246
|
-
/// @notice Launch a project that accepts `_terminalToken()`.
|
|
247
|
-
function _launchProject() internal {
|
|
248
|
-
address token = _terminalToken();
|
|
249
|
-
|
|
250
|
-
// Ensure baseCurrency matches the terminal token so no price feed is needed.
|
|
251
|
-
_metadata.baseCurrency = uint32(uint160(token));
|
|
252
|
-
|
|
253
|
-
JBCurrencyAmount[] memory _surplusAllowances = new JBCurrencyAmount[](1);
|
|
254
|
-
_surplusAllowances[0] = JBCurrencyAmount({amount: 5 * 10 ** 18, currency: uint32(uint160(token))});
|
|
255
|
-
|
|
256
|
-
JBFundAccessLimitGroup[] memory _fundAccessLimitGroup = new JBFundAccessLimitGroup[](1);
|
|
257
|
-
_fundAccessLimitGroup[0] = JBFundAccessLimitGroup({
|
|
258
|
-
terminal: address(jbMultiTerminal()),
|
|
259
|
-
token: token,
|
|
260
|
-
payoutLimits: new JBCurrencyAmount[](0),
|
|
261
|
-
surplusAllowances: _surplusAllowances
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
JBRulesetConfig[] memory _rulesetConfigurations = new JBRulesetConfig[](1);
|
|
265
|
-
_rulesetConfigurations[0].mustStartAtOrAfter = 0;
|
|
266
|
-
_rulesetConfigurations[0].duration = 0;
|
|
267
|
-
_rulesetConfigurations[0].weight = 1000 * 10 ** 18;
|
|
268
|
-
_rulesetConfigurations[0].weightCutPercent = 0;
|
|
269
|
-
_rulesetConfigurations[0].approvalHook = IJBRulesetApprovalHook(address(0));
|
|
270
|
-
_rulesetConfigurations[0].metadata = _metadata;
|
|
271
|
-
_rulesetConfigurations[0].splitGroups = new JBSplitGroup[](0);
|
|
272
|
-
_rulesetConfigurations[0].fundAccessLimitGroups = _fundAccessLimitGroup;
|
|
273
|
-
|
|
274
|
-
JBAccountingContext[] memory _tokensToAccept = new JBAccountingContext[](1);
|
|
275
|
-
_tokensToAccept[0] = JBAccountingContext({token: token, decimals: 18, currency: uint32(uint160(token))});
|
|
276
|
-
|
|
277
|
-
JBTerminalConfig[] memory _terminalConfigurations = new JBTerminalConfig[](1);
|
|
278
|
-
_terminalConfigurations[0] =
|
|
279
|
-
JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: _tokensToAccept});
|
|
280
|
-
|
|
281
|
-
jbController()
|
|
282
|
-
.launchProjectFor({
|
|
283
|
-
owner: multisig(),
|
|
284
|
-
projectUri: "claim-fork-test",
|
|
285
|
-
rulesetConfigurations: _rulesetConfigurations,
|
|
286
|
-
terminalConfigurations: _terminalConfigurations,
|
|
287
|
-
memo: ""
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
|
|
291
176
|
// ── Helpers
|
|
292
177
|
// ─────────────────────────────────────────────────────────────
|
|
293
178
|
|