@bananapus/suckers-v6 0.0.31 → 0.0.33

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.
Files changed (101) hide show
  1. package/README.md +2 -2
  2. package/foundry.lock +1 -7
  3. package/foundry.toml +1 -2
  4. package/package.json +24 -13
  5. package/references/operations.md +1 -1
  6. package/references/runtime.md +1 -1
  7. package/script/Deploy.s.sol +9 -0
  8. package/src/JBArbitrumSucker.sol +6 -2
  9. package/src/JBBaseSucker.sol +3 -1
  10. package/src/JBCCIPSucker.sol +19 -10
  11. package/src/JBCeloSucker.sol +6 -4
  12. package/src/JBOptimismSucker.sol +7 -3
  13. package/src/JBSucker.sol +215 -65
  14. package/src/JBSuckerRegistry.sol +105 -78
  15. package/src/JBSwapCCIPSucker.sol +43 -58
  16. package/src/deployers/JBArbitrumSuckerDeployer.sol +2 -1
  17. package/src/deployers/JBCCIPSuckerDeployer.sol +2 -1
  18. package/src/deployers/JBOptimismSuckerDeployer.sol +2 -1
  19. package/src/deployers/JBSuckerDeployer.sol +14 -8
  20. package/src/interfaces/IJBPeerChainAdjustedAccounts.sol +21 -0
  21. package/src/interfaces/IJBSuckerDeployer.sol +3 -2
  22. package/src/interfaces/IJBSuckerExtended.sol +56 -0
  23. package/src/libraries/ARBAddresses.sol +1 -1
  24. package/src/libraries/ARBChains.sol +1 -1
  25. package/src/libraries/JBSuckerLib.sol +148 -73
  26. package/src/libraries/JBSwapLib.sol +4 -0
  27. package/src/libraries/JBSwapPoolLib.sol +214 -56
  28. package/src/structs/JBMessageRoot.sol +3 -2
  29. package/src/structs/JBSuckerDeployerConfig.sol +3 -0
  30. package/ADMINISTRATION.md +0 -26
  31. package/ARCHITECTURE.md +0 -34
  32. package/AUDIT_INSTRUCTIONS.md +0 -33
  33. package/RISKS.md +0 -161
  34. package/SKILLS.md +0 -25
  35. package/STYLE_GUIDE.md +0 -610
  36. package/USER_JOURNEYS.md +0 -50
  37. package/slither-ci.config.json +0 -10
  38. package/test/AdversarialSuckerFork.t.sol +0 -449
  39. package/test/ForkArbitrum.t.sol +0 -404
  40. package/test/ForkCelo.t.sol +0 -387
  41. package/test/ForkClaimMainnet.t.sol +0 -924
  42. package/test/ForkMainnet.t.sol +0 -521
  43. package/test/ForkOPStack.t.sol +0 -358
  44. package/test/ForkSwap.t.sol +0 -445
  45. package/test/ForkSwapMainnet.t.sol +0 -441
  46. package/test/InteropCompat.t.sol +0 -705
  47. package/test/MultiSuckerFork.t.sol +0 -529
  48. package/test/SuckerAttacks.t.sol +0 -540
  49. package/test/SuckerCrossChainAdversarial.t.sol +0 -745
  50. package/test/SuckerDeepAttacks.t.sol +0 -1740
  51. package/test/SuckerRegressions.t.sol +0 -366
  52. package/test/TestAuditGaps.sol +0 -1073
  53. package/test/audit/ArbitrumL2ToRemoteFeeDoS.t.sol +0 -137
  54. package/test/audit/CCIPLegacyFormatCompatibility.t.sol +0 -85
  55. package/test/audit/CCIPWrappedNativeMisunwrap.t.sol +0 -196
  56. package/test/audit/DeprecatedRemovalUndercount.t.sol +0 -141
  57. package/test/audit/DeprecatedSuckerAggregateViews.t.sol +0 -247
  58. package/test/audit/DeprecatedSuckerDestination.t.sol +0 -177
  59. package/test/audit/DeprecationBoundaryAndMapToken.t.sol +0 -352
  60. package/test/audit/FeeLocking.t.sol +0 -328
  61. package/test/audit/FreshRound.t.sol +0 -383
  62. package/test/audit/MapTokensEnableOnlyValueStuck.t.sol +0 -84
  63. package/test/audit/PeerDeterminism.t.sol +0 -174
  64. package/test/audit/PeerSnapshotDesync.t.sol +0 -162
  65. package/test/audit/PeerTopologyAuthBreak.t.sol +0 -112
  66. package/test/audit/RegistryPeerAuthBreak.t.sol +0 -198
  67. package/test/audit/RegistryPeerMismatch.t.sol +0 -112
  68. package/test/audit/RegistryStaleDeprecatedMaxSurplus.t.sol +0 -127
  69. package/test/audit/RegistryStaleMaxAggregation.t.sol +0 -88
  70. package/test/audit/StaleNonceMetadataOverwrite.t.sol +0 -318
  71. package/test/audit/SwapBatchRateMixing.t.sol +0 -139
  72. package/test/audit/SwapQueueOrder.t.sol +0 -186
  73. package/test/audit/SwapZeroAmountBatchGap.t.sol +0 -203
  74. package/test/audit/SwapZeroLocalTotalUnbackedClaim.t.sol +0 -179
  75. package/test/audit/ToRemoteFeeFallback.t.sol +0 -140
  76. package/test/audit/ToRemoteFeeIrrecoverable.t.sol +0 -253
  77. package/test/audit/TransientClaimContext.t.sol +0 -258
  78. package/test/audit/TrustedForwarderSpoof.t.sol +0 -121
  79. package/test/audit/TrustedForwarderSpoofCCIP.t.sol +0 -121
  80. package/test/audit/ZeroOutputRetryClaim.t.sol +0 -162
  81. package/test/audit/ZeroOutputSwapPending.t.sol +0 -218
  82. package/test/fork/OptimismSuckerFork.t.sol +0 -459
  83. package/test/helpers/SuckerForkHelpers.sol +0 -126
  84. package/test/mocks/ERC20Mock.sol +0 -37
  85. package/test/mocks/MockMessenger.sol +0 -43
  86. package/test/regression/MapTokensDust.t.sol +0 -237
  87. package/test/unit/arb.t.sol +0 -28
  88. package/test/unit/ccip_native_interop.t.sol +0 -737
  89. package/test/unit/ccip_refund.t.sol +0 -245
  90. package/test/unit/deployer.t.sol +0 -739
  91. package/test/unit/emergency.t.sol +0 -323
  92. package/test/unit/fee_fallback.t.sol +0 -259
  93. package/test/unit/invariants.t.sol +0 -480
  94. package/test/unit/merkle.t.sol +0 -232
  95. package/test/unit/merkle_equivalence.t.sol +0 -121
  96. package/test/unit/multi_chain_evolution.t.sol +0 -605
  97. package/test/unit/peer_chain_state.t.sol +0 -618
  98. package/test/unit/pool_discovery.t.sol +0 -366
  99. package/test/unit/registry.t.sol +0 -25
  100. package/test/unit/relay_beneficiary.t.sol +0 -141
  101. package/test/unit/swap_ccip.t.sol +0 -577
