@rev-net/core-v6 0.0.32 → 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/USER_JOURNEYS.md CHANGED
@@ -1,110 +1,195 @@
1
1
  # User Journeys
2
2
 
3
- ## Who This Repo Serves
3
+ ## Repo Purpose
4
+
5
+ This repo packages autonomous Revnets: staged Juicebox projects whose runtime behavior is intentionally constrained after launch. It owns deploy-time stage encoding, runtime enforcement, hidden-token mechanics, and lending against revnet token exposure.
6
+
7
+ ## Primary Actors
4
8
 
5
9
  - teams launching autonomous Revnets with encoded stage transitions
6
10
  - participants buying, holding, and cashing out Revnet exposure over time
7
11
  - borrowers using Revnet tokens as collateral instead of selling them
8
12
  - operators working inside the narrow post-launch envelope the deployer allows
9
13
 
14
+ ## Key Surfaces
15
+
16
+ - `REVDeployer`: launch-time packaging, stage config, and operator envelope
17
+ - `REVOwner`: runtime pay and cash-out behavior for active Revnets
18
+ - `REVLoans`: borrowing, repayment, transfer, reallocation, and liquidation
19
+ - `REVHiddenTokens`: temporarily hide and later reveal token supply
20
+ - `autoIssueFor(...)`, `hideTokensOf(...)`, `revealTokensOf(...)`, `borrowFrom(...)`: high-signal runtime entrypoints
21
+
10
22
  ## Journey 1: Launch A Revnet With Its Long-Lived Rules Encoded Up Front
11
23
 
12
- **Starting state:** you know the stage schedule, issuance behavior, optional integrations, and runtime controls the Revnet should allow.
24
+ **Actor:** launch team.
25
+
26
+ **Intent:** deploy a Revnet whose economic envelope is encoded up front and stays bounded afterward.
27
+
28
+ **Preconditions**
29
+
30
+ - the team knows the stage schedule, issuance behavior, operator envelope, and optional integrations
31
+ - the team accepts that many choices become expensive or impossible to change later
32
+
33
+ **Main Flow**
34
+
35
+ 1. Use `REVDeployer` with the staged config, split operators, and optional integrations.
36
+ 2. The deployer launches the underlying project and preserves the intended ownership model.
37
+ 3. Stage and auxiliary behavior are committed at launch instead of left to ordinary owner discretion.
38
+ 4. The Revnet can now accept payments and progress through stages under the encoded rules.
39
+
40
+ **Failure Modes**
41
+
42
+ - teams assume deploy-time parameters can be revisited casually
43
+ - optional integrations are enabled without auditing their effect on the resulting network
44
+
45
+ **Postconditions**
46
+
47
+ - the Revnet launches with its long-lived stage envelope encoded up front
48
+
49
+ ## Journey 2: Participate Across Stage Transitions
50
+
51
+ **Actor:** participant.
52
+
53
+ **Intent:** buy, hold, and exit Revnet exposure across stage changes.
54
+
55
+ **Preconditions**
56
+
57
+ - the Revnet is already live
58
+ - the participant understands active stage parameters can change behavior over time
59
+
60
+ **Main Flow**
13
61
 
14
- **Success:** the Revnet launches as a Juicebox project whose deploy-time shape already encodes its economic envelope.
62
+ 1. Pay through the configured terminal or router.
63
+ 2. Let `REVOwner` enforce runtime behavior such as pay handling, delayed exits, and stage-sensitive constraints.
64
+ 3. As stages advance, later pays and exits follow the new active parameters while project identity stays constant.
15
65
 
16
- **Flow**
17
- 1. Use `REVDeployer` with the staged config, split operators, and optional integrations such as 721 hooks, buyback routing, router terminal support, or suckers.
18
- 2. The deployer launches the underlying project and keeps the ownership model aligned with the Revnet runtime contracts.
19
- 3. Stage config, issuance behavior, and auxiliary surfaces are committed as part of the launch instead of being left to human operators later.
20
- 4. The network can now accept payments and transition across stages without ordinary project-owner governance.
66
+ **Failure Modes**
21
67
 
22
- ## Journey 2: Participate In The Revnet Across Stage Transitions
68
+ - stage parameters are misread as mutable when they are fixed
69
+ - delayed cash-out behavior is misunderstood
70
+ - optional integrations materially change the participant experience
23
71
 
24
- **Starting state:** the Revnet is live and a participant wants to pay in, hold exposure, or cash out later.
72
+ **Postconditions**
25
73
 
26
- **Success:** participation follows the current stage's rules without requiring the user to reason about every deploy-time parameter directly.
74
+ - the participant's buys and exits follow the active stage's constraints
27
75
 
28
- **Flow**
29
- 1. Users pay through the configured terminal or router surface.
30
- 2. `REVOwner` enforces runtime behavior such as pay handling, cash-out rules, delayed exits, and other stage-sensitive constraints.
31
- 3. As stages advance, later pays and cash outs follow the newly active parameters while the project identity stays constant.
76
+ ## Journey 3: Claim Stage-Based Auto-Issuance
32
77
 
