@bananapus/core-v6 0.0.15 → 0.0.17
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/ADMINISTRATION.md +5 -1
- package/ARCHITECTURE.md +2 -1
- package/AUDIT_INSTRUCTIONS.md +342 -0
- package/CHANGE_LOG.md +375 -0
- package/README.md +6 -6
- package/RISKS.md +171 -50
- package/SKILLS.md +11 -6
- package/STYLE_GUIDE.md +16 -2
- package/USER_JOURNEYS.md +622 -0
- package/package.json +2 -2
- package/script/Deploy.s.sol +22 -13
- package/script/DeployPeriphery.s.sol +76 -52
- package/script/helpers/CoreDeploymentLib.sol +83 -35
- package/src/JBChainlinkV3PriceFeed.sol +1 -0
- package/src/JBController.sol +23 -3
- package/src/JBDeadline.sol +3 -0
- package/src/JBDirectory.sol +2 -1
- package/src/JBERC20.sol +12 -3
- package/src/JBFundAccessLimits.sol +12 -2
- package/src/JBMultiTerminal.sol +53 -10
- package/src/JBPermissions.sol +3 -0
- package/src/JBPrices.sol +8 -2
- package/src/JBProjects.sol +1 -1
- package/src/JBRulesets.sol +14 -0
- package/src/JBSplits.sol +14 -5
- package/src/JBTerminalStore.sol +57 -47
- package/src/JBTokens.sol +43 -4
- package/src/interfaces/IJBController.sol +6 -0
- package/src/interfaces/IJBPermitTerminal.sol +1 -0
- package/src/interfaces/IJBTerminalStore.sol +3 -0
- package/src/interfaces/IJBToken.sol +5 -0
- package/src/interfaces/IJBTokens.sol +13 -0
- package/src/libraries/JBFees.sol +2 -0
- package/src/libraries/JBMetadataResolver.sol +24 -7
- package/src/libraries/JBRulesetMetadataResolver.sol +21 -21
- package/src/structs/JBAccountingContext.sol +1 -0
- package/src/structs/JBAfterCashOutRecordedContext.sol +1 -0
- package/src/structs/JBAfterPayRecordedContext.sol +1 -0
- package/src/structs/JBBeforeCashOutRecordedContext.sol +5 -0
- package/src/structs/JBBeforePayRecordedContext.sol +1 -0
- package/src/structs/JBCashOutHookSpecification.sol +1 -0
- package/src/structs/JBCurrencyAmount.sol +1 -0
- package/src/structs/JBFee.sol +1 -0
- package/src/structs/JBFundAccessLimitGroup.sol +1 -0
- package/src/structs/JBPayHookSpecification.sol +1 -0
- package/src/structs/JBPermissionsData.sol +1 -0
- package/src/structs/JBRuleset.sol +1 -0
- package/src/structs/JBRulesetConfig.sol +1 -0
- package/src/structs/JBRulesetMetadata.sol +1 -0
- package/src/structs/JBRulesetWeightCache.sol +1 -0
- package/src/structs/JBRulesetWithMetadata.sol +1 -0
- package/src/structs/JBSingleAllowance.sol +1 -0
- package/src/structs/JBSplit.sol +1 -0
- package/src/structs/JBSplitGroup.sol +1 -0
- package/src/structs/JBSplitHookContext.sol +1 -0
- package/src/structs/JBTerminalConfig.sol +1 -0
- package/src/structs/JBTokenAmount.sol +1 -0
- package/test/ComprehensiveInvariant.t.sol +15 -2
- package/test/CoreExploitTests.t.sol +34 -1
- package/test/EconomicSimulation.t.sol +10 -2
- package/test/EntryPointPermutations.t.sol +17 -3
- package/test/FlashLoanAttacks.t.sol +12 -1
- package/test/PermissionEscalation.t.sol +53 -10
- package/test/RulesetTransitions.t.sol +15 -1
- package/test/SplitLoopTests.t.sol +25 -2
- package/test/TestAccessToFunds.sol +17 -2
- package/test/TestAuditResponseDesignProofs.sol +434 -0
- package/test/TestCashOut.sol +15 -1
- package/test/TestCashOutCountFor.sol +1 -1
- package/test/TestCashOutHooks.sol +47 -25
- package/test/TestCashOutTimingEdge.sol +13 -1
- package/test/TestDataHookFuzzing.sol +520 -0
- package/test/TestDurationUnderflow.sol +13 -1
- package/test/TestFeeFreeCashOutBypass.sol +617 -0
- package/test/TestFeeProcessingFailure.sol +16 -1
- package/test/TestFees.sol +14 -1
- package/test/TestInterfaceSupport.sol +20 -1
- package/test/TestJBERC20Inheritance.sol +11 -1
- package/test/TestL2SequencerPriceFeed.sol +292 -0
- package/test/TestLaunchProject.sol +13 -1
- package/test/TestMetaTx.sol +15 -1
- package/test/TestMetadataOffsetOverflow.sol +179 -0
- package/test/TestMetadataParserLib.sol +37 -4
- package/test/TestMigrationHeldFees.sol +16 -1
- package/test/TestMintTokensOf.sol +14 -1
- package/test/TestMultiTerminalSurplus.sol +348 -0
- package/test/TestMultiTokenSurplus.sol +14 -1
- package/test/TestMultipleAccessLimits.sol +23 -1
- package/test/TestPayBurnRedeemFlow.sol +16 -1
- package/test/TestPayHooks.sol +33 -14
- package/test/TestPermissions.sol +20 -1
- package/test/TestPermissionsEdge.sol +5 -1
- package/test/TestPermit2DataHook.t.sol +360 -0
- package/test/TestPermit2Terminal.sol +36 -3
- package/test/TestRulesetQueueing.sol +23 -1
- package/test/TestRulesetQueuingStress.sol +20 -1
- package/test/TestRulesetWeightCaching.sol +127 -125
- package/test/TestSplits.sol +23 -1
- package/test/TestTerminalMigration.sol +11 -1
- package/test/TestTokenFlow.sol +18 -1
- package/test/TestWeightCacheStaleAfterRejection.sol +15 -1
- package/test/WeirdTokenTests.t.sol +54 -1
- package/test/fork/TestChainlinkPriceFeedFork.sol +6 -1
- package/test/formal/BondingCurveProperties.t.sol +8 -1
- package/test/formal/FeeProperties.t.sol +7 -1
- package/test/helpers/JBTest.sol +1 -1
- package/test/helpers/TestBaseWorkflow.sol +84 -1
- package/test/invariants/Phase3DeepInvariant.t.sol +13 -2
- package/test/invariants/RulesetsInvariant.t.sol +12 -2
- package/test/invariants/TerminalStoreInvariant.t.sol +11 -2
- package/test/invariants/TokensInvariant.t.sol +13 -2
- package/test/invariants/handlers/ComprehensiveHandler.sol +19 -1
- package/test/invariants/handlers/EconomicHandler.sol +31 -1
- package/test/invariants/handlers/Phase3Handler.sol +31 -1
- package/test/invariants/handlers/RulesetsHandler.sol +5 -1
- package/test/invariants/handlers/TerminalStoreHandler.sol +6 -1
- package/test/invariants/handlers/TokensHandler.sol +1 -1
- package/test/mock/MockERC20.sol +0 -2
- package/test/mock/MockMaliciousBeneficiary.sol +2 -1
- package/test/mock/MockMaliciousSplitHook.sol +2 -1
- package/test/mock/MockPriceFeed.sol +1 -1
- package/test/regression/HoldFeesCashOutReserved.t.sol +415 -0
- package/test/regression/WeightCacheBoundary.t.sol +291 -0
- package/test/units/static/JBChainlinkV3PriceFeed/TestPriceFeed.sol +0 -1
- package/test/units/static/JBController/JBControllerSetup.sol +10 -1
- package/test/units/static/JBController/TestBurnTokensOf.sol +8 -1
- package/test/units/static/JBController/TestClaimTokensFor.sol +4 -1
- package/test/units/static/JBController/TestDeployErc20For.sol +7 -1
- package/test/units/static/JBController/TestLaunchProjectFor.sol +21 -1
- package/test/units/static/JBController/TestLaunchRulesetsFor.sol +21 -1
- package/test/units/static/JBController/TestMigrateController.sol +10 -1
- package/test/units/static/JBController/TestMintTokensOfUnits.sol +10 -1
- package/test/units/static/JBController/TestPayReservedTokenToTerminal.sol +4 -1
- package/test/units/static/JBController/TestReceiveMigrationFrom.sol +5 -1
- package/test/units/static/JBController/TestRulesetViews.sol +7 -1
- package/test/units/static/JBController/TestSendReservedTokensToSplitsOf.sol +21 -1
- package/test/units/static/JBController/TestSetSplitGroupsOf.sol +6 -1
- package/test/units/static/JBController/TestSetTokenFor.sol +13 -1
- package/test/units/static/JBController/TestSetUriOf.sol +5 -1
- package/test/units/static/JBController/TestTransferCreditsFrom.sol +11 -1
- package/test/units/static/JBDeadline/TestDeadlineFuzz.sol +12 -1
- package/test/units/static/JBDirectory/JBDirectorySetup.sol +4 -1
- package/test/units/static/JBDirectory/TestPrimaryTerminalOf.sol +5 -1
- package/test/units/static/JBDirectory/TestSetControllerOf.sol +11 -1
- package/test/units/static/JBDirectory/TestSetControllerOfMigrationOrder.sol +7 -1
- package/test/units/static/JBDirectory/TestSetPrimaryTerminalOf.sol +11 -1
- package/test/units/static/JBDirectory/TestSetTerminalsOf.sol +10 -1
- package/test/units/static/JBERC20/JBERC20Setup.sol +2 -1
- package/test/units/static/JBERC20/SigUtils.sol +2 -0
- package/test/units/static/JBERC20/TestInitialize.sol +1 -1
- package/test/units/static/JBERC20/TestName.sol +1 -1
- package/test/units/static/JBERC20/TestNonces.sol +3 -1
- package/test/units/static/JBERC20/TestSymbol.sol +1 -1
- package/test/units/static/JBFeelessAdresses/JBFeelessSetup.sol +2 -1
- package/test/units/static/JBFeelessAdresses/TestInterfaces.sol +2 -1
- package/test/units/static/JBFeelessAdresses/TestSetFeelessAddress.sol +1 -1
- package/test/units/static/JBFees/TestFeesFuzz.sol +1 -1
- package/test/units/static/JBFixedPointNumber/TestAdjustDecimals.sol +0 -1
- package/test/units/static/JBFixedPointNumber/TestAdjustDecimalsFuzz.sol +0 -1
- package/test/units/static/JBFundAccessLimits/JBFundAccessSetup.sol +3 -1
- package/test/units/static/JBFundAccessLimits/TestFundAccessLimitsEdge.sol +4 -1
- package/test/units/static/JBFundAccessLimits/TestPayoutLimitOf.sol +4 -1
- package/test/units/static/JBFundAccessLimits/TestPayoutLimitsOf.sol +8 -1
- package/test/units/static/JBFundAccessLimits/TestSetFundAccessLimitsFor.sol +8 -1
- package/test/units/static/JBFundAccessLimits/TestSurplusAllowanceOf.sol +4 -1
- package/test/units/static/JBFundAccessLimits/TestSurplusAllowancesOf.sol +7 -1
- package/test/units/static/JBMetadataResolver/TestGetDataFor.sol +1 -1
- package/test/units/static/JBMetadataResolver/TestMetadataResolverEdgeCases.sol +2 -1
- package/test/units/static/JBMetadataResolver/TestMetadataResolverFuzz.sol +2 -1
- package/test/units/static/JBMultiTerminal/JBMultiTerminalSetup.sol +12 -1
- package/test/units/static/JBMultiTerminal/TestAccountingContextsOf.sol +9 -1
- package/test/units/static/JBMultiTerminal/TestAddAccountingContextsFor.sol +18 -2
- package/test/units/static/JBMultiTerminal/TestAddToBalanceOf.sol +44 -9
- package/test/units/static/JBMultiTerminal/TestCashOutTokensOf.sol +48 -23
- package/test/units/static/JBMultiTerminal/TestExecutePayout.sol +18 -2
- package/test/units/static/JBMultiTerminal/TestExecuteProcessFee.sol +13 -3
- package/test/units/static/JBMultiTerminal/TestMigrateBalanceOf.sol +21 -4
- package/test/units/static/JBMultiTerminal/TestPay.sol +35 -7
- package/test/units/static/JBMultiTerminal/TestProcessHeldFeesOf.sol +206 -19
- package/test/units/static/JBMultiTerminal/TestSendPayoutsOf.sol +15 -1
- package/test/units/static/JBMultiTerminal/TestUseAllowanceOf.sol +297 -1
- package/test/units/static/JBPermissions/JBPermissionsSetup.sol +2 -1
- package/test/units/static/JBPermissions/TestHasPermission.sol +1 -1
- package/test/units/static/JBPermissions/TestHasPermissions.sol +1 -1
- package/test/units/static/JBPermissions/TestSetPermissionsFor.sol +3 -1
- package/test/units/static/JBPrices/JBPricesSetup.sol +6 -1
- package/test/units/static/JBPrices/TestAddPriceFeedFor.sol +6 -1
- package/test/units/static/JBPrices/TestPricePerUnitOf.sol +4 -1
- package/test/units/static/JBPrices/TestPrices.sol +4 -1
- package/test/units/static/JBProjects/JBProjectsSetup.sol +2 -1
- package/test/units/static/JBProjects/TestCreateFor.sol +3 -1
- package/test/units/static/JBProjects/TestInitialProject.sol +2 -1
- package/test/units/static/JBProjects/TestInterfaces.sol +0 -1
- package/test/units/static/JBProjects/TestSetResolver.sol +2 -1
- package/test/units/static/JBProjects/TestTokenUri.sol +3 -1
- package/test/units/static/JBRulesetMetadataResolver/TestSetCashOutTaxRateTo.sol +9 -1
- package/test/units/static/JBRulesets/JBRulesetsSetup.sol +3 -1
- package/test/units/static/JBRulesets/TestCurrentApprovalStatusForLatestRulesetOf.sol +9 -1
- package/test/units/static/JBRulesets/TestCurrentOf.sol +10 -1
- package/test/units/static/JBRulesets/TestGetRulesetOf.sol +7 -1
- package/test/units/static/JBRulesets/TestLatestQueuedRulesetOf.sol +9 -1
- package/test/units/static/JBRulesets/TestRulesets.sol +12 -1
- package/test/units/static/JBRulesets/TestRulesetsOf.sol +1 -1
- package/test/units/static/JBRulesets/TestUpcomingRulesetOf.sol +10 -1
- package/test/units/static/JBRulesets/TestUpdateRulesetWeightCache.sol +6 -1
- package/test/units/static/JBSplits/JBSplitsSetup.sol +3 -1
- package/test/units/static/JBSplits/TestSelfManagedSplitGroups.sol +63 -13
- package/test/units/static/JBSplits/TestSetSplitGroupsOf.sol +8 -1
- package/test/units/static/JBSplits/TestSplitsLockedEdge.sol +6 -1
- package/test/units/static/JBSplits/TestSplitsOf.sol +1 -1
- package/test/units/static/JBSplits/TestSplitsPacking.sol +5 -2
- package/test/units/static/JBSurplus/TestSurplusFuzz.sol +3 -1
- package/test/units/static/JBTerminalStore/JBTerminalStoreSetup.sol +5 -1
- package/test/units/static/JBTerminalStore/TestCurrentReclaimableSurplusOf.sol +14 -1
- package/test/units/static/JBTerminalStore/TestCurrentSurplusOf.sol +14 -1
- package/test/units/static/JBTerminalStore/TestCurrentTotalSurplusOf.sol +3 -1
- package/test/units/static/JBTerminalStore/TestRecordCashOutsFor.sol +92 -1
- package/test/units/static/JBTerminalStore/TestRecordPaymentFrom.sol +15 -1
- package/test/units/static/JBTerminalStore/TestRecordPayoutFor.sol +13 -1
- package/test/units/static/JBTerminalStore/TestRecordTerminalMigration.sol +8 -1
- package/test/units/static/JBTerminalStore/TestRecordUsedAllowanceOf.sol +16 -1
- package/test/units/static/JBTerminalStore/TestUint224Overflow.sol +15 -1
- package/test/units/static/JBTokens/JBTokensSetup.sol +5 -1
- package/test/units/static/JBTokens/TestBurnFrom.sol +4 -1
- package/test/units/static/JBTokens/TestClaimTokensFor.sol +4 -1
- package/test/units/static/JBTokens/TestDeployERC20ForUnits.sol +4 -1
- package/test/units/static/JBTokens/TestMintFor.sol +4 -1
- package/test/units/static/JBTokens/TestSetTokenFor.sol +4 -1
- package/test/units/static/JBTokens/TestTotalBalanceOf.sol +1 -1
- package/test/units/static/JBTokens/TestTotalSupplyOf.sol +1 -1
- package/test/units/static/JBTokens/TestTransferCreditsFrom.sol +3 -1
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.6;
|
|
3
|
+
|
|
4
|
+
import {TestBaseWorkflow} from "./helpers/TestBaseWorkflow.sol";
|
|
5
|
+
import {JBTokens} from "../src/JBTokens.sol";
|
|
6
|
+
import {IJBController} from "../src/interfaces/IJBController.sol";
|
|
7
|
+
import {IJBMultiTerminal} from "../src/interfaces/IJBMultiTerminal.sol";
|
|
8
|
+
import {IJBSplitHook} from "../src/interfaces/IJBSplitHook.sol";
|
|
9
|
+
import {JBConstants} from "../src/libraries/JBConstants.sol";
|
|
10
|
+
import {JBAccountingContext} from "../src/structs/JBAccountingContext.sol";
|
|
11
|
+
import {JBCurrencyAmount} from "../src/structs/JBCurrencyAmount.sol";
|
|
12
|
+
import {JBFundAccessLimitGroup} from "../src/structs/JBFundAccessLimitGroup.sol";
|
|
13
|
+
import {JBRulesetConfig} from "../src/structs/JBRulesetConfig.sol";
|
|
14
|
+
import {JBRulesetMetadata} from "../src/structs/JBRulesetMetadata.sol";
|
|
15
|
+
import {JBSplit} from "../src/structs/JBSplit.sol";
|
|
16
|
+
import {JBSplitGroup} from "../src/structs/JBSplitGroup.sol";
|
|
17
|
+
import {JBTerminalConfig} from "../src/structs/JBTerminalConfig.sol";
|
|
18
|
+
import {mulDiv} from "@prb/math/src/Common.sol";
|
|
19
|
+
|
|
20
|
+
/// @notice Tests that the fee-free cashout bypass via same-terminal round-trip is closed:
|
|
21
|
+
/// fees apply on cashout up to the cumulative fee-free payout amount, then deplete.
|
|
22
|
+
contract TestFeeFreeCashOutBypass is TestBaseWorkflow {
|
|
23
|
+
IJBController private _controller;
|
|
24
|
+
IJBMultiTerminal private _terminal;
|
|
25
|
+
JBTokens private _tokens;
|
|
26
|
+
|
|
27
|
+
address private _projectOwner;
|
|
28
|
+
address private _attacker;
|
|
29
|
+
|
|
30
|
+
// Project A: sends payouts to project B via same terminal.
|
|
31
|
+
uint256 private _projectIdA;
|
|
32
|
+
// Project B: pass-through project with cashOutTaxRate = 0.
|
|
33
|
+
uint256 private _projectIdB;
|
|
34
|
+
|
|
35
|
+
uint112 private _weight = 1000 * 10 ** 18;
|
|
36
|
+
uint224 private _payoutLimit = 10 ether;
|
|
37
|
+
|
|
38
|
+
function setUp() public override {
|
|
39
|
+
super.setUp();
|
|
40
|
+
|
|
41
|
+
_projectOwner = multisig();
|
|
42
|
+
_attacker = makeAddr("attacker");
|
|
43
|
+
_controller = jbController();
|
|
44
|
+
_terminal = jbMultiTerminal();
|
|
45
|
+
_tokens = jbTokens();
|
|
46
|
+
|
|
47
|
+
// Shared terminal config.
|
|
48
|
+
JBTerminalConfig[] memory _terminalConfigurations = new JBTerminalConfig[](1);
|
|
49
|
+
JBAccountingContext[] memory _tokensToAccept = new JBAccountingContext[](1);
|
|
50
|
+
_tokensToAccept[0] = JBAccountingContext({
|
|
51
|
+
token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
52
|
+
});
|
|
53
|
+
_terminalConfigurations[0] =
|
|
54
|
+
JBTerminalConfig({terminal: _terminal, accountingContextsToAccept: _tokensToAccept});
|
|
55
|
+
|
|
56
|
+
// --- Fee-receiving project (project #1) ---
|
|
57
|
+
JBRulesetConfig[] memory _feeRulesetConfig = new JBRulesetConfig[](1);
|
|
58
|
+
_feeRulesetConfig[0].mustStartAtOrAfter = 0;
|
|
59
|
+
_feeRulesetConfig[0].duration = 0;
|
|
60
|
+
_feeRulesetConfig[0].weight = _weight;
|
|
61
|
+
_feeRulesetConfig[0].metadata = _zeroTaxMetadata();
|
|
62
|
+
_feeRulesetConfig[0].splitGroups = new JBSplitGroup[](0);
|
|
63
|
+
_feeRulesetConfig[0].fundAccessLimitGroups = new JBFundAccessLimitGroup[](0);
|
|
64
|
+
|
|
65
|
+
_controller.launchProjectFor({
|
|
66
|
+
owner: address(420),
|
|
67
|
+
projectUri: "fee-project",
|
|
68
|
+
rulesetConfigurations: _feeRulesetConfig,
|
|
69
|
+
terminalConfigurations: _terminalConfigurations,
|
|
70
|
+
memo: ""
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// --- Project B: pass-through, cashOutTaxRate = 0 ---
|
|
74
|
+
JBRulesetConfig[] memory _bRulesetConfig = new JBRulesetConfig[](1);
|
|
75
|
+
_bRulesetConfig[0].mustStartAtOrAfter = 0;
|
|
76
|
+
_bRulesetConfig[0].duration = 0;
|
|
77
|
+
_bRulesetConfig[0].weight = _weight;
|
|
78
|
+
_bRulesetConfig[0].metadata = _zeroTaxMetadata();
|
|
79
|
+
_bRulesetConfig[0].splitGroups = new JBSplitGroup[](0);
|
|
80
|
+
_bRulesetConfig[0].fundAccessLimitGroups = new JBFundAccessLimitGroup[](0);
|
|
81
|
+
|
|
82
|
+
_projectIdB = _controller.launchProjectFor({
|
|
83
|
+
owner: _projectOwner,
|
|
84
|
+
projectUri: "project-b",
|
|
85
|
+
rulesetConfigurations: _bRulesetConfig,
|
|
86
|
+
terminalConfigurations: _terminalConfigurations,
|
|
87
|
+
memo: ""
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// --- Project A: routes 100% payouts to project B (same terminal) ---
|
|
91
|
+
JBSplit[] memory _splits = new JBSplit[](1);
|
|
92
|
+
_splits[0] = JBSplit({
|
|
93
|
+
preferAddToBalance: false,
|
|
94
|
+
percent: JBConstants.SPLITS_TOTAL_PERCENT,
|
|
95
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
96
|
+
projectId: uint64(_projectIdB),
|
|
97
|
+
beneficiary: payable(_attacker),
|
|
98
|
+
lockedUntil: 0,
|
|
99
|
+
hook: IJBSplitHook(address(0))
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
JBSplitGroup[] memory _splitGroups = new JBSplitGroup[](1);
|
|
103
|
+
_splitGroups[0] = JBSplitGroup({groupId: uint32(uint160(JBConstants.NATIVE_TOKEN)), splits: _splits});
|
|
104
|
+
|
|
105
|
+
JBFundAccessLimitGroup[] memory _fundAccessLimitGroup = new JBFundAccessLimitGroup[](1);
|
|
106
|
+
JBCurrencyAmount[] memory _payoutLimits = new JBCurrencyAmount[](1);
|
|
107
|
+
_payoutLimits[0] = JBCurrencyAmount({amount: _payoutLimit, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))});
|
|
108
|
+
_fundAccessLimitGroup[0] = JBFundAccessLimitGroup({
|
|
109
|
+
terminal: address(_terminal),
|
|
110
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
111
|
+
payoutLimits: _payoutLimits,
|
|
112
|
+
surplusAllowances: new JBCurrencyAmount[](0)
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
JBRulesetConfig[] memory _aRulesetConfig = new JBRulesetConfig[](1);
|
|
116
|
+
_aRulesetConfig[0].mustStartAtOrAfter = 0;
|
|
117
|
+
_aRulesetConfig[0].duration = 0;
|
|
118
|
+
_aRulesetConfig[0].weight = _weight;
|
|
119
|
+
_aRulesetConfig[0].metadata = _zeroTaxMetadata();
|
|
120
|
+
_aRulesetConfig[0].splitGroups = _splitGroups;
|
|
121
|
+
_aRulesetConfig[0].fundAccessLimitGroups = _fundAccessLimitGroup;
|
|
122
|
+
|
|
123
|
+
_projectIdA = _controller.launchProjectFor({
|
|
124
|
+
owner: _projectOwner,
|
|
125
|
+
projectUri: "project-a",
|
|
126
|
+
rulesetConfigurations: _aRulesetConfig,
|
|
127
|
+
terminalConfigurations: _terminalConfigurations,
|
|
128
|
+
memo: ""
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/// @notice After an intra-terminal payout from A → B, cashing out from B charges a fee
|
|
133
|
+
/// even though B has cashOutTaxRate = 0.
|
|
134
|
+
function testCashOutChargesFeeAfterFeeFreePayout() external {
|
|
135
|
+
uint256 payAmount = 10 ether;
|
|
136
|
+
vm.deal(_attacker, payAmount);
|
|
137
|
+
|
|
138
|
+
// Deploy ERC-20 for project B so the attacker can cash out.
|
|
139
|
+
vm.prank(_projectOwner);
|
|
140
|
+
_controller.deployERC20For(_projectIdB, "ProjectB", "PB", bytes32(0));
|
|
141
|
+
|
|
142
|
+
// Step 1: Pay project A.
|
|
143
|
+
vm.prank(_attacker);
|
|
144
|
+
_terminal.pay{value: payAmount}({
|
|
145
|
+
projectId: _projectIdA,
|
|
146
|
+
amount: payAmount,
|
|
147
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
148
|
+
beneficiary: _attacker,
|
|
149
|
+
minReturnedTokens: 0,
|
|
150
|
+
memo: "",
|
|
151
|
+
metadata: new bytes(0)
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Step 2: Project A sends payouts → funds route to project B (same terminal, fee-free).
|
|
155
|
+
_terminal.sendPayoutsOf({
|
|
156
|
+
projectId: _projectIdA,
|
|
157
|
+
amount: _payoutLimit,
|
|
158
|
+
currency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
159
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
160
|
+
minTokensPaidOut: 0
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// The attacker now has tokens in project B (from the pay-in routed by the split).
|
|
164
|
+
uint256 attackerTokenBalance = _tokens.totalBalanceOf(_attacker, _projectIdB);
|
|
165
|
+
assertGt(attackerTokenBalance, 0, "attacker should have project B tokens");
|
|
166
|
+
|
|
167
|
+
// Step 3: Cash out from project B.
|
|
168
|
+
vm.prank(_attacker);
|
|
169
|
+
uint256 reclaimAmount = _terminal.cashOutTokensOf({
|
|
170
|
+
holder: _attacker,
|
|
171
|
+
projectId: _projectIdB,
|
|
172
|
+
cashOutCount: attackerTokenBalance,
|
|
173
|
+
tokenToReclaim: JBConstants.NATIVE_TOKEN,
|
|
174
|
+
minTokensReclaimed: 0,
|
|
175
|
+
beneficiary: payable(_attacker),
|
|
176
|
+
metadata: new bytes(0)
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// With cashOutTaxRate = 0 the gross reclaim equals the full terminal balance (the payout amount).
|
|
180
|
+
// A 2.5% fee must have been charged, so net = gross * (1 - 2.5%).
|
|
181
|
+
uint256 expectedFee = mulDiv(_payoutLimit, 25, 1000); // 2.5% of 10 ETH
|
|
182
|
+
uint256 expectedNet = _payoutLimit - expectedFee;
|
|
183
|
+
assertApproxEqAbs(reclaimAmount, expectedNet, 2, "fee should be charged on cashout");
|
|
184
|
+
assertLt(reclaimAmount, _payoutLimit, "attacker should not get full amount (fee must apply)");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/// @notice Direct pay-in → cashout with cashOutTaxRate = 0 remains fee-free (no payout flag set).
|
|
188
|
+
function testCashOutRemainsFeeFreForDirectPayIn() external {
|
|
189
|
+
uint256 payAmount = 10 ether;
|
|
190
|
+
address user = makeAddr("user");
|
|
191
|
+
vm.deal(user, payAmount);
|
|
192
|
+
|
|
193
|
+
// Deploy ERC-20 for project B.
|
|
194
|
+
vm.prank(_projectOwner);
|
|
195
|
+
_controller.deployERC20For(_projectIdB, "ProjectB", "PB", bytes32(0));
|
|
196
|
+
|
|
197
|
+
// Pay directly into project B (no payout from another project).
|
|
198
|
+
vm.prank(user);
|
|
199
|
+
_terminal.pay{value: payAmount}({
|
|
200
|
+
projectId: _projectIdB,
|
|
201
|
+
amount: payAmount,
|
|
202
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
203
|
+
beneficiary: user,
|
|
204
|
+
minReturnedTokens: 0,
|
|
205
|
+
memo: "",
|
|
206
|
+
metadata: new bytes(0)
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
uint256 userTokenBalance = _tokens.totalBalanceOf(user, _projectIdB);
|
|
210
|
+
assertGt(userTokenBalance, 0, "user should have project B tokens");
|
|
211
|
+
|
|
212
|
+
// Cash out — should be fee-free since no intra-terminal payout was received.
|
|
213
|
+
vm.prank(user);
|
|
214
|
+
uint256 reclaimAmount = _terminal.cashOutTokensOf({
|
|
215
|
+
holder: user,
|
|
216
|
+
projectId: _projectIdB,
|
|
217
|
+
cashOutCount: userTokenBalance,
|
|
218
|
+
tokenToReclaim: JBConstants.NATIVE_TOKEN,
|
|
219
|
+
minTokensReclaimed: 0,
|
|
220
|
+
beneficiary: payable(user),
|
|
221
|
+
metadata: new bytes(0)
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// With cashOutTaxRate = 0, full cashout returns the entire surplus 1:1.
|
|
225
|
+
// No fee should be charged.
|
|
226
|
+
assertEq(reclaimAmount, payAmount, "direct pay-in cashout should be fee-free");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/// @notice After the fee-free surplus is fully consumed, subsequent direct pay-ins cash out fee-free.
|
|
230
|
+
function testSurplusDepletionRestoresFeeFreeCashOut() external {
|
|
231
|
+
uint256 payAmount = 10 ether;
|
|
232
|
+
vm.deal(_attacker, payAmount * 2);
|
|
233
|
+
|
|
234
|
+
vm.prank(_projectOwner);
|
|
235
|
+
_controller.deployERC20For(_projectIdB, "ProjectB", "PB", bytes32(0));
|
|
236
|
+
|
|
237
|
+
// Pay project A and trigger payout → accumulates fee-free surplus on project B.
|
|
238
|
+
vm.prank(_attacker);
|
|
239
|
+
_terminal.pay{value: payAmount}({
|
|
240
|
+
projectId: _projectIdA,
|
|
241
|
+
amount: payAmount,
|
|
242
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
243
|
+
beneficiary: _attacker,
|
|
244
|
+
minReturnedTokens: 0,
|
|
245
|
+
memo: "",
|
|
246
|
+
metadata: new bytes(0)
|
|
247
|
+
});
|
|
248
|
+
_terminal.sendPayoutsOf({
|
|
249
|
+
projectId: _projectIdA,
|
|
250
|
+
amount: _payoutLimit,
|
|
251
|
+
currency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
252
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
253
|
+
minTokensPaidOut: 0
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// Cash out all tokens from project B — fee applied against fee-free surplus, which is now depleted.
|
|
257
|
+
uint256 tokensFromPayout = _tokens.totalBalanceOf(_attacker, _projectIdB);
|
|
258
|
+
vm.prank(_attacker);
|
|
259
|
+
uint256 firstReclaim = _terminal.cashOutTokensOf({
|
|
260
|
+
holder: _attacker,
|
|
261
|
+
projectId: _projectIdB,
|
|
262
|
+
cashOutCount: tokensFromPayout,
|
|
263
|
+
tokenToReclaim: JBConstants.NATIVE_TOKEN,
|
|
264
|
+
minTokensReclaimed: 0,
|
|
265
|
+
beneficiary: payable(_attacker),
|
|
266
|
+
metadata: new bytes(0)
|
|
267
|
+
});
|
|
268
|
+
// First cashout should have had a fee deducted.
|
|
269
|
+
assertLt(firstReclaim, _payoutLimit, "first cashout should have fee deducted");
|
|
270
|
+
|
|
271
|
+
// Now pay project B directly with fresh funds.
|
|
272
|
+
vm.prank(_attacker);
|
|
273
|
+
_terminal.pay{value: payAmount}({
|
|
274
|
+
projectId: _projectIdB,
|
|
275
|
+
amount: payAmount,
|
|
276
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
277
|
+
beneficiary: _attacker,
|
|
278
|
+
minReturnedTokens: 0,
|
|
279
|
+
memo: "",
|
|
280
|
+
metadata: new bytes(0)
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
uint256 newTokens = _tokens.totalBalanceOf(_attacker, _projectIdB);
|
|
284
|
+
assertGt(newTokens, 0, "attacker should have new tokens");
|
|
285
|
+
|
|
286
|
+
// Cash out again — surplus is depleted, so this direct pay-in should be fee-free.
|
|
287
|
+
vm.prank(_attacker);
|
|
288
|
+
uint256 secondReclaim = _terminal.cashOutTokensOf({
|
|
289
|
+
holder: _attacker,
|
|
290
|
+
projectId: _projectIdB,
|
|
291
|
+
cashOutCount: newTokens,
|
|
292
|
+
tokenToReclaim: JBConstants.NATIVE_TOKEN,
|
|
293
|
+
minTokensReclaimed: 0,
|
|
294
|
+
beneficiary: payable(_attacker),
|
|
295
|
+
metadata: new bytes(0)
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// After surplus depletion, direct pay-in cashout should be fee-free.
|
|
299
|
+
assertEq(secondReclaim, payAmount, "direct pay-in cashout should be fee-free after surplus depleted");
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/// @notice Fee-free surplus only covers the exact payout amount — partial cashout leaves remainder.
|
|
303
|
+
function testPartialCashOutConsumesPartialSurplus() external {
|
|
304
|
+
uint256 payAmount = 10 ether;
|
|
305
|
+
vm.deal(_attacker, payAmount);
|
|
306
|
+
|
|
307
|
+
vm.prank(_projectOwner);
|
|
308
|
+
_controller.deployERC20For(_projectIdB, "ProjectB", "PB", bytes32(0));
|
|
309
|
+
|
|
310
|
+
// Pay project A and trigger payout → 10 ETH fee-free surplus on project B.
|
|
311
|
+
vm.prank(_attacker);
|
|
312
|
+
_terminal.pay{value: payAmount}({
|
|
313
|
+
projectId: _projectIdA,
|
|
314
|
+
amount: payAmount,
|
|
315
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
316
|
+
beneficiary: _attacker,
|
|
317
|
+
minReturnedTokens: 0,
|
|
318
|
+
memo: "",
|
|
319
|
+
metadata: new bytes(0)
|
|
320
|
+
});
|
|
321
|
+
_terminal.sendPayoutsOf({
|
|
322
|
+
projectId: _projectIdA,
|
|
323
|
+
amount: _payoutLimit,
|
|
324
|
+
currency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
325
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
326
|
+
minTokensPaidOut: 0
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// Cash out half the tokens — should consume roughly half the fee-free surplus.
|
|
330
|
+
uint256 totalTokens = _tokens.totalBalanceOf(_attacker, _projectIdB);
|
|
331
|
+
uint256 halfTokens = totalTokens / 2;
|
|
332
|
+
|
|
333
|
+
vm.prank(_attacker);
|
|
334
|
+
uint256 firstReclaim = _terminal.cashOutTokensOf({
|
|
335
|
+
holder: _attacker,
|
|
336
|
+
projectId: _projectIdB,
|
|
337
|
+
cashOutCount: halfTokens,
|
|
338
|
+
tokenToReclaim: JBConstants.NATIVE_TOKEN,
|
|
339
|
+
minTokensReclaimed: 0,
|
|
340
|
+
beneficiary: payable(_attacker),
|
|
341
|
+
metadata: new bytes(0)
|
|
342
|
+
});
|
|
343
|
+
// First half-cashout should have fee deducted (fee-free surplus partially consumed).
|
|
344
|
+
assertLt(firstReclaim, _payoutLimit / 2, "partial cashout should have fee deducted");
|
|
345
|
+
|
|
346
|
+
// Cash out the remaining tokens — should also have fee (remaining surplus covers it).
|
|
347
|
+
uint256 remainingTokens = _tokens.totalBalanceOf(_attacker, _projectIdB);
|
|
348
|
+
vm.prank(_attacker);
|
|
349
|
+
uint256 secondReclaim = _terminal.cashOutTokensOf({
|
|
350
|
+
holder: _attacker,
|
|
351
|
+
projectId: _projectIdB,
|
|
352
|
+
cashOutCount: remainingTokens,
|
|
353
|
+
tokenToReclaim: JBConstants.NATIVE_TOKEN,
|
|
354
|
+
minTokensReclaimed: 0,
|
|
355
|
+
beneficiary: payable(_attacker),
|
|
356
|
+
metadata: new bytes(0)
|
|
357
|
+
});
|
|
358
|
+
// Second cashout also has fee deducted from remaining surplus.
|
|
359
|
+
assertLt(secondReclaim, firstReclaim + 1 ether, "second partial cashout should also have fee");
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/// @notice Griefing with a tiny payout only costs the victim fees on that tiny amount.
|
|
363
|
+
function testGriefingWithTinyPayoutIsScoped() external {
|
|
364
|
+
address victim = makeAddr("victim");
|
|
365
|
+
vm.deal(victim, 10 ether);
|
|
366
|
+
vm.deal(_attacker, 1); // 1 wei for the griefing payout
|
|
367
|
+
|
|
368
|
+
vm.prank(_projectOwner);
|
|
369
|
+
_controller.deployERC20For(_projectIdB, "ProjectB", "PB", bytes32(0));
|
|
370
|
+
|
|
371
|
+
// Victim pays directly into project B.
|
|
372
|
+
vm.prank(victim);
|
|
373
|
+
_terminal.pay{value: 10 ether}({
|
|
374
|
+
projectId: _projectIdB,
|
|
375
|
+
amount: 10 ether,
|
|
376
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
377
|
+
beneficiary: victim,
|
|
378
|
+
minReturnedTokens: 0,
|
|
379
|
+
memo: "",
|
|
380
|
+
metadata: new bytes(0)
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
// Attacker triggers a 1 wei payout to project B via project A to set fee-free surplus.
|
|
384
|
+
// First fund project A with 1 wei.
|
|
385
|
+
vm.prank(_attacker);
|
|
386
|
+
_terminal.pay{value: 1}({
|
|
387
|
+
projectId: _projectIdA,
|
|
388
|
+
amount: 1,
|
|
389
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
390
|
+
beneficiary: _attacker,
|
|
391
|
+
minReturnedTokens: 0,
|
|
392
|
+
memo: "",
|
|
393
|
+
metadata: new bytes(0)
|
|
394
|
+
});
|
|
395
|
+
_terminal.sendPayoutsOf({
|
|
396
|
+
projectId: _projectIdA,
|
|
397
|
+
amount: 1,
|
|
398
|
+
currency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
399
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
400
|
+
minTokensPaidOut: 0
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// Victim cashes out — fee should only apply to 1 wei of surplus, not their full 10 ETH.
|
|
404
|
+
uint256 victimTokens = _tokens.totalBalanceOf(victim, _projectIdB);
|
|
405
|
+
vm.prank(victim);
|
|
406
|
+
uint256 reclaimAmount = _terminal.cashOutTokensOf({
|
|
407
|
+
holder: victim,
|
|
408
|
+
projectId: _projectIdB,
|
|
409
|
+
cashOutCount: victimTokens,
|
|
410
|
+
tokenToReclaim: JBConstants.NATIVE_TOKEN,
|
|
411
|
+
minTokensReclaimed: 0,
|
|
412
|
+
beneficiary: payable(victim),
|
|
413
|
+
metadata: new bytes(0)
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
// Fee on 1 wei is 0 (rounds down). Victim should get essentially their full amount back.
|
|
417
|
+
// The key assertion: victim is NOT penalized with a fee on their full 10 ETH.
|
|
418
|
+
uint256 feeOn1Wei = mulDiv(1, 25, 1000); // 0 (rounds down)
|
|
419
|
+
assertGe(reclaimAmount, 10 ether - feeOn1Wei - 1, "griefing should cost at most fee on 1 wei");
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/// @notice Non-zero cashOutTaxRate applies fees to the full reclaim, ignoring surplus.
|
|
423
|
+
function testNonZeroTaxRateIgnoresSurplus() external {
|
|
424
|
+
uint256 payAmount = 10 ether;
|
|
425
|
+
vm.deal(_attacker, payAmount);
|
|
426
|
+
|
|
427
|
+
// Launch project C with cashOutTaxRate = 5000 (50%).
|
|
428
|
+
JBTerminalConfig[] memory _terminalConfigurations = new JBTerminalConfig[](1);
|
|
429
|
+
JBAccountingContext[] memory _tokensToAccept = new JBAccountingContext[](1);
|
|
430
|
+
_tokensToAccept[0] = JBAccountingContext({
|
|
431
|
+
token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
432
|
+
});
|
|
433
|
+
_terminalConfigurations[0] =
|
|
434
|
+
JBTerminalConfig({terminal: _terminal, accountingContextsToAccept: _tokensToAccept});
|
|
435
|
+
|
|
436
|
+
JBRulesetMetadata memory taxMeta = _zeroTaxMetadata();
|
|
437
|
+
taxMeta.cashOutTaxRate = 5000; // 50%
|
|
438
|
+
|
|
439
|
+
JBRulesetConfig[] memory _cRulesetConfig = new JBRulesetConfig[](1);
|
|
440
|
+
_cRulesetConfig[0].mustStartAtOrAfter = 0;
|
|
441
|
+
_cRulesetConfig[0].duration = 0;
|
|
442
|
+
_cRulesetConfig[0].weight = _weight;
|
|
443
|
+
_cRulesetConfig[0].metadata = taxMeta;
|
|
444
|
+
_cRulesetConfig[0].splitGroups = new JBSplitGroup[](0);
|
|
445
|
+
_cRulesetConfig[0].fundAccessLimitGroups = new JBFundAccessLimitGroup[](0);
|
|
446
|
+
|
|
447
|
+
uint256 projectIdC = _controller.launchProjectFor({
|
|
448
|
+
owner: _projectOwner,
|
|
449
|
+
projectUri: "project-c",
|
|
450
|
+
rulesetConfigurations: _cRulesetConfig,
|
|
451
|
+
terminalConfigurations: _terminalConfigurations,
|
|
452
|
+
memo: ""
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
vm.prank(_projectOwner);
|
|
456
|
+
_controller.deployERC20For(projectIdC, "ProjectC", "PC", bytes32(0));
|
|
457
|
+
|
|
458
|
+
// Pay and cash out — non-zero tax rate means full fee applies (surplus irrelevant).
|
|
459
|
+
vm.prank(_attacker);
|
|
460
|
+
_terminal.pay{value: payAmount}({
|
|
461
|
+
projectId: projectIdC,
|
|
462
|
+
amount: payAmount,
|
|
463
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
464
|
+
beneficiary: _attacker,
|
|
465
|
+
minReturnedTokens: 0,
|
|
466
|
+
memo: "",
|
|
467
|
+
metadata: new bytes(0)
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
uint256 tokens = _tokens.totalBalanceOf(_attacker, projectIdC);
|
|
471
|
+
vm.prank(_attacker);
|
|
472
|
+
uint256 reclaimAmount = _terminal.cashOutTokensOf({
|
|
473
|
+
holder: _attacker,
|
|
474
|
+
projectId: projectIdC,
|
|
475
|
+
cashOutCount: tokens,
|
|
476
|
+
tokenToReclaim: JBConstants.NATIVE_TOKEN,
|
|
477
|
+
minTokensReclaimed: 0,
|
|
478
|
+
beneficiary: payable(_attacker),
|
|
479
|
+
metadata: new bytes(0)
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
// With non-zero tax, the 2.5% fee applies to the full bonding curve output.
|
|
483
|
+
// Full cashout (count == supply) returns the entire surplus regardless of tax rate,
|
|
484
|
+
// so gross = 10 ETH, fee = 2.5%, net = 9.75 ETH.
|
|
485
|
+
assertLt(reclaimAmount, payAmount, "non-zero tax should charge fee on full amount");
|
|
486
|
+
uint256 expectedFee = mulDiv(payAmount, 25, 1000);
|
|
487
|
+
uint256 expectedNet = payAmount - expectedFee;
|
|
488
|
+
assertApproxEqAbs(reclaimAmount, expectedNet, 2, "fee should apply to full bonding curve output");
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/// @notice Multiple payouts accumulate fee-free surplus correctly.
|
|
492
|
+
function testMultiplePayoutsAccumulateSurplus() external {
|
|
493
|
+
uint256 payAmount = 10 ether;
|
|
494
|
+
// Need to fund project A twice, so give attacker enough.
|
|
495
|
+
vm.deal(_attacker, payAmount * 2);
|
|
496
|
+
|
|
497
|
+
vm.prank(_projectOwner);
|
|
498
|
+
_controller.deployERC20For(_projectIdB, "ProjectB", "PB", bytes32(0));
|
|
499
|
+
|
|
500
|
+
// First payout cycle: pay A, send payouts → 10 ETH fee-free surplus on B.
|
|
501
|
+
vm.prank(_attacker);
|
|
502
|
+
_terminal.pay{value: payAmount}({
|
|
503
|
+
projectId: _projectIdA,
|
|
504
|
+
amount: payAmount,
|
|
505
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
506
|
+
beneficiary: _attacker,
|
|
507
|
+
minReturnedTokens: 0,
|
|
508
|
+
memo: "",
|
|
509
|
+
metadata: new bytes(0)
|
|
510
|
+
});
|
|
511
|
+
_terminal.sendPayoutsOf({
|
|
512
|
+
projectId: _projectIdA,
|
|
513
|
+
amount: _payoutLimit,
|
|
514
|
+
currency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
515
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
516
|
+
minTokensPaidOut: 0
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
// Queue a new ruleset so the payout limit resets for the next cycle.
|
|
520
|
+
JBSplit[] memory _splits = new JBSplit[](1);
|
|
521
|
+
_splits[0] = JBSplit({
|
|
522
|
+
preferAddToBalance: false,
|
|
523
|
+
percent: JBConstants.SPLITS_TOTAL_PERCENT,
|
|
524
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
525
|
+
projectId: uint64(_projectIdB),
|
|
526
|
+
beneficiary: payable(_attacker),
|
|
527
|
+
lockedUntil: 0,
|
|
528
|
+
hook: IJBSplitHook(address(0))
|
|
529
|
+
});
|
|
530
|
+
JBSplitGroup[] memory _splitGroups = new JBSplitGroup[](1);
|
|
531
|
+
_splitGroups[0] = JBSplitGroup({groupId: uint32(uint160(JBConstants.NATIVE_TOKEN)), splits: _splits});
|
|
532
|
+
|
|
533
|
+
JBCurrencyAmount[] memory _payoutLimits = new JBCurrencyAmount[](1);
|
|
534
|
+
_payoutLimits[0] = JBCurrencyAmount({amount: _payoutLimit, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))});
|
|
535
|
+
JBFundAccessLimitGroup[] memory _fundAccessLimitGroup = new JBFundAccessLimitGroup[](1);
|
|
536
|
+
_fundAccessLimitGroup[0] = JBFundAccessLimitGroup({
|
|
537
|
+
terminal: address(_terminal),
|
|
538
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
539
|
+
payoutLimits: _payoutLimits,
|
|
540
|
+
surplusAllowances: new JBCurrencyAmount[](0)
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
JBRulesetConfig[] memory _newRuleset = new JBRulesetConfig[](1);
|
|
544
|
+
_newRuleset[0].mustStartAtOrAfter = 0;
|
|
545
|
+
_newRuleset[0].duration = 0;
|
|
546
|
+
_newRuleset[0].weight = _weight;
|
|
547
|
+
_newRuleset[0].metadata = _zeroTaxMetadata();
|
|
548
|
+
_newRuleset[0].splitGroups = _splitGroups;
|
|
549
|
+
_newRuleset[0].fundAccessLimitGroups = _fundAccessLimitGroup;
|
|
550
|
+
|
|
551
|
+
vm.prank(_projectOwner);
|
|
552
|
+
_controller.queueRulesetsOf({projectId: _projectIdA, rulesetConfigurations: _newRuleset, memo: ""});
|
|
553
|
+
|
|
554
|
+
// Second payout cycle: pay A again, send payouts → another 10 ETH, total 20 ETH surplus.
|
|
555
|
+
vm.prank(_attacker);
|
|
556
|
+
_terminal.pay{value: payAmount}({
|
|
557
|
+
projectId: _projectIdA,
|
|
558
|
+
amount: payAmount,
|
|
559
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
560
|
+
beneficiary: _attacker,
|
|
561
|
+
minReturnedTokens: 0,
|
|
562
|
+
memo: "",
|
|
563
|
+
metadata: new bytes(0)
|
|
564
|
+
});
|
|
565
|
+
_terminal.sendPayoutsOf({
|
|
566
|
+
projectId: _projectIdA,
|
|
567
|
+
amount: _payoutLimit,
|
|
568
|
+
currency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
569
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
570
|
+
minTokensPaidOut: 0
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
// Cash out all tokens from B — fee should apply to the full 20 ETH surplus.
|
|
574
|
+
uint256 allTokens = _tokens.totalBalanceOf(_attacker, _projectIdB);
|
|
575
|
+
vm.prank(_attacker);
|
|
576
|
+
uint256 reclaimAmount = _terminal.cashOutTokensOf({
|
|
577
|
+
holder: _attacker,
|
|
578
|
+
projectId: _projectIdB,
|
|
579
|
+
cashOutCount: allTokens,
|
|
580
|
+
tokenToReclaim: JBConstants.NATIVE_TOKEN,
|
|
581
|
+
minTokensReclaimed: 0,
|
|
582
|
+
beneficiary: payable(_attacker),
|
|
583
|
+
metadata: new bytes(0)
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
// The 2.5% fee should apply to the entire reclaim (covered by 20 ETH surplus).
|
|
587
|
+
// Gross reclaim ≈ 20 ETH (full balance of B), fee = 2.5% of that.
|
|
588
|
+
assertLt(reclaimAmount, 20 ether, "fee should be charged on accumulated surplus");
|
|
589
|
+
uint256 expectedFee = mulDiv(20 ether, 25, 1000);
|
|
590
|
+
uint256 expectedNet = 20 ether - expectedFee;
|
|
591
|
+
assertApproxEqAbs(reclaimAmount, expectedNet, 2, "fee should apply to full 20 ETH surplus");
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function _zeroTaxMetadata() internal pure returns (JBRulesetMetadata memory) {
|
|
595
|
+
return JBRulesetMetadata({
|
|
596
|
+
reservedPercent: 0,
|
|
597
|
+
cashOutTaxRate: 0,
|
|
598
|
+
baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
599
|
+
pausePay: false,
|
|
600
|
+
pauseCreditTransfers: false,
|
|
601
|
+
allowOwnerMinting: false,
|
|
602
|
+
allowSetCustomToken: false,
|
|
603
|
+
allowTerminalMigration: false,
|
|
604
|
+
allowSetTerminals: false,
|
|
605
|
+
ownerMustSendPayouts: false,
|
|
606
|
+
allowSetController: false,
|
|
607
|
+
allowAddAccountingContext: true,
|
|
608
|
+
allowAddPriceFeed: false,
|
|
609
|
+
holdFees: false,
|
|
610
|
+
useTotalSurplusForCashOuts: false,
|
|
611
|
+
useDataHookForPay: false,
|
|
612
|
+
useDataHookForCashOut: false,
|
|
613
|
+
dataHook: address(0),
|
|
614
|
+
metadata: 0
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
}
|
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
pragma solidity ^0.8.6;
|
|
3
3
|
|
|
4
|
-
import
|
|
4
|
+
import {TestBaseWorkflow} from "./helpers/TestBaseWorkflow.sol";
|
|
5
|
+
import {JBMultiTerminal} from "../src/JBMultiTerminal.sol";
|
|
6
|
+
import {JBTerminalStore} from "../src/JBTerminalStore.sol";
|
|
7
|
+
import {JBTokens} from "../src/JBTokens.sol";
|
|
8
|
+
import {IJBController} from "../src/interfaces/IJBController.sol";
|
|
9
|
+
import {IJBRulesetApprovalHook} from "../src/interfaces/IJBRulesetApprovalHook.sol";
|
|
10
|
+
import {IJBTerminal} from "../src/interfaces/IJBTerminal.sol";
|
|
11
|
+
import {JBConstants} from "../src/libraries/JBConstants.sol";
|
|
12
|
+
import {JBFees} from "../src/libraries/JBFees.sol";
|
|
13
|
+
import {JBAccountingContext} from "../src/structs/JBAccountingContext.sol";
|
|
14
|
+
import {JBCurrencyAmount} from "../src/structs/JBCurrencyAmount.sol";
|
|
15
|
+
import {JBFundAccessLimitGroup} from "../src/structs/JBFundAccessLimitGroup.sol";
|
|
16
|
+
import {JBRulesetConfig} from "../src/structs/JBRulesetConfig.sol";
|
|
17
|
+
import {JBRulesetMetadata} from "../src/structs/JBRulesetMetadata.sol";
|
|
18
|
+
import {JBSplitGroup} from "../src/structs/JBSplitGroup.sol";
|
|
19
|
+
import {JBTerminalConfig} from "../src/structs/JBTerminalConfig.sol";
|
|
5
20
|
|
|
6
21
|
/// @notice Tests for the fee processing try-catch path in JBMultiTerminal.
|
|
7
22
|
/// Proves that when fee payment reverts, the fee amount is credited back to
|
package/test/TestFees.sol
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
pragma solidity ^0.8.6;
|
|
3
3
|
|
|
4
|
-
import
|
|
4
|
+
import {TestBaseWorkflow} from "./helpers/TestBaseWorkflow.sol";
|
|
5
|
+
import {IJBController} from "../src/interfaces/IJBController.sol";
|
|
6
|
+
import {IJBMultiTerminal} from "../src/interfaces/IJBMultiTerminal.sol";
|
|
7
|
+
import {IJBRulesetApprovalHook} from "../src/interfaces/IJBRulesetApprovalHook.sol";
|
|
8
|
+
import {IJBRulesets} from "../src/interfaces/IJBRulesets.sol";
|
|
9
|
+
import {JBConstants} from "../src/libraries/JBConstants.sol";
|
|
10
|
+
import {JBAccountingContext} from "../src/structs/JBAccountingContext.sol";
|
|
11
|
+
import {JBCurrencyAmount} from "../src/structs/JBCurrencyAmount.sol";
|
|
12
|
+
import {JBFee} from "../src/structs/JBFee.sol";
|
|
13
|
+
import {JBFundAccessLimitGroup} from "../src/structs/JBFundAccessLimitGroup.sol";
|
|
14
|
+
import {JBRulesetConfig} from "../src/structs/JBRulesetConfig.sol";
|
|
15
|
+
import {JBRulesetMetadata} from "../src/structs/JBRulesetMetadata.sol";
|
|
16
|
+
import {JBSplitGroup} from "../src/structs/JBSplitGroup.sol";
|
|
17
|
+
import {JBTerminalConfig} from "../src/structs/JBTerminalConfig.sol";
|
|
5
18
|
|
|
6
19
|
// Projects can be launched.
|
|
7
20
|
contract TestFees_Local is TestBaseWorkflow {
|