@bananapus/suckers-v6 0.0.21 → 0.0.22
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 +1 -1
- package/RISKS.md +3 -3
- package/package.json +2 -2
- package/src/JBArbitrumSucker.sol +5 -3
- package/src/JBCCIPSucker.sol +5 -0
- package/src/JBCeloSucker.sol +28 -38
- package/src/JBOptimismSucker.sol +5 -3
- package/src/JBSucker.sol +56 -31
- package/src/JBSuckerRegistry.sol +21 -6
- package/src/structs/JBOutboxTree.sol +3 -3
- package/src/utils/MerkleLib.sol +13 -6
- package/test/SuckerDeepAttacks.t.sol +1 -1
- package/test/TestAuditGaps.sol +1 -1
- package/test/audit/codex-MapTokensEnableOnlyValueStuck.t.sol +84 -0
- package/test/unit/emergency.t.sol +1 -1
- package/test/unit/invariants.t.sol +1 -1
- package/test/unit/merkle.t.sol +1 -1
package/README.md
CHANGED
|
@@ -62,7 +62,7 @@ The shortest useful reading order is:
|
|
|
62
62
|
| [`JBBaseSuckerDeployer`](src/deployers/JBBaseSuckerDeployer.sol) | Thin wrapper around `JBOptimismSuckerDeployer` for Base. |
|
|
63
63
|
| [`JBCeloSuckerDeployer`](src/deployers/JBCeloSuckerDeployer.sol) | Deployer for `JBCeloSucker`. Extends `JBOptimismSuckerDeployer` with `wrappedNative` (`IWrappedNativeToken`) storage for the local chain's WETH address. |
|
|
64
64
|
| [`JBArbitrumSuckerDeployer`](src/deployers/JBArbitrumSuckerDeployer.sol) | Deployer for `JBArbitrumSucker`. Stores Arbitrum Inbox, Gateway Router, and layer (`JBLayer.L1` or `JBLayer.L2`). |
|
|
65
|
-
| [`MerkleLib`](src/utils/MerkleLib.sol) | Incremental merkle tree (depth 32, max ~4 billion leaves, modeled on eth2 deposit contract). Used for outbox/inbox trees. Gas-optimized with inline assembly for `root()` and `branchRoot()`. |
|
|
65
|
+
| [`MerkleLib`](src/utils/MerkleLib.sol) | Incremental merkle tree (depth 32, max ~4 billion leaves, modeled on eth2 deposit contract). Used for outbox/inbox trees. `insert` and `root` operate directly on `Tree storage` (not memory copies) to avoid redundant SLOAD/SSTORE round-trips. Gas-optimized with inline assembly for `root()` and `branchRoot()`. |
|
|
66
66
|
| [`CCIPHelper`](src/libraries/CCIPHelper.sol) | CCIP router addresses, chain selectors, and WETH addresses per chain. Covers Ethereum, Optimism, Arbitrum, Base, Polygon, Avalanche, and BNB Chain (mainnet and testnets). |
|
|
67
67
|
| [`ARBAddresses`](src/libraries/ARBAddresses.sol) | Arbitrum bridge contract addresses (Inbox, Gateway Router) for mainnet and Sepolia. |
|
|
68
68
|
| [`ARBChains`](src/libraries/ARBChains.sol) | Arbitrum chain ID constants. |
|
package/RISKS.md
CHANGED
|
@@ -91,7 +91,7 @@ This file focuses on the bridge-like risks in the sucker system: merkle-root pro
|
|
|
91
91
|
- **uint128 cap for SVM compatibility.** `_insertIntoTree` reverts if `projectTokenCount` or `terminalTokenAmount` exceeds `type(uint128).max`. This is enforced for cross-VM compatibility but limits EVM-only use cases to ~3.4e38 wei per leaf.
|
|
92
92
|
- **Arbitrum retryable ticket pricing.** `_toL2` uses `block.basefee` as `maxFeePerGas`. If L2 gas prices spike above L1's `block.basefee`, the retryable ticket may not auto-redeem and requires manual retry.
|
|
93
93
|
- **CCIP fee volatility.** `_sendRootOverAMB` checks `CCIP_ROUTER.getFee()` at call time. If fees spike between estimation and execution, the transaction reverts with `JBSucker_InsufficientMsgValue`. No retry mechanism exists.
|
|
94
|
-
- **`toRemote` fee fallback
|
|
94
|
+
- **`toRemote` fee fallback retains ETH, absorbed by future claims.** If the fee project's terminal is missing or `terminal.pay()` reverts, `toRemote()` keeps the fee ETH in the sucker so zero-cost bridges can still proceed with `transportPayment = msg.value - fee`. That retained ETH is absorbed by future native token claims: `amountToAddToBalanceOf(NATIVE_TOKEN)` computes `_balanceOf(token, address(this)) - _outboxOf[token].balance`, so any extra ETH in the contract (including retained fees) increases the claimable amount and is forwarded to the project's terminal via `_addToBalance` when the next native token claim is processed. The retained amount per affected call is bounded by `MAX_TO_REMOTE_FEE` (currently `0.001 ether`).
|
|
95
95
|
- **CCIP transport payment refund failure.** If `_msgSender()` is a non-payable contract, the refund `call` fails silently. The excess ETH (transportPayment - fees) is permanently stuck in the sucker. The contract emits `TransportPaymentRefundFailed` but has no sweep mechanism.
|
|
96
96
|
- **Unbounded sucker count per project.** `JBSuckerRegistry._suckersOf` uses an EnumerableMap with no cap. `suckerPairsOf` iterates all suckers with external calls per iteration. Extremely large sucker counts could cause view functions to exceed gas limits.
|
|
97
97
|
- **Unrestricted `receive()`.** Anyone can send ETH to the sucker, inflating `amountToAddToBalanceOf`. This is by design (needed for bridge/terminal returns) but means the project can receive unexpected balance additions.
|
|
@@ -127,6 +127,6 @@ The registry owner can adjust `toRemoteFee` via `JBSuckerRegistry.setToRemoteFee
|
|
|
127
127
|
|
|
128
128
|
The fee is paid to `FEE_PROJECT_ID` (the protocol project), not to the sucker's own `projectId()`. This centralizes fee collection, but it is still only best-effort: if the fee project's native terminal is missing or its `pay` call reverts, the fee ETH stays in the sucker contract and is later recoverable through the normal claim path. The sucker's project does not directly benefit from the anti-spam fee.
|
|
129
129
|
|
|
130
|
-
### 10.5 `mapTokens`
|
|
130
|
+
### 10.5 `mapTokens` refunds ETH on enable-only batches
|
|
131
131
|
|
|
132
|
-
`mapTokens()` only uses `msg.value` when one or more mappings are being disabled and need transport payment for the final root flush. If every mapping in the batch is enable-only
|
|
132
|
+
`mapTokens()` only uses `msg.value` when one or more mappings are being disabled and need transport payment for the final root flush. If every mapping in the batch is enable-only (`numberToDisable == 0`), the full `msg.value` is refunded to `_msgSender()`. If the refund transfer fails (e.g., the caller is a non-payable contract), the call reverts with `JBSucker_RefundFailed`. When disables are present, any dust remainder from integer division (`msg.value % numberToDisable`) is also refunded on a best-effort basis.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bananapus/suckers-v6",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.22",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"@arbitrum/nitro-contracts": "^1.2.1",
|
|
22
|
-
"@bananapus/core-v6": "^0.0.
|
|
22
|
+
"@bananapus/core-v6": "^0.0.32",
|
|
23
23
|
"@bananapus/permission-ids-v6": "^0.0.15",
|
|
24
24
|
"@chainlink/contracts-ccip": "^1.5.0",
|
|
25
25
|
"@chainlink/local": "github:smartcontractkit/chainlink-local",
|
package/src/JBArbitrumSucker.sol
CHANGED
|
@@ -190,6 +190,9 @@ contract JBArbitrumSucker is JBSucker, IJBArbitrumSucker {
|
|
|
190
190
|
function _toL1(address token, uint256 amount, bytes memory data, JBRemoteToken memory remoteToken) internal {
|
|
191
191
|
uint256 nativeValue;
|
|
192
192
|
|
|
193
|
+
// Cache peer address to avoid redundant calls.
|
|
194
|
+
address peerAddress = _toAddress(peer());
|
|
195
|
+
|
|
193
196
|
// If the token is an ERC-20, bridge it to the peer.
|
|
194
197
|
// If the amount is `0` then we do not need to bridge any ERC20.
|
|
195
198
|
if (token != JBConstants.NATIVE_TOKEN && amount != 0) {
|
|
@@ -200,7 +203,7 @@ contract JBArbitrumSucker is JBSucker, IJBArbitrumSucker {
|
|
|
200
203
|
// slither-disable-next-line calls-loop,unused-return
|
|
201
204
|
IArbL2GatewayRouter(address(GATEWAYROUTER))
|
|
202
205
|
.outboundTransfer({
|
|
203
|
-
l1Token: _toAddress(remoteToken.addr), to:
|
|
206
|
+
l1Token: _toAddress(remoteToken.addr), to: peerAddress, amount: amount, data: bytes("")
|
|
204
207
|
});
|
|
205
208
|
} else {
|
|
206
209
|
// Otherwise, the token is the native token, and the amount will be sent as `msg.value`.
|
|
@@ -209,9 +212,8 @@ contract JBArbitrumSucker is JBSucker, IJBArbitrumSucker {
|
|
|
209
212
|
|
|
210
213
|
// Send the message to the peer with the reclaimed ETH.
|
|
211
214
|
// Address `100` is the ArbSys precompile address.
|
|
212
|
-
// Convert bytes32 peer to address at the Arbitrum API boundary.
|
|
213
215
|
// slither-disable-next-line calls-loop,unused-return
|
|
214
|
-
ArbSys(address(100)).sendTxToL1{value: nativeValue}({destination:
|
|
216
|
+
ArbSys(address(100)).sendTxToL1{value: nativeValue}({destination: peerAddress, data: data});
|
|
215
217
|
}
|
|
216
218
|
|
|
217
219
|
/// @notice Bridge the `token` and data to the remote L2 chain.
|
package/src/JBCCIPSucker.sol
CHANGED
|
@@ -199,6 +199,11 @@ contract JBCCIPSucker is JBSucker, IAny2EVMMessageReceiver {
|
|
|
199
199
|
//*********************************************************************//
|
|
200
200
|
|
|
201
201
|
/// @notice Uses CCIP to send the root and assets over the bridge to the peer.
|
|
202
|
+
/// @dev CCIP transport payment refund failures emit a `TransportPaymentRefundFailed` event by design rather
|
|
203
|
+
/// than reverting. After `ccipSend` commits the bridge message and transfers tokens, reverting the transaction
|
|
204
|
+
/// would leave the CCIP message in-flight with no corresponding on-chain state update — the tokens would be
|
|
205
|
+
/// gone, the merkle root never processed, and the outbox inconsistent. Emitting an event preserves
|
|
206
|
+
/// observability while preventing a single failed refund from blocking the entire bridge operation.
|
|
202
207
|
/// @param transportPayment the amount of `msg.value` that is going to get paid for sending this message.
|
|
203
208
|
/// @param token The token to bridge the outbox tree for.
|
|
204
209
|
/// @param remoteToken Information about the remote token being bridged to.
|
package/src/JBCeloSucker.sol
CHANGED
|
@@ -76,7 +76,8 @@ contract JBCeloSucker is JBOptimismSucker {
|
|
|
76
76
|
/// and adds native ETH to the project's balance.
|
|
77
77
|
/// @param token The terminal token to add to the project's balance.
|
|
78
78
|
/// @param amount The amount of terminal tokens to add to the project's balance.
|
|
79
|
-
|
|
79
|
+
/// @param projectId The cached project ID to avoid redundant storage reads.
|
|
80
|
+
function _addToBalance(address token, uint256 amount, uint256 projectId) internal override {
|
|
80
81
|
if (token == address(WRAPPED_NATIVE)) {
|
|
81
82
|
// Check addable amount against WETH balance before unwrapping.
|
|
82
83
|
uint256 addableAmount = amountToAddToBalanceOf(token);
|
|
@@ -84,24 +85,22 @@ contract JBCeloSucker is JBOptimismSucker {
|
|
|
84
85
|
revert JBSucker_InsufficientBalance(amount, addableAmount);
|
|
85
86
|
}
|
|
86
87
|
|
|
87
|
-
uint256 _projectId = projectId();
|
|
88
|
-
|
|
89
88
|
// Unwrap WETH → native ETH.
|
|
90
89
|
// slither-disable-next-line calls-loop
|
|
91
90
|
WRAPPED_NATIVE.withdraw(amount);
|
|
92
91
|
|
|
93
92
|
// Get the project's primary terminal for native token.
|
|
94
93
|
// slither-disable-next-line calls-loop
|
|
95
|
-
IJBTerminal terminal = DIRECTORY.primaryTerminalOf({projectId:
|
|
94
|
+
IJBTerminal terminal = DIRECTORY.primaryTerminalOf({projectId: projectId, token: JBConstants.NATIVE_TOKEN});
|
|
96
95
|
|
|
97
96
|
if (address(terminal) == address(0)) {
|
|
98
|
-
revert JBSucker_NoTerminalForToken(
|
|
97
|
+
revert JBSucker_NoTerminalForToken(projectId, JBConstants.NATIVE_TOKEN);
|
|
99
98
|
}
|
|
100
99
|
|
|
101
100
|
// Add native ETH to the project's balance.
|
|
102
101
|
// slither-disable-next-line arbitrary-send-eth,calls-loop
|
|
103
102
|
terminal.addToBalanceOf{value: amount}({
|
|
104
|
-
projectId:
|
|
103
|
+
projectId: projectId,
|
|
105
104
|
token: JBConstants.NATIVE_TOKEN,
|
|
106
105
|
amount: amount,
|
|
107
106
|
shouldReturnHeldFees: false,
|
|
@@ -109,7 +108,7 @@ contract JBCeloSucker is JBOptimismSucker {
|
|
|
109
108
|
metadata: ""
|
|
110
109
|
});
|
|
111
110
|
} else {
|
|
112
|
-
super._addToBalance({token: token, amount: amount});
|
|
111
|
+
super._addToBalance({token: token, amount: amount, projectId: projectId});
|
|
113
112
|
}
|
|
114
113
|
}
|
|
115
114
|
|
|
@@ -139,42 +138,33 @@ contract JBCeloSucker is JBOptimismSucker {
|
|
|
139
138
|
revert JBSucker_UnexpectedMsgValue(transportPayment);
|
|
140
139
|
}
|
|
141
140
|
|
|
141
|
+
// Cache peer address to avoid redundant calls.
|
|
142
|
+
address peerAddress = _toAddress(peer());
|
|
143
|
+
|
|
142
144
|
if (amount != 0) {
|
|
145
|
+
// Determine the local token to bridge — native ETH is wrapped to WETH first.
|
|
146
|
+
address bridgeToken = token;
|
|
143
147
|
if (token == JBConstants.NATIVE_TOKEN) {
|
|
144
148
|
// Wrap native ETH → WETH so it can be bridged as ERC-20.
|
|
145
149
|
// slither-disable-next-line arbitrary-send-eth,calls-loop
|
|
146
150
|
WRAPPED_NATIVE.deposit{value: amount}();
|
|
147
|
-
|
|
148
|
-
// Approve the bridge to spend the WETH.
|
|
149
|
-
SafeERC20.forceApprove({
|
|
150
|
-
token: IERC20(address(WRAPPED_NATIVE)), spender: address(OPBRIDGE), value: amount
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
// Bridge WETH as ERC-20.
|
|
154
|
-
// slither-disable-next-line reentrancy-events,calls-loop
|
|
155
|
-
OPBRIDGE.bridgeERC20To({
|
|
156
|
-
localToken: address(WRAPPED_NATIVE),
|
|
157
|
-
remoteToken: _toAddress(remoteToken.addr),
|
|
158
|
-
to: _toAddress(peer()),
|
|
159
|
-
amount: amount,
|
|
160
|
-
minGasLimit: remoteToken.minGas,
|
|
161
|
-
extraData: bytes("")
|
|
162
|
-
});
|
|
163
|
-
} else {
|
|
164
|
-
// ERC-20 token — bridge directly.
|
|
165
|
-
// slither-disable-next-line reentrancy-events
|
|
166
|
-
SafeERC20.forceApprove({token: IERC20(token), spender: address(OPBRIDGE), value: amount});
|
|
167
|
-
|
|
168
|
-
// slither-disable-next-line reentrancy-events,calls-loop
|
|
169
|
-
OPBRIDGE.bridgeERC20To({
|
|
170
|
-
localToken: token,
|
|
171
|
-
remoteToken: _toAddress(remoteToken.addr),
|
|
172
|
-
to: _toAddress(peer()),
|
|
173
|
-
amount: amount,
|
|
174
|
-
minGasLimit: remoteToken.minGas,
|
|
175
|
-
extraData: bytes("")
|
|
176
|
-
});
|
|
151
|
+
bridgeToken = address(WRAPPED_NATIVE);
|
|
177
152
|
}
|
|
153
|
+
|
|
154
|
+
// Approve the bridge to spend the token.
|
|
155
|
+
// slither-disable-next-line reentrancy-events
|
|
156
|
+
SafeERC20.forceApprove({token: IERC20(bridgeToken), spender: address(OPBRIDGE), value: amount});
|
|
157
|
+
|
|
158
|
+
// Bridge the ERC-20 token to the peer.
|
|
159
|
+
// slither-disable-next-line reentrancy-events,calls-loop
|
|
160
|
+
OPBRIDGE.bridgeERC20To({
|
|
161
|
+
localToken: bridgeToken,
|
|
162
|
+
remoteToken: _toAddress(remoteToken.addr),
|
|
163
|
+
to: peerAddress,
|
|
164
|
+
amount: amount,
|
|
165
|
+
minGasLimit: remoteToken.minGas,
|
|
166
|
+
extraData: bytes("")
|
|
167
|
+
});
|
|
178
168
|
}
|
|
179
169
|
|
|
180
170
|
// Send the messenger message with nativeValue = 0.
|
|
@@ -182,7 +172,7 @@ contract JBCeloSucker is JBOptimismSucker {
|
|
|
182
172
|
// On L1, the ETH was already wrapped and bridged as ERC-20 above.
|
|
183
173
|
// slither-disable-next-line reentrancy-events,calls-loop
|
|
184
174
|
OPMESSENGER.sendMessage({
|
|
185
|
-
target:
|
|
175
|
+
target: peerAddress,
|
|
186
176
|
message: abi.encodeCall(JBSucker.fromRemote, (message)),
|
|
187
177
|
gasLimit: MESSENGER_BASE_GAS_LIMIT
|
|
188
178
|
});
|
package/src/JBOptimismSucker.sol
CHANGED
|
@@ -108,6 +108,9 @@ contract JBOptimismSucker is JBSucker, IJBOptimismSucker {
|
|
|
108
108
|
revert JBSucker_UnexpectedMsgValue(transportPayment);
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
// Cache peer address to avoid redundant calls.
|
|
112
|
+
address peerAddress = _toAddress(peer());
|
|
113
|
+
|
|
111
114
|
// If the token is an ERC20, bridge it to the peer.
|
|
112
115
|
// If the amount is `0` then we do not need to bridge any ERC20.
|
|
113
116
|
if (token != JBConstants.NATIVE_TOKEN && amount != 0) {
|
|
@@ -120,7 +123,7 @@ contract JBOptimismSucker is JBSucker, IJBOptimismSucker {
|
|
|
120
123
|
OPBRIDGE.bridgeERC20To({
|
|
121
124
|
localToken: token,
|
|
122
125
|
remoteToken: _toAddress(remoteToken.addr),
|
|
123
|
-
to:
|
|
126
|
+
to: peerAddress,
|
|
124
127
|
amount: amount,
|
|
125
128
|
minGasLimit: remoteToken.minGas,
|
|
126
129
|
extraData: bytes("")
|
|
@@ -131,10 +134,9 @@ contract JBOptimismSucker is JBSucker, IJBOptimismSucker {
|
|
|
131
134
|
}
|
|
132
135
|
|
|
133
136
|
// Send the message to the peer with the reclaimed ETH.
|
|
134
|
-
// Convert bytes32 peer to address at the OP Messenger API boundary.
|
|
135
137
|
// slither-disable-next-line arbitrary-send-eth,reentrancy-events,calls-loop
|
|
136
138
|
OPMESSENGER.sendMessage{value: nativeValue}({
|
|
137
|
-
target:
|
|
139
|
+
target: peerAddress,
|
|
138
140
|
message: abi.encodeCall(JBSucker.fromRemote, (message)),
|
|
139
141
|
gasLimit: MESSENGER_BASE_GAS_LIMIT
|
|
140
142
|
});
|
package/src/JBSucker.sol
CHANGED
|
@@ -65,6 +65,7 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
65
65
|
error JBSucker_NoTerminalForToken(uint256 projectId, address token);
|
|
66
66
|
error JBSucker_NotPeer(bytes32 caller);
|
|
67
67
|
error JBSucker_NothingToSend();
|
|
68
|
+
error JBSucker_RefundFailed();
|
|
68
69
|
error JBSucker_TokenAlreadyMapped(address localToken, bytes32 mappedTo);
|
|
69
70
|
error JBSucker_TokenHasInvalidEmergencyHatchState(address token);
|
|
70
71
|
error JBSucker_TokenNotMapped(address token);
|
|
@@ -302,10 +303,17 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
302
303
|
)
|
|
303
304
|
internal
|
|
304
305
|
pure
|
|
305
|
-
returns (bytes32)
|
|
306
|
+
returns (bytes32 hash)
|
|
306
307
|
{
|
|
308
|
+
// All three arguments are 32 bytes — hash from free memory to avoid abi.encode allocation overhead.
|
|
307
309
|
// forge-lint: disable-next-line(asm-keccak256)
|
|
308
|
-
|
|
310
|
+
assembly {
|
|
311
|
+
let ptr := mload(0x40)
|
|
312
|
+
mstore(ptr, projectTokenCount)
|
|
313
|
+
mstore(add(ptr, 0x20), terminalTokenAmount)
|
|
314
|
+
mstore(add(ptr, 0x40), beneficiary)
|
|
315
|
+
hash := keccak256(ptr, 0x60)
|
|
316
|
+
}
|
|
309
317
|
}
|
|
310
318
|
|
|
311
319
|
/// @dev ERC-2771 specifies the context as being a single address (20 bytes).
|
|
@@ -365,8 +373,11 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
365
373
|
/// claim).
|
|
366
374
|
function claim(JBClaim[] calldata claims) external override {
|
|
367
375
|
// Claim each.
|
|
368
|
-
for (uint256 i; i < claims.length;
|
|
376
|
+
for (uint256 i; i < claims.length;) {
|
|
369
377
|
claim(claims[i]);
|
|
378
|
+
unchecked {
|
|
379
|
+
++i;
|
|
380
|
+
}
|
|
370
381
|
}
|
|
371
382
|
}
|
|
372
383
|
|
|
@@ -416,10 +427,13 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
416
427
|
});
|
|
417
428
|
|
|
418
429
|
// Enable the emergency hatch for each token.
|
|
419
|
-
for (uint256 i; i < tokens.length;
|
|
430
|
+
for (uint256 i; i < tokens.length;) {
|
|
420
431
|
// We have an invariant where if emergencyHatch is true, enabled should be false.
|
|
421
432
|
_remoteTokenFor[tokens[i]].enabled = false;
|
|
422
433
|
_remoteTokenFor[tokens[i]].emergencyHatch = true;
|
|
434
|
+
unchecked {
|
|
435
|
+
++i;
|
|
436
|
+
}
|
|
423
437
|
}
|
|
424
438
|
|
|
425
439
|
emit EmergencyHatchOpened(tokens, _msgSender());
|
|
@@ -543,23 +557,34 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
543
557
|
|
|
544
558
|
// Loop over the number of mappings and increase numberToDisable to correctly set transportPaymentValue.
|
|
545
559
|
// Note: if all mappings are enable-only (no disables), `numberToDisable` stays 0 and `transportPaymentValue`
|
|
546
|
-
// is set to 0 for each call. Any ETH sent with the transaction is
|
|
547
|
-
|
|
548
|
-
for (uint256 h; h < maps.length; h++) {
|
|
560
|
+
// is set to 0 for each call. Any ETH sent with the transaction is refunded after the second loop.
|
|
561
|
+
for (uint256 h; h < maps.length;) {
|
|
549
562
|
JBOutboxTree storage _outbox = _outboxOf[maps[h].localToken];
|
|
550
563
|
if (maps[h].remoteToken == bytes32(0) && _outbox.numberOfClaimsSent != _outbox.tree.count) {
|
|
551
564
|
numberToDisable++;
|
|
552
565
|
}
|
|
566
|
+
unchecked {
|
|
567
|
+
++h;
|
|
568
|
+
}
|
|
553
569
|
}
|
|
554
570
|
|
|
555
571
|
// Perform each token mapping.
|
|
556
|
-
for (uint256 i; i < maps.length;
|
|
572
|
+
for (uint256 i; i < maps.length;) {
|
|
557
573
|
// slither-disable-next-line msg-value-loop
|
|
558
574
|
_mapToken({map: maps[i], transportPaymentValue: numberToDisable > 0 ? msg.value / numberToDisable : 0});
|
|
575
|
+
unchecked {
|
|
576
|
+
++i;
|
|
577
|
+
}
|
|
559
578
|
}
|
|
560
579
|
|
|
561
|
-
//
|
|
562
|
-
if (numberToDisable
|
|
580
|
+
// If no tokens were disabled, the full `msg.value` is unused — refund it.
|
|
581
|
+
if (numberToDisable == 0) {
|
|
582
|
+
if (msg.value > 0) {
|
|
583
|
+
(bool _ok,) = _msgSender().call{value: msg.value}("");
|
|
584
|
+
if (!_ok) revert JBSucker_RefundFailed();
|
|
585
|
+
}
|
|
586
|
+
} else {
|
|
587
|
+
// Refund any remainder from integer division so dust wei isn't stuck in the contract.
|
|
563
588
|
uint256 remainder = msg.value % numberToDisable;
|
|
564
589
|
if (remainder > 0) {
|
|
565
590
|
// Best-effort refund — don't revert if caller can't accept ETH.
|
|
@@ -681,6 +706,11 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
681
706
|
/// (not added back to `transportPayment`) to avoid reverting the entire transaction. This preserves
|
|
682
707
|
/// `transportPayment = msg.value - fee`, which is critical for zero-cost bridges (OP, Base, Celo, Arb L2->L1)
|
|
683
708
|
/// that revert on non-zero transport payment. The fee amount is typically small (max 0.001 ETH).
|
|
709
|
+
/// @dev Retained fee ETH is absorbed by future native token claims. Because `amountToAddToBalanceOf` computes
|
|
710
|
+
/// `_balanceOf(token, address(this)) - _outboxOf[token].balance`, any extra ETH in the contract (including
|
|
711
|
+
/// retained fees) increases the claimable amount and will be forwarded to the project's terminal via
|
|
712
|
+
/// `_addToBalance` when the next native token claim is processed. This is by design — reverting on fee
|
|
713
|
+
/// failure would block all bridging.
|
|
684
714
|
/// @param token The terminal token being bridged.
|
|
685
715
|
function toRemote(address token) external payable override {
|
|
686
716
|
JBRemoteToken memory remoteToken = _remoteTokenFor[token];
|
|
@@ -731,7 +761,7 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
731
761
|
}
|
|
732
762
|
}
|
|
733
763
|
// If no terminal exists, fee ETH stays in this contract. transportPayment is already correct.
|
|
734
|
-
// This retained ETH is
|
|
764
|
+
// This retained ETH is absorbed by future native token claims via `amountToAddToBalanceOf`.
|
|
735
765
|
|
|
736
766
|
// Send the merkle root to the remote chain.
|
|
737
767
|
_sendRoot({transportPayment: transportPayment, token: token, remoteToken: remoteToken});
|
|
@@ -759,22 +789,20 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
759
789
|
/// @notice Adds funds to the projects balance.
|
|
760
790
|
/// @param token The terminal token to add to the project's balance.
|
|
761
791
|
/// @param amount The amount of terminal tokens to add to the project's balance.
|
|
762
|
-
|
|
792
|
+
/// @param projectId The cached project ID to avoid redundant storage reads.
|
|
793
|
+
function _addToBalance(address token, uint256 amount, uint256 projectId) internal virtual {
|
|
763
794
|
// Make sure that the current `amountToAddToBalance` is greater than or equal to the amount being added.
|
|
764
795
|
uint256 addableAmount = amountToAddToBalanceOf(token);
|
|
765
796
|
if (amount > addableAmount) {
|
|
766
797
|
revert JBSucker_InsufficientBalance(amount, addableAmount);
|
|
767
798
|
}
|
|
768
799
|
|
|
769
|
-
uint256 _projectId = projectId();
|
|
770
|
-
|
|
771
800
|
// Get the project's primary terminal for the token.
|
|
772
|
-
// slither
|
|
773
801
|
// slither-disable-next-line calls-loop
|
|
774
|
-
IJBTerminal terminal = DIRECTORY.primaryTerminalOf({projectId:
|
|
802
|
+
IJBTerminal terminal = DIRECTORY.primaryTerminalOf({projectId: projectId, token: token});
|
|
775
803
|
|
|
776
804
|
// slither-disable-next-line incorrect-equality
|
|
777
|
-
if (address(terminal) == address(0)) revert JBSucker_NoTerminalForToken(
|
|
805
|
+
if (address(terminal) == address(0)) revert JBSucker_NoTerminalForToken(projectId, token);
|
|
778
806
|
|
|
779
807
|
// Perform the `addToBalance`.
|
|
780
808
|
if (token != JBConstants.NATIVE_TOKEN) {
|
|
@@ -785,7 +813,7 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
785
813
|
|
|
786
814
|
// slither-disable-next-line calls-loop
|
|
787
815
|
terminal.addToBalanceOf({
|
|
788
|
-
projectId:
|
|
816
|
+
projectId: projectId, token: token, amount: amount, shouldReturnHeldFees: false, memo: "", metadata: ""
|
|
789
817
|
});
|
|
790
818
|
|
|
791
819
|
// Sanity check: make sure we transfer the full amount.
|
|
@@ -795,7 +823,7 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
795
823
|
// If the token is the native token, use `msg.value`.
|
|
796
824
|
// slither-disable-next-line arbitrary-send-eth,calls-loop
|
|
797
825
|
terminal.addToBalanceOf{value: amount}({
|
|
798
|
-
projectId:
|
|
826
|
+
projectId: projectId, token: token, amount: amount, shouldReturnHeldFees: false, memo: "", metadata: ""
|
|
799
827
|
});
|
|
800
828
|
}
|
|
801
829
|
}
|
|
@@ -813,13 +841,13 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
813
841
|
)
|
|
814
842
|
internal
|
|
815
843
|
{
|
|
844
|
+
uint256 cachedProjectId = projectId();
|
|
845
|
+
|
|
816
846
|
// Add the cashed out funds to the project's balance.
|
|
817
847
|
if (terminalTokenAmount != 0) {
|
|
818
|
-
_addToBalance({token: terminalToken, amount: terminalTokenAmount});
|
|
848
|
+
_addToBalance({token: terminalToken, amount: terminalTokenAmount, projectId: cachedProjectId});
|
|
819
849
|
}
|
|
820
850
|
|
|
821
|
-
uint256 _projectId = projectId();
|
|
822
|
-
|
|
823
851
|
// Cast the bytes32 beneficiary to an EVM address for the local mint.
|
|
824
852
|
address beneficiaryAddress = _toAddress(beneficiary);
|
|
825
853
|
|
|
@@ -830,9 +858,9 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
830
858
|
//
|
|
831
859
|
// Mint the project tokens for the beneficiary.
|
|
832
860
|
// slither-disable-next-line calls-loop,unused-return
|
|
833
|
-
IJBController(address(DIRECTORY.controllerOf(
|
|
861
|
+
IJBController(address(DIRECTORY.controllerOf(cachedProjectId)))
|
|
834
862
|
.mintTokensOf({
|
|
835
|
-
projectId:
|
|
863
|
+
projectId: cachedProjectId,
|
|
836
864
|
tokenCount: projectTokenAmount,
|
|
837
865
|
beneficiary: beneficiaryAddress,
|
|
838
866
|
memo: "",
|
|
@@ -865,18 +893,15 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
865
893
|
// Get the outbox in storage.
|
|
866
894
|
JBOutboxTree storage outbox = _outboxOf[token];
|
|
867
895
|
|
|
868
|
-
//
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
// Update the outbox tree and balance for the terminal token.
|
|
872
|
-
outbox.tree = tree;
|
|
896
|
+
// Insert the hash directly into the storage-backed tree — writes only the changed branch slot and count.
|
|
897
|
+
outbox.tree.insert(hashed);
|
|
873
898
|
outbox.balance += terminalTokenAmount;
|
|
874
899
|
|
|
875
900
|
emit InsertToOutboxTree({
|
|
876
901
|
beneficiary: beneficiary,
|
|
877
902
|
token: token,
|
|
878
903
|
hashed: hashed,
|
|
879
|
-
index: tree.count - 1, // Subtract 1 since we want the 0-based index.
|
|
904
|
+
index: outbox.tree.count - 1, // Subtract 1 since we want the 0-based index.
|
|
880
905
|
root: outbox.tree.root(),
|
|
881
906
|
projectTokenCount: projectTokenCount,
|
|
882
907
|
terminalTokenAmount: terminalTokenAmount,
|
|
@@ -1051,7 +1076,7 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
1051
1076
|
|
|
1052
1077
|
// Update the numberOfClaimsSent to the current count of the tree.
|
|
1053
1078
|
// This is used as in the fallback to allow users to withdraw locally if the bridge is reverting.
|
|
1054
|
-
outbox.numberOfClaimsSent = count;
|
|
1079
|
+
outbox.numberOfClaimsSent = uint192(count);
|
|
1055
1080
|
uint256 index = count - 1;
|
|
1056
1081
|
|
|
1057
1082
|
// Emit an event for the relayers to watch for.
|
package/src/JBSuckerRegistry.sol
CHANGED
|
@@ -120,13 +120,16 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
|
|
|
120
120
|
pairs = new JBSuckersPair[](suckers.length);
|
|
121
121
|
|
|
122
122
|
// Populate the array of pairs.
|
|
123
|
-
for (uint256 i; i < suckers.length;
|
|
123
|
+
for (uint256 i; i < suckers.length;) {
|
|
124
124
|
// Get the sucker being iterated over.
|
|
125
125
|
IJBSucker sucker = IJBSucker(suckers[i]);
|
|
126
126
|
|
|
127
127
|
// slither-disable-next-line calls-loop
|
|
128
128
|
pairs[i] =
|
|
129
129
|
JBSuckersPair({local: address(sucker), remote: sucker.peer(), remoteChainId: sucker.peerChainId()});
|
|
130
|
+
unchecked {
|
|
131
|
+
++i;
|
|
132
|
+
}
|
|
130
133
|
}
|
|
131
134
|
}
|
|
132
135
|
|
|
@@ -174,14 +177,20 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
|
|
|
174
177
|
/// @dev Can only be called by this contract's owner (initially project ID 1, or JuiceboxDAO).
|
|
175
178
|
/// @param deployers The address of the deployer to add.
|
|
176
179
|
function allowSuckerDeployers(address[] calldata deployers) public override onlyOwner {
|
|
180
|
+
// Cache _msgSender() to avoid redundant calls in the loop.
|
|
181
|
+
address sender = _msgSender();
|
|
182
|
+
|
|
177
183
|
// Iterate through the deployers and allow them.
|
|
178
|
-
for (uint256 i; i < deployers.length;
|
|
184
|
+
for (uint256 i; i < deployers.length;) {
|
|
179
185
|
// Get the deployer being iterated over.
|
|
180
186
|
address deployer = deployers[i];
|
|
181
187
|
|
|
182
188
|
// Allow the deployer.
|
|
183
189
|
suckerDeployerIsAllowed[deployer] = true;
|
|
184
|
-
emit SuckerDeployerAllowed({deployer: deployer, caller:
|
|
190
|
+
emit SuckerDeployerAllowed({deployer: deployer, caller: sender});
|
|
191
|
+
unchecked {
|
|
192
|
+
++i;
|
|
193
|
+
}
|
|
185
194
|
}
|
|
186
195
|
}
|
|
187
196
|
|
|
@@ -210,14 +219,17 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
|
|
|
210
219
|
// Create an array to store the suckers as they are deployed.
|
|
211
220
|
suckers = new address[](configurations.length);
|
|
212
221
|
|
|
222
|
+
// Cache _msgSender() to avoid redundant calls in the loop.
|
|
223
|
+
address sender = _msgSender();
|
|
224
|
+
|
|
213
225
|
// Calculate the salt using the sender's address and the provided `salt`.
|
|
214
226
|
// This is an intentional part of the same-address peer invariant: if projects deploy suckers from
|
|
215
227
|
// different sender addresses on different chains, the resulting sucker addresses will differ and the
|
|
216
228
|
// default peer symmetry assumption will not hold.
|
|
217
|
-
salt = keccak256(abi.encode(
|
|
229
|
+
salt = keccak256(abi.encode(sender, salt));
|
|
218
230
|
|
|
219
231
|
// Iterate through the configurations and deploy the suckers.
|
|
220
|
-
for (uint256 i; i < configurations.length;
|
|
232
|
+
for (uint256 i; i < configurations.length;) {
|
|
221
233
|
// Get the configuration being iterated over.
|
|
222
234
|
JBSuckerDeployerConfig memory configuration = configurations[i];
|
|
223
235
|
|
|
@@ -239,8 +251,11 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
|
|
|
239
251
|
// slither-disable-next-line reentrancy-events,calls-loop
|
|
240
252
|
sucker.mapTokens(configuration.mappings);
|
|
241
253
|
emit SuckerDeployedFor({
|
|
242
|
-
projectId: projectId, sucker: address(sucker), configuration: configuration, caller:
|
|
254
|
+
projectId: projectId, sucker: address(sucker), configuration: configuration, caller: sender
|
|
243
255
|
});
|
|
256
|
+
unchecked {
|
|
257
|
+
++i;
|
|
258
|
+
}
|
|
244
259
|
}
|
|
245
260
|
}
|
|
246
261
|
|
|
@@ -6,14 +6,14 @@ import {MerkleLib} from "../utils/MerkleLib.sol";
|
|
|
6
6
|
/// @notice A merkle tree used to track the outbox for a given token in a `JBSucker`.
|
|
7
7
|
/// @dev The outbox is used to send from the local chain to the remote chain.
|
|
8
8
|
/// @custom:member nonce The nonce of the outbox.
|
|
9
|
+
/// @custom:member numberOfClaimsSent the number of claims that have been sent to the peer. Used to determine which
|
|
10
|
+
/// claims have been sent. Packed with `nonce` into one storage slot.
|
|
9
11
|
/// @custom:member balance The balance of the outbox.
|
|
10
12
|
/// @custom:member tree The merkle tree.
|
|
11
|
-
/// @custom:member numberOfClaimsSent the number of claims that have been sent to the peer. Used to determine which
|
|
12
|
-
/// claims have been sent.
|
|
13
13
|
// forge-lint: disable-next-line(pascal-case-struct)
|
|
14
14
|
struct JBOutboxTree {
|
|
15
15
|
uint64 nonce;
|
|
16
|
+
uint192 numberOfClaimsSent;
|
|
16
17
|
uint256 balance;
|
|
17
18
|
MerkleLib.Tree tree;
|
|
18
|
-
uint256 numberOfClaimsSent;
|
|
19
19
|
}
|
package/src/utils/MerkleLib.sol
CHANGED
|
@@ -80,14 +80,15 @@ library MerkleLib {
|
|
|
80
80
|
//*********************************************************************//
|
|
81
81
|
|
|
82
82
|
/**
|
|
83
|
-
* @notice Inserts a given node (leaf) into merkle tree
|
|
84
|
-
*
|
|
83
|
+
* @notice Inserts a given node (leaf) into the merkle tree in storage.
|
|
84
|
+
* @dev Operates directly on storage, writing only the single branch entry that changes (plus count).
|
|
85
|
+
* This avoids the 33-slot memory round-trip of the previous memory-based approach.
|
|
85
86
|
* @dev Reverts if the tree is already full.
|
|
87
|
+
* @param tree The storage reference to the tree.
|
|
86
88
|
* @param node Element to insert into tree.
|
|
87
|
-
* @return Tree Updated tree.
|
|
88
89
|
*
|
|
89
90
|
*/
|
|
90
|
-
function insert(Tree
|
|
91
|
+
function insert(Tree storage tree, bytes32 node) internal {
|
|
91
92
|
// Update tree.count to increase the current count by 1 since we'll be including a new node.
|
|
92
93
|
uint256 size = ++tree.count;
|
|
93
94
|
if (size > MAX_LEAVES) revert MerkleLib_InsertTreeIsFull();
|
|
@@ -101,10 +102,16 @@ library MerkleLib {
|
|
|
101
102
|
// If i > 0, then this node will be a hash of the original node with every layer up
|
|
102
103
|
// until layer `i`.
|
|
103
104
|
tree.branch[i] = node;
|
|
104
|
-
return
|
|
105
|
+
return;
|
|
105
106
|
}
|
|
106
107
|
// If the size is not yet odd, we hash the current index in the tree branch with the node.
|
|
107
|
-
|
|
108
|
+
// Use assembly to hash directly from scratch space, avoiding abi.encodePacked allocation.
|
|
109
|
+
bytes32 branchVal = tree.branch[i];
|
|
110
|
+
assembly {
|
|
111
|
+
mstore(0x00, branchVal)
|
|
112
|
+
mstore(0x20, node)
|
|
113
|
+
node := keccak256(0x00, 0x40)
|
|
114
|
+
}
|
|
108
115
|
size >>= 1; // Cut size in half (statement equivalent to: `size /= 2`).
|
|
109
116
|
|
|
110
117
|
unchecked {
|
|
@@ -147,7 +147,7 @@ contract DeepAttackSucker is JBSucker {
|
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
function test_setNumberOfClaimsSent(address token, uint256 count) external {
|
|
150
|
-
_outboxOf[token].numberOfClaimsSent = count;
|
|
150
|
+
_outboxOf[token].numberOfClaimsSent = uint192(count);
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
function test_getInboxRoot(address token) external view returns (bytes32) {
|
package/test/TestAuditGaps.sol
CHANGED
|
@@ -139,7 +139,7 @@ contract AuditGapSucker is JBSucker {
|
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
function test_setNumberOfClaimsSent(address token, uint256 count) external {
|
|
142
|
-
_outboxOf[token].numberOfClaimsSent = count;
|
|
142
|
+
_outboxOf[token].numberOfClaimsSent = uint192(count);
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
function test_getNumberOfClaimsSent(address token) external view returns (uint256) {
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.28;
|
|
3
|
+
|
|
4
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
5
|
+
import "forge-std/Test.sol";
|
|
6
|
+
|
|
7
|
+
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
8
|
+
import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
|
|
9
|
+
import {IJBTokens} from "@bananapus/core-v6/src/interfaces/IJBTokens.sol";
|
|
10
|
+
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
|
|
11
|
+
import {LibClone} from "solady/src/utils/LibClone.sol";
|
|
12
|
+
|
|
13
|
+
import "../../src/JBSucker.sol";
|
|
14
|
+
import "../../src/interfaces/IJBSuckerRegistry.sol";
|
|
15
|
+
import "../../src/structs/JBMessageRoot.sol";
|
|
16
|
+
import "../../src/structs/JBRemoteToken.sol";
|
|
17
|
+
import "../../src/structs/JBTokenMapping.sol";
|
|
18
|
+
|
|
19
|
+
contract CodexMapTokensHarness is JBSucker {
|
|
20
|
+
constructor(
|
|
21
|
+
IJBDirectory directory,
|
|
22
|
+
IJBPermissions permissions,
|
|
23
|
+
IJBTokens tokens
|
|
24
|
+
)
|
|
25
|
+
JBSucker(directory, permissions, tokens, 1, IJBSuckerRegistry(address(1)), address(0))
|
|
26
|
+
{}
|
|
27
|
+
|
|
28
|
+
function peerChainId() external view override returns (uint256) {
|
|
29
|
+
return block.chainid;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function _isRemotePeer(address sender) internal view override returns (bool) {
|
|
33
|
+
return sender == _toAddress(peer());
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function _sendRootOverAMB(
|
|
37
|
+
uint256,
|
|
38
|
+
uint256,
|
|
39
|
+
address,
|
|
40
|
+
uint256,
|
|
41
|
+
JBRemoteToken memory,
|
|
42
|
+
JBMessageRoot memory
|
|
43
|
+
)
|
|
44
|
+
internal
|
|
45
|
+
override
|
|
46
|
+
{}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
contract CodexMapTokensEnableOnlyValueStuckTest is Test {
|
|
50
|
+
address internal constant DIRECTORY = address(0x1000);
|
|
51
|
+
address internal constant PERMISSIONS = address(0x2000);
|
|
52
|
+
address internal constant TOKENS = address(0x3000);
|
|
53
|
+
address internal constant PROJECT = address(0x4000);
|
|
54
|
+
|
|
55
|
+
uint256 internal constant PROJECT_ID = 1;
|
|
56
|
+
|
|
57
|
+
function test_mapTokensEnableOnlyBatchRefundsMsgValue() external {
|
|
58
|
+
vm.mockCall(DIRECTORY, abi.encodeCall(IJBDirectory.PROJECTS, ()), abi.encode(PROJECT));
|
|
59
|
+
vm.mockCall(PROJECT, abi.encodeCall(IERC721.ownerOf, (PROJECT_ID)), abi.encode(address(this)));
|
|
60
|
+
vm.mockCall(PERMISSIONS, abi.encodeWithSelector(IJBPermissions.hasPermission.selector), abi.encode(true));
|
|
61
|
+
|
|
62
|
+
CodexMapTokensHarness singleton =
|
|
63
|
+
new CodexMapTokensHarness(IJBDirectory(DIRECTORY), IJBPermissions(PERMISSIONS), IJBTokens(TOKENS));
|
|
64
|
+
CodexMapTokensHarness sucker = CodexMapTokensHarness(
|
|
65
|
+
payable(address(LibClone.cloneDeterministic(address(singleton), bytes32("codex-enable-only-msgvalue"))))
|
|
66
|
+
);
|
|
67
|
+
sucker.initialize(PROJECT_ID);
|
|
68
|
+
|
|
69
|
+
JBTokenMapping[] memory maps = new JBTokenMapping[](1);
|
|
70
|
+
maps[0] = JBTokenMapping({
|
|
71
|
+
localToken: address(0xBEEF),
|
|
72
|
+
minGas: sucker.MESSENGER_ERC20_MIN_GAS_LIMIT(),
|
|
73
|
+
remoteToken: bytes32(uint256(uint160(address(0xCAFE))))
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
uint256 balanceBefore = address(this).balance;
|
|
77
|
+
sucker.mapTokens{value: 1 ether}(maps);
|
|
78
|
+
|
|
79
|
+
assertEq(address(sucker).balance, 0, "enable-only msg.value should not stay in the sucker");
|
|
80
|
+
assertEq(address(this).balance, balanceBefore, "enable-only msg.value should be refunded to caller");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
receive() external payable {}
|
|
84
|
+
}
|
|
@@ -314,5 +314,5 @@ contract TestSucker is JBSucker {
|
|
|
314
314
|
|
|
315
315
|
/// @dev Override _addToBalance to be a no-op for fuzz testing.
|
|
316
316
|
/// These tests focus on emergency exit state machine behavior, not token balance mechanics.
|
|
317
|
-
function _addToBalance(address, uint256) internal override {}
|
|
317
|
+
function _addToBalance(address, uint256, uint256) internal override {}
|
|
318
318
|
}
|
|
@@ -133,7 +133,7 @@ contract InvariantSucker is JBSucker {
|
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
function test_setNumberOfClaimsSent(address token, uint256 count) external {
|
|
136
|
-
_outboxOf[token].numberOfClaimsSent = count;
|
|
136
|
+
_outboxOf[token].numberOfClaimsSent = uint192(count);
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
function test_setDeprecatedAfter(uint256 timestamp) external {
|
package/test/unit/merkle.t.sol
CHANGED
|
@@ -174,7 +174,7 @@ contract MerkleUnitTest is JBSucker, Test {
|
|
|
174
174
|
|
|
175
175
|
/// @dev Override _addToBalance to be a no-op for merkle proof testing.
|
|
176
176
|
/// These tests focus on merkle tree proof validation, not token balance mechanics.
|
|
177
|
-
function _addToBalance(address, uint256) internal override {}
|
|
177
|
+
function _addToBalance(address, uint256, uint256) internal override {}
|
|
178
178
|
|
|
179
179
|
function _isRemotePeer(address) internal view virtual override returns (bool valid) {
|
|
180
180
|
return false;
|