@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.
- package/LICENSE +21 -0
- package/README.md +422 -0
- package/SECURITY.md +55 -0
- package/SKILLS.md +163 -0
- package/deployments/nana-suckers-v5/arbitrum/JBArbitrumSucker.json +1425 -0
- package/deployments/nana-suckers-v5/arbitrum/JBArbitrumSuckerDeployer.json +391 -0
- package/deployments/nana-suckers-v5/arbitrum/JBCCIPSucker.json +1479 -0
- package/deployments/nana-suckers-v5/arbitrum/JBCCIPSuckerDeployer.json +433 -0
- package/deployments/nana-suckers-v5/arbitrum/JBCCIPSuckerDeployer_1.json +433 -0
- package/deployments/nana-suckers-v5/arbitrum/JBCCIPSuckerDeployer_2.json +433 -0
- package/deployments/nana-suckers-v5/arbitrum/JBCCIPSucker_1.json +1479 -0
- package/deployments/nana-suckers-v5/arbitrum/JBCCIPSucker_2.json +1479 -0
- package/deployments/nana-suckers-v5/arbitrum/JBSuckerRegistry.json +690 -0
- package/deployments/nana-suckers-v5/arbitrum_sepolia/JBArbitrumSucker.json +1425 -0
- package/deployments/nana-suckers-v5/arbitrum_sepolia/JBArbitrumSuckerDeployer.json +391 -0
- package/deployments/nana-suckers-v5/arbitrum_sepolia/JBCCIPSucker.json +1479 -0
- package/deployments/nana-suckers-v5/arbitrum_sepolia/JBCCIPSuckerDeployer.json +433 -0
- package/deployments/nana-suckers-v5/arbitrum_sepolia/JBCCIPSuckerDeployer_1.json +433 -0
- package/deployments/nana-suckers-v5/arbitrum_sepolia/JBCCIPSuckerDeployer_2.json +433 -0
- package/deployments/nana-suckers-v5/arbitrum_sepolia/JBCCIPSucker_1.json +1479 -0
- package/deployments/nana-suckers-v5/arbitrum_sepolia/JBCCIPSucker_2.json +1479 -0
- package/deployments/nana-suckers-v5/arbitrum_sepolia/JBSuckerRegistry.json +690 -0
- package/deployments/nana-suckers-v5/base/JBBaseSucker.json +1389 -0
- package/deployments/nana-suckers-v5/base/JBBaseSuckerDeployer.json +376 -0
- package/deployments/nana-suckers-v5/base/JBCCIPSucker.json +1483 -0
- package/deployments/nana-suckers-v5/base/JBCCIPSuckerDeployer.json +436 -0
- package/deployments/nana-suckers-v5/base/JBCCIPSuckerDeployer_1.json +436 -0
- package/deployments/nana-suckers-v5/base/JBCCIPSuckerDeployer_2.json +436 -0
- package/deployments/nana-suckers-v5/base/JBCCIPSucker_1.json +1483 -0
- package/deployments/nana-suckers-v5/base/JBCCIPSucker_2.json +1483 -0
- package/deployments/nana-suckers-v5/base/JBSuckerRegistry.json +694 -0
- package/deployments/nana-suckers-v5/base_sepolia/JBBaseSucker.json +1389 -0
- package/deployments/nana-suckers-v5/base_sepolia/JBBaseSuckerDeployer.json +376 -0
- package/deployments/nana-suckers-v5/base_sepolia/JBCCIPSucker.json +1483 -0
- package/deployments/nana-suckers-v5/base_sepolia/JBCCIPSuckerDeployer.json +436 -0
- package/deployments/nana-suckers-v5/base_sepolia/JBCCIPSuckerDeployer_1.json +436 -0
- package/deployments/nana-suckers-v5/base_sepolia/JBCCIPSuckerDeployer_2.json +436 -0
- package/deployments/nana-suckers-v5/base_sepolia/JBCCIPSucker_1.json +1483 -0
- package/deployments/nana-suckers-v5/base_sepolia/JBCCIPSucker_2.json +1483 -0
- package/deployments/nana-suckers-v5/base_sepolia/JBSuckerRegistry.json +694 -0
- package/deployments/nana-suckers-v5/ethereum/JBArbitrumSucker.json +1429 -0
- package/deployments/nana-suckers-v5/ethereum/JBArbitrumSuckerDeployer.json +394 -0
- package/deployments/nana-suckers-v5/ethereum/JBBaseSucker.json +1389 -0
- package/deployments/nana-suckers-v5/ethereum/JBBaseSuckerDeployer.json +376 -0
- package/deployments/nana-suckers-v5/ethereum/JBCCIPSucker.json +1483 -0
- package/deployments/nana-suckers-v5/ethereum/JBCCIPSuckerDeployer.json +436 -0
- package/deployments/nana-suckers-v5/ethereum/JBCCIPSuckerDeployer_1.json +436 -0
- package/deployments/nana-suckers-v5/ethereum/JBCCIPSuckerDeployer_2.json +436 -0
- package/deployments/nana-suckers-v5/ethereum/JBCCIPSucker_1.json +1483 -0
- package/deployments/nana-suckers-v5/ethereum/JBCCIPSucker_2.json +1483 -0
- package/deployments/nana-suckers-v5/ethereum/JBOptimismSucker.json +1389 -0
- package/deployments/nana-suckers-v5/ethereum/JBOptimismSuckerDeployer.json +376 -0
- package/deployments/nana-suckers-v5/ethereum/JBSuckerRegistry.json +694 -0
- package/deployments/nana-suckers-v5/optimism/JBCCIPSucker.json +1479 -0
- package/deployments/nana-suckers-v5/optimism/JBCCIPSuckerDeployer.json +433 -0
- package/deployments/nana-suckers-v5/optimism/JBCCIPSuckerDeployer_1.json +433 -0
- package/deployments/nana-suckers-v5/optimism/JBCCIPSuckerDeployer_2.json +433 -0
- package/deployments/nana-suckers-v5/optimism/JBCCIPSucker_1.json +1479 -0
- package/deployments/nana-suckers-v5/optimism/JBCCIPSucker_2.json +1479 -0
- package/deployments/nana-suckers-v5/optimism/JBOptimismSucker.json +1385 -0
- package/deployments/nana-suckers-v5/optimism/JBOptimismSuckerDeployer.json +373 -0
- package/deployments/nana-suckers-v5/optimism/JBSuckerRegistry.json +690 -0
- package/deployments/nana-suckers-v5/optimism_sepolia/JBCCIPSucker.json +1483 -0
- package/deployments/nana-suckers-v5/optimism_sepolia/JBCCIPSuckerDeployer.json +436 -0
- package/deployments/nana-suckers-v5/optimism_sepolia/JBCCIPSuckerDeployer_1.json +436 -0
- package/deployments/nana-suckers-v5/optimism_sepolia/JBCCIPSuckerDeployer_2.json +436 -0
- package/deployments/nana-suckers-v5/optimism_sepolia/JBCCIPSucker_1.json +1483 -0
- package/deployments/nana-suckers-v5/optimism_sepolia/JBCCIPSucker_2.json +1483 -0
- package/deployments/nana-suckers-v5/optimism_sepolia/JBOptimismSucker.json +1389 -0
- package/deployments/nana-suckers-v5/optimism_sepolia/JBOptimismSuckerDeployer.json +376 -0
- package/deployments/nana-suckers-v5/optimism_sepolia/JBSuckerRegistry.json +694 -0
- package/deployments/nana-suckers-v5/sepolia/JBArbitrumSucker.json +1429 -0
- package/deployments/nana-suckers-v5/sepolia/JBArbitrumSuckerDeployer.json +394 -0
- package/deployments/nana-suckers-v5/sepolia/JBBaseSucker.json +1389 -0
- package/deployments/nana-suckers-v5/sepolia/JBBaseSuckerDeployer.json +376 -0
- package/deployments/nana-suckers-v5/sepolia/JBCCIPSucker.json +1483 -0
- package/deployments/nana-suckers-v5/sepolia/JBCCIPSuckerDeployer.json +436 -0
- package/deployments/nana-suckers-v5/sepolia/JBCCIPSuckerDeployer_1.json +436 -0
- package/deployments/nana-suckers-v5/sepolia/JBCCIPSuckerDeployer_2.json +436 -0
- package/deployments/nana-suckers-v5/sepolia/JBCCIPSucker_1.json +1483 -0
- package/deployments/nana-suckers-v5/sepolia/JBCCIPSucker_2.json +1483 -0
- package/deployments/nana-suckers-v5/sepolia/JBOptimismSucker.json +1389 -0
- package/deployments/nana-suckers-v5/sepolia/JBOptimismSuckerDeployer.json +376 -0
- package/deployments/nana-suckers-v5/sepolia/JBSuckerRegistry.json +694 -0
- package/foundry.lock +11 -0
- package/foundry.toml +22 -0
- package/package.json +33 -0
- package/remappings.txt +1 -0
- package/script/Deploy.s.sol +506 -0
- package/script/helpers/SuckerDeploymentLib.sol +97 -0
- package/slither-ci.config.json +10 -0
- package/sphinx.lock +476 -0
- package/src/JBArbitrumSucker.sol +311 -0
- package/src/JBBaseSucker.sol +41 -0
- package/src/JBCCIPSucker.sol +303 -0
- package/src/JBOptimismSucker.sol +143 -0
- package/src/JBSucker.sol +1159 -0
- package/src/JBSuckerRegistry.sol +262 -0
- package/src/deployers/JBArbitrumSuckerDeployer.sol +86 -0
- package/src/deployers/JBBaseSuckerDeployer.sol +26 -0
- package/src/deployers/JBCCIPSuckerDeployer.sol +88 -0
- package/src/deployers/JBOptimismSuckerDeployer.sol +82 -0
- package/src/deployers/JBSuckerDeployer.sol +147 -0
- package/src/enums/JBAddToBalanceMode.sol +11 -0
- package/src/enums/JBLayer.sol +8 -0
- package/src/enums/JBSuckerState.sol +14 -0
- package/src/interfaces/IArbGatewayRouter.sol +11 -0
- package/src/interfaces/IArbL1GatewayRouter.sol +17 -0
- package/src/interfaces/IArbL2GatewayRouter.sol +14 -0
- package/src/interfaces/ICCIPRouter.sol +11 -0
- package/src/interfaces/IJBArbitrumSucker.sol +13 -0
- package/src/interfaces/IJBArbitrumSuckerDeployer.sol +12 -0
- package/src/interfaces/IJBCCIPSuckerDeployer.sol +15 -0
- package/src/interfaces/IJBOpSuckerDeployer.sol +11 -0
- package/src/interfaces/IJBOptimismSucker.sol +10 -0
- package/src/interfaces/IJBSucker.sol +144 -0
- package/src/interfaces/IJBSuckerDeployer.sol +40 -0
- package/src/interfaces/IJBSuckerExtended.sol +22 -0
- package/src/interfaces/IJBSuckerRegistry.sol +75 -0
- package/src/interfaces/IOPMessenger.sol +18 -0
- package/src/interfaces/IOPStandardBridge.sol +29 -0
- package/src/interfaces/IWrappedNativeToken.sol +13 -0
- package/src/libraries/ARBAddresses.sol +17 -0
- package/src/libraries/ARBChains.sol +11 -0
- package/src/libraries/CCIPHelper.sol +136 -0
- package/src/structs/JBClaim.sol +13 -0
- package/src/structs/JBInboxTreeRoot.sol +12 -0
- package/src/structs/JBLeaf.sol +14 -0
- package/src/structs/JBMessageRoot.sol +16 -0
- package/src/structs/JBOutboxTree.sol +18 -0
- package/src/structs/JBRemoteToken.sol +17 -0
- package/src/structs/JBSuckerDeployerConfig.sol +12 -0
- package/src/structs/JBSuckersPair.sol +11 -0
- package/src/structs/JBTokenMapping.sol +13 -0
- package/src/utils/MerkleLib.sol +1020 -0
- package/test/Fork.t.sol +514 -0
- package/test/InteropCompat.t.sol +676 -0
- package/test/SuckerAttacks.t.sol +509 -0
- package/test/SuckerDeepAttacks.t.sol +1563 -0
- package/test/mocks/ERC20Mock.sol +36 -0
- package/test/mocks/MockMessenger.sol +42 -0
- package/test/unit/arb.t.sol +28 -0
- package/test/unit/ccip_native_interop.t.sol +719 -0
- package/test/unit/ccip_refund.t.sol +234 -0
- package/test/unit/deployer.t.sol +475 -0
- package/test/unit/emergency.t.sol +305 -0
- package/test/unit/merkle.t.sol +212 -0
- package/test/unit/multi_chain_evolution.t.sol +622 -0
- package/test/unit/registry.t.sol +26 -0
|
@@ -0,0 +1,1563 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.13;
|
|
3
|
+
|
|
4
|
+
import "forge-std/Test.sol";
|
|
5
|
+
|
|
6
|
+
import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
|
|
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 {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
11
|
+
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
|
|
12
|
+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
13
|
+
import {LibClone} from "solady/src/utils/LibClone.sol";
|
|
14
|
+
|
|
15
|
+
import "../src/JBSucker.sol";
|
|
16
|
+
import {JBAddToBalanceMode} from "../src/enums/JBAddToBalanceMode.sol";
|
|
17
|
+
import {JBSuckerState} from "../src/enums/JBSuckerState.sol";
|
|
18
|
+
import {JBClaim} from "../src/structs/JBClaim.sol";
|
|
19
|
+
import {JBLeaf} from "../src/structs/JBLeaf.sol";
|
|
20
|
+
import {JBInboxTreeRoot} from "../src/structs/JBInboxTreeRoot.sol";
|
|
21
|
+
import {JBMessageRoot} from "../src/structs/JBMessageRoot.sol";
|
|
22
|
+
import {JBOutboxTree} from "../src/structs/JBOutboxTree.sol";
|
|
23
|
+
import {JBRemoteToken} from "../src/structs/JBRemoteToken.sol";
|
|
24
|
+
import {JBTokenMapping} from "../src/structs/JBTokenMapping.sol";
|
|
25
|
+
import {MerkleLib} from "../src/utils/MerkleLib.sol";
|
|
26
|
+
|
|
27
|
+
/// @notice Extended test sucker with additional helpers for deep attack testing.
|
|
28
|
+
contract DeepAttackSucker is JBSucker {
|
|
29
|
+
using MerkleLib for MerkleLib.Tree;
|
|
30
|
+
using BitMaps for BitMaps.BitMap;
|
|
31
|
+
|
|
32
|
+
bool nextCheckShouldPass;
|
|
33
|
+
bool public sendRootOverAMBReverted;
|
|
34
|
+
bool public shouldRevertAMB;
|
|
35
|
+
uint256 public lastAMBAmount;
|
|
36
|
+
|
|
37
|
+
constructor(
|
|
38
|
+
IJBDirectory directory,
|
|
39
|
+
IJBPermissions permissions,
|
|
40
|
+
IJBTokens tokens,
|
|
41
|
+
JBAddToBalanceMode addToBalanceMode,
|
|
42
|
+
address forwarder
|
|
43
|
+
)
|
|
44
|
+
JBSucker(directory, permissions, tokens, addToBalanceMode, forwarder)
|
|
45
|
+
{}
|
|
46
|
+
|
|
47
|
+
function _sendRootOverAMB(
|
|
48
|
+
uint256,
|
|
49
|
+
uint256,
|
|
50
|
+
address,
|
|
51
|
+
uint256 amount,
|
|
52
|
+
JBRemoteToken memory,
|
|
53
|
+
JBMessageRoot memory
|
|
54
|
+
)
|
|
55
|
+
internal
|
|
56
|
+
override
|
|
57
|
+
{
|
|
58
|
+
lastAMBAmount = amount;
|
|
59
|
+
if (shouldRevertAMB) {
|
|
60
|
+
sendRootOverAMBReverted = true;
|
|
61
|
+
revert("AMB reverted");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function _isRemotePeer(address sender) internal view override returns (bool) {
|
|
66
|
+
return sender == _toAddress(peer());
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function peerChainId() external view virtual override returns (uint256) {
|
|
70
|
+
return block.chainid;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function _validateBranchRoot(
|
|
74
|
+
bytes32 expectedRoot,
|
|
75
|
+
uint256 projectTokenCount,
|
|
76
|
+
uint256 terminalTokenAmount,
|
|
77
|
+
bytes32 beneficiary,
|
|
78
|
+
uint256 index,
|
|
79
|
+
bytes32[_TREE_DEPTH] calldata leaves
|
|
80
|
+
)
|
|
81
|
+
internal
|
|
82
|
+
virtual
|
|
83
|
+
override
|
|
84
|
+
{
|
|
85
|
+
if (!nextCheckShouldPass) {
|
|
86
|
+
super._validateBranchRoot(expectedRoot, projectTokenCount, terminalTokenAmount, beneficiary, index, leaves);
|
|
87
|
+
}
|
|
88
|
+
nextCheckShouldPass = false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ========================= Test helpers =========================
|
|
92
|
+
|
|
93
|
+
function test_setNextMerkleCheckToBe(bool _pass) external {
|
|
94
|
+
nextCheckShouldPass = _pass;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function test_setShouldRevertAMB(bool _revert) external {
|
|
98
|
+
shouldRevertAMB = _revert;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function test_setOutboxBalance(address token, uint256 amount) external {
|
|
102
|
+
_outboxOf[token].balance = amount;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function test_setInboxRoot(address token, uint64 nonce, bytes32 root) external {
|
|
106
|
+
_inboxOf[token] = JBInboxTreeRoot({nonce: nonce, root: root});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function test_insertIntoTree(
|
|
110
|
+
uint256 projectTokenCount,
|
|
111
|
+
address token,
|
|
112
|
+
uint256 terminalTokenAmount,
|
|
113
|
+
bytes32 beneficiary
|
|
114
|
+
)
|
|
115
|
+
external
|
|
116
|
+
{
|
|
117
|
+
_insertIntoTree(projectTokenCount, token, terminalTokenAmount, beneficiary);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function test_getOutboxRoot(address token) external view returns (bytes32) {
|
|
121
|
+
return _outboxOf[token].tree.root();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function test_getOutboxCount(address token) external view returns (uint256) {
|
|
125
|
+
return _outboxOf[token].tree.count;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function test_getOutboxNonce(address token) external view returns (uint64) {
|
|
129
|
+
return _outboxOf[token].nonce;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function test_getOutboxBalance(address token) external view returns (uint256) {
|
|
133
|
+
return _outboxOf[token].balance;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function test_getNumberOfClaimsSent(address token) external view returns (uint256) {
|
|
137
|
+
return _outboxOf[token].numberOfClaimsSent;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function test_setNumberOfClaimsSent(address token, uint256 count) external {
|
|
141
|
+
_outboxOf[token].numberOfClaimsSent = count;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function test_getInboxRoot(address token) external view returns (bytes32) {
|
|
145
|
+
return _inboxOf[token].root;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function test_getInboxNonce(address token) external view returns (uint64) {
|
|
149
|
+
return _inboxOf[token].nonce;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function test_setRemoteToken(address localToken, JBRemoteToken memory remoteToken) external {
|
|
153
|
+
_remoteTokenFor[localToken] = remoteToken;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function test_getRemoteToken(address localToken) external view returns (JBRemoteToken memory) {
|
|
157
|
+
return _remoteTokenFor[localToken];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function test_isExecuted(address token, uint256 index) external view returns (bool) {
|
|
161
|
+
return _executedFor[token].get(index);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function test_isEmergencyExecuted(address terminalToken, uint256 index) external view returns (bool) {
|
|
165
|
+
address emergencyExitAddress = address(bytes20(keccak256(abi.encode(terminalToken))));
|
|
166
|
+
return _executedFor[emergencyExitAddress].get(index);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function test_setDeprecatedAfter(uint256 timestamp) external {
|
|
170
|
+
deprecatedAfter = timestamp;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/// @title SuckerDeepAttacks
|
|
175
|
+
/// @notice Comprehensive adversarial security tests for JBSucker covering nonce handling,
|
|
176
|
+
/// double-spend vectors, deprecation state machine, emergency exit edge cases,
|
|
177
|
+
/// token mapping, merkle proof forgery, and balance manipulation.
|
|
178
|
+
contract SuckerDeepAttacks is Test {
|
|
179
|
+
using MerkleLib for MerkleLib.Tree;
|
|
180
|
+
|
|
181
|
+
address constant DIRECTORY = address(600);
|
|
182
|
+
address constant PERMISSIONS = address(800);
|
|
183
|
+
address constant TOKENS = address(700);
|
|
184
|
+
address constant CONTROLLER = address(900);
|
|
185
|
+
address constant PROJECT = address(1000);
|
|
186
|
+
address constant FORWARDER = address(1100);
|
|
187
|
+
|
|
188
|
+
uint256 constant PROJECT_ID = 1;
|
|
189
|
+
address constant TOKEN = address(0x000000000000000000000000000000000000EEEe); // JBConstants.NATIVE_TOKEN
|
|
190
|
+
|
|
191
|
+
DeepAttackSucker sucker;
|
|
192
|
+
|
|
193
|
+
function setUp() public {
|
|
194
|
+
// Warp to a reasonable timestamp so deprecation math doesn't underflow.
|
|
195
|
+
vm.warp(100 days);
|
|
196
|
+
|
|
197
|
+
vm.label(DIRECTORY, "MOCK_DIRECTORY");
|
|
198
|
+
vm.label(PERMISSIONS, "MOCK_PERMISSIONS");
|
|
199
|
+
vm.label(TOKENS, "MOCK_TOKENS");
|
|
200
|
+
vm.label(CONTROLLER, "MOCK_CONTROLLER");
|
|
201
|
+
|
|
202
|
+
sucker = _createTestSucker(PROJECT_ID, "deep_attack_salt");
|
|
203
|
+
|
|
204
|
+
// Mock directory
|
|
205
|
+
vm.mockCall(DIRECTORY, abi.encodeCall(IJBDirectory.PROJECTS, ()), abi.encode(PROJECT));
|
|
206
|
+
vm.mockCall(PROJECT, abi.encodeCall(IERC721.ownerOf, (PROJECT_ID)), abi.encode(address(this)));
|
|
207
|
+
vm.mockCall(DIRECTORY, abi.encodeCall(IJBDirectory.controllerOf, (PROJECT_ID)), abi.encode(CONTROLLER));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function _createTestSucker(uint256 projectId, bytes32 salt) internal returns (DeepAttackSucker) {
|
|
211
|
+
DeepAttackSucker singleton = new DeepAttackSucker(
|
|
212
|
+
IJBDirectory(DIRECTORY),
|
|
213
|
+
IJBPermissions(PERMISSIONS),
|
|
214
|
+
IJBTokens(TOKENS),
|
|
215
|
+
JBAddToBalanceMode.MANUAL,
|
|
216
|
+
FORWARDER
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
DeepAttackSucker clone =
|
|
220
|
+
DeepAttackSucker(payable(address(LibClone.cloneDeterministic(address(singleton), salt))));
|
|
221
|
+
clone.initialize(projectId);
|
|
222
|
+
return clone;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function _mockMint(address beneficiary, uint256 amount) internal {
|
|
226
|
+
vm.mockCall(
|
|
227
|
+
CONTROLLER,
|
|
228
|
+
abi.encodeCall(IJBController.mintTokensOf, (PROJECT_ID, amount, beneficiary, "", false)),
|
|
229
|
+
abi.encode(amount)
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function _enableTokenMapping(address token) internal {
|
|
234
|
+
sucker.test_setRemoteToken(
|
|
235
|
+
token,
|
|
236
|
+
JBRemoteToken({
|
|
237
|
+
enabled: true,
|
|
238
|
+
emergencyHatch: false,
|
|
239
|
+
minGas: 200_000,
|
|
240
|
+
addr: bytes32(uint256(uint160(makeAddr("remoteToken")))),
|
|
241
|
+
minBridgeAmount: 0
|
|
242
|
+
})
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// =========================================================================
|
|
247
|
+
// SECTION 1: fromRemote nonce handling
|
|
248
|
+
// =========================================================================
|
|
249
|
+
|
|
250
|
+
/// @notice fromRemote with nonce == current nonce: should silently ignore (no update).
|
|
251
|
+
function test_fromRemote_sameNonce_silentlyIgnored() public {
|
|
252
|
+
// Set current inbox nonce to 5.
|
|
253
|
+
sucker.test_setInboxRoot(TOKEN, 5, bytes32(uint256(0xdead)));
|
|
254
|
+
|
|
255
|
+
// Try to deliver a root with nonce=5 (same as current).
|
|
256
|
+
JBMessageRoot memory root = JBMessageRoot({
|
|
257
|
+
version: 1,
|
|
258
|
+
token: bytes32(uint256(uint160(TOKEN))),
|
|
259
|
+
amount: 1 ether,
|
|
260
|
+
remoteRoot: JBInboxTreeRoot({nonce: 5, root: bytes32(uint256(0xbeef))})
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
vm.prank(address(sucker)); // peer = address(this) for clones
|
|
264
|
+
sucker.fromRemote(root);
|
|
265
|
+
|
|
266
|
+
// Inbox should still have the old root, not the new one.
|
|
267
|
+
assertEq(sucker.test_getInboxRoot(TOKEN), bytes32(uint256(0xdead)), "Root should NOT be updated for same nonce");
|
|
268
|
+
assertEq(sucker.test_getInboxNonce(TOKEN), 5, "Nonce should remain 5");
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/// @notice fromRemote with nonce < current: should silently ignore.
|
|
272
|
+
function test_fromRemote_lowerNonce_silentlyIgnored() public {
|
|
273
|
+
sucker.test_setInboxRoot(TOKEN, 10, bytes32(uint256(0xdead)));
|
|
274
|
+
|
|
275
|
+
JBMessageRoot memory root = JBMessageRoot({
|
|
276
|
+
version: 1,
|
|
277
|
+
token: bytes32(uint256(uint160(TOKEN))),
|
|
278
|
+
amount: 1 ether,
|
|
279
|
+
remoteRoot: JBInboxTreeRoot({nonce: 3, root: bytes32(uint256(0xbeef))})
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
vm.prank(address(sucker));
|
|
283
|
+
sucker.fromRemote(root);
|
|
284
|
+
|
|
285
|
+
assertEq(sucker.test_getInboxRoot(TOKEN), bytes32(uint256(0xdead)), "Root should NOT update for lower nonce");
|
|
286
|
+
assertEq(sucker.test_getInboxNonce(TOKEN), 10, "Nonce should remain 10");
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/// @notice fromRemote with nonce gap (1 → 5): should accept, skipping intermediate nonces.
|
|
290
|
+
function test_fromRemote_nonceGap_accepted() public {
|
|
291
|
+
sucker.test_setInboxRoot(TOKEN, 1, bytes32(uint256(0xaaa)));
|
|
292
|
+
|
|
293
|
+
JBMessageRoot memory root = JBMessageRoot({
|
|
294
|
+
version: 1,
|
|
295
|
+
token: bytes32(uint256(uint160(TOKEN))),
|
|
296
|
+
amount: 1 ether,
|
|
297
|
+
remoteRoot: JBInboxTreeRoot({nonce: 5, root: bytes32(uint256(0xbbb))})
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
vm.prank(address(sucker));
|
|
301
|
+
sucker.fromRemote(root);
|
|
302
|
+
|
|
303
|
+
assertEq(sucker.test_getInboxNonce(TOKEN), 5, "Should accept nonce 5 after nonce 1");
|
|
304
|
+
assertEq(sucker.test_getInboxRoot(TOKEN), bytes32(uint256(0xbbb)), "Root should update");
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/// @notice fromRemote when DEPRECATED: should silently ignore even with valid higher nonce.
|
|
308
|
+
function test_fromRemote_deprecated_silentlyIgnored() public {
|
|
309
|
+
// Set deprecation in the past so state=DEPRECATED.
|
|
310
|
+
sucker.test_setDeprecatedAfter(block.timestamp - 1);
|
|
311
|
+
assertEq(uint256(sucker.state()), uint256(JBSuckerState.DEPRECATED), "Should be DEPRECATED");
|
|
312
|
+
|
|
313
|
+
sucker.test_setInboxRoot(TOKEN, 1, bytes32(uint256(0xaaaa)));
|
|
314
|
+
|
|
315
|
+
JBMessageRoot memory root = JBMessageRoot({
|
|
316
|
+
version: 1,
|
|
317
|
+
token: bytes32(uint256(uint160(TOKEN))),
|
|
318
|
+
amount: 1 ether,
|
|
319
|
+
remoteRoot: JBInboxTreeRoot({nonce: 2, root: bytes32(uint256(0xbbbb))})
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// Should NOT revert (native tokens would be lost), but should NOT update.
|
|
323
|
+
vm.prank(address(sucker));
|
|
324
|
+
sucker.fromRemote(root);
|
|
325
|
+
|
|
326
|
+
assertEq(sucker.test_getInboxRoot(TOKEN), bytes32(uint256(0xaaaa)), "Root should NOT update when DEPRECATED");
|
|
327
|
+
assertEq(sucker.test_getInboxNonce(TOKEN), 1, "Nonce should remain 1 when DEPRECATED");
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/// @notice fromRemote when SENDING_DISABLED: should still accept roots (only sending is disabled).
|
|
331
|
+
function test_fromRemote_sendingDisabled_stillAccepts() public {
|
|
332
|
+
// Set deprecation so we're in SENDING_DISABLED window:
|
|
333
|
+
// state() checks: block.timestamp < deprecatedAfter → not DEPRECATED
|
|
334
|
+
// block.timestamp >= deprecatedAfter - 14 days → SENDING_DISABLED
|
|
335
|
+
uint256 deprecateAt = block.timestamp + 1 days; // within 14 day window
|
|
336
|
+
sucker.test_setDeprecatedAfter(deprecateAt);
|
|
337
|
+
|
|
338
|
+
// Warp to SENDING_DISABLED window.
|
|
339
|
+
vm.warp(deprecateAt - 1);
|
|
340
|
+
assertEq(uint256(sucker.state()), uint256(JBSuckerState.SENDING_DISABLED), "Should be SENDING_DISABLED");
|
|
341
|
+
|
|
342
|
+
JBMessageRoot memory root = JBMessageRoot({
|
|
343
|
+
version: 1,
|
|
344
|
+
token: bytes32(uint256(uint160(TOKEN))),
|
|
345
|
+
amount: 1 ether,
|
|
346
|
+
remoteRoot: JBInboxTreeRoot({nonce: 1, root: bytes32(uint256(0xabc))})
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
vm.prank(address(sucker));
|
|
350
|
+
sucker.fromRemote(root);
|
|
351
|
+
|
|
352
|
+
assertEq(sucker.test_getInboxRoot(TOKEN), bytes32(uint256(0xabc)), "Should accept root in SENDING_DISABLED");
|
|
353
|
+
assertEq(sucker.test_getInboxNonce(TOKEN), 1, "Nonce should update");
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// =========================================================================
|
|
357
|
+
// SECTION 2: Claim proof forgery and cross-token attacks
|
|
358
|
+
// =========================================================================
|
|
359
|
+
|
|
360
|
+
/// @notice Claim with wrong beneficiary: merkle proof should fail.
|
|
361
|
+
function test_claim_wrongBeneficiary_reverts() public {
|
|
362
|
+
address realBeneficiary = address(0xAAA);
|
|
363
|
+
address fakeBeneficiary = address(0xBBB);
|
|
364
|
+
|
|
365
|
+
// Insert a leaf for the real beneficiary.
|
|
366
|
+
sucker.test_insertIntoTree(10 ether, TOKEN, 5 ether, bytes32(uint256(uint160(realBeneficiary))));
|
|
367
|
+
bytes32 root = sucker.test_getOutboxRoot(TOKEN);
|
|
368
|
+
sucker.test_setInboxRoot(TOKEN, 1, root);
|
|
369
|
+
sucker.test_setOutboxBalance(TOKEN, 100 ether);
|
|
370
|
+
|
|
371
|
+
// Try to claim with the WRONG beneficiary — proof won't match.
|
|
372
|
+
bytes32[32] memory proof;
|
|
373
|
+
JBClaim memory claimData = JBClaim({
|
|
374
|
+
token: TOKEN,
|
|
375
|
+
leaf: JBLeaf({
|
|
376
|
+
index: 0,
|
|
377
|
+
beneficiary: bytes32(uint256(uint160(fakeBeneficiary))), // WRONG
|
|
378
|
+
projectTokenCount: 10 ether,
|
|
379
|
+
terminalTokenAmount: 5 ether
|
|
380
|
+
}),
|
|
381
|
+
proof: proof
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// Should revert with InvalidProof (the hash won't match the tree).
|
|
385
|
+
vm.expectRevert();
|
|
386
|
+
sucker.claim(claimData);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/// @notice Claim with wrong amount: merkle proof should fail.
|
|
390
|
+
function test_claim_wrongAmount_reverts() public {
|
|
391
|
+
address beneficiary = address(0xAAA);
|
|
392
|
+
|
|
393
|
+
sucker.test_insertIntoTree(10 ether, TOKEN, 5 ether, bytes32(uint256(uint160(beneficiary))));
|
|
394
|
+
bytes32 root = sucker.test_getOutboxRoot(TOKEN);
|
|
395
|
+
sucker.test_setInboxRoot(TOKEN, 1, root);
|
|
396
|
+
sucker.test_setOutboxBalance(TOKEN, 100 ether);
|
|
397
|
+
|
|
398
|
+
// Try with inflated amount.
|
|
399
|
+
bytes32[32] memory proof;
|
|
400
|
+
JBClaim memory claimData = JBClaim({
|
|
401
|
+
token: TOKEN,
|
|
402
|
+
leaf: JBLeaf({
|
|
403
|
+
index: 0,
|
|
404
|
+
beneficiary: bytes32(uint256(uint160(beneficiary))),
|
|
405
|
+
projectTokenCount: 10 ether,
|
|
406
|
+
terminalTokenAmount: 100 ether // WRONG — real is 5 ether
|
|
407
|
+
}),
|
|
408
|
+
proof: proof
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
vm.expectRevert();
|
|
412
|
+
sucker.claim(claimData);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/// @notice Claim with proof from token A against token B's inbox: should fail.
|
|
416
|
+
function test_claim_crossTokenProof_reverts() public {
|
|
417
|
+
address tokenA = makeAddr("tokenA");
|
|
418
|
+
address tokenB = makeAddr("tokenB");
|
|
419
|
+
|
|
420
|
+
// Insert into tokenA's tree.
|
|
421
|
+
sucker.test_insertIntoTree(10 ether, tokenA, 5 ether, bytes32(uint256(uint160(address(0xAAA)))));
|
|
422
|
+
|
|
423
|
+
// Set token B's inbox root to something different.
|
|
424
|
+
sucker.test_setInboxRoot(tokenB, 1, bytes32(uint256(0xcccc)));
|
|
425
|
+
|
|
426
|
+
// Claim against token B using token A's proof data → mismatch.
|
|
427
|
+
bytes32[32] memory proof;
|
|
428
|
+
JBClaim memory claimData = JBClaim({
|
|
429
|
+
token: tokenB, // targeting token B's inbox
|
|
430
|
+
leaf: JBLeaf({
|
|
431
|
+
index: 0,
|
|
432
|
+
beneficiary: bytes32(uint256(uint160(address(0xAAA)))),
|
|
433
|
+
projectTokenCount: 10 ether,
|
|
434
|
+
terminalTokenAmount: 5 ether
|
|
435
|
+
}),
|
|
436
|
+
proof: proof
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
vm.expectRevert();
|
|
440
|
+
sucker.claim(claimData);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// =========================================================================
|
|
444
|
+
// SECTION 3: Double-spend — claim + emergency exit on same leaf
|
|
445
|
+
// =========================================================================
|
|
446
|
+
|
|
447
|
+
/// @notice Verify that a leaf claimed via `claim()` uses a different executed slot
|
|
448
|
+
/// than `exitThroughEmergencyHatch()`. This is by design — they track
|
|
449
|
+
/// different trees (inbox vs outbox).
|
|
450
|
+
function test_claimAndEmergencyExit_differentSlots() public {
|
|
451
|
+
// Insert a leaf at index 0 in the outbox.
|
|
452
|
+
sucker.test_insertIntoTree(10 ether, TOKEN, 5 ether, bytes32(uint256(uint160(address(this)))));
|
|
453
|
+
|
|
454
|
+
bytes32 outboxRoot = sucker.test_getOutboxRoot(TOKEN);
|
|
455
|
+
|
|
456
|
+
// Set inbox root to outbox root (simulating a round-trip).
|
|
457
|
+
sucker.test_setInboxRoot(TOKEN, 1, outboxRoot);
|
|
458
|
+
sucker.test_setOutboxBalance(TOKEN, 100 ether);
|
|
459
|
+
vm.deal(address(sucker), 100 ether);
|
|
460
|
+
|
|
461
|
+
_mockMint(address(this), 10 ether);
|
|
462
|
+
|
|
463
|
+
// Do a regular claim at index 0 (bypass merkle for testing).
|
|
464
|
+
sucker.test_setNextMerkleCheckToBe(true);
|
|
465
|
+
bytes32[32] memory proof;
|
|
466
|
+
JBClaim memory claimData = JBClaim({
|
|
467
|
+
token: TOKEN,
|
|
468
|
+
leaf: JBLeaf({
|
|
469
|
+
index: 0,
|
|
470
|
+
beneficiary: bytes32(uint256(uint160(address(this)))),
|
|
471
|
+
projectTokenCount: 10 ether,
|
|
472
|
+
terminalTokenAmount: 5 ether
|
|
473
|
+
}),
|
|
474
|
+
proof: proof
|
|
475
|
+
});
|
|
476
|
+
sucker.claim(claimData);
|
|
477
|
+
|
|
478
|
+
// Verify claim executed for inbox (token-keyed).
|
|
479
|
+
assertTrue(sucker.test_isExecuted(TOKEN, 0), "Index 0 should be marked executed for claim");
|
|
480
|
+
|
|
481
|
+
// The emergency exit uses a DIFFERENT key: address(bytes20(keccak256(abi.encode(token)))).
|
|
482
|
+
// So the same index 0 is NOT marked in the emergency slot.
|
|
483
|
+
assertFalse(sucker.test_isEmergencyExecuted(TOKEN, 0), "Emergency slot should NOT be marked by claim");
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/// @notice If a root was already sent (numberOfClaimsSent covers index),
|
|
487
|
+
/// emergency exit should reject the claim to prevent double-spend.
|
|
488
|
+
function test_emergencyExit_alreadySentRoot_reverts() public {
|
|
489
|
+
// Insert 3 leaves.
|
|
490
|
+
sucker.test_insertIntoTree(1 ether, TOKEN, 1 ether, bytes32(uint256(uint160(address(this)))));
|
|
491
|
+
sucker.test_insertIntoTree(2 ether, TOKEN, 2 ether, bytes32(uint256(uint160(address(this)))));
|
|
492
|
+
sucker.test_insertIntoTree(3 ether, TOKEN, 3 ether, bytes32(uint256(uint160(address(this)))));
|
|
493
|
+
|
|
494
|
+
// Mark that 2 claims were sent (indices 0 and 1 covered).
|
|
495
|
+
sucker.test_setNumberOfClaimsSent(TOKEN, 2);
|
|
496
|
+
sucker.test_setOutboxBalance(TOKEN, 100 ether);
|
|
497
|
+
vm.deal(address(sucker), 100 ether);
|
|
498
|
+
|
|
499
|
+
// Enable emergency hatch.
|
|
500
|
+
sucker.test_setRemoteToken(
|
|
501
|
+
TOKEN,
|
|
502
|
+
JBRemoteToken({
|
|
503
|
+
enabled: false,
|
|
504
|
+
emergencyHatch: true,
|
|
505
|
+
minGas: 0,
|
|
506
|
+
addr: bytes32(uint256(uint160(makeAddr("remote")))),
|
|
507
|
+
minBridgeAmount: 0
|
|
508
|
+
})
|
|
509
|
+
);
|
|
510
|
+
|
|
511
|
+
// Index 0 was part of the sent root → should revert (could be double-spent on remote).
|
|
512
|
+
sucker.test_setNextMerkleCheckToBe(true);
|
|
513
|
+
bytes32[32] memory proof;
|
|
514
|
+
JBClaim memory claimData = JBClaim({
|
|
515
|
+
token: TOKEN,
|
|
516
|
+
leaf: JBLeaf({
|
|
517
|
+
index: 0,
|
|
518
|
+
beneficiary: bytes32(uint256(uint160(address(this)))),
|
|
519
|
+
projectTokenCount: 1 ether,
|
|
520
|
+
terminalTokenAmount: 1 ether
|
|
521
|
+
}),
|
|
522
|
+
proof: proof
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
vm.expectRevert(abi.encodeWithSelector(JBSucker.JBSucker_LeafAlreadyExecuted.selector, TOKEN, 0));
|
|
526
|
+
sucker.exitThroughEmergencyHatch(claimData);
|
|
527
|
+
|
|
528
|
+
// Index 1 (last sent) → should also revert.
|
|
529
|
+
claimData.leaf.index = 1;
|
|
530
|
+
claimData.leaf.projectTokenCount = 2 ether;
|
|
531
|
+
claimData.leaf.terminalTokenAmount = 2 ether;
|
|
532
|
+
sucker.test_setNextMerkleCheckToBe(true);
|
|
533
|
+
|
|
534
|
+
vm.expectRevert(abi.encodeWithSelector(JBSucker.JBSucker_LeafAlreadyExecuted.selector, TOKEN, 1));
|
|
535
|
+
sucker.exitThroughEmergencyHatch(claimData);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/// @notice Index NOT covered by numberOfClaimsSent → emergency exit should succeed.
|
|
539
|
+
function test_emergencyExit_unsentIndex_succeeds() public {
|
|
540
|
+
// Insert 3 leaves.
|
|
541
|
+
sucker.test_insertIntoTree(1 ether, TOKEN, 1 ether, bytes32(uint256(uint160(address(this)))));
|
|
542
|
+
sucker.test_insertIntoTree(2 ether, TOKEN, 2 ether, bytes32(uint256(uint160(address(this)))));
|
|
543
|
+
sucker.test_insertIntoTree(3 ether, TOKEN, 3 ether, bytes32(uint256(uint160(address(this)))));
|
|
544
|
+
|
|
545
|
+
// Only 2 were sent (indices 0 and 1). Index 2 was NOT sent.
|
|
546
|
+
sucker.test_setNumberOfClaimsSent(TOKEN, 2);
|
|
547
|
+
sucker.test_setOutboxBalance(TOKEN, 100 ether);
|
|
548
|
+
vm.deal(address(sucker), 100 ether);
|
|
549
|
+
|
|
550
|
+
// Enable emergency hatch.
|
|
551
|
+
sucker.test_setRemoteToken(
|
|
552
|
+
TOKEN,
|
|
553
|
+
JBRemoteToken({
|
|
554
|
+
enabled: false,
|
|
555
|
+
emergencyHatch: true,
|
|
556
|
+
minGas: 0,
|
|
557
|
+
addr: bytes32(uint256(uint160(makeAddr("remote")))),
|
|
558
|
+
minBridgeAmount: 0
|
|
559
|
+
})
|
|
560
|
+
);
|
|
561
|
+
|
|
562
|
+
_mockMint(address(this), 3 ether);
|
|
563
|
+
|
|
564
|
+
// Index 2 → was NOT sent, so emergency exit should work.
|
|
565
|
+
sucker.test_setNextMerkleCheckToBe(true);
|
|
566
|
+
bytes32[32] memory proof;
|
|
567
|
+
JBClaim memory claimData = JBClaim({
|
|
568
|
+
token: TOKEN,
|
|
569
|
+
leaf: JBLeaf({
|
|
570
|
+
index: 2,
|
|
571
|
+
beneficiary: bytes32(uint256(uint160(address(this)))),
|
|
572
|
+
projectTokenCount: 3 ether,
|
|
573
|
+
terminalTokenAmount: 3 ether
|
|
574
|
+
}),
|
|
575
|
+
proof: proof
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
sucker.exitThroughEmergencyHatch(claimData);
|
|
579
|
+
|
|
580
|
+
// Verify it was marked as executed in the emergency slot.
|
|
581
|
+
assertTrue(sucker.test_isEmergencyExecuted(TOKEN, 2), "Emergency exit at index 2 should be marked");
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/// @notice Double emergency exit with the same index → should revert on second try.
|
|
585
|
+
function test_emergencyExit_doubleClaim_reverts() public {
|
|
586
|
+
sucker.test_insertIntoTree(5 ether, TOKEN, 5 ether, bytes32(uint256(uint160(address(this)))));
|
|
587
|
+
sucker.test_setOutboxBalance(TOKEN, 100 ether);
|
|
588
|
+
vm.deal(address(sucker), 100 ether);
|
|
589
|
+
|
|
590
|
+
sucker.test_setRemoteToken(
|
|
591
|
+
TOKEN,
|
|
592
|
+
JBRemoteToken({
|
|
593
|
+
enabled: false,
|
|
594
|
+
emergencyHatch: true,
|
|
595
|
+
minGas: 0,
|
|
596
|
+
addr: bytes32(uint256(uint160(makeAddr("remote")))),
|
|
597
|
+
minBridgeAmount: 0
|
|
598
|
+
})
|
|
599
|
+
);
|
|
600
|
+
|
|
601
|
+
_mockMint(address(this), 5 ether);
|
|
602
|
+
|
|
603
|
+
sucker.test_setNextMerkleCheckToBe(true);
|
|
604
|
+
bytes32[32] memory proof;
|
|
605
|
+
JBClaim memory claimData = JBClaim({
|
|
606
|
+
token: TOKEN,
|
|
607
|
+
leaf: JBLeaf({
|
|
608
|
+
index: 0,
|
|
609
|
+
beneficiary: bytes32(uint256(uint160(address(this)))),
|
|
610
|
+
projectTokenCount: 5 ether,
|
|
611
|
+
terminalTokenAmount: 5 ether
|
|
612
|
+
}),
|
|
613
|
+
proof: proof
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
// First exit succeeds.
|
|
617
|
+
sucker.exitThroughEmergencyHatch(claimData);
|
|
618
|
+
|
|
619
|
+
// Second exit with same index → revert.
|
|
620
|
+
sucker.test_setNextMerkleCheckToBe(true);
|
|
621
|
+
vm.expectRevert(abi.encodeWithSelector(JBSucker.JBSucker_LeafAlreadyExecuted.selector, TOKEN, 0));
|
|
622
|
+
sucker.exitThroughEmergencyHatch(claimData);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/// @notice Emergency exit when hatch not enabled and not deprecated → should revert.
|
|
626
|
+
function test_emergencyExit_noHatchNoDeprecation_reverts() public {
|
|
627
|
+
sucker.test_insertIntoTree(5 ether, TOKEN, 5 ether, bytes32(uint256(uint160(address(this)))));
|
|
628
|
+
sucker.test_setOutboxBalance(TOKEN, 100 ether);
|
|
629
|
+
|
|
630
|
+
// Token is enabled (not emergency hatch), sucker is ENABLED (not deprecated).
|
|
631
|
+
_enableTokenMapping(TOKEN);
|
|
632
|
+
|
|
633
|
+
sucker.test_setNextMerkleCheckToBe(true);
|
|
634
|
+
bytes32[32] memory proof;
|
|
635
|
+
JBClaim memory claimData = JBClaim({
|
|
636
|
+
token: TOKEN,
|
|
637
|
+
leaf: JBLeaf({
|
|
638
|
+
index: 0,
|
|
639
|
+
beneficiary: bytes32(uint256(uint160(address(this)))),
|
|
640
|
+
projectTokenCount: 5 ether,
|
|
641
|
+
terminalTokenAmount: 5 ether
|
|
642
|
+
}),
|
|
643
|
+
proof: proof
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
vm.expectRevert(abi.encodeWithSelector(JBSucker.JBSucker_TokenHasInvalidEmergencyHatchState.selector, TOKEN));
|
|
647
|
+
sucker.exitThroughEmergencyHatch(claimData);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/// @notice Emergency exit balance underflow — claim more than tracked outbox balance.
|
|
651
|
+
function test_emergencyExit_balanceUnderflow_reverts() public {
|
|
652
|
+
sucker.test_insertIntoTree(10 ether, TOKEN, 10 ether, bytes32(uint256(uint160(address(this)))));
|
|
653
|
+
|
|
654
|
+
// Set outbox balance to only 1 ether, but claim asks for 10 ether.
|
|
655
|
+
sucker.test_setOutboxBalance(TOKEN, 1 ether);
|
|
656
|
+
vm.deal(address(sucker), 100 ether);
|
|
657
|
+
|
|
658
|
+
sucker.test_setRemoteToken(
|
|
659
|
+
TOKEN,
|
|
660
|
+
JBRemoteToken({
|
|
661
|
+
enabled: false,
|
|
662
|
+
emergencyHatch: true,
|
|
663
|
+
minGas: 0,
|
|
664
|
+
addr: bytes32(uint256(uint160(makeAddr("remote")))),
|
|
665
|
+
minBridgeAmount: 0
|
|
666
|
+
})
|
|
667
|
+
);
|
|
668
|
+
|
|
669
|
+
sucker.test_setNextMerkleCheckToBe(true);
|
|
670
|
+
bytes32[32] memory proof;
|
|
671
|
+
JBClaim memory claimData = JBClaim({
|
|
672
|
+
token: TOKEN,
|
|
673
|
+
leaf: JBLeaf({
|
|
674
|
+
index: 0,
|
|
675
|
+
beneficiary: bytes32(uint256(uint160(address(this)))),
|
|
676
|
+
projectTokenCount: 10 ether,
|
|
677
|
+
terminalTokenAmount: 10 ether
|
|
678
|
+
}),
|
|
679
|
+
proof: proof
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
// _outboxOf[token].balance -= 10 ether when balance is only 1 ether → arithmetic underflow.
|
|
683
|
+
vm.expectRevert();
|
|
684
|
+
sucker.exitThroughEmergencyHatch(claimData);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// =========================================================================
|
|
688
|
+
// SECTION 4: Deprecation state machine
|
|
689
|
+
// =========================================================================
|
|
690
|
+
|
|
691
|
+
/// @notice Verify the full state progression: ENABLED → DEPRECATION_PENDING → SENDING_DISABLED → DEPRECATED.
|
|
692
|
+
function test_deprecationStateMachine_fullProgression() public {
|
|
693
|
+
// Initially ENABLED.
|
|
694
|
+
assertEq(uint256(sucker.state()), uint256(JBSuckerState.ENABLED));
|
|
695
|
+
|
|
696
|
+
// Set deprecation 30 days from now.
|
|
697
|
+
uint256 deprecateAt = block.timestamp + 30 days;
|
|
698
|
+
sucker.test_setDeprecatedAfter(deprecateAt);
|
|
699
|
+
|
|
700
|
+
// Still before (deprecateAt - 14 days) → DEPRECATION_PENDING.
|
|
701
|
+
assertEq(uint256(sucker.state()), uint256(JBSuckerState.DEPRECATION_PENDING));
|
|
702
|
+
|
|
703
|
+
// Warp to exactly (deprecateAt - 14 days) → SENDING_DISABLED.
|
|
704
|
+
vm.warp(deprecateAt - 14 days);
|
|
705
|
+
assertEq(uint256(sucker.state()), uint256(JBSuckerState.SENDING_DISABLED));
|
|
706
|
+
|
|
707
|
+
// Warp to just before deprecateAt → still SENDING_DISABLED.
|
|
708
|
+
vm.warp(deprecateAt - 1);
|
|
709
|
+
assertEq(uint256(sucker.state()), uint256(JBSuckerState.SENDING_DISABLED));
|
|
710
|
+
|
|
711
|
+
// Warp to exactly deprecateAt → DEPRECATED.
|
|
712
|
+
vm.warp(deprecateAt);
|
|
713
|
+
assertEq(uint256(sucker.state()), uint256(JBSuckerState.DEPRECATED));
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/// @notice setDeprecation too soon (less than maxMessagingDelay from now) → should revert.
|
|
717
|
+
function test_setDeprecation_tooSoon_reverts() public {
|
|
718
|
+
// Mock permissions for the owner.
|
|
719
|
+
vm.mockCall(PERMISSIONS, abi.encodeWithSelector(IJBPermissions.hasPermission.selector), abi.encode(true));
|
|
720
|
+
|
|
721
|
+
// Try to set deprecation 1 day from now (too soon, min is 14 days).
|
|
722
|
+
uint256 tooSoon = uint40(block.timestamp + 1 days);
|
|
723
|
+
vm.expectRevert();
|
|
724
|
+
sucker.setDeprecation(uint40(tooSoon));
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/// @notice setDeprecation(0) cancels deprecation and returns to ENABLED.
|
|
728
|
+
function test_setDeprecation_zero_cancelsDeprecation() public {
|
|
729
|
+
vm.mockCall(PERMISSIONS, abi.encodeWithSelector(IJBPermissions.hasPermission.selector), abi.encode(true));
|
|
730
|
+
|
|
731
|
+
// Set a valid deprecation.
|
|
732
|
+
uint256 validTime = block.timestamp + 30 days;
|
|
733
|
+
sucker.setDeprecation(uint40(validTime));
|
|
734
|
+
assertEq(uint256(sucker.state()), uint256(JBSuckerState.DEPRECATION_PENDING));
|
|
735
|
+
|
|
736
|
+
// Cancel it.
|
|
737
|
+
sucker.setDeprecation(0);
|
|
738
|
+
assertEq(uint256(sucker.state()), uint256(JBSuckerState.ENABLED));
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/// @notice setDeprecation when already SENDING_DISABLED → should revert.
|
|
742
|
+
function test_setDeprecation_whenSendingDisabled_reverts() public {
|
|
743
|
+
vm.mockCall(PERMISSIONS, abi.encodeWithSelector(IJBPermissions.hasPermission.selector), abi.encode(true));
|
|
744
|
+
|
|
745
|
+
// Force SENDING_DISABLED state.
|
|
746
|
+
uint256 deprecateAt = block.timestamp + 1 days;
|
|
747
|
+
sucker.test_setDeprecatedAfter(deprecateAt);
|
|
748
|
+
vm.warp(deprecateAt - 1); // within 14-day window
|
|
749
|
+
assertEq(uint256(sucker.state()), uint256(JBSuckerState.SENDING_DISABLED));
|
|
750
|
+
|
|
751
|
+
// Try to change deprecation — should revert (already in terminal path).
|
|
752
|
+
vm.expectRevert(abi.encodeWithSelector(JBSucker.JBSucker_Deprecated.selector));
|
|
753
|
+
sucker.setDeprecation(uint40(block.timestamp + 60 days));
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// =========================================================================
|
|
757
|
+
// SECTION 5: Token mapping edge cases
|
|
758
|
+
// =========================================================================
|
|
759
|
+
|
|
760
|
+
/// @notice Remapping a token with existing outbox items → should revert.
|
|
761
|
+
function test_mapToken_remapWithExistingOutbox_reverts() public {
|
|
762
|
+
vm.mockCall(PERMISSIONS, abi.encodeWithSelector(IJBPermissions.hasPermission.selector), abi.encode(true));
|
|
763
|
+
|
|
764
|
+
address token = makeAddr("erc20Token");
|
|
765
|
+
address remoteA = makeAddr("remoteA");
|
|
766
|
+
address remoteB = makeAddr("remoteB");
|
|
767
|
+
|
|
768
|
+
// Initial mapping.
|
|
769
|
+
sucker.test_setRemoteToken(
|
|
770
|
+
token,
|
|
771
|
+
JBRemoteToken({
|
|
772
|
+
enabled: true,
|
|
773
|
+
emergencyHatch: false,
|
|
774
|
+
minGas: 200_000,
|
|
775
|
+
addr: bytes32(uint256(uint160(remoteA))),
|
|
776
|
+
minBridgeAmount: 0
|
|
777
|
+
})
|
|
778
|
+
);
|
|
779
|
+
|
|
780
|
+
// Insert items into the outbox (creating tree count > 0).
|
|
781
|
+
sucker.test_insertIntoTree(10 ether, token, 5 ether, bytes32(uint256(uint160(address(this)))));
|
|
782
|
+
|
|
783
|
+
// Try to remap to a different remote token.
|
|
784
|
+
vm.expectRevert(
|
|
785
|
+
abi.encodeWithSelector(
|
|
786
|
+
JBSucker.JBSucker_TokenAlreadyMapped.selector, token, bytes32(uint256(uint160(remoteA)))
|
|
787
|
+
)
|
|
788
|
+
);
|
|
789
|
+
sucker.mapToken(
|
|
790
|
+
JBTokenMapping({
|
|
791
|
+
localToken: token, minGas: 200_000, remoteToken: bytes32(uint256(uint160(remoteB))), minBridgeAmount: 0
|
|
792
|
+
})
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/// @notice Mapping after emergency hatch enabled → should revert.
|
|
797
|
+
function test_mapToken_afterEmergencyHatch_reverts() public {
|
|
798
|
+
vm.mockCall(PERMISSIONS, abi.encodeWithSelector(IJBPermissions.hasPermission.selector), abi.encode(true));
|
|
799
|
+
|
|
800
|
+
address token = makeAddr("erc20Token");
|
|
801
|
+
|
|
802
|
+
// Enable emergency hatch.
|
|
803
|
+
sucker.test_setRemoteToken(
|
|
804
|
+
token,
|
|
805
|
+
JBRemoteToken({
|
|
806
|
+
enabled: false,
|
|
807
|
+
emergencyHatch: true,
|
|
808
|
+
minGas: 200_000,
|
|
809
|
+
addr: bytes32(uint256(uint160(makeAddr("remote")))),
|
|
810
|
+
minBridgeAmount: 0
|
|
811
|
+
})
|
|
812
|
+
);
|
|
813
|
+
|
|
814
|
+
// Try any mapping operation.
|
|
815
|
+
vm.expectRevert(abi.encodeWithSelector(JBSucker.JBSucker_TokenHasInvalidEmergencyHatchState.selector, token));
|
|
816
|
+
sucker.mapToken(
|
|
817
|
+
JBTokenMapping({
|
|
818
|
+
localToken: token,
|
|
819
|
+
minGas: 200_000,
|
|
820
|
+
remoteToken: bytes32(uint256(uint160(makeAddr("newRemote")))),
|
|
821
|
+
minBridgeAmount: 0
|
|
822
|
+
})
|
|
823
|
+
);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/// @notice mapToken without permission → should revert.
|
|
827
|
+
function test_mapToken_noPermission_reverts() public {
|
|
828
|
+
vm.mockCall(PERMISSIONS, abi.encodeWithSelector(IJBPermissions.hasPermission.selector), abi.encode(false));
|
|
829
|
+
|
|
830
|
+
address attacker = makeAddr("attacker");
|
|
831
|
+
|
|
832
|
+
vm.prank(attacker);
|
|
833
|
+
vm.expectRevert();
|
|
834
|
+
sucker.mapToken(
|
|
835
|
+
JBTokenMapping({
|
|
836
|
+
localToken: makeAddr("token"),
|
|
837
|
+
minGas: 200_000,
|
|
838
|
+
remoteToken: bytes32(uint256(uint160(makeAddr("remote")))),
|
|
839
|
+
minBridgeAmount: 0
|
|
840
|
+
})
|
|
841
|
+
);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/// @notice mapToken: native token → non-native remote → should revert.
|
|
845
|
+
function test_mapToken_nativeToNonNative_reverts() public {
|
|
846
|
+
vm.mockCall(PERMISSIONS, abi.encodeWithSelector(IJBPermissions.hasPermission.selector), abi.encode(true));
|
|
847
|
+
|
|
848
|
+
bytes32 nonNativeRemote = bytes32(uint256(uint160(makeAddr("nonNative"))));
|
|
849
|
+
vm.expectRevert(abi.encodeWithSelector(JBSucker.JBSucker_InvalidNativeRemoteAddress.selector, nonNativeRemote));
|
|
850
|
+
sucker.mapToken(
|
|
851
|
+
JBTokenMapping({
|
|
852
|
+
localToken: JBConstants.NATIVE_TOKEN, minGas: 0, remoteToken: nonNativeRemote, minBridgeAmount: 0
|
|
853
|
+
})
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/// @notice mapToken: ERC20 with gas below minimum → should revert.
|
|
858
|
+
function test_mapToken_belowMinGas_reverts() public {
|
|
859
|
+
vm.mockCall(PERMISSIONS, abi.encodeWithSelector(IJBPermissions.hasPermission.selector), abi.encode(true));
|
|
860
|
+
|
|
861
|
+
vm.expectRevert(
|
|
862
|
+
abi.encodeWithSelector(
|
|
863
|
+
JBSucker.JBSucker_BelowMinGas.selector,
|
|
864
|
+
100, // given
|
|
865
|
+
200_000 // minimum
|
|
866
|
+
)
|
|
867
|
+
);
|
|
868
|
+
sucker.mapToken(
|
|
869
|
+
JBTokenMapping({
|
|
870
|
+
localToken: makeAddr("erc20"),
|
|
871
|
+
minGas: 100, // Way below MESSENGER_ERC20_MIN_GAS_LIMIT
|
|
872
|
+
remoteToken: bytes32(uint256(uint160(makeAddr("remote")))),
|
|
873
|
+
minBridgeAmount: 0
|
|
874
|
+
})
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// =========================================================================
|
|
879
|
+
// SECTION 6: prepare() edge cases
|
|
880
|
+
// =========================================================================
|
|
881
|
+
|
|
882
|
+
/// @notice prepare with zero beneficiary → should revert.
|
|
883
|
+
function test_prepare_zeroBeneficiary_reverts() public {
|
|
884
|
+
_enableTokenMapping(TOKEN);
|
|
885
|
+
|
|
886
|
+
vm.expectRevert(abi.encodeWithSelector(JBSucker.JBSucker_ZeroBeneficiary.selector));
|
|
887
|
+
sucker.prepare(10 ether, bytes32(0), 0, TOKEN);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
/// @notice prepare when SENDING_DISABLED → should revert.
|
|
891
|
+
function test_prepare_sendingDisabled_reverts() public {
|
|
892
|
+
_enableTokenMapping(TOKEN);
|
|
893
|
+
|
|
894
|
+
// Mock TOKENS.tokenOf() so prepare gets past the project token check.
|
|
895
|
+
vm.mockCall(TOKENS, abi.encodeCall(IJBTokens.tokenOf, (PROJECT_ID)), abi.encode(makeAddr("projectToken")));
|
|
896
|
+
|
|
897
|
+
// Force SENDING_DISABLED.
|
|
898
|
+
uint256 deprecateAt = block.timestamp + 1 days;
|
|
899
|
+
sucker.test_setDeprecatedAfter(deprecateAt);
|
|
900
|
+
vm.warp(deprecateAt - 1);
|
|
901
|
+
assertEq(uint256(sucker.state()), uint256(JBSuckerState.SENDING_DISABLED));
|
|
902
|
+
|
|
903
|
+
vm.expectRevert(abi.encodeWithSelector(JBSucker.JBSucker_Deprecated.selector));
|
|
904
|
+
sucker.prepare(10 ether, bytes32(uint256(uint160(address(this)))), 0, TOKEN);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/// @notice prepare when DEPRECATED → should revert.
|
|
908
|
+
function test_prepare_deprecated_reverts() public {
|
|
909
|
+
_enableTokenMapping(TOKEN);
|
|
910
|
+
|
|
911
|
+
// Mock TOKENS.tokenOf() so prepare gets past the project token check.
|
|
912
|
+
vm.mockCall(TOKENS, abi.encodeCall(IJBTokens.tokenOf, (PROJECT_ID)), abi.encode(makeAddr("projectToken")));
|
|
913
|
+
|
|
914
|
+
sucker.test_setDeprecatedAfter(block.timestamp - 1);
|
|
915
|
+
assertEq(uint256(sucker.state()), uint256(JBSuckerState.DEPRECATED));
|
|
916
|
+
|
|
917
|
+
vm.expectRevert(abi.encodeWithSelector(JBSucker.JBSucker_Deprecated.selector));
|
|
918
|
+
sucker.prepare(10 ether, bytes32(uint256(uint160(address(this)))), 0, TOKEN);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/// @notice prepare with unmapped token → should revert.
|
|
922
|
+
function test_prepare_unmappedToken_reverts() public {
|
|
923
|
+
address unmapped = makeAddr("unmappedToken");
|
|
924
|
+
|
|
925
|
+
// Mock token check (project has an ERC20 token).
|
|
926
|
+
vm.mockCall(TOKENS, abi.encodeCall(IJBTokens.tokenOf, (PROJECT_ID)), abi.encode(makeAddr("projectToken")));
|
|
927
|
+
|
|
928
|
+
vm.expectRevert(abi.encodeWithSelector(JBSucker.JBSucker_TokenNotMapped.selector, unmapped));
|
|
929
|
+
sucker.prepare(10 ether, bytes32(uint256(uint160(address(this)))), 0, unmapped);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// =========================================================================
|
|
933
|
+
// SECTION 7: toRemote / _sendRoot edge cases
|
|
934
|
+
// =========================================================================
|
|
935
|
+
|
|
936
|
+
/// @notice toRemote with emergency hatch enabled → should revert.
|
|
937
|
+
function test_toRemote_emergencyHatch_reverts() public {
|
|
938
|
+
sucker.test_setRemoteToken(
|
|
939
|
+
TOKEN,
|
|
940
|
+
JBRemoteToken({
|
|
941
|
+
enabled: false,
|
|
942
|
+
emergencyHatch: true,
|
|
943
|
+
minGas: 0,
|
|
944
|
+
addr: bytes32(uint256(uint160(makeAddr("remote")))),
|
|
945
|
+
minBridgeAmount: 0
|
|
946
|
+
})
|
|
947
|
+
);
|
|
948
|
+
|
|
949
|
+
vm.expectRevert(abi.encodeWithSelector(JBSucker.JBSucker_TokenHasInvalidEmergencyHatchState.selector, TOKEN));
|
|
950
|
+
sucker.toRemote(TOKEN);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/// @notice toRemote below minimum bridge amount → should revert.
|
|
954
|
+
function test_toRemote_belowMinBridgeAmount_reverts() public {
|
|
955
|
+
sucker.test_setRemoteToken(
|
|
956
|
+
TOKEN,
|
|
957
|
+
JBRemoteToken({
|
|
958
|
+
enabled: true,
|
|
959
|
+
emergencyHatch: false,
|
|
960
|
+
minGas: 200_000,
|
|
961
|
+
addr: bytes32(uint256(uint160(makeAddr("remote")))),
|
|
962
|
+
minBridgeAmount: 10 ether
|
|
963
|
+
})
|
|
964
|
+
);
|
|
965
|
+
|
|
966
|
+
// Insert only 1 ether into outbox.
|
|
967
|
+
sucker.test_insertIntoTree(1 ether, TOKEN, 1 ether, bytes32(uint256(uint160(address(this)))));
|
|
968
|
+
|
|
969
|
+
vm.expectRevert(abi.encodeWithSelector(JBSucker.JBSucker_QueueInsufficientSize.selector, 1 ether, 10 ether));
|
|
970
|
+
sucker.toRemote(TOKEN);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
/// @notice _sendRoot clears balance BEFORE AMB call — verify balance is 0 after toRemote.
|
|
974
|
+
function test_sendRoot_clearsBalanceBeforeAMB() public {
|
|
975
|
+
_enableTokenMapping(TOKEN);
|
|
976
|
+
|
|
977
|
+
// Insert items with total balance of 10 ether.
|
|
978
|
+
sucker.test_insertIntoTree(5 ether, TOKEN, 5 ether, bytes32(uint256(uint160(address(this)))));
|
|
979
|
+
sucker.test_insertIntoTree(5 ether, TOKEN, 5 ether, bytes32(uint256(uint160(address(0xBBB)))));
|
|
980
|
+
|
|
981
|
+
assertEq(sucker.test_getOutboxBalance(TOKEN), 10 ether, "Outbox balance should be 10 ether");
|
|
982
|
+
|
|
983
|
+
// Send root.
|
|
984
|
+
sucker.toRemote(TOKEN);
|
|
985
|
+
|
|
986
|
+
// Balance should be cleared.
|
|
987
|
+
assertEq(sucker.test_getOutboxBalance(TOKEN), 0, "Outbox balance should be 0 after sendRoot");
|
|
988
|
+
|
|
989
|
+
// Nonce should be incremented.
|
|
990
|
+
assertEq(sucker.test_getOutboxNonce(TOKEN), 1, "Nonce should be 1");
|
|
991
|
+
|
|
992
|
+
// numberOfClaimsSent should be updated.
|
|
993
|
+
assertEq(sucker.test_getNumberOfClaimsSent(TOKEN), 2, "numberOfClaimsSent should be 2");
|
|
994
|
+
|
|
995
|
+
// The AMB received the correct amount.
|
|
996
|
+
assertEq(sucker.lastAMBAmount(), 10 ether, "AMB should receive 10 ether");
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
/// @notice If AMB reverts after balance cleared, funds are lost. Verify the behavior.
|
|
1000
|
+
function test_sendRoot_AMBRevert_fundsLost() public {
|
|
1001
|
+
_enableTokenMapping(TOKEN);
|
|
1002
|
+
|
|
1003
|
+
sucker.test_insertIntoTree(10 ether, TOKEN, 10 ether, bytes32(uint256(uint160(address(this)))));
|
|
1004
|
+
|
|
1005
|
+
// Make AMB revert.
|
|
1006
|
+
sucker.test_setShouldRevertAMB(true);
|
|
1007
|
+
|
|
1008
|
+
// toRemote will revert, which means the entire tx reverts and balance is NOT cleared.
|
|
1009
|
+
// This is actually the correct behavior — the revert rolls back state.
|
|
1010
|
+
vm.expectRevert("AMB reverted");
|
|
1011
|
+
sucker.toRemote(TOKEN);
|
|
1012
|
+
|
|
1013
|
+
// Verify balance was NOT cleared (tx reverted).
|
|
1014
|
+
assertEq(sucker.test_getOutboxBalance(TOKEN), 10 ether, "Balance should remain if AMB reverts");
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// =========================================================================
|
|
1018
|
+
// SECTION 8: Balance manipulation
|
|
1019
|
+
// =========================================================================
|
|
1020
|
+
|
|
1021
|
+
/// @notice Direct ETH transfer inflates actual balance but not tracked outbox balance.
|
|
1022
|
+
/// amountToAddToBalanceOf should reflect the difference.
|
|
1023
|
+
function test_amountToAddToBalance_inflatedByDirectTransfer() public {
|
|
1024
|
+
// Send 10 ether directly first, then set tracked outbox balance.
|
|
1025
|
+
vm.deal(address(sucker), 10 ether);
|
|
1026
|
+
|
|
1027
|
+
// Set tracked outbox balance (must be <= actual balance to avoid underflow).
|
|
1028
|
+
sucker.test_setOutboxBalance(TOKEN, 3 ether);
|
|
1029
|
+
|
|
1030
|
+
// amountToAddToBalanceOf = actualBalance - outboxBalance = 10 - 3 = 7 ether.
|
|
1031
|
+
uint256 addable = sucker.amountToAddToBalanceOf(TOKEN);
|
|
1032
|
+
assertEq(addable, 7 ether, "Addable should be actual minus tracked");
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
/// @notice addOutstandingAmountToBalance with ON_CLAIM mode → should revert (wrong mode).
|
|
1036
|
+
function test_addOutstandingAmountToBalance_wrongMode_reverts() public {
|
|
1037
|
+
// Our sucker was created with MANUAL mode, so this should work.
|
|
1038
|
+
// Create a different sucker with ON_CLAIM mode.
|
|
1039
|
+
DeepAttackSucker onClaimSucker;
|
|
1040
|
+
{
|
|
1041
|
+
DeepAttackSucker singleton = new DeepAttackSucker(
|
|
1042
|
+
IJBDirectory(DIRECTORY),
|
|
1043
|
+
IJBPermissions(PERMISSIONS),
|
|
1044
|
+
IJBTokens(TOKENS),
|
|
1045
|
+
JBAddToBalanceMode.ON_CLAIM,
|
|
1046
|
+
FORWARDER
|
|
1047
|
+
);
|
|
1048
|
+
onClaimSucker =
|
|
1049
|
+
DeepAttackSucker(payable(address(LibClone.cloneDeterministic(address(singleton), "onclaim_salt"))));
|
|
1050
|
+
onClaimSucker.initialize(PROJECT_ID);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
vm.expectRevert(
|
|
1054
|
+
abi.encodeWithSelector(JBSucker.JBSucker_ManualNotAllowed.selector, JBAddToBalanceMode.ON_CLAIM)
|
|
1055
|
+
);
|
|
1056
|
+
onClaimSucker.addOutstandingAmountToBalance(TOKEN);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// =========================================================================
|
|
1060
|
+
// SECTION 9: enableEmergencyHatchFor edge cases
|
|
1061
|
+
// =========================================================================
|
|
1062
|
+
|
|
1063
|
+
/// @notice enableEmergencyHatchFor without SUCKER_SAFETY permission → should revert.
|
|
1064
|
+
function test_enableEmergencyHatch_noPermission_reverts() public {
|
|
1065
|
+
vm.mockCall(PERMISSIONS, abi.encodeWithSelector(IJBPermissions.hasPermission.selector), abi.encode(false));
|
|
1066
|
+
|
|
1067
|
+
address[] memory tokens = new address[](1);
|
|
1068
|
+
tokens[0] = TOKEN;
|
|
1069
|
+
|
|
1070
|
+
address attacker = makeAddr("attacker");
|
|
1071
|
+
vm.prank(attacker);
|
|
1072
|
+
vm.expectRevert();
|
|
1073
|
+
sucker.enableEmergencyHatchFor(tokens);
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
/// @notice enableEmergencyHatchFor sets emergencyHatch=true and enabled=false.
|
|
1077
|
+
function test_enableEmergencyHatch_setsCorrectState() public {
|
|
1078
|
+
vm.mockCall(PERMISSIONS, abi.encodeWithSelector(IJBPermissions.hasPermission.selector), abi.encode(true));
|
|
1079
|
+
|
|
1080
|
+
// Start with enabled token.
|
|
1081
|
+
_enableTokenMapping(TOKEN);
|
|
1082
|
+
JBRemoteToken memory before = sucker.test_getRemoteToken(TOKEN);
|
|
1083
|
+
assertTrue(before.enabled, "Should start enabled");
|
|
1084
|
+
assertFalse(before.emergencyHatch, "Should start without hatch");
|
|
1085
|
+
|
|
1086
|
+
address[] memory tokens = new address[](1);
|
|
1087
|
+
tokens[0] = TOKEN;
|
|
1088
|
+
sucker.enableEmergencyHatchFor(tokens);
|
|
1089
|
+
|
|
1090
|
+
JBRemoteToken memory after_ = sucker.test_getRemoteToken(TOKEN);
|
|
1091
|
+
assertFalse(after_.enabled, "Should be disabled after hatch");
|
|
1092
|
+
assertTrue(after_.emergencyHatch, "Emergency hatch should be true");
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// =========================================================================
|
|
1096
|
+
// SECTION 10: Merkle tree integrity
|
|
1097
|
+
// =========================================================================
|
|
1098
|
+
|
|
1099
|
+
/// @notice Multiple insertions produce unique roots. Verify no collisions.
|
|
1100
|
+
function test_merkleTree_uniqueRootsPerInsertion() public {
|
|
1101
|
+
bytes32[] memory roots = new bytes32[](20);
|
|
1102
|
+
|
|
1103
|
+
for (uint256 i = 0; i < 20; i++) {
|
|
1104
|
+
sucker.test_insertIntoTree((i + 1) * 1 ether, TOKEN, (i + 1) * 0.5 ether, bytes32(uint256(1000 + i)));
|
|
1105
|
+
roots[i] = sucker.test_getOutboxRoot(TOKEN);
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
// Verify all roots are unique.
|
|
1109
|
+
for (uint256 i = 0; i < 20; i++) {
|
|
1110
|
+
for (uint256 j = i + 1; j < 20; j++) {
|
|
1111
|
+
assertTrue(roots[i] != roots[j], "Roots should be unique");
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
assertEq(sucker.test_getOutboxCount(TOKEN), 20, "Count should be 20");
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
/// @notice Verify that the _buildTreeHash is deterministic and order-sensitive.
|
|
1119
|
+
function test_treeHash_deterministic() public pure {
|
|
1120
|
+
bytes32 hash1 =
|
|
1121
|
+
keccak256(abi.encode(uint256(10 ether), uint256(5 ether), bytes32(uint256(uint160(address(0xAAA))))));
|
|
1122
|
+
bytes32 hash2 =
|
|
1123
|
+
keccak256(abi.encode(uint256(10 ether), uint256(5 ether), bytes32(uint256(uint160(address(0xAAA))))));
|
|
1124
|
+
bytes32 hash3 =
|
|
1125
|
+
keccak256(abi.encode(uint256(5 ether), uint256(10 ether), bytes32(uint256(uint160(address(0xAAA)))))); // swapped
|
|
1126
|
+
|
|
1127
|
+
assertEq(hash1, hash2, "Same inputs should produce same hash");
|
|
1128
|
+
assertTrue(hash1 != hash3, "Different inputs should produce different hash");
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// =========================================================================
|
|
1132
|
+
// SECTION 11: Claim when no inbox root set
|
|
1133
|
+
// =========================================================================
|
|
1134
|
+
|
|
1135
|
+
/// @notice Claim when inbox root is bytes32(0) → proof check will fail.
|
|
1136
|
+
function test_claim_noInboxRoot_reverts() public {
|
|
1137
|
+
// Don't set any inbox root. inbox.root is bytes32(0).
|
|
1138
|
+
assertEq(sucker.test_getInboxRoot(TOKEN), bytes32(0), "Inbox should be empty");
|
|
1139
|
+
|
|
1140
|
+
bytes32[32] memory proof;
|
|
1141
|
+
JBClaim memory claimData = JBClaim({
|
|
1142
|
+
token: TOKEN,
|
|
1143
|
+
leaf: JBLeaf({
|
|
1144
|
+
index: 0,
|
|
1145
|
+
beneficiary: bytes32(uint256(uint160(address(this)))),
|
|
1146
|
+
projectTokenCount: 10 ether,
|
|
1147
|
+
terminalTokenAmount: 5 ether
|
|
1148
|
+
}),
|
|
1149
|
+
proof: proof
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1152
|
+
// The computed branch root won't match bytes32(0).
|
|
1153
|
+
vm.expectRevert();
|
|
1154
|
+
sucker.claim(claimData);
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// =========================================================================
|
|
1158
|
+
// SECTION 12: toRemote when DEPRECATED or SENDING_DISABLED
|
|
1159
|
+
// =========================================================================
|
|
1160
|
+
|
|
1161
|
+
/// @notice toRemote when DEPRECATED → _sendRoot should revert.
|
|
1162
|
+
function test_toRemote_deprecated_reverts() public {
|
|
1163
|
+
_enableTokenMapping(TOKEN);
|
|
1164
|
+
sucker.test_insertIntoTree(1 ether, TOKEN, 1 ether, bytes32(uint256(uint160(address(this)))));
|
|
1165
|
+
|
|
1166
|
+
sucker.test_setDeprecatedAfter(block.timestamp - 1);
|
|
1167
|
+
assertEq(uint256(sucker.state()), uint256(JBSuckerState.DEPRECATED));
|
|
1168
|
+
|
|
1169
|
+
vm.expectRevert(abi.encodeWithSelector(JBSucker.JBSucker_Deprecated.selector));
|
|
1170
|
+
sucker.toRemote(TOKEN);
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
/// @notice toRemote when SENDING_DISABLED → _sendRoot should revert.
|
|
1174
|
+
function test_toRemote_sendingDisabled_reverts() public {
|
|
1175
|
+
_enableTokenMapping(TOKEN);
|
|
1176
|
+
sucker.test_insertIntoTree(1 ether, TOKEN, 1 ether, bytes32(uint256(uint160(address(this)))));
|
|
1177
|
+
|
|
1178
|
+
uint256 deprecateAt = block.timestamp + 1 days;
|
|
1179
|
+
sucker.test_setDeprecatedAfter(deprecateAt);
|
|
1180
|
+
vm.warp(deprecateAt - 1);
|
|
1181
|
+
assertEq(uint256(sucker.state()), uint256(JBSuckerState.SENDING_DISABLED));
|
|
1182
|
+
|
|
1183
|
+
vm.expectRevert(abi.encodeWithSelector(JBSucker.JBSucker_Deprecated.selector));
|
|
1184
|
+
sucker.toRemote(TOKEN);
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// =========================================================================
|
|
1188
|
+
// SECTION 13: Emergency exit via deprecation (not per-token hatch)
|
|
1189
|
+
// =========================================================================
|
|
1190
|
+
|
|
1191
|
+
/// @notice When sucker is DEPRECATED (not per-token hatch), emergency exit should work.
|
|
1192
|
+
function test_emergencyExit_viaDeprecation_succeeds() public {
|
|
1193
|
+
// Insert a leaf.
|
|
1194
|
+
sucker.test_insertIntoTree(5 ether, TOKEN, 5 ether, bytes32(uint256(uint160(address(this)))));
|
|
1195
|
+
sucker.test_setOutboxBalance(TOKEN, 100 ether);
|
|
1196
|
+
vm.deal(address(sucker), 100 ether);
|
|
1197
|
+
|
|
1198
|
+
// Token is NOT hatch-enabled, but sucker is DEPRECATED.
|
|
1199
|
+
_enableTokenMapping(TOKEN);
|
|
1200
|
+
sucker.test_setDeprecatedAfter(block.timestamp - 1);
|
|
1201
|
+
assertEq(uint256(sucker.state()), uint256(JBSuckerState.DEPRECATED));
|
|
1202
|
+
|
|
1203
|
+
_mockMint(address(this), 5 ether);
|
|
1204
|
+
|
|
1205
|
+
sucker.test_setNextMerkleCheckToBe(true);
|
|
1206
|
+
bytes32[32] memory proof;
|
|
1207
|
+
JBClaim memory claimData = JBClaim({
|
|
1208
|
+
token: TOKEN,
|
|
1209
|
+
leaf: JBLeaf({
|
|
1210
|
+
index: 0,
|
|
1211
|
+
beneficiary: bytes32(uint256(uint160(address(this)))),
|
|
1212
|
+
projectTokenCount: 5 ether,
|
|
1213
|
+
terminalTokenAmount: 5 ether
|
|
1214
|
+
}),
|
|
1215
|
+
proof: proof
|
|
1216
|
+
});
|
|
1217
|
+
|
|
1218
|
+
// Should succeed because DEPRECATED state allows emergency exit.
|
|
1219
|
+
sucker.exitThroughEmergencyHatch(claimData);
|
|
1220
|
+
assertTrue(sucker.test_isEmergencyExecuted(TOKEN, 0), "Should be marked executed");
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
/// @notice When SENDING_DISABLED, emergency exit should also work.
|
|
1224
|
+
function test_emergencyExit_viaSendingDisabled_succeeds() public {
|
|
1225
|
+
sucker.test_insertIntoTree(5 ether, TOKEN, 5 ether, bytes32(uint256(uint160(address(this)))));
|
|
1226
|
+
sucker.test_setOutboxBalance(TOKEN, 100 ether);
|
|
1227
|
+
vm.deal(address(sucker), 100 ether);
|
|
1228
|
+
|
|
1229
|
+
_enableTokenMapping(TOKEN);
|
|
1230
|
+
|
|
1231
|
+
// Force SENDING_DISABLED.
|
|
1232
|
+
uint256 deprecateAt = block.timestamp + 1 days;
|
|
1233
|
+
sucker.test_setDeprecatedAfter(deprecateAt);
|
|
1234
|
+
vm.warp(deprecateAt - 1);
|
|
1235
|
+
assertEq(uint256(sucker.state()), uint256(JBSuckerState.SENDING_DISABLED));
|
|
1236
|
+
|
|
1237
|
+
_mockMint(address(this), 5 ether);
|
|
1238
|
+
|
|
1239
|
+
sucker.test_setNextMerkleCheckToBe(true);
|
|
1240
|
+
bytes32[32] memory proof;
|
|
1241
|
+
JBClaim memory claimData = JBClaim({
|
|
1242
|
+
token: TOKEN,
|
|
1243
|
+
leaf: JBLeaf({
|
|
1244
|
+
index: 0,
|
|
1245
|
+
beneficiary: bytes32(uint256(uint160(address(this)))),
|
|
1246
|
+
projectTokenCount: 5 ether,
|
|
1247
|
+
terminalTokenAmount: 5 ether
|
|
1248
|
+
}),
|
|
1249
|
+
proof: proof
|
|
1250
|
+
});
|
|
1251
|
+
|
|
1252
|
+
sucker.exitThroughEmergencyHatch(claimData);
|
|
1253
|
+
assertTrue(sucker.test_isEmergencyExecuted(TOKEN, 0), "Should be marked executed");
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
// =========================================================================
|
|
1257
|
+
// SECTION 14: Multiple claims on different indices
|
|
1258
|
+
// =========================================================================
|
|
1259
|
+
|
|
1260
|
+
/// @notice Claim multiple indices sequentially — each should succeed once.
|
|
1261
|
+
function test_claim_multipleIndices_eachSucceedsOnce() public {
|
|
1262
|
+
// Insert 5 leaves.
|
|
1263
|
+
for (uint256 i = 0; i < 5; i++) {
|
|
1264
|
+
sucker.test_insertIntoTree((i + 1) * 1 ether, TOKEN, (i + 1) * 0.5 ether, bytes32(uint256(100 + i)));
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
bytes32 root = sucker.test_getOutboxRoot(TOKEN);
|
|
1268
|
+
sucker.test_setInboxRoot(TOKEN, 1, root);
|
|
1269
|
+
sucker.test_setOutboxBalance(TOKEN, 100 ether);
|
|
1270
|
+
|
|
1271
|
+
// Claim each index.
|
|
1272
|
+
for (uint256 i = 0; i < 5; i++) {
|
|
1273
|
+
_mockMint(address(uint160(100 + i)), (i + 1) * 1 ether);
|
|
1274
|
+
|
|
1275
|
+
sucker.test_setNextMerkleCheckToBe(true);
|
|
1276
|
+
bytes32[32] memory proof;
|
|
1277
|
+
JBClaim memory claimData = JBClaim({
|
|
1278
|
+
token: TOKEN,
|
|
1279
|
+
leaf: JBLeaf({
|
|
1280
|
+
index: i,
|
|
1281
|
+
beneficiary: bytes32(uint256(100 + i)),
|
|
1282
|
+
projectTokenCount: (i + 1) * 1 ether,
|
|
1283
|
+
terminalTokenAmount: (i + 1) * 0.5 ether
|
|
1284
|
+
}),
|
|
1285
|
+
proof: proof
|
|
1286
|
+
});
|
|
1287
|
+
|
|
1288
|
+
sucker.claim(claimData);
|
|
1289
|
+
assertTrue(sucker.test_isExecuted(TOKEN, i), "Index should be marked executed");
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
// Re-claiming any index should fail.
|
|
1293
|
+
sucker.test_setNextMerkleCheckToBe(true);
|
|
1294
|
+
bytes32[32] memory dupProof;
|
|
1295
|
+
JBClaim memory dupClaim = JBClaim({
|
|
1296
|
+
token: TOKEN,
|
|
1297
|
+
leaf: JBLeaf({
|
|
1298
|
+
index: 2, beneficiary: bytes32(uint256(102)), projectTokenCount: 3 ether, terminalTokenAmount: 1.5 ether
|
|
1299
|
+
}),
|
|
1300
|
+
proof: dupProof
|
|
1301
|
+
});
|
|
1302
|
+
|
|
1303
|
+
vm.expectRevert(abi.encodeWithSelector(JBSucker.JBSucker_LeafAlreadyExecuted.selector, TOKEN, 2));
|
|
1304
|
+
sucker.claim(dupClaim);
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
// =========================================================================
|
|
1308
|
+
// SECTION 15: Nonce increment on toRemote
|
|
1309
|
+
// =========================================================================
|
|
1310
|
+
|
|
1311
|
+
/// @notice Multiple toRemote calls increment nonce sequentially.
|
|
1312
|
+
function test_toRemote_nonceIncrementsSequentially() public {
|
|
1313
|
+
_enableTokenMapping(TOKEN);
|
|
1314
|
+
|
|
1315
|
+
// Round 1.
|
|
1316
|
+
sucker.test_insertIntoTree(1 ether, TOKEN, 1 ether, bytes32(uint256(uint160(address(this)))));
|
|
1317
|
+
sucker.toRemote(TOKEN);
|
|
1318
|
+
assertEq(sucker.test_getOutboxNonce(TOKEN), 1);
|
|
1319
|
+
|
|
1320
|
+
// Round 2.
|
|
1321
|
+
sucker.test_insertIntoTree(2 ether, TOKEN, 2 ether, bytes32(uint256(uint160(address(this)))));
|
|
1322
|
+
sucker.toRemote(TOKEN);
|
|
1323
|
+
assertEq(sucker.test_getOutboxNonce(TOKEN), 2);
|
|
1324
|
+
|
|
1325
|
+
// Round 3.
|
|
1326
|
+
sucker.test_insertIntoTree(3 ether, TOKEN, 3 ether, bytes32(uint256(uint160(address(this)))));
|
|
1327
|
+
sucker.toRemote(TOKEN);
|
|
1328
|
+
assertEq(sucker.test_getOutboxNonce(TOKEN), 3);
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
/// @notice toRemote with zero outbox balance but minBridgeAmount=0 → should still work
|
|
1332
|
+
/// as long as there are tree items.
|
|
1333
|
+
function test_toRemote_zeroBalance_zeroMin_succeeds() public {
|
|
1334
|
+
sucker.test_setRemoteToken(
|
|
1335
|
+
TOKEN,
|
|
1336
|
+
JBRemoteToken({
|
|
1337
|
+
enabled: true,
|
|
1338
|
+
emergencyHatch: false,
|
|
1339
|
+
minGas: 200_000,
|
|
1340
|
+
addr: bytes32(uint256(uint160(makeAddr("remote")))),
|
|
1341
|
+
minBridgeAmount: 0
|
|
1342
|
+
})
|
|
1343
|
+
);
|
|
1344
|
+
|
|
1345
|
+
// Insert an item with 0 terminal token amount.
|
|
1346
|
+
sucker.test_insertIntoTree(1 ether, TOKEN, 0, bytes32(uint256(uint160(address(this)))));
|
|
1347
|
+
|
|
1348
|
+
// Balance is 0, min is 0 → should pass the check.
|
|
1349
|
+
sucker.toRemote(TOKEN);
|
|
1350
|
+
assertEq(sucker.test_getOutboxNonce(TOKEN), 1);
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
// =========================================================================
|
|
1354
|
+
// SECTION 16: MESSAGE_VERSION validation (INTEROP)
|
|
1355
|
+
// =========================================================================
|
|
1356
|
+
|
|
1357
|
+
/// @notice fromRemote with wrong message version → should revert with InvalidMessageVersion.
|
|
1358
|
+
function test_fromRemote_wrongVersion_reverts() public {
|
|
1359
|
+
JBMessageRoot memory root = JBMessageRoot({
|
|
1360
|
+
version: 0, // Wrong version — current is 1
|
|
1361
|
+
token: bytes32(uint256(uint160(TOKEN))),
|
|
1362
|
+
amount: 1 ether,
|
|
1363
|
+
remoteRoot: JBInboxTreeRoot({nonce: 1, root: bytes32(uint256(0xbeef))})
|
|
1364
|
+
});
|
|
1365
|
+
|
|
1366
|
+
vm.expectRevert(
|
|
1367
|
+
abi.encodeWithSelector(JBSucker.JBSucker_InvalidMessageVersion.selector, 0, sucker.MESSAGE_VERSION())
|
|
1368
|
+
);
|
|
1369
|
+
vm.prank(address(sucker)); // peer
|
|
1370
|
+
sucker.fromRemote(root);
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
/// @notice fromRemote with future message version → should revert with InvalidMessageVersion.
|
|
1374
|
+
function test_fromRemote_futureVersion_reverts() public {
|
|
1375
|
+
JBMessageRoot memory root = JBMessageRoot({
|
|
1376
|
+
version: 2, // Future version
|
|
1377
|
+
token: bytes32(uint256(uint160(TOKEN))),
|
|
1378
|
+
amount: 1 ether,
|
|
1379
|
+
remoteRoot: JBInboxTreeRoot({nonce: 1, root: bytes32(uint256(0xbeef))})
|
|
1380
|
+
});
|
|
1381
|
+
|
|
1382
|
+
vm.expectRevert(
|
|
1383
|
+
abi.encodeWithSelector(JBSucker.JBSucker_InvalidMessageVersion.selector, 2, sucker.MESSAGE_VERSION())
|
|
1384
|
+
);
|
|
1385
|
+
vm.prank(address(sucker));
|
|
1386
|
+
sucker.fromRemote(root);
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
/// @notice fromRemote with correct version → should NOT revert with InvalidMessageVersion.
|
|
1390
|
+
function test_fromRemote_correctVersion_passesVersionCheck() public {
|
|
1391
|
+
JBMessageRoot memory root = JBMessageRoot({
|
|
1392
|
+
version: sucker.MESSAGE_VERSION(),
|
|
1393
|
+
token: bytes32(uint256(uint160(TOKEN))),
|
|
1394
|
+
amount: 0,
|
|
1395
|
+
remoteRoot: JBInboxTreeRoot({nonce: 1, root: bytes32(uint256(0xbeef))})
|
|
1396
|
+
});
|
|
1397
|
+
|
|
1398
|
+
vm.prank(address(sucker));
|
|
1399
|
+
sucker.fromRemote(root);
|
|
1400
|
+
|
|
1401
|
+
// Should update inbox since version is correct and nonce is higher.
|
|
1402
|
+
assertEq(sucker.test_getInboxNonce(TOKEN), 1, "Nonce should be 1 after valid message");
|
|
1403
|
+
assertEq(sucker.test_getInboxRoot(TOKEN), bytes32(uint256(0xbeef)), "Root should be updated");
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
// =========================================================================
|
|
1407
|
+
// L-10: StaleRootRejected event emission
|
|
1408
|
+
// =========================================================================
|
|
1409
|
+
|
|
1410
|
+
/// @notice fromRemote with stale nonce should emit StaleRootRejected event.
|
|
1411
|
+
function test_fromRemote_staleNonce_emitsEvent() public {
|
|
1412
|
+
sucker.test_setInboxRoot(TOKEN, 5, bytes32(uint256(0xdead)));
|
|
1413
|
+
|
|
1414
|
+
JBMessageRoot memory root = JBMessageRoot({
|
|
1415
|
+
version: sucker.MESSAGE_VERSION(),
|
|
1416
|
+
token: bytes32(uint256(uint160(TOKEN))),
|
|
1417
|
+
amount: 1 ether,
|
|
1418
|
+
remoteRoot: JBInboxTreeRoot({nonce: 3, root: bytes32(uint256(0xbeef))})
|
|
1419
|
+
});
|
|
1420
|
+
|
|
1421
|
+
vm.expectEmit(true, false, false, true, address(sucker));
|
|
1422
|
+
emit IJBSucker.StaleRootRejected({token: TOKEN, receivedNonce: 3, currentNonce: 5});
|
|
1423
|
+
|
|
1424
|
+
vm.prank(address(sucker));
|
|
1425
|
+
sucker.fromRemote(root);
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
// =========================================================================
|
|
1429
|
+
// SECTION 17: uint128 overflow guard (INTEROP-5)
|
|
1430
|
+
// =========================================================================
|
|
1431
|
+
|
|
1432
|
+
/// @notice prepare with terminalTokenAmount > uint128 max → should revert.
|
|
1433
|
+
function test_prepare_terminalAmountExceedsUint128_reverts() public {
|
|
1434
|
+
_enableTokenMapping(TOKEN);
|
|
1435
|
+
|
|
1436
|
+
vm.mockCall(TOKENS, abi.encodeCall(IJBTokens.tokenOf, (PROJECT_ID)), abi.encode(makeAddr("projectToken")));
|
|
1437
|
+
|
|
1438
|
+
// Mock the token transfer for prepare
|
|
1439
|
+
vm.mockCall(makeAddr("projectToken"), abi.encodeWithSelector(IERC20.transferFrom.selector), abi.encode(true));
|
|
1440
|
+
|
|
1441
|
+
// The overflow guard fires inside _insertIntoTree, which is called from prepare
|
|
1442
|
+
// after computing terminalTokenAmount. For a direct test, use the test helper.
|
|
1443
|
+
uint256 overflowAmount = uint256(type(uint128).max) + 1;
|
|
1444
|
+
|
|
1445
|
+
vm.expectRevert(abi.encodeWithSelector(JBSucker.JBSucker_AmountExceedsUint128.selector, overflowAmount));
|
|
1446
|
+
sucker.test_insertIntoTree(1 ether, TOKEN, overflowAmount, bytes32(uint256(uint160(address(this)))));
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
/// @notice prepare with projectTokenCount > uint128 max → should revert.
|
|
1450
|
+
function test_prepare_projectTokenCountExceedsUint128_reverts() public {
|
|
1451
|
+
uint256 overflowAmount = uint256(type(uint128).max) + 1;
|
|
1452
|
+
|
|
1453
|
+
vm.expectRevert(abi.encodeWithSelector(JBSucker.JBSucker_AmountExceedsUint128.selector, overflowAmount));
|
|
1454
|
+
sucker.test_insertIntoTree(overflowAmount, TOKEN, 1 ether, bytes32(uint256(uint160(address(this)))));
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
/// @notice Amounts at exactly uint128 max → should succeed.
|
|
1458
|
+
function test_insertIntoTree_exactUint128Max_succeeds() public {
|
|
1459
|
+
uint256 maxU128 = uint256(type(uint128).max);
|
|
1460
|
+
|
|
1461
|
+
// Both at max should work.
|
|
1462
|
+
sucker.test_insertIntoTree(maxU128, TOKEN, maxU128, bytes32(uint256(uint160(address(this)))));
|
|
1463
|
+
assertEq(sucker.test_getOutboxCount(TOKEN), 1, "Should have 1 item");
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
/// @notice Fuzz: any amount > uint128 max should revert.
|
|
1467
|
+
function test_insertIntoTree_fuzz_uint128Overflow_reverts(uint256 amount) public {
|
|
1468
|
+
amount = bound(amount, uint256(type(uint128).max) + 1, type(uint256).max);
|
|
1469
|
+
|
|
1470
|
+
vm.expectRevert(abi.encodeWithSelector(JBSucker.JBSucker_AmountExceedsUint128.selector, amount));
|
|
1471
|
+
sucker.test_insertIntoTree(amount, TOKEN, 1 ether, bytes32(uint256(uint160(address(this)))));
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
// =========================================================================
|
|
1475
|
+
// SECTION 18: bytes32 peer and beneficiary (INTEROP cross-VM compat)
|
|
1476
|
+
// =========================================================================
|
|
1477
|
+
|
|
1478
|
+
/// @notice peer() returns bytes32 representation of address(this).
|
|
1479
|
+
function test_peer_returnBytes32() public view {
|
|
1480
|
+
bytes32 peerValue = sucker.peer();
|
|
1481
|
+
assertEq(peerValue, bytes32(uint256(uint160(address(sucker)))), "peer should be bytes32 of address");
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
/// @notice prepare with bytes32(0) beneficiary → reverts with ZeroBeneficiary.
|
|
1485
|
+
function test_prepare_zeroBeneficiaryBytes32_reverts() public {
|
|
1486
|
+
_enableTokenMapping(TOKEN);
|
|
1487
|
+
|
|
1488
|
+
vm.expectRevert(abi.encodeWithSelector(JBSucker.JBSucker_ZeroBeneficiary.selector));
|
|
1489
|
+
sucker.prepare(10 ether, bytes32(0), 0, TOKEN);
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
/// @notice prepare with a valid 32-byte SVM beneficiary (non-EVM format) → should succeed past beneficiary check.
|
|
1493
|
+
function test_prepare_svmBeneficiary_passesCheck() public {
|
|
1494
|
+
_enableTokenMapping(TOKEN);
|
|
1495
|
+
vm.mockCall(TOKENS, abi.encodeCall(IJBTokens.tokenOf, (PROJECT_ID)), abi.encode(makeAddr("projectToken")));
|
|
1496
|
+
|
|
1497
|
+
// A typical SVM address has all 32 bytes used (high bits non-zero).
|
|
1498
|
+
bytes32 svmBeneficiary = bytes32(uint256(0xdeadbeefcafebabe1234567890abcdef1234567890abcdef1234567890abcdef));
|
|
1499
|
+
|
|
1500
|
+
// This will proceed past the beneficiary check. It may fail later (e.g., token transfer),
|
|
1501
|
+
// but should NOT fail with ZeroBeneficiary.
|
|
1502
|
+
vm.mockCall(makeAddr("projectToken"), abi.encodeWithSelector(IERC20.transferFrom.selector), abi.encode(true));
|
|
1503
|
+
|
|
1504
|
+
// May revert for other reasons (token handling), but NOT ZeroBeneficiary.
|
|
1505
|
+
try sucker.prepare(10 ether, svmBeneficiary, 0, TOKEN) {}
|
|
1506
|
+
catch (bytes memory reason) {
|
|
1507
|
+
bytes4 selector = bytes4(reason);
|
|
1508
|
+
assertTrue(selector != JBSucker.JBSucker_ZeroBeneficiary.selector, "Should not revert with ZeroBeneficiary");
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
/// @notice mapToken with bytes32(0) remoteToken disables the mapping.
|
|
1513
|
+
function test_mapToken_bytes32ZeroDisables() public {
|
|
1514
|
+
vm.mockCall(PERMISSIONS, abi.encodeWithSelector(IJBPermissions.hasPermission.selector), abi.encode(true));
|
|
1515
|
+
|
|
1516
|
+
address token = makeAddr("erc20Token");
|
|
1517
|
+
|
|
1518
|
+
// First enable a mapping.
|
|
1519
|
+
sucker.test_setRemoteToken(
|
|
1520
|
+
token,
|
|
1521
|
+
JBRemoteToken({
|
|
1522
|
+
enabled: true,
|
|
1523
|
+
emergencyHatch: false,
|
|
1524
|
+
minGas: 200_000,
|
|
1525
|
+
addr: bytes32(uint256(uint160(makeAddr("remoteA")))),
|
|
1526
|
+
minBridgeAmount: 0
|
|
1527
|
+
})
|
|
1528
|
+
);
|
|
1529
|
+
|
|
1530
|
+
// Map to bytes32(0) to disable.
|
|
1531
|
+
sucker.mapToken(
|
|
1532
|
+
JBTokenMapping({localToken: token, minGas: 200_000, remoteToken: bytes32(0), minBridgeAmount: 0})
|
|
1533
|
+
);
|
|
1534
|
+
|
|
1535
|
+
JBRemoteToken memory mapping_ = sucker.test_getRemoteToken(token);
|
|
1536
|
+
assertFalse(mapping_.enabled, "Should be disabled");
|
|
1537
|
+
// addr is preserved (so it can be re-enabled to the same remote).
|
|
1538
|
+
assertEq(mapping_.addr, bytes32(uint256(uint160(makeAddr("remoteA")))), "Remote addr should be preserved");
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
/// @notice fromRemote with same nonce (not strictly greater) should emit StaleRootRejected.
|
|
1542
|
+
function test_fromRemote_sameNonce_emitsEvent() public {
|
|
1543
|
+
sucker.test_setInboxRoot(TOKEN, 5, bytes32(uint256(0xdead)));
|
|
1544
|
+
|
|
1545
|
+
JBMessageRoot memory root = JBMessageRoot({
|
|
1546
|
+
version: sucker.MESSAGE_VERSION(),
|
|
1547
|
+
token: bytes32(uint256(uint160(TOKEN))),
|
|
1548
|
+
amount: 1 ether,
|
|
1549
|
+
remoteRoot: JBInboxTreeRoot({nonce: 5, root: bytes32(uint256(0xbeef))})
|
|
1550
|
+
});
|
|
1551
|
+
|
|
1552
|
+
vm.expectEmit(true, false, false, true, address(sucker));
|
|
1553
|
+
emit IJBSucker.StaleRootRejected({token: TOKEN, receivedNonce: 5, currentNonce: 5});
|
|
1554
|
+
|
|
1555
|
+
vm.prank(address(sucker));
|
|
1556
|
+
sucker.fromRemote(root);
|
|
1557
|
+
|
|
1558
|
+
// Inbox should remain unchanged.
|
|
1559
|
+
assertEq(sucker.test_getInboxNonce(TOKEN), 5, "Nonce should remain unchanged");
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
receive() external payable {}
|
|
1563
|
+
}
|