@rev-net/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 +38 -51
- package/ARCHITECTURE.md +10 -11
- package/AUDIT_INSTRUCTIONS.md +43 -80
- package/CHANGELOG.md +4 -3
- package/README.md +20 -32
- package/RISKS.md +49 -214
- package/SKILLS.md +10 -17
- package/USER_JOURNEYS.md +47 -66
- package/package.json +1 -1
- package/references/runtime.md +3 -2
- package/src/REVDeployer.sol +3 -2
- package/src/REVHiddenTokens.sol +48 -26
- package/src/REVLoans.sol +2 -2
- package/src/REVOwner.sol +10 -2
- package/src/interfaces/IREVHiddenTokens.sol +21 -6
- package/test/TestAuditFixVerification.t.sol +675 -0
- package/test/TestHiddenTokens.t.sol +51 -8
- package/test/audit/CodexCrossChainBuybackRouteMismatch.t.sol +184 -0
- package/test/audit/NemesisOperatorDelegation.t.sol +77 -10
package/RISKS.md
CHANGED
|
@@ -1,254 +1,89 @@
|
|
|
1
1
|
# Revnet Core Risk Register
|
|
2
2
|
|
|
3
|
-
This file focuses on the risks
|
|
4
|
-
|
|
5
|
-
Read [ARCHITECTURE.md](./ARCHITECTURE.md) and [SKILLS.md](./SKILLS.md) for protocol context first.
|
|
3
|
+
This file focuses on the staged-economics, runtime-hook, hidden-supply, and loan risks that matter in Revnets. The main question is whether the deployed economic shape still holds under real runtime behavior.
|
|
6
4
|
|
|
7
5
|
## How to use this file
|
|
8
6
|
|
|
9
|
-
- Read `Priority risks` first
|
|
10
|
-
- Use the detailed sections
|
|
11
|
-
- Treat `Accepted Behaviors` and `Invariants to Verify` as the
|
|
7
|
+
- Read `Priority risks` first.
|
|
8
|
+
- Use the detailed sections to separate stage design, hook composition, hidden supply, and loan accounting.
|
|
9
|
+
- Treat `Accepted Behaviors` and `Invariants to Verify` as the line between intended product tradeoffs and defects.
|
|
12
10
|
|
|
13
11
|
## Priority risks
|
|
14
12
|
|
|
15
13
|
| Priority | Risk | Why it matters | Primary controls |
|
|
16
14
|
|----------|------|----------------|------------------|
|
|
17
|
-
| P0 |
|
|
18
|
-
|
|
|
19
|
-
| P1 |
|
|
15
|
+
| P0 | Borrowability drift from live surplus or cross-chain state | Loans can overextend or under-credit if revnet state is read incorrectly. | Borrowability tests, omnichain-state checks, and cash-out-delay gating. |
|
|
16
|
+
| P1 | Stage configuration mistakes | Revnet economics are hard to change after launch, so bad stages are expensive. | Deployment review, stage-transition tests, and launch-time validation. |
|
|
17
|
+
| P1 | Hidden-supply and burned-collateral misunderstandings | Hidden tokens and loans both change visible supply in non-obvious ways. | Explicit supply invariants and product-level review. |
|
|
20
18
|
|
|
21
19
|
## 1. Trust Assumptions
|
|
22
20
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
- **
|
|
26
|
-
- **
|
|
27
|
-
- **Stage immutability is the trust model.** Once `deployFor()` completes, stage parameters (issuance, `cashOutTaxRate`, splits, auto-issuances) are locked forever. No owner, no governance, no upgrade path. A misconfigured deployment is permanent. This is intentional -- the absence of admin keys IS the security property.
|
|
28
|
-
- **Bonding curve is the sole collateral oracle.** `REVLoans` uses `JBCashOuts.cashOutFrom` to value collateral. There is no external price oracle, no liquidation margin, and no health factor. The borrowable amount equals the cash-out value at the moment of borrowing.
|
|
29
|
-
- **Juicebox core contracts are correct.** `JBController`, `JBMultiTerminal`, `JBTerminalStore`, `JBTokens`, `JBPrices` -- a bug in any of these is a bug in every revnet.
|
|
30
|
-
- **Buyback hook operates correctly.** `BUYBACK_HOOK` handles swap-vs-mint routing. All revnets from the same deployer share one instance. Failure falls back to direct minting (not a revert), so the failure mode is economic inefficiency, not fund loss.
|
|
31
|
-
- **Buyback pool auto-initialization is best-effort, not guaranteed.** `REVDeployer._tryInitializeBuybackPoolFor` silently catches `initializePoolFor(...)` failures. That means a revnet can deploy successfully while the expected default buyback pool is missing, misconfigured, or already initialized differently. Operators must verify pool state after deployment instead of assuming launch implies the buyback pool is ready.
|
|
32
|
-
- **REVOwner expects at most one buyback cash-out hook spec.** In `beforeCashOutRecordedWith`, `REVOwner` keeps only `buybackHookSpecifications[0]` when composing the buyback path with the rev fee hook. If a future buyback hook implementation returns multiple cash-out hook specs, the additional specs are silently dropped.
|
|
33
|
-
- **Suckers are honest bridges.** Suckers get 0% cashout tax in `beforeCashOutRecordedWith`. A compromised or malicious sucker registered in `SUCKER_REGISTRY` can extract funds from any revnet at zero cost.
|
|
34
|
-
- **Auto-issuance beneficiaries are set at deployment.** Beneficiary addresses are baked into the stage configuration. If a beneficiary address is a contract that becomes compromised, or an EOA whose keys are lost, those auto-issuance tokens are either captured or permanently unclaimable.
|
|
35
|
-
- **REVLoans contract address is immutable per deployer.** `LOANS` is set once in the REVDeployer constructor (and shared as an immutable on REVOwner) with wildcard `USE_ALLOWANCE` permission (`projectId=0`). If the loans contract has a vulnerability, every revnet's surplus is exposed.
|
|
36
|
-
|
|
37
|
-
### What you do NOT need to trust
|
|
38
|
-
|
|
39
|
-
- **Project owners.** There are none. REVDeployer permanently holds the project NFT.
|
|
40
|
-
- **Split operators.** They can change splits, manage 721 tiers, deploy suckers, and set buyback pools, but cannot change stage parameters, issuance rates, cashout tax rates, or directly access treasury funds.
|
|
41
|
-
- **Token holders.** They can only cash out proportional to the bonding curve. Borrowers can only borrow up to the bonding curve value of their burned collateral.
|
|
42
|
-
|
|
43
|
-
---
|
|
21
|
+
- **`REVDeployer` and `REVOwner` are one design.** Misreading them independently is a review hazard.
|
|
22
|
+
- **Core protocol state is still upstream truth.** Revnet economics sit on top of `nana-core-v6`, not outside it.
|
|
23
|
+
- **Optional integrations matter.** Buybacks, 721 hooks, and suckers can materially change runtime behavior.
|
|
24
|
+
- **Price feeds and source accounting matter for loans.** Cross-currency debt aggregation depends on working feed assumptions.
|
|
44
25
|
|
|
45
26
|
## 2. Economic Risks
|
|
46
27
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
- **
|
|
50
|
-
- **
|
|
51
|
-
- **
|
|
52
|
-
|
|
53
|
-
### Stage transition edge cases
|
|
54
|
-
|
|
55
|
-
- **`cashOutTaxRate` increase destroys loan health.** Active loans use the CURRENT stage's `cashOutTaxRate`. When a stage transition increases this rate, existing loans become under-collateralized -- the collateral's cash-out value drops but the loan amount remains unchanged. No forced repayment or margin call mechanism exists. Over 10 years with multiple stage transitions, this compounds.
|
|
56
|
-
- **`cashOutTaxRate` decrease creates refinancing opportunity.** When a new stage lowers the tax rate, existing collateral becomes worth more. Borrowers can `reallocateCollateralFromLoan` to extract the surplus value. This creates a predictable, front-runnable event at every stage boundary.
|
|
57
|
-
- **Weight decay approaching zero over long periods.** With `issuanceCutPercent > 0` and `issuanceCutFrequency > 0`, issuance weight decays exponentially through successive rulesets. After long enough, new payments mint negligibly few tokens, meaning the token supply effectively freezes. This concentrates cash-out value among existing holders and makes the bonding curve increasingly sensitive to individual cash-outs. The current repo models decay through ruleset duration and `weightCutPercent`, so validation should focus on long-horizon ruleset progression and issuance-decay tests rather than any separate weight-cache updater.
|
|
58
|
-
- **Duration=0 stages never auto-expire.** A stage with `duration=0` (no issuance cut frequency) persists until explicitly replaced by a subsequent stage's `startsAtOrAfter`. If the next stage's `startsAtOrAfter` is far in the future, the current stage runs indefinitely at its configured parameters.
|
|
59
|
-
|
|
60
|
-
### Cross-currency reclaim calculations
|
|
61
|
-
|
|
62
|
-
- **`_totalBorrowedFrom` aggregates across currencies via `JBPrices`.** Each loan source may be in a different token/currency. Aggregation normalizes decimals and converts via price feeds. If a price feed returns zero, that source is skipped (prevents division-by-zero DoS). But a stale or manipulated price feed silently over- or under-counts total borrowed amount, affecting all subsequent borrow operations for the revnet.
|
|
63
|
-
- **Undercount from zero-price feeds is unbounded.** When `PRICES.pricePerUnitOf` returns 0 for a source, that source's entire outstanding debt is excluded from the `_totalBorrowedFrom` total. There is no cap on the magnitude of the omission -- it equals the full borrowed amount from the affected source. Concretely: if a revnet has 100 ETH borrowed across 3 sources and one source's feed returns 0, that source's debt becomes invisible, potentially allowing the remaining borrowing capacity to be over-utilized. The more debt concentrated in the affected source, the larger the undercount.
|
|
64
|
-
- **Cascading effect on borrowing capacity.** Because `_totalBorrowedFrom` is called on every `borrowFrom` to enforce `_borrowableAmountFrom`, an undercount directly inflates the perceived remaining borrowing capacity for the entire revnet -- not just the affected source. New borrowers across all sources benefit from the artificially low total, and the over-extension compounds with each new loan taken while the feed is down.
|
|
65
|
-
- **No automatic recovery mechanism.** Once a price feed returns 0, the affected source's debt remains invisible to `_totalBorrowedFrom` until the feed recovers. There is no event emitted when a source is skipped, no fallback oracle, and no circuit breaker that pauses borrowing when feed health degrades. Operators should actively monitor price feed health and treat any feed returning 0 as a risk event for the revnet's loan system.
|
|
66
|
-
- **Decimal normalization truncation.** When converting from higher-decimal tokens (18) to lower-decimal targets (6), integer division truncates. For sources with large outstanding borrows in high-decimal tokens, this truncation can systematically undercount the total borrowed amount, allowing slightly more borrowing than intended. For example, converting 999,999,999,999 wei (18-decimal) to 6-decimal precision discards 12 digits of precision. Across many sources, these per-source truncation errors accumulate additively, further widening the gap between the reported and actual total debt.
|
|
67
|
-
|
|
68
|
-
### Hidden token supply manipulation
|
|
69
|
-
|
|
70
|
-
- **Hiding reduces totalSupply, inflating per-token cash-out value.** When a holder hides tokens via `REVHiddenTokens`, those tokens are burned and excluded from `totalSupply`. The bonding curve sees fewer tokens, so each remaining token is worth more on cash-out. A holder with a large position can hide tokens, have an accomplice cash out at the inflated rate, then reveal. The net effect depends on the `cashOutTaxRate` and the relative positions. Operator delegation (`HIDE_TOKENS`/`REVEAL_TOKENS` permissions) extends this to permissioned operators acting on behalf of holders.
|
|
71
|
-
- **Hidden tokens must be revealed before use as loan collateral.** `REVHiddenTokens` and `REVLoans` are separate systems. A holder cannot borrow against hidden tokens — they must first reveal (re-mint) them, then borrow. This is by design but may confuse users.
|
|
72
|
-
- **Reveal mints without reserved percent.** `revealTokensOf` calls `mintTokensOf` with `useReservedPercent: false`. This is correct because the tokens were previously burned and are being restored, not newly issued. But it means revealed tokens bypass the reserved-token mechanism entirely.
|
|
73
|
-
- **REVHiddenTokens has mint permission via REVOwner.** The contract is added to `REVOwner.hasMintPermissionFor`. If `REVHiddenTokens` has a vulnerability, it could mint unbounded tokens for any revnet.
|
|
74
|
-
|
|
75
|
-
### Auto-issuance overflow potential
|
|
76
|
-
|
|
77
|
-
- **`REVAutoIssuance.count` is `uint104` (~2.03e31).** Multiple auto-issuances for the same beneficiary in the same stage are summed via `+=` in `_makeRulesetConfigurations`. If cumulative auto-issuances exceed `uint256`, this wraps. In practice, `uint104` inputs limit each addition, but verify no path allows the mapping value to overflow.
|
|
78
|
-
- **Auto-issuance dilutes existing holders.** Large auto-issuances at stage boundaries dilute the token supply, reducing per-token cash-out value. This is permissionless (`autoIssueFor` can be called by anyone). A griefing vector exists where someone calls `autoIssueFor` immediately before another user's cash-out to reduce their reclaim amount. However, the dilution is pre-configured and predictable.
|
|
79
|
-
|
|
80
|
-
---
|
|
81
|
-
|
|
82
|
-
## 3. Loan System Risks
|
|
83
|
-
|
|
84
|
-
### Collateral valuation during price volatility
|
|
85
|
-
|
|
86
|
-
- **Bonding curve is internal, not market-price.** Collateral is valued by the bonding curve (`JBCashOuts.cashOutFrom`) which depends on surplus, total supply, and `cashOutTaxRate`. External market price (e.g., DEX price of the revnet token) is irrelevant to collateral valuation. If the market price diverges significantly from the bonding curve value, arbitrage opportunities arise between borrowing and trading.
|
|
87
|
-
- **Surplus is cross-terminal aggregate.** `_borrowableAmountFrom` calls `JBSurplus.currentSurplusOf` across all terminals. If one terminal holds tokens in a volatile asset that has crashed, the aggregate surplus drops, reducing collateral value for ALL borrowers regardless of which terminal their loan draws from.
|
|
88
|
-
|
|
89
|
-
### Liquidation concerns
|
|
90
|
-
|
|
91
|
-
- **No cascading liquidation mechanism.** There is no health factor, no margin call, and no keeper-triggered liquidation for under-collateralized loans. The only liquidation path is `liquidateExpiredLoansFrom` after 10 years. Under-collateralized loans persist indefinitely within that window.
|
|
92
|
-
- **Liquidation iterates by loan number.** `liquidateExpiredLoansFrom` takes `startingLoanId` and `count`, iterating sequentially. Repaid and already-liquidated loans are skipped (`createdAt == 0`), but the caller pays gas for every skip. If a revnet has thousands of loans with sparse gaps (many repaid), liquidation becomes expensive. The `count` parameter bounds gas per call, but a malicious actor could create many small loans to increase cleanup costs.
|
|
93
|
-
- **Liquidation permanently destroys collateral.** Collateral was burned at borrow time. Upon liquidation, `totalCollateralOf` is decremented but no tokens are minted or returned. The collateral is permanently removed from the token supply. This deflates the total supply, increasing per-token value for remaining holders -- a mild positive externality from defaults.
|
|
94
|
-
|
|
95
|
-
### Loan source rotation after deployment
|
|
96
|
-
|
|
97
|
-
- **Loan sources grow monotonically.** `_loanSourcesOf[revnetId]` is append-only. Each new `(terminal, token)` pair used for borrowing adds an entry. Entries are never removed, even if all loans from that source are repaid. `_totalBorrowedFrom` iterates the entire array on every borrow/repay.
|
|
98
|
-
- **Removed terminals remain as loan sources.** If a terminal is de-registered from `JBDirectory` (via migration), existing loans from that terminal remain valid because each loan stores a direct terminal reference. New borrows against that terminal are blocked by the `DIRECTORY.isTerminalOf` check in `borrowFrom`, but `_totalBorrowedFrom` still queries the old terminal's `accountingContextForTokenOf`. On current core terminals that call returns the stored accounting context rather than reverting, so debt accounting continues to depend on that legacy terminal's retained token configuration even after directory migration.
|
|
99
|
-
|
|
100
|
-
### `reallocateCollateralFromLoan` sandwich potential
|
|
101
|
-
|
|
102
|
-
- **Reallocation is two operations in one transaction.** `reallocateCollateralFromLoan` first reduces collateral on the existing loan (via `_reallocateCollateralFromLoan`), then opens a new loan (via `borrowFrom`). Between these two operations, the surplus and total supply have changed (collateral was returned to the caller, changing supply). The new loan's borrowable amount is computed with the post-reallocation state.
|
|
103
|
-
- **Source mismatch check.** `reallocateCollateralFromLoan` enforces that the new loan's source matches the existing loan's source (`source.token == existingSource.token && source.terminal == existingSource.terminal`). This prevents cross-source value extraction.
|
|
104
|
-
- **MEV opportunity at stage boundaries.** If a borrower knows a stage transition will decrease `cashOutTaxRate`, they can wait until just after the transition and `reallocateCollateralFromLoan` to extract more borrowed funds from the same collateral. This is predictable and not preventable by design.
|
|
105
|
-
|
|
106
|
-
### Cross-chain cash-out delay enforcement
|
|
107
|
-
|
|
108
|
-
- **Loans enforce the same 30-day cash-out delay as direct cash outs.** When a revnet is deployed to a new chain where its first stage has already started, REVDeployer calls `REVOwner.setCashOutDelayOf()` to set a 30-day delay (stored on REVOwner). `borrowFrom` resolves the REVOwner from the current ruleset's `dataHook` and checks `IREVOwner.cashOutDelayOf(revnetId)` (read directly from REVOwner storage), reverting with `REVLoans_CashOutDelayNotFinished` if the delay hasn't passed. `borrowableAmountFrom` returns 0 during the delay. This prevents cross-chain arbitrage via the loan system (bridging tokens to a new chain and immediately borrowing against them before prices equilibrate).
|
|
109
|
-
|
|
110
|
-
### BURN_TOKENS permission prerequisite
|
|
111
|
-
|
|
112
|
-
- **Borrowers must grant BURN_TOKENS permission before calling `borrowFrom`.** The loans contract burns the caller's tokens as collateral via `JBController.burnTokensOf`, which requires the caller to have granted `BURN_TOKENS` permission to the loans contract for the revnet's project ID. Without this, the transaction reverts deep in `JBController` with `JBPermissioned_Unauthorized`. The prerequisite is documented in `borrowFrom`'s NatSpec.
|
|
113
|
-
|
|
114
|
-
---
|
|
115
|
-
|
|
116
|
-
## 4. Data Hook Proxy Risks
|
|
117
|
-
|
|
118
|
-
REVOwner sits between the terminal and the actual hooks (buyback hook, 721 hook). This proxy pattern creates composition risks.
|
|
119
|
-
|
|
120
|
-
### Underlying hook reverts
|
|
28
|
+
- **Stage immutability cuts both ways.** A bad stage schedule or bad cash-out tax choice is expensive to unwind.
|
|
29
|
+
- **Borrowability depends on live economics.** If surplus, supply, or cross-chain state are wrong, loan capacity becomes wrong.
|
|
30
|
+
- **Zero or degraded price feeds can undercount debt.** If a source becomes invisible to debt aggregation, later borrowing can become too permissive.
|
|
31
|
+
- **Hidden-token mechanics change visible supply.** That affects per-token cash-out value and can change the economics seen by other holders.
|
|
32
|
+
- **Auto-issuance dilutes holders predictably but still materially.** Timing is permissionless, even if the amounts are fixed at deployment.
|
|
33
|
+
- **Omnichain expansion can corrupt surplus aggregation.** Since borrowability aggregates surplus from all registered terminals across chains, a compromised or misconfigured terminal on a remote chain affects global surplus accounting.
|
|
121
34
|
|
|
122
|
-
|
|
123
|
-
- **Buyback hook is more resilient.** The `BUYBACK_HOOK.beforePayRecordedWith(buybackHookContext)` call in REVOwner is also not try-caught, but the buyback hook is a shared singleton controlled by the protocol. If it reverts, all revnets from that deployer are affected.
|
|
124
|
-
- **Cash-out fee terminal revert.** In `REVOwner.afterCashOutRecordedWith`, the fee payment to the fee terminal IS wrapped in try-catch with a fallback to `addToBalanceOf`. If the fallback also reverts, the entire cashout transaction reverts — no funds are stuck, but the cashout is blocked until the terminal is available.
|
|
35
|
+
## 3. Loan Risks
|
|
125
36
|
|
|
126
|
-
|
|
37
|
+
- **Burned collateral is not escrow.** Reviewers and integrators who model it as escrow will misread liquidation and repayment behavior.
|
|
38
|
+
- **No short-term liquidation model.** Under-collateralized loans can persist until the long expiry model allows cleanup.
|
|
39
|
+
- **Loan sources grow over time.** Debt aggregation cost and complexity increase as new source pairs are used.
|
|
40
|
+
- **Reallocation still depends on live state.** Reallocate flows can change outcomes around stage boundaries.
|
|
127
41
|
|
|
128
|
-
|
|
129
|
-
- **Sucker registration is controlled by `SUCKER_REGISTRY`.** The registry has `MAP_SUCKER_TOKEN` wildcard permission from the deployer. The split operator has `SUCKER_SAFETY` permission. Verify that `deploySuckersFor` correctly checks the `extraMetadata` bit (bit 2) for sucker deployment permission per stage.
|
|
42
|
+
## 4. Hidden-Token Risks
|
|
130
43
|
|
|
131
|
-
|
|
44
|
+
- **Visible-supply manipulation is intentional.** Hiding tokens changes visible supply and therefore changes redemption economics.
|
|
45
|
+
- **Hidden tokens are not usable collateral while hidden.** They must be revealed before borrowing.
|
|
46
|
+
- **Reveal is restoration, not fresh issuance.** It intentionally bypasses reserved-percent behavior.
|
|
132
47
|
|
|
133
|
-
|
|
134
|
-
- **Wildcard permissions.** REVDeployer grants `USE_ALLOWANCE` to `LOANS` with `projectId=0` (wildcard). This means the loans contract can drain surplus from ANY revnet deployed by this deployer, constrained only by the loan logic itself. A bug in `REVLoans._addTo` that miscalculates `addedBorrowAmount` could drain treasuries.
|
|
48
|
+
## 5. Hook-Composition Risks
|
|
135
49
|
|
|
136
|
-
|
|
50
|
+
- **`REVOwner` is a real runtime authority surface.** It composes pay hooks, cash-out hooks, sucker exemptions, and fee logic.
|
|
51
|
+
- **Suckers can bypass tax and fee paths by design.** That privilege is safe only if registry and deployer assumptions are correct.
|
|
52
|
+
- **Mint-permission surfaces are broad enough to matter.** Loans, hidden tokens, buyback flows, and suckers all touch mint authority in some deployments.
|
|
137
53
|
|
|
138
|
-
##
|
|
54
|
+
## 6. Access-Control Risks
|
|
139
55
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
- **
|
|
143
|
-
- **Project NFT cannot be recovered.** Once transferred to REVDeployer via `safeTransferFrom` or minted by `launchProjectFor`, the NFT is permanently held. REVDeployer implements `onERC721Received` but only accepts from `PROJECTS`. It has no `transferFrom` or equivalent for project NFTs.
|
|
144
|
-
|
|
145
|
-
### Who can deploy and modify
|
|
146
|
-
|
|
147
|
-
- **`deployFor` is permissionless for new revnets** (`revnetId == 0`). Anyone can deploy a revnet with arbitrary configuration.
|
|
148
|
-
- **`deployFor` with existing project requires ownership.** The caller must be `PROJECTS.ownerOf(revnetId)` and the project must be blank (no controller, no rulesets).
|
|
149
|
-
- **Split operator is singular and self-replacing.** `setSplitOperatorOf` can only be called by the current split operator. If the split operator is set to address(0) or a contract with no ability to call `setSplitOperatorOf`, the role is permanently lost.
|
|
150
|
-
- **Split operator permissions are cumulative during deployment.** `_extraOperatorPermissions[revnetId]` is populated via `.push()` during deployment. If the same permission ID is pushed twice, the array has duplicates but this is harmless (the permission check uses `hasPermissions` which doesn't care about duplicates).
|
|
151
|
-
|
|
152
|
-
### Split operator trust boundaries
|
|
153
|
-
|
|
154
|
-
- **ADD_PRICE_FEED.** The split operator can add price feeds for the revnet. A malicious price feed could return manipulated values, affecting cross-currency surplus calculations and loan collateral valuations. Price feeds are immutable once added (cannot be replaced).
|
|
155
|
-
- **SET_SPLIT_GROUPS.** The split operator controls where reserved tokens go. A compromised operator can redirect all reserved tokens to themselves.
|
|
156
|
-
- **DEPLOY_SUCKERS (via `deploySuckersFor`).** The split operator can deploy new suckers if the current stage allows it (`extraMetadata` bit 2). A malicious sucker gets 0% cashout tax privilege. This is the highest-impact split operator action.
|
|
157
|
-
- **SET_ROUTER_TERMINAL.** The split operator can configure the router terminal, potentially redirecting payments.
|
|
158
|
-
|
|
159
|
-
---
|
|
160
|
-
|
|
161
|
-
## 6. DoS Vectors
|
|
162
|
-
|
|
163
|
-
### Long stage chains
|
|
164
|
-
|
|
165
|
-
- **`_makeRulesetConfigurations` iterates all stages.** Deployment cost scales linearly with stage count. There is no explicit cap on the number of stages. A deployment with hundreds of stages would be expensive but is not blocked.
|
|
166
|
-
- **Auto-issuance inner loop.** For each stage, the deployment iterates all `autoIssuances[]`. The combined iteration (stages x auto-issuances per stage) could hit the block gas limit for extreme configurations.
|
|
167
|
-
|
|
168
|
-
### Many auto-issuances
|
|
169
|
-
|
|
170
|
-
- **`autoIssueFor` is one-per-call.** Each call processes a single `(revnetId, stageId, beneficiary)` tuple. If a stage has many beneficiaries, they must each be claimed individually. Not a DoS vector against the protocol, but a usability concern.
|
|
171
|
-
|
|
172
|
-
### Loan source enumeration
|
|
173
|
-
|
|
174
|
-
- **`_totalBorrowedFrom` iterates ALL sources on every borrow and repay.** Gas cost: ~20k per source (external `accountingContextForTokenOf` call + storage read + potential price feed call). With 10 sources, this adds ~200k gas per loan operation. With 50+ sources (unlikely but possible), operations become prohibitively expensive.
|
|
175
|
-
- **Mitigation.** `borrowFrom` checks `DIRECTORY.isTerminalOf` before accepting a new source. The number of registered terminals per project is practically bounded. But nothing prevents a terminal from being registered, used for one loan, de-registered, and then a new terminal registered -- leaving stale entries in the source array.
|
|
176
|
-
|
|
177
|
-
---
|
|
56
|
+
- **The deployer-held project NFT can be misunderstood.** Revnets are owner-minimized, but the deployer path still matters for the trust model.
|
|
57
|
+
- **Split operator mistakes are high-impact.** Narrow powers like price-feed installation, split updates, sucker deployment, or router setup still matter.
|
|
58
|
+
- **There is intentionally no broad admin recovery path.** Operational teams may try to reach for powers the design never intended to leave available.
|
|
178
59
|
|
|
179
60
|
## 7. Invariants to Verify
|
|
180
61
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
-
|
|
186
|
-
-
|
|
187
|
-
- **No double-mint collateral.** Repaying a loan mints collateral back exactly once. A repaid loan cannot be repaid again (NFT is burned, storage is deleted).
|
|
188
|
-
- **No zero-collateral loans.** Every active loan has `collateral > 0` and `amount > 0`. The `borrowFrom` function reverts on `collateralCount == 0` and `borrowAmount == 0`.
|
|
189
|
-
- **Liquidation only after expiry.** `liquidateExpiredLoansFrom` skips loans where `block.timestamp <= loan.createdAt + LOAN_LIQUIDATION_DURATION`.
|
|
190
|
-
- **Total loans counter monotonicity.** `totalLoansBorrowedFor[revnetId]` only increments (on borrow, repay-with-replacement, and reallocation). It never decrements. Loan IDs are unique and never reused.
|
|
191
|
-
|
|
192
|
-
### Stage and deployment
|
|
193
|
-
|
|
194
|
-
- **Stage immutability.** After `deployFor` completes, no function in REVDeployer calls `CONTROLLER.queueRulesetsOf` or modifies ruleset parameters.
|
|
195
|
-
- **Stage progression monotonicity.** `startsAtOrAfter` values strictly increase between stages. The first stage can be 0 (mapped to `block.timestamp`).
|
|
196
|
-
- **Auto-issuance single-claim.** Each `(revnetId, stageId, beneficiary)` can only be claimed once. `amountToAutoIssue` is zeroed BEFORE the external `mintTokensOf` call (CEI pattern).
|
|
197
|
-
- **Split percentages.** Per-stage `splitPercent > 0` requires `splits.length > 0`. Split percentages are validated by `JBSplits` in core (must sum to <= `SPLITS_TOTAL_PERCENT`).
|
|
198
|
-
- **Buyback pool readiness is not implied by successful deployment.** `deployFor` can finish even when `_tryInitializeBuybackPoolFor` failed internally. Post-deploy verification must check the actual buyback pool registration and initialization state rather than inferring success from the absence of a revert.
|
|
199
|
-
|
|
200
|
-
### Fee flows
|
|
201
|
-
|
|
202
|
-
- **Cash-out fees flow to fee revnet.** If the fee terminal `pay` succeeds, the fee goes to `FEE_REVNET_ID`. If it fails, the fee returns to the originating project via `addToBalanceOf`. Funds are never lost and never kept by the caller.
|
|
203
|
-
- **Loan REV fees flow to REV revnet.** If `feeTerminal.pay` succeeds, the fee goes to `REV_ID`. If it fails (try-catch), the fee amount is zeroed and added to the borrower's payout. The fee is never lost -- it either reaches the REV revnet or goes to the borrower.
|
|
204
|
-
|
|
205
|
-
### Hidden token accounting
|
|
206
|
-
|
|
207
|
-
- **Hidden balance conservation.** `totalHiddenOf[revnetId]` == sum of `hiddenBalanceOf[holder][revnetId]` across all holders.
|
|
208
|
-
- **Reveal bounded by hide.** No holder can reveal more tokens than they have hidden. `revealTokensOf` reverts with `REVHiddenTokens_InsufficientHiddenBalance` if `tokenCount > hiddenBalanceOf[caller][revnetId]`.
|
|
209
|
-
- **No double-mint from reveal.** Each hidden token can only be revealed once. Decrementing `hiddenBalanceOf` before minting prevents double-reveal.
|
|
210
|
-
|
|
211
|
-
### Privilege isolation
|
|
212
|
-
|
|
213
|
-
- **Sucker privilege.** Only addresses returning `true` from `SUCKER_REGISTRY.isSuckerOf(projectId, addr)` get 0% cashout tax. No other code path grants this exemption.
|
|
214
|
-
- **Loan ownership.** Only `_ownerOf(loanId)` — or an operator with the relevant `JBPermissionIds` (`REPAY_LOAN` for repayment, `REALLOCATE_LOAN` for reallocation) — can call `repayLoan` and `reallocateCollateralFromLoan`. Similarly, `borrowFrom` requires the caller to be the `holder` or to have `OPEN_LOAN` permission. The loan NFT is burned before any state changes in repayment, preventing double-use. Replacement loan NFTs are minted to the original holder/owner, not the operator. However, delegated operators control the `beneficiary` parameter and can direct borrowed funds, returned collateral, or revealed tokens to any address (see §8.5).
|
|
215
|
-
- **Mint permission.** Only `LOANS`, `HIDDEN_TOKENS`, `BUYBACK_HOOK`, buyback hook delegates (via `BUYBACK_HOOK.hasMintPermissionFor`), and suckers (via `REVOwner._isSuckerOf`) can mint tokens. No other address passes the `REVOwner.hasMintPermissionFor` check.
|
|
216
|
-
|
|
217
|
-
---
|
|
62
|
+
- Collateral and debt conservation across all active loans.
|
|
63
|
+
- Stage immutability after deployment.
|
|
64
|
+
- Borrowability dropping to zero when cash-out delay should still block borrowing.
|
|
65
|
+
- Hidden-balance conservation across hide and reveal flows.
|
|
66
|
+
- Sucker-only privileges staying restricted to real registered suckers.
|
|
67
|
+
- Mint permission remaining limited to the documented runtime surfaces.
|
|
218
68
|
|
|
219
69
|
## 8. Accepted Behaviors
|
|
220
70
|
|
|
221
|
-
### 8.1 Suckers receive 0%
|
|
71
|
+
### 8.1 Suckers receive 0% cash-out tax
|
|
222
72
|
|
|
223
|
-
|
|
73
|
+
Trusted suckers are intentionally exempt so bridged value preserves its economic meaning across chains.
|
|
224
74
|
|
|
225
|
-
### 8.2
|
|
75
|
+
### 8.2 There is no short-horizon liquidation model
|
|
226
76
|
|
|
227
|
-
|
|
77
|
+
Revnet loans are designed more like long-dated economic positions than instantly mark-to-market margin loans.
|
|
228
78
|
|
|
229
79
|
### 8.3 Auto-issuance dilution is permissionless but predictable
|
|
230
80
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
### 8.4 Surplus manipulation via donations is economically irrational (by design)
|
|
234
|
-
|
|
235
|
-
`_borrowableAmountFrom` reads live surplus. An attacker could inflate surplus via `addToBalanceOf`, but donations are permanent (no recovery), and the extra borrowable amount is always less than the donation. `pay` increases both surplus AND supply, neutralizing the effect. With non-zero `cashOutTaxRate`, the concave bonding curve makes this even worse for attackers. The attack is self-defeating by construction.
|
|
236
|
-
|
|
237
|
-
### 8.5 Delegated operators control beneficiary independently of holder/owner (by design)
|
|
238
|
-
|
|
239
|
-
When a holder grants `OPEN_LOAN`, `REALLOCATE_LOAN`, `REPAY_LOAN`, or `REVEAL_TOKENS` permission to an operator, the operator can set the `beneficiary` parameter to any address — including themselves. This means:
|
|
240
|
-
|
|
241
|
-
- **`borrowFrom`**: An operator with `OPEN_LOAN` permission burns the holder's tokens as collateral but can direct the borrowed funds to an arbitrary beneficiary.
|
|
242
|
-
- **`reallocateCollateralFromLoan`**: An operator with `REALLOCATE_LOAN` permission can direct the borrowed funds from the new loan to an arbitrary beneficiary.
|
|
243
|
-
- **`repayLoan`**: An operator with `REPAY_LOAN` permission can direct returned collateral tokens to an arbitrary beneficiary.
|
|
244
|
-
- **`revealTokensOf`**: An operator with `REVEAL_TOKENS` permission can direct revealed (re-minted) tokens to an arbitrary beneficiary instead of the original holder.
|
|
245
|
-
|
|
246
|
-
This is accepted because the JBPermissions delegation model is opt-in: a holder explicitly grants permission to a specific operator for a specific project. The holder trusts the operator to act in their interest. Restricting `beneficiary` to the holder would break legitimate delegation use cases (e.g., automated vaults, multi-sig workflows, portfolio managers). Holders should only grant these permissions to operators they fully trust.
|
|
81
|
+
Anyone can trigger a valid auto-issuance once a stage is live, but the amount was fixed at deployment.
|
|
247
82
|
|
|
248
|
-
### 8.
|
|
83
|
+
### 8.4 Surplus manipulation by pure donation is economically self-defeating
|
|
249
84
|
|
|
250
|
-
|
|
85
|
+
The model assumes that attempts to inflate surplus through donations are not profitable once the surrounding bonding-curve math is considered.
|
|
251
86
|
|
|
252
|
-
### 8.
|
|
87
|
+
### 8.5 Omnichain terminal expansion inherits remote-chain trust
|
|
253
88
|
|
|
254
|
-
|
|
89
|
+
A project that expands to a new chain can register additional terminals on that chain. Because borrowability calculations aggregate surplus from all registered terminals across all chains, a compromised or misconfigured terminal on a remote chain can corrupt the project's surplus accounting globally. This is accepted because revnet terminals are set and fixed on deploy for the initial chain, but expansion to new chains inherently requires trust in the new chain's terminal infrastructure, bridge integrity, and deployment configuration. Project operators should treat each chain expansion as a trust-boundary decision.
|
package/SKILLS.md
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
## Use This File For
|
|
4
4
|
|
|
5
|
-
- Use this file when the task involves revnet deployment, staged issuance, split
|
|
6
|
-
- Start here, then decide whether the issue is really in `REVDeployer
|
|
5
|
+
- Use this file when the task involves revnet deployment, staged issuance, split-operator logic, auto-issuance, hidden tokens, or the revnet loan system.
|
|
6
|
+
- Start here, then decide whether the issue is really in `REVDeployer`, `REVOwner`, `REVLoans`, or `REVHiddenTokens`.
|
|
7
7
|
|
|
8
8
|
## Read This Next
|
|
9
9
|
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
| Temporary token hiding and supply exclusion | [`src/REVHiddenTokens.sol`](./src/REVHiddenTokens.sol) |
|
|
17
17
|
| Types and helpers | [`src/structs/`](./src/structs/), [`src/interfaces/`](./src/interfaces/), [`test/helpers/`](./test/helpers/) |
|
|
18
18
|
| Lifecycle, loans, and economic proofs | [`test/REVLifecycle.t.sol`](./test/REVLifecycle.t.sol), [`test/REVLoansRegressions.t.sol`](./test/REVLoansRegressions.t.sol), [`test/REVLoans.invariants.t.sol`](./test/REVLoans.invariants.t.sol), [`test/TestLongTailEconomics.t.sol`](./test/TestLongTailEconomics.t.sol) |
|
|
19
|
-
| Stage, fee, and adversarial edge cases | [`test/TestStageTransitionBorrowable.t.sol`](./test/TestStageTransitionBorrowable.t.sol), [`test/TestSplitWeightE2E.t.sol`](./test/TestSplitWeightE2E.t.sol), [`test/TestLoansCashOutDelay.t.sol`](./test/TestLoansCashOutDelay.t.sol), [`test/REVLoansAttacks.t.sol`](./test/REVLoansAttacks.t.sol), [`test/TestFlashLoanSurplus.t.sol`](./test/TestFlashLoanSurplus.t.sol) |
|
|
20
19
|
|
|
21
20
|
## Repo Map
|
|
22
21
|
|
|
@@ -29,25 +28,19 @@
|
|
|
29
28
|
|
|
30
29
|
## Purpose
|
|
31
30
|
|
|
32
|
-
Deploy and manage Revnets
|
|
31
|
+
Deploy and manage Revnets: autonomous Juicebox project shapes with staged issuance schedules, optional buyback pools, cross-chain suckers, hidden-token mechanics, and token-collateralized lending.
|
|
33
32
|
|
|
34
33
|
## Reference Files
|
|
35
34
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
| Contract roles, deploy/runtime entrypoints, integration points, or key structs | [`references/runtime.md`](./references/runtime.md) |
|
|
39
|
-
| Events, errors, constants, storage, gotchas, state-reading recipes, or examples | [`references/operations.md`](./references/operations.md) |
|
|
35
|
+
- Open [`references/runtime.md`](./references/runtime.md) for contract roles, deploy/runtime entrypoints, integration points, and key structs.
|
|
36
|
+
- Open [`references/operations.md`](./references/operations.md) for events, errors, constants, storage, gotchas, and state-reading recipes.
|
|
40
37
|
|
|
41
38
|
## Working Rules
|
|
42
39
|
|
|
43
|
-
- Start in `REVDeployer` for launch-time behavior, `REVOwner` for runtime hook behavior, `REVLoans` for
|
|
44
|
-
- Revnets are intentionally ownerless after deployment. Treat any
|
|
45
|
-
- `REVOwner` is not a minor helper; it is
|
|
46
|
-
- `REVOwner.beforePayRecordedWith(...)` intentionally composes 721 split specs before buyback routing and then rescales weight to the amount that actually enters the project. Treat that ordering as an invariant.
|
|
40
|
+
- Start in `REVDeployer` for launch-time behavior, `REVOwner` for runtime hook behavior, `REVLoans` for debt accounting, and `REVHiddenTokens` for supply exclusion.
|
|
41
|
+
- Revnets are intentionally ownerless after deployment. Treat any admin-recovery instinct as suspect unless the code proves it.
|
|
42
|
+
- `REVOwner` is not a minor helper; it is a live runtime policy surface.
|
|
47
43
|
- Loan collateral is burned and re-minted, not escrowed. Any change that assumes escrow semantics is likely wrong.
|
|
48
44
|
- Cash-out delay is enforced in both runtime cash-outs and loan borrowing. If one path changes without the other, the protection is broken.
|
|
49
|
-
- Hidden tokens are supply exclusion, not a side balance.
|
|
50
|
-
-
|
|
51
|
-
- Loan behavior, stage transitions, and split-weight adjustments interact. Do not treat them as independent subsystems when editing economics.
|
|
52
|
-
- Cash-out delay, buyback defaults, and sucker deployment rules all exist to protect cross-chain and launch-time economics. They are not optional configuration sugar.
|
|
53
|
-
- For anything cross-chain or stage-related, check both the deployer path and the reading-state reference before editing.
|
|
45
|
+
- Hidden tokens are supply exclusion, not a side balance.
|
|
46
|
+
- Loan behavior, stage transitions, hidden supply, and split-weight adjustments interact. Do not treat them as independent subsystems.
|