package/README.md CHANGED
@@ -72,8 +72,8 @@ That means every bridge path has two trust surfaces:
72
72
  1. `test/unit/registry.t.sol`
73
73
  2. `test/unit/multi_chain_evolution.t.sol`
74
74
  3. `test/ForkClaimMainnet.t.sol`
75
- 4. `test/audit/codex-PeerSnapshotDesync.t.sol`
76
- 5. `test/audit/codex-ToRemoteFeeIrrecoverable.t.sol`
75
+ 4. `test/audit/PeerSnapshotDesync.t.sol`
76
+ 5. `test/audit/ToRemoteFeeIrrecoverable.t.sol`
77
77
 
78
78
  ## Install
79
79
 
package/foundry.lock CHANGED
@@ -1,11 +1,5 @@
1
1
  {
2
2
  "lib/forge-std": {
3
3
  "rev": "83c5d212a01f8950727da4095cdfe2654baccb5b"
4
- },
5
- "lib/sphinx": {
6
- "branch": {
7
- "name": "v0.23.0",
8
- "rev": "5fb24a825f46bd6ae0b5359fe0da1d2346126b09"
9
- }
10
4
  }
11
- }
5
+ }
package/foundry.toml CHANGED
@@ -14,8 +14,7 @@ depth = 100
14
14
  fail_on_revert = false