33
- **Failure cases that matter:** assuming a stage parameter is mutable when it is fixed at launch, misreading delayed cash-out behavior, and forgetting that optional integrations materially change the participant experience.
78
+ **Actor:** auto-issuance beneficiary.
34
79
 
35
- ## Journey 3: Claim Stage-Based Auto-Issuance When It Becomes Available
80
+ **Intent:** claim stage-specific issuance only when it is actually live.
36
81
 
37
- **Starting state:** the Revnet was deployed with auto-issuance allocations for one or more beneficiaries.
82
+ **Preconditions**
38
83
 
39
- **Success:** the beneficiary claims the right amount for the right stage only after that stage is actually live.
84
+ - the Revnet was deployed with auto-issuance allocations
85
+ - the target stage has started
40
86
 
41
- **Flow**
42
- 1. Check `amountToAutoIssue(...)` for the revnet, stage, and beneficiary.
43
- 2. Call `autoIssueFor(...)` only once the target stage has started.
44
- 3. The stored allocation is consumed so the same stage allocation cannot be claimed twice.
87
+ **Main Flow**
45
88
 
46
- ## Journey 3a: Hide Tokens To Increase Cash-Out Value For Remaining Holders
89
+ 1. Check `amountToAutoIssue(...)`.
90
+ 2. Call `autoIssueFor(...)` once the stage is active.
91
+ 3. The stored allocation is consumed and cannot be claimed twice.
47
92
 
48
- **Starting state:** a holder wants to temporarily exclude some tokens from totalSupply without permanently giving them up.
93
+ **Failure Modes**
49
94
 
50
- **Success:** the holder burns tokens via REVHiddenTokens, reducing totalSupply and increasing the per-token cash-out value for everyone else. The holder can reveal (re-mint) their hidden tokens at any time.
95
+ - callers try to claim before the stage is active
96
+ - reviewers assume auto-issuance is a generic mint path rather than a bounded stage allocation
51
97
 
52
- **Flow**
53
- 1. The holder grants `BURN_TOKENS` permission to the `REVHiddenTokens` contract for the revnet.
54
- 2. Call `hideTokensOf(revnetId, tokenCount, holder)` to burn the tokens and track the hidden balance. An operator with `HIDE_TOKENS` permission can call this on behalf of the holder.
55
- 3. The bonding curve now sees a smaller totalSupply, so each remaining token is worth more on cash out.
56
- 4. When the holder wants their tokens back, call `revealTokensOf(revnetId, tokenCount, beneficiary, holder)` to re-mint them. An operator with `REVEAL_TOKENS` permission can call this on behalf of the holder.
98
+ **Postconditions**
57
99
 
58
- **Failure cases that matter:** trying to reveal more tokens than were hidden, forgetting that revealed tokens increase totalSupply again (reducing per-token cash-out value), and not realizing that hidden tokens must be revealed before they can be used as loan collateral.
100
+ - the stage allocation is either claimed once or remains reserved until valid
59
101
 
60
- ## Journey 4: Borrow Against Revnet Tokens Instead Of Selling Them
102
+ ## Journey 4: Hide Tokens To Change Visible Supply
61
103
 
62
- **Starting state:** a holder wants liquidity but does not want to exit the Revnet position.
104
+ **Actor:** holder or authorized operator.
63
105
 
64
- **Success:** the holder opens a loan, receives borrowed value, and keeps an NFT loan position representing the debt.
106
+ **Intent:** remove tokens from visible supply temporarily and later restore them.
65
107
 
66
- **Flow**
67
- 1. The holder (or an operator with `OPEN_LOAN` permission) interacts with `REVLoans` using the eligible Revnet token exposure as collateral. The `holder` parameter specifies whose tokens are burned; the loan NFT is minted to `holder`.
68
- 2. The system burns the relevant token exposure and mints a loan-position NFT to the holder.
69
- 3. Loan terms depend on the live Revnet economics rather than a static side system.
70
- 4. The borrower (or an operator with `REPAY_LOAN` / `REALLOCATE_LOAN` permission) can later repay, reallocate collateral, transfer the loan NFT, or face liquidation if conditions require it.
108
+ **Preconditions**
71
109
 
72
- ## Journey 5: Repay, Transfer, Or Liquidate A Loan Position
110
+ - the holder granted `BURN_TOKENS` to `REVHiddenTokens`
111
+ - either the holder has been allowlisted for hidden-token actions, or the caller is the project owner / an operator with `HIDE_TOKENS`
112
+ - the holder accepts the supply and collateral implications of hiding tokens
73
113
 
74
- **Starting state:** a loan already exists and either the borrower or another actor needs to change its state.
114
+ **Main Flow**
75
115
 
76
- **Success:** the debt path settles cleanly and the collateral outcome matches the Revnet's current rules.
116
+ 1. Grant `BURN_TOKENS` to `REVHiddenTokens`.
117
+ 2. An operator calls `setTokenHidingAllowedFor(...)` to allow the holder to hide their own tokens.
118
+ 3. The holder, project owner, or a `HIDE_TOKENS` operator calls `hideTokensOf(...)` to burn tokens and track the hidden balance.
119
+ 4. The lower visible supply changes per-token cash-out value.
120
+ 5. Later, the holder calls `revealTokensOf(...)` to remint hidden tokens back to themselves.
77
121
 
