@bananapus/core-v6 0.0.37 → 0.0.38

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.
Files changed (287) hide show
  1. package/foundry.lock +1 -7
  2. package/foundry.toml +1 -1
  3. package/package.json +19 -7
  4. package/src/JBController.sol +19 -1
  5. package/src/JBMultiTerminal.sol +68 -34
  6. package/src/JBTerminalStore.sol +6 -6
  7. package/src/interfaces/IJBController.sol +4 -1
  8. package/src/libraries/JBFees.sol +47 -9
  9. package/src/libraries/JBPayoutSplitGroupLib.sol +2 -2
  10. package/src/periphery/JBMatchingPriceFeed.sol +1 -1
  11. package/test/mock/MockMaliciousBeneficiary.sol +15 -15
  12. package/ADMINISTRATION.md +0 -103
  13. package/ARCHITECTURE.md +0 -133
  14. package/AUDIT_INSTRUCTIONS.md +0 -139
  15. package/RISKS.md +0 -215
  16. package/SKILLS.md +0 -55
  17. package/STYLE_GUIDE.md +0 -610
  18. package/USER_JOURNEYS.md +0 -215
  19. package/script/Deploy.s.sol +0 -124
  20. package/script/DeployPeriphery.s.sol +0 -354
  21. package/slither-ci.config.json +0 -10
  22. package/test/AuditFixes.t.sol +0 -808
  23. package/test/ComprehensiveInvariant.t.sol +0 -306
  24. package/test/CoreExploitTests.t.sol +0 -2741
  25. package/test/EconomicSimulation.t.sol +0 -348
  26. package/test/EntryPointPermutations.t.sol +0 -684
  27. package/test/FlashLoanAttacks.t.sol +0 -797
  28. package/test/PermissionEscalation.t.sol +0 -711
  29. package/test/PermissionsInvariant.t.sol +0 -403
  30. package/test/RulesetTransitions.t.sol +0 -713
  31. package/test/SplitLoopTests.t.sol +0 -752
  32. package/test/TestAccessToFunds.sol +0 -2683
  33. package/test/TestAuditResponseDesignProofs.sol +0 -434
  34. package/test/TestCashOut.sol +0 -198
  35. package/test/TestCashOutCountFor.sol +0 -271
  36. package/test/TestCashOutHooks.sol +0 -351
  37. package/test/TestCashOutTimingEdge.sol +0 -241
  38. package/test/TestDataHookFuzzing.sol +0 -524
  39. package/test/TestDurationUnderflow.sol +0 -233
  40. package/test/TestFeeFreeCashOutBypass.sol +0 -949
  41. package/test/TestFeeProcessingFailure.sol +0 -218
  42. package/test/TestFees.sol +0 -619
  43. package/test/TestForwardedTokenConsumption.sol +0 -425
  44. package/test/TestInterfaceSupport.sol +0 -81
  45. package/test/TestJBERC20Inheritance.sol +0 -103
  46. package/test/TestL2SequencerPriceFeed.sol +0 -292
  47. package/test/TestLaunchProject.sol +0 -188
  48. package/test/TestMetaTx.sol +0 -217
  49. package/test/TestMetadataOffsetOverflow.sol +0 -179
  50. package/test/TestMetadataParserLib.sol +0 -471
  51. package/test/TestMigrationHeldFees.sol +0 -255
  52. package/test/TestMintTokensOf.sol +0 -185
  53. package/test/TestMultiTerminalSurplus.sol +0 -348
  54. package/test/TestMultiTokenSurplus.sol +0 -202
  55. package/test/TestMultipleAccessLimits.sol +0 -664
  56. package/test/TestPayBurnRedeemFlow.sol +0 -195
  57. package/test/TestPayHooks.sol +0 -209
  58. package/test/TestPermissions.sol +0 -324
  59. package/test/TestPermissionsEdge.sol +0 -290
  60. package/test/TestPermit2DataHook.t.sol +0 -360
  61. package/test/TestPermit2Terminal.sol +0 -372
  62. package/test/TestRulesetQueueing.sol +0 -1025
  63. package/test/TestRulesetQueuingStress.sol +0 -806
  64. package/test/TestRulesetWeightCaching.sol +0 -178
  65. package/test/TestSplits.sol +0 -391
  66. package/test/TestTerminalMigration.sol +0 -274
  67. package/test/TestTerminalPreviewParity.sol +0 -208
  68. package/test/TestTokenFlow.sol +0 -191
  69. package/test/TestWeightCacheStaleAfterRejection.sol +0 -303
  70. package/test/WeirdTokenTests.t.sol +0 -817
  71. package/test/audit/CashOutReenterPay.t.sol +0 -501
  72. package/test/audit/CodexHeldFeeRounding.t.sol +0 -159
  73. package/test/audit/CodexMigrationFeeFailure.t.sol +0 -163
  74. package/test/audit/CrossTerminalSurplusSpoof.t.sol +0 -140
  75. package/test/audit/CycledSurplusAllowanceReset.t.sol +0 -184
  76. package/test/audit/FeeFreeSurplusLifecycle.t.sol +0 -399
  77. package/test/audit/FeeFreeSurplusStale.t.sol +0 -248
  78. package/test/audit/USDTVoidReturnCompat.t.sol +0 -525
  79. package/test/fork/TestChainlinkPriceFeedFork.sol +0 -254
  80. package/test/fork/TestSequencerPriceFeedFork.sol +0 -168
  81. package/test/fork/TestTerminalPreviewParityFork.sol +0 -108
  82. package/test/formal/BondingCurveProperties.t.sol +0 -420
  83. package/test/formal/FeeProperties.t.sol +0 -252
  84. package/test/invariants/Phase3DeepInvariant.t.sol +0 -412
  85. package/test/invariants/RulesetsInvariant.t.sol +0 -125
  86. package/test/invariants/TerminalStoreInvariant.t.sol +0 -227
  87. package/test/invariants/TokensInvariant.t.sol +0 -195
  88. package/test/invariants/handlers/ComprehensiveHandler.sol +0 -303
  89. package/test/invariants/handlers/EconomicHandler.sol +0 -377
  90. package/test/invariants/handlers/Phase3Handler.sol +0 -443
  91. package/test/invariants/handlers/RulesetsHandler.sol +0 -115
  92. package/test/invariants/handlers/TerminalStoreHandler.sol +0 -151
  93. package/test/invariants/handlers/TokensHandler.sol +0 -126
  94. package/test/regression/HoldFeesCashOutReserved.t.sol +0 -415
  95. package/test/regression/WeightCacheBoundary.t.sol +0 -291
  96. package/test/trees/JBController/burnTokensOf.tree +0 -9
  97. package/test/trees/JBController/claimTokensFor.tree +0 -5
  98. package/test/trees/JBController/deployERC20For.tree +0 -5
  99. package/test/trees/JBController/getRulesetOf.tree +0 -5
  100. package/test/trees/JBController/launchProjectFor.tree +0 -12
  101. package/test/trees/JBController/launchRulesetsFor.tree +0 -8
  102. package/test/trees/JBController/migrateController.tree +0 -12
  103. package/test/trees/JBController/mintTokensOf.tree +0 -12
  104. package/test/trees/JBController/payReservedTokenToTerminal.tree +0 -8
  105. package/test/trees/JBController/receiveMigrationFrom.tree +0 -4
  106. package/test/trees/JBController/sendReservedTokensToSplitsOf.tree +0 -12
  107. package/test/trees/JBController/setMetadataOf.tree +0 -5
  108. package/test/trees/JBController/setSplitGroupsOf.tree +0 -5
  109. package/test/trees/JBController/setTokenFor.tree +0 -5
  110. package/test/trees/JBController/transferCreditsFrom.tree +0 -8
  111. package/test/trees/JBDirectory/primaryTerminalOf.tree +0 -8
  112. package/test/trees/JBDirectory/setControllerOf.tree +0 -11
  113. package/test/trees/JBDirectory/setPrimaryTerminalOf.tree +0 -15
  114. package/test/trees/JBDirectory/setTerminalsOf.tree +0 -11
  115. package/test/trees/JBERC20/initialize.tree +0 -7
  116. package/test/trees/JBERC20/name.tree +0 -5
  117. package/test/trees/JBERC20/nonces.tree +0 -5
  118. package/test/trees/JBERC20/symbol.tree +0 -5
  119. package/test/trees/JBFeelessAddresses/setFeelessAddress.tree +0 -5
  120. package/test/trees/JBFeelessAddresses/supportsInterface.tree +0 -5
  121. package/test/trees/JBFundAccessLimits/payoutLimitOf.tree +0 -5
  122. package/test/trees/JBFundAccessLimits/payoutLimitsOf.tree +0 -8
  123. package/test/trees/JBFundAccessLimits/setFundAccessLimitsFor.tree +0 -18
  124. package/test/trees/JBFundAccessLimits/surplusAllowanceOf.tree +0 -5
  125. package/test/trees/JBFundAccessLimits/surplusAllowancesOf.tree +0 -8
  126. package/test/trees/JBMetadataResolver/getDataFor.tree +0 -8
  127. package/test/trees/JBMultiTerminal/accountingContextsOf.tree +0 -5
  128. package/test/trees/JBMultiTerminal/addAccountingContextsFor.tree +0 -10
  129. package/test/trees/JBMultiTerminal/addToBalanceOf.tree +0 -23
  130. package/test/trees/JBMultiTerminal/cashOutTokensOf.tree +0 -23
  131. package/test/trees/JBMultiTerminal/executePayout.tree +0 -32
  132. package/test/trees/JBMultiTerminal/executeProcessFee.tree +0 -14
  133. package/test/trees/JBMultiTerminal/migrateBalanceOf.tree +0 -12
  134. package/test/trees/JBMultiTerminal/pay.tree +0 -23
  135. package/test/trees/JBMultiTerminal/processHeldFeesOf.tree +0 -8
  136. package/test/trees/JBMultiTerminal/sendPayoutsOf.tree +0 -34
  137. package/test/trees/JBMultiTerminal/useAllowanceOf.tree +0 -16
  138. package/test/trees/JBPermissions/hasPermission.tree +0 -8
  139. package/test/trees/JBPermissions/hasPermissions.tree +0 -8
  140. package/test/trees/JBPermissions/setPermissionsFor.tree +0 -5
  141. package/test/trees/JBPrices/addPriceFeedFor.tree +0 -14
  142. package/test/trees/JBPrices/pricePerUnitOf.tree +0 -11
  143. package/test/trees/JBProjects/createFor.tree +0 -11
  144. package/test/trees/JBProjects/setTokenUriResolver.tree +0 -5
  145. package/test/trees/JBProjects/supportsInterface.tree +0 -9
  146. package/test/trees/JBProjects/tokenURI.tree +0 -5
  147. package/test/trees/JBRulesets/currentApprovalStatusForLatestRulesetOf.tree +0 -8
  148. package/test/trees/JBRulesets/currentOf.tree +0 -12
  149. package/test/trees/JBRulesets/getRulesetOf.tree +0 -5
  150. package/test/trees/JBRulesets/latestQueuedRulesetOf.tree +0 -10
  151. package/test/trees/JBRulesets/rulesetsOf.tree +0 -11
  152. package/test/trees/JBRulesets/upcomingRulesetOf.tree +0 -20
  153. package/test/trees/JBRulesets/updateRulesetWeightCache.tree +0 -5
  154. package/test/trees/JBSplits/setSplitGroupsOf.tree +0 -17
  155. package/test/trees/JBSplits/splitsOf.tree +0 -5
  156. package/test/trees/JBTerminalStore/currentReclaimableSurplusOf.tree +0 -16
  157. package/test/trees/JBTerminalStore/currentSurplusOf.tree +0 -25
  158. package/test/trees/JBTerminalStore/currentTotalSurplusOf.tree +0 -5
  159. package/test/trees/JBTerminalStore/recordCashOutsFor.tree +0 -16
  160. package/test/trees/JBTerminalStore/recordPaymentFrom.tree +0 -14
  161. package/test/trees/JBTerminalStore/recordPayoutFor.tree +0 -10
  162. package/test/trees/JBTerminalStore/recordTerminalMigration.tree +0 -5
  163. package/test/trees/JBTerminalStore/recordUsedAllowanceOf.tree +0 -10
  164. package/test/trees/JBTokens/burnFrom.tree +0 -10
  165. package/test/trees/JBTokens/claimTokensFor.tree +0 -10
  166. package/test/trees/JBTokens/deployERC20For.tree +0 -12
  167. package/test/trees/JBTokens/mintFor.tree +0 -10
  168. package/test/trees/JBTokens/setTokenFor.tree +0 -11
  169. package/test/trees/JBTokens/totalBalanceOf.tree +0 -5
  170. package/test/trees/JBTokens/totalSupplyOf.tree +0 -5
  171. package/test/trees/JBTokens/transferCreditsFrom.tree +0 -8
  172. package/test/trees/mintTokensOf.tree +0 -12
  173. package/test/units/static/JBChainlinkV3PriceFeed/TestPriceFeed.sol +0 -223
  174. package/test/units/static/JBController/JBControllerSetup.sol +0 -50
  175. package/test/units/static/JBController/TestBurnTokensOf.sol +0 -114
  176. package/test/units/static/JBController/TestClaimTokensFor.sol +0 -63
  177. package/test/units/static/JBController/TestDeployErc20For.sol +0 -86
  178. package/test/units/static/JBController/TestLaunchProjectFor.sol +0 -302
  179. package/test/units/static/JBController/TestLaunchRulesetsFor.sol +0 -342
  180. package/test/units/static/JBController/TestMigrateController.sol +0 -157
  181. package/test/units/static/JBController/TestMintTokensOfUnits.sol +0 -111
  182. package/test/units/static/JBController/TestOmnichainRulesetOperator.sol +0 -324
  183. package/test/units/static/JBController/TestPayReservedTokenToTerminal.sol +0 -74
  184. package/test/units/static/JBController/TestPreviewMintOf.sol +0 -117
  185. package/test/units/static/JBController/TestReceiveMigrationFrom.sol +0 -99
  186. package/test/units/static/JBController/TestRulesetViews.sol +0 -225
  187. package/test/units/static/JBController/TestSendReservedTokensToSplitsOf.sol +0 -615
  188. package/test/units/static/JBController/TestSetSplitGroupsOf.sol +0 -68
  189. package/test/units/static/JBController/TestSetTokenFor.sol +0 -239
  190. package/test/units/static/JBController/TestSetUriOf.sol +0 -57
  191. package/test/units/static/JBController/TestTransferCreditsFrom.sol +0 -169
  192. package/test/units/static/JBDeadline/TestDeadlineFuzz.sol +0 -211
  193. package/test/units/static/JBDirectory/JBDirectorySetup.sol +0 -26
  194. package/test/units/static/JBDirectory/TestPrimaryTerminalOf.sol +0 -126
  195. package/test/units/static/JBDirectory/TestSetControllerOf.sol +0 -183
  196. package/test/units/static/JBDirectory/TestSetControllerOfMigrationOrder.sol +0 -104
  197. package/test/units/static/JBDirectory/TestSetPrimaryTerminalOf.sol +0 -179
  198. package/test/units/static/JBDirectory/TestSetTerminalsOf.sol +0 -137
  199. package/test/units/static/JBERC20/JBERC20Setup.sol +0 -34
  200. package/test/units/static/JBERC20/SigUtils.sol +0 -36
  201. package/test/units/static/JBERC20/TestInitialize.sol +0 -60
  202. package/test/units/static/JBERC20/TestName.sol +0 -30
  203. package/test/units/static/JBERC20/TestNonces.sol +0 -62
  204. package/test/units/static/JBERC20/TestSymbol.sol +0 -31
  205. package/test/units/static/JBFeelessAdresses/JBFeelessSetup.sol +0 -22
  206. package/test/units/static/JBFeelessAdresses/TestInterfaces.sol +0 -30
  207. package/test/units/static/JBFeelessAdresses/TestSetFeelessAddress.sol +0 -35
  208. package/test/units/static/JBFees/TestFeesFuzz.sol +0 -79
  209. package/test/units/static/JBFixedPointNumber/TestAdjustDecimals.sol +0 -16
  210. package/test/units/static/JBFixedPointNumber/TestAdjustDecimalsFuzz.sol +0 -71
  211. package/test/units/static/JBFundAccessLimits/JBFundAccessSetup.sol +0 -24
  212. package/test/units/static/JBFundAccessLimits/TestFundAccessLimitsEdge.sol +0 -163
  213. package/test/units/static/JBFundAccessLimits/TestPayoutLimitOf.sol +0 -59
  214. package/test/units/static/JBFundAccessLimits/TestPayoutLimitsOf.sol +0 -101
  215. package/test/units/static/JBFundAccessLimits/TestSetFundAccessLimitsFor.sol +0 -189
  216. package/test/units/static/JBFundAccessLimits/TestSurplusAllowanceOf.sol +0 -64
  217. package/test/units/static/JBFundAccessLimits/TestSurplusAllowancesOf.sol +0 -102
  218. package/test/units/static/JBMetadataResolver/TestGetDataFor.sol +0 -90
  219. package/test/units/static/JBMetadataResolver/TestMetadataResolverEdgeCases.sol +0 -247
  220. package/test/units/static/JBMetadataResolver/TestMetadataResolverFuzz.sol +0 -229
  221. package/test/units/static/JBMultiTerminal/JBMultiTerminalSetup.sol +0 -50
  222. package/test/units/static/JBMultiTerminal/TestAccountingContextsOf.sol +0 -72
  223. package/test/units/static/JBMultiTerminal/TestAddAccountingContextsFor.sol +0 -289
  224. package/test/units/static/JBMultiTerminal/TestAddToBalanceOf.sol +0 -474
  225. package/test/units/static/JBMultiTerminal/TestCashOutTokensOf.sol +0 -624
  226. package/test/units/static/JBMultiTerminal/TestExecutePayout.sol +0 -578
  227. package/test/units/static/JBMultiTerminal/TestExecuteProcessFee.sol +0 -202
  228. package/test/units/static/JBMultiTerminal/TestMigrateBalanceOf.sol +0 -222
  229. package/test/units/static/JBMultiTerminal/TestPay.sol +0 -604
  230. package/test/units/static/JBMultiTerminal/TestPreviewCashOutFrom.sol +0 -117
  231. package/test/units/static/JBMultiTerminal/TestPreviewPayFor.sol +0 -114
  232. package/test/units/static/JBMultiTerminal/TestProcessHeldFeesOf.sol +0 -228
  233. package/test/units/static/JBMultiTerminal/TestSelfPayRevert.sol +0 -55
  234. package/test/units/static/JBMultiTerminal/TestSendPayoutsOf.sol +0 -257
  235. package/test/units/static/JBMultiTerminal/TestUseAllowanceOf.sol +0 -611
  236. package/test/units/static/JBPermissions/JBPermissionsSetup.sol +0 -20
  237. package/test/units/static/JBPermissions/TestHasPermission.sol +0 -50
  238. package/test/units/static/JBPermissions/TestHasPermissions.sol +0 -93
  239. package/test/units/static/JBPermissions/TestSetPermissionsFor.sol +0 -64
  240. package/test/units/static/JBPrices/JBPricesSetup.sol +0 -32
  241. package/test/units/static/JBPrices/TestAddPriceFeedFor.sol +0 -107
  242. package/test/units/static/JBPrices/TestPricePerUnitOf.sol +0 -132
  243. package/test/units/static/JBPrices/TestPrices.sol +0 -265
  244. package/test/units/static/JBProjects/JBProjectsSetup.sol +0 -22
  245. package/test/units/static/JBProjects/TestCreateFor.sol +0 -71
  246. package/test/units/static/JBProjects/TestInitialProject.sol +0 -21
  247. package/test/units/static/JBProjects/TestInterfaces.sol +0 -26
  248. package/test/units/static/JBProjects/TestSetResolver.sol +0 -37
  249. package/test/units/static/JBProjects/TestTokenUri.sol +0 -40
  250. package/test/units/static/JBRulesetMetadataResolver/TestSetCashOutTaxRateTo.sol +0 -108
  251. package/test/units/static/JBRulesets/JBRulesetsSetup.sol +0 -24
  252. package/test/units/static/JBRulesets/TestCurrentApprovalStatusForLatestRulesetOf.sol +0 -265
  253. package/test/units/static/JBRulesets/TestCurrentOf.sol +0 -242
  254. package/test/units/static/JBRulesets/TestGetRulesetOf.sol +0 -100
  255. package/test/units/static/JBRulesets/TestLatestQueuedRulesetOf.sol +0 -260
  256. package/test/units/static/JBRulesets/TestRulesets.sol +0 -632
  257. package/test/units/static/JBRulesets/TestRulesetsOf.sol +0 -37
  258. package/test/units/static/JBRulesets/TestUpcomingRulesetOf.sol +0 -522
  259. package/test/units/static/JBRulesets/TestUpdateRulesetWeightCache.sol +0 -96
  260. package/test/units/static/JBSplits/JBSplitsSetup.sol +0 -26
  261. package/test/units/static/JBSplits/TestSelfManagedSplitGroups.sol +0 -552
  262. package/test/units/static/JBSplits/TestSetSplitGroupsOf.sol +0 -377
  263. package/test/units/static/JBSplits/TestSplitsLockedEdge.sol +0 -267
  264. package/test/units/static/JBSplits/TestSplitsOf.sol +0 -24
  265. package/test/units/static/JBSplits/TestSplitsPacking.sol +0 -36
  266. package/test/units/static/JBSurplus/TestSurplusFuzz.sol +0 -160
  267. package/test/units/static/JBTerminalStore/JBTerminalStoreSetup.sol +0 -45
  268. package/test/units/static/JBTerminalStore/TestCurrentReclaimableSurplusOf.sol +0 -536
  269. package/test/units/static/JBTerminalStore/TestCurrentSurplusOf.sol +0 -463
  270. package/test/units/static/JBTerminalStore/TestCurrentTotalSurplusOf.sol +0 -135
  271. package/test/units/static/JBTerminalStore/TestPreviewCashOutFrom.sol +0 -476
  272. package/test/units/static/JBTerminalStore/TestPreviewPayFrom.sol +0 -494
  273. package/test/units/static/JBTerminalStore/TestRecordCashOutsFor.sol +0 -652
  274. package/test/units/static/JBTerminalStore/TestRecordPaymentFrom.sol +0 -744
  275. package/test/units/static/JBTerminalStore/TestRecordPayoutFor.sol +0 -289
  276. package/test/units/static/JBTerminalStore/TestRecordTerminalMigration.sol +0 -138
  277. package/test/units/static/JBTerminalStore/TestRecordUsedAllowanceOf.sol +0 -415
  278. package/test/units/static/JBTerminalStore/TestUint224Overflow.sol +0 -219
  279. package/test/units/static/JBTokens/JBTokensSetup.sol +0 -32
  280. package/test/units/static/JBTokens/TestBurnFrom.sol +0 -107
  281. package/test/units/static/JBTokens/TestClaimTokensFor.sol +0 -110
  282. package/test/units/static/JBTokens/TestDeployERC20ForUnits.sol +0 -92
  283. package/test/units/static/JBTokens/TestMintFor.sol +0 -100
  284. package/test/units/static/JBTokens/TestSetTokenFor.sol +0 -98
  285. package/test/units/static/JBTokens/TestTotalBalanceOf.sol +0 -65
  286. package/test/units/static/JBTokens/TestTotalSupplyOf.sol +0 -56
  287. package/test/units/static/JBTokens/TestTransferCreditsFrom.sol +0 -56
