@bananapus/suckers-v6 0.0.35 → 0.0.37
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/README.md +3 -3
- package/package.json +2 -2
- package/references/operations.md +2 -2
- package/references/runtime.md +1 -1
- package/src/JBArbitrumSucker.sol +5 -17
- package/src/JBCCIPSucker.sol +7 -10
- package/src/JBCeloSucker.sol +20 -26
- package/src/JBOptimismSucker.sol +1 -4
- package/src/JBSucker.sol +55 -63
- package/src/JBSuckerRegistry.sol +13 -59
- package/src/JBSwapCCIPSucker.sol +62 -31
- package/src/deployers/JBArbitrumSuckerDeployer.sol +5 -3
- package/src/deployers/JBCCIPSuckerDeployer.sol +5 -3
- package/src/deployers/JBCeloSuckerDeployer.sol +7 -5
- package/src/deployers/JBOptimismSuckerDeployer.sol +5 -3
- package/src/deployers/JBSuckerDeployer.sol +14 -10
- package/src/deployers/JBSwapCCIPSuckerDeployer.sol +7 -9
- package/src/interfaces/IJBCeloSuckerDeployer.sol +1 -1
- package/src/interfaces/IJBSwapCCIPSuckerDeployer.sol +2 -1
- package/src/libraries/ARBAddresses.sol +6 -3
- package/src/libraries/ARBChains.sol +4 -1
- package/src/libraries/CCIPHelper.sol +68 -11
- package/src/libraries/JBCCIPLib.sol +8 -17
- package/src/libraries/JBSuckerLib.sol +2 -13
- package/src/libraries/JBSwapPoolLib.sol +57 -49
- package/src/utils/MerkleLib.sol +3 -5
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/
|
|
76
|
-
5. `test/
|
|
75
|
+
4. `test/regression/PeerSnapshotDesync.t.sol`
|
|
76
|
+
5. `test/regression/ToRemoteFeeIrrecoverable.t.sol`
|
|
77
77
|
|
|
78
78
|
## Install
|
|
79
79
|
|
|
@@ -113,7 +113,7 @@ src/
|
|
|
113
113
|
structs/
|
|
114
114
|
utils/
|
|
115
115
|
test/
|
|
116
|
-
unit, fork, interoperability, attack,
|
|
116
|
+
unit, fork, interoperability, attack, review, and regression coverage
|
|
117
117
|
script/
|
|
118
118
|
Deploy.s.sol
|
|
119
119
|
helpers/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bananapus/suckers-v6",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.37",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"deploy:mainnets": "source ./.env && npx sphinx propose ./script/Deploy.s.sol --networks mainnets",
|
|
27
27
|
"deploy:testnets": "source ./.env && npx sphinx propose ./script/Deploy.s.sol --networks testnets",
|
|
28
28
|
"artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'nana-suckers-v6'",
|
|
29
|
-
"analyze": "
|
|
29
|
+
"analyze": "forge lint --deny notes"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@arbitrum/nitro-contracts": "3.2.0",
|
package/references/operations.md
CHANGED
|
@@ -23,6 +23,6 @@
|
|
|
23
23
|
|
|
24
24
|
## Useful Proof Points
|
|
25
25
|
|
|
26
|
-
- [`test/SuckerAttacks.t.sol`](../test/SuckerAttacks.t.sol), [`test/SuckerDeepAttacks.t.sol`](../test/SuckerDeepAttacks.t.sol), and [`test/
|
|
26
|
+
- [`test/SuckerAttacks.t.sol`](../test/SuckerAttacks.t.sol), [`test/SuckerDeepAttacks.t.sol`](../test/SuckerDeepAttacks.t.sol), and [`test/TestRegressionGaps.sol`](../test/TestRegressionGaps.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/
|
|
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/regression/PeerSnapshotDesync.t.sol`](../test/regression/PeerSnapshotDesync.t.sol) when shared accounting or snapshot boundaries are in doubt.
|
package/references/runtime.md
CHANGED
|
@@ -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/
|
|
30
|
+
- [`test/SuckerAttacks.t.sol`](../test/SuckerAttacks.t.sol), [`test/SuckerDeepAttacks.t.sol`](../test/SuckerDeepAttacks.t.sol), [`test/regression/PeerSnapshotDesync.t.sol`](../test/regression/PeerSnapshotDesync.t.sol), and [`test/regression/PeerDeterminism.t.sol`](../test/regression/PeerDeterminism.t.sol) when the bug could involve base logic, registry behavior, or a specific bridge implementation.
|
package/src/JBArbitrumSucker.sol
CHANGED
|
@@ -127,7 +127,6 @@ contract JBArbitrumSucker is JBSucker, IJBArbitrumSucker {
|
|
|
127
127
|
internal
|
|
128
128
|
{
|
|
129
129
|
address peerAddress = _peerAddress();
|
|
130
|
-
// slither-disable-next-line unused-return,calls-loop
|
|
131
130
|
ARBINBOX.unsafeCreateRetryableTicket{value: callTransportCost + nativeValue}({
|
|
132
131
|
to: peerAddress,
|
|
133
132
|
l2CallValue: nativeValue,
|
|
@@ -144,7 +143,6 @@ contract JBArbitrumSucker is JBSucker, IJBArbitrumSucker {
|
|
|
144
143
|
/// @param token The ERC-20 token to approve.
|
|
145
144
|
/// @param amount The amount to approve.
|
|
146
145
|
function _approveGateway(address token, uint256 amount) internal {
|
|
147
|
-
// slither-disable-next-line calls-loop
|
|
148
146
|
SafeERC20.forceApprove({token: IERC20(token), spender: GATEWAYROUTER.getGateway(token), value: amount});
|
|
149
147
|
}
|
|
150
148
|
|
|
@@ -168,10 +166,9 @@ contract JBArbitrumSucker is JBSucker, IJBArbitrumSucker {
|
|
|
168
166
|
bytes memory data = abi.encodeCall(JBSucker.fromRemote, (message));
|
|
169
167
|
|
|
170
168
|
// Depending on which layer we are on, send the call to the other layer.
|
|
171
|
-
// slither-disable-start out-of-order-retryable
|
|
172
169
|
if (LAYER == JBLayer.L1) {
|
|
173
170
|
// L1→L2 requires transport payment for retryable tickets.
|
|
174
|
-
if (transportPayment == 0) revert JBSucker_ExpectedMsgValue();
|
|
171
|
+
if (transportPayment == 0) revert JBSucker_ExpectedMsgValue({msgValue: transportPayment});
|
|
175
172
|
_toL2({
|
|
176
173
|
token: token, transportPayment: transportPayment, amount: amount, data: data, remoteToken: remoteToken
|
|
177
174
|
});
|
|
@@ -180,7 +177,6 @@ contract JBArbitrumSucker is JBSucker, IJBArbitrumSucker {
|
|
|
180
177
|
if (transportPayment != 0) revert JBSucker_UnexpectedMsgValue(transportPayment);
|
|
181
178
|
_toL1({token: token, amount: amount, data: data, remoteToken: remoteToken});
|
|
182
179
|
}
|
|
183
|
-
// slither-disable-end out-of-order-retryable
|
|
184
180
|
}
|
|
185
181
|
|
|
186
182
|
/// @notice Bridge the `token` and data to the remote L1 chain.
|
|
@@ -208,7 +204,6 @@ contract JBArbitrumSucker is JBSucker, IJBArbitrumSucker {
|
|
|
208
204
|
_approveGateway({token: token, amount: amount});
|
|
209
205
|
|
|
210
206
|
// Convert bytes32 types to address at the Arbitrum bridge API boundary.
|
|
211
|
-
// slither-disable-next-line calls-loop,unused-return
|
|
212
207
|
IArbL2GatewayRouter(address(GATEWAYROUTER))
|
|
213
208
|
.outboundTransfer({
|
|
214
209
|
l1Token: _toAddress(remoteToken.addr), to: peerAddress, amount: amount, data: bytes("")
|
|
@@ -220,7 +215,6 @@ contract JBArbitrumSucker is JBSucker, IJBArbitrumSucker {
|
|
|
220
215
|
|
|
221
216
|
// Send the message to the peer with the reclaimed ETH.
|
|
222
217
|
// Address `100` is the ArbSys precompile address.
|
|
223
|
-
// slither-disable-next-line calls-loop,unused-return
|
|
224
218
|
ArbSys(address(100)).sendTxToL1{value: nativeValue}({destination: peerAddress, data: data});
|
|
225
219
|
}
|
|
226
220
|
|
|
@@ -251,7 +245,6 @@ contract JBArbitrumSucker is JBSucker, IJBArbitrumSucker {
|
|
|
251
245
|
uint256 maxSubmissionCost;
|
|
252
246
|
|
|
253
247
|
{
|
|
254
|
-
// slither-disable-next-line calls-loop
|
|
255
248
|
maxSubmissionCost =
|
|
256
249
|
ARBINBOX.calculateRetryableSubmissionFee({dataLength: data.length, baseFee: maxFeePerGas});
|
|
257
250
|
|
|
@@ -267,15 +260,12 @@ contract JBArbitrumSucker is JBSucker, IJBArbitrumSucker {
|
|
|
267
260
|
{
|
|
268
261
|
// Get the exact calldata length the gateway will create for the retryable ticket.
|
|
269
262
|
// The Arbitrum Inbox validates maxSubmissionCost against this actual payload, not the user data.
|
|
270
|
-
// slither-disable-next-line calls-loop
|
|
271
263
|
address gateway = GATEWAYROUTER.getGateway(token);
|
|
272
|
-
// slither-disable-next-line calls-loop
|
|
273
264
|
uint256 outboundCalldataLength =
|
|
274
265
|
IL1ArbitrumGateway(gateway)
|
|
275
266
|
.getOutboundCalldata({
|
|
276
267
|
_token: token, _from: address(this), _to: _peerAddress(), _amount: amount, _data: bytes("")
|
|
277
268
|
}).length;
|
|
278
|
-
// slither-disable-next-line calls-loop
|
|
279
269
|
maxSubmissionCostERC20 = ARBINBOX.calculateRetryableSubmissionFee({
|
|
280
270
|
dataLength: outboundCalldataLength, baseFee: maxFeePerGas
|
|
281
271
|
});
|
|
@@ -284,7 +274,9 @@ contract JBArbitrumSucker is JBSucker, IJBArbitrumSucker {
|
|
|
284
274
|
|
|
285
275
|
// Ensure we bridge enough for gas costs on L2 side
|
|
286
276
|
if (transportPayment < callTransportCost + tokenTransportCost) {
|
|
287
|
-
revert JBArbitrumSucker_NotEnoughGas(
|
|
277
|
+
revert JBArbitrumSucker_NotEnoughGas({
|
|
278
|
+
payment: transportPayment, cost: callTransportCost + tokenTransportCost
|
|
279
|
+
});
|
|
288
280
|
}
|
|
289
281
|
|
|
290
282
|
{
|
|
@@ -298,8 +290,6 @@ contract JBArbitrumSucker is JBSucker, IJBArbitrumSucker {
|
|
|
298
290
|
_approveGateway({token: token, amount: amount});
|
|
299
291
|
|
|
300
292
|
// Perform the ERC-20 bridge transfer. Convert bytes32 peer to address at the Arbitrum bridge API boundary.
|
|
301
|
-
// slither-disable-start out-of-order-retryable
|
|
302
|
-
// slither-disable-next-line calls-loop,unused-return
|
|
303
293
|
IArbL1GatewayRouter(address(GATEWAYROUTER)).outboundTransferCustomRefund{value: tokenTransportCost}({
|
|
304
294
|
token: token,
|
|
305
295
|
refundTo: _msgSender(),
|
|
@@ -312,7 +302,7 @@ contract JBArbitrumSucker is JBSucker, IJBArbitrumSucker {
|
|
|
312
302
|
} else {
|
|
313
303
|
// Ensure we bridge enough for gas costs on L2 side
|
|
314
304
|
if (transportPayment < callTransportCost) {
|
|
315
|
-
revert JBArbitrumSucker_NotEnoughGas(transportPayment, callTransportCost);
|
|
305
|
+
revert JBArbitrumSucker_NotEnoughGas({payment: transportPayment, cost: callTransportCost});
|
|
316
306
|
}
|
|
317
307
|
|
|
318
308
|
// If the token is the native token then we only need to do a single call.
|
|
@@ -328,7 +318,6 @@ contract JBArbitrumSucker is JBSucker, IJBArbitrumSucker {
|
|
|
328
318
|
// The above check is the same check that makes it `safeCreateRetryableTicket`.
|
|
329
319
|
|
|
330
320
|
// Convert bytes32 peer to address at the Arbitrum inbox API boundary.
|
|
331
|
-
// slither-disable-next-line calls-loop,unused-return
|
|
332
321
|
_createRetryableTicket({
|
|
333
322
|
callTransportCost: callTransportCost,
|
|
334
323
|
nativeValue: nativeValue,
|
|
@@ -336,6 +325,5 @@ contract JBArbitrumSucker is JBSucker, IJBArbitrumSucker {
|
|
|
336
325
|
maxFeePerGas: maxFeePerGas,
|
|
337
326
|
data: data
|
|
338
327
|
});
|
|
339
|
-
// slither-disable-end out-of-order-retryable
|
|
340
328
|
}
|
|
341
329
|
}
|
package/src/JBCCIPSucker.sol
CHANGED
|
@@ -163,7 +163,7 @@ contract JBCCIPSucker is JBSucker, IAny2EVMMessageReceiver {
|
|
|
163
163
|
|
|
164
164
|
// Make sure that the message came from our peer.
|
|
165
165
|
if (origin != _peerAddress() || any2EvmMessage.sourceChainSelector != REMOTE_CHAIN_SELECTOR) {
|
|
166
|
-
revert JBSucker_NotPeer(_toBytes32(origin));
|
|
166
|
+
revert JBSucker_NotPeer({caller: _toBytes32(origin)});
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
// Discriminate message type: abi.encode(uint8 type, bytes payload).
|
|
@@ -174,7 +174,7 @@ contract JBCCIPSucker is JBSucker, IAny2EVMMessageReceiver {
|
|
|
174
174
|
// Decode the root message from the payload.
|
|
175
175
|
JBMessageRoot memory root = abi.decode(payload, (JBMessageRoot));
|
|
176
176
|
|
|
177
|
-
// Only unwrap
|
|
177
|
+
// Only unwrap wrapped native token when the root targets native token (not when claiming it as ERC-20).
|
|
178
178
|
if (root.token == bytes32(uint256(uint160(JBConstants.NATIVE_TOKEN)))) {
|
|
179
179
|
JBCCIPLib.unwrapReceivedTokens({
|
|
180
180
|
ccipRouter: CCIP_ROUTER, destTokenAmounts: any2EvmMessage.destTokenAmounts
|
|
@@ -184,7 +184,7 @@ contract JBCCIPSucker is JBSucker, IAny2EVMMessageReceiver {
|
|
|
184
184
|
// Forward the root message to this contract's fromRemote handler.
|
|
185
185
|
this.fromRemote(root);
|
|
186
186
|
} else {
|
|
187
|
-
revert JBCCIPSucker_UnknownMessageType(messageType);
|
|
187
|
+
revert JBCCIPSucker_UnknownMessageType({messageType: messageType});
|
|
188
188
|
}
|
|
189
189
|
}
|
|
190
190
|
|
|
@@ -224,8 +224,7 @@ contract JBCCIPSucker is JBSucker, IAny2EVMMessageReceiver {
|
|
|
224
224
|
// Add extra gas for the ERC-20 token transfer on the remote chain.
|
|
225
225
|
gasLimit += remoteToken.minGas;
|
|
226
226
|
|
|
227
|
-
// Wrap native
|
|
228
|
-
// slither-disable-next-line unused-return
|
|
227
|
+
// Wrap native tokens if needed, build the CCIP token amounts array, and approve the router.
|
|
229
228
|
(tokenAmounts,) = JBCCIPLib.prepareTokenAmounts({ccipRouter: CCIP_ROUTER, token: token, amount: amount});
|
|
230
229
|
} else {
|
|
231
230
|
// No tokens to bridge — use an empty array.
|
|
@@ -239,7 +238,6 @@ contract JBCCIPSucker is JBSucker, IAny2EVMMessageReceiver {
|
|
|
239
238
|
address feeToken = transportPayment == 0 ? CCIPHelper.linkOfChain(block.chainid) : address(0);
|
|
240
239
|
|
|
241
240
|
// Build and send the CCIP message with the root payload.
|
|
242
|
-
// slither-disable-next-line reentrancy-events
|
|
243
241
|
(bool refundFailed, uint256 refundAmount) = JBCCIPLib.sendCCIPMessage({
|
|
244
242
|
ccipRouter: CCIP_ROUTER,
|
|
245
243
|
remoteChainSelector: REMOTE_CHAIN_SELECTOR,
|
|
@@ -256,7 +254,6 @@ contract JBCCIPSucker is JBSucker, IAny2EVMMessageReceiver {
|
|
|
256
254
|
// Retain failed refunds as caller credit instead of leaving them project-addable or stranded.
|
|
257
255
|
if (refundFailed) {
|
|
258
256
|
// Refund accounting is isolated per caller; reentry cannot increase the retained credit.
|
|
259
|
-
// slither-disable-next-line reentrancy-benign
|
|
260
257
|
_retainTransportPaymentRefund({account: _msgSender(), amount: refundAmount});
|
|
261
258
|
emit TransportPaymentRefundFailed({recipient: _msgSender(), amount: refundAmount});
|
|
262
259
|
}
|
|
@@ -281,15 +278,15 @@ contract JBCCIPSucker is JBSucker, IAny2EVMMessageReceiver {
|
|
|
281
278
|
///
|
|
282
279
|
/// Example: ETH mainnet (native = ETH) <-> Celo (native = CELO, ETH is an ERC-20).
|
|
283
280
|
/// - On mainnet: `mapToken({localToken: NATIVE_TOKEN, remoteToken: celoETH_address})`
|
|
284
|
-
/// - Sending: `_sendRootOverAMB` wraps native
|
|
285
|
-
/// - Receiving: `ccipReceive` checks `root.token == NATIVE_TOKEN` to decide whether to unwrap
|
|
281
|
+
/// - Sending: `_sendRootOverAMB` wraps native tokens, bridges them via CCIP.
|
|
282
|
+
/// - Receiving: `ccipReceive` checks `root.token == NATIVE_TOKEN` to decide whether to unwrap.
|
|
286
283
|
/// If `root.token` is an ERC-20 address (like celoETH), no unwrap occurs — tokens stay as ERC-20.
|
|
287
284
|
///
|
|
288
285
|
/// The base class restriction (`NATIVE_TOKEN` can only map to `NATIVE_TOKEN` or `address(0)`) is intentionally
|
|
289
286
|
/// removed here. The base class retains that restriction for OP/Arbitrum where both chains share ETH as native.
|
|
290
287
|
function _validateTokenMapping(JBTokenMapping calldata map) internal pure virtual override {
|
|
291
288
|
// Enforce a reasonable minimum gas limit for bridging. A minimum which is too low could lead to the loss of
|
|
292
|
-
// funds. CCIP wraps native tokens
|
|
289
|
+
// funds. CCIP wraps native tokens before bridging (see `_sendRootOverAMB`), so ALL tokens —
|
|
293
290
|
// including native — need sufficient gas for an ERC-20 transfer on the remote chain.
|
|
294
291
|
if (map.minGas < MESSENGER_ERC20_MIN_GAS_LIMIT) {
|
|
295
292
|
revert JBSucker_BelowMinGas({minGas: map.minGas, minGasLimit: MESSENGER_ERC20_MIN_GAS_LIMIT});
|
package/src/JBCeloSucker.sol
CHANGED
|
@@ -20,7 +20,7 @@ import {JBRemoteToken} from "./structs/JBRemoteToken.sol";
|
|
|
20
20
|
import {JBTokenMapping} from "./structs/JBTokenMapping.sol";
|
|
21
21
|
|
|
22
22
|
/// @notice A `JBSucker` implementation for Celo — an OP Stack chain with a custom gas token (CELO, not ETH).
|
|
23
|
-
/// @dev ETH exists on Celo only as an ERC-20
|
|
23
|
+
/// @dev ETH exists on Celo only as an ERC-20. This sucker wraps native ETH before bridging
|
|
24
24
|
/// as ERC-20 via the OP standard bridge, and removes the `NATIVE_TOKEN → NATIVE_TOKEN` restriction so that
|
|
25
25
|
/// native ETH can map to a remote ERC-20.
|
|
26
26
|
contract JBCeloSucker is JBOptimismSucker {
|
|
@@ -28,8 +28,8 @@ contract JBCeloSucker is JBOptimismSucker {
|
|
|
28
28
|
// --------------- public immutable stored properties ---------------- //
|
|
29
29
|
//*********************************************************************//
|
|
30
30
|
|
|
31
|
-
/// @notice The
|
|
32
|
-
IWrappedNativeToken public immutable
|
|
31
|
+
/// @notice The ERC-20 wrapper for the chain's native token on the local chain.
|
|
32
|
+
IWrappedNativeToken public immutable WRAPPED_NATIVE_TOKEN;
|
|
33
33
|
|
|
34
34
|
//*********************************************************************//
|
|
35
35
|
// ---------------------------- constructor -------------------------- //
|
|
@@ -53,7 +53,7 @@ contract JBCeloSucker is JBOptimismSucker {
|
|
|
53
53
|
JBOptimismSucker(deployer, directory, permissions, prices, tokens, feeProjectId, registry, trustedForwarder)
|
|
54
54
|
{
|
|
55
55
|
// Fetch the wrapped native token by doing a callback to the deployer contract.
|
|
56
|
-
|
|
56
|
+
WRAPPED_NATIVE_TOKEN = JBCeloSuckerDeployer(deployer).wrappedNative();
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
//*********************************************************************//
|
|
@@ -73,27 +73,26 @@ contract JBCeloSucker is JBOptimismSucker {
|
|
|
73
73
|
// --------------------- internal transactions ----------------------- //
|
|
74
74
|
//*********************************************************************//
|
|
75
75
|
|
|
76
|
-
/// @notice Unwraps
|
|
77
|
-
/// @dev When tokens are bridged from Celo → L1 via the OP bridge,
|
|
78
|
-
/// But the L1 project's terminal accepts native ETH (NATIVE_TOKEN), not
|
|
76
|
+
/// @notice Unwraps wrapped native tokens before adding to the project's balance.
|
|
77
|
+
/// @dev When tokens are bridged from Celo → L1 via the OP bridge, wrapped native tokens (ERC-20) are released to
|
|
78
|
+
/// the sucker. But the L1 project's terminal accepts native ETH (NATIVE_TOKEN), not the wrapped form. This override
|
|
79
|
+
/// unwraps
|
|
79
80
|
/// and adds native ETH to the project's balance.
|
|
80
81
|
/// @param token The terminal token to add to the project's balance.
|
|
81
82
|
/// @param amount The amount of terminal tokens to add to the project's balance.
|
|
82
83
|
/// @param cachedProjectId The cached project ID to avoid redundant storage reads.
|
|
83
84
|
function _addToBalance(address token, uint256 amount, uint256 cachedProjectId) internal override {
|
|
84
|
-
if (token == address(
|
|
85
|
-
// Check addable amount against
|
|
85
|
+
if (token == address(WRAPPED_NATIVE_TOKEN)) {
|
|
86
|
+
// Check addable amount against wrapped native token balance before unwrapping.
|
|
86
87
|
uint256 addableAmount = amountToAddToBalanceOf(token);
|
|
87
88
|
if (amount > addableAmount) {
|
|
88
89
|
revert JBSucker_InsufficientBalance({amount: amount, balance: addableAmount});
|
|
89
90
|
}
|
|
90
91
|
|
|
91
|
-
// Unwrap
|
|
92
|
-
|
|
93
|
-
WRAPPED_NATIVE.withdraw(amount);
|
|
92
|
+
// Unwrap wrapped native tokens → native tokens.
|
|
93
|
+
WRAPPED_NATIVE_TOKEN.withdraw(amount);
|
|
94
94
|
|
|
95
95
|
// Get the project's primary terminal for native token.
|
|
96
|
-
// slither-disable-next-line calls-loop
|
|
97
96
|
IJBTerminal terminal =
|
|
98
97
|
DIRECTORY.primaryTerminalOf({projectId: cachedProjectId, token: JBConstants.NATIVE_TOKEN});
|
|
99
98
|
|
|
@@ -102,7 +101,6 @@ contract JBCeloSucker is JBOptimismSucker {
|
|
|
102
101
|
}
|
|
103
102
|
|
|
104
103
|
// Add native ETH to the project's balance.
|
|
105
|
-
// slither-disable-next-line arbitrary-send-eth,calls-loop
|
|
106
104
|
terminal.addToBalanceOf{value: amount}({
|
|
107
105
|
projectId: cachedProjectId,
|
|
108
106
|
token: JBConstants.NATIVE_TOKEN,
|
|
@@ -118,7 +116,7 @@ contract JBCeloSucker is JBOptimismSucker {
|
|
|
118
116
|
|
|
119
117
|
/// @notice Use the `OPMESSENGER` to send the outbox tree for the `token` and the corresponding funds to the peer
|
|
120
118
|
/// over the `OPBRIDGE`.
|
|
121
|
-
/// @dev For Celo, native ETH is wrapped
|
|
119
|
+
/// @dev For Celo, native ETH is wrapped and bridged as ERC-20. The messenger message is sent with
|
|
122
120
|
/// `nativeValue = 0` because Celo's native token is CELO (not ETH), so we never attach ETH as msg.value.
|
|
123
121
|
/// @param transportPayment the amount of `msg.value` that is going to get paid for sending this message.
|
|
124
122
|
/// @param token The token to bridge the outbox tree for.
|
|
@@ -139,28 +137,25 @@ contract JBCeloSucker is JBOptimismSucker {
|
|
|
139
137
|
|
|
140
138
|
// Revert if there's a `msg.value`. The OP bridge does not expect to be paid.
|
|
141
139
|
if (transportPayment != 0) {
|
|
142
|
-
revert JBSucker_UnexpectedMsgValue(transportPayment);
|
|
140
|
+
revert JBSucker_UnexpectedMsgValue({value: transportPayment});
|
|
143
141
|
}
|
|
144
142
|
|
|
145
143
|
// Cache peer address to avoid redundant calls.
|
|
146
144
|
address peerAddress = _toAddress(peer());
|
|
147
145
|
|
|
148
146
|
if (amount != 0) {
|
|
149
|
-
// Determine the local token to bridge — native ETH is wrapped
|
|
147
|
+
// Determine the local token to bridge — native ETH is wrapped first.
|
|
150
148
|
address bridgeToken = token;
|
|
151
149
|
if (token == JBConstants.NATIVE_TOKEN) {
|
|
152
|
-
// Wrap native
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
bridgeToken = address(WRAPPED_NATIVE);
|
|
150
|
+
// Wrap native tokens so they can be bridged as ERC-20.
|
|
151
|
+
WRAPPED_NATIVE_TOKEN.deposit{value: amount}();
|
|
152
|
+
bridgeToken = address(WRAPPED_NATIVE_TOKEN);
|
|
156
153
|
}
|
|
157
154
|
|
|
158
155
|
// Approve the bridge to spend the token.
|
|
159
|
-
// slither-disable-next-line reentrancy-events
|
|
160
156
|
SafeERC20.forceApprove({token: IERC20(bridgeToken), spender: address(OPBRIDGE), value: amount});
|
|
161
157
|
|
|
162
158
|
// Bridge the ERC-20 token to the peer.
|
|
163
|
-
// slither-disable-next-line reentrancy-events,calls-loop
|
|
164
159
|
OPBRIDGE.bridgeERC20To({
|
|
165
160
|
localToken: bridgeToken,
|
|
166
161
|
remoteToken: _toAddress(remoteToken.addr),
|
|
@@ -174,7 +169,6 @@ contract JBCeloSucker is JBOptimismSucker {
|
|
|
174
169
|
// Send the messenger message with nativeValue = 0.
|
|
175
170
|
// Celo's native token is CELO, not ETH — we never attach ETH as msg.value on the messenger.
|
|
176
171
|
// On L1, the ETH was already wrapped and bridged as ERC-20 above.
|
|
177
|
-
// slither-disable-next-line reentrancy-events,calls-loop
|
|
178
172
|
OPMESSENGER.sendMessage({
|
|
179
173
|
target: peerAddress,
|
|
180
174
|
message: abi.encodeCall(JBSucker.fromRemote, (message)),
|
|
@@ -183,11 +177,11 @@ contract JBCeloSucker is JBOptimismSucker {
|
|
|
183
177
|
}
|
|
184
178
|
|
|
185
179
|
/// @notice Allow `NATIVE_TOKEN` to map to any remote token (not just `NATIVE_TOKEN`).
|
|
186
|
-
/// @dev Celo uses CELO as native gas token. ETH is an ERC-20 on Celo
|
|
180
|
+
/// @dev Celo uses CELO as native gas token. ETH is an ERC-20 on Celo. So `NATIVE_TOKEN` on L1
|
|
187
181
|
/// maps to an ERC-20 address on Celo, not to `NATIVE_TOKEN`. The base class restriction is removed.
|
|
188
182
|
function _validateTokenMapping(JBTokenMapping calldata map) internal pure virtual override {
|
|
189
183
|
// Enforce a reasonable minimum gas limit for bridging. Since we always bridge as ERC-20
|
|
190
|
-
// (wrapping native
|
|
184
|
+
// (wrapping native tokens), all tokens need sufficient gas for an ERC-20 transfer.
|
|
191
185
|
if (map.minGas < MESSENGER_ERC20_MIN_GAS_LIMIT) {
|
|
192
186
|
revert JBSucker_BelowMinGas({minGas: map.minGas, minGasLimit: MESSENGER_ERC20_MIN_GAS_LIMIT});
|
|
193
187
|
}
|
package/src/JBOptimismSucker.sol
CHANGED
|
@@ -105,7 +105,7 @@ contract JBOptimismSucker is JBSucker, IJBOptimismSucker {
|
|
|
105
105
|
|
|
106
106
|
// Revert if there's a `msg.value`. The OP bridge does not expect to be paid.
|
|
107
107
|
if (transportPayment != 0) {
|
|
108
|
-
revert JBSucker_UnexpectedMsgValue(transportPayment);
|
|
108
|
+
revert JBSucker_UnexpectedMsgValue({value: transportPayment});
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
// Cache peer address to avoid redundant calls.
|
|
@@ -115,11 +115,9 @@ contract JBOptimismSucker is JBSucker, IJBOptimismSucker {
|
|
|
115
115
|
// If the amount is `0` then we do not need to bridge any ERC20.
|
|
116
116
|
if (token != JBConstants.NATIVE_TOKEN && amount != 0) {
|
|
117
117
|
// Approve the tokens being bridged.
|
|
118
|
-
// slither-disable-next-line reentrancy-events
|
|
119
118
|
SafeERC20.forceApprove({token: IERC20(token), spender: address(OPBRIDGE), value: amount});
|
|
120
119
|
|
|
121
120
|
// Bridge the tokens to the peer sucker. Convert bytes32 types to address at the OP Bridge API boundary.
|
|
122
|
-
// slither-disable-next-line reentrancy-events,calls-loop
|
|
123
121
|
OPBRIDGE.bridgeERC20To({
|
|
124
122
|
localToken: token,
|
|
125
123
|
remoteToken: _toAddress(remoteToken.addr),
|
|
@@ -134,7 +132,6 @@ contract JBOptimismSucker is JBSucker, IJBOptimismSucker {
|
|
|
134
132
|
}
|
|
135
133
|
|
|
136
134
|
// Send the message to the peer with the reclaimed ETH.
|
|
137
|
-
// slither-disable-next-line arbitrary-send-eth,reentrancy-events,calls-loop
|
|
138
135
|
OPMESSENGER.sendMessage{value: nativeValue}({
|
|
139
136
|
target: peerAddress,
|
|
140
137
|
message: abi.encodeCall(JBSucker.fromRemote, (message)),
|