78
- **Flow**
79
- 1. Repayment burns the debt and remints or releases the collateralized Revnet token position.
80
- 2. Transfers move the loan NFT, not the original collateralized exposure.
81
- 3. Liquidation consumes the loan under the rules encoded by `REVLoans` and the current Revnet state.
122
+ **Failure Modes**
82
123
 
83
- **Edge conditions that change user experience:** cross-ruleset loan behavior, zero-amount or zero-price edge cases, and sourced versus unsourced loan paths.
124
+ - more tokens are revealed than were hidden
125
+ - holders attempt to hide tokens without being allowlisted
126
+ - non-holders attempt to reveal hidden tokens
127
+ - hidden tokens are assumed to remain usable as loan collateral
128
+
129
+ **Postconditions**
130
+
131
+ - visible supply is reduced or restored according to the holder's hidden-token state
132
+
133
+ ## Journey 5: Borrow Against Revnet Tokens Instead Of Selling Them
134
+
135
+ **Actor:** holder or delegated loan operator.
136
+
137
+ **Intent:** borrow against Revnet exposure instead of selling it.
138
+
139
+ **Preconditions**
140
+
141
+ - the holder has eligible Revnet token exposure
142
+ - the holder trusts any delegated operator with `OPEN_LOAN`
143
+
144
+ **Main Flow**
145
+
146
+ 1. Interact with `REVLoans` using eligible token exposure as collateral.
147
+ 2. The system burns the collateralized exposure and mints a loan NFT.
148
+ 3. Borrowed value is issued under live Revnet economics.
149
+ 4. The borrower can later repay, reallocate, transfer, or face liquidation.
150
+
151
+ **Failure Modes**
152
+
153
+ - delegated operators redirect value in ways the holder did not intend
154
+ - reviewers model the loan system as escrowed collateral when it is burned-collateral lending
155
+
156
+ **Postconditions**
157
+
158
+ - collateralized exposure becomes a live loan position under Revnet economics
84
159
 
85
160
  ## Journey 6: Operate Inside The Bounded Post-Launch Control Envelope
86
161
 
87
- **Starting state:** the Revnet is live and an operator wants to use whatever ongoing controls the deployment allowed.
162
+ **Actor:** operator with ongoing powers.
163
+
164
+ **Intent:** use the sanctioned post-launch controls without violating the autonomous model.
165
+
166
+ **Preconditions**
167
+
168
+ - the Revnet is live
169
+ - the operator knows exactly which controls the deployment left available
170
+
171
+ **Main Flow**
172
+
173
+ 1. Review what `REVDeployer` allowed.
174
+ 2. Use only those sanctioned surfaces.
175
+ 3. Audit cross-package behavior whenever optional integrations are enabled.
88
176
 
89
- **Success:** the operator can use sanctioned controls without escaping the "autonomous after launch" model.
177
+ **Failure Modes**
90
178
 
91
- **Flow**
92
- 1. Review what `REVDeployer` allowed for split operators, stage evolution, and auxiliary integrations.
93
- 2. Use only those surfaces rather than treating the project like a normal owner-governed Juicebox project.
94
- 3. Audit cross-package behavior whenever the Revnet enabled buybacks, 721 hooks, router terminals, or suckers.
179
+ - operators behave as though the Revnet were a normal owner-governed project
180
+ - reviewers inspect controls in isolation and miss integrated runtime behavior
95
181
 
96
- ## Journey 7: Receive Cross-Chain Payments With Correct Hook Routing
182
+ **Postconditions**
97
183
 
98
- **Starting state:** a sucker pays the Revnet on behalf of a remote user via `payRemote`, and the hooks attached via `REVOwner.beforePayRecordedWith` need to see the real user.
184
+ - post-launch control remains inside the bounded envelope left by deployment
99
185
 
100
- **Success:** the 721 hook and buyback hook see the real remote user as the beneficiary so NFTs mint to and buyback routing benefits the correct person.
186
+ ## Trust Boundaries
101
187
 
102
- **Flow**
103
- 1. The sucker calls `terminal.pay()` with relay-beneficiary metadata.
104
- 2. `REVOwner.beforePayRecordedWith()` resolves the relay beneficiary from the metadata when the payer is a registered sucker.
105
- 3. The swapped beneficiary is forwarded to both the 721 hook and the buyback hook.
188
+ - this repo is trusted for revnet-specific economics and runtime policy
189
+ - treasury accounting still comes from core
190
+ - optional integrations materially change revnet behavior and must be reviewed together with the local code
106
191
 
107
192
  ## Hand-Offs
108
193
 