15
15
 
16
16
  [lint]
17
- exclude_lints = ["pascal-case-struct", "mixed-case-variable"]
18
- lint_on_build = false
17
+ exclude_lints = ["mixed-case-variable", "pascal-case-struct"]
19
18
 
20
19
  [fmt]
21
20
  number_underscore = "thousands"
package/package.json CHANGED
@@ -1,11 +1,22 @@
1
1
  {
2
2
  "name": "@bananapus/suckers-v6",
3
- "version": "0.0.31",
3
+ "version": "0.0.33",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/Bananapus/nana-suckers-v6"
8
8
  },
9
+ "files": [
10
+ "CHANGELOG.md",
11
+ "foundry.lock",
12
+ "foundry.toml",
13
+ "references/",
14
+ "remappings.txt",
15
+ "script/Deploy.s.sol",
16
+ "script/helpers/",
17
+ "sphinx.lock",
18
+ "src/"
19
+ ],
9
20
  "engines": {
10
21
  "node": ">=20.0.0"
11
22
  },
@@ -18,19 +29,19 @@
18
29
  "analyze": "slither . --config-file slither-ci.config.json"
19
30
  },
20
31
  "dependencies": {
21
- "@arbitrum/nitro-contracts": "^1.2.1",
22
- "@bananapus/core-v6": "^0.0.36",
23
- "@bananapus/permission-ids-v6": "^0.0.19",
24
- "@chainlink/contracts-ccip": "^1.6.0",
25
- "@chainlink/local": "github:smartcontractkit/chainlink-local#v0.2.7",
26
- "@openzeppelin/contracts": "^5.6.1",
27
- "@prb/math": "^4.1.0",
28
- "@uniswap/v3-core": "github:Uniswap/v3-core#0.8",
29
- "@uniswap/v3-periphery": "github:Uniswap/v3-periphery#0.8",
30
- "@uniswap/v4-core": "^1.0.2",
31
- "solady": "^0.1.26"
32
+ "@arbitrum/nitro-contracts": "3.2.0",
33
+ "@bananapus/core-v6": "0.0.39",
34
+ "@bananapus/permission-ids-v6": "0.0.22",
35
+ "@chainlink/contracts-ccip": "1.6.4",
36
+ "@chainlink/local": "0.2.7",
37
+ "@openzeppelin/contracts": "5.6.1",
38
+ "@prb/math": "4.1.1",
39
+ "@uniswap/v3-core": "github:Uniswap/v3-core#6562c52e8f75f0c10f9deaf44861847585fc8129",
40
+ "@uniswap/v3-periphery": "github:Uniswap/v3-periphery#b325bb0905d922ae61fcc7df85ee802e8df5e96c",
41
+ "@uniswap/v4-core": "1.0.2",
42
+ "solady": "0.1.26"
32
43
  },
33
44
  "devDependencies": {
34
- "@sphinx-labs/plugins": "^0.33.2"
45
+ "@sphinx-labs/plugins": "0.33.3"
35
46
  }
36
47
  }
@@ -25,4 +25,4 @@
25
25
 
26
26
  - [`test/SuckerAttacks.t.sol`](../test/SuckerAttacks.t.sol), [`test/SuckerDeepAttacks.t.sol`](../test/SuckerDeepAttacks.t.sol), and [`test/TestAuditGaps.sol`](../test/TestAuditGaps.sol) for security-sensitive assumptions.
27
27
  - [`test/InteropCompat.t.sol`](../test/InteropCompat.t.sol) when the problem is deployment wiring rather than runtime logic.
