@bananapus/core-v6 0.0.33 → 0.0.35

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 (55) hide show
  1. package/ADMINISTRATION.md +75 -348
  2. package/ARCHITECTURE.md +86 -44
  3. package/AUDIT_INSTRUCTIONS.md +29 -42
  4. package/README.md +22 -3
  5. package/RISKS.md +45 -1
  6. package/SKILLS.md +16 -4
  7. package/USER_JOURNEYS.md +130 -30
  8. package/foundry.toml +2 -0
  9. package/package.json +2 -2
  10. package/references/entrypoints.md +1 -1
  11. package/script/Deploy.s.sol +2 -1
  12. package/src/JBERC20.sol +100 -30
  13. package/src/JBTerminalStore.sol +64 -23
  14. package/src/JBTokens.sol +1 -1
  15. package/src/abstract/JBPermissioned.sol +28 -0
  16. package/src/interfaces/IJBRulesetDataHook.sol +6 -1
  17. package/src/interfaces/IJBToken.sol +3 -3
  18. package/src/structs/JBAccountingContext.sol +0 -1
  19. package/src/structs/JBAfterCashOutRecordedContext.sol +0 -1
  20. package/src/structs/JBAfterPayRecordedContext.sol +0 -1
  21. package/src/structs/JBBeforeCashOutRecordedContext.sol +0 -1
  22. package/src/structs/JBBeforePayRecordedContext.sol +0 -1
  23. package/src/structs/JBCashOutHookSpecification.sol +0 -1
  24. package/src/structs/JBCurrencyAmount.sol +0 -1
  25. package/src/structs/JBFee.sol +0 -1
  26. package/src/structs/JBFundAccessLimitGroup.sol +0 -1
  27. package/src/structs/JBPayHookSpecification.sol +0 -1
  28. package/src/structs/JBPermissionsData.sol +0 -1
  29. package/src/structs/JBRuleset.sol +0 -1
  30. package/src/structs/JBRulesetConfig.sol +0 -1
  31. package/src/structs/JBRulesetMetadata.sol +0 -1
  32. package/src/structs/JBRulesetWeightCache.sol +0 -1
  33. package/src/structs/JBRulesetWithMetadata.sol +0 -1
  34. package/src/structs/JBSingleAllowance.sol +0 -1
  35. package/src/structs/JBSplit.sol +0 -1
  36. package/src/structs/JBSplitGroup.sol +0 -1
  37. package/src/structs/JBSplitHookContext.sol +0 -1
  38. package/src/structs/JBTerminalConfig.sol +0 -1
  39. package/src/structs/JBTokenAmount.sol +0 -1
  40. package/test/TestCashOutHooks.sol +12 -2
  41. package/test/TestDataHookFuzzing.sol +4 -4
  42. package/test/TestForwardedTokenConsumption.sol +7 -1
  43. package/test/TestJBERC20Inheritance.sol +3 -1
  44. package/test/TestTokenFlow.sol +2 -2
  45. package/test/audit/CashOutReenterPay.t.sol +5 -0
  46. package/test/audit/CodexHeldFeeRounding.t.sol +159 -0
  47. package/test/helpers/TestBaseWorkflow.sol +1 -1
  48. package/test/units/static/JBERC20/JBERC20Setup.sol +8 -3
  49. package/test/units/static/JBERC20/TestInitialize.sol +12 -13
  50. package/test/units/static/JBERC20/TestName.sol +1 -1
  51. package/test/units/static/JBERC20/TestNonces.sol +2 -1
  52. package/test/units/static/JBERC20/TestSymbol.sol +1 -1
  53. package/test/units/static/JBTerminalStore/TestPreviewCashOutFrom.sol +1 -1
  54. package/test/units/static/JBTerminalStore/TestRecordCashOutsFor.sol +4 -4
  55. package/test/units/static/JBTokens/JBTokensSetup.sol +5 -1