109
- - Use [nana-core-v6](../nana-core-v6/USER_JOURNEYS.md) for the underlying project, terminal, and ruleset mechanics that Revnets package and constrain.
110
- - Use [nana-721-hook-v6](../nana-721-hook-v6/USER_JOURNEYS.md), [nana-buyback-hook-v6](../nana-buyback-hook-v6/USER_JOURNEYS.md), and [nana-suckers-v6](../nana-suckers-v6/USER_JOURNEYS.md) when a Revnet deployment enables those optional features.
194
+ - Use [nana-core-v6](../nana-core-v6/USER_JOURNEYS.md) for underlying terminal and project accounting.
195
+ - Use [nana-buyback-hook-v6](../nana-buyback-hook-v6/USER_JOURNEYS.md), [nana-suckers-v6](../nana-suckers-v6/USER_JOURNEYS.md), and [nana-721-hook-v6](../nana-721-hook-v6/USER_JOURNEYS.md) when those integrations are enabled.
package/foundry.toml CHANGED
@@ -22,6 +22,7 @@ runs = 64
22
22
  depth = 50
23
23
 
24
24
  [lint]
25
+ exclude_lints = ["pascal-case-struct", "mixed-case-variable"]
25
26
  lint_on_build = false
26
27
 
27
28
  [rpc_endpoints]
package/package.json CHANGED
@@ -1,38 +1,38 @@
1
1
  {
2
- "name": "@rev-net/core-v6",
3
- "version": "0.0.32",
4
- "license": "MIT",
5
- "repository": {
6
- "type": "git",
7
- "url": "git+https://github.com/rev-net/revnet-core-v6"
8
- },
9
- "engines": {
10
- "node": ">=20.0.0"
11
- },
12
- "scripts": {
13
- "test": "forge test",
14
- "coverage": "forge coverage --match-path \"./src/*.sol\" --report lcov --report summary",
15
- "deploy:mainnets": "source ./.env && export START_TIME=$(date +%s) && npx sphinx propose ./script/Deploy.s.sol --networks mainnets",
16
- "deploy:mainnets:1_1": "source ./.env && npx sphinx propose ./script/Deploy1_1.s.sol --networks mainnets",
17
- "deploy:testnets": "source ./.env && export START_TIME=$(date +%s) && npx sphinx propose ./script/Deploy.s.sol --networks testnets",
18
- "deploy:testnets:1_1": "source ./.env && npx sphinx propose ./script/Deploy1_1.s.sol --networks testnets",
19
- "artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'revnet-core-v6'"
20
- },
21
- "dependencies": {
22
- "@bananapus/721-hook-v6": "^0.0.33",
23
- "@bananapus/buyback-hook-v6": "^0.0.27",
24
- "@bananapus/core-v6": "^0.0.34",
25
- "@bananapus/ownable-v6": "^0.0.17",
26
- "@bananapus/permission-ids-v6": "^0.0.17",
27
- "@bananapus/router-terminal-v6": "^0.0.26",
28
- "@bananapus/suckers-v6": "^0.0.25",
29
- "@croptop/core-v6": "github:mejango/croptop-core-v6",
30
- "@openzeppelin/contracts": "^5.6.1",
31
- "@uniswap/v4-core": "^1.0.2",
32
- "@uniswap/v4-periphery": "^1.0.3"
33
- },
34
- "devDependencies": {
35
- "@bananapus/address-registry-v6": "^0.0.17",
36
- "@sphinx-labs/plugins": "^0.33.2"
37
- }
2
+ "name": "@rev-net/core-v6",
3
+ "version": "0.0.34",
4
+ "license": "MIT",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/rev-net/revnet-core-v6"
8
+ },
9
+ "engines": {
10
+ "node": ">=20.0.0"
11
+ },
12
+ "scripts": {
13
+ "test": "forge test",
14
+ "coverage": "forge coverage --match-path \"./src/*.sol\" --report lcov --report summary",
15
+ "deploy:mainnets": "source ./.env && export START_TIME=$(date +%s) && npx sphinx propose ./script/Deploy.s.sol --networks mainnets",
16
+ "deploy:mainnets:1_1": "source ./.env && npx sphinx propose ./script/Deploy1_1.s.sol --networks mainnets",
17
+ "deploy:testnets": "source ./.env && export START_TIME=$(date +%s) && npx sphinx propose ./script/Deploy.s.sol --networks testnets",
18
+ "deploy:testnets:1_1": "source ./.env && npx sphinx propose ./script/Deploy1_1.s.sol --networks testnets",
19
+ "artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'revnet-core-v6'"
20
+ },
21
+ "dependencies": {
22
+ "@bananapus/721-hook-v6": "^0.0.35",
23
+ "@bananapus/buyback-hook-v6": "^0.0.27",
24
+ "@bananapus/core-v6": "^0.0.34",
25
+ "@bananapus/ownable-v6": "^0.0.17",
26
+ "@bananapus/permission-ids-v6": "^0.0.17",
27
+ "@bananapus/router-terminal-v6": "^0.0.26",
28
+ "@bananapus/suckers-v6": "^0.0.25",
29
+ "@croptop/core-v6": "github:mejango/croptop-core-v6",
30
+ "@openzeppelin/contracts": "^5.6.1",
31
+ "@uniswap/v4-core": "^1.0.2",
32
+ "@uniswap/v4-periphery": "^1.0.3"
33
+ },
34
+ "devDependencies": {
35
+ "@bananapus/address-registry-v6": "^0.0.17",
36
+ "@sphinx-labs/plugins": "^0.33.2"
37
+ }
38
38
  }
