@bananapus/suckers-v6 0.0.1

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 (149) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +422 -0
  3. package/SECURITY.md +55 -0
  4. package/SKILLS.md +163 -0
  5. package/deployments/nana-suckers-v5/arbitrum/JBArbitrumSucker.json +1425 -0
  6. package/deployments/nana-suckers-v5/arbitrum/JBArbitrumSuckerDeployer.json +391 -0
  7. package/deployments/nana-suckers-v5/arbitrum/JBCCIPSucker.json +1479 -0
  8. package/deployments/nana-suckers-v5/arbitrum/JBCCIPSuckerDeployer.json +433 -0
  9. package/deployments/nana-suckers-v5/arbitrum/JBCCIPSuckerDeployer_1.json +433 -0
  10. package/deployments/nana-suckers-v5/arbitrum/JBCCIPSuckerDeployer_2.json +433 -0
  11. package/deployments/nana-suckers-v5/arbitrum/JBCCIPSucker_1.json +1479 -0
  12. package/deployments/nana-suckers-v5/arbitrum/JBCCIPSucker_2.json +1479 -0
  13. package/deployments/nana-suckers-v5/arbitrum/JBSuckerRegistry.json +690 -0
  14. package/deployments/nana-suckers-v5/arbitrum_sepolia/JBArbitrumSucker.json +1425 -0
  15. package/deployments/nana-suckers-v5/arbitrum_sepolia/JBArbitrumSuckerDeployer.json +391 -0
  16. package/deployments/nana-suckers-v5/arbitrum_sepolia/JBCCIPSucker.json +1479 -0
  17. package/deployments/nana-suckers-v5/arbitrum_sepolia/JBCCIPSuckerDeployer.json +433 -0
  18. package/deployments/nana-suckers-v5/arbitrum_sepolia/JBCCIPSuckerDeployer_1.json +433 -0
  19. package/deployments/nana-suckers-v5/arbitrum_sepolia/JBCCIPSuckerDeployer_2.json +433 -0
  20. package/deployments/nana-suckers-v5/arbitrum_sepolia/JBCCIPSucker_1.json +1479 -0
  21. package/deployments/nana-suckers-v5/arbitrum_sepolia/JBCCIPSucker_2.json +1479 -0
  22. package/deployments/nana-suckers-v5/arbitrum_sepolia/JBSuckerRegistry.json +690 -0
  23. package/deployments/nana-suckers-v5/base/JBBaseSucker.json +1389 -0
  24. package/deployments/nana-suckers-v5/base/JBBaseSuckerDeployer.json +376 -0
  25. package/deployments/nana-suckers-v5/base/JBCCIPSucker.json +1483 -0
  26. package/deployments/nana-suckers-v5/base/JBCCIPSuckerDeployer.json +436 -0
  27. package/deployments/nana-suckers-v5/base/JBCCIPSuckerDeployer_1.json +436 -0
  28. package/deployments/nana-suckers-v5/base/JBCCIPSuckerDeployer_2.json +436 -0
  29. package/deployments/nana-suckers-v5/base/JBCCIPSucker_1.json +1483 -0
  30. package/deployments/nana-suckers-v5/base/JBCCIPSucker_2.json +1483 -0
  31. package/deployments/nana-suckers-v5/base/JBSuckerRegistry.json +694 -0
  32. package/deployments/nana-suckers-v5/base_sepolia/JBBaseSucker.json +1389 -0
  33. package/deployments/nana-suckers-v5/base_sepolia/JBBaseSuckerDeployer.json +376 -0
  34. package/deployments/nana-suckers-v5/base_sepolia/JBCCIPSucker.json +1483 -0
  35. package/deployments/nana-suckers-v5/base_sepolia/JBCCIPSuckerDeployer.json +436 -0
  36. package/deployments/nana-suckers-v5/base_sepolia/JBCCIPSuckerDeployer_1.json +436 -0
  37. package/deployments/nana-suckers-v5/base_sepolia/JBCCIPSuckerDeployer_2.json +436 -0
  38. package/deployments/nana-suckers-v5/base_sepolia/JBCCIPSucker_1.json +1483 -0
  39. package/deployments/nana-suckers-v5/base_sepolia/JBCCIPSucker_2.json +1483 -0
  40. package/deployments/nana-suckers-v5/base_sepolia/JBSuckerRegistry.json +694 -0
  41. package/deployments/nana-suckers-v5/ethereum/JBArbitrumSucker.json +1429 -0
  42. package/deployments/nana-suckers-v5/ethereum/JBArbitrumSuckerDeployer.json +394 -0
  43. package/deployments/nana-suckers-v5/ethereum/JBBaseSucker.json +1389 -0
  44. package/deployments/nana-suckers-v5/ethereum/JBBaseSuckerDeployer.json +376 -0
  45. package/deployments/nana-suckers-v5/ethereum/JBCCIPSucker.json +1483 -0
  46. package/deployments/nana-suckers-v5/ethereum/JBCCIPSuckerDeployer.json +436 -0
  47. package/deployments/nana-suckers-v5/ethereum/JBCCIPSuckerDeployer_1.json +436 -0
  48. package/deployments/nana-suckers-v5/ethereum/JBCCIPSuckerDeployer_2.json +436 -0
  49. package/deployments/nana-suckers-v5/ethereum/JBCCIPSucker_1.json +1483 -0
  50. package/deployments/nana-suckers-v5/ethereum/JBCCIPSucker_2.json +1483 -0
  51. package/deployments/nana-suckers-v5/ethereum/JBOptimismSucker.json +1389 -0
  52. package/deployments/nana-suckers-v5/ethereum/JBOptimismSuckerDeployer.json +376 -0
  53. package/deployments/nana-suckers-v5/ethereum/JBSuckerRegistry.json +694 -0
  54. package/deployments/nana-suckers-v5/optimism/JBCCIPSucker.json +1479 -0
  55. package/deployments/nana-suckers-v5/optimism/JBCCIPSuckerDeployer.json +433 -0
  56. package/deployments/nana-suckers-v5/optimism/JBCCIPSuckerDeployer_1.json +433 -0
  57. package/deployments/nana-suckers-v5/optimism/JBCCIPSuckerDeployer_2.json +433 -0
  58. package/deployments/nana-suckers-v5/optimism/JBCCIPSucker_1.json +1479 -0
  59. package/deployments/nana-suckers-v5/optimism/JBCCIPSucker_2.json +1479 -0
  60. package/deployments/nana-suckers-v5/optimism/JBOptimismSucker.json +1385 -0
  61. package/deployments/nana-suckers-v5/optimism/JBOptimismSuckerDeployer.json +373 -0
  62. package/deployments/nana-suckers-v5/optimism/JBSuckerRegistry.json +690 -0
  63. package/deployments/nana-suckers-v5/optimism_sepolia/JBCCIPSucker.json +1483 -0
  64. package/deployments/nana-suckers-v5/optimism_sepolia/JBCCIPSuckerDeployer.json +436 -0
  65. package/deployments/nana-suckers-v5/optimism_sepolia/JBCCIPSuckerDeployer_1.json +436 -0
  66. package/deployments/nana-suckers-v5/optimism_sepolia/JBCCIPSuckerDeployer_2.json +436 -0
  67. package/deployments/nana-suckers-v5/optimism_sepolia/JBCCIPSucker_1.json +1483 -0
  68. package/deployments/nana-suckers-v5/optimism_sepolia/JBCCIPSucker_2.json +1483 -0
  69. package/deployments/nana-suckers-v5/optimism_sepolia/JBOptimismSucker.json +1389 -0
  70. package/deployments/nana-suckers-v5/optimism_sepolia/JBOptimismSuckerDeployer.json +376 -0
  71. package/deployments/nana-suckers-v5/optimism_sepolia/JBSuckerRegistry.json +694 -0
  72. package/deployments/nana-suckers-v5/sepolia/JBArbitrumSucker.json +1429 -0
  73. package/deployments/nana-suckers-v5/sepolia/JBArbitrumSuckerDeployer.json +394 -0
  74. package/deployments/nana-suckers-v5/sepolia/JBBaseSucker.json +1389 -0
  75. package/deployments/nana-suckers-v5/sepolia/JBBaseSuckerDeployer.json +376 -0
  76. package/deployments/nana-suckers-v5/sepolia/JBCCIPSucker.json +1483 -0
  77. package/deployments/nana-suckers-v5/sepolia/JBCCIPSuckerDeployer.json +436 -0
  78. package/deployments/nana-suckers-v5/sepolia/JBCCIPSuckerDeployer_1.json +436 -0
  79. package/deployments/nana-suckers-v5/sepolia/JBCCIPSuckerDeployer_2.json +436 -0
  80. package/deployments/nana-suckers-v5/sepolia/JBCCIPSucker_1.json +1483 -0
  81. package/deployments/nana-suckers-v5/sepolia/JBCCIPSucker_2.json +1483 -0
  82. package/deployments/nana-suckers-v5/sepolia/JBOptimismSucker.json +1389 -0
  83. package/deployments/nana-suckers-v5/sepolia/JBOptimismSuckerDeployer.json +376 -0
  84. package/deployments/nana-suckers-v5/sepolia/JBSuckerRegistry.json +694 -0
  85. package/foundry.lock +11 -0
  86. package/foundry.toml +22 -0
  87. package/package.json +33 -0
  88. package/remappings.txt +1 -0
  89. package/script/Deploy.s.sol +506 -0
  90. package/script/helpers/SuckerDeploymentLib.sol +97 -0
  91. package/slither-ci.config.json +10 -0
  92. package/sphinx.lock +476 -0
  93. package/src/JBArbitrumSucker.sol +311 -0
  94. package/src/JBBaseSucker.sol +41 -0
  95. package/src/JBCCIPSucker.sol +303 -0
  96. package/src/JBOptimismSucker.sol +143 -0
  97. package/src/JBSucker.sol +1159 -0
  98. package/src/JBSuckerRegistry.sol +262 -0
  99. package/src/deployers/JBArbitrumSuckerDeployer.sol +86 -0
  100. package/src/deployers/JBBaseSuckerDeployer.sol +26 -0
  101. package/src/deployers/JBCCIPSuckerDeployer.sol +88 -0
  102. package/src/deployers/JBOptimismSuckerDeployer.sol +82 -0
  103. package/src/deployers/JBSuckerDeployer.sol +147 -0
  104. package/src/enums/JBAddToBalanceMode.sol +11 -0
  105. package/src/enums/JBLayer.sol +8 -0
  106. package/src/enums/JBSuckerState.sol +14 -0
  107. package/src/interfaces/IArbGatewayRouter.sol +11 -0
  108. package/src/interfaces/IArbL1GatewayRouter.sol +17 -0
  109. package/src/interfaces/IArbL2GatewayRouter.sol +14 -0
  110. package/src/interfaces/ICCIPRouter.sol +11 -0
  111. package/src/interfaces/IJBArbitrumSucker.sol +13 -0
  112. package/src/interfaces/IJBArbitrumSuckerDeployer.sol +12 -0
  113. package/src/interfaces/IJBCCIPSuckerDeployer.sol +15 -0
  114. package/src/interfaces/IJBOpSuckerDeployer.sol +11 -0
  115. package/src/interfaces/IJBOptimismSucker.sol +10 -0
  116. package/src/interfaces/IJBSucker.sol +144 -0
  117. package/src/interfaces/IJBSuckerDeployer.sol +40 -0
  118. package/src/interfaces/IJBSuckerExtended.sol +22 -0
  119. package/src/interfaces/IJBSuckerRegistry.sol +75 -0
  120. package/src/interfaces/IOPMessenger.sol +18 -0
  121. package/src/interfaces/IOPStandardBridge.sol +29 -0
  122. package/src/interfaces/IWrappedNativeToken.sol +13 -0
  123. package/src/libraries/ARBAddresses.sol +17 -0
  124. package/src/libraries/ARBChains.sol +11 -0
  125. package/src/libraries/CCIPHelper.sol +136 -0
  126. package/src/structs/JBClaim.sol +13 -0
  127. package/src/structs/JBInboxTreeRoot.sol +12 -0
  128. package/src/structs/JBLeaf.sol +14 -0
  129. package/src/structs/JBMessageRoot.sol +16 -0
  130. package/src/structs/JBOutboxTree.sol +18 -0
  131. package/src/structs/JBRemoteToken.sol +17 -0
  132. package/src/structs/JBSuckerDeployerConfig.sol +12 -0
  133. package/src/structs/JBSuckersPair.sol +11 -0
  134. package/src/structs/JBTokenMapping.sol +13 -0
  135. package/src/utils/MerkleLib.sol +1020 -0
  136. package/test/Fork.t.sol +514 -0
  137. package/test/InteropCompat.t.sol +676 -0
  138. package/test/SuckerAttacks.t.sol +509 -0
  139. package/test/SuckerDeepAttacks.t.sol +1563 -0
  140. package/test/mocks/ERC20Mock.sol +36 -0
  141. package/test/mocks/MockMessenger.sol +42 -0
  142. package/test/unit/arb.t.sol +28 -0
  143. package/test/unit/ccip_native_interop.t.sol +719 -0
  144. package/test/unit/ccip_refund.t.sol +234 -0
  145. package/test/unit/deployer.t.sol +475 -0
  146. package/test/unit/emergency.t.sol +305 -0
  147. package/test/unit/merkle.t.sol +212 -0
  148. package/test/unit/multi_chain_evolution.t.sol +622 -0
  149. package/test/unit/registry.t.sol +26 -0