@@ -34,6 +34,34 @@ abstract contract JBPermissioned is Context, IJBPermissioned {
34
34
  // -------------------------- internal views ------------------------- //
35
35
  //*********************************************************************//
36
36
 
37
+ /// @notice Check whether an operator is the account or has the relevant permission.
38
+ /// @param operator The address to check.
39
+ /// @param account The account to allow.
40
+ /// @param projectId The project ID to check the permission under.
41
+ /// @param permissionId The required permission ID. The operator must have this permission within the specified
42
+ /// project ID.
43
+ /// @return Whether the operator is the account or has the permission.
44
+ function _hasPermissionFrom(
45
+ address operator,
46
+ address account,
47
+ uint256 projectId,
48
+ uint256 permissionId
49
+ )
50
+ internal
51
+ view
52
+ returns (bool)
53
+ {
54
+ return operator == account
55
+ || PERMISSIONS.hasPermission({
56
+ operator: operator,
57
+ account: account,
58
+ projectId: projectId,
59
+ permissionId: permissionId,
60
+ includeRoot: true,
61
+ includeWildcardProjectId: true
62
+ });
63
+ }
64
+
37
65
  /// @notice Require the message sender to be the account or have the relevant permission.
38
66
  /// @param account The account to allow.
39
67
  /// @param projectId The project ID to check the permission under.
@@ -19,7 +19,11 @@ interface IJBRulesetDataHook is IERC165 {
19
19
  /// @return cashOutTaxRate The rate determining the reclaimable amount for a given surplus and token supply.
20
20
  /// @return effectiveCashOutCount The effective token count to use for pricing the cash out. The terminal still
21
21
  /// burns the caller-supplied token count.
22
- /// @return effectiveTotalSupply The effective total supply to use for pricing the cash out.
22
+ /// @return effectiveTotalSupply The effective total supply to use for both the proportional reclaim and tax
23
+ /// calculations. For omnichain projects, this should include tokens on other chains so the tax cannot be bypassed.
24
+ /// @return effectiveSurplusValue The surplus value to use for the bonding curve calculation, denominated in the
25
+ /// same token, decimals, and currency as `context.surplus`. The terminal caps the reclaim at locally available
26
+ /// funds.
23
27
  /// @return hookSpecifications The amount and data to send to cash out hooks instead of returning to the
24
28
  /// beneficiary.
25
29
  function beforeCashOutRecordedWith(JBBeforeCashOutRecordedContext calldata context)
@@ -29,6 +33,7 @@ interface IJBRulesetDataHook is IERC165 {
29
33
  uint256 cashOutTaxRate,
30
34
  uint256 effectiveCashOutCount,
31
35
  uint256 effectiveTotalSupply,
36
+ uint256 effectiveSurplusValue,
32
37
  JBCashOutHookSpecification[] memory hookSpecifications
33
38
  );
34
39
 
@@ -26,11 +26,11 @@ interface IJBToken {
26
26
  /// @param amount The amount of tokens to burn.
27
27
  function burn(address account, uint256 amount) external;
28
28
 
29
- /// @notice Initializes the token with a name, symbol, and owner.
29
+ /// @notice Initializes the token with a name, symbol, and the JBTokens contract.
30
30
  /// @param name The token's name.
31
31
  /// @param symbol The token's symbol.
32
- /// @param owner The token contract's owner.
33
- function initialize(string memory name, string memory symbol, address owner) external;
32
+ /// @param tokens The JBTokens contract that manages this token.
33
+ function initialize(string memory name, string memory symbol, address tokens) external;
34
34
 
35
35
  /// @notice Mints tokens to an account.
36
36
  /// @param account The address to mint tokens to.
@@ -5,7 +5,6 @@ pragma solidity ^0.8.0;
5
5
  /// @custom:member decimals The number of decimals expected in that token's fixed point accounting.
6
6
  /// @custom:member currency The currency that the token is priced in terms of. By convention, this is
7
7
  /// `uint32(uint160(tokenAddress))` for tokens, or a constant ID from e.g. `JBCurrencyIds` for other currencies.
8
- // forge-lint: disable-next-line(pascal-case-struct)
9
8
  struct JBAccountingContext {
10
9
  address token;
11
10
  uint8 decimals;
@@ -16,7 +16,6 @@ import {JBTokenAmount} from "./JBTokenAmount.sol";
16
16
  /// @custom:member beneficiary The address the reclaimed amount will be sent to.
17
17
  /// @custom:member hookMetadata Extra data specified by the data hook, which is sent to the cash out hook.
18
18
  /// @custom:member cashOutMetadata Extra data specified by the account cashing out, which is sent to the cash out hook.
19
- // forge-lint: disable-next-line(pascal-case-struct)
20
19
  struct JBAfterCashOutRecordedContext {
21
20
  address holder;
22
21
  uint256 projectId;
@@ -15,7 +15,6 @@ import {JBTokenAmount} from "./JBTokenAmount.sol";
15
15
  /// @custom:member beneficiary The address which receives any tokens this payment yields.
16
16
  /// @custom:member hookMetadata Extra data specified by the data hook, which is sent to the pay hook.
17
17
  /// @custom:member payerMetadata Extra data specified by the payer, which is sent to the pay hook.
18
- // forge-lint: disable-next-line(pascal-case-struct)
19
18
  struct JBAfterPayRecordedContext {
20
19
  address payer;
21
20
  uint256 projectId;
@@ -20,7 +20,6 @@ import {JBTokenAmount} from "./JBTokenAmount.sol";
20
20
  /// that charge their own fees — they can skip fees when value stays in the protocol (e.g. project-to-project
21
21
  /// routing).
22
22
  /// @custom:member metadata Extra data provided by the casher.
23
- // forge-lint: disable-next-line(pascal-case-struct)
24
23
  struct JBBeforeCashOutRecordedContext {
25
24
  address terminal;
26
25
  address holder;
@@ -15,7 +15,6 @@ import {JBTokenAmount} from "./JBTokenAmount.sol";
15
15
  /// @custom:member weight The weight of the ruleset during which the payment is being made.
16
16
  /// @custom:member reservedPercent The reserved percent of the ruleset the payment is being made during.
17
17
  /// @custom:member metadata Extra data specified by the payer.
18
- // forge-lint: disable-next-line(pascal-case-struct)
19
18
  struct JBBeforePayRecordedContext {
20
19
  address terminal;
21
20
  address payer;
@@ -9,7 +9,6 @@ import {IJBCashOutHook} from "../interfaces/IJBCashOutHook.sol";
9
9
  /// @custom:member noop A flag indicating if the hook callback should be skipped.
10
10
  /// @custom:member amount The amount to send to the hook.
11
11
  /// @custom:member metadata Metadata to pass to the hook.
12
- // forge-lint: disable-next-line(pascal-case-struct)
13
12
  struct JBCashOutHookSpecification {
14
13
  IJBCashOutHook hook;
15
14
  bool noop;
@@ -4,7 +4,6 @@ pragma solidity ^0.8.0;
4
4
  /// @custom:member amount The amount of the currency.
5
5
  /// @custom:member currency The currency. By convention, this is `uint32(uint160(tokenAddress))` for tokens, or a
6
6
  /// constant ID from e.g. `JBCurrencyIds` for other currencies.
7
- // forge-lint: disable-next-line(pascal-case-struct)
8
7
  struct JBCurrencyAmount {
9
8
  uint224 amount;
10
9
  uint32 currency;
@@ -5,7 +5,6 @@ pragma solidity ^0.8.0;
5
5
  /// decimals as the terminal in which this struct was created.
6
6
  /// @custom:member beneficiary The address that will receive the tokens that are minted as a result of the fee payment.
7
7
  /// @custom:member unlockTimestamp The timestamp at which the fee is unlocked and can be processed.
8
- // forge-lint: disable-next-line(pascal-case-struct)
9
8
  struct JBFee {
10
9
  uint256 amount;
11
10
  address beneficiary;
@@ -20,7 +20,6 @@ import {JBCurrencyAmount} from "./JBCurrencyAmount.sol";
20
20
  /// @custom:member surplusAllowances An array of surplus allowances. The surplus allowances cumulatively dictates the
21
21
  /// maximum value of `token`s a project can pay out from its surplus (balance less payouts) in a terminal during a
22
22
  /// ruleset. Each surplus allowance can have a unique currency and amount.
23
- // forge-lint: disable-next-line(pascal-case-struct)
24
23
  struct JBFundAccessLimitGroup {
25
24
  address terminal;
26
25
  address token;
@@ -9,7 +9,6 @@ import {IJBPayHook} from "../interfaces/IJBPayHook.sol";
9
9
  /// @custom:member noop A flag indicating if the hook callback should be skipped.
10
10
  /// @custom:member amount The amount to send to the hook.
11
11
  /// @custom:member metadata Metadata to pass the hook.
12
- // forge-lint: disable-next-line(pascal-case-struct)
13
12
  struct JBPayHookSpecification {
14
13
  IJBPayHook hook;
15
14
  bool noop;
@@ -6,7 +6,6 @@ pragma solidity ^0.8.0;
6
6
  /// permissions under this project's scope. An ID of 0 is a wildcard, which gives an operator permissions across all
7
7
  /// projects.
8
8
  /// @custom:member permissionIds The IDs of the permissions being given. See the `JBPermissionIds` library.
9
- // forge-lint: disable-next-line(pascal-case-struct)
10
9
  struct JBPermissionsData {
11
10
  address operator;
12
11
  uint64 projectId;
@@ -29,7 +29,6 @@ import {IJBRulesetApprovalHook} from "./../interfaces/IJBRulesetApprovalHook.sol
29
29
  /// ruleset is rejected, it won't go into effect. An approval hook can be used to create rules which dictate how a
30
30
  /// project owner can change their ruleset over time.
31
31
  /// @custom:member metadata Extra data associated with a ruleset which can be used by other contracts.
32
- // forge-lint: disable-next-line(pascal-case-struct)
33
32
  struct JBRuleset {
34
33
  uint48 cycleNumber;
35
34
  uint48 id;
@@ -31,7 +31,6 @@ import {JBSplitGroup} from "./JBSplitGroup.sol";
31
31
  /// its balance in each payment terminal while the ruleset is active. Amounts are fixed point numbers using the same
32
32
  /// number of decimals as the corresponding terminal. The `_payoutLimit` and `_surplusAllowance` parameters must fit in
33
33
  /// a `uint232`.
34
- // forge-lint: disable-next-line(pascal-case-struct)
35
34
  struct JBRulesetConfig {
36
35
  uint48 mustStartAtOrAfter;
37
36
  uint32 duration;
@@ -33,7 +33,6 @@ pragma solidity ^0.8.0;
33
33
  /// @custom:member dataHook The data hook to use during this ruleset.
34
34
  /// @custom:member metadata Metadata of the metadata, only the 14 least significant bits can be used, the 2 most
35
35
  /// significant bits are disregarded.
36
- // forge-lint: disable-next-line(pascal-case-struct)
37
36
  struct JBRulesetMetadata {
38
37
  uint16 reservedPercent;
39
38
  uint16 cashOutTaxRate;
@@ -3,7 +3,6 @@ pragma solidity ^0.8.0;
3
3
 
4
4
  /// @custom:member weight The cached weight value.
5
5
  /// @custom:member weightCutMultiple The weight cut multiple that produces the given weight.
6
- // forge-lint: disable-next-line(pascal-case-struct)
7
6
  struct JBRulesetWeightCache {
8
7
  uint112 weight;
9
8
  uint168 weightCutMultiple;
@@ -6,7 +6,6 @@ import {JBRulesetMetadata} from "./JBRulesetMetadata.sol";
6
6
 
7
7
  /// @custom:member ruleset The ruleset.
8
8
  /// @custom:member metadata The ruleset's metadata.
9
- // forge-lint: disable-next-line(pascal-case-struct)
10
9
  struct JBRulesetWithMetadata {
11
10
  JBRuleset ruleset;
12
11
  JBRulesetMetadata metadata;
@@ -7,7 +7,6 @@ pragma solidity ^0.8.0;
7
7
  /// @custom:member nonce An incrementing value indexed per owner,token,and spender for each signature.
8
8
  /// @custom:member signature The signature over the permit data. Supports EOA signatures, compact signatures defined by
9
9
  /// EIP-2098, and contract signatures defined by EIP-1271.
10
- // forge-lint: disable-next-line(pascal-case-struct)
11
10
  struct JBSingleAllowance {
12
11
  uint256 sigDeadline;
13
12
  uint160 amount;
@@ -30,7 +30,6 @@ import {IJBSplitHook} from "./../interfaces/IJBSplitHook.sol";
30
30
  /// preserve those splits at the governance/configuration layer.
31
31
  /// @custom:member hook A contract which will receive this split's tokens and properties, and can define custom
32
32
  /// behavior.
33
- // forge-lint: disable-next-line(pascal-case-struct)
34
33
  struct JBSplit {
35
34
  uint32 percent;
36
35
  uint64 projectId;
@@ -6,7 +6,6 @@ import {JBSplit} from "./JBSplit.sol";
6
6
  /// @custom:member groupId An identifier for the group. By convention, this ID is `uint256(uint160(tokenAddress))` for
7
7
  /// payouts and `1` for reserved tokens.
8
8
  /// @custom:member splits The splits in the group.
9
- // forge-lint: disable-next-line(pascal-case-struct)
10
9
  struct JBSplitGroup {
11
10
  uint256 groupId;
12
11
  JBSplit[] splits;
@@ -10,7 +10,6 @@ import {JBSplit} from "./JBSplit.sol";
10
10
  /// @custom:member groupId The group the split belongs to. By convention, this ID is `uint256(uint160(tokenAddress))`
11
11
  /// for payouts and `1` for reserved tokens.
12
12
  /// @custom:member split The split which specified the hook.
13
- // forge-lint: disable-next-line(pascal-case-struct)
14
13
  struct JBSplitHookContext {
15
14
  address token;
16
15
  uint256 amount;
@@ -6,7 +6,6 @@ import {IJBTerminal} from "./../interfaces/IJBTerminal.sol";
6
6
 
7
7
  /// @custom:member terminal The terminal to configure.
8
8
  /// @custom:member accountingContextsToAccept The accounting contexts to accept from the terminal.
9
- // forge-lint: disable-next-line(pascal-case-struct)
10
9
  struct JBTerminalConfig {
11
10
  IJBTerminal terminal;
12
11
  JBAccountingContext[] accountingContextsToAccept;
@@ -6,7 +6,6 @@ pragma solidity ^0.8.0;
6
6
  /// @custom:member currency The currency. By convention, this is `uint32(uint160(tokenAddress))` for tokens, or a
7
7
  /// constant ID from e.g. `JBCurrencyIds` for other currencies.
8
8
  /// @custom:member value The amount of tokens that was paid, as a fixed point number.
9
- // forge-lint: disable-next-line(pascal-case-struct)
10
9
  struct JBTokenAmount {
11
10
  address token;
12
11
  uint8 decimals;
@@ -207,7 +207,11 @@ contract TestCashOutHooks_Local is TestBaseWorkflow {
207
207
  _DATA_HOOK,
208
208
  abi.encodeWithSelector(IJBRulesetDataHook.beforeCashOutRecordedWith.selector),
209
209
  abi.encode(
210
- _ruleset.cashOutTaxRate(), _beneficiaryTokenBalance / 2, _beneficiaryTokenBalance, _specifications
210
+ _ruleset.cashOutTaxRate(),
211
+ _beneficiaryTokenBalance / 2,
212
+ _beneficiaryTokenBalance,
213
+ _nativePayAmount,
214
+ _specifications
211
215
  )
212
216
  );
213
217
 
@@ -322,7 +326,13 @@ contract TestCashOutHooks_Local is TestBaseWorkflow {
322
326
  vm.mockCall(
323
327
  _DATA_HOOK,
324
328
  abi.encodeWithSelector(IJBRulesetDataHook.beforeCashOutRecordedWith.selector),
325
- abi.encode(_customCashOutTaxRate, _customCashOutCount, _customTotalSupply, _specifications)
329
+ abi.encode(
330
+ _customCashOutTaxRate,
331
+ _customCashOutCount,
332
+ _customTotalSupply,
333
+ _nativeTerminalBalance, // Same as _nativePayAmount (no payouts); avoids stack depth limit.
334
+ _specifications
335
+ )
326
336
  );
327
337
 
328
338
  _terminal.cashOutTokensOf({
@@ -261,12 +261,12 @@ contract TestDataHookFuzzing_Local is TestBaseWorkflow {
261
261
  uint256 cashOutCount = tokenBalance / 2;
262
262
  _hookTotalSupply = bound(_hookTotalSupply, cashOutCount, tokenBalance * 10);
263
263
 
264
- // Data hook returns: cashOutTaxRate=0, cashOutCount=half, custom totalSupply, no hook specs.
264
+ // Data hook returns: cashOutTaxRate=0, cashOutCount=half, custom totalSupply, local surplus, no hook specs.
265
265
  JBCashOutHookSpecification[] memory _emptyCashOutSpecs = new JBCashOutHookSpecification[](0);
266
266
  vm.mockCall(
267
267
  _DATA_HOOK,
268
268
  abi.encodeWithSelector(IJBRulesetDataHook.beforeCashOutRecordedWith.selector),
269
- abi.encode(uint256(0), cashOutCount, _hookTotalSupply, _emptyCashOutSpecs)
269
+ abi.encode(uint256(0), cashOutCount, _hookTotalSupply, _payAmount, _emptyCashOutSpecs)
270
270
  );
271
271
 
272
272
  uint256 balanceBefore = address(this).balance;
@@ -336,7 +336,7 @@ contract TestDataHookFuzzing_Local is TestBaseWorkflow {
336
336
  vm.mockCall(
337
337
  _DATA_HOOK,
338
338
  abi.encodeWithSelector(IJBRulesetDataHook.beforeCashOutRecordedWith.selector),
339
- abi.encode(uint256(0), cashOutCount, tokenBalance, _specs)
339
+ abi.encode(uint256(0), cashOutCount, tokenBalance, _payAmount, _specs)
340
340
  );
341
341
 
342
342
  // Mock the cash out hook call.
@@ -500,7 +500,7 @@ contract TestDataHookFuzzing_Local is TestBaseWorkflow {
500
500
  vm.mockCall(
501
501
  _DATA_HOOK,
502
502
  abi.encodeWithSelector(IJBRulesetDataHook.beforeCashOutRecordedWith.selector),
503
- abi.encode(uint256(0), tokenBalance, tokenBalance, _specs)
503
+ abi.encode(uint256(0), tokenBalance, tokenBalance, _payAmount, _specs)
504
504
  );
505
505
 
506
506
  vm.expectRevert(
@@ -145,7 +145,13 @@ contract TestForwardedTokenConsumption_Local is TestBaseWorkflow {
145
145
  vm.mockCall(
146
146
  _DATA_HOOK,
147
147
  abi.encodeWithSelector(IJBRulesetDataHook.beforeCashOutRecordedWith.selector),
148
- abi.encode(0, cashOutCount, _controller.totalTokenSupplyWithReservedTokensOf(_projectId), specifications)
148
+ abi.encode(
149
+ 0,
150
+ cashOutCount,
151
+ _controller.totalTokenSupplyWithReservedTokensOf(_projectId),
152
+ _PAY_AMOUNT,
153
+ specifications
154
+ )
149
155
  );
150
156
 
151
157
  vm.prank(multisig());
@@ -3,6 +3,8 @@ pragma solidity 0.8.28;
3
3
 
4
4
  import {TestBaseWorkflow} from "./helpers/TestBaseWorkflow.sol";
5
5
  import {JBERC20} from "../src/JBERC20.sol";
6
+ import {IJBPermissions} from "../src/interfaces/IJBPermissions.sol";
7
+ import {IJBProjects} from "../src/interfaces/IJBProjects.sol";
6
8
  import {IJBRulesetApprovalHook} from "../src/interfaces/IJBRulesetApprovalHook.sol";
7
9
  import {IJBToken} from "../src/interfaces/IJBToken.sol";
8
10
  import {JBConstants} from "../src/libraries/JBConstants.sol";
@@ -15,7 +17,7 @@ import {JBTerminalConfig} from "../src/structs/JBTerminalConfig.sol";
15
17
 
16
18
  import {ERC20Votes} from "../src/JBERC20.sol";
17
19
 
18
- contract JBERC20Inheritance_Local is JBERC20, TestBaseWorkflow {
20
+ contract JBERC20Inheritance_Local is JBERC20(IJBPermissions(address(1)), IJBProjects(address(2))), TestBaseWorkflow {
19
21
  /// This test is to verify that the inheritance order of JBERC20 is correct and that it calls the
20
22
  /// `ERC20Votes._update()`
21
23
  /// forge-config: default.allow_internal_expect_revert = true
@@ -99,8 +99,8 @@ contract TestTokenFlow_Local is TestBaseWorkflow {
99
99
  });
100
100
  } else {
101
101
  // Create a new `IJBToken` and change it's owner to the `JBTokens` contract.
102
- IJBToken _newToken = IJBToken(Clones.clone(address(new JBERC20())));
103
- _newToken.initialize({name: "NewTestName", symbol: "NewTestSymbol", owner: address(_tokens)});
102
+ IJBToken _newToken = IJBToken(Clones.clone(address(new JBERC20(jbPermissions(), jbProjects()))));
103
+ _newToken.initialize({name: "NewTestName", symbol: "NewTestSymbol", tokens: address(_tokens)});
104
104
 
105
105
  // Mock the token can be added to the project.
106
106
  vm.mockCall(
@@ -260,6 +260,7 @@ contract CashOutReenterPay is TestBaseWorkflow {
260
260
  ruleset.cashOutTaxRate(), // Use the ruleset's 50% cash out tax rate.
261
261
  cashOutCount, // Number of tokens being cashed out.
262
262
  totalSupply, // Total supply for the bonding curve.
263
+ PAY_AMOUNT, // effectiveSurplusValue — full initial funding, no payouts yet.
263
264
  specifications // Our malicious hook specification.
264
265
  )
265
266
  );
@@ -475,6 +476,9 @@ contract CashOutReenterPay is TestBaseWorkflow {
475
476
  // Read the current total supply for the bonding curve calculation.
476
477
  uint256 totalSupply = _tokens.totalSupplyOf(_projectId);
477
478
 
479
+ // Read the current surplus for the bonding curve.
480
+ uint256 surplus = jbTerminalStore().balanceOf(address(_terminal), _projectId, JBConstants.NATIVE_TOKEN);
481
+
478
482
  // Mock the data hook to return no hook specifications (simple cashout).
479
483
  vm.mockCall(
480
484
  DATA_HOOK,
@@ -483,6 +487,7 @@ contract CashOutReenterPay is TestBaseWorkflow {
483
487
  ruleset.cashOutTaxRate(), // Pass through the ruleset's tax rate.
484
488
  cashOutCount, // Number of tokens being cashed out.
485
489
  totalSupply, // Current total supply.
490
+ surplus, // effectiveSurplusValue — current terminal balance.
486
491
  new JBCashOutHookSpecification[](0) // No hooks for this cashout.
487
492
  )
488
493
  );
@@ -0,0 +1,159 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.6;
3
+
4
+ import {TestBaseWorkflow} from "../helpers/TestBaseWorkflow.sol";
5
+ import {IJBController} from "../../src/interfaces/IJBController.sol";
6
+ import {IJBMultiTerminal} from "../../src/interfaces/IJBMultiTerminal.sol";
7
+ import {IJBRulesetApprovalHook} from "../../src/interfaces/IJBRulesetApprovalHook.sol";
8
+ import {JBConstants} from "../../src/libraries/JBConstants.sol";
9
+ import {JBAccountingContext} from "../../src/structs/JBAccountingContext.sol";
10
+ import {JBCurrencyAmount} from "../../src/structs/JBCurrencyAmount.sol";
11
+ import {JBFundAccessLimitGroup} from "../../src/structs/JBFundAccessLimitGroup.sol";
12
+ import {JBRulesetConfig} from "../../src/structs/JBRulesetConfig.sol";
13
+ import {JBRulesetMetadata} from "../../src/structs/JBRulesetMetadata.sol";
14
+ import {JBSplitGroup} from "../../src/structs/JBSplitGroup.sol";
15
+ import {JBTerminalConfig} from "../../src/structs/JBTerminalConfig.sol";
16
+
17
+ contract CodexHeldFeeRoundingTest is TestBaseWorkflow {
18
+ IJBController private _controller;
19
+ IJBMultiTerminal private _terminal;
20
+
21
+ uint256 private _projectId;
22
+ address private _projectOwner;
23
+ address private _beneficiary;
24
+
25
+ function setUp() public override {
26
+ super.setUp();
27
+
28
+ _projectOwner = multisig();
29
+ _beneficiary = beneficiary();
30
+ _terminal = jbMultiTerminal();
31
+ _controller = jbController();
32
+
33
+ JBRulesetMetadata memory metadata = JBRulesetMetadata({
34
+ reservedPercent: 0,
35
+ cashOutTaxRate: 0,
36
+ baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
37
+ pausePay: false,
38
+ pauseCreditTransfers: false,
39
+ allowOwnerMinting: false,
40
+ allowSetCustomToken: false,
41
+ allowTerminalMigration: true,
42
+ allowSetTerminals: false,
43
+ ownerMustSendPayouts: false,
44
+ allowSetController: false,
45
+ allowAddAccountingContext: true,
46
+ allowAddPriceFeed: false,
47
+ holdFees: true,
48
+ useTotalSurplusForCashOuts: false,
49
+ useDataHookForPay: false,
50
+ useDataHookForCashOut: false,
51
+ dataHook: address(0),
52
+ metadata: 0
53
+ });
54
+
55
+ JBCurrencyAmount[] memory payoutLimits = new JBCurrencyAmount[](1);
56
+ payoutLimits[0] = JBCurrencyAmount({amount: 100, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))});
57
+
58
+ JBCurrencyAmount[] memory surplusAllowances = new JBCurrencyAmount[](1);
59
+ surplusAllowances[0] = JBCurrencyAmount({amount: 0, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))});
60
+
61
+ JBFundAccessLimitGroup[] memory fundAccessLimits = new JBFundAccessLimitGroup[](1);
62
+ fundAccessLimits[0] = JBFundAccessLimitGroup({
63
+ terminal: address(_terminal),
64
+ token: JBConstants.NATIVE_TOKEN,
65
+ payoutLimits: payoutLimits,
66
+ surplusAllowances: surplusAllowances
67
+ });
68
+
69
+ JBRulesetConfig[] memory rulesetConfigs = new JBRulesetConfig[](1);
70
+ rulesetConfigs[0] = JBRulesetConfig({
71
+ mustStartAtOrAfter: 0,
72
+ duration: 0,
73
+ weight: 0,
74
+ weightCutPercent: 0,
75
+ approvalHook: IJBRulesetApprovalHook(address(0)),
76
+ metadata: metadata,
77
+ splitGroups: new JBSplitGroup[](0),
78
+ fundAccessLimitGroups: fundAccessLimits
79
+ });
80
+
81
+ JBAccountingContext[] memory contexts = new JBAccountingContext[](1);
82
+ contexts[0] = JBAccountingContext({
83
+ token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
84
+ });
85
+
86
+ JBTerminalConfig[] memory terminalConfigs = new JBTerminalConfig[](1);
87
+ terminalConfigs[0] = JBTerminalConfig({terminal: _terminal, accountingContextsToAccept: contexts});
88
+
89
+ // Project 1 is the fee project.
90
+ _controller.launchProjectFor({
91
+ owner: _projectOwner,
92
+ projectUri: "fee-project",
93
+ rulesetConfigurations: rulesetConfigs,
94
+ terminalConfigurations: terminalConfigs,
95
+ memo: ""
96
+ });
97
+
98
+ _projectId = _controller.launchProjectFor({
99
+ owner: _projectOwner,
100
+ projectUri: "project",
101
+ rulesetConfigurations: rulesetConfigs,
102
+ terminalConfigurations: terminalConfigs,
103
+ memo: ""
104
+ });
105
+ }
106
+
107
+ function test_partialHeldFeeRepaymentCanEraseRemainingFee() external {
108
+ // Seed the project with enough balance to send a payout that holds fees.
109
+ _terminal.pay{value: 100}({
110
+ projectId: _projectId,
111
+ amount: 100,
112
+ token: JBConstants.NATIVE_TOKEN,
113
+ beneficiary: _beneficiary,
114
+ minReturnedTokens: 0,
115
+ memo: "",
116
+ metadata: new bytes(0)
117
+ });
118
+
119
+ _terminal.sendPayoutsOf({
120
+ projectId: _projectId,
121
+ token: JBConstants.NATIVE_TOKEN,
122
+ amount: 40,
123
+ currency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
124
+ minTokensPaidOut: 0
125
+ });
126
+
127
+ // 40 gross produces a 1 wei fee and 39 wei net payout.
128
+ assertEq(address(_projectOwner).balance, 39);
129
+
130
+ vm.prank(_projectOwner);
131
+ _terminal.addToBalanceOf{value: 1}({
132
+ projectId: _projectId,
133
+ token: JBConstants.NATIVE_TOKEN,
134
+ amount: 1,
135
+ shouldReturnHeldFees: true,
136
+ memo: "",
137
+ metadata: new bytes(0)
138
+ });
139
+
140
+ // After repaying only 1 wei of the 39 wei payout, the fee should still be owed in full.
141
+ uint256 feeProjectBalanceBefore = jbTerminalStore().balanceOf(address(_terminal), 1, JBConstants.NATIVE_TOKEN);
142
+ assertEq(feeProjectBalanceBefore, 0);
143
+
144
+ vm.warp(block.timestamp + 2_419_200);
145
+ _terminal.processHeldFeesOf(_projectId, JBConstants.NATIVE_TOKEN, 10);
146
+
147
+ uint256 feeProjectBalanceAfter = jbTerminalStore().balanceOf(address(_terminal), 1, JBConstants.NATIVE_TOKEN);
148
+ uint256 projectBalanceAfter =
149
+ jbTerminalStore().balanceOf(address(_terminal), _projectId, JBConstants.NATIVE_TOKEN);
150
+
151
+ // The fee project never receives the original 1 wei fee.
152
+ assertEq(feeProjectBalanceAfter, 0);
153
+ // The payer project only gets its explicit top-up recorded.
154
+ assertEq(projectBalanceAfter, 61);
155
+ // One wei remains stranded in the terminal: actual native balance exceeds tracked balances.
156
+ assertEq(address(_terminal).balance, 62);
157
+ assertEq(address(_terminal).balance - (feeProjectBalanceAfter + projectBalanceAfter), 1);
158
+ }
159
+ }
@@ -293,7 +293,7 @@ contract TestBaseWorkflow is JBTest, DeployPermit2 {
293
293
  _jbPermissions = new JBPermissions(_trustedForwarder);
294
294
  _jbProjects = new JBProjects(_multisig, address(0), _trustedForwarder);
295
295
  _jbDirectory = new JBDirectory(_jbPermissions, _jbProjects, _multisig);
296
- _jbErc20 = new JBERC20();
296
+ _jbErc20 = new JBERC20(_jbPermissions, _jbProjects);
297
297
  _jbTokens = new JBTokens(_jbDirectory, _jbErc20);
298
298
  _jbRulesets = new JBRulesets(_jbDirectory);
299
299
  _jbPrices = new JBPrices(_jbDirectory, _jbPermissions, _jbProjects, _multisig, _trustedForwarder);
@@ -3,6 +3,8 @@ pragma solidity 0.8.28;
3
3
 
4
4
  import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol";
5
5
  import {JBERC20} from "../../../../src/JBERC20.sol";
6
+ import {IJBPermissions} from "../../../../src/interfaces/IJBPermissions.sol";
7
+ import {IJBProjects} from "../../../../src/interfaces/IJBProjects.sol";
6
8
  import {IJBToken} from "../../../../src/interfaces/IJBToken.sol";
7
9
  import {JBTest} from "../../../helpers/JBTest.sol";
8
10
 
@@ -11,7 +13,10 @@ Contract that deploys a target contract with other mock contracts to satisfy the
11
13
  Tests relative to this contract will be dependent on mock calls/emits and stdStorage.
12
14
  */
13
15
  contract JBERC20Setup is JBTest {
14
- address _owner = makeAddr("owner");
16
+ // Mocks
17
+ address _tokens = makeAddr("tokens");
18
+ IJBProjects _projects = IJBProjects(makeAddr("projects"));
19
+ IJBPermissions _permissions = IJBPermissions(makeAddr("permissions"));
15
20
 
16
21
  // Implementation (constructor sets _name = "invalid", cannot be initialized)
17
22
  IJBToken public _implementation;
@@ -20,8 +25,8 @@ contract JBERC20Setup is JBTest {
20
25
  IJBToken public _erc20;
21
26
 
22
27
  function erc20Setup() public virtual {
23
- // Deploy the implementation
24
- _implementation = new JBERC20();
28
+ // Deploy the implementation with immutable permissions and projects
29
+ _implementation = new JBERC20(_permissions, _projects);
25
30
 
26
31
  // Clone it — clones start with empty storage, so initialize() works
27
32
  _erc20 = IJBToken(Clones.clone(address(_implementation)));