@@ -143,6 +143,7 @@ Use this file when you need revnet-specific risks, state reads, constants, or ex
143
143
  19. **39.16% cash-out tax crossover.** Below ~39% cash-out tax, cashing out is more capital-efficient than borrowing. Above ~39%, loans become more efficient because they preserve upside while providing liquidity. Based on CryptoEconLab academic research. Design implication: revnets intended for active token trading should consider this threshold when setting `cashOutTaxRate`.
144
144
  20. **REVDeployer always deploys a 721 hook** via `HOOK_DEPLOYER.deployHookFor` — even if `baseline721HookConfiguration` has empty tiers. This is correct by design: it lets the split operator add and sell NFTs later without migration. Non-revnet projects should follow the same pattern by using `JB721TiersHookProjectDeployer.launchProjectFor` (or `JBOmnichainDeployer.launchProjectFor`) instead of bare `launchProjectFor`.
145
145
  21. **REVOwner deployer binding is precomputed.** REVOwner records the account that created it as an internal one-time binder. That account must call `setDeployer(precomputedRevDeployerAddress)` exactly once before the canonical REVDeployer is deployed. This avoids an ambient public initializer while keeping the circular dependency manageable. If `setDeployer(...)` is never called, all DEPLOYER-gated runtime configuration breaks.
146
+ 22. **Hidden tokens are economic, not cosmetic.** Hiding burns visible tokens and lowers visible supply until reveal. That changes cash-out and loan-relative economics for everyone else.
146
147
 
147
148
  ### NATIVE_TOKEN Accounting on Non-ETH Chains
148
149
 
@@ -198,6 +199,12 @@ Quick-reference for common read operations. All functions are `view`/`pure` and
198
199
  |------|------|---------|
199
200
  | Remaining auto-issuance for beneficiary | `REVDeployer.amountToAutoIssue(revnetId, stageId, beneficiary)` | `uint256` (0 if already claimed) |
200
201
 
202
+ ### Hidden Tokens
203
+
204
+ | What | Call | Returns |
205
+ |------|------|---------|
206
+ | Hidden balance for holder | `REVHiddenTokens.hiddenBalanceOf(holder, revnetId)` | `uint256` |
207
+
201
208
  ### Loans
202
209
 
203
210
  | What | Call | Returns |
@@ -13,6 +13,7 @@ Deploy and manage Revnets -- autonomous, unowned Juicebox projects with staged i
13
13
  | `REVDeployer` | Deploys revnets, permanently owns the project NFT. Manages stages, splits, auto-issuance, buyback hooks, suckers, split operators, and configuration state storage. Exposes `OWNER()` view returning the REVOwner address. Calls DEPLOYER-restricted setters on REVOwner during deployment to store `cashOutDelayOf` and `tiered721HookOf`. |
14
14
  | `REVOwner` | Runtime hook contract for all revnets. Implements `IJBRulesetDataHook` + `IJBCashOutHook`. Set as the `dataHook` in each revnet's ruleset metadata. Handles pay hooks, cash-out hooks, mint permissions, and sucker verification. Stores `cashOutDelayOf` and `tiered721HookOf` mappings (set by REVDeployer via DEPLOYER-restricted setters `setCashOutDelayOf()` and `setTiered721HookOf()`). |
15
15
  | `REVLoans` | Issues token-collateralized loans from revnet treasuries. Each loan is an ERC-721 NFT. Burns collateral on borrow, re-mints on repay. Charges tiered fees (REV protocol fee + source fee + prepaid fee). |
16
+ | `REVHiddenTokens` | Burns tokens into a hidden balance and can later re-mint them. This is a supply-management primitive, not just a wallet convenience feature. |
16
17
 
17
18
  ## Key Functions
18
19
 
@@ -66,6 +67,7 @@ Deploy and manage Revnets -- autonomous, unowned Juicebox projects with staged i
66
67
  | `REVLoans.loanOf(loanId)` | Returns the full `REVLoan` struct for a loan. |
67
68
  | `REVLoans.loanSourcesOf(revnetId)` | Returns all `(terminal, token)` pairs used for loans by a revnet. |
68
69
  | `REVLoans.revnetIdOfLoanWith(loanId)` | Decode the revnet ID from a loan ID (`loanId / 1_000_000_000_000`). |
70
+ | `REVHiddenTokens.hiddenBalanceOf(holder, revnetId)` | Returns how many tokens a holder has hidden from visible supply. |
69
71
 
70
72
  ## Integration Points
71
73
 