@@ -0,0 +1,311 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.23;
3
+
4
+ import {IBridge} from "@arbitrum/nitro-contracts/src/bridge/IBridge.sol";
5
+ import {IInbox} from "@arbitrum/nitro-contracts/src/bridge/IInbox.sol";
6
+ import {IOutbox} from "@arbitrum/nitro-contracts/src/bridge/IOutbox.sol";
7
+ import {AddressAliasHelper} from "@arbitrum/nitro-contracts/src/libraries/AddressAliasHelper.sol";
8
+ import {ArbSys} from "@arbitrum/nitro-contracts/src/precompiles/ArbSys.sol";
9
+ import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
10
+ import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
11
+ import {IJBTokens} from "@bananapus/core-v6/src/interfaces/IJBTokens.sol";
12
+ import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
13
+ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
14
+ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
15
+ import {BitMaps} from "@openzeppelin/contracts/utils/structs/BitMaps.sol";
16
+
17
+ import {JBSucker} from "./JBSucker.sol";
18
+ import {JBArbitrumSuckerDeployer} from "./deployers/JBArbitrumSuckerDeployer.sol";
19
+ import {JBAddToBalanceMode} from "./enums/JBAddToBalanceMode.sol";
20
+ import {JBLayer} from "./enums/JBLayer.sol";
21
+ import {IArbGatewayRouter} from "./interfaces/IArbGatewayRouter.sol";
22
+ import {IArbL1GatewayRouter} from "./interfaces/IArbL1GatewayRouter.sol";
23
+ import {IArbL2GatewayRouter} from "./interfaces/IArbL2GatewayRouter.sol";
24
+ import {IJBArbitrumSucker} from "./interfaces/IJBArbitrumSucker.sol";
25
+ import {IJBSuckerDeployer} from "./interfaces/IJBSuckerDeployer.sol";
26
+ import {ARBAddresses} from "./libraries/ARBAddresses.sol";
27
+ import {ARBChains} from "./libraries/ARBChains.sol";
28
+ import {JBInboxTreeRoot} from "./structs/JBInboxTreeRoot.sol";
29
+ import {JBMessageRoot} from "./structs/JBMessageRoot.sol";
30
+ import {JBOutboxTree} from "./structs/JBOutboxTree.sol";
31
+ import {JBRemoteToken} from "./structs/JBRemoteToken.sol";
32
+ import {MerkleLib} from "./utils/MerkleLib.sol";
33
+
34
+ /// @notice A `JBSucker` implementation to suck tokens between two chains connected by an Arbitrum bridge.
35
+ contract JBArbitrumSucker is JBSucker, IJBArbitrumSucker {
36
+ using BitMaps for BitMaps.BitMap;
37
+ using MerkleLib for MerkleLib.Tree;
38
+
39
+ //*********************************************************************//
40
+ // --------------------------- custom errors ------------------------- //
41
+ //*********************************************************************//
42
+
43
+ error JBArbitrumSucker_NotEnoughGas(uint256 payment, uint256 cost);
44
+
45
+ //*********************************************************************//
46
+ // --------------- public immutable stored properties ---------------- //
47
+ //*********************************************************************//
48
+
49
+ /// @notice The inbox used to send messages between the local and remote sucker.
50
+ IInbox public immutable override ARBINBOX;
51
+
52
+ /// @notice The gateway router for the specific chain
53
+ IArbGatewayRouter public immutable override GATEWAYROUTER;
54
+
55
+ /// @notice The layer that this contract is on.
56
+ JBLayer public immutable override LAYER;
57
+
58
+ //*********************************************************************//
59
+ // ---------------------------- constructor -------------------------- //
60
+ //*********************************************************************//
61
+
62
+ /// @param directory A contract storing directories of terminals and controllers for each project.
63
+ /// @param permissions A contract storing permissions.
64
+ /// @param tokens A contract that manages token minting and burning.
65
+ /// @param addToBalanceMode The mode of adding tokens to balance.
66
+ constructor(
67
+ JBArbitrumSuckerDeployer deployer,
68
+ IJBDirectory directory,
69
+ IJBPermissions permissions,
70
+ IJBTokens tokens,
71
+ JBAddToBalanceMode addToBalanceMode,
72
+ address trustedForwarder
73
+ )
74
+ JBSucker(directory, permissions, tokens, addToBalanceMode, trustedForwarder)
75
+ {
76
+ GATEWAYROUTER = JBArbitrumSuckerDeployer(deployer).arbGatewayRouter();
77
+ ARBINBOX = JBArbitrumSuckerDeployer(deployer).arbInbox();
78
+ LAYER = JBArbitrumSuckerDeployer(deployer).arbLayer();
79
+ }
80
+
81
+ //*********************************************************************//
82
+ // ------------------------ external views --------------------------- //
83
+ //*********************************************************************//
84
+
85
+ /// @notice Returns the chain on which the peer is located.
86
+ /// @return chainId of the peer.
87
+ function peerChainId() external view virtual override returns (uint256) {
88
+ uint256 chainId = block.chainid;
89
+ if (chainId == ARBChains.ETH_CHAINID) return ARBChains.ARB_CHAINID;
90
+ if (chainId == ARBChains.ARB_CHAINID) return ARBChains.ETH_CHAINID;
91
+ if (chainId == ARBChains.ETH_SEP_CHAINID) return ARBChains.ARB_SEP_CHAINID;
92
+ if (chainId == ARBChains.ARB_SEP_CHAINID) return ARBChains.ETH_SEP_CHAINID;
93
+ return 0;
94
+ }
95
+
96
+ //*********************************************************************//
97
+ // ------------------------ internal views --------------------------- //
98
+ //*********************************************************************//
99
+
100
+ /// @notice Checks if the `sender` (`_msgSender()`) is a valid representative of the remote peer.
101
+ /// @param sender The message's sender.
102
+ /// @return valid A flag if the sender is a valid representative of the remote peer.
103
+ function _isRemotePeer(address sender) internal view override returns (bool) {
104
+ // Convert the bytes32 peer to an address for comparison with EVM bridge contracts.
105
+ address peerAddress = _toAddress(peer());
106
+
107
+ // If we are the L1 peer,
108
+ if (LAYER == JBLayer.L1) {
109
+ IBridge bridge = ARBINBOX.bridge();
110
+ // Check that the sender is the bridge and that the outbox has our peer as the sender.
111
+ return sender == address(bridge) && peerAddress == IOutbox(bridge.activeOutbox()).l2ToL1Sender();
112
+ }
113
+
114
+ // If we are the L2 peer, check using the `AddressAliasHelper`.
115
+ return sender == AddressAliasHelper.applyL1ToL2Alias(peerAddress);
116
+ }
117
+
118
+ //*********************************************************************//
119
+ // --------------------- internal transactions ----------------------- //
120
+ //*********************************************************************//
121
+
122
+ /// @notice Uses the L1/L2 gateway to send the root and assets over the bridge to the peer.
123
+ /// @param transportPayment the amount of `msg.value` that is going to get paid for sending this message.
124
+ /// @param token The token to bridge the outbox tree for.
125
+ /// @param remoteToken Information about the remote token being bridged to.
126
+ function _sendRootOverAMB(
127
+ uint256 transportPayment,
128
+ uint256,
129
+ address token,
130
+ uint256 amount,
131
+ JBRemoteToken memory remoteToken,
132
+ JBMessageRoot memory message
133
+ )
134
+ internal
135
+ override
136
+ {
137
+ // Bridge expects to be paid
138
+ if (transportPayment == 0 && LAYER == JBLayer.L1) revert JBSucker_ExpectedMsgValue();
139
+
140
+ // Build the calldata that will be send to the peer. This will call `JBSucker.fromRemote` on the remote peer.
141
+ bytes memory data = abi.encodeCall(JBSucker.fromRemote, (message));
142
+
143
+ // Depending on which layer we are on, send the call to the other layer.
144
+ // slither-disable-start out-of-order-retryable
145
+ if (LAYER == JBLayer.L1) {
146
+ _toL2({
147
+ token: token, transportPayment: transportPayment, amount: amount, data: data, remoteToken: remoteToken
148
+ });
149
+ } else {
150
+ _toL1({token: token, amount: amount, data: data, remoteToken: remoteToken});
151
+ }
152
+ // slither-disable-end out-of-order-retryable
153
+ }
154
+
155
+ /// @notice Bridge the `token` and data to the remote L1 chain.
156
+ /// @param token The token to bridge.
157
+ /// @param amount The amount of tokens to bridge.
158
+ /// @param data The calldata to send to the remote chain. This calls `JBSucker.fromRemote` on the remote peer.
159
+ /// @param remoteToken Information about the remote token to bridged to.
160
+ function _toL1(address token, uint256 amount, bytes memory data, JBRemoteToken memory remoteToken) internal {
161
+ uint256 nativeValue;
162
+
163
+ // Revert if there's a `msg.value`. Sending a message to L1 does not require any payment.
164
+ if (msg.value != 0) {
165
+ // slither-disable-next-line msg-value-loop
166
+ revert JBSucker_UnexpectedMsgValue(msg.value);
167
+ }
168
+
169
+ // If the token is an ERC-20, bridge it to the peer.
170
+ // If the amount is `0` then we do not need to bridge any ERC20.
171
+ if (token != JBConstants.NATIVE_TOKEN && amount != 0) {
172
+ // slither-disable-next-line calls-loop
173
+ SafeERC20.forceApprove({token: IERC20(token), spender: GATEWAYROUTER.getGateway(token), value: amount});
174
+
175
+ // Convert bytes32 types to address at the Arbitrum bridge API boundary.
176
+ // slither-disable-next-line calls-loop,unused-return
177
+ IArbL2GatewayRouter(address(GATEWAYROUTER))
178
+ .outboundTransfer({
179
+ l1Token: _toAddress(remoteToken.addr), to: _toAddress(peer()), amount: amount, data: bytes("")
180
+ });
181
+ } else {
182
+ // Otherwise, the token is the native token, and the amount will be sent as `msg.value`.
183
+ nativeValue = amount;
184
+ }
185
+
186
+ // Send the message to the peer with the reclaimed ETH.
187
+ // Address `100` is the ArbSys precompile address.
188
+ // Convert bytes32 peer to address at the Arbitrum API boundary.
189
+ // slither-disable-next-line calls-loop,unused-return
190
+ ArbSys(address(100)).sendTxToL1{value: nativeValue}({destination: _toAddress(peer()), data: data});
191
+ }
192
+
193
+ /// @notice Bridge the `token` and data to the remote L2 chain.
194
+ /// @param token The token to bridge.
195
+ /// @param amount The amount of tokens to bridge.
196
+ /// @param data The calldata to send to the remote chain. This calls `JBSucker.fromRemote` on the remote peer.
197
+ function _toL2(
198
+ address token,
199
+ uint256 transportPayment,
200
+ uint256 amount,
201
+ bytes memory data,
202
+ JBRemoteToken memory remoteToken
203
+ )
204
+ internal
205
+ {
206
+ uint256 nativeValue;
207
+ uint256 maxFeePerGas = block.basefee;
208
+ uint256 callTransportCost;
209
+ uint256 maxSubmissionCost;
210
+
211
+ {
212
+ // slither-disable-next-line calls-loop
213
+ maxSubmissionCost =
214
+ ARBINBOX.calculateRetryableSubmissionFee({dataLength: data.length, baseFee: maxFeePerGas});
215
+
216
+ // Tracks the cost for the call to the remote peer.
217
+ callTransportCost = maxSubmissionCost + (MESSENGER_BASE_GAS_LIMIT * maxFeePerGas);
218
+ }
219
+
220
+ // If the token is an ERC-20, bridge it to the peer.
221
+ // If the amount is `0` then we do not need to bridge any ERC20.
222
+ if (token != JBConstants.NATIVE_TOKEN && amount != 0) {
223
+ // Calculate the cost of the ERC-20 transfer. (96 is the length of the abi encoded `data`)
224
+ // slither-disable-next-line calls-loop
225
+ uint256 maxSubmissionCostERC20 =
226
+ ARBINBOX.calculateRetryableSubmissionFee({dataLength: 96, baseFee: maxFeePerGas});
227
+
228
+ uint256 tokenTransportCost = maxSubmissionCostERC20 + (remoteToken.minGas * maxFeePerGas);
229
+
230
+ // Ensure we bridge enough for gas costs on L2 side
231
+ if (transportPayment < callTransportCost + tokenTransportCost) {
232
+ revert JBArbitrumSucker_NotEnoughGas(transportPayment, callTransportCost + tokenTransportCost);
233
+ }
234
+
235
+ {
236
+ // The amount of left over transportPayment will be split over the two calls.
237
+ uint256 transportPaymentRemainder = (transportPayment - callTransportCost - tokenTransportCost) / 2;
238
+ tokenTransportCost += transportPaymentRemainder;
239
+ callTransportCost += transportPaymentRemainder;
240
+ }
241
+
242
+ // Approve the tokens to be bridged.
243
+ // slither-disable-next-line calls-loop
244
+ SafeERC20.forceApprove({token: IERC20(token), spender: GATEWAYROUTER.getGateway(token), value: amount});
245
+
246
+ // Perform the ERC-20 bridge transfer. Convert bytes32 peer to address at the Arbitrum bridge API boundary.
247
+ // slither-disable-start out-of-order-retryable
248
+ // slither-disable-next-line calls-loop,unused-return
249
+ IArbL1GatewayRouter(address(GATEWAYROUTER)).outboundTransferCustomRefund{value: tokenTransportCost}({
250
+ token: token,
251
+ refundTo: _msgSender(),
252
+ to: _toAddress(peer()),
253
+ amount: amount,
254
+ maxGas: remoteToken.minGas,
255
+ gasPriceBid: maxFeePerGas,
256
+ data: bytes(abi.encode(maxSubmissionCostERC20, bytes("")))
257
+ });
258
+ } else {
259
+ // Ensure we bridge enough for gas costs on L2 side
260
+ if (transportPayment < callTransportCost) {
261
+ revert JBArbitrumSucker_NotEnoughGas(transportPayment, callTransportCost);
262
+ }
263
+
264
+ // If the token is the native token then we only need to do a single call.
265
+ // So it should use all of the transportPayment.
266
+ callTransportCost = transportPayment;
267
+
268
+ // Otherwise, the token is the native token, and the amount will be sent as `msg.value`.
269
+ nativeValue = amount;
270
+ }
271
+
272
+ // Create the retryable ticket containing the merkleRoot.
273
+ // We call unsafe as we do not want the refund address to be aliased to L2.
274
+ // The above check is the same check that makes it `safeCreateRetryableTicket`.
275
+
276
+ // Convert bytes32 peer to address at the Arbitrum inbox API boundary.
277
+ // slither-disable-next-line calls-loop,unused-return
278
+ _createRetryableTicket({
279
+ callTransportCost: callTransportCost,
280
+ nativeValue: nativeValue,
281
+ maxSubmissionCost: maxSubmissionCost,
282
+ maxFeePerGas: maxFeePerGas,
283
+ data: data
284
+ });
285
+ // slither-disable-end out-of-order-retryable
286
+ }
287
+
288
+ /// @notice Helper to create the retryable ticket, avoiding stack-too-deep.
289
+ function _createRetryableTicket(
290
+ uint256 callTransportCost,
291
+ uint256 nativeValue,
292
+ uint256 maxSubmissionCost,
293
+ uint256 maxFeePerGas,
294
+ bytes memory data
295
+ )
296
+ internal
297
+ {
298
+ address peerAddress = _toAddress(peer());
299
+ // slither-disable-next-line unused-return,calls-loop
300
+ ARBINBOX.unsafeCreateRetryableTicket{value: callTransportCost + nativeValue}({
301
+ to: peerAddress,
302
+ l2CallValue: nativeValue,
303
+ maxSubmissionCost: maxSubmissionCost,
304
+ excessFeeRefundAddress: _msgSender(),
305
+ callValueRefundAddress: peerAddress,
306
+ gasLimit: MESSENGER_BASE_GAS_LIMIT,
307
+ maxFeePerGas: maxFeePerGas,
308
+ data: data
309
+ });
310
+ }
311
+ }
@@ -0,0 +1,41 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.23;
3
+
4
+ import "./JBOptimismSucker.sol";
5
+
6
+ contract JBBaseSucker is JBOptimismSucker {
7
+ //*********************************************************************//
8
+ // ---------------------------- constructor -------------------------- //
9
+ //*********************************************************************//
10
+
11
+ /// @param deployer A contract that deploys the clones for this contracts.
12
+ /// @param directory A contract storing directories of terminals and controllers for each project.
13
+ /// @param permissions A contract storing permissions.
14
+ /// @param tokens A contract that manages token minting and burning.
15
+ /// @param addToBalanceMode The mode of adding tokens to balance.
16
+ constructor(
17
+ JBOptimismSuckerDeployer deployer,
18
+ IJBDirectory directory,
19
+ IJBPermissions permissions,
20
+ IJBTokens tokens,
21
+ JBAddToBalanceMode addToBalanceMode,
22
+ address trustedForwarder
23
+ )
24
+ JBOptimismSucker(deployer, directory, permissions, tokens, addToBalanceMode, trustedForwarder)
25
+ {}
26
+
27
+ //*********************************************************************//
28
+ // ------------------------ external views --------------------------- //
29
+ //*********************************************************************//
30
+
31
+ /// @notice Returns the chain on which the peer is located.
32
+ /// @return chainId of the peer.
33
+ function peerChainId() external view virtual override returns (uint256) {
34
+ uint256 chainId = block.chainid;
35
+ if (chainId == 1) return 8453;
36
+ if (chainId == 8453) return 1;
37
+ if (chainId == 11_155_111) return 84_532;
38
+ if (chainId == 84_532) return 11_155_111;
39
+ return 0;
40
+ }
41
+ }
@@ -0,0 +1,303 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.23;
3
+
4
+ import {IAny2EVMMessageReceiver} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IAny2EVMMessageReceiver.sol";
5
+ import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
6
+ import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
7
+ import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
8
+ import {IJBPrices} from "@bananapus/core-v6/src/interfaces/IJBPrices.sol";
9
+ import {IJBRulesets} from "@bananapus/core-v6/src/interfaces/IJBRulesets.sol";
10
+ import {IJBTokens} from "@bananapus/core-v6/src/interfaces/IJBTokens.sol";
11
+ import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
12
+ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
13
+ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
14
+ import {BitMaps} from "@openzeppelin/contracts/utils/structs/BitMaps.sol";
15
+
16
+ import {JBSucker} from "./JBSucker.sol";
17
+ import {JBCCIPSuckerDeployer} from "./deployers/JBCCIPSuckerDeployer.sol";
18
+ import {JBAddToBalanceMode} from "./enums/JBAddToBalanceMode.sol";
19
+ import {ICCIPRouter, IWrappedNativeToken} from "./interfaces/ICCIPRouter.sol";
20
+ import {IJBCCIPSuckerDeployer} from "./interfaces/IJBCCIPSuckerDeployer.sol";
21
+ import {CCIPHelper} from "./libraries/CCIPHelper.sol";
22
+ import {JBInboxTreeRoot} from "./structs/JBInboxTreeRoot.sol";
23
+ import {JBMessageRoot} from "./structs/JBMessageRoot.sol";
24
+ import {JBRemoteToken} from "./structs/JBRemoteToken.sol";
25
+ import {JBTokenMapping} from "./structs/JBTokenMapping.sol";
26
+ import {MerkleLib} from "./utils/MerkleLib.sol";
27
+
28
+ /// @notice A `JBSucker` implementation to suck tokens between chains with Chainlink CCIP
29
+ contract JBCCIPSucker is JBSucker, IAny2EVMMessageReceiver {
30
+ using MerkleLib for MerkleLib.Tree;
31
+ using BitMaps for BitMaps.BitMap;
32
+
33
+ //*********************************************************************//
34
+ // --------------------------- custom errors ------------------------- //
35
+ //*********************************************************************//
36
+
37
+ error JBCCIPSucker_InvalidRouter(address router);
38
+
39
+ //*********************************************************************//
40
+ // ------------------------------ events ----------------------------- //
41
+ //*********************************************************************//
42
+
43
+ /// @notice Emitted when a transport payment refund fails after a successful CCIP send.
44
+ /// @dev The refunded ETH is permanently stuck in this contract — there is no recovery function.
45
+ /// This is an accepted tradeoff to avoid reverting after CCIP has committed the bridge message.
46
+ /// @param recipient The address that was supposed to receive the refund.
47
+ /// @param amount The amount of the failed refund (permanently stuck in this contract).
48
+ event TransportPaymentRefundFailed(address indexed recipient, uint256 amount);
49
+
50
+ //*********************************************************************//
51
+ // --------------- public immutable stored properties ---------------- //
52
+ //*********************************************************************//
53
+
54
+ /// @notice The CCIP router used to bridge tokens between the local and remote chain.
55
+ ICCIPRouter public immutable CCIP_ROUTER;
56
+
57
+ /// @notice The chain id of the remote chain.
58
+ uint256 public immutable REMOTE_CHAIN_ID;
59
+
60
+ /// @notice The CCIP chain selector of the remote chain.
61
+ uint64 public immutable REMOTE_CHAIN_SELECTOR;
62
+
63
+ //*********************************************************************//
64
+ // ---------------------------- constructor -------------------------- //
65
+ //*********************************************************************//
66
+
67
+ /// @param deployer A contract that deploys the clones for this contracts.
68
+ /// @param directory A contract storing directories of terminals and controllers for each project.
69
+ /// @param tokens A contract that manages token minting and burning.
70
+ /// @param permissions A contract storing permissions.
71
+ /// @param addToBalanceMode The mode of adding tokens to balance.
72
+ constructor(
73
+ JBCCIPSuckerDeployer deployer,
74
+ IJBDirectory directory,
75
+ IJBTokens tokens,
76
+ IJBPermissions permissions,
77
+ JBAddToBalanceMode addToBalanceMode,
78
+ address trustedForwarder
79
+ )
80
+ JBSucker(directory, permissions, tokens, addToBalanceMode, trustedForwarder)
81
+ {
82
+ REMOTE_CHAIN_ID = IJBCCIPSuckerDeployer(deployer).ccipRemoteChainId();
83
+ REMOTE_CHAIN_SELECTOR = IJBCCIPSuckerDeployer(deployer).ccipRemoteChainSelector();
84
+ CCIP_ROUTER = IJBCCIPSuckerDeployer(deployer).ccipRouter();
85
+
86
+ if (address(CCIP_ROUTER) == address(0)) revert JBCCIPSucker_InvalidRouter(address(CCIP_ROUTER));
87
+ }
88
+
89
+ //*********************************************************************//
90
+ // ------------------------ external views --------------------------- //
91
+ //*********************************************************************//
92
+
93
+ /// @notice Returns the chain on which the peer is located.
94
+ /// @return chainId of the peer.
95
+ function peerChainId() external view virtual override returns (uint256 chainId) {
96
+ // Return the remote chain id
97
+ return REMOTE_CHAIN_ID;
98
+ }
99
+
100
+ //*********************************************************************//
101
+ // ------------------------- public views ---------------------------- //
102
+ //*********************************************************************//
103
+
104
+ /// @notice Return the current router
105
+ /// @return CCIP router address
106
+ function getRouter() public view returns (address) {
107
+ return address(CCIP_ROUTER);
108
+ }
109
+
110
+ /// @notice IERC165 supports an interfaceId
111
+ /// @param interfaceId The interfaceId to check
112
+ /// @return true if the interfaceId is supported
113
+ /// @dev Should indicate whether the contract implements IAny2EVMMessageReceiver
114
+ /// e.g. return interfaceId == type(IAny2EVMMessageReceiver).interfaceId || interfaceId == type(IERC165).interfaceId
115
+ /// This allows CCIP to check if ccipReceive is available before calling it.
116
+ /// If this returns false or reverts, only tokens are transferred to the receiver.
117
+ /// If this returns true, tokens are transferred and ccipReceive is called atomically.
118
+ /// Additionally, if the receiver address does not have code associated with
119
+ /// it at the time of execution (EXTCODESIZE returns 0), only tokens will be transferred.
120
+ function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
121
+ return interfaceId == type(IAny2EVMMessageReceiver).interfaceId || super.supportsInterface(interfaceId);
122
+ }
123
+
124
+ //*********************************************************************//
125
+ // --------------------- external transactions ----------------------- //
126
+ //*********************************************************************//
127
+
128
+ /// @notice The entrypoint for the CCIP router to call. This function should
129
+ /// never revert, all errors should be handled internally in this contract.
130
+ /// @param any2EvmMessage The message to process.
131
+ /// @dev Extremely important to ensure only router calls this.
132
+ function ccipReceive(Client.Any2EVMMessage calldata any2EvmMessage) external override {
133
+ // only calls from the set router are accepted.
134
+ if (_msgSender() != address(CCIP_ROUTER)) revert JBSucker_NotPeer(_toBytes32(_msgSender()));
135
+
136
+ // Decode the message root from the peer
137
+ JBMessageRoot memory root = abi.decode(any2EvmMessage.data, (JBMessageRoot));
138
+ address origin = abi.decode(any2EvmMessage.sender, (address));
139
+
140
+ // Make sure that the message came from our peer.
141
+ if (origin != _toAddress(peer()) || any2EvmMessage.sourceChainSelector != REMOTE_CHAIN_SELECTOR) {
142
+ revert JBSucker_NotPeer(_toBytes32(origin));
143
+ }
144
+
145
+ // Note (M-28): We intentionally do NOT validate root.amount against destTokenAmounts[0].amount here.
146
+ // CCIP fees are paid separately (via feeToken), so delivered amounts should always match what was sent.
147
+ // If we reverted on a mismatch, the tokens already transferred by CCIP would be locked in the router
148
+ // with no recovery path — a concrete fund-loss risk that outweighs the theoretical defense-in-depth
149
+ // benefit against a CCIP-level failure or peer compromise. See AUDIT_FINDINGS.md M-28.
150
+
151
+ // We either send no tokens or a single token.
152
+ if (any2EvmMessage.destTokenAmounts.length == 1) {
153
+ // The sucker only handles ERC-20s or native. CCIP delivers wrapped native (WETH).
154
+ Client.EVMTokenAmount memory tokenAmount = any2EvmMessage.destTokenAmounts[0];
155
+ // Unwrap WETH -> ETH only when the root says the token is NATIVE_TOKEN.
156
+ // When root.token is an ERC-20 address (e.g., bridging to a chain where ETH is an ERC-20), no unwrap.
157
+ if (root.token == _toBytes32(JBConstants.NATIVE_TOKEN)) {
158
+ // We can (safely) assume that the token that is set in the `destTokenAmounts` is a valid wrapped
159
+ // native.
160
+ // If this ends up not being the case then our sanity check to see if we unwrapped the native asset will
161
+ // fail.
162
+ IWrappedNativeToken wrapped_native = IWrappedNativeToken(tokenAmount.token);
163
+ uint256 balanceBefore = _balanceOf({token: JBConstants.NATIVE_TOKEN, addr: address(this)});
164
+
165
+ // Withdraw the wrapped native asset.
166
+ wrapped_native.withdraw(tokenAmount.amount);
167
+
168
+ // Sanity check the unwrapping of the native asset.
169
+ // slither-disable-next-line incorrect-equality
170
+ assert(
171
+ balanceBefore + tokenAmount.amount
172
+ == _balanceOf({token: JBConstants.NATIVE_TOKEN, addr: address(this)})
173
+ );
174
+ }
175
+ }
176
+
177
+ // Call ourselves to process the root.
178
+ this.fromRemote(root);
179
+ }
180
+
181
+ //*********************************************************************//
182
+ // --------------------- internal transactions ----------------------- //
183
+ //*********************************************************************//
184
+
185
+ /// @notice Unused in this context.
186
+ function _isRemotePeer(address sender) internal view override returns (bool _valid) {
187
+ // NOTICE: We do not check if its the `peer` here, as this contract is supposed to be the caller *NOT* the peer.
188
+ return sender == address(this);
189
+ }
190
+
191
+ /// @notice Uses CCIP to send the root and assets over the bridge to the peer.
192
+ /// @param transportPayment the amount of `msg.value` that is going to get paid for sending this message.
193
+ /// @param token The token to bridge the outbox tree for.
194
+ /// @param remoteToken Information about the remote token being bridged to.
195
+ function _sendRootOverAMB(
196
+ uint256 transportPayment,
197
+ uint256,
198
+ address token,
199
+ uint256 amount,
200
+ JBRemoteToken memory remoteToken,
201
+ JBMessageRoot memory sucker_message
202
+ )
203
+ internal
204
+ override
205
+ {
206
+ // Make sure we are attempting to pay the bridge
207
+ if (transportPayment == 0) revert JBSucker_ExpectedMsgValue();
208
+
209
+ uint256 gasLimit = MESSENGER_BASE_GAS_LIMIT;
210
+ Client.EVMTokenAmount[] memory tokenAmounts;
211
+ if (amount != 0) {
212
+ // If we also do an asset transfer then we increase the min required gas amount.
213
+ gasLimit += remoteToken.minGas;
214
+
215
+ // Wrap native ETH -> WETH for CCIP bridging. CCIP only transports ERC-20s.
216
+ // This is why `_validateTokenMapping` enforces minGas for native tokens too.
217
+ if (token == JBConstants.NATIVE_TOKEN) {
218
+ // Get the wrapped native token.
219
+ // slither-disable-next-line calls-loop
220
+ IWrappedNativeToken wrapped_native = CCIP_ROUTER.getWrappedNative();
221
+ // Deposit the wrapped native asset.
222
+ // slither-disable-next-line calls-loop,arbitrary-send-eth
223
+ wrapped_native.deposit{value: amount}();
224
+ // Update the token to be the wrapped native asset.
225
+ token = address(wrapped_native);
226
+ }
227
+
228
+ // Set the token amounts
229
+ tokenAmounts = new Client.EVMTokenAmount[](1);
230
+ tokenAmounts[0] = Client.EVMTokenAmount({token: token, amount: amount});
231
+
232
+ // approve the Router to spend tokens on contract's behalf. It will spend the amount of the given token
233
+ SafeERC20.forceApprove(IERC20(token), address(CCIP_ROUTER), amount);
234
+ }
235
+
236
+ // Create an EVM2AnyMessage struct in memory with necessary information for sending a cross-chain message
237
+ // CCIP requires EVM addresses, so convert the bytes32 peer to an address for the receiver field.
238
+ Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({
239
+ receiver: abi.encode(_toAddress(peer())),
240
+ data: abi.encode(sucker_message),
241
+ tokenAmounts: tokenAmounts,
242
+ extraArgs: Client._argsToBytes(
243
+ // Additional arguments, setting gas limit
244
+ Client.EVMExtraArgsV1({gasLimit: gasLimit})
245
+ ),
246
+ // Set the feeToken to a feeTokenAddress, indicating specific asset will be used for fees,
247
+ // We pay in the native asset.
248
+ feeToken: address(0)
249
+ });
250
+
251
+ // Get the fee required to send the CCIP message
252
+ // slither-disable-next-line calls-loop
253
+ uint256 fees = CCIP_ROUTER.getFee({destinationChainSelector: REMOTE_CHAIN_SELECTOR, message: message});
254
+
255
+ if (fees > transportPayment) {
256
+ revert JBSucker_InsufficientMsgValue(transportPayment, fees);
257
+ }
258
+
259
+ // slither-disable-next-line calls-loop,unused-return
260
+ CCIP_ROUTER.ccipSend{value: fees}({destinationChainSelector: REMOTE_CHAIN_SELECTOR, message: message});
261
+
262
+ // Refund remaining balance. We use a low-level call that does not revert on failure because
263
+ // `ccipSend` above has already committed the bridge message and transferred the tokens. If we
264
+ // reverted here (e.g. because the caller is a non-payable contract), the entire transaction
265
+ // would roll back — but the CCIP message is already in-flight. The tokens would be gone, the
266
+ // merkle root never gets processed, and the outbox state is inconsistent.
267
+ //
268
+ // If the refund fails, the ETH (transportPayment - fees) will be permanently stuck in this
269
+ // contract. There is no sweep or recovery function — `addOutstandingAmountToBalance` only
270
+ // moves funds tracked via `fromRemote`, not arbitrary ETH. This is an accepted tradeoff:
271
+ // stuck dust from a fee overpayment is far less harmful than bricking the entire bridge
272
+ // operation. The event provides observability so it doesn't go unnoticed.
273
+ //
274
+ // See AUDIT_FINDINGS.md M-2 for the full analysis.
275
+ uint256 refundAmount = transportPayment - fees;
276
+ if (refundAmount != 0) {
277
+ // slither-disable-next-line calls-loop,msg-value-loop,reentrancy-events
278
+ (bool sent,) = _msgSender().call{value: refundAmount}("");
279
+ if (!sent) emit TransportPaymentRefundFailed(_msgSender(), refundAmount);
280
+ }
281
+ }
282
+
283
+ /// @notice Allow sucker implementations to add/override mapping rules to suite their specific needs.
284
+ /// @dev Unlike OP/Arbitrum suckers (which share ETH as native on both chains), this CCIP sucker can connect
285
+ /// chains with different native tokens. This means `NATIVE_TOKEN` may map to an ERC-20 on the remote chain.
286
+ ///
287
+ /// Example: ETH mainnet (native = ETH) <-> Celo (native = CELO, ETH is an ERC-20).
288
+ /// - On mainnet: `mapToken({localToken: NATIVE_TOKEN, remoteToken: celoETH_address})`
289
+ /// - Sending: `_sendRootOverAMB` wraps native ETH -> WETH, bridges WETH via CCIP.
290
+ /// - Receiving: `ccipReceive` checks `root.token == NATIVE_TOKEN` to decide whether to unwrap WETH -> ETH.
291
+ /// If `root.token` is an ERC-20 address (like celoETH), no unwrap occurs — tokens stay as ERC-20.
292
+ ///
293
+ /// The base class restriction (`NATIVE_TOKEN` can only map to `NATIVE_TOKEN` or `address(0)`) is intentionally
294
+ /// removed here. The base class retains that restriction for OP/Arbitrum where both chains share ETH as native.
295
+ function _validateTokenMapping(JBTokenMapping calldata map) internal pure virtual override {
296
+ // Enforce a reasonable minimum gas limit for bridging. A minimum which is too low could lead to the loss of
297
+ // funds. CCIP wraps native tokens to WETH before bridging (see `_sendRootOverAMB`), so ALL tokens —
298
+ // including native — need sufficient gas for an ERC-20 transfer on the remote chain.
299
+ if (map.minGas < MESSENGER_ERC20_MIN_GAS_LIMIT) {
300
+ revert JBSucker_BelowMinGas(map.minGas, MESSENGER_ERC20_MIN_GAS_LIMIT);
301
+ }
302
+ }
303
+ }