@@ -1,808 +0,0 @@
1
- // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.6;
3
-
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 {IJBPayHook} from "../src/interfaces/IJBPayHook.sol";
10
- import {IJBRulesetApprovalHook} from "../src/interfaces/IJBRulesetApprovalHook.sol";
11
- import {IJBRulesetDataHook} from "../src/interfaces/IJBRulesetDataHook.sol";
12
- import {IJBSplitHook} from "../src/interfaces/IJBSplitHook.sol";
13
- import {IJBTerminal} from "../src/interfaces/IJBTerminal.sol";
14
- import {JBConstants} from "../src/libraries/JBConstants.sol";
15
- import {JBAccountingContext} from "../src/structs/JBAccountingContext.sol";
16
- import {JBCurrencyAmount} from "../src/structs/JBCurrencyAmount.sol";
17
- import {JBFundAccessLimitGroup} from "../src/structs/JBFundAccessLimitGroup.sol";
18
- import {JBPayHookSpecification} from "../src/structs/JBPayHookSpecification.sol";
19
- import {JBRulesetConfig} from "../src/structs/JBRulesetConfig.sol";
20
- import {JBRulesetMetadata} from "../src/structs/JBRulesetMetadata.sol";
21
- import {JBSplit} from "../src/structs/JBSplit.sol";
22
- import {JBSplitGroup} from "../src/structs/JBSplitGroup.sol";
23
- import {JBTerminalConfig} from "../src/structs/JBTerminalConfig.sol";
24
- import {mulDiv} from "@prb/math/src/Common.sol";
25
-
26
- /// @notice Tests for three audit fixes: F-3, F-4, and F-5.
27
- /// F-5: Saturating subtraction in _tokenSurplusFrom prevents underflow when usedPayoutLimit > payoutLimit.amount.
28
- /// F-3: _capFeeFreeSurplus after _efficientPay in executePayout caps fee-free surplus at STORE.balanceOf.
29
- /// F-4: _capFeeFreeSurplus after hook fulfillment in _cashOutTokensOf caps fee-free surplus at remaining balance.
30
- contract AuditFixesTest is TestBaseWorkflow {
31
- // --- Core protocol references ---
32
- IJBController private _controller;
33
- JBMultiTerminal private _terminal;
34
- JBTerminalStore private _store;
35
- JBTokens private _tokens;
36
- address private _projectOwner;
37
-
38
- // Token issuance weight: 1000 tokens per ETH.
39
- uint112 private constant WEIGHT = 1000 * 10 ** 18;
40
-
41
- // Storage slot index for _feeFreeSurplusOf in JBMultiTerminal.
42
- // This is the first state variable in the contract (slot 0).
43
- uint256 private constant FEE_FREE_SURPLUS_SLOT = 0;
44
-
45
- function setUp() public override {
46
- super.setUp();
47
-
48
- _controller = jbController();
49
- _terminal = jbMultiTerminal();
50
- _store = jbTerminalStore();
51
- _tokens = jbTokens();
52
- _projectOwner = multisig();
53
- }
54
-
55
- // ==========================================
56
- // F-5: Saturating subtraction in _tokenSurplusFrom
57
- // ==========================================
58
-
59
- /// @notice When a new ruleset activates with a lower payout limit than what was already used under a previous
60
- /// ruleset (same cycle number), `currentSurplusOf` must not revert. The saturating subtraction clamps
61
- /// (used - limit) to zero instead of underflowing.
62
- /// @dev Setup:
63
- /// 1. Launch a project with duration=1 day and a high payout limit (10 ETH).
64
- /// 2. Pay in and use some of the payout limit (5 ETH).
65
- /// 3. Queue a new ruleset with a much lower payout limit (1 ETH) and no approval hook (takes effect immediately
66
- /// as the replacement for the next cycle derived from the queued config).
67
- /// 4. Warp forward to the next cycle so the new ruleset activates. The usedPayoutLimitOf from the previous cycle
68
- /// is keyed by cycleNumber, so the new cycle starts fresh. However, if the new ruleset had duration=0 and
69
- /// replaced mid-cycle, the used amount could exceed the new limit.
70
- ///
71
- /// NOTE: In practice, triggering usedPayoutLimit > payoutLimit.amount is difficult because:
72
- /// - For cycling rulesets (duration > 0), usedPayoutLimitOf resets each cycle.
73
- /// - For duration=0 rulesets, queueing a replacement takes effect immediately with cycleNumber=1.
74
- /// The saturating subtraction is defensive coding that prevents edge cases from DOS-ing the view function.
75
- /// This test verifies the view function works correctly under normal conditions and does not revert.
76
- function test_F5_currentSurplusOfDoesNotRevertWithLowerPayoutLimit() external {
77
- // --- Fee project (project #1) ---
78
- _launchFeeProject();
79
-
80
- // --- Main project: duration=1 day, high payout limit ---
81
- uint224 highPayoutLimit = 10 ether;
82
- uint32 cycleDuration = 1 days;
83
-
84
- JBFundAccessLimitGroup[] memory fundAccess = _makeFundAccessLimitGroup(highPayoutLimit);
85
-
86
- JBRulesetConfig[] memory rulesetConfig = new JBRulesetConfig[](1);
87
- rulesetConfig[0] = _makeRulesetConfig({
88
- duration: cycleDuration,
89
- metadata: _defaultMetadata(),
90
- splitGroups: new JBSplitGroup[](0),
91
- fundAccessLimitGroups: fundAccess
92
- });
93
-
94
- JBTerminalConfig[] memory termConfigs = _makeTerminalConfig();
95
-
96
- uint256 projectId = _controller.launchProjectFor({
97
- owner: _projectOwner,
98
- projectUri: "f5-project",
99
- rulesetConfigurations: rulesetConfig,
100
- terminalConfigurations: termConfigs,
101
- memo: ""
102
- });
103
-
104
- // Pay 20 ETH into the project.
105
- address payer = makeAddr("f5-payer");
106
- vm.deal(payer, 20 ether);
107
- vm.prank(payer);
108
- _terminal.pay{value: 20 ether}({
109
- projectId: projectId,
110
- amount: 20 ether,
111
- token: JBConstants.NATIVE_TOKEN,
112
- beneficiary: payer,
113
- minReturnedTokens: 0,
114
- memo: "",
115
- metadata: new bytes(0)
116
- });
117
-
118
- // Use 5 ETH of the 10 ETH payout limit.
119
- _terminal.sendPayoutsOf({
120
- projectId: projectId,
121
- token: JBConstants.NATIVE_TOKEN,
122
- amount: 5 ether,
123
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
124
- minTokensPaidOut: 0
125
- });
126
-
127
- // Queue a new ruleset with a much lower payout limit (1 ETH).
128
- JBFundAccessLimitGroup[] memory lowFundAccess = _makeFundAccessLimitGroup(1 ether);
129
- JBRulesetConfig[] memory newRulesetConfig = new JBRulesetConfig[](1);
130
- newRulesetConfig[0] = _makeRulesetConfig({
131
- duration: cycleDuration,
132
- metadata: _defaultMetadata(),
133
- splitGroups: new JBSplitGroup[](0),
134
- fundAccessLimitGroups: lowFundAccess
135
- });
136
-
137
- vm.prank(_projectOwner);
138
- _controller.queueRulesetsOf({projectId: projectId, rulesetConfigurations: newRulesetConfig, memo: ""});
139
-
140
- // Warp to next cycle so the new ruleset activates.
141
- vm.warp(block.timestamp + cycleDuration);
142
-
143
- // KEY ASSERTION: currentSurplusOf must NOT revert.
144
- // If the saturating subtraction were missing and somehow used > limit, this would underflow.
145
- // Even though in this specific setup the cycle resets used to 0, this test verifies the view
146
- // function works correctly after a payout limit reduction.
147
- IJBTerminal[] memory terminals = new IJBTerminal[](1);
148
- terminals[0] = IJBTerminal(address(_terminal));
149
- address[] memory tokensToCheck = new address[](1);
150
- tokensToCheck[0] = JBConstants.NATIVE_TOKEN;
151
-
152
- uint256 surplus = _store.currentSurplusOf({
153
- projectId: projectId,
154
- terminals: terminals,
155
- tokens: tokensToCheck,
156
- decimals: 18,
157
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
158
- });
159
-
160
- // After paying out 5 ETH from 20 ETH, balance is 15 ETH. New payout limit is 1 ETH.
161
- // Surplus = balance - payoutLimitRemaining = 15 ETH - 1 ETH = 14 ETH.
162
- assertEq(surplus, 14 ether, "Surplus should be balance minus new (lower) payout limit");
163
- }
164
-
165
- /// @notice Verify that currentSurplusOf does not revert when the project has zero payout limits in the new ruleset
166
- /// (meaning all balance is surplus). This is a simpler version of the F-5 scenario.
167
- function test_F5_currentSurplusOfWithZeroPayoutLimit() external {
168
- // Fee project.
169
- _launchFeeProject();
170
-
171
- // Launch project with payout limit, use it, then queue ruleset with no limits.
172
- uint224 payoutLimit = 5 ether;
173
- JBFundAccessLimitGroup[] memory fundAccess = _makeFundAccessLimitGroup(payoutLimit);
174
-
175
- JBRulesetConfig[] memory rulesetConfig = new JBRulesetConfig[](1);
176
- rulesetConfig[0] = _makeRulesetConfig({
177
- duration: 1 days,
178
- metadata: _defaultMetadata(),
179
- splitGroups: new JBSplitGroup[](0),
180
- fundAccessLimitGroups: fundAccess
181
- });
182
-
183
- uint256 projectId = _controller.launchProjectFor({
184
- owner: _projectOwner,
185
- projectUri: "f5-zero-limit",
186
- rulesetConfigurations: rulesetConfig,
187
- terminalConfigurations: _makeTerminalConfig(),
188
- memo: ""
189
- });
190
-
191
- // Pay in and use the full payout limit.
192
- address payer = makeAddr("f5-payer2");
193
- vm.deal(payer, 10 ether);
194
- vm.prank(payer);
195
- _terminal.pay{value: 10 ether}({
196
- projectId: projectId,
197
- amount: 10 ether,
198
- token: JBConstants.NATIVE_TOKEN,
199
- beneficiary: payer,
200
- minReturnedTokens: 0,
201
- memo: "",
202
- metadata: new bytes(0)
203
- });
204
-
205
- _terminal.sendPayoutsOf({
206
- projectId: projectId,
207
- token: JBConstants.NATIVE_TOKEN,
208
- amount: payoutLimit,
209
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
210
- minTokensPaidOut: 0
211
- });
212
-
213
- // Queue a new ruleset with zero payout limits (all balance is surplus).
214
- JBRulesetConfig[] memory newRulesetConfig = new JBRulesetConfig[](1);
215
- newRulesetConfig[0] = _makeRulesetConfig({
216
- duration: 1 days,
217
- metadata: _defaultMetadata(),
218
- splitGroups: new JBSplitGroup[](0),
219
- fundAccessLimitGroups: new JBFundAccessLimitGroup[](0)
220
- });
221
-
222
- vm.prank(_projectOwner);
223
- _controller.queueRulesetsOf({projectId: projectId, rulesetConfigurations: newRulesetConfig, memo: ""});
224
-
225
- // Warp to next cycle.
226
- vm.warp(block.timestamp + 1 days);
227
-
228
- // currentSurplusOf must not revert. With zero limits, surplus = entire balance.
229
- IJBTerminal[] memory terminals = new IJBTerminal[](1);
230
- terminals[0] = IJBTerminal(address(_terminal));
231
- address[] memory tokensToCheck = new address[](1);
232
- tokensToCheck[0] = JBConstants.NATIVE_TOKEN;
233
-
234
- uint256 surplus = _store.currentSurplusOf({
235
- projectId: projectId,
236
- terminals: terminals,
237
- tokens: tokensToCheck,
238
- decimals: 18,
239
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
240
- });
241
-
242
- // Balance after paying out 5 ETH from 10 ETH is 5 ETH. No payout limits = all surplus.
243
- assertEq(surplus, 5 ether, "Surplus should equal full remaining balance with zero payout limits");
244
- }
245
-
246
- // ==========================================
247
- // F-3: _capFeeFreeSurplus after _efficientPay in executePayout
248
- // ==========================================
249
-
250
- /// @notice When project A pays out to project B via a same-terminal split (not addToBalance), and project B has
251
- /// a data hook that diverts some of the payment to pay hooks, the store only records a partial balance increase.
252
- /// The fix ensures _feeFreeSurplusOf[B] is capped at STORE.balanceOf[B] after the pay.
253
- /// @dev Setup:
254
- /// 1. Project B: has a data hook that returns a pay hook specification diverting 50% of the payment.
255
- /// 2. Project A: has 100% split paying project B (same terminal, preferAddToBalance=false).
256
- /// 3. After sendPayoutsOf, verify _feeFreeSurplusOf[B] <= STORE.balanceOf[B].
257
- /// 4. Verify a subsequent zero-tax cashout from B charges correct fees (not overcharged).
258
- function test_F3_feeFreeSurplusCappedWhenDataHookDivertsFunds() external {
259
- // --- Fee project ---
260
- _launchFeeProject();
261
-
262
- // --- Setup data hook and pay hook mocks ---
263
- address dataHook = makeAddr("data-hook-f3");
264
- address payHook = makeAddr("pay-hook-f3");
265
-
266
- // --- Project B: zero tax, data hook enabled for pay ---
267
- JBRulesetMetadata memory metadataB = _defaultMetadata();
268
- metadataB.useDataHookForPay = true;
269
- metadataB.dataHook = dataHook;
270
-
271
- JBRulesetConfig[] memory rulesetConfigB = new JBRulesetConfig[](1);
272
- rulesetConfigB[0] = _makeRulesetConfig({
273
- duration: 0,
274
- metadata: metadataB,
275
- splitGroups: new JBSplitGroup[](0),
276
- fundAccessLimitGroups: new JBFundAccessLimitGroup[](0)
277
- });
278
-
279
- uint256 projectIdB = _controller.launchProjectFor({
280
- owner: _projectOwner,
281
- projectUri: "project-b-f3",
282
- rulesetConfigurations: rulesetConfigB,
283
- terminalConfigurations: _makeTerminalConfig(),
284
- memo: ""
285
- });
286
-
287
- // Deploy ERC-20 for project B so we can cash out.
288
- vm.prank(_projectOwner);
289
- _controller.deployERC20For(projectIdB, "ProjectB", "PB", bytes32(0));
290
-
291
- // --- Project A: 100% split to project B, preferAddToBalance=false ---
292
- uint224 payoutLimit = 10 ether;
293
-
294
- JBSplit[] memory splits = new JBSplit[](1);
295
- splits[0] = JBSplit({
296
- preferAddToBalance: false,
297
- percent: JBConstants.SPLITS_TOTAL_PERCENT,
298
- // forge-lint: disable-next-line(unsafe-typecast)
299
- projectId: uint64(projectIdB),
300
- beneficiary: payable(makeAddr("split-beneficiary")),
301
- lockedUntil: 0,
302
- hook: IJBSplitHook(address(0))
303
- });
304
-
305
- JBSplitGroup[] memory splitGroups = new JBSplitGroup[](1);
306
- splitGroups[0] = JBSplitGroup({groupId: uint32(uint160(JBConstants.NATIVE_TOKEN)), splits: splits});
307
-
308
- JBFundAccessLimitGroup[] memory fundAccess = _makeFundAccessLimitGroup(payoutLimit);
309
-
310
- JBRulesetConfig[] memory rulesetConfigA = new JBRulesetConfig[](1);
311
- rulesetConfigA[0] = _makeRulesetConfig({
312
- duration: 0, metadata: _defaultMetadata(), splitGroups: splitGroups, fundAccessLimitGroups: fundAccess
313
- });
314
-
315
- uint256 projectIdA = _controller.launchProjectFor({
316
- owner: _projectOwner,
317
- projectUri: "project-a-f3",
318
- rulesetConfigurations: rulesetConfigA,
319
- terminalConfigurations: _makeTerminalConfig(),
320
- memo: ""
321
- });
322
-
323
- // --- Mock the data hook to divert 50% of payments to a pay hook ---
324
- // The data hook returns a pay hook specification that takes 50% of the payment amount.
325
- // This means the store only records 50% as project B's balance.
326
- // We need to mock the data hook's beforePayRecordedWith and the pay hook's afterPayRecordedWith.
327
- // The data hook must also support ERC-165.
328
-
329
- // Mock ERC-165 support for IJBRulesetDataHook.
330
- vm.mockCall(dataHook, abi.encodeWithSelector(bytes4(keccak256("supportsInterface(bytes4)"))), abi.encode(true));
331
-
332
- // Mock hasMintPermissionFor to return false (not needed for this test).
333
- vm.mockCall(
334
- dataHook, abi.encodeWithSelector(IJBRulesetDataHook.hasMintPermissionFor.selector), abi.encode(false)
335
- );
336
-
337
- // The data hook will be called when project B receives the payment via _efficientPay.
338
- // We need to return a weight and pay hook specifications.
339
- // The pay hook spec diverts 50% of the payment to the pay hook.
340
- // We use a wildcard mock: any call to beforePayRecordedWith returns our response.
341
- //
342
- // NOTE: We can't predict the exact calldata because the `amount.value` depends on payout calculations.
343
- // So we use a broad mock that always returns the same response for any beforePayRecordedWith call.
344
- // The diversion amount will be calculated as 50% of whatever is sent.
345
-
346
- // We'll use a helper approach: mock the data hook to return weight=WEIGHT and a pay hook spec
347
- // that takes 5 ETH (half of the 10 ETH payout).
348
- JBPayHookSpecification[] memory hookSpecs = new JBPayHookSpecification[](1);
349
- hookSpecs[0] = JBPayHookSpecification({
350
- hook: IJBPayHook(payHook),
351
- noop: false,
352
- amount: 5 ether, // Divert 50% (5 ETH of the 10 ETH payout)
353
- metadata: new bytes(0)
354
- });
355
-
356
- vm.mockCall(
357
- dataHook,
358
- abi.encodeWithSelector(IJBRulesetDataHook.beforePayRecordedWith.selector),
359
- abi.encode(WEIGHT, hookSpecs)
360
- );
361
-
362
- // Mock the pay hook to accept the call without reverting.
363
- vm.mockCall(payHook, abi.encodeWithSelector(IJBPayHook.afterPayRecordedWith.selector), abi.encode());
364
-
365
- // --- Fund project A and send payouts ---
366
- address payer = makeAddr("f3-payer");
367
- vm.deal(payer, 10 ether);
368
- vm.prank(payer);
369
- _terminal.pay{value: 10 ether}({
370
- projectId: projectIdA,
371
- amount: 10 ether,
372
- token: JBConstants.NATIVE_TOKEN,
373
- beneficiary: payer,
374
- minReturnedTokens: 0,
375
- memo: "",
376
- metadata: new bytes(0)
377
- });
378
-
379
- // Send payouts from project A: 10 ETH goes to project B via the split.
380
- // The data hook diverts 5 ETH to the pay hook, so STORE.balanceOf[B] only increases by 5 ETH.
381
- // But _feeFreeSurplusOf[B] was incremented by the full 10 ETH before the pay.
382
- // After the fix (F-3), _capFeeFreeSurplus caps it at 5 ETH.
383
- _terminal.sendPayoutsOf({
384
- projectId: projectIdA,
385
- token: JBConstants.NATIVE_TOKEN,
386
- amount: payoutLimit,
387
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
388
- minTokensPaidOut: 0
389
- });
390
-
391
- // --- Verify invariant: _feeFreeSurplusOf[B] <= STORE.balanceOf[B] ---
392
- uint256 feeFreeSurplus = _readFeeFreeSurplus(projectIdB, JBConstants.NATIVE_TOKEN);
393
- uint256 storeBalance = _store.balanceOf(address(_terminal), projectIdB, JBConstants.NATIVE_TOKEN);
394
-
395
- assertLe(
396
- feeFreeSurplus,
397
- storeBalance,
398
- "F-3: _feeFreeSurplusOf must not exceed STORE.balanceOf after data hook diverts funds"
399
- );
400
-
401
- // The store should have recorded only the portion not diverted to the pay hook.
402
- // With 10 ETH payout and 5 ETH diverted: store balance = 5 ETH.
403
- assertEq(storeBalance, 5 ether, "Store balance should be payment minus pay hook diversion");
404
- assertEq(feeFreeSurplus, 5 ether, "Fee-free surplus should be capped at store balance");
405
-
406
- // Verify zero-tax cashout charges correct fees via helper (avoids stack-too-deep).
407
- _verifyCashOutFees(projectIdB);
408
- }
409
-
410
- /// @dev Helper to verify cashout fees for F-3 (extracted to reduce stack depth).
411
- function _verifyCashOutFees(uint256 projectIdB) private {
412
- address splitBeneficiary = makeAddr("split-beneficiary");
413
- uint256 beneficiaryTokens = _tokens.totalBalanceOf(splitBeneficiary, projectIdB);
414
-
415
- if (beneficiaryTokens > 0) {
416
- vm.prank(splitBeneficiary);
417
- uint256 reclaimAmount = _terminal.cashOutTokensOf({
418
- holder: splitBeneficiary,
419
- projectId: projectIdB,
420
- cashOutCount: beneficiaryTokens,
421
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
422
- minTokensReclaimed: 0,
423
- beneficiary: payable(splitBeneficiary),
424
- metadata: new bytes(0)
425
- });
426
-
427
- // With cashOutTaxRate = 0, the fee is charged on the feeFreeSurplus portion (5 ETH).
428
- // Fee = 2.5% of 5 ETH = 0.125 ETH. Net = 5 - 0.125 = 4.875 ETH.
429
- uint256 expectedFee = mulDiv(5 ether, 25, 1000);
430
- uint256 expectedNet = 5 ether - expectedFee;
431
- assertApproxEqAbs(
432
- reclaimAmount,
433
- expectedNet,
434
- 2,
435
- "F-3: Cashout should charge correct fee (2.5% of capped fee-free surplus)"
436
- );
437
-
438
- // Crucially, the fee should NOT be calculated on the full 10 ETH payout amount.
439
- // Without the fix, feeFreeSurplus would be 10 ETH but balance only 5 ETH,
440
- // causing an overcharge.
441
- assertLt(reclaimAmount, 5 ether, "F-3: Fee must be deducted from cashout");
442
- }
443
- }
444
-
445
- // ==========================================
446
- // F-4: _capFeeFreeSurplus after hook fulfillment in _cashOutTokensOf
447
- // ==========================================
448
-
449
- /// @notice After a cashout, _feeFreeSurplusOf is capped at the remaining STORE.balanceOf.
450
- /// This prevents stale _feeFreeSurplusOf from overcharging fees on subsequent zero-tax cashouts.
451
- /// @dev Setup:
452
- /// 1. Project A pays out to project B (same terminal) to build up _feeFreeSurplusOf[B].
453
- /// 2. Some users pay B directly (increasing balance but not fee-free surplus).
454
- /// 3. Cash out from B. After the cashout, _feeFreeSurplusOf[B] should be capped at remaining balance.
455
- /// 4. Subsequent direct-pay cashout should be fee-free (no remaining fee-free surplus).
456
- function test_F4_feeFreeSurplusCappedAfterCashOut() external {
457
- // --- Fee project ---
458
- _launchFeeProject();
459
-
460
- // Launch project B and project A with payout split to B.
461
- (uint256 projectIdA, uint256 projectIdB) = _launchPayoutPairForF4("f4-cap");
462
-
463
- // Step 1: Pay into project A and trigger payout to B.
464
- _payAndSendPayouts(projectIdA, 10 ether, 10 ether);
465
-
466
- // B now has: balance = 10 ETH, _feeFreeSurplusOf = 10 ETH.
467
- assertEq(
468
- _readFeeFreeSurplus(projectIdB, JBConstants.NATIVE_TOKEN),
469
- _store.balanceOf(address(_terminal), projectIdB, JBConstants.NATIVE_TOKEN),
470
- "Initial: fee-free surplus should equal balance"
471
- );
472
-
473
- // Step 2: Pay project B directly (increases balance but NOT fee-free surplus).
474
- _payProject(projectIdB, makeAddr("f4-direct-payer"), 10 ether);
475
-
476
- // B now has: balance = 20 ETH, _feeFreeSurplusOf = 10 ETH.
477
- assertEq(
478
- _store.balanceOf(address(_terminal), projectIdB, JBConstants.NATIVE_TOKEN),
479
- 20 ether,
480
- "Balance should be 20 ETH after direct payment"
481
- );
482
-
483
- // Step 3: Cash out half of the direct payer's tokens and verify the invariant.
484
- _cashOutHalfAndVerifyInvariant(projectIdB, makeAddr("f4-direct-payer"));
485
-
486
- // Step 4: Cash out remaining tokens and verify again.
487
- _cashOutRemainingAndVerifyInvariant(projectIdB, makeAddr("f4-direct-payer"));
488
- }
489
-
490
- /// @dev Cash out half of a holder's tokens and assert the fee-free surplus invariant.
491
- function _cashOutHalfAndVerifyInvariant(uint256 projectId, address holder) private {
492
- uint256 holderTokens = _tokens.totalBalanceOf(holder, projectId);
493
- uint256 halfTokens = holderTokens / 2;
494
-
495
- vm.prank(holder);
496
- uint256 reclaim = _terminal.cashOutTokensOf({
497
- holder: holder,
498
- projectId: projectId,
499
- cashOutCount: halfTokens,
500
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
501
- minTokensReclaimed: 0,
502
- beneficiary: payable(holder),
503
- metadata: new bytes(0)
504
- });
505
-
506
- assertLe(
507
- _readFeeFreeSurplus(projectId, JBConstants.NATIVE_TOKEN),
508
- _store.balanceOf(address(_terminal), projectId, JBConstants.NATIVE_TOKEN),
509
- "F-4: _feeFreeSurplusOf must be <= STORE.balanceOf after partial cashout"
510
- );
511
- assertGt(reclaim, 0, "Partial cashout should reclaim something");
512
- }
513
-
514
- /// @dev Cash out remaining tokens for a holder and assert the fee-free surplus invariant.
515
- function _cashOutRemainingAndVerifyInvariant(uint256 projectId, address holder) private {
516
- uint256 remaining = _tokens.totalBalanceOf(holder, projectId);
517
- if (remaining == 0) return;
518
-
519
- vm.prank(holder);
520
- uint256 reclaim = _terminal.cashOutTokensOf({
521
- holder: holder,
522
- projectId: projectId,
523
- cashOutCount: remaining,
524
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
525
- minTokensReclaimed: 0,
526
- beneficiary: payable(holder),
527
- metadata: new bytes(0)
528
- });
529
-
530
- assertLe(
531
- _readFeeFreeSurplus(projectId, JBConstants.NATIVE_TOKEN),
532
- _store.balanceOf(address(_terminal), projectId, JBConstants.NATIVE_TOKEN),
533
- "F-4: _feeFreeSurplusOf must be <= STORE.balanceOf after second cashout"
534
- );
535
- assertGt(reclaim, 0, "Second cashout should reclaim something");
536
- }
537
-
538
- /// @notice After a cashout that fully drains a project's balance, _feeFreeSurplusOf should be zero.
539
- /// This prevents overcharging fees on subsequent direct payments.
540
- function test_F4_feeFreeSurplusZeroAfterFullDrain() external {
541
- // Fee project.
542
- _launchFeeProject();
543
-
544
- // Launch project B and project A with payout split to B.
545
- (uint256 projectIdA, uint256 projectIdB) = _launchPayoutPairForF4("f4-drain");
546
-
547
- // Pay project A, send payouts to B.
548
- _payAndSendPayouts(projectIdA, 10 ether, 10 ether);
549
-
550
- // B: balance = 10 ETH, feeFreeSurplus = 10 ETH. No token holders in B (addToBalance doesn't mint).
551
- // To cash out, we need someone to pay B directly first so they get tokens.
552
- _payProject(projectIdB, makeAddr("f4-casher"), 10 ether);
553
-
554
- // B: balance = 20 ETH, feeFreeSurplus = 10 ETH.
555
- // Cash out ALL tokens (this is the only holder, so they get the full balance).
556
- _cashOutAll(projectIdB, makeAddr("f4-casher"));
557
-
558
- // After full drain, fee-free surplus should be capped at 0 (or whatever balance remains after fees).
559
- uint256 feeFreeSurplusAfterDrain = _readFeeFreeSurplus(projectIdB, JBConstants.NATIVE_TOKEN);
560
- uint256 balanceAfterDrain = _store.balanceOf(address(_terminal), projectIdB, JBConstants.NATIVE_TOKEN);
561
-
562
- assertLe(
563
- feeFreeSurplusAfterDrain, balanceAfterDrain, "F-4: fee-free surplus must be <= balance after full drain"
564
- );
565
-
566
- // Now pay B again directly. This should be fee-free since fee-free surplus was cleared.
567
- _payProject(projectIdB, makeAddr("f4-fresh-user"), 5 ether);
568
-
569
- // Cash out and verify it's fee-free.
570
- _verifyFeeFreeCashOut(projectIdB, makeAddr("f4-fresh-user"), 5 ether);
571
- }
572
-
573
- // ==========================================
574
- // Helper functions
575
- // ==========================================
576
-
577
- /// @dev Launch a pair of projects for the F-4 drain test: project B (zero tax) and project A (100% split to B).
578
- function _launchPayoutPairForF4(string memory label) private returns (uint256 projectIdA, uint256 projectIdB) {
579
- // Project B: zero tax.
580
- JBRulesetConfig[] memory rulesetConfigB = new JBRulesetConfig[](1);
581
- rulesetConfigB[0] = _makeRulesetConfig({
582
- duration: 0,
583
- metadata: _defaultMetadata(),
584
- splitGroups: new JBSplitGroup[](0),
585
- fundAccessLimitGroups: new JBFundAccessLimitGroup[](0)
586
- });
587
-
588
- projectIdB = _controller.launchProjectFor({
589
- owner: _projectOwner,
590
- projectUri: string.concat("project-b-", label),
591
- rulesetConfigurations: rulesetConfigB,
592
- terminalConfigurations: _makeTerminalConfig(),
593
- memo: ""
594
- });
595
-
596
- vm.prank(_projectOwner);
597
- _controller.deployERC20For(projectIdB, "PBDrain", "PBD", bytes32(0));
598
-
599
- // Project A: 100% split to B (addToBalance).
600
- JBSplit[] memory splits = new JBSplit[](1);
601
- splits[0] = JBSplit({
602
- preferAddToBalance: true,
603
- percent: JBConstants.SPLITS_TOTAL_PERCENT,
604
- // forge-lint: disable-next-line(unsafe-typecast)
605
- projectId: uint64(projectIdB),
606
- beneficiary: payable(address(0)),
607
- lockedUntil: 0,
608
- hook: IJBSplitHook(address(0))
609
- });
610
-
611
- JBSplitGroup[] memory splitGroups = new JBSplitGroup[](1);
612
- splitGroups[0] = JBSplitGroup({groupId: uint32(uint160(JBConstants.NATIVE_TOKEN)), splits: splits});
613
-
614
- JBFundAccessLimitGroup[] memory fundAccess = _makeFundAccessLimitGroup(10 ether);
615
-
616
- JBRulesetConfig[] memory rulesetConfigA = new JBRulesetConfig[](1);
617
- rulesetConfigA[0] = _makeRulesetConfig({
618
- duration: 0, metadata: _defaultMetadata(), splitGroups: splitGroups, fundAccessLimitGroups: fundAccess
619
- });
620
-
621
- projectIdA = _controller.launchProjectFor({
622
- owner: _projectOwner,
623
- projectUri: string.concat("project-a-", label),
624
- rulesetConfigurations: rulesetConfigA,
625
- terminalConfigurations: _makeTerminalConfig(),
626
- memo: ""
627
- });
628
- }
629
-
630
- /// @dev Pay a project and then send payouts for the given amount.
631
- function _payAndSendPayouts(uint256 projectId, uint256 payAmount, uint224 payoutAmount) private {
632
- address payer = makeAddr("pay-and-send-payer");
633
- vm.deal(payer, payAmount);
634
- vm.prank(payer);
635
- _terminal.pay{value: payAmount}({
636
- projectId: projectId,
637
- amount: payAmount,
638
- token: JBConstants.NATIVE_TOKEN,
639
- beneficiary: payer,
640
- minReturnedTokens: 0,
641
- memo: "",
642
- metadata: new bytes(0)
643
- });
644
-
645
- _terminal.sendPayoutsOf({
646
- projectId: projectId,
647
- token: JBConstants.NATIVE_TOKEN,
648
- amount: payoutAmount,
649
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
650
- minTokensPaidOut: 0
651
- });
652
- }
653
-
654
- /// @dev Pay ETH directly into a project as the given user.
655
- function _payProject(uint256 projectId, address user, uint256 amount) private {
656
- vm.deal(user, amount);
657
- vm.prank(user);
658
- _terminal.pay{value: amount}({
659
- projectId: projectId,
660
- amount: amount,
661
- token: JBConstants.NATIVE_TOKEN,
662
- beneficiary: user,
663
- minReturnedTokens: 0,
664
- memo: "",
665
- metadata: new bytes(0)
666
- });
667
- }
668
-
669
- /// @dev Cash out all tokens for a holder from a project.
670
- function _cashOutAll(uint256 projectId, address holder) private {
671
- uint256 tokenBalance = _tokens.totalBalanceOf(holder, projectId);
672
- if (tokenBalance == 0) return;
673
-
674
- vm.prank(holder);
675
- _terminal.cashOutTokensOf({
676
- holder: holder,
677
- projectId: projectId,
678
- cashOutCount: tokenBalance,
679
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
680
- minTokensReclaimed: 0,
681
- beneficiary: payable(holder),
682
- metadata: new bytes(0)
683
- });
684
- }
685
-
686
- /// @dev Verify that cashing out all tokens returns the exact expected amount (fee-free).
687
- function _verifyFeeFreeCashOut(uint256 projectId, address holder, uint256 expectedAmount) private {
688
- uint256 tokenBalance = _tokens.totalBalanceOf(holder, projectId);
689
- assertGt(tokenBalance, 0, "Holder should have tokens");
690
-
691
- vm.prank(holder);
692
- uint256 reclaimAmount = _terminal.cashOutTokensOf({
693
- holder: holder,
694
- projectId: projectId,
695
- cashOutCount: tokenBalance,
696
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
697
- minTokensReclaimed: 0,
698
- beneficiary: payable(holder),
699
- metadata: new bytes(0)
700
- });
701
-
702
- assertEq(reclaimAmount, expectedAmount, "Direct payment cashout should be fee-free");
703
- }
704
-
705
- /// @notice Read `_feeFreeSurplusOf[projectId][token]` from JBMultiTerminal storage via vm.load.
706
- function _readFeeFreeSurplus(uint256 projectId, address token) private view returns (uint256) {
707
- bytes32 innerSlot = keccak256(abi.encode(projectId, FEE_FREE_SURPLUS_SLOT));
708
- bytes32 finalSlot = keccak256(abi.encode(token, innerSlot));
709
- return uint256(vm.load(address(_terminal), finalSlot));
710
- }
711
-
712
- /// @notice Launch the fee project (project #1) required by the protocol for fee collection.
713
- function _launchFeeProject() private returns (uint256) {
714
- JBRulesetConfig[] memory rc = new JBRulesetConfig[](1);
715
- rc[0] = _makeRulesetConfig({
716
- duration: 0,
717
- metadata: _defaultMetadata(),
718
- splitGroups: new JBSplitGroup[](0),
719
- fundAccessLimitGroups: new JBFundAccessLimitGroup[](0)
720
- });
721
-
722
- return _controller.launchProjectFor({
723
- owner: makeAddr("fee-owner"),
724
- projectUri: "fee-project",
725
- rulesetConfigurations: rc,
726
- terminalConfigurations: _makeTerminalConfig(),
727
- memo: ""
728
- });
729
- }
730
-
731
- /// @notice Build default ruleset metadata with zero tax rate and no data hook.
732
- function _defaultMetadata() private pure returns (JBRulesetMetadata memory) {
733
- return JBRulesetMetadata({
734
- reservedPercent: 0,
735
- cashOutTaxRate: 0,
736
- baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
737
- pausePay: false,
738
- pauseCreditTransfers: false,
739
- allowOwnerMinting: false,
740
- allowSetCustomToken: false,
741
- allowTerminalMigration: false,
742
- allowSetTerminals: false,
743
- ownerMustSendPayouts: false,
744
- allowSetController: false,
745
- allowAddAccountingContext: true,
746
- allowAddPriceFeed: false,
747
- holdFees: false,
748
- useTotalSurplusForCashOuts: false,
749
- useDataHookForPay: false,
750
- useDataHookForCashOut: false,
751
- dataHook: address(0),
752
- metadata: 0
753
- });
754
- }
755
-
756
- /// @notice Build a standard terminal configuration accepting native ETH.
757
- function _makeTerminalConfig() private view returns (JBTerminalConfig[] memory termConfigs) {
758
- JBAccountingContext[] memory ctxs = new JBAccountingContext[](1);
759
- ctxs[0] = JBAccountingContext({
760
- token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
761
- });
762
-
763
- termConfigs = new JBTerminalConfig[](1);
764
- termConfigs[0] = JBTerminalConfig({terminal: _terminal, accountingContextsToAccept: ctxs});
765
- }
766
-
767
- /// @notice Build a fund access limit group with a single payout limit in native token.
768
- function _makeFundAccessLimitGroup(uint224 payoutLimitAmount)
769
- private
770
- view
771
- returns (JBFundAccessLimitGroup[] memory)
772
- {
773
- JBCurrencyAmount[] memory payoutLimits = new JBCurrencyAmount[](1);
774
- payoutLimits[0] =
775
- JBCurrencyAmount({amount: payoutLimitAmount, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))});
776
-
777
- JBFundAccessLimitGroup[] memory groups = new JBFundAccessLimitGroup[](1);
778
- groups[0] = JBFundAccessLimitGroup({
779
- terminal: address(_terminal),
780
- token: JBConstants.NATIVE_TOKEN,
781
- payoutLimits: payoutLimits,
782
- surplusAllowances: new JBCurrencyAmount[](0)
783
- });
784
-
785
- return groups;
786
- }
787
-
788
- /// @notice Build a ruleset config with the given parameters.
789
- function _makeRulesetConfig(
790
- uint32 duration,
791
- JBRulesetMetadata memory metadata,
792
- JBSplitGroup[] memory splitGroups,
793
- JBFundAccessLimitGroup[] memory fundAccessLimitGroups
794
- )
795
- private
796
- pure
797
- returns (JBRulesetConfig memory config)
798
- {
799
- config.mustStartAtOrAfter = 0;
800
- config.duration = duration;
801
- config.weight = WEIGHT;
802
- config.weightCutPercent = 0;
803
- config.approvalHook = IJBRulesetApprovalHook(address(0));
804
- config.metadata = metadata;
805
- config.splitGroups = splitGroups;
806
- config.fundAccessLimitGroups = fundAccessLimitGroups;
807
- }
808
- }