@@ -96,3 +98,10 @@ Deploy and manage Revnets -- autonomous, unowned Juicebox projects with staged i
96
98
  | `REV721TiersHookFlags` | `noNewTiersWithReserves`, `noNewTiersWithVotes`, `noNewTiersWithOwnerMinting`, `preventOverspending` | Same as `JB721TiersHookFlags` minus `issueTokensForSplits`. Revnets do their own weight adjustment for splits. |
97
99
  | `REVCroptopAllowedPost` | `category` (uint24), `minimumPrice` (uint104), `minimumTotalSupply` (uint32), `maximumTotalSupply` (uint32), `allowedAddresses[]` | Croptop posting criteria |
98
100
  | `REVSuckerDeploymentConfig` | `deployerConfigurations[]`, `salt` | Cross-chain sucker deployment |
101
+ ### Hidden Tokens
102
+
103
+ | Function | Permissions | What it does |
104
+ |----------|------------|-------------|
105
+ | `REVHiddenTokens.hideTokensOf(revnetId, tokenCount, holder)` | Holder only. The holder must either be allowlisted or personally hold `HIDE_TOKENS`. | Burns visible tokens, increases hidden balance, and lowers visible supply. |
106
+ | `REVHiddenTokens.revealTokensOf(revnetId, tokenCount, holder)` | Holder only | Re-mints previously hidden tokens back to the holder and reduces hidden balance. |
107
+ | `REVHiddenTokens.setTokenHidingAllowedFor(revnetId, holder, isAllowed)` | Operator with `HIDE_TOKENS` | Allows or revokes a holder's ability to hide their own tokens. |
@@ -375,7 +375,7 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
375
375
  uint256[] memory customSplitOperatorPermissionIndexes = _extraOperatorPermissions[revnetId];
376
376
 
377
377
  // Make the array that merges the default and custom operator permissions.
378
- allOperatorPermissions = new uint256[](10 + customSplitOperatorPermissionIndexes.length);
378
+ allOperatorPermissions = new uint256[](11 + customSplitOperatorPermissionIndexes.length);
379
379
  allOperatorPermissions[0] = JBPermissionIds.SET_SPLIT_GROUPS;
380
380
  allOperatorPermissions[1] = JBPermissionIds.SET_BUYBACK_POOL;
381
381
  allOperatorPermissions[2] = JBPermissionIds.SET_BUYBACK_TWAP;
@@ -386,10 +386,11 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
386
386
  allOperatorPermissions[7] = JBPermissionIds.SET_ROUTER_TERMINAL;
387
387
  allOperatorPermissions[8] = JBPermissionIds.SET_TOKEN_METADATA;
388
388
  allOperatorPermissions[9] = JBPermissionIds.SIGN_FOR_ERC20;
389
+ allOperatorPermissions[10] = JBPermissionIds.HIDE_TOKENS;
389
390
 
390
391
  // Copy the custom permissions into the array.
