@bananapus/core-v6 0.0.83 → 0.0.85

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/README.md CHANGED
@@ -72,6 +72,7 @@ The shortest reading path is:
72
72
  | `JBTerminalStore` | Shared accounting for balances, surplus, fees, and reclaim math. |
73
73
  | `JBDirectory` | Registry for controller and terminal routing. |
74
74
  | `JBProjects` | ERC-721 project registry and ownership surface. |
75
+ | `JBERC20` | Cloneable project-token ERC-20 with Votes, Permit, ERC-1271, and active-vote total checkpoints. |
75
76
  | `JBPermissions` | Packed operator-permission registry. |
76
77
  | `JBPrices` | Price-feed routing used by terminals and integrations. |
77
78
 
@@ -81,6 +82,8 @@ The shortest reading path is:
81
82
  - Data hooks and cash-out hooks can change economics and side effects. They are part of the protocol surface.
82
83
  - Permission checks are not always against the project owner. Some flows are scoped to the token holder instead.
83
84
  - Preview and execution are intentionally close, but callers should still treat them as separate surfaces when hooks or routing can change behavior.
85
+ - `JBERC20.getPastTotalSupply(...)` includes undelegated balances. Use `getPastTotalActiveVotes(...)` when an
86
+ integration must split value only among addresses with checkpointed voting power.
84
87
 
85
88
  ## Where state lives
86
89
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bananapus/core-v6",
3
- "version": "0.0.83",
3
+ "version": "0.0.85",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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` | Append-only price feed registry with project-specific feeds, protocol defaults, inverse lookup, and backup feeds. |
24
- | `JBERC20` | Cloneable ERC-20 with Votes + Permit + ERC-1271. Controlled by `JBTokens` via `onlyTokens`. Deployed via `Clones.clone()`. |
24
+ | `JBERC20` | Cloneable ERC-20 with Votes + Permit + ERC-1271 and active-vote total checkpoints. Controlled by `JBTokens` via `onlyTokens`. Deployed via `Clones.clone()`. |
25
25
  | `JBFeelessAddresses` | Static and hook-driven fee-exemption registry. |
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`). |
@@ -148,6 +148,13 @@ The core Juicebox V6 protocol on EVM: a modular system for launching treasury-ba
148
148
  | `totalCreditSupplyOf(uint256 projectId)` | Returns the internal credit supply for a project. |
149
149
  | `setTokenMetadataFor(uint256 projectId, string name, string symbol)` | Sets the name and symbol of a project's token. Controller-only. |
150
150
 
151
+ ### JBERC20
152
+
153
+ | Function | What it does |
154
+ |----------|--------------|
155
+ | `getPastTotalActiveVotes(uint256 timepoint)` | Returns the total voting units delegated to nonzero delegates at a past block. Unlike `getPastTotalSupply`, this excludes undelegated balances such as AMM-held tokens with no delegate. |
156
+ | `getTotalActiveVotes()` | Returns the current total voting units delegated to nonzero delegates. |
157
+
151
158
  ### JBSplits
152
159
 
153
160
  | Function | What it does |
@@ -87,6 +87,8 @@ Use this file when you need deeper protocol reference material after the repo-lo
87
87
  - Credits are burned before ERC-20 tokens in `JBTokens.burnFrom()`
88
88
  - `JBRuleset.weight` is `uint112` with 18 decimals; `JBRuleset.metadata` is packed -- use `JBRulesetMetadataResolver` to unpack
89
89
  - `JBERC20` is cloned via `Clones.clone()` -- its constructor sets invalid name/symbol; real values set in `initialize()`
90
+ - `JBERC20.getPastTotalSupply()` includes undelegated balances; `getPastTotalActiveVotes()` only counts balances whose
91
+ owner had a nonzero delegate at the queried block.
90
92
  - Fee is 2.5% (`FEE = 25` out of `MAX_FEE = 1000`)
91
93
  - Project #1 is the fee beneficiary project (receives all protocol fees)
92
94
  - **Fee-free cashout exemption is scoped to fee-free intra-terminal payout amounts.** `feeFreeSurplusOf[projectId][token]` accumulates the value of fee-free payouts. After any outflow (payouts, `useAllowanceOf`, non-zero-tax or feeless cashouts), the counter is capped at the remaining balance — non-fee-free funds leave first, preserving the fee-free counter. During cashout with `cashOutTaxRate=0`, the 2.5% fee applies only up to this surplus, then depletes. Once consumed, subsequent cashouts are fee-free again. Cleared on terminal migration. This prevents a round-trip fee bypass (intra-terminal payout → zero-tax cashout) while scoping fees precisely to the fee-free inflow.
