@bananapus/core-v6 0.0.33 → 0.0.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ADMINISTRATION.md +8 -7
- package/RISKS.md +38 -1
- package/package.json +2 -2
- package/references/entrypoints.md +1 -1
- package/script/Deploy.s.sol +2 -1
- package/src/JBERC20.sol +101 -30
- package/src/JBTerminalStore.sol +64 -23
- package/src/JBTokens.sol +1 -1
- package/src/abstract/JBPermissioned.sol +28 -0
- package/src/interfaces/IJBRulesetDataHook.sol +6 -1
- package/src/interfaces/IJBToken.sol +3 -3
- package/test/TestCashOutHooks.sol +12 -2
- package/test/TestDataHookFuzzing.sol +4 -4
- package/test/TestForwardedTokenConsumption.sol +7 -1
- package/test/TestJBERC20Inheritance.sol +3 -1
- package/test/TestTokenFlow.sol +2 -2
- package/test/audit/CashOutReenterPay.t.sol +5 -0
- package/test/audit/CodexHeldFeeRounding.t.sol +159 -0
- package/test/helpers/TestBaseWorkflow.sol +1 -1
- package/test/units/static/JBERC20/JBERC20Setup.sol +8 -3
- package/test/units/static/JBERC20/TestInitialize.sol +12 -13
- package/test/units/static/JBERC20/TestName.sol +1 -1
- package/test/units/static/JBERC20/TestNonces.sol +2 -1
- package/test/units/static/JBERC20/TestSymbol.sol +1 -1
- package/test/units/static/JBTerminalStore/TestPreviewCashOutFrom.sol +1 -1
- package/test/units/static/JBTerminalStore/TestRecordCashOutsFor.sol +4 -4
- package/test/units/static/JBTokens/JBTokensSetup.sol +5 -1
package/ADMINISTRATION.md
CHANGED
|
@@ -78,10 +78,10 @@ Admin privileges and their scope in nana-core-v6.
|
|
|
78
78
|
- **How assigned:** Set at JBFeelessAddresses deployment via the Ownable constructor. Transferable via `Ownable.transferOwnership()`.
|
|
79
79
|
- **Scope:** Protocol-wide. Controls which addresses are exempt from protocol fees.
|
|
80
80
|
|
|
81
|
-
### JBERC20
|
|
81
|
+
### JBERC20 Access Control
|
|
82
82
|
|
|
83
|
-
- **How assigned:**
|
|
84
|
-
- **Scope:** Single token contract. Only the
|
|
83
|
+
- **How assigned:** The `TOKENS` reference is set to the `JBTokens` contract address during `initialize()`. `PERMISSIONS` and `PROJECTS` are constructor immutables inherited from `JBPermissioned` and the implementation contract respectively.
|
|
84
|
+
- **Scope:** Single token contract. Only the `JBTokens` contract (via the `onlyTokens` modifier) can mint, burn, and update token metadata. Project owners or operators with the `SIGN_FOR_ERC20` permission can authorize ERC-1271 signatures.
|
|
85
85
|
|
|
86
86
|
### Omnichain Ruleset Operator
|
|
87
87
|
|
|
@@ -211,10 +211,11 @@ Admin privileges and their scope in nana-core-v6.
|
|
|
211
211
|
|
|
212
212
|
| Function | Required Role | Permission ID | Scope | What It Does |
|
|
213
213
|
|----------|--------------|---------------|-------|-------------|
|
|
214
|
-
| `mint` |
|
|
215
|
-
| `burn` |
|
|
216
|
-
| `initialize` | Anyone (once) | N/A | Per token | Initializes the token name, symbol, and
|
|
217
|
-
| `setMetadata` |
|
|
214
|
+
| `mint` | JBTokens contract | N/A (onlyTokens) | Per token | Mints new tokens to an address. |
|
|
215
|
+
| `burn` | JBTokens contract | N/A (onlyTokens) | Per token | Burns tokens from an address. |
|
|
216
|
+
| `initialize` | Anyone (once) | N/A | Per token | Initializes the token name, symbol, and JBTokens contract reference. Can only be called once. |
|
|
217
|
+
| `setMetadata` | JBTokens contract | N/A (onlyTokens) | Per token | Updates the token's name and symbol. |
|
|
218
|
+
| `isValidSignature` | Anyone (view) | `SIGN_FOR_ERC20` | Per token | Validates ERC-1271 signatures for project owner or permitted operators. |
|
|
218
219
|
|
|
219
220
|
### JBTerminalStore
|
|
220
221
|
|
package/RISKS.md
CHANGED
|
@@ -174,7 +174,44 @@ No `ReentrancyGuard` is used. The system relies on state ordering and the `Inade
|
|
|
174
174
|
|
|
175
175
|
- `JBTerminalStore.recordAddedBalanceFor` has **no access control**. Any address can call it. The balance is keyed by `msg.sender` (the terminal address), so only a terminal can inflate its own recorded balance. This is safe as long as all terminals correctly track their actual holdings. A buggy or malicious terminal implementation could call `recordAddedBalanceFor` without actually receiving tokens, inflating the recorded balance above actual holdings.
|
|
176
176
|
|
|
177
|
-
## 8.
|
|
177
|
+
## 8. Accepted Behaviors
|
|
178
|
+
|
|
179
|
+
### 8.1 Cross-terminal surplus is an explicit trust boundary
|
|
180
|
+
|
|
181
|
+
When a project enables `useTotalSurplusForCashOuts`, core intentionally stops treating a single terminal's local
|
|
182
|
+
balance as the full economic truth and instead trusts every registered terminal's reported surplus. This means a
|
|
183
|
+
project can get richer cash-out pricing from value held elsewhere, but it also means a bad or economically
|
|
184
|
+
incompatible terminal can distort the aggregate. This is accepted because cross-terminal projects explicitly opt into
|
|
185
|
+
shared treasury semantics; the alternative is forcing every terminal to behave as an isolated silo. Projects should
|
|
186
|
+
only enable this mode when all participating terminals are mutually trusted and economically compatible.
|
|
187
|
+
|
|
188
|
+
### 8.2 Held-fee forgiveness on failed fee routing is fail-open by design
|
|
189
|
+
|
|
190
|
+
Core intentionally prefers liveness over strict protocol fee collection. If project `#1` cannot accept a fee payment,
|
|
191
|
+
`_processFee` returns the fee amount to the originating project's balance instead of locking funds. For held fees,
|
|
192
|
+
`processHeldFeesOf` advances the queue before retrying the payment, so a failed held-fee processing attempt
|
|
193
|
+
permanently forgives that fee. This is accepted because a broken fee route should not brick project treasury flows.
|
|
194
|
+
The tradeoff is explicit revenue leakage for the fee beneficiary when the fee route is unavailable or incompletely
|
|
195
|
+
wired.
|
|
196
|
+
|
|
197
|
+
### 8.3 Surplus allowance is ruleset-scoped, not implicit-cycle-scoped
|
|
198
|
+
|
|
199
|
+
`usedSurplusAllowanceOf` is keyed by terminal, project, token, ruleset, and currency rather than by an independently
|
|
200
|
+
incrementing "cycle" counter. For projects whose rulesets roll forward implicitly without a new ruleset ID, allowance
|
|
201
|
+
usage carries forward until a new ruleset actually takes effect. This is accepted because surplus allowance is meant
|
|
202
|
+
to be tied to the active ruleset's economics, not to a synthetic cycle abstraction layered on top of an unchanged
|
|
203
|
+
ruleset. Integrators that expect per-cycle resets should queue distinct rulesets instead of relying on implicit
|
|
204
|
+
rollover.
|
|
205
|
+
|
|
206
|
+
### 8.4 Core deployment alone leaves fee routing fail-open until periphery wiring completes
|
|
207
|
+
|
|
208
|
+
Core contracts are intentionally deployable before project `#1` is fully operational. Until the fee project's
|
|
209
|
+
controller, terminals, and accounting contexts are wired, fee-bearing flows remain fail-open: fees are forgiven back
|
|
210
|
+
to the originating project rather than trapped. This is accepted because deployment sequencing across repos is staged,
|
|
211
|
+
and the protocol prioritizes keeping project flows live during rollout over enforcing fee collection before the fee
|
|
212
|
+
beneficiary is ready.
|
|
213
|
+
|
|
214
|
+
## 9. Invariants to Verify
|
|
178
215
|
|
|
179
216
|
These should hold at all times and are the most productive targets for formal verification or invariant testing:
|
|
180
217
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bananapus/core-v6",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.34",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'nana-core-v6'"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@bananapus/permission-ids-v6": "^0.0.
|
|
29
|
+
"@bananapus/permission-ids-v6": "^0.0.17",
|
|
30
30
|
"@chainlink/contracts": "^1.5.0",
|
|
31
31
|
"@openzeppelin/contracts": "^5.6.1",
|
|
32
32
|
"@prb/math": "^4.1.1",
|
|
@@ -21,7 +21,7 @@ The core Juicebox V6 protocol on EVM: a modular system for launching treasury-ba
|
|
|
21
21
|
| `JBSplits` | Split configurations per project/ruleset/group. Packed storage for gas efficiency. |
|
|
22
22
|
| `JBFundAccessLimits` | Payout limits and surplus allowances per project/ruleset/terminal/token. |
|
|
23
23
|
| `JBPrices` | Price feed registry with project-specific and protocol-wide default feeds. Immutable once set. |
|
|
24
|
-
| `JBERC20` | Cloneable ERC-20 with Votes + Permit.
|
|
24
|
+
| `JBERC20` | Cloneable ERC-20 with Votes + Permit + ERC-1271. Controlled by `JBTokens` via `onlyTokens`. Deployed via `Clones.clone()`. |
|
|
25
25
|
| `JBFeelessAddresses` | Allowlist for fee-exempt addresses. |
|
|
26
26
|
| `JBChainlinkV3PriceFeed` | Chainlink AggregatorV3 price feed with staleness threshold. Rejects negative/zero prices, incomplete rounds (`updatedAt == 0`), and stale answers carried from previous rounds (`answeredInRound < roundId`). |
|
|
27
27
|
| `JBChainlinkV3SequencerPriceFeed` | L2 sequencer-aware Chainlink feed (Optimism/Arbitrum) with grace period after restart. Treats any non-zero sequencer answer as down (`answer != 0`). |
|
package/script/Deploy.s.sol
CHANGED
|
@@ -85,7 +85,8 @@ contract Deploy is Script, Sphinx {
|
|
|
85
85
|
trustedForwarder: TRUSTED_FORWARDER
|
|
86
86
|
});
|
|
87
87
|
JBTokens tokens = new JBTokens{salt: keccak256(abi.encode(CORE_DEPLOYMENT_NONCE))}({
|
|
88
|
-
directory: directory,
|
|
88
|
+
directory: directory,
|
|
89
|
+
token: new JBERC20{salt: keccak256(abi.encode(CORE_DEPLOYMENT_NONCE))}(permissions, projects)
|
|
89
90
|
});
|
|
90
91
|
|
|
91
92
|
new JBFundAccessLimits{salt: keccak256(abi.encode(CORE_DEPLOYMENT_NONCE))}(directory);
|
package/src/JBERC20.sol
CHANGED
|
@@ -1,27 +1,50 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
pragma solidity 0.8.28;
|
|
3
3
|
|
|
4
|
-
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
|
|
5
4
|
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
|
6
5
|
import {ERC20Permit, Nonces} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
|
|
7
6
|
import {ERC20Votes} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";
|
|
7
|
+
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
|
|
8
|
+
import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol";
|
|
8
9
|
import {Nonces} from "@openzeppelin/contracts/utils/Nonces.sol";
|
|
9
10
|
|
|
11
|
+
import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
|
|
12
|
+
import {JBPermissioned} from "./abstract/JBPermissioned.sol";
|
|
13
|
+
import {IJBPermissions} from "./interfaces/IJBPermissions.sol";
|
|
14
|
+
import {IJBProjects} from "./interfaces/IJBProjects.sol";
|
|
10
15
|
import {IJBToken} from "./interfaces/IJBToken.sol";
|
|
16
|
+
import {IJBTokens} from "./interfaces/IJBTokens.sol";
|
|
11
17
|
|
|
12
18
|
/// @notice An ERC-20 token that can be used by a project in `JBTokens` and `JBController`.
|
|
13
19
|
/// @dev By default, a project uses "credits" to track balances. Once a project sets their `IJBToken` using
|
|
14
20
|
/// `JBController.deployERC20For(...)` or `JBController.setTokenFor(...)`, credits can be redeemed to claim tokens.
|
|
15
21
|
/// @dev `JBController.deployERC20For(...)` deploys a `JBERC20` contract and sets it as the project's token.
|
|
16
|
-
contract JBERC20 is ERC20Votes, ERC20Permit,
|
|
22
|
+
contract JBERC20 is ERC20Votes, ERC20Permit, JBPermissioned, IERC1271, IJBToken {
|
|
17
23
|
//*********************************************************************//
|
|
18
24
|
// --------------------------- custom errors ------------------------- //
|
|
19
25
|
//*********************************************************************//
|
|
20
26
|
|
|
21
27
|
error JBERC20_AlreadyInitialized();
|
|
28
|
+
error JBERC20_Unauthorized();
|
|
22
29
|
|
|
23
30
|
//*********************************************************************//
|
|
24
|
-
//
|
|
31
|
+
// --------------- public immutable stored properties ---------------- //
|
|
32
|
+
//*********************************************************************//
|
|
33
|
+
|
|
34
|
+
/// @notice The projects contract used to resolve project ownership.
|
|
35
|
+
IJBProjects public immutable PROJECTS;
|
|
36
|
+
|
|
37
|
+
//*********************************************************************//
|
|
38
|
+
// --------------------- public stored properties -------------------- //
|
|
39
|
+
//*********************************************************************//
|
|
40
|
+
|
|
41
|
+
/// @notice The JBTokens contract that owns this token.
|
|
42
|
+
/// @dev Set via `initialize` because JBERC20 is deployed before JBTokens (circular dependency).
|
|
43
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
44
|
+
IJBTokens public TOKENS;
|
|
45
|
+
|
|
46
|
+
//*********************************************************************//
|
|
47
|
+
// -------------------- private stored properties -------------------- //
|
|
25
48
|
//*********************************************************************//
|
|
26
49
|
|
|
27
50
|
/// @notice The token's name.
|
|
@@ -38,8 +61,29 @@ contract JBERC20 is ERC20Votes, ERC20Permit, Ownable, IJBToken {
|
|
|
38
61
|
|
|
39
62
|
/// @dev Set `_name` on the implementation contract to prevent it from being initialized directly.
|
|
40
63
|
/// Clones start with empty `_name`, so `initialize(...)` works only on clones.
|
|
41
|
-
|
|
64
|
+
/// @param permissions The permissions contract.
|
|
65
|
+
/// @param projects The projects contract.
|
|
66
|
+
constructor(
|
|
67
|
+
IJBPermissions permissions,
|
|
68
|
+
IJBProjects projects
|
|
69
|
+
)
|
|
70
|
+
ERC20("invalid", "invalid")
|
|
71
|
+
ERC20Permit("JBToken")
|
|
72
|
+
JBPermissioned(permissions)
|
|
73
|
+
{
|
|
42
74
|
_name = "invalid";
|
|
75
|
+
PROJECTS = projects;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
//*********************************************************************//
|
|
79
|
+
// --------------------------- modifiers ---------------------------- //
|
|
80
|
+
//*********************************************************************//
|
|
81
|
+
|
|
82
|
+
/// @notice Only the JBTokens contract can call this function.
|
|
83
|
+
// forge-lint: disable-next-line(unwrapped-modifier-logic)
|
|
84
|
+
modifier onlyTokens() {
|
|
85
|
+
if (msg.sender != address(TOKENS)) revert JBERC20_Unauthorized();
|
|
86
|
+
_;
|
|
43
87
|
}
|
|
44
88
|
|
|
45
89
|
//*********************************************************************//
|
|
@@ -47,51 +91,32 @@ contract JBERC20 is ERC20Votes, ERC20Permit, Ownable, IJBToken {
|
|
|
47
91
|
//*********************************************************************//
|
|
48
92
|
|
|
49
93
|
/// @notice Burn some outstanding tokens.
|
|
50
|
-
/// @dev Can only be called by
|
|
94
|
+
/// @dev Can only be called by the JBTokens contract.
|
|
51
95
|
/// @param account The address to burn tokens from.
|
|
52
96
|
/// @param amount The amount of tokens to burn, as a fixed point number with 18 decimals.
|
|
53
|
-
function burn(address account, uint256 amount) external override
|
|
97
|
+
function burn(address account, uint256 amount) external override onlyTokens {
|
|
54
98
|
return _burn({account: account, value: amount});
|
|
55
99
|
}
|
|
56
100
|
|
|
57
101
|
/// @notice Mints more of this token.
|
|
58
|
-
/// @dev Can only be called by
|
|
102
|
+
/// @dev Can only be called by the JBTokens contract.
|
|
59
103
|
/// @param account The address to mint the new tokens to.
|
|
60
104
|
/// @param amount The amount of tokens to mint, as a fixed point number with 18 decimals.
|
|
61
|
-
function mint(address account, uint256 amount) external override
|
|
105
|
+
function mint(address account, uint256 amount) external override onlyTokens {
|
|
62
106
|
return _mint({account: account, value: amount});
|
|
63
107
|
}
|
|
64
108
|
|
|
65
109
|
/// @notice Sets the token's name and symbol.
|
|
66
|
-
/// @dev Can only be called by
|
|
110
|
+
/// @dev Can only be called by the JBTokens contract.
|
|
67
111
|
/// @param name_ The new name.
|
|
68
112
|
/// @param symbol_ The new symbol.
|
|
69
|
-
function setMetadata(string memory name_, string memory symbol_) external override
|
|
70
|
-
_name = name_;
|
|
71
|
-
_symbol = symbol_;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
//*********************************************************************//
|
|
75
|
-
// ----------------------- public transactions ----------------------- //
|
|
76
|
-
//*********************************************************************//
|
|
77
|
-
|
|
78
|
-
/// @notice Initializes the token.
|
|
79
|
-
/// @param name_ The token's name.
|
|
80
|
-
/// @param symbol_ The token's symbol.
|
|
81
|
-
/// @param owner The token contract's owner.
|
|
82
|
-
function initialize(string memory name_, string memory symbol_, address owner) public override {
|
|
83
|
-
// Prevent re-initialization by reverting if a name is already set or if the provided name is empty.
|
|
84
|
-
if (bytes(_name).length != 0 || bytes(name_).length == 0) revert JBERC20_AlreadyInitialized();
|
|
85
|
-
|
|
113
|
+
function setMetadata(string memory name_, string memory symbol_) external override onlyTokens {
|
|
86
114
|
_name = name_;
|
|
87
115
|
_symbol = symbol_;
|
|
88
|
-
|
|
89
|
-
// Transfer ownership to the owner.
|
|
90
|
-
_transferOwnership(owner);
|
|
91
116
|
}
|
|
92
117
|
|
|
93
118
|
//*********************************************************************//
|
|
94
|
-
//
|
|
119
|
+
// ----------------------- external views ---------------------------- //
|
|
95
120
|
//*********************************************************************//
|
|
96
121
|
|
|
97
122
|
/// @notice This token can only be added to a project when its created by the `JBTokens` contract.
|
|
@@ -99,6 +124,35 @@ contract JBERC20 is ERC20Votes, ERC20Permit, Ownable, IJBToken {
|
|
|
99
124
|
return false;
|
|
100
125
|
}
|
|
101
126
|
|
|
127
|
+
/// @notice Validates a signature on behalf of this token contract (ERC-1271).
|
|
128
|
+
/// @dev Allows the project owner or an operator with `SIGN_FOR_ERC20` permission to sign messages on behalf of
|
|
129
|
+
/// this token. Useful for Etherscan contract verification and other off-chain signature flows.
|
|
130
|
+
/// @param hash The hash of the data being signed.
|
|
131
|
+
/// @param signature The signature to validate.
|
|
132
|
+
/// @return magicValue `0x1626ba7e` if the signature is valid, `0xffffffff` otherwise.
|
|
133
|
+
function isValidSignature(bytes32 hash, bytes memory signature) external view override returns (bytes4 magicValue) {
|
|
134
|
+
// Recover the signer from the signature. Return invalid if recovery fails.
|
|
135
|
+
// slither-disable-next-line unused-return
|
|
136
|
+
(address signer, ECDSA.RecoverError error,) = ECDSA.tryRecover(hash, signature);
|
|
137
|
+
if (error != ECDSA.RecoverError.NoError) return 0xffffffff;
|
|
138
|
+
|
|
139
|
+
// Get the project ID this token belongs to.
|
|
140
|
+
uint256 projectId = TOKENS.projectIdOf(IJBToken(address(this)));
|
|
141
|
+
|
|
142
|
+
// Get the project owner (the NFT holder).
|
|
143
|
+
address projectOwner = PROJECTS.ownerOf(projectId);
|
|
144
|
+
|
|
145
|
+
// Valid if the signer is the project owner or has the SIGN_FOR_ERC20 permission.
|
|
146
|
+
if (_hasPermissionFrom({
|
|
147
|
+
operator: signer,
|
|
148
|
+
account: projectOwner,
|
|
149
|
+
projectId: projectId,
|
|
150
|
+
permissionId: JBPermissionIds.SIGN_FOR_ERC20
|
|
151
|
+
})) return IERC1271.isValidSignature.selector;
|
|
152
|
+
|
|
153
|
+
return 0xffffffff;
|
|
154
|
+
}
|
|
155
|
+
|
|
102
156
|
//*********************************************************************//
|
|
103
157
|
// -------------------------- public views --------------------------- //
|
|
104
158
|
//*********************************************************************//
|
|
@@ -137,6 +191,23 @@ contract JBERC20 is ERC20Votes, ERC20Permit, Ownable, IJBToken {
|
|
|
137
191
|
return super.totalSupply();
|
|
138
192
|
}
|
|
139
193
|
|
|
194
|
+
//*********************************************************************//
|
|
195
|
+
// ----------------------- public transactions ----------------------- //
|
|
196
|
+
//*********************************************************************//
|
|
197
|
+
|
|
198
|
+
/// @notice Initializes the token.
|
|
199
|
+
/// @param name_ The token's name.
|
|
200
|
+
/// @param symbol_ The token's symbol.
|
|
201
|
+
/// @param tokens The JBTokens contract that manages this token.
|
|
202
|
+
function initialize(string memory name_, string memory symbol_, address tokens) public override {
|
|
203
|
+
// Prevent re-initialization by reverting if a name is already set or if the provided name is empty.
|
|
204
|
+
if (bytes(_name).length != 0 || bytes(name_).length == 0) revert JBERC20_AlreadyInitialized();
|
|
205
|
+
|
|
206
|
+
_name = name_;
|
|
207
|
+
_symbol = symbol_;
|
|
208
|
+
TOKENS = IJBTokens(tokens);
|
|
209
|
+
}
|
|
210
|
+
|
|
140
211
|
//*********************************************************************//
|
|
141
212
|
// ---------------------- internal transactions ---------------------- //
|
|
142
213
|
//*********************************************************************//
|
package/src/JBTerminalStore.sol
CHANGED
|
@@ -599,6 +599,8 @@ contract JBTerminalStore is IJBTerminalStore {
|
|
|
599
599
|
JBRuleset memory ruleset = RULESETS.currentOf(projectId);
|
|
600
600
|
|
|
601
601
|
// Return the amount of surplus terminal tokens that would be reclaimed.
|
|
602
|
+
// NOTE: This view does not run the data hook, so it cannot reflect a cross-chain totalSupply override.
|
|
603
|
+
// For accurate omnichain estimates, use the data hook or simulate recordCashOutFor.
|
|
602
604
|
return JBCashOuts.cashOutFrom({
|
|
603
605
|
surplus: surplus,
|
|
604
606
|
cashOutCount: cashOutCount,
|
|
@@ -814,6 +816,8 @@ contract JBTerminalStore is IJBTerminalStore {
|
|
|
814
816
|
if (cashOutCount > totalSupply) return 0;
|
|
815
817
|
|
|
816
818
|
// Return the amount of surplus terminal tokens that would be reclaimed.
|
|
819
|
+
// NOTE: This view does not run the data hook, so it cannot reflect a cross-chain totalSupply override.
|
|
820
|
+
// For accurate omnichain estimates, use the data hook or simulate recordCashOutFor.
|
|
817
821
|
return JBCashOuts.cashOutFrom({
|
|
818
822
|
surplus: currentSurplus,
|
|
819
823
|
cashOutCount: cashOutCount,
|
|
@@ -877,6 +881,54 @@ contract JBTerminalStore is IJBTerminalStore {
|
|
|
877
881
|
});
|
|
878
882
|
}
|
|
879
883
|
|
|
884
|
+
/// @notice Calls the data hook, validates noop specifications, and computes the bonding curve reclaim amount.
|
|
885
|
+
/// @dev Extracted from `_computeCashOutFrom` to keep it under the EVM stack depth limit (16 slots).
|
|
886
|
+
/// @param ruleset The current ruleset (used to resolve the data hook address).
|
|
887
|
+
/// @param context The fully-populated cash out context to forward to the data hook.
|
|
888
|
+
/// @param surplus The locally available surplus (used as a cap — can't reclaim more than what's on-chain here).
|
|
889
|
+
/// @return reclaimAmount The amount of surplus tokens reclaimable after the bonding curve and cap.
|
|
890
|
+
/// @return cashOutTaxRate The cash out tax rate returned by the data hook.
|
|
891
|
+
/// @return hookSpecifications The hook specifications returned by the data hook.
|
|
892
|
+
function _cashOutWithDataHook(
|
|
893
|
+
JBRuleset memory ruleset,
|
|
894
|
+
JBBeforeCashOutRecordedContext memory context,
|
|
895
|
+
uint256 surplus
|
|
896
|
+
)
|
|
897
|
+
internal
|
|
898
|
+
view
|
|
899
|
+
returns (uint256 reclaimAmount, uint256 cashOutTaxRate, JBCashOutHookSpecification[] memory hookSpecifications)
|
|
900
|
+
{
|
|
901
|
+
// Ask the data hook for the effective bonding curve parameters and any hook specifications.
|
|
902
|
+
uint256 effectiveCashOutCount;
|
|
903
|
+
uint256 effectiveTotalSupply;
|
|
904
|
+
uint256 effectiveSurplusValue;
|
|
905
|
+
(cashOutTaxRate, effectiveCashOutCount, effectiveTotalSupply, effectiveSurplusValue, hookSpecifications) =
|
|
906
|
+
IJBRulesetDataHook(ruleset.dataHook()).beforeCashOutRecordedWith(context);
|
|
907
|
+
|
|
908
|
+
// Noop specifications are informational only, so they can't also request forwarded funds.
|
|
909
|
+
for (uint256 i; i < hookSpecifications.length;) {
|
|
910
|
+
if (hookSpecifications[i].noop && hookSpecifications[i].amount != 0) {
|
|
911
|
+
revert JBTerminalStore_NoopHookSpecHasAmount(hookSpecifications[i].amount);
|
|
912
|
+
}
|
|
913
|
+
unchecked {
|
|
914
|
+
++i;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// Apply the bonding curve to calculate how much of the surplus is reclaimable.
|
|
919
|
+
if (surplus != 0) {
|
|
920
|
+
reclaimAmount = JBCashOuts.cashOutFrom({
|
|
921
|
+
surplus: effectiveSurplusValue,
|
|
922
|
+
cashOutCount: effectiveCashOutCount,
|
|
923
|
+
totalSupply: effectiveTotalSupply,
|
|
924
|
+
cashOutTaxRate: cashOutTaxRate
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
// Cap at local surplus — can't reclaim more than what's locally available.
|
|
928
|
+
if (reclaimAmount > surplus) reclaimAmount = surplus;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
880
932
|
/// @notice Computes cash out results without writing state.
|
|
881
933
|
/// @param terminal The terminal recording the cash out.
|
|
882
934
|
/// @param holder The account that is cashing out tokens.
|
|
@@ -934,8 +986,6 @@ contract JBTerminalStore is IJBTerminalStore {
|
|
|
934
986
|
// The terminal still burns the caller-supplied cashOutCount after pricing completes.
|
|
935
987
|
// Project owners MUST audit their data hooks with the same rigor as the terminal.
|
|
936
988
|
|
|
937
|
-
uint256 effectiveCashOutCount = cashOutCount;
|
|
938
|
-
|
|
939
989
|
// If the ruleset has a data hook which is enabled for cash outs, use it to derive a claim amount and memo.
|
|
940
990
|
if (ruleset.useDataHookForCashOut() && ruleset.dataHook() != address(0)) {
|
|
941
991
|
// Build the cash out context field-by-field — the struct has 11 fields, too many for a literal.
|
|
@@ -957,30 +1007,21 @@ contract JBTerminalStore is IJBTerminalStore {
|
|
|
957
1007
|
context.beneficiaryIsFeeless = beneficiaryIsFeeless;
|
|
958
1008
|
context.metadata = metadata;
|
|
959
1009
|
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
// Noop specifications are informational only, so they can't also request forwarded funds.
|
|
964
|
-
for (uint256 i; i < hookSpecifications.length;) {
|
|
965
|
-
if (hookSpecifications[i].noop && hookSpecifications[i].amount != 0) {
|
|
966
|
-
revert JBTerminalStore_NoopHookSpecHasAmount(hookSpecifications[i].amount);
|
|
967
|
-
}
|
|
968
|
-
unchecked {
|
|
969
|
-
++i;
|
|
970
|
-
}
|
|
971
|
-
}
|
|
1010
|
+
// Hook call + bonding curve in a helper to stay under the stack depth limit.
|
|
1011
|
+
(reclaimAmount, cashOutTaxRate, hookSpecifications) =
|
|
1012
|
+
_cashOutWithDataHook({ruleset: ruleset, context: context, surplus: surplus});
|
|
972
1013
|
} else {
|
|
973
1014
|
cashOutTaxRate = ruleset.cashOutTaxRate();
|
|
974
|
-
}
|
|
975
1015
|
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
1016
|
+
// Apply the bonding curve to calculate how much of the surplus is reclaimable.
|
|
1017
|
+
if (surplus != 0) {
|
|
1018
|
+
reclaimAmount = JBCashOuts.cashOutFrom({
|
|
1019
|
+
surplus: surplus,
|
|
1020
|
+
cashOutCount: cashOutCount,
|
|
1021
|
+
totalSupply: effectiveTotalSupply,
|
|
1022
|
+
cashOutTaxRate: cashOutTaxRate
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
984
1025
|
}
|
|
985
1026
|
}
|
|
986
1027
|
|
package/src/JBTokens.sol
CHANGED
|
@@ -228,7 +228,7 @@ contract JBTokens is JBControlled, IJBTokens {
|
|
|
228
228
|
});
|
|
229
229
|
|
|
230
230
|
// Initialize the token.
|
|
231
|
-
token.initialize({name: name, symbol: symbol,
|
|
231
|
+
token.initialize({name: name, symbol: symbol, tokens: address(this)});
|
|
232
232
|
}
|
|
233
233
|
|
|
234
234
|
/// @notice Mint (create) new tokens or credits.
|
|
@@ -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
|
|
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
|
|
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
|
|
33
|
-
function initialize(string memory name, string memory symbol, address
|
|
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.
|
|
@@ -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(),
|
|
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(
|
|
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(
|
|
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
|
package/test/TestTokenFlow.sol
CHANGED
|
@@ -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",
|
|
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
|
-
|
|
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)));
|
|
@@ -4,7 +4,6 @@ pragma solidity 0.8.28;
|
|
|
4
4
|
import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
|
|
5
5
|
import {JBERC20} from "../../../../src/JBERC20.sol";
|
|
6
6
|
import {JBERC20Setup} from "./JBERC20Setup.sol";
|
|
7
|
-
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
|
|
8
7
|
|
|
9
8
|
contract TestInitialize_Local is JBERC20Setup {
|
|
10
9
|
string _name = "Nana";
|
|
@@ -17,21 +16,21 @@ contract TestInitialize_Local is JBERC20Setup {
|
|
|
17
16
|
function test_ImplementationCannotBeInitialized() external {
|
|
18
17
|
// The implementation has _name = "invalid" set in constructor, so initialize() must revert.
|
|
19
18
|
vm.expectRevert(JBERC20.JBERC20_AlreadyInitialized.selector);
|
|
20
|
-
_implementation.initialize(_name, _symbol,
|
|
19
|
+
_implementation.initialize(_name, _symbol, _tokens);
|
|
21
20
|
}
|
|
22
21
|
|
|
23
22
|
function test_WhenANameIsAlreadySet() external {
|
|
24
23
|
// it will revert
|
|
25
24
|
|
|
26
|
-
_erc20.initialize(_name, _symbol,
|
|
25
|
+
_erc20.initialize(_name, _symbol, _tokens);
|
|
27
26
|
|
|
28
|
-
// ensure
|
|
29
|
-
address
|
|
30
|
-
assertEq(
|
|
27
|
+
// ensure TOKENS is set
|
|
28
|
+
address setTokens = address(JBERC20(address(_erc20)).TOKENS());
|
|
29
|
+
assertEq(setTokens, _tokens);
|
|
31
30
|
|
|
32
31
|
// will fail as internal name is no longer zero length
|
|
33
32
|
vm.expectRevert();
|
|
34
|
-
_erc20.initialize(_name, _symbol,
|
|
33
|
+
_erc20.initialize(_name, _symbol, _tokens);
|
|
35
34
|
}
|
|
36
35
|
|
|
37
36
|
function test_WhenName_EQNothing() external {
|
|
@@ -39,17 +38,17 @@ contract TestInitialize_Local is JBERC20Setup {
|
|
|
39
38
|
|
|
40
39
|
// will fail as internal name is no longer than zero length
|
|
41
40
|
vm.expectRevert();
|
|
42
|
-
_erc20.initialize("", _symbol,
|
|
41
|
+
_erc20.initialize("", _symbol, _tokens);
|
|
43
42
|
}
|
|
44
43
|
|
|
45
44
|
function test_WhenNameIsValidAndNotAlreadySet() external {
|
|
46
|
-
// it will set the name
|
|
45
|
+
// it will set the name, symbol, and store references
|
|
47
46
|
|
|
48
|
-
_erc20.initialize(_name, _symbol,
|
|
47
|
+
_erc20.initialize(_name, _symbol, _tokens);
|
|
49
48
|
|
|
50
|
-
// ensure
|
|
51
|
-
address
|
|
52
|
-
assertEq(
|
|
49
|
+
// ensure TOKENS is set
|
|
50
|
+
address setTokens = address(JBERC20(address(_erc20)).TOKENS());
|
|
51
|
+
assertEq(setTokens, _tokens);
|
|
53
52
|
|
|
54
53
|
// name is set
|
|
55
54
|
string memory _setName = IERC20Metadata(address(_erc20)).name();
|
|
@@ -15,7 +15,7 @@ contract TestName_Local is JBERC20Setup {
|
|
|
15
15
|
|
|
16
16
|
function test_WhenANameIsSet() external {
|
|
17
17
|
// it will return the name
|
|
18
|
-
_erc20.initialize("NANAPUS", "NANA",
|
|
18
|
+
_erc20.initialize("NANAPUS", "NANA", _tokens);
|
|
19
19
|
|
|
20
20
|
string memory _setName = _token.name();
|
|
21
21
|
assertEq(_setName, "NANAPUS");
|
|
@@ -6,6 +6,7 @@ import {JBERC20Setup} from "./JBERC20Setup.sol";
|
|
|
6
6
|
import {SigUtils} from "./SigUtils.sol";
|
|
7
7
|
|
|
8
8
|
contract TestNonces_Local is JBERC20Setup {
|
|
9
|
+
address _user = makeAddr("user");
|
|
9
10
|
IERC20Permit _token;
|
|
10
11
|
SigUtils sigUtils;
|
|
11
12
|
|
|
@@ -30,7 +31,7 @@ contract TestNonces_Local is JBERC20Setup {
|
|
|
30
31
|
function test_WhenAUserHasNotCalledPermit() external view {
|
|
31
32
|
// it will return zero
|
|
32
33
|
|
|
33
|
-
uint256 _nonce = _token.nonces(
|
|
34
|
+
uint256 _nonce = _token.nonces(_user);
|
|
34
35
|
|
|
35
36
|
assertEq(_nonce, 0);
|
|
36
37
|
}
|
|
@@ -16,7 +16,7 @@ contract TestSymbol_Local is JBERC20Setup {
|
|
|
16
16
|
function test_WhenASymbolIsSet() external {
|
|
17
17
|
// it will return a non-empty string
|
|
18
18
|
|
|
19
|
-
_erc20.initialize("NANAPUS", "NANA",
|
|
19
|
+
_erc20.initialize("NANAPUS", "NANA", _tokens);
|
|
20
20
|
|
|
21
21
|
string memory _setSymbol = _token.symbol();
|
|
22
22
|
assertEq(_setSymbol, "NANA");
|
|
@@ -453,7 +453,7 @@ contract TestPreviewCashOutFor_Local is JBTerminalStoreSetup {
|
|
|
453
453
|
mockExpect(
|
|
454
454
|
address(_dataHook),
|
|
455
455
|
abi.encodeCall(IJBRulesetDataHook.beforeCashOutRecordedWith, (_context)),
|
|
456
|
-
abi.encode(0, 10e18, _totalSupply, _spec)
|
|
456
|
+
abi.encode(0, 10e18, _totalSupply, 3e18, _spec)
|
|
457
457
|
);
|
|
458
458
|
|
|
459
459
|
(, uint256 reclaimAmount, uint256 cashOutTaxRate, JBCashOutHookSpecification[] memory hookSpecifications) = _store.previewCashOutFrom({
|
|
@@ -417,7 +417,7 @@ contract TestRecordCashOutsFor_Local is JBTerminalStoreSetup {
|
|
|
417
417
|
mockExpect(
|
|
418
418
|
address(_dataHook),
|
|
419
419
|
abi.encodeCall(IJBRulesetDataHook.beforeCashOutRecordedWith, (_context)),
|
|
420
|
-
abi.encode(0, 1e18, _totalSupply, _spec)
|
|
420
|
+
abi.encode(0, 1e18, _totalSupply, 3e18, _spec)
|
|
421
421
|
);
|
|
422
422
|
|
|
423
423
|
uint256 balanceBefore = _store.balanceOf(address(this), _projectId, _accountingContexts.token);
|
|
@@ -521,7 +521,7 @@ contract TestRecordCashOutsFor_Local is JBTerminalStoreSetup {
|
|
|
521
521
|
mockExpect(
|
|
522
522
|
address(_dataHook),
|
|
523
523
|
abi.encodeCall(IJBRulesetDataHook.beforeCashOutRecordedWith, (_context)),
|
|
524
|
-
abi.encode(0, 1e18, _totalSupply, _spec)
|
|
524
|
+
abi.encode(0, 1e18, _totalSupply, 3e18, _spec)
|
|
525
525
|
);
|
|
526
526
|
|
|
527
527
|
(, uint256 reclaimed,,) = _store.recordCashOutFor({
|
|
@@ -576,7 +576,7 @@ contract TestRecordCashOutsFor_Local is JBTerminalStoreSetup {
|
|
|
576
576
|
mockExpect(
|
|
577
577
|
address(_dataHook),
|
|
578
578
|
abi.encodeCall(IJBRulesetDataHook.beforeCashOutRecordedWith, (_context)),
|
|
579
|
-
abi.encode(0, 1e18, _totalSupply, _spec)
|
|
579
|
+
abi.encode(0, 1e18, _totalSupply, 3e18, _spec)
|
|
580
580
|
);
|
|
581
581
|
|
|
582
582
|
vm.expectRevert(abi.encodeWithSelector(JBTerminalStore.JBTerminalStore_NoopHookSpecHasAmount.selector, 1));
|
|
@@ -631,7 +631,7 @@ contract TestRecordCashOutsFor_Local is JBTerminalStoreSetup {
|
|
|
631
631
|
mockExpect(
|
|
632
632
|
address(_dataHook),
|
|
633
633
|
abi.encodeCall(IJBRulesetDataHook.beforeCashOutRecordedWith, (_context)),
|
|
634
|
-
abi.encode(0, 1e18, _totalSupply, _spec)
|
|
634
|
+
abi.encode(0, 1e18, _totalSupply, 3e18, _spec)
|
|
635
635
|
);
|
|
636
636
|
|
|
637
637
|
vm.expectRevert(abi.encodeWithSelector(JBTerminalStore.JBTerminalStore_NoopHookSpecHasAmount.selector, 1));
|
|
@@ -4,6 +4,8 @@ pragma solidity 0.8.28;
|
|
|
4
4
|
import {JBERC20} from "../../../../src/JBERC20.sol";
|
|
5
5
|
import {JBTokens} from "../../../../src/JBTokens.sol";
|
|
6
6
|
import {IJBDirectory} from "../../../../src/interfaces/IJBDirectory.sol";
|
|
7
|
+
import {IJBPermissions} from "../../../../src/interfaces/IJBPermissions.sol";
|
|
8
|
+
import {IJBProjects} from "../../../../src/interfaces/IJBProjects.sol";
|
|
7
9
|
import {IJBToken} from "../../../../src/interfaces/IJBToken.sol";
|
|
8
10
|
import {IJBTokens} from "../../../../src/interfaces/IJBTokens.sol";
|
|
9
11
|
import {JBTest} from "../../../helpers/JBTest.sol";
|
|
@@ -15,6 +17,8 @@ Tests relative to this contract will be dependent on mock calls/emits and stdSto
|
|
|
15
17
|
contract JBTokensSetup is JBTest {
|
|
16
18
|
// Mocks
|
|
17
19
|
IJBDirectory public directory = IJBDirectory(makeAddr("directory"));
|
|
20
|
+
IJBPermissions public permissions = IJBPermissions(makeAddr("permissions"));
|
|
21
|
+
IJBProjects public projects = IJBProjects(makeAddr("projects"));
|
|
18
22
|
IJBToken public jbToken;
|
|
19
23
|
|
|
20
24
|
// Target Contract
|
|
@@ -22,7 +26,7 @@ contract JBTokensSetup is JBTest {
|
|
|
22
26
|
|
|
23
27
|
function tokensSetup() public virtual {
|
|
24
28
|
// Instantiate the contract being tested
|
|
25
|
-
jbToken = new JBERC20();
|
|
29
|
+
jbToken = new JBERC20(permissions, projects);
|
|
26
30
|
_tokens = new JBTokens(directory, jbToken);
|
|
27
31
|
}
|
|
28
32
|
}
|