@bananapus/721-hook-v6 0.0.41 → 0.0.43
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/foundry.lock +1 -7
- package/foundry.toml +1 -1
- package/package.json +20 -9
- package/script/Deploy.s.sol +2 -2
- package/src/JB721Checkpoints.sol +60 -18
- package/src/JB721CheckpointsDeployer.sol +10 -5
- package/src/JB721TiersHook.sol +4 -1
- package/src/JB721TiersHookProjectDeployer.sol +68 -30
- package/src/JB721TiersHookStore.sol +1 -4
- package/src/interfaces/IJB721Checkpoints.sol +21 -14
- package/src/interfaces/IJB721CheckpointsDeployer.sol +6 -2
- package/src/interfaces/IJB721TiersHookProjectDeployer.sol +2 -0
- package/test/utils/AccessJBLib.sol +49 -0
- package/test/utils/ForTest_JB721TiersHook.sol +246 -0
- package/test/utils/TestBaseWorkflow.sol +213 -0
- package/test/utils/UnitTestSetup.sol +805 -0
- package/.gas-snapshot +0 -152
- package/ADMINISTRATION.md +0 -87
- package/ARCHITECTURE.md +0 -98
- package/AUDIT_INSTRUCTIONS.md +0 -77
- package/RISKS.md +0 -118
- package/SKILLS.md +0 -43
- package/STYLE_GUIDE.md +0 -610
- package/USER_JOURNEYS.md +0 -121
- package/assets/findings/nana-721-hook-v6-pashov-ai-audit-report-20260330-091257.md +0 -83
- package/slither-ci.config.json +0 -10
- package/test/721HookAttacks.t.sol +0 -408
- package/test/E2E/Pay_Mint_Redeem_E2E.t.sol +0 -985
- package/test/Fork.t.sol +0 -2346
- package/test/TestAuditGaps.sol +0 -1075
- package/test/TestCheckpoints.t.sol +0 -341
- package/test/TestSafeTransferReentrancy.t.sol +0 -305
- package/test/TestVotingUnitsLifecycle.t.sol +0 -313
- package/test/audit/AuditRegressions.t.sol +0 -83
- package/test/audit/CrossCurrencySplitNoPrices.t.sol +0 -123
- package/test/audit/FreshAudit.t.sol +0 -197
- package/test/audit/FutureTierPoC.t.sol +0 -39
- package/test/audit/FutureTierRemoval.t.sol +0 -47
- package/test/audit/Pass12L18.t.sol +0 -80
- package/test/audit/PayCreditsBypassTierSplits.t.sol +0 -200
- package/test/audit/ProjectDeployerAuth.t.sol +0 -266
- package/test/audit/RepoFindings.t.sol +0 -195
- package/test/audit/ReserveActivation.t.sol +0 -87
- package/test/audit/RetroactiveReserveBeneficiaryDilution.t.sol +0 -149
- package/test/audit/SameCurrencyDecimalMismatch.t.sol +0 -249
- package/test/audit/SplitCreditsMismatch.t.sol +0 -219
- package/test/audit/SplitFailureRedistribution.t.sol +0 -143
- package/test/audit/USDTVoidReturnCompat.t.sol +0 -301
- package/test/fork/ERC20CashOutFork.t.sol +0 -633
- package/test/fork/ERC20TierSplitFork.t.sol +0 -596
- package/test/fork/IssueTokensForSplitsFork.t.sol +0 -516
- package/test/invariants/TierLifecycleInvariant.t.sol +0 -188
- package/test/invariants/TieredHookStoreInvariant.t.sol +0 -86
- package/test/invariants/handlers/TierLifecycleHandler.sol +0 -300
- package/test/invariants/handlers/TierStoreHandler.sol +0 -165
- package/test/regression/BrokenTerminalDoesNotDos.t.sol +0 -277
- package/test/regression/CacheTierLookup.t.sol +0 -190
- package/test/regression/ProjectDeployerRulesets.t.sol +0 -358
- package/test/regression/ReserveBeneficiaryOverwrite.t.sol +0 -155
- package/test/regression/SplitDistributionBugs.t.sol +0 -751
- package/test/regression/SplitNoBeneficiary.t.sol +0 -140
- package/test/unit/AuditFixes_Unit.t.sol +0 -624
- package/test/unit/JB721CheckpointsDeployer_AccessControl.t.sol +0 -116
- package/test/unit/JB721TiersRulesetMetadataResolver.t.sol +0 -144
- package/test/unit/JBBitmap.t.sol +0 -170
- package/test/unit/JBIpfsDecoder.t.sol +0 -136
- package/test/unit/TierSupplyReserveCheck.t.sol +0 -221
- package/test/unit/adjustTier_Unit.t.sol +0 -1942
- package/test/unit/deployer_Unit.t.sol +0 -114
- package/test/unit/getters_constructor_Unit.t.sol +0 -593
- package/test/unit/mintFor_mintReservesFor_Unit.t.sol +0 -452
- package/test/unit/pay_CrossCurrency_Unit.t.sol +0 -530
- package/test/unit/pay_Unit.t.sol +0 -1661
- package/test/unit/redeem_Unit.t.sol +0 -473
- package/test/unit/relayBeneficiary_Unit.t.sol +0 -182
- package/test/unit/splitHookDistribution_Unit.t.sol +0 -604
- package/test/unit/tierSplitRouting_Unit.t.sol +0 -757
|
@@ -1,301 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
pragma solidity 0.8.28;
|
|
3
|
-
|
|
4
|
-
// Import the forge-std test framework.
|
|
5
|
-
import {Test} from "forge-std/Test.sol";
|
|
6
|
-
// Import IERC20 for the encodeCall used in the low-level call pattern.
|
|
7
|
-
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
8
|
-
|
|
9
|
-
/// @notice Mimics USDT's void-returning transfer/transferFrom/approve behavior.
|
|
10
|
-
/// @dev Identical to nana-core-v6/test/mock/MockUSDT.sol, inlined here to avoid cross-repo imports.
|
|
11
|
-
contract MockUSDT {
|
|
12
|
-
// Token metadata matching USDT's 6-decimal convention.
|
|
13
|
-
string public name = "Mock Tether USD";
|
|
14
|
-
// Short ticker symbol for the mock token.
|
|
15
|
-
string public symbol = "USDT";
|
|
16
|
-
// USDT uses 6 decimals, not 18 like most ERC-20 tokens.
|
|
17
|
-
uint8 public decimals = 6;
|
|
18
|
-
// Running total of all minted tokens.
|
|
19
|
-
uint256 public totalSupply;
|
|
20
|
-
|
|
21
|
-
// Maps each address to its token balance.
|
|
22
|
-
mapping(address => uint256) public balanceOf;
|
|
23
|
-
// Maps owner => spender => allowance for delegated transfers.
|
|
24
|
-
mapping(address => mapping(address => uint256)) public allowance;
|
|
25
|
-
|
|
26
|
-
/// @notice Mints tokens to a recipient (test helper, not part of USDT interface).
|
|
27
|
-
/// @param to The address to receive newly minted tokens.
|
|
28
|
-
/// @param amount The number of tokens to mint.
|
|
29
|
-
function mint(address to, uint256 amount) external {
|
|
30
|
-
// Credit the recipient's balance with the minted amount.
|
|
31
|
-
balanceOf[to] += amount;
|
|
32
|
-
// Increase the total supply to reflect the new tokens.
|
|
33
|
-
totalSupply += amount;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/// @notice Sets the spender's allowance. Returns VOID like real USDT.
|
|
37
|
-
/// @param spender The address authorized to spend tokens.
|
|
38
|
-
/// @param amount The maximum amount the spender can transfer.
|
|
39
|
-
function approve(address spender, uint256 amount) external {
|
|
40
|
-
// Record the new allowance for the caller-spender pair.
|
|
41
|
-
allowance[msg.sender][spender] = amount;
|
|
42
|
-
// Use assembly to return without any data (void), matching USDT behavior.
|
|
43
|
-
assembly {
|
|
44
|
-
return(0, 0)
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/// @notice Transfers tokens from caller to recipient. Returns VOID like real USDT.
|
|
49
|
-
/// @param to The address to receive the tokens.
|
|
50
|
-
/// @param amount The number of tokens to transfer.
|
|
51
|
-
function transfer(address to, uint256 amount) external {
|
|
52
|
-
// Ensure the sender has enough tokens to cover the transfer.
|
|
53
|
-
require(balanceOf[msg.sender] >= amount, "MockUSDT: insufficient balance");
|
|
54
|
-
// Debit the sender's balance by the transfer amount.
|
|
55
|
-
balanceOf[msg.sender] -= amount;
|
|
56
|
-
// Credit the recipient's balance with the transferred tokens.
|
|
57
|
-
balanceOf[to] += amount;
|
|
58
|
-
// Use assembly to return without any data (void), matching USDT behavior.
|
|
59
|
-
assembly {
|
|
60
|
-
return(0, 0)
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/// @notice Transfers tokens on behalf of an owner. Returns VOID like real USDT.
|
|
65
|
-
/// @param from The address whose tokens are being spent.
|
|
66
|
-
/// @param to The address to receive the tokens.
|
|
67
|
-
/// @param amount The number of tokens to transfer.
|
|
68
|
-
function transferFrom(address from, address to, uint256 amount) external {
|
|
69
|
-
// Ensure the owner has enough tokens for the transfer.
|
|
70
|
-
require(balanceOf[from] >= amount, "MockUSDT: insufficient balance");
|
|
71
|
-
// Ensure the caller is authorized to spend at least this amount.
|
|
72
|
-
require(allowance[from][msg.sender] >= amount, "MockUSDT: insufficient allowance");
|
|
73
|
-
// Reduce the caller's remaining allowance by the transferred amount.
|
|
74
|
-
allowance[from][msg.sender] -= amount;
|
|
75
|
-
// Debit the owner's balance by the transfer amount.
|
|
76
|
-
balanceOf[from] -= amount;
|
|
77
|
-
// Credit the recipient's balance with the transferred tokens.
|
|
78
|
-
balanceOf[to] += amount;
|
|
79
|
-
// Use assembly to return without any data (void), matching USDT behavior.
|
|
80
|
-
assembly {
|
|
81
|
-
return(0, 0)
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/// @notice Tests that JB721TiersHookLib's low-level call pattern (lines 605-611) handles
|
|
87
|
-
/// void-returning tokens like USDT correctly.
|
|
88
|
-
/// @dev The fix replaced `try IERC20.transfer()` with a low-level call that checks:
|
|
89
|
-
/// 1. The call succeeded (callSuccess == true)
|
|
90
|
-
/// 2. Either no data was returned (void) OR the returned data decodes to true
|
|
91
|
-
/// This test exercises that exact pattern against a void-returning mock.
|
|
92
|
-
contract USDTVoidReturnCompat is Test {
|
|
93
|
-
// The USDT mock token instance.
|
|
94
|
-
MockUSDT public usdt;
|
|
95
|
-
// Address that simulates the hook contract holding tokens for distribution.
|
|
96
|
-
address public hookCaller;
|
|
97
|
-
// Address that receives split payout funds.
|
|
98
|
-
address public splitBeneficiary;
|
|
99
|
-
|
|
100
|
-
function setUp() public {
|
|
101
|
-
// Deploy the void-returning USDT mock.
|
|
102
|
-
usdt = new MockUSDT();
|
|
103
|
-
// Label the USDT contract for clearer trace output.
|
|
104
|
-
vm.label(address(usdt), "MockUSDT");
|
|
105
|
-
// Create a caller address that simulates the hook distributing tokens.
|
|
106
|
-
hookCaller = makeAddr("hookCaller");
|
|
107
|
-
// Create a beneficiary address that receives split payouts.
|
|
108
|
-
splitBeneficiary = makeAddr("splitBeneficiary");
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// =========================================================================
|
|
112
|
-
// Test 1: The exact low-level call pattern from JB721TiersHookLib works
|
|
113
|
-
// with void-returning tokens
|
|
114
|
-
// =========================================================================
|
|
115
|
-
|
|
116
|
-
/// @notice Proves the fixed transfer pattern handles USDT's void return.
|
|
117
|
-
/// @dev This replicates the exact code from JB721TiersHookLib lines 609-611:
|
|
118
|
-
/// (bool callSuccess, bytes memory returndata) =
|
|
119
|
-
/// address(token).call(abi.encodeCall(IERC20.transfer, (split.beneficiary, amount)));
|
|
120
|
-
/// if (!callSuccess || (returndata.length != 0 && !abi.decode(returndata, (bool)))) return false;
|
|
121
|
-
function test_lowLevelTransfer_voidReturn_succeeds() public {
|
|
122
|
-
// The amount to transfer in the split payout.
|
|
123
|
-
uint256 amount = 500e6;
|
|
124
|
-
// Mint USDT to the hook caller (simulating tokens held by the 721 hook).
|
|
125
|
-
usdt.mint(hookCaller, amount);
|
|
126
|
-
|
|
127
|
-
// Execute the exact low-level call pattern from JB721TiersHookLib.
|
|
128
|
-
vm.prank(hookCaller);
|
|
129
|
-
// Encode the transfer call exactly as the library does.
|
|
130
|
-
(bool callSuccess, bytes memory returndata) =
|
|
131
|
-
address(usdt).call(abi.encodeCall(IERC20.transfer, (splitBeneficiary, amount)));
|
|
132
|
-
|
|
133
|
-
// Verify the low-level call did not revert.
|
|
134
|
-
assertTrue(callSuccess, "Low-level transfer call should succeed");
|
|
135
|
-
|
|
136
|
-
// Verify void return: USDT returns no data, so returndata.length should be 0.
|
|
137
|
-
assertEq(returndata.length, 0, "Void-returning token should return empty data");
|
|
138
|
-
|
|
139
|
-
// Verify the combined condition from line 611 evaluates correctly.
|
|
140
|
-
// For void returns: callSuccess=true, returndata.length=0, so the condition is false (no revert).
|
|
141
|
-
bool wouldRevert = !callSuccess || (returndata.length != 0 && !abi.decode(returndata, (bool)));
|
|
142
|
-
// The pattern should NOT flag this as a failure.
|
|
143
|
-
assertFalse(wouldRevert, "The fixed pattern should accept void returns as success");
|
|
144
|
-
|
|
145
|
-
// Verify the beneficiary actually received the tokens.
|
|
146
|
-
assertEq(usdt.balanceOf(splitBeneficiary), amount, "Beneficiary should receive the full transfer amount");
|
|
147
|
-
// Verify the caller's balance was debited.
|
|
148
|
-
assertEq(usdt.balanceOf(hookCaller), 0, "Caller should have zero balance after transfer");
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// =========================================================================
|
|
152
|
-
// Test 2: The pattern also works with standard bool-returning tokens
|
|
153
|
-
// =========================================================================
|
|
154
|
-
|
|
155
|
-
/// @notice Ensures the low-level call pattern still works with compliant ERC-20 tokens.
|
|
156
|
-
/// @dev A standard token returns abi.encode(true) — the pattern must accept this too.
|
|
157
|
-
function test_lowLevelTransfer_boolReturn_succeeds() public {
|
|
158
|
-
// Deploy a standard ERC-20 that returns bool (using forge's mock).
|
|
159
|
-
StandardMockERC20 standardToken = new StandardMockERC20();
|
|
160
|
-
// Label it for tracing.
|
|
161
|
-
vm.label(address(standardToken), "StandardERC20");
|
|
162
|
-
// The amount to transfer.
|
|
163
|
-
uint256 amount = 500e6;
|
|
164
|
-
// Mint tokens to the hook caller.
|
|
165
|
-
standardToken.mint(hookCaller, amount);
|
|
166
|
-
|
|
167
|
-
// Execute the exact low-level call pattern from JB721TiersHookLib.
|
|
168
|
-
vm.prank(hookCaller);
|
|
169
|
-
// Encode the transfer call.
|
|
170
|
-
(bool callSuccess, bytes memory returndata) =
|
|
171
|
-
address(standardToken).call(abi.encodeCall(IERC20.transfer, (splitBeneficiary, amount)));
|
|
172
|
-
|
|
173
|
-
// Verify the low-level call succeeded.
|
|
174
|
-
assertTrue(callSuccess, "Low-level transfer call should succeed for standard token");
|
|
175
|
-
|
|
176
|
-
// Standard tokens return 32 bytes encoding `true`.
|
|
177
|
-
assertEq(returndata.length, 32, "Standard token should return 32 bytes of data");
|
|
178
|
-
|
|
179
|
-
// Verify the combined condition correctly accepts a true return.
|
|
180
|
-
bool wouldRevert = !callSuccess || (returndata.length != 0 && !abi.decode(returndata, (bool)));
|
|
181
|
-
// Should NOT flag as failure since the decoded bool is true.
|
|
182
|
-
assertFalse(wouldRevert, "The pattern should accept bool(true) returns as success");
|
|
183
|
-
|
|
184
|
-
// Verify the beneficiary received the tokens.
|
|
185
|
-
assertEq(standardToken.balanceOf(splitBeneficiary), amount, "Beneficiary should receive tokens");
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// =========================================================================
|
|
189
|
-
// Test 3: The pattern correctly rejects a return-false token
|
|
190
|
-
// =========================================================================
|
|
191
|
-
|
|
192
|
-
/// @notice Ensures the pattern detects when a token returns false.
|
|
193
|
-
/// @dev A token that returns abi.encode(false) without reverting should be caught.
|
|
194
|
-
function test_lowLevelTransfer_returnsFalse_detected() public {
|
|
195
|
-
// Deploy a token that returns false on transfer.
|
|
196
|
-
ReturnFalseToken falseToken = new ReturnFalseToken();
|
|
197
|
-
// Label it for tracing.
|
|
198
|
-
vm.label(address(falseToken), "ReturnFalseToken");
|
|
199
|
-
// The amount doesn't matter since the token always returns false.
|
|
200
|
-
uint256 amount = 500e6;
|
|
201
|
-
// Mint tokens to the caller.
|
|
202
|
-
falseToken.mint(hookCaller, amount);
|
|
203
|
-
|
|
204
|
-
// Execute the exact low-level call pattern from JB721TiersHookLib.
|
|
205
|
-
vm.prank(hookCaller);
|
|
206
|
-
(bool callSuccess, bytes memory returndata) =
|
|
207
|
-
address(falseToken).call(abi.encodeCall(IERC20.transfer, (splitBeneficiary, amount)));
|
|
208
|
-
|
|
209
|
-
// The call itself succeeds (no revert), but the return value is false.
|
|
210
|
-
assertTrue(callSuccess, "Low-level call should not revert");
|
|
211
|
-
|
|
212
|
-
// Verify the combined condition catches the false return.
|
|
213
|
-
bool wouldRevert = !callSuccess || (returndata.length != 0 && !abi.decode(returndata, (bool)));
|
|
214
|
-
// The pattern SHOULD flag this as a failure.
|
|
215
|
-
assertTrue(wouldRevert, "The pattern should detect false return and flag failure");
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// =========================================================================
|
|
219
|
-
// Test 4: The pattern correctly handles a reverting token
|
|
220
|
-
// =========================================================================
|
|
221
|
-
|
|
222
|
-
/// @notice Ensures the pattern handles a reverting transfer gracefully.
|
|
223
|
-
/// @dev If the transfer reverts, callSuccess is false, and the pattern returns false.
|
|
224
|
-
function test_lowLevelTransfer_revert_detected() public {
|
|
225
|
-
// Use MockUSDT but don't give the caller any tokens — transfer will revert.
|
|
226
|
-
uint256 amount = 500e6;
|
|
227
|
-
|
|
228
|
-
// Execute the low-level call with insufficient balance.
|
|
229
|
-
vm.prank(hookCaller);
|
|
230
|
-
(bool callSuccess,) = address(usdt).call(abi.encodeCall(IERC20.transfer, (splitBeneficiary, amount)));
|
|
231
|
-
|
|
232
|
-
// The call should fail because the caller has zero balance.
|
|
233
|
-
assertFalse(callSuccess, "Transfer should fail with insufficient balance");
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/// @notice A standard ERC-20 mock that returns true on transfer (compliant behavior).
|
|
238
|
-
contract StandardMockERC20 {
|
|
239
|
-
// Token name for identification.
|
|
240
|
-
string public name = "Standard Mock";
|
|
241
|
-
// Token symbol.
|
|
242
|
-
string public symbol = "STD";
|
|
243
|
-
// Uses 6 decimals to match USDT comparison.
|
|
244
|
-
uint8 public decimals = 6;
|
|
245
|
-
// Running total supply.
|
|
246
|
-
uint256 public totalSupply;
|
|
247
|
-
|
|
248
|
-
// Balance mapping.
|
|
249
|
-
mapping(address => uint256) public balanceOf;
|
|
250
|
-
// Allowance mapping.
|
|
251
|
-
mapping(address => mapping(address => uint256)) public allowance;
|
|
252
|
-
|
|
253
|
-
/// @notice Mints tokens to a recipient (test helper).
|
|
254
|
-
function mint(address to, uint256 amount) external {
|
|
255
|
-
// Credit the recipient.
|
|
256
|
-
balanceOf[to] += amount;
|
|
257
|
-
// Increase total supply.
|
|
258
|
-
totalSupply += amount;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/// @notice Standard transfer that returns true.
|
|
262
|
-
function transfer(address to, uint256 amount) external returns (bool) {
|
|
263
|
-
// Check the sender has enough balance.
|
|
264
|
-
require(balanceOf[msg.sender] >= amount, "insufficient balance");
|
|
265
|
-
// Debit the sender.
|
|
266
|
-
balanceOf[msg.sender] -= amount;
|
|
267
|
-
// Credit the recipient.
|
|
268
|
-
balanceOf[to] += amount;
|
|
269
|
-
// Return true per ERC-20 spec.
|
|
270
|
-
return true;
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/// @notice A mock ERC-20 that returns false on transfer without reverting.
|
|
275
|
-
contract ReturnFalseToken {
|
|
276
|
-
// Token name for identification.
|
|
277
|
-
string public name = "Return False";
|
|
278
|
-
// Token symbol.
|
|
279
|
-
string public symbol = "RF";
|
|
280
|
-
// Uses 6 decimals.
|
|
281
|
-
uint8 public decimals = 6;
|
|
282
|
-
// Running total supply.
|
|
283
|
-
uint256 public totalSupply;
|
|
284
|
-
|
|
285
|
-
// Balance mapping.
|
|
286
|
-
mapping(address => uint256) public balanceOf;
|
|
287
|
-
|
|
288
|
-
/// @notice Mints tokens to a recipient (test helper).
|
|
289
|
-
function mint(address to, uint256 amount) external {
|
|
290
|
-
// Credit the recipient.
|
|
291
|
-
balanceOf[to] += amount;
|
|
292
|
-
// Increase total supply.
|
|
293
|
-
totalSupply += amount;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
/// @notice Always returns false without reverting (malicious/broken token behavior).
|
|
297
|
-
function transfer(address, uint256) external pure returns (bool) {
|
|
298
|
-
// Return false to simulate a failed transfer that doesn't revert.
|
|
299
|
-
return false;
|
|
300
|
-
}
|
|
301
|
-
}
|