package/src/JBERC20.sol CHANGED
@@ -1,15 +1,18 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  pragma solidity 0.8.28;
3
3
 
4
+ import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol";
4
5
  import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
5
6
  import {ERC20Permit, Nonces} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
6
7
  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";
9
8
  import {Nonces} from "@openzeppelin/contracts/utils/Nonces.sol";
9
+ import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
10
+ import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol";
11
+ import {Checkpoints} from "@openzeppelin/contracts/utils/structs/Checkpoints.sol";
10
12
 
11
13
  import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
12
14
  import {JBPermissioned} from "./abstract/JBPermissioned.sol";
15
+ import {IJBActiveVotes} from "./interfaces/IJBActiveVotes.sol";
13
16
  import {IJBPermissions} from "./interfaces/IJBPermissions.sol";
14
17
  import {IJBProjects} from "./interfaces/IJBProjects.sol";
15
18
  import {IJBToken} from "./interfaces/IJBToken.sol";
@@ -20,7 +23,9 @@ import {IJBTokens} from "./interfaces/IJBTokens.sol";
20
23
  /// holders can claim their internal credits into this transferable token.
21
24
  /// @dev Only `JBTokens` can mint and burn. The project owner (via `SET_TOKEN_METADATA` permission) can rename the
22
25
  /// token. Supports ERC-1271 signature validation for smart-contract wallets.