391
392
  for (uint256 i; i < customSplitOperatorPermissionIndexes.length;) {
392
- allOperatorPermissions[10 + i] = customSplitOperatorPermissionIndexes[i];
393
+ allOperatorPermissions[11 + i] = customSplitOperatorPermissionIndexes[i];
393
394
  unchecked {
394
395
  ++i;
395
396
  }
@@ -4,6 +4,7 @@ pragma solidity 0.8.28;
4
4
  import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
5
5
  import {IJBPermissioned} from "@bananapus/core-v6/src/interfaces/IJBPermissioned.sol";
6
6
  import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
7
+ import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
7
8
  import {JBPermissioned} from "@bananapus/core-v6/src/abstract/JBPermissioned.sol";
8
9
  import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
9
10
  import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol";
@@ -11,14 +12,17 @@ import {Context} from "@openzeppelin/contracts/utils/Context.sol";
11
12
 
12
13
  import {IREVHiddenTokens} from "./interfaces/IREVHiddenTokens.sol";
13
14
 
14
- /// @notice Allows revnet token holders to temporarily hide (burn) tokens, excluding them from totalSupply and
15
- /// increasing cash-out value for remaining holders. Hidden tokens can be revealed (re-minted) at any time.
15
+ /// @notice Allows authorized operators to hide (burn) revnet tokens on behalf of holders, excluding them from
16
+ /// governance weight. Hidden tokens are burned from circulating supply, so they also stop contributing to
17
+ /// cash-out and borrow valuations until revealed again.
18
+ /// Hidden tokens can be revealed (re-minted) at any time.
16
19
  contract REVHiddenTokens is ERC2771Context, JBPermissioned, IREVHiddenTokens {
17
20
  //*********************************************************************//
18
21
  // --------------------------- custom errors ------------------------- //
19
22
  //*********************************************************************//
20
23
 
21
24
  error REVHiddenTokens_InsufficientHiddenBalance(uint256 hiddenBalance, uint256 requested);
25
+ error REVHiddenTokens_Unauthorized(uint256 revnetId, address caller);
22
26
 
23
27
  //*********************************************************************//
24
28
  // --------------- public immutable stored properties ---------------- //
@@ -27,6 +31,9 @@ contract REVHiddenTokens is ERC2771Context, JBPermissioned, IREVHiddenTokens {
27
31
  /// @notice The controller that manages revnets using this contract.
28
32
  IJBController public immutable override CONTROLLER;
29
33
 
34
+ /// @notice The projects contract used to resolve revnet owners.
35
+ IJBProjects public immutable PROJECTS;
36
+
30
37
  //*********************************************************************//
31
38
  // --------------------- public stored properties -------------------- //
32
39
  //*********************************************************************//
@@ -40,6 +47,11 @@ contract REVHiddenTokens is ERC2771Context, JBPermissioned, IREVHiddenTokens {
40
47
  /// @custom:param revnetId The ID of the revnet.
41
48
  mapping(uint256 revnetId => uint256 count) public override totalHiddenOf;
42
49
 
50
+ /// @notice Whether a holder is allowed to hide their own tokens.
51
+ /// @custom:param holder The holder whose tokens are being managed.
52
+ /// @custom:param revnetId The ID of the revnet.
53
+ mapping(address holder => mapping(uint256 revnetId => bool isAllowed)) public override tokenHidingIsAllowedFor;
54
+
43
55
  //*********************************************************************//
44
56
  // -------------------------- constructor ---------------------------- //
45
57
  //*********************************************************************//
@@ -54,20 +66,27 @@ contract REVHiddenTokens is ERC2771Context, JBPermissioned, IREVHiddenTokens {
54
66
  JBPermissioned(IJBPermissioned(address(controller)).PERMISSIONS())
55
67
  {
56
68
  CONTROLLER = controller;
69
+ PROJECTS = controller.PROJECTS();
57
70
  }
58
71
 
59
- //*********************************************************************//
60
- // --------------------- external transactions ----------------------- //
61
- //*********************************************************************//
62
-
63
72
  /// @notice Hide tokens by burning them and tracking them for later reveal.
73
+ /// @dev The caller must be the holder. Hiding is allowed for holders on the operator-managed allowlist,
74
+ /// and also for holders who are themselves the project owner or an operator with `HIDE_TOKENS`.
64
75
  /// @dev The holder must have granted BURN_TOKENS permission to this contract.
65
76
  /// @param revnetId The ID of the revnet whose tokens to hide.
66
77
  /// @param tokenCount The number of tokens to hide.
67
78
  /// @param holder The address whose tokens to hide.
68
79
  function hideTokensOf(uint256 revnetId, uint256 tokenCount, address holder) external override {
69
- // Only the holder or a permissioned operator can hide tokens.
70
- _requirePermissionFrom({account: holder, projectId: revnetId, permissionId: JBPermissionIds.HIDE_TOKENS});
80
+ address caller = _msgSender();
81
+ if (caller != holder) revert REVHiddenTokens_Unauthorized(revnetId, caller);
82
+
83
+ bool isAllowlistedHolder = tokenHidingIsAllowedFor[holder][revnetId];
84
+ bool isPermissionedOperator =
85
+ _hasPermissionFrom(caller, PROJECTS.ownerOf(revnetId), revnetId, JBPermissionIds.HIDE_TOKENS);
86
+
87
+ if (!isAllowlistedHolder && !isPermissionedOperator) {
88
+ revert REVHiddenTokens_Unauthorized(revnetId, caller);
89
+ }
71
90
 
72
91
  // Increment the holder's hidden balance.
73
92
  hiddenBalanceOf[holder][revnetId] += tokenCount;
@@ -83,24 +102,14 @@ contract REVHiddenTokens is ERC2771Context, JBPermissioned, IREVHiddenTokens {
83
102
  }
84
103
 
85
104
  /// @notice Reveal previously hidden tokens by re-minting them.
86
- /// @dev A delegated operator (with REVEAL_TOKENS permission) can set `beneficiary` to any address, directing
87
- /// revealed tokens away from the holder. Grant this permission only to trusted operators.
105
+ /// @dev Any holder can reveal their own hidden tokens without special permissions.
106
+ /// Revealed tokens always return to the holder.
88
107
  /// @param revnetId The ID of the revnet whose tokens to reveal.
89
108
  /// @param tokenCount The number of tokens to reveal.
90
- /// @param beneficiary The address that will receive the revealed tokens.
91
109
  /// @param holder The address whose hidden balance to decrement.
92
- function revealTokensOf(
93
- uint256 revnetId,
94
- uint256 tokenCount,
95
- address beneficiary,
96
- address holder
97
- )
98
- external
99
- override
100
- {
101
- // Only the holder or a permissioned operator can reveal tokens.
102
- // Note: the operator controls `beneficiary`, so they can direct revealed tokens to any address.
103
- _requirePermissionFrom({account: holder, projectId: revnetId, permissionId: JBPermissionIds.REVEAL_TOKENS});
110
+ function revealTokensOf(uint256 revnetId, uint256 tokenCount, address holder) external override {
111
+ address caller = _msgSender();
112
+ if (caller != holder) revert REVHiddenTokens_Unauthorized(revnetId, caller);
104
113
 
105
114
  uint256 hidden = hiddenBalanceOf[holder][revnetId];
106
115
 
@@ -118,12 +127,25 @@ contract REVHiddenTokens is ERC2771Context, JBPermissioned, IREVHiddenTokens {
118
127
  // Mint the tokens to the beneficiary without applying the reserved percent.
119
128
  // slither-disable-next-line unused-return,reentrancy-events
120
129
  CONTROLLER.mintTokensOf({
121
- projectId: revnetId, tokenCount: tokenCount, beneficiary: beneficiary, memo: "", useReservedPercent: false
130
+ projectId: revnetId, tokenCount: tokenCount, beneficiary: holder, memo: "", useReservedPercent: false
122
131
  });
123
132
 
124
- emit RevealTokens({
125
- revnetId: revnetId, tokenCount: tokenCount, beneficiary: beneficiary, holder: holder, caller: _msgSender()
133
+ emit RevealTokens({revnetId: revnetId, tokenCount: tokenCount, holder: holder, caller: _msgSender()});
134
+ }
135
+
136
+ /// @notice Allow or disallow a holder to hide their own tokens.
137
+ /// @dev The caller must have `HIDE_TOKENS` permission for the revnet.
138
+ /// @param revnetId The ID of the revnet.
139
+ /// @param holder The holder to update.
140
+ /// @param isAllowed Whether the holder should be allowed.
141
+ function setTokenHidingAllowedFor(uint256 revnetId, address holder, bool isAllowed) external override {
142
+ _requirePermissionFrom({
143
+ account: PROJECTS.ownerOf(revnetId), projectId: revnetId, permissionId: JBPermissionIds.HIDE_TOKENS
126
144
  });
145
+
146
+ tokenHidingIsAllowedFor[holder][revnetId] = isAllowed;
147
+
148
+ emit SetTokenHidingAllowed({revnetId: revnetId, holder: holder, isAllowed: isAllowed});
127
149
  }
128
150
 
129
151
  //*********************************************************************//
package/src/REVLoans.sol CHANGED
@@ -374,8 +374,8 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
374
374
  // collateral. This is by design: loan value tracks the current bonding curve parameters, just as cash-out
375
375
  // value does. Borrowers benefit from decreasing tax rates and are constrained by increasing ones.
376
376
  // Add cross-chain remote values for proportional reclaim.
377
- uint256 omnichainSurplus =
378
- localSurplus + SUCKER_REGISTRY.remoteSurplusOf({projectId: revnetId, decimals: 18, currency: currency});
377
+ uint256 omnichainSurplus = localSurplus
378
+ + SUCKER_REGISTRY.remoteSurplusOf({projectId: revnetId, decimals: decimals, currency: currency});
379
379
  uint256 omnichainSupply = localSupply + SUCKER_REGISTRY.remoteTotalSupplyOf(revnetId);
380
380
  uint256 reclaimable = JBCashOuts.cashOutFrom({
381
381
  surplus: omnichainSurplus,
package/src/REVOwner.sol CHANGED
@@ -184,8 +184,13 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook {
184
184
  // feeless (e.g. the router terminal routing value between projects), proxy to the buyback hook with our
185
185
  // totalSupply and effectiveSurplusValue.
186
186
  if (context.cashOutTaxRate == 0 || address(feeTerminal) == address(0) || context.beneficiaryIsFeeless) {
187
+ // Build a modified context with cross-chain-adjusted values so the buyback hook sees the global state
188
+ // for its swap-vs-passthrough routing decision.
189
+ JBBeforeCashOutRecordedContext memory routedContext = context;
190
+ routedContext.totalSupply = totalSupply;
191
+ routedContext.surplus.value = effectiveSurplusValue;
187
192
  // slither-disable-next-line unused-return
188
- (cashOutTaxRate, cashOutCount,,, hookSpecifications) = BUYBACK_HOOK.beforeCashOutRecordedWith(context);
193
+ (cashOutTaxRate, cashOutCount,,, hookSpecifications) = BUYBACK_HOOK.beforeCashOutRecordedWith(routedContext);
189
194
  return (cashOutTaxRate, cashOutCount, totalSupply, effectiveSurplusValue, hookSpecifications);
190
195
  }
191
196
 
@@ -227,9 +232,12 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook {
227
232
  feeAmount = context.surplus.value - postFeeReclaimedAmount;
228
233
  }
229
234
 
230
- // Build a context for the buyback hook using only the non-fee token count.
235
+ // Build a context for the buyback hook using the non-fee token count and cross-chain-adjusted values
236
+ // so the buyback hook sees the global state for its swap-vs-passthrough routing decision.
231
237
  JBBeforeCashOutRecordedContext memory buybackHookContext = context;
232
238
  buybackHookContext.cashOutCount = nonFeeCashOutCount;
239
+ buybackHookContext.totalSupply = totalSupply;
240
+ buybackHookContext.surplus.value = effectiveSurplusValue;
233
241
 
234
242
  // Let the buyback hook adjust the cash out parameters and optionally return a hook specification.
235
243
  JBCashOutHookSpecification[] memory buybackHookSpecifications;