@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,949 +0,0 @@
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
- /// @notice Multi-hop payouts: A → B → C. Both B and C should incur fees on cashout.
595
- /// Project A pays Project B (0 cashout tax, same terminal). Project B receives external payment.
596
- /// Project B pays Project C (0 cashout tax, same terminal). Both B and C cashouts incur fees.
597
- function testMultiHopPayoutsCannotAvoidFees() external {
598
- uint256 projectIdC = _launchMultiHopProjectC();
599
- _configureProjectBPayoutsTo(projectIdC);
600
-
601
- vm.prank(_projectOwner);
602
- _controller.deployERC20For(_projectIdB, "ProjectB", "PB", bytes32(0));
603
- vm.prank(_projectOwner);
604
- _controller.deployERC20For(projectIdC, "ProjectC", "PC", bytes32(0));
605
-
606
- // Step 1: Pay project A, trigger payout → 10 ETH lands on B (fee-free).
607
- vm.deal(_attacker, 100 ether);
608
- vm.prank(_attacker);
609
- _terminal.pay{value: 100 ether}({
610
- projectId: _projectIdA,
611
- amount: 100 ether,
612
- token: JBConstants.NATIVE_TOKEN,
613
- beneficiary: _attacker,
614
- minReturnedTokens: 0,
615
- memo: "",
616
- metadata: new bytes(0)
617
- });
618
- _terminal.sendPayoutsOf({
619
- projectId: _projectIdA,
620
- amount: _payoutLimit,
621
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
622
- token: JBConstants.NATIVE_TOKEN,
623
- minTokensPaidOut: 0
624
- });
625
- // B: balance = 10 ETH, fee-free = 10 ETH.
626
-
627
- // Step 2: External payment of 100 ETH to project B.
628
- address user = makeAddr("user");
629
- vm.deal(user, 100 ether);
630
- vm.prank(user);
631
- _terminal.pay{value: 100 ether}({
632
- projectId: _projectIdB,
633
- amount: 100 ether,
634
- token: JBConstants.NATIVE_TOKEN,
635
- beneficiary: user,
636
- minReturnedTokens: 0,
637
- memo: "",
638
- metadata: new bytes(0)
639
- });
640
- // B: balance = 110 ETH, fee-free = 10 ETH.
641
-
642
- // Step 3: Project B pays out 100 ETH to Project C (same terminal, fee-free for C).
643
- _terminal.sendPayoutsOf({
644
- projectId: _projectIdB,
645
- amount: 100 ether,
646
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
647
- token: JBConstants.NATIVE_TOKEN,
648
- minTokensPaidOut: 0
649
- });
650
- // B: balance = 10 ETH, fee-free = 10 (stays: non-fee-free left first).
651
- // C: balance = 100 ETH, fee-free = 100 ETH.
652
-
653
- // Step 4: Cash out from C — fees must apply to the full 100 ETH.
654
- _assertCashOutIncursFees(projectIdC, _attacker);
655
-
656
- // Step 5: Cash out attacker's tokens from B — fees must apply (B is all fee-free).
657
- _assertCashOutIncursFees(_projectIdB, _attacker);
658
- }
659
-
660
- /// @dev Assert that cashing out all tokens from `projectId` for `holder` incurs a fee.
661
- function _assertCashOutIncursFees(uint256 projectId, address holder) internal {
662
- uint256 holderTokens = _tokens.totalBalanceOf(holder, projectId);
663
- if (holderTokens == 0) return;
664
-
665
- uint256 balBefore = holder.balance;
666
- vm.prank(holder);
667
- uint256 reclaim = _terminal.cashOutTokensOf({
668
- holder: holder,
669
- projectId: projectId,
670
- cashOutCount: holderTokens,
671
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
672
- minTokensReclaimed: 0,
673
- beneficiary: payable(holder),
674
- metadata: new bytes(0)
675
- });
676
- // With cashOutTaxRate = 0, gross reclaim = proportional share of balance.
677
- // Fee-free surplus means a 2.5% fee is applied → net < gross.
678
- assertGt(reclaim, 0, "should reclaim something");
679
- // The fee should have been deducted: actual ETH received < reclaim + fee.
680
- uint256 ethReceived = holder.balance - balBefore;
681
- assertEq(ethReceived, reclaim, "ETH received should match reclaim");
682
- }
683
-
684
- /// @dev Launch Project C with zero tax for multi-hop test.
685
- function _launchMultiHopProjectC() internal returns (uint256) {
686
- JBTerminalConfig[] memory termConfigs = new JBTerminalConfig[](1);
687
- JBAccountingContext[] memory ctxs = new JBAccountingContext[](1);
688
- ctxs[0] = JBAccountingContext({
689
- token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
690
- });
691
- termConfigs[0] = JBTerminalConfig({terminal: _terminal, accountingContextsToAccept: ctxs});
692
-
693
- JBRulesetConfig[] memory rc = new JBRulesetConfig[](1);
694
- rc[0].mustStartAtOrAfter = 0;
695
- rc[0].duration = 0;
696
- rc[0].weight = _weight;
697
- rc[0].metadata = _zeroTaxMetadata();
698
- rc[0].splitGroups = new JBSplitGroup[](0);
699
- rc[0].fundAccessLimitGroups = new JBFundAccessLimitGroup[](0);
700
-
701
- return _controller.launchProjectFor({
702
- owner: _projectOwner,
703
- projectUri: "project-c-multihop",
704
- rulesetConfigurations: rc,
705
- terminalConfigurations: termConfigs,
706
- memo: ""
707
- });
708
- }
709
-
710
- /// @dev Reconfigure Project B to route 100% payouts to `targetProjectId`.
711
- function _configureProjectBPayoutsTo(uint256 targetProjectId) internal {
712
- JBSplit[] memory splits = new JBSplit[](1);
713
- splits[0] = JBSplit({
714
- preferAddToBalance: false,
715
- percent: JBConstants.SPLITS_TOTAL_PERCENT,
716
- // forge-lint: disable-next-line(unsafe-typecast)
717
- projectId: uint64(targetProjectId),
718
- beneficiary: payable(_attacker),
719
- lockedUntil: 0,
720
- hook: IJBSplitHook(address(0))
721
- });
722
-
723
- JBSplitGroup[] memory splitGroups = new JBSplitGroup[](1);
724
- splitGroups[0] = JBSplitGroup({groupId: uint32(uint160(JBConstants.NATIVE_TOKEN)), splits: splits});
725
-
726
- JBCurrencyAmount[] memory limits = new JBCurrencyAmount[](1);
727
- limits[0] = JBCurrencyAmount({amount: 100 ether, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))});
728
- JBFundAccessLimitGroup[] memory fundAccess = new JBFundAccessLimitGroup[](1);
729
- fundAccess[0] = JBFundAccessLimitGroup({
730
- terminal: address(_terminal),
731
- token: JBConstants.NATIVE_TOKEN,
732
- payoutLimits: limits,
733
- surplusAllowances: new JBCurrencyAmount[](0)
734
- });
735
-
736
- JBRulesetConfig[] memory rc = new JBRulesetConfig[](1);
737
- rc[0].mustStartAtOrAfter = 0;
738
- rc[0].duration = 0;
739
- rc[0].weight = _weight;
740
- rc[0].metadata = _zeroTaxMetadata();
741
- rc[0].splitGroups = splitGroups;
742
- rc[0].fundAccessLimitGroups = fundAccess;
743
-
744
- vm.prank(_projectOwner);
745
- _controller.queueRulesetsOf({projectId: _projectIdB, rulesetConfigurations: rc, memo: ""});
746
- }
747
-
748
- /// @notice Non-zero-tax cashouts cap fee-free surplus at remaining balance.
749
- /// Without this, switching from non-zero to zero tax could let stale surplus over-charge.
750
- function testNonZeroTaxCashOutCapsFeeFreeSurplus() external {
751
- vm.prank(_projectOwner);
752
- _controller.deployERC20For(_projectIdB, "ProjectB", "PB", bytes32(0));
753
-
754
- // Step 1: Pay project A, trigger payout → 10 ETH fee-free on B.
755
- vm.deal(_attacker, 10 ether);
756
- vm.prank(_attacker);
757
- _terminal.pay{value: 10 ether}({
758
- projectId: _projectIdA,
759
- amount: 10 ether,
760
- token: JBConstants.NATIVE_TOKEN,
761
- beneficiary: _attacker,
762
- minReturnedTokens: 0,
763
- memo: "",
764
- metadata: new bytes(0)
765
- });
766
- _terminal.sendPayoutsOf({
767
- projectId: _projectIdA,
768
- amount: _payoutLimit,
769
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
770
- token: JBConstants.NATIVE_TOKEN,
771
- minTokensPaidOut: 0
772
- });
773
- // B: balance = 10 ETH, fee-free = 10 ETH.
774
-
775
- // Step 2: Pay B directly with 90 ETH more.
776
- address user = makeAddr("user");
777
- vm.deal(user, 90 ether);
778
- vm.prank(user);
779
- _terminal.pay{value: 90 ether}({
780
- projectId: _projectIdB,
781
- amount: 90 ether,
782
- token: JBConstants.NATIVE_TOKEN,
783
- beneficiary: user,
784
- minReturnedTokens: 0,
785
- memo: "",
786
- metadata: new bytes(0)
787
- });
788
- // B: balance = 100 ETH, fee-free = 10 ETH.
789
-
790
- // Step 3: Reconfigure B with non-zero cash out tax, then cash out most of the balance.
791
- _reconfigureWithTaxRate(_projectIdB, 5000); // 50% tax
792
-
793
- // Cash out 95% of user's tokens at 50% tax → drains most balance.
794
- uint256 userTokens = _tokens.totalBalanceOf(user, _projectIdB);
795
- vm.prank(user);
796
- _terminal.cashOutTokensOf({
797
- holder: user,
798
- projectId: _projectIdB,
799
- cashOutCount: userTokens * 95 / 100,
800
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
801
- minTokensReclaimed: 0,
802
- beneficiary: payable(user),
803
- metadata: new bytes(0)
804
- });
805
- // Balance dropped significantly. _capFeeFreeSurplus should have capped fee-free at remaining balance.
806
-
807
- // Step 4: Switch back to zero tax. Cash out remaining.
808
- _reconfigureWithTaxRate(_projectIdB, 0);
809
-
810
- uint256 remainingUserTokens = _tokens.totalBalanceOf(user, _projectIdB);
811
- if (remainingUserTokens > 0) {
812
- vm.prank(user);
813
- uint256 reclaim = _terminal.cashOutTokensOf({
814
- holder: user,
815
- projectId: _projectIdB,
816
- cashOutCount: remainingUserTokens,
817
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
818
- minTokensReclaimed: 0,
819
- beneficiary: payable(user),
820
- metadata: new bytes(0)
821
- });
822
- // Should NOT revert. Fee-free was capped during the non-zero-tax cashout,
823
- // so it doesn't exceed the remaining balance.
824
- assertGt(reclaim, 0, "should reclaim something after tax rate switch");
825
- }
826
- }
827
-
828
- /// @notice Feeless beneficiary cashout still caps fee-free surplus at remaining balance.
829
- function testFeelessBeneficiaryCashOutCapsFeeFreeSurplus() external {
830
- vm.prank(_projectOwner);
831
- _controller.deployERC20For(_projectIdB, "ProjectB", "PB", bytes32(0));
832
-
833
- // Step 1: Pay project A, trigger payout → 10 ETH fee-free on B.
834
- vm.deal(_attacker, 10 ether);
835
- vm.prank(_attacker);
836
- _terminal.pay{value: 10 ether}({
837
- projectId: _projectIdA,
838
- amount: 10 ether,
839
- token: JBConstants.NATIVE_TOKEN,
840
- beneficiary: _attacker,
841
- minReturnedTokens: 0,
842
- memo: "",
843
- metadata: new bytes(0)
844
- });
845
- _terminal.sendPayoutsOf({
846
- projectId: _projectIdA,
847
- amount: _payoutLimit,
848
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
849
- token: JBConstants.NATIVE_TOKEN,
850
- minTokensPaidOut: 0
851
- });
852
- // B: balance = 10 ETH, fee-free = 10 ETH. Attacker has tokens.
853
-
854
- // Step 2: Mark attacker as feeless.
855
- vm.prank(multisig());
856
- jbFeelessAddresses().setFeelessAddress(_attacker, true);
857
-
858
- // Step 3: Feeless cashout — no fees charged, but _capFeeFreeSurplus should still cap.
859
- uint256 attackerTokens = _tokens.totalBalanceOf(_attacker, _projectIdB);
860
- vm.prank(_attacker);
861
- uint256 reclaim = _terminal.cashOutTokensOf({
862
- holder: _attacker,
863
- projectId: _projectIdB,
864
- cashOutCount: attackerTokens,
865
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
866
- minTokensReclaimed: 0,
867
- beneficiary: payable(_attacker),
868
- metadata: new bytes(0)
869
- });
870
- // Feeless beneficiary gets full amount — no fee deducted.
871
- assertEq(reclaim, 10 ether, "feeless beneficiary should get full reclaim");
872
-
873
- // Step 4: Pay B again directly. If fee-free wasn't capped, it would still be 10 ETH
874
- // even though balance went to 0 after the feeless cashout.
875
- address user = makeAddr("user");
876
- vm.deal(user, 10 ether);
877
- vm.prank(user);
878
- _terminal.pay{value: 10 ether}({
879
- projectId: _projectIdB,
880
- amount: 10 ether,
881
- token: JBConstants.NATIVE_TOKEN,
882
- beneficiary: user,
883
- minReturnedTokens: 0,
884
- memo: "",
885
- metadata: new bytes(0)
886
- });
887
-
888
- // Step 5: Cash out as user (not feeless). Fee-free should be 0 (capped at 0 after feeless cashout).
889
- // So this direct-pay cashout should be fee-free.
890
- uint256 userTokens = _tokens.totalBalanceOf(user, _projectIdB);
891
- vm.prank(user);
892
- uint256 userReclaim = _terminal.cashOutTokensOf({
893
- holder: user,
894
- projectId: _projectIdB,
895
- cashOutCount: userTokens,
896
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
897
- minTokensReclaimed: 0,
898
- beneficiary: payable(user),
899
- metadata: new bytes(0)
900
- });
901
- // Direct payment → zero fee-free surplus → no fee. User gets full amount.
902
- assertEq(userReclaim, 10 ether, "direct pay-in should be fee-free after feeless cashout cleared surplus");
903
-
904
- // Clean up: unmark feeless.
905
- vm.prank(multisig());
906
- jbFeelessAddresses().setFeelessAddress(_attacker, false);
907
- }
908
-
909
- /// @dev Reconfigure project with a new cashOutTaxRate (keeps zero-tax metadata otherwise).
910
- function _reconfigureWithTaxRate(uint256 projectId, uint16 taxRate) internal {
911
- JBRulesetMetadata memory meta = _zeroTaxMetadata();
912
- meta.cashOutTaxRate = taxRate;
913
-
914
- JBRulesetConfig[] memory rc = new JBRulesetConfig[](1);
915
- rc[0].mustStartAtOrAfter = 0;
916
- rc[0].duration = 0;
917
- rc[0].weight = _weight;
918
- rc[0].metadata = meta;
919
- rc[0].splitGroups = new JBSplitGroup[](0);
920
- rc[0].fundAccessLimitGroups = new JBFundAccessLimitGroup[](0);
921
-
922
- vm.prank(_projectOwner);
923
- _controller.queueRulesetsOf({projectId: projectId, rulesetConfigurations: rc, memo: ""});
924
- }
925
-
926
- function _zeroTaxMetadata() internal pure returns (JBRulesetMetadata memory) {
927
- return JBRulesetMetadata({
928
- reservedPercent: 0,
929
- cashOutTaxRate: 0,
930
- baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
931
- pausePay: false,
932
- pauseCreditTransfers: false,
933
- allowOwnerMinting: false,
934
- allowSetCustomToken: false,
935
- allowTerminalMigration: false,
936
- allowSetTerminals: false,
937
- ownerMustSendPayouts: false,
938
- allowSetController: false,
939
- allowAddAccountingContext: true,
940
- allowAddPriceFeed: false,
941
- holdFees: false,
942
- useTotalSurplusForCashOuts: false,
943
- useDataHookForPay: false,
944
- useDataHookForCashOut: false,
945
- dataHook: address(0),
946
- metadata: 0
947
- });
948
- }
949
- }