23
- contract JBERC20 is ERC20Votes, ERC20Permit, JBPermissioned, IERC1271, IJBToken {
26
+ contract JBERC20 is ERC20Votes, ERC20Permit, JBPermissioned, IERC1271, IJBActiveVotes, IJBToken {
27
+ using Checkpoints for Checkpoints.Trace208;
28
+
24
29
  //*********************************************************************//
25
30
  // --------------------------- custom errors ------------------------- //
26
31
  //*********************************************************************//
@@ -50,6 +55,9 @@ contract JBERC20 is ERC20Votes, ERC20Permit, JBPermissioned, IERC1271, IJBToken
50
55
  // -------------------- private stored properties -------------------- //
51
56
  //*********************************************************************//
52
57
 
58
+ /// @notice The total voting units currently delegated to nonzero delegates.
59
+ Checkpoints.Trace208 private _activeSupplyCheckpoints;
60
+
53
61
  /// @notice The token's name.
54
62
  string private _name;
55
63
 
@@ -170,6 +178,19 @@ contract JBERC20 is ERC20Votes, ERC20Permit, JBPermissioned, IERC1271, IJBToken
170
178
  return super.decimals();
171
179
  }
172
180
 
181
+ /// @notice The total delegated voting units at a past block.
182
+ /// @param timepoint The past block number to look up.
183
+ /// @return activeVotes The total voting units delegated to nonzero delegates at `timepoint`.
184
+ function getPastTotalActiveVotes(uint256 timepoint) public view override returns (uint256 activeVotes) {
185
+ activeVotes = _activeSupplyCheckpoints.upperLookupRecent(_validateTimepoint(timepoint));
186
+ }
187
+
188
+ /// @notice The current total delegated voting units.
189
+ /// @return activeVotes The current total voting units delegated to nonzero delegates.
190
+ function getTotalActiveVotes() public view override returns (uint256 activeVotes) {
191
+ activeVotes = _activeSupplyCheckpoints.latest();
192
+ }
193
+
173
194
  /// @notice The token's name, set during initialization.
174
195
  function name() public view virtual override returns (string memory) {
175
196
  return _name;
@@ -216,8 +237,75 @@ contract JBERC20 is ERC20Votes, ERC20Permit, JBPermissioned, IERC1271, IJBToken
216
237
  // ---------------------- internal transactions ---------------------- //
217
238
  //*********************************************************************//
218
239
 
240
+ /// @notice Track active-vote-total changes when an account changes its delegate.
241
+ /// @dev Delegating to a nonzero address makes all of `account`'s voting units active. Clearing delegation removes
242
+ /// all of `account`'s voting units from the active total. Redelegating between two nonzero delegates only moves
243
+ /// votes inside OZ `Votes`, so the active total does not change.
244
+ /// @param account The account whose delegation is changing.
245
+ /// @param delegatee The new delegate. Use `address(0)` to clear delegation.
246
+ function _delegate(address account, address delegatee) internal virtual override {
247
+ // Read the current delegate before OZ mutates the delegate mapping.
248
+ address oldDelegate = delegates(account);
249
+
250
+ // Read the account's current voting units so any active-total delta matches the units OZ moves.
251
+ uint256 votingUnits = _getVotingUnits(account);
252
+
253
+ // Let OZ update the delegate mapping and the per-delegate vote checkpoints.
254
+ super._delegate({account: account, delegatee: delegatee});
255
+
256
+ // If the account had no delegate and now has one, its voting units just became active.
257
+ if (oldDelegate == address(0) && delegatee != address(0)) {
258
+ // Add the account's voting units to the checkpointed active total.
259
+ _updateActiveVotes({amount: votingUnits, increase: true});
260
+ } else if (oldDelegate != address(0) && delegatee == address(0)) {
261
+ // If the account had a delegate and now has none, its voting units just became inactive.
262
+ _updateActiveVotes({amount: votingUnits, increase: false});
263
+ }
264
+ }
265
+
266
+ /// @notice Track active-vote-total changes when voting units move between accounts.
267
+ /// @dev Moving voting units between two accounts with the same delegation status does not change the active total.
268
+ /// Moving voting units out of a delegated account and into an undelegated account decreases the active total, while
269
+ /// moving voting units out of an undelegated account and into a delegated account increases it.
270
+ /// @param from The account whose voting units are leaving. `address(0)` means the units are being minted.
271
+ /// @param to The account whose voting units are arriving. `address(0)` means the units are being burned.
272
+ /// @param amount The voting units moving between `from` and `to`.
273
+ function _transferVotingUnits(address from, address to, uint256 amount) internal virtual override {
274
+ // The active total decreases if units leave an account that already has a nonzero delegate.
275
+ bool decreaseActiveVotes = from != address(0) && delegates(from) != address(0);
276
+
277
+ // The active total increases if units arrive at an account that already has a nonzero delegate.
278
+ bool increaseActiveVotes = to != address(0) && delegates(to) != address(0);
279
+
280
+ // Let OZ update total voting-unit supply and per-delegate checkpoints first.
281
+ super._transferVotingUnits({from: from, to: to, amount: amount});
282
+
283
+ // If both sides are delegated or both sides are undelegated, the active total is unchanged.
284
+ if (decreaseActiveVotes == increaseActiveVotes) return;
285
+
286
+ // Otherwise apply the one-sided active-total delta implied by the receiver's delegated status.
287
+ _updateActiveVotes({amount: amount, increase: increaseActiveVotes});
288
+ }
289
+
219
290
  /// @notice Required override.
220
291
  function _update(address from, address to, uint256 value) internal virtual override(ERC20, ERC20Votes) {
221
292
  super._update({from: from, to: to, value: value});
222
293
  }
294
+
295
+ /// @notice Update the checkpointed total of delegated voting units.
296
+ /// @dev Writes at most one active-total checkpoint at the current OZ clock. A zero amount is ignored so zero-value
297
+ /// delegation or transfer hooks do not create empty checkpoints.
298
+ /// @param amount The amount of voting units to add or remove.
299
+ /// @param increase Whether to add `amount`; if false, `amount` is removed.
300
+ function _updateActiveVotes(uint256 amount, bool increase) internal {
301
+ // Ignore zero-unit updates because they do not change the active total.
302
+ if (amount == 0) return;
303
+
304
+ // Calculate the next active total by adding or subtracting from the latest checkpointed value.
305
+ uint256 updated =
306
+ increase ? _activeSupplyCheckpoints.latest() + amount : _activeSupplyCheckpoints.latest() - amount;
307
+
308
+ // Write the new active total at the current ERC-6372 clock using the same uint208 width as OZ `Votes`.
309
+ _activeSupplyCheckpoints.push({key: clock(), value: SafeCast.toUint208(updated)});
310
+ }
223
311
  }
@@ -0,0 +1,14 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.0;
3
+
4
+ /// @notice Provides checkpointed totals for voting units delegated to nonzero delegates.
5
+ interface IJBActiveVotes {
6
+ /// @notice The total delegated voting units at a past timepoint.
7
+ /// @param timepoint The past block number to look up.
8
+ /// @return activeVotes The total voting units delegated to nonzero delegates at `timepoint`.
9
+ function getPastTotalActiveVotes(uint256 timepoint) external view returns (uint256 activeVotes);
10
+
11
+ /// @notice The current total delegated voting units.
12
+ /// @return activeVotes The current total voting units delegated to nonzero delegates.
13
+ function getTotalActiveVotes() external view returns (uint256 activeVotes);
14
+ }