28
- - [`test/unit/invariants.t.sol`](../test/unit/invariants.t.sol), [`test/unit/peer_chain_state.t.sol`](../test/unit/peer_chain_state.t.sol), and [`test/audit/codex-PeerSnapshotDesync.t.sol`](../test/audit/codex-PeerSnapshotDesync.t.sol) when shared accounting or snapshot boundaries are in doubt.
28
+ - [`test/unit/invariants.t.sol`](../test/unit/invariants.t.sol), [`test/unit/peer_chain_state.t.sol`](../test/unit/peer_chain_state.t.sol), and [`test/audit/PeerSnapshotDesync.t.sol`](../test/audit/PeerSnapshotDesync.t.sol) when shared accounting or snapshot boundaries are in doubt.
@@ -27,4 +27,4 @@
27
27
  - [`test/ForkMainnet.t.sol`](../test/ForkMainnet.t.sol), [`test/ForkArbitrum.t.sol`](../test/ForkArbitrum.t.sol), [`test/ForkCelo.t.sol`](../test/ForkCelo.t.sol), and [`test/ForkOPStack.t.sol`](../test/ForkOPStack.t.sol) for real transport assumptions.
28
28
  - [`test/ForkSwap.t.sol`](../test/ForkSwap.t.sol), [`test/ForkClaimMainnet.t.sol`](../test/ForkClaimMainnet.t.sol), and [`test/SuckerRegressions.t.sol`](../test/SuckerRegressions.t.sol) for pinned cross-chain edge cases.
29
29
  - [`test/unit/invariants.t.sol`](../test/unit/invariants.t.sol), [`test/unit/peer_chain_state.t.sol`](../test/unit/peer_chain_state.t.sol), and [`test/unit/registry.t.sol`](../test/unit/registry.t.sol) for shared-accounting invariants.
30
- - [`test/SuckerAttacks.t.sol`](../test/SuckerAttacks.t.sol), [`test/SuckerDeepAttacks.t.sol`](../test/SuckerDeepAttacks.t.sol), [`test/audit/codex-PeerSnapshotDesync.t.sol`](../test/audit/codex-PeerSnapshotDesync.t.sol), and [`test/audit/codex-PeerDeterminism.t.sol`](../test/audit/codex-PeerDeterminism.t.sol) when the bug could involve base logic, registry behavior, or a specific bridge implementation.
30
+ - [`test/SuckerAttacks.t.sol`](../test/SuckerAttacks.t.sol), [`test/SuckerDeepAttacks.t.sol`](../test/SuckerDeepAttacks.t.sol), [`test/audit/PeerSnapshotDesync.t.sol`](../test/audit/PeerSnapshotDesync.t.sol), and [`test/audit/PeerDeterminism.t.sol`](../test/audit/PeerDeterminism.t.sol) when the bug could involve base logic, registry behavior, or a specific bridge implementation.
@@ -224,6 +224,7 @@ contract DeployScript is Script, Sphinx {
224
224
  deployer: _opDeployer,
225
225
  directory: core.directory,
226
226
  permissions: core.permissions,
227
+ prices: address(core.prices),
227
228
  tokens: core.tokens,
228
229
  feeProjectId: 1,
229
230
  registry: REGISTRY,
@@ -275,6 +276,7 @@ contract DeployScript is Script, Sphinx {
275
276
  deployer: _opDeployer,
276
277
  directory: core.directory,
277
278
  permissions: core.permissions,
279
+ prices: address(core.prices),
278
280
  tokens: core.tokens,
279
281
  feeProjectId: 1,
280
282
  registry: REGISTRY,
@@ -357,6 +359,7 @@ contract DeployScript is Script, Sphinx {
357
359
  deployer: _baseDeployer,
358
360
  directory: core.directory,
359
361
  permissions: core.permissions,
362
+ prices: address(core.prices),
360
363
  tokens: core.tokens,
361
364
  feeProjectId: 1,
362
365
  registry: REGISTRY,
@@ -408,6 +411,7 @@ contract DeployScript is Script, Sphinx {
408
411
  deployer: _baseDeployer,
409
412
  directory: core.directory,
410
413
  permissions: core.permissions,
414
+ prices: address(core.prices),
411
415
  tokens: core.tokens,
412
416
  feeProjectId: 1,
413
417
  registry: REGISTRY,
@@ -486,6 +490,7 @@ contract DeployScript is Script, Sphinx {
486
490
  deployer: _arbDeployer,
487
491
  directory: core.directory,
488
492
  permissions: core.permissions,
493
+ prices: address(core.prices),
489
494
  tokens: core.tokens,
490
495
  feeProjectId: 1,
491
496
  registry: REGISTRY,
@@ -540,6 +545,7 @@ contract DeployScript is Script, Sphinx {
540
545
  deployer: _arbDeployer,
541
546
  directory: core.directory,
542
547
  permissions: core.permissions,
548
+ prices: address(core.prices),
543
549
  tokens: core.tokens,
544
550
  feeProjectId: 1,
545
551
  registry: REGISTRY,
@@ -715,6 +721,7 @@ contract DeployScript is Script, Sphinx {
715
721
  salt: salt,
716
722
  directory: core.directory,
717
723
  permissions: core.permissions,
724
+ prices: address(core.prices),
718
725
  tokens: core.tokens,
719
726
  configurator: safeAddress(),
720
727
  trustedForwarder: TRUSTED_FORWARDER,
@@ -731,6 +738,7 @@ contract DeployScript is Script, Sphinx {
731
738
  bytes32 salt,
732
739
  IJBDirectory directory,
733
740
  IJBPermissions permissions,
741
+ address prices,
734
742
  IJBTokens tokens,
735
743
  address configurator,
736
744
  address trustedForwarder,
@@ -786,6 +794,7 @@ contract DeployScript is Script, Sphinx {
786
794
  directory: directory,
787
795
  tokens: tokens,
788
796
  permissions: permissions,
797
+ prices: prices,
789
798
  feeProjectId: 1,
790
799
  registry: REGISTRY,
791
800
  trustedForwarder: trustedForwarder
@@ -26,7 +26,9 @@ import {ARBChains} from "./libraries/ARBChains.sol";
26
26
  import {JBMessageRoot} from "./structs/JBMessageRoot.sol";
27
27
  import {JBRemoteToken} from "./structs/JBRemoteToken.sol";
28
28
 
29
- /// @notice A `JBSucker` implementation to suck tokens between two chains connected by an Arbitrum bridge.
29
+ /// @notice A `JBSucker` implementation that bridges Juicebox project tokens and terminal-token funds between Ethereum
30
+ /// L1 and Arbitrum L2 using the native Arbitrum Inbox/Outbox (for messages) and Gateway Router (for ERC-20 transfers).
31
+ /// Deploys on both layers — L1 instances create retryable tickets, L2 instances call `ArbSys` for L2-to-L1 messages.
30
32
  contract JBArbitrumSucker is JBSucker, IJBArbitrumSucker {
31
33
  //*********************************************************************//
32
34
  // --------------------------- custom errors ------------------------- //
@@ -53,17 +55,19 @@ contract JBArbitrumSucker is JBSucker, IJBArbitrumSucker {
53
55
 
54
56
  /// @param directory A contract storing directories of terminals and controllers for each project.
55
57
  /// @param permissions A contract storing permissions.
58
+ /// @param prices The price oracle used to convert peer-chain balances and surplus.
56
59
  /// @param tokens A contract that manages token minting and burning.
57
60
  constructor(
58
61
  JBArbitrumSuckerDeployer deployer,
59
62
  IJBDirectory directory,
60
63
  IJBPermissions permissions,
64
+ address prices,
61
65
  IJBTokens tokens,
62
66
  uint256 feeProjectId,
63
67
  IJBSuckerRegistry registry,
64
68
  address trustedForwarder
65
69
  )
66
- JBSucker(directory, permissions, tokens, feeProjectId, registry, trustedForwarder)
70
+ JBSucker(directory, permissions, prices, tokens, feeProjectId, registry, trustedForwarder)
67
71
  {
68
72
  GATEWAYROUTER = JBArbitrumSuckerDeployer(deployer).arbGatewayRouter();
69
73
  ARBINBOX = JBArbitrumSuckerDeployer(deployer).arbInbox();
@@ -18,17 +18,19 @@ contract JBBaseSucker is JBOptimismSucker {
18
18
  /// @param deployer A contract that deploys the clones for this contracts.
19
19
  /// @param directory A contract storing directories of terminals and controllers for each project.
20
20
  /// @param permissions A contract storing permissions.
21
+ /// @param prices The price oracle used to convert peer-chain balances and surplus.
21
22
  /// @param tokens A contract that manages token minting and burning.
22
23
  constructor(
23
24
  JBOptimismSuckerDeployer deployer,
24
25
  IJBDirectory directory,
25
26
  IJBPermissions permissions,
27
+ address prices,
26
28
  IJBTokens tokens,
27
29
  uint256 feeProjectId,
28
30
  IJBSuckerRegistry registry,
29
31
  address trustedForwarder
30
32
  )
31
- JBOptimismSucker(deployer, directory, permissions, tokens, feeProjectId, registry, trustedForwarder)
33
+ JBOptimismSucker(deployer, directory, permissions, prices, tokens, feeProjectId, registry, trustedForwarder)
32
34
  {}
33
35
 
34
36
  //*********************************************************************//
@@ -28,7 +28,9 @@ import {JBMessageRoot} from "./structs/JBMessageRoot.sol";
28
28
  import {JBRemoteToken} from "./structs/JBRemoteToken.sol";
29
29
  import {JBTokenMapping} from "./structs/JBTokenMapping.sol";
30
30
 
31
- /// @notice A `JBSucker` implementation to suck tokens between chains with Chainlink CCIP
31
+ /// @notice A `JBSucker` implementation that bridges Juicebox project tokens and terminal-token funds across any pair of
32
+ /// chains supported by Chainlink CCIP. Messages and token transfers are bundled into a single CCIP lane message, with
33
+ /// the CCIP Router handling cross-chain delivery and fee estimation.
32
34
  contract JBCCIPSucker is JBSucker, IAny2EVMMessageReceiver {
33
35
  //*********************************************************************//
34
36
  // --------------------------- custom errors ------------------------- //
@@ -42,10 +44,10 @@ contract JBCCIPSucker is JBSucker, IAny2EVMMessageReceiver {
42
44
  //*********************************************************************//
43
45
 
44
46
  /// @notice Emitted when a transport payment refund fails after a successful CCIP send.
45
- /// @dev The refunded ETH is permanently stuck in this contract there is no recovery function.
46
- /// This is an accepted tradeoff to avoid reverting after CCIP has committed the bridge message.
47
+ /// @dev The refunded ETH is retained as account-scoped credit so the CCIP send does not revert after
48
+ /// committing the bridge message.
47
49
  /// @param recipient The address that was supposed to receive the refund.
48
- /// @param amount The amount of the failed refund (permanently stuck in this contract).
50
+ /// @param amount The amount of the failed refund.
49
51
  event TransportPaymentRefundFailed(address indexed recipient, uint256 amount);
50
52
 
51
53
  //*********************************************************************//
@@ -74,21 +76,23 @@ contract JBCCIPSucker is JBSucker, IAny2EVMMessageReceiver {
74
76
 
75
77
  /// @param deployer A contract that deploys the clones for this contract.
76
78
  /// @param directory A contract storing directories of terminals and controllers for each project.
77
- /// @param tokens A contract that manages token minting and burning.
78
79
  /// @param permissions A contract storing permissions.
80
+ /// @param prices The price oracle used to convert peer-chain balances and surplus.
81
+ /// @param tokens A contract that manages token minting and burning.
79
82
  /// @param feeProjectId The ID of the project that receives fees.
80
83
  /// @param registry The sucker registry that tracks deployed suckers.
81
84
  /// @param trustedForwarder The trusted forwarder for ERC-2771 meta-transactions.
82
85
  constructor(
83
86
  JBCCIPSuckerDeployer deployer,
84
87
  IJBDirectory directory,
85
- IJBTokens tokens,
86
88
  IJBPermissions permissions,
89
+ address prices,
90
+ IJBTokens tokens,
87
91
  uint256 feeProjectId,
88
92
  IJBSuckerRegistry registry,
89
93
  address trustedForwarder
90
94
  )
91
- JBSucker(directory, permissions, tokens, feeProjectId, registry, trustedForwarder)
95
+ JBSucker(directory, permissions, prices, tokens, feeProjectId, registry, trustedForwarder)
92
96
  {
93
97
  // Read the remote chain ID from the deployer.
94
98
  REMOTE_CHAIN_ID = IJBCCIPSuckerDeployer(deployer).ccipRemoteChainId();
@@ -248,8 +252,13 @@ contract JBCCIPSucker is JBSucker, IAny2EVMMessageReceiver {
248
252
  refundRecipient: _msgSender()
249
253
  });
250
254
 
251
- // Emit an event if the excess transport payment refund failed.
252
- if (refundFailed) emit TransportPaymentRefundFailed(_msgSender(), refundAmount);
255
+ // Retain failed refunds as caller credit instead of leaving them project-addable or stranded.
256
+ if (refundFailed) {
257
+ // Refund accounting is isolated per caller; reentry cannot increase the retained credit.
258
+ // slither-disable-next-line reentrancy-benign
259
+ _retainTransportPaymentRefund({account: _msgSender(), amount: refundAmount});
260
+ emit TransportPaymentRefundFailed({recipient: _msgSender(), amount: refundAmount});
261
+ }
253
262
  }
254
263
 
255
264
  //*********************************************************************//
@@ -282,7 +291,7 @@ contract JBCCIPSucker is JBSucker, IAny2EVMMessageReceiver {
282
291
  // funds. CCIP wraps native tokens to WETH before bridging (see `_sendRootOverAMB`), so ALL tokens —
283
292
  // including native — need sufficient gas for an ERC-20 transfer on the remote chain.
284
293
  if (map.minGas < MESSENGER_ERC20_MIN_GAS_LIMIT) {
285
- revert JBSucker_BelowMinGas(map.minGas, MESSENGER_ERC20_MIN_GAS_LIMIT);
294
+ revert JBSucker_BelowMinGas({minGas: map.minGas, minGasLimit: MESSENGER_ERC20_MIN_GAS_LIMIT});
286
295
  }
287
296
  }
288
297
  }
@@ -37,17 +37,19 @@ contract JBCeloSucker is JBOptimismSucker {
37
37
  /// @param deployer A contract that deploys the clones for this contracts.
38
38
  /// @param directory A contract storing directories of terminals and controllers for each project.
39
39
  /// @param permissions A contract storing permissions.
40
+ /// @param prices The price oracle used to convert peer-chain balances and surplus.
40
41
  /// @param tokens A contract that manages token minting and burning.
41
42
  constructor(
42
43
  JBCeloSuckerDeployer deployer,
43
44
  IJBDirectory directory,
44
45
  IJBPermissions permissions,
46
+ address prices,
45
47
  IJBTokens tokens,
46
48
  uint256 feeProjectId,
47
49
  IJBSuckerRegistry registry,
48
50
  address trustedForwarder
49
51
  )
50
- JBOptimismSucker(deployer, directory, permissions, tokens, feeProjectId, registry, trustedForwarder)
52
+ JBOptimismSucker(deployer, directory, permissions, prices, tokens, feeProjectId, registry, trustedForwarder)
51
53
  {
52
54
  // Fetch the wrapped native token by doing a callback to the deployer contract.
53
55
  WRAPPED_NATIVE = JBCeloSuckerDeployer(deployer).wrappedNative();
@@ -82,7 +84,7 @@ contract JBCeloSucker is JBOptimismSucker {
82
84
  // Check addable amount against WETH balance before unwrapping.
83
85
  uint256 addableAmount = amountToAddToBalanceOf(token);
84
86
  if (amount > addableAmount) {
85
- revert JBSucker_InsufficientBalance(amount, addableAmount);
87
+ revert JBSucker_InsufficientBalance({amount: amount, balance: addableAmount});
86
88
  }
87
89
 
88
90
  // Unwrap WETH → native ETH.
@@ -95,7 +97,7 @@ contract JBCeloSucker is JBOptimismSucker {
95
97
  DIRECTORY.primaryTerminalOf({projectId: cachedProjectId, token: JBConstants.NATIVE_TOKEN});
96
98
 
97
99
  if (address(terminal) == address(0)) {
98
- revert JBSucker_NoTerminalForToken(cachedProjectId, JBConstants.NATIVE_TOKEN);
100
+ revert JBSucker_NoTerminalForToken({projectId: cachedProjectId, token: JBConstants.NATIVE_TOKEN});
99
101
  }
100
102
 
101
103
  // Add native ETH to the project's balance.
@@ -186,7 +188,7 @@ contract JBCeloSucker is JBOptimismSucker {
186
188
  // Enforce a reasonable minimum gas limit for bridging. Since we always bridge as ERC-20
187
189
  // (wrapping native ETH to WETH), all tokens need sufficient gas for an ERC-20 transfer.
188
190
  if (map.minGas < MESSENGER_ERC20_MIN_GAS_LIMIT) {
189
- revert JBSucker_BelowMinGas(map.minGas, MESSENGER_ERC20_MIN_GAS_LIMIT);
191
+ revert JBSucker_BelowMinGas({minGas: map.minGas, minGasLimit: MESSENGER_ERC20_MIN_GAS_LIMIT});
190
192
  }
191
193
  }
192
194
  }
@@ -17,7 +17,9 @@ import {IOPStandardBridge} from "./interfaces/IOPStandardBridge.sol";
17
17
  import {JBMessageRoot} from "./structs/JBMessageRoot.sol";
18
18
  import {JBRemoteToken} from "./structs/JBRemoteToken.sol";
19
19
 
20
- /// @notice A `JBSucker` implementation to suck tokens between two chains connected by an OP Bridge.
20
+ /// @notice A `JBSucker` implementation that bridges Juicebox project tokens and terminal-token funds between two
21
+ /// chains connected by an OP Stack bridge (Optimism, Base, etc.). Uses the `CrossDomainMessenger` for merkle-root
22
+ /// messages and the `StandardBridge` for ERC-20/native token transfers.
21
23
  contract JBOptimismSucker is JBSucker, IJBOptimismSucker {
22
24
  //*********************************************************************//
23
25
  // --------------- public immutable stored properties ---------------- //
@@ -35,18 +37,20 @@ contract JBOptimismSucker is JBSucker, IJBOptimismSucker {
35
37
 
36
38
  /// @param deployer A contract that deploys the clones for this contracts.
37
39
  /// @param directory A contract storing directories of terminals and controllers for each project.
38
- /// @param tokens A contract that manages token minting and burning.
39
40
  /// @param permissions A contract storing permissions.
41
+ /// @param prices The price oracle used to convert peer-chain balances and surplus.
42
+ /// @param tokens A contract that manages token minting and burning.
40
43
  constructor(
41
44
  JBOptimismSuckerDeployer deployer,
42
45
  IJBDirectory directory,
43
46
  IJBPermissions permissions,
47
+ address prices,
44
48
  IJBTokens tokens,
45
49
  uint256 feeProjectId,
46
50
  IJBSuckerRegistry registry,
47
51
  address trustedForwarder
48
52
  )
49
- JBSucker(directory, permissions, tokens, feeProjectId, registry, trustedForwarder)
53
+ JBSucker(directory, permissions, prices, tokens, feeProjectId, registry, trustedForwarder)
50
54
  {
51
55
  // Fetch the messenger and bridge by doing a callback to the deployer contract.
52
56
  OPBRIDGE = JBOptimismSuckerDeployer(deployer).opBridge();