@rev-net/core-v6 0.0.12 → 0.0.14
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/AUDIT_INSTRUCTIONS.md +295 -0
- package/CHANGE_LOG.md +321 -0
- package/README.md +2 -2
- package/RISKS.md +180 -35
- package/SKILLS.md +1 -1
- package/USER_JOURNEYS.md +489 -0
- package/package.json +9 -9
- package/script/Deploy.s.sol +40 -6
- package/script/helpers/RevnetCoreDeploymentLib.sol +7 -1
- package/src/REVDeployer.sol +63 -47
- package/src/REVLoans.sol +51 -15
- package/src/interfaces/IREVDeployer.sol +0 -1
- package/src/structs/REV721TiersHookFlags.sol +1 -0
- package/src/structs/REVAutoIssuance.sol +1 -0
- package/src/structs/REVBaseline721HookConfig.sol +1 -0
- package/src/structs/REVConfig.sol +1 -0
- package/src/structs/REVCroptopAllowedPost.sol +1 -0
- package/src/structs/REVDeploy721TiersHookConfig.sol +1 -0
- package/src/structs/REVDescription.sol +1 -0
- package/src/structs/REVLoan.sol +1 -0
- package/src/structs/REVLoanSource.sol +1 -0
- package/src/structs/REVStageConfig.sol +1 -0
- package/src/structs/REVSuckerDeploymentConfig.sol +1 -0
- package/test/REV.integrations.t.sol +132 -12
- package/test/REVAutoIssuanceFuzz.t.sol +23 -3
- package/test/REVDeployerRegressions.t.sol +35 -4
- package/test/REVInvincibility.t.sol +58 -8
- package/test/REVInvincibilityHandler.sol +29 -0
- package/test/REVLifecycle.t.sol +28 -3
- package/test/REVLoans.invariants.t.sol +52 -5
- package/test/REVLoansAttacks.t.sol +43 -5
- package/test/REVLoansFeeRecovery.t.sol +50 -11
- package/test/REVLoansFindings.t.sol +27 -3
- package/test/REVLoansRegressions.t.sol +25 -3
- package/test/REVLoansSourceFeeRecovery.t.sol +491 -0
- package/test/REVLoansSourced.t.sol +56 -7
- package/test/REVLoansUnSourced.t.sol +49 -5
- package/test/TestBurnHeldTokens.t.sol +32 -5
- package/test/TestCEIPattern.t.sol +26 -2
- package/test/TestCashOutCallerValidation.t.sol +30 -4
- package/test/TestConversionDocumentation.t.sol +26 -5
- package/test/TestCrossCurrencyReclaim.t.sol +584 -0
- package/test/TestCrossSourceReallocation.t.sol +26 -2
- package/test/TestERC2771MetaTx.t.sol +557 -0
- package/test/TestEmptyBuybackSpecs.t.sol +23 -3
- package/test/TestFlashLoanSurplus.t.sol +28 -3
- package/test/TestHookArrayOOB.t.sol +24 -4
- package/test/TestLiquidationBehavior.t.sol +26 -3
- package/test/TestLoanSourceRotation.t.sol +525 -0
- package/test/TestLongTailEconomics.t.sol +651 -0
- package/test/TestLowFindings.t.sol +65 -2
- package/test/TestMixedFixes.t.sol +28 -3
- package/test/TestPermit2Signatures.t.sol +657 -0
- package/test/TestReallocationSandwich.t.sol +384 -0
- package/test/TestRevnetRegressions.t.sol +324 -0
- package/test/TestSplitWeightAdjustment.t.sol +24 -2
- package/test/TestSplitWeightE2E.t.sol +29 -2
- package/test/TestSplitWeightFork.t.sol +46 -7
- package/test/TestStageTransitionBorrowable.t.sol +24 -2
- package/test/TestSwapTerminalPermission.t.sol +23 -3
- package/test/TestUint112Overflow.t.sol +28 -2
- package/test/TestZeroRepayment.t.sol +26 -2
- package/test/fork/ForkTestBase.sol +46 -3
- package/test/fork/TestCashOutFork.t.sol +1 -1
- package/test/fork/TestLoanBorrowFork.t.sol +1 -0
- package/test/fork/TestLoanCrossRulesetFork.t.sol +3 -1
- package/test/fork/TestLoanLiquidationFork.t.sol +1 -0
- package/test/fork/TestLoanReallocateFork.t.sol +1 -0
- package/test/fork/TestLoanRepayFork.t.sol +1 -0
- package/test/fork/TestLoanTransferFork.t.sol +133 -0
- package/test/fork/TestSplitWeightFork.t.sol +3 -0
- package/test/helpers/REVEmpty721Config.sol +1 -0
- package/test/mock/MockBuybackDataHook.sol +1 -0
- package/test/regression/TestBurnPermissionRequired.t.sol +267 -0
- package/test/regression/TestCrossRevnetLiquidation.t.sol +228 -0
- package/test/regression/TestCumulativeLoanCounter.t.sol +27 -4
- package/test/regression/TestLiquidateGapHandling.t.sol +29 -4
- package/test/regression/TestZeroPriceFeed.t.sol +396 -0
- package/deployments/revnet-core-v5/arbitrum/REVDeployer.json +0 -2821
- package/deployments/revnet-core-v5/arbitrum/REVLoans.json +0 -2260
- package/deployments/revnet-core-v5/arbitrum_sepolia/REVDeployer.json +0 -2821
- package/deployments/revnet-core-v5/arbitrum_sepolia/REVLoans.json +0 -2260
- package/deployments/revnet-core-v5/base/REVDeployer.json +0 -2825
- package/deployments/revnet-core-v5/base/REVLoans.json +0 -2264
- package/deployments/revnet-core-v5/base_sepolia/REVDeployer.json +0 -2825
- package/deployments/revnet-core-v5/base_sepolia/REVLoans.json +0 -2264
- package/deployments/revnet-core-v5/ethereum/REVDeployer.json +0 -2825
- package/deployments/revnet-core-v5/ethereum/REVLoans.json +0 -2264
- package/deployments/revnet-core-v5/optimism/REVDeployer.json +0 -2821
- package/deployments/revnet-core-v5/optimism/REVLoans.json +0 -2260
- package/deployments/revnet-core-v5/optimism_sepolia/REVDeployer.json +0 -2825
- package/deployments/revnet-core-v5/optimism_sepolia/REVLoans.json +0 -2264
- package/deployments/revnet-core-v5/sepolia/REVDeployer.json +0 -2825
- package/deployments/revnet-core-v5/sepolia/REVLoans.json +0 -2264
- package/docs/book.css +0 -13
- package/docs/book.toml +0 -13
- package/docs/solidity.min.js +0 -74
- package/docs/src/README.md +0 -185
- package/docs/src/SUMMARY.md +0 -18
- package/docs/src/src/README.md +0 -7
- package/docs/src/src/REVDeployer.sol/contract.REVDeployer.md +0 -999
- package/docs/src/src/REVLoans.sol/contract.REVLoans.md +0 -1108
- package/docs/src/src/interfaces/IREVDeployer.sol/interface.IREVDeployer.md +0 -525
- package/docs/src/src/interfaces/IREVLoans.sol/interface.IREVLoans.md +0 -598
- package/docs/src/src/interfaces/README.md +0 -5
- package/docs/src/src/structs/README.md +0 -12
- package/docs/src/src/structs/REVAutoIssuance.sol/struct.REVAutoIssuance.md +0 -19
- package/docs/src/src/structs/REVBuybackHookConfig.sol/struct.REVBuybackHookConfig.md +0 -19
- package/docs/src/src/structs/REVBuybackPoolConfig.sol/struct.REVBuybackPoolConfig.md +0 -21
- package/docs/src/src/structs/REVConfig.sol/struct.REVConfig.md +0 -23
- package/docs/src/src/structs/REVCroptopAllowedPost.sol/struct.REVCroptopAllowedPost.md +0 -32
- package/docs/src/src/structs/REVDeploy721TiersHookConfig.sol/struct.REVDeploy721TiersHookConfig.md +0 -34
- package/docs/src/src/structs/REVDescription.sol/struct.REVDescription.md +0 -23
- package/docs/src/src/structs/REVLoan.sol/struct.REVLoan.md +0 -28
- package/docs/src/src/structs/REVLoanSource.sol/struct.REVLoanSource.md +0 -16
- package/docs/src/src/structs/REVStageConfig.sol/struct.REVStageConfig.md +0 -44
- package/docs/src/src/structs/REVSuckerDeploymentConfig.sol/struct.REVSuckerDeploymentConfig.md +0 -16
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
# Audit Instructions -- revnet-core-v6
|
|
2
|
+
|
|
3
|
+
You are auditing the Revnet + Loans system for Juicebox V6. Revnets are autonomous, ownerless Juicebox projects with pre-programmed multi-stage tokenomics. REVLoans enables borrowing against locked revnet tokens using the bonding curve as the sole collateral valuation mechanism.
|
|
4
|
+
|
|
5
|
+
Read [RISKS.md](./RISKS.md) for the trust model and known risks. Read [ARCHITECTURE.md](./ARCHITECTURE.md) for the system overview. Read [SKILLS.md](./SKILLS.md) for the complete function reference. Then come back here.
|
|
6
|
+
|
|
7
|
+
## Scope
|
|
8
|
+
|
|
9
|
+
**In scope:**
|
|
10
|
+
|
|
11
|
+
| Contract | Lines | Role |
|
|
12
|
+
|----------|-------|------|
|
|
13
|
+
| `src/REVDeployer.sol` | ~1,287 | Deploys revnets. Acts as data hook and cash-out hook for all revnets. Manages stages, splits, auto-issuance, buyback hook delegation, 721 hook deployment, suckers, and split operator permissions. |
|
|
14
|
+
| `src/REVLoans.sol` | ~1,359 | Token-collateralized lending. Burns collateral on borrow, re-mints on repay. ERC-721 loan NFTs. Three-layer fee model. Permit2 integration. |
|
|
15
|
+
| `src/interfaces/` | ~525 | Interface definitions for both contracts |
|
|
16
|
+
| `src/structs/` | ~150 | All struct definitions |
|
|
17
|
+
|
|
18
|
+
**Dependencies (assumed correct, but verify integration points):**
|
|
19
|
+
- `@bananapus/core-v6` -- JBController, JBMultiTerminal, JBTerminalStore, JBTokens, JBPrices, JBRulesets
|
|
20
|
+
- `@bananapus/721-hook-v6` -- IJB721TiersHook, IJB721TiersHookDeployer
|
|
21
|
+
- `@bananapus/buyback-hook-v6` -- IJBBuybackHookRegistry
|
|
22
|
+
- `@bananapus/suckers-v6` -- IJBSuckerRegistry
|
|
23
|
+
- `@croptop/core-v6` -- CTPublisher
|
|
24
|
+
- `@openzeppelin/contracts` -- ERC721, ERC2771Context, Ownable, SafeERC20
|
|
25
|
+
- `@uniswap/permit2` -- IPermit2, IAllowanceTransfer
|
|
26
|
+
- `@prb/math` -- mulDiv
|
|
27
|
+
|
|
28
|
+
## The System in 90 Seconds
|
|
29
|
+
|
|
30
|
+
A **revnet** is a Juicebox project that nobody owns. REVDeployer deploys it, permanently holds its project NFT, and acts as the data hook for all payments and cash-outs. The revnet's economics are encoded as a sequence of **stages** that map 1:1 to Juicebox rulesets. Stages are immutable after deployment.
|
|
31
|
+
|
|
32
|
+
Each stage defines:
|
|
33
|
+
- **Initial issuance** (`initialIssuance`): tokens minted per unit of base currency
|
|
34
|
+
- **Issuance decay** (`issuanceCutFrequency` + `issuanceCutPercent`): how issuance decreases over time
|
|
35
|
+
- **Cash-out tax** (`cashOutTaxRate`): bonding curve parameter (0 = no tax, 9999 = max allowed)
|
|
36
|
+
- **Split percent** (`splitPercent`): percentage of minted tokens sent to reserved splits
|
|
37
|
+
- **Auto-issuances**: pre-configured token mints that can be claimed once per stage per beneficiary
|
|
38
|
+
|
|
39
|
+
**REVLoans** lets users borrow against their revnet tokens:
|
|
40
|
+
1. Burn tokens as collateral
|
|
41
|
+
2. Borrow up to the bonding curve cash-out value of those tokens
|
|
42
|
+
3. Pay a three-layer fee (2.5% protocol + 1% REV + 2.5%-50% source prepaid)
|
|
43
|
+
4. Receive an ERC-721 representing the loan
|
|
44
|
+
5. Repay anytime to re-mint collateral tokens
|
|
45
|
+
6. After 10 years, anyone can liquidate (collateral permanently lost)
|
|
46
|
+
|
|
47
|
+
## How Revnets Interact with Juicebox Core
|
|
48
|
+
|
|
49
|
+
Understanding this interaction is essential. REVDeployer wraps core Juicebox functions with revnet-specific logic.
|
|
50
|
+
|
|
51
|
+
### Payment Flow
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
User pays terminal
|
|
55
|
+
-> Terminal calls JBTerminalStore.recordPaymentFrom()
|
|
56
|
+
-> Store calls REVDeployer.beforePayRecordedWith() [data hook]
|
|
57
|
+
-> REVDeployer calls 721 hook's beforePayRecordedWith() for split specs
|
|
58
|
+
-> REVDeployer calls buyback hook's beforePayRecordedWith() for swap decision
|
|
59
|
+
-> REVDeployer scales weight: mulDiv(weight, projectAmount, totalAmount)
|
|
60
|
+
-> Returns merged specs: [721 hook spec, buyback hook spec]
|
|
61
|
+
-> Store records payment with modified weight
|
|
62
|
+
-> Terminal mints tokens via Controller
|
|
63
|
+
-> Terminal executes pay hook specs (721 hook first, then buyback hook)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Key insight:** The weight scaling in `beforePayRecordedWith` ensures the terminal only mints tokens proportional to the amount entering the project (excluding 721 tier split amounts). Without this scaling, payers would get token credit for the split portion too.
|
|
67
|
+
|
|
68
|
+
### Cash-Out Flow
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
User cashes out via terminal
|
|
72
|
+
-> Terminal calls JBTerminalStore.recordCashOutFor()
|
|
73
|
+
-> Store calls REVDeployer.beforeCashOutRecordedWith() [data hook]
|
|
74
|
+
-> If sucker: return 0% tax, full amount (fee exempt)
|
|
75
|
+
-> If cashOutDelay not passed: revert
|
|
76
|
+
-> If cashOutTaxRate == 0 or no fee terminal: return as-is
|
|
77
|
+
-> Otherwise: split cashOutCount into fee portion + non-fee portion
|
|
78
|
+
-> Compute reclaim for non-fee portion via bonding curve
|
|
79
|
+
-> Compute fee amount via bonding curve on remaining surplus
|
|
80
|
+
-> Return modified cashOutCount + hook spec for fee payment
|
|
81
|
+
-> Store records cash-out with modified parameters
|
|
82
|
+
-> Terminal burns tokens
|
|
83
|
+
-> Terminal transfers reclaimed amount to user
|
|
84
|
+
-> Terminal calls REVDeployer.afterCashOutRecordedWith() [cash-out hook]
|
|
85
|
+
-> REVDeployer pays fee to fee revnet terminal
|
|
86
|
+
-> On failure: returns funds to originating project
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Key insight:** The cash-out fee is computed as a two-step bonding curve calculation, not a simple percentage of the reclaimed amount. This is because burning fewer tokens (non-fee portion) changes the surplus-to-supply ratio for the fee portion.
|
|
90
|
+
|
|
91
|
+
### Loan Flow
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
Borrower calls REVLoans.borrowFrom()
|
|
95
|
+
-> Prerequisite: caller must have granted BURN_TOKENS permission to REVLoans via JBPermissions
|
|
96
|
+
-> Validate: collateral > 0, terminal registered, prepaidFeePercent in range
|
|
97
|
+
-> Generate loan ID: revnetId * 1T + loanNumber
|
|
98
|
+
-> Create loan in storage
|
|
99
|
+
-> Calculate borrowAmount via bonding curve:
|
|
100
|
+
-> totalSurplus = aggregate from all terminals
|
|
101
|
+
-> totalBorrowed = aggregate from all loan sources
|
|
102
|
+
-> borrowable = JBCashOuts.cashOutFrom(surplus + borrowed, collateral, supply + totalCollateral, taxRate)
|
|
103
|
+
-> Calculate source fee: JBFees.feeAmountFrom(borrowAmount, prepaidFeePercent)
|
|
104
|
+
-> _adjust():
|
|
105
|
+
-> Write loan.amount and loan.collateral to storage (CEI)
|
|
106
|
+
-> _addTo(): pull funds via useAllowanceOf, pay REV fee, transfer to beneficiary
|
|
107
|
+
-> _addCollateralTo(): burn collateral tokens via Controller
|
|
108
|
+
-> Pay source fee to terminal
|
|
109
|
+
-> Mint loan ERC-721 to borrower
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**Key insight:** `_borrowableAmountFrom` includes `totalBorrowed` in the surplus calculation (`surplus + totalBorrowed`) and `totalCollateral` in the supply calculation (`totalSupply + totalCollateral`). This means outstanding loans don't reduce the borrowable amount for new loans -- the virtual surplus and virtual supply are used.
|
|
113
|
+
|
|
114
|
+
## Key State Variables
|
|
115
|
+
|
|
116
|
+
### REVDeployer Storage
|
|
117
|
+
|
|
118
|
+
| Variable | Purpose | Audit Focus |
|
|
119
|
+
|----------|---------|-------------|
|
|
120
|
+
| `amountToAutoIssue[revnetId][stageId][beneficiary]` | Premint tokens per stage per beneficiary | Single-claim enforcement (zeroed before mint) |
|
|
121
|
+
| `cashOutDelayOf[revnetId]` | Timestamp when cash-outs unlock | Applied only for existing revnets deployed to new chains |
|
|
122
|
+
| `hashedEncodedConfigurationOf[revnetId]` | Config hash for cross-chain sucker validation | Gap: does NOT cover terminal configs |
|
|
123
|
+
| `tiered721HookOf[revnetId]` | 721 hook address | Set once during deploy, never changed |
|
|
124
|
+
| `_extraOperatorPermissions[revnetId]` | Custom permissions for split operator | Set during deploy based on 721 hook prevention flags |
|
|
125
|
+
|
|
126
|
+
### REVLoans Storage
|
|
127
|
+
|
|
128
|
+
| Variable | Purpose | Audit Focus |
|
|
129
|
+
|----------|---------|-------------|
|
|
130
|
+
| `_loanOf[loanId]` | Per-loan state (REVLoan struct) | Deleted on repay/liquidate; verify no stale reads |
|
|
131
|
+
| `totalCollateralOf[revnetId]` | Sum of all burned collateral for a revnet | Must match sum of active loan collaterals |
|
|
132
|
+
| `totalBorrowedFrom[revnetId][terminal][token]` | Total debt per loan source | Must match sum of active loan amounts per source |
|
|
133
|
+
| `totalLoansBorrowedFor[revnetId]` | Monotonically increasing loan counter | Used for loan ID generation; never decrements |
|
|
134
|
+
| `isLoanSourceOf[revnetId][terminal][token]` | Whether a source has been used | Only set to true, never back to false |
|
|
135
|
+
| `_loanSourcesOf[revnetId]` | Array of all loan sources | Only grows; iterated in `_totalBorrowedFrom` |
|
|
136
|
+
|
|
137
|
+
### REVLoan Struct (packed storage)
|
|
138
|
+
|
|
139
|
+
```solidity
|
|
140
|
+
struct REVLoan {
|
|
141
|
+
uint112 amount; // Borrowed amount in source token's decimals
|
|
142
|
+
uint112 collateral; // Number of revnet tokens burned as collateral
|
|
143
|
+
uint48 createdAt; // Block timestamp when loan was created
|
|
144
|
+
uint16 prepaidFeePercent; // Fee percent prepaid (25-500, out of MAX_FEE=1000)
|
|
145
|
+
uint32 prepaidDuration; // Seconds of interest-free window
|
|
146
|
+
REVLoanSource source; // (token, terminal) pair
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**Note:** `uint112` max is ~5.19e33. Amounts above this are checked in `_adjust` and revert with `REVLoans_OverflowAlert`.
|
|
151
|
+
|
|
152
|
+
## Priority Audit Areas
|
|
153
|
+
|
|
154
|
+
Audit in this order. Earlier items have higher blast radius:
|
|
155
|
+
|
|
156
|
+
### 1. Loan collateral valuation and manipulation
|
|
157
|
+
|
|
158
|
+
The bonding curve is the sole collateral oracle. Verify:
|
|
159
|
+
|
|
160
|
+
- `_borrowableAmountFrom` correctly aggregates surplus across all terminals
|
|
161
|
+
- `totalBorrowed` and `totalCollateral` adjustments in the virtual surplus/supply calculation are correct
|
|
162
|
+
- Stage transitions don't allow arbitrage (borrow under old tax rate, benefit from new rate)
|
|
163
|
+
- Rounding in `JBCashOuts.cashOutFrom` doesn't favor the borrower
|
|
164
|
+
- Cross-currency aggregation in `_totalBorrowedFrom` handles decimal normalization correctly
|
|
165
|
+
- Price feed failures (zero price) are handled gracefully (sources skipped, not reverted)
|
|
166
|
+
|
|
167
|
+
### 2. CEI pattern in loan operations
|
|
168
|
+
|
|
169
|
+
No reentrancy guard. Verify the CEI ordering in:
|
|
170
|
+
|
|
171
|
+
- `_adjust`: writes `loan.amount` and `loan.collateral` before `_addTo` / `_removeFrom` / `_addCollateralTo` / `_returnCollateralFrom`
|
|
172
|
+
- `borrowFrom`: `_adjust` before `_mint` (ERC-721 onReceived callback)
|
|
173
|
+
- `repayLoan`: `_burn` before `_adjust` before `_mint` (for partial repay)
|
|
174
|
+
- `reallocateCollateralFromLoan`: `_reallocateCollateralFromLoan` before `borrowFrom` -- two full loan operations in sequence
|
|
175
|
+
- `liquidateExpiredLoansFrom`: `_burn` and `delete` before storage updates
|
|
176
|
+
|
|
177
|
+
**Specific concern:** In `reallocateCollateralFromLoan`, the reallocation creates a new loan NFT and then `borrowFrom` creates another. Between these two operations, tokens are minted back to the caller (returned collateral) which are then immediately burned (new loan collateral). If `borrowFrom` triggers an external callback (via pay hooks or the ERC-721 mint), can the caller manipulate state between the two operations?
|
|
178
|
+
|
|
179
|
+
### 3. Data hook composition
|
|
180
|
+
|
|
181
|
+
REVDeployer proxies between the terminal and two hooks. Verify:
|
|
182
|
+
|
|
183
|
+
- The 721 hook's `beforePayRecordedWith` is called with the full context, but the buyback hook's is called with a reduced amount. Is this always correct?
|
|
184
|
+
- When the 721 hook returns specs with `amount >= context.amount.value`, `projectAmount` is 0 and weight is 0. This means no tokens are minted by the terminal (all funds go to 721 splits). Verify this is safe -- does the buyback hook handle a zero-amount context gracefully?
|
|
185
|
+
- The `hookSpecifications` array sizing assumes at most one spec from each hook. Verify neither hook can return multiple specs.
|
|
186
|
+
- The weight scaling `mulDiv(weight, projectAmount, context.amount.value)` -- can this produce a weight of 0 when it shouldn't, or a weight > 0 when it should be 0?
|
|
187
|
+
|
|
188
|
+
### 4. Cash-out fee calculation
|
|
189
|
+
|
|
190
|
+
The two-step bonding curve fee calculation in `beforeCashOutRecordedWith`:
|
|
191
|
+
|
|
192
|
+
```solidity
|
|
193
|
+
feeCashOutCount = mulDiv(cashOutCount, FEE, MAX_FEE) // 2.5% of tokens
|
|
194
|
+
nonFeeCashOutCount = cashOutCount - feeCashOutCount
|
|
195
|
+
|
|
196
|
+
postFeeReclaimedAmount = JBCashOuts.cashOutFrom(surplus, nonFeeCashOutCount, totalSupply, taxRate)
|
|
197
|
+
feeAmount = JBCashOuts.cashOutFrom(surplus - postFeeReclaimedAmount, feeCashOutCount, totalSupply - nonFeeCashOutCount, taxRate)
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Verify:
|
|
201
|
+
- `postFeeReclaimedAmount + feeAmount <= directReclaim` (total <= what you'd get without fee splitting)
|
|
202
|
+
- Micro cash-outs (< 40 wei at 2.5%) round `feeCashOutCount` to zero, bypassing the fee. This is documented as economically insignificant. Verify.
|
|
203
|
+
- The `cashOutCount` returned to the terminal is `nonFeeCashOutCount`, but the terminal still burns the full `cashOutCount` tokens. **Wait, is this correct?** Trace through the terminal to verify how many tokens are actually burned.
|
|
204
|
+
|
|
205
|
+
### 5. Permission model
|
|
206
|
+
|
|
207
|
+
REVDeployer grants wildcard permissions during construction:
|
|
208
|
+
|
|
209
|
+
```solidity
|
|
210
|
+
constructor() {
|
|
211
|
+
_setPermission(SUCKER_REGISTRY, 0, MAP_SUCKER_TOKEN); // All revnets
|
|
212
|
+
_setPermission(LOANS, 0, USE_ALLOWANCE); // All revnets
|
|
213
|
+
_setPermission(BUYBACK_HOOK, 0, SET_BUYBACK_POOL); // All revnets
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
These are projectId=0 (wildcard) permissions. Verify:
|
|
218
|
+
- `JBPermissions` resolves wildcard correctly -- these grant the permission for ALL revnets owned by REVDeployer, not just project 0
|
|
219
|
+
- The LOANS contract can call `useAllowanceOf` on any revnet's terminal -- verify this is constrained by the bonding curve calculation in `borrowFrom`
|
|
220
|
+
- No other permission is granted at wildcard level
|
|
221
|
+
|
|
222
|
+
### 6. Auto-issuance timing
|
|
223
|
+
|
|
224
|
+
Stage IDs computed during deployment must match JBRulesets-assigned IDs:
|
|
225
|
+
|
|
226
|
+
```solidity
|
|
227
|
+
amountToAutoIssue[revnetId][block.timestamp + i][beneficiary] += count;
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Later claimed via:
|
|
231
|
+
```solidity
|
|
232
|
+
(JBRuleset memory ruleset,) = CONTROLLER.getRulesetOf(revnetId, stageId);
|
|
233
|
+
if (ruleset.start > block.timestamp) revert REVDeployer_StageNotStarted(stageId);
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Verify:
|
|
237
|
+
- JBRulesets assigns IDs as `latestId >= block.timestamp ? latestId + 1 : block.timestamp`. Does this produce `block.timestamp, block.timestamp+1, block.timestamp+2, ...` when all stages are queued in one transaction?
|
|
238
|
+
- What if another contract queued a ruleset for the same project in the same block? (Shouldn't be possible since REVDeployer owns the project, but verify.)
|
|
239
|
+
- `getRulesetOf` returns the ruleset by ID. If the stage hasn't started yet, `ruleset.start` is the derived start time, not the queue time. The timing guard uses `ruleset.start`, which is correct. But what if `startsAtOrAfter` is 0 for the first stage and `block.timestamp` is used? The stage starts immediately -- can auto-issuance be claimed in the same transaction as deployment?
|
|
240
|
+
|
|
241
|
+
### 7. Loan fee model
|
|
242
|
+
|
|
243
|
+
Three layers of fees on borrow:
|
|
244
|
+
|
|
245
|
+
1. **Protocol fee (2.5%)** -- charged by `useAllowanceOf` (JBMultiTerminal takes it automatically)
|
|
246
|
+
2. **REV fee (1%)** -- `JBFees.feeAmountFrom(borrowAmount, REV_PREPAID_FEE_PERCENT=10)` paid to REV revnet. Try-catch; zeroed on failure.
|
|
247
|
+
3. **Source prepaid fee (2.5%-50%)** -- `JBFees.feeAmountFrom(borrowAmount, prepaidFeePercent)` paid back to the revnet via `terminal.pay`. NOT try-catch; reverts on failure.
|
|
248
|
+
|
|
249
|
+
On repay, the source fee is time-proportional:
|
|
250
|
+
|
|
251
|
+
```solidity
|
|
252
|
+
if (timeSinceLoanCreated <= prepaidDuration) return 0; // Free window
|
|
253
|
+
// After prepaid window: linear accrual
|
|
254
|
+
fullSourceFeeAmount = JBFees.feeAmountFrom(
|
|
255
|
+
loan.amount - prepaid,
|
|
256
|
+
mulDiv(timeSinceLoanCreated - prepaidDuration, MAX_FEE, LOAN_LIQUIDATION_DURATION - prepaidDuration)
|
|
257
|
+
);
|
|
258
|
+
sourceFeeAmount = mulDiv(fullSourceFeeAmount, amount, loan.amount);
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Verify:
|
|
262
|
+
- The `prepaidDuration` calculation: `mulDiv(prepaidFeePercent, LOAN_LIQUIDATION_DURATION, MAX_PREPAID_FEE_PERCENT)`. At 2.5% (25), this is `25 * 3650 days / 500 = 182.5 days`. At 50% (500), it's `500 * 3650 days / 500 = 3650 days` (full duration). Is this the intended mapping?
|
|
263
|
+
- The linear accrual formula: at `timeSinceLoanCreated = LOAN_LIQUIDATION_DURATION`, the fee percent approaches MAX_FEE (100%). Is this correct? The borrower would owe the full remaining loan amount as a fee, making repayment impossible.
|
|
264
|
+
- Actually, at liquidation time, `_determineSourceFeeAmount` reverts with `REVLoans_LoanExpired`. So the fee approaches but never reaches 100%. Verify the revert boundary is correct: `>=` vs `>`.
|
|
265
|
+
|
|
266
|
+
## How to Run Tests
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
cd revnet-core-v6
|
|
270
|
+
npm install
|
|
271
|
+
forge build
|
|
272
|
+
forge test
|
|
273
|
+
|
|
274
|
+
# Run with verbosity for debugging
|
|
275
|
+
forge test -vvvv --match-test testName
|
|
276
|
+
|
|
277
|
+
# Write a PoC
|
|
278
|
+
forge test --match-path test/audit/ExploitPoC.t.sol -vvv
|
|
279
|
+
|
|
280
|
+
# Gas analysis
|
|
281
|
+
forge test --gas-report
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## Anti-Patterns to Hunt
|
|
285
|
+
|
|
286
|
+
| Pattern | Where | Why |
|
|
287
|
+
|---------|-------|-----|
|
|
288
|
+
| `mulDiv` rounding direction | `beforePayRecordedWith` weight scaling, `_determineSourceFeeAmount`, `_borrowableAmountFrom` | Rounding in borrower's favor compounds over many loans |
|
|
289
|
+
| Source fee `pay` without try-catch | `_adjust` line 1086 | If source fee terminal reverts, entire borrow/repay reverts (DoS) |
|
|
290
|
+
| `delete _loanOf[loanId]` after external calls | `_repayLoan`, `_reallocateCollateralFromLoan` | Verify delete happens after all references to the loan are resolved |
|
|
291
|
+
| Loan storage read after `_adjust` mutates it | `_repayLoan` partial repay path | `_adjust` modifies `loan` via storage pointer; subsequent reads see mutated values |
|
|
292
|
+
| Unbounded loop in `_totalBorrowedFrom` | Called during every borrow operation | Gas griefing if many distinct loan sources accumulate |
|
|
293
|
+
| `uint112` truncation | `_adjust` explicit check | Verify all paths that set `loan.amount` or `loan.collateral` go through `_adjust` |
|
|
294
|
+
| Permit2 try-catch swallowing | `_acceptFundsFor` | If permit fails, fall through to regular transfer. Is the state consistent? |
|
|
295
|
+
| ERC-721 `_mint` callback | `borrowFrom`, `_repayLoan`, `_reallocateCollateralFromLoan` | `onERC721Received` can re-enter. Verify all state is settled before mint. |
|
package/CHANGE_LOG.md
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
# revnet-core-v6 Changelog (v5 -> v6)
|
|
2
|
+
|
|
3
|
+
This document describes all changes between `revnet-core` (v5, Solidity 0.8.23) and `revnet-core-v6` (v6, Solidity 0.8.26).
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Breaking Changes
|
|
8
|
+
|
|
9
|
+
### 1.1 Removed Structs
|
|
10
|
+
|
|
11
|
+
| Struct | Notes |
|
|
12
|
+
|--------|-------|
|
|
13
|
+
| `REVBuybackHookConfig` | Removed entirely. Buyback hook configuration is no longer passed by the caller. The deployer auto-configures buyback pools via an immutable `BUYBACK_HOOK` registry. |
|
|
14
|
+
| `REVBuybackPoolConfig` | Removed entirely. Was used within `REVBuybackHookConfig`. Buyback pools are now auto-initialized with default parameters. |
|
|
15
|
+
|
|
16
|
+
### 1.2 Struct Field Changes
|
|
17
|
+
|
|
18
|
+
#### REVConfig
|
|
19
|
+
|
|
20
|
+
| Change | v5 | v6 |
|
|
21
|
+
|--------|----|----|
|
|
22
|
+
| `loanSources` field | `REVLoanSource[] loanSources` | Removed |
|
|
23
|
+
| `loans` field | `address loans` | Removed |
|
|
24
|
+
|
|
25
|
+
Loan sources and the loans contract address are no longer part of the per-revnet configuration. In v6, loans are managed via a single immutable `LOANS` address on the deployer, and fund access limits for loans are derived from terminal configurations rather than explicit loan sources.
|
|
26
|
+
|
|
27
|
+
#### REVDeploy721TiersHookConfig
|
|
28
|
+
|
|
29
|
+
| Change | v5 | v6 |
|
|
30
|
+
|--------|----|----|
|
|
31
|
+
| `baseline721HookConfiguration` type | `JBDeploy721TiersHookConfig` | `REVBaseline721HookConfig` |
|
|
32
|
+
| `splitOperatorCanAdjustTiers` | `bool splitOperatorCanAdjustTiers` | Renamed to `bool preventSplitOperatorAdjustingTiers` |
|
|
33
|
+
| `splitOperatorCanUpdateMetadata` | `bool splitOperatorCanUpdateMetadata` | Renamed to `bool preventSplitOperatorUpdatingMetadata` |
|
|
34
|
+
| `splitOperatorCanMint` | `bool splitOperatorCanMint` | Renamed to `bool preventSplitOperatorMinting` |
|
|
35
|
+
| `splitOperatorCanIncreaseDiscountPercent` | `bool splitOperatorCanIncreaseDiscountPercent` | Renamed to `bool preventSplitOperatorIncreasingDiscountPercent` |
|
|
36
|
+
|
|
37
|
+
The boolean semantics are **inverted**: v5 used opt-in flags (`splitOperatorCan*`), v6 uses opt-out flags (`preventSplitOperator*`). In v6, the permissions are granted by default unless explicitly prevented.
|
|
38
|
+
|
|
39
|
+
#### REVCroptopAllowedPost
|
|
40
|
+
|
|
41
|
+
| Change | v5 | v6 |
|
|
42
|
+
|--------|----|----|
|
|
43
|
+
| `maximumSplitPercent` field | Not present | Added: `uint32 maximumSplitPercent` |
|
|
44
|
+
|
|
45
|
+
### 1.3 IREVDeployer Interface Changes
|
|
46
|
+
|
|
47
|
+
| Change | v5 | v6 |
|
|
48
|
+
|--------|----|----|
|
|
49
|
+
| `deployFor` (no 721s) return type | `returns (uint256)` | `returns (uint256, IJB721TiersHook)` |
|
|
50
|
+
| `deployFor` (no 721s) parameters | `(uint256, REVConfig, JBTerminalConfig[], REVBuybackHookConfig, REVSuckerDeploymentConfig)` | `(uint256, REVConfig, JBTerminalConfig[], REVSuckerDeploymentConfig)` |
|
|
51
|
+
| `deployWith721sFor` | `deployWith721sFor(uint256, REVConfig, JBTerminalConfig[], REVBuybackHookConfig, REVSuckerDeploymentConfig, REVDeploy721TiersHookConfig, REVCroptopAllowedPost[])` | Removed. Replaced by `deployFor` overload with 6 parameters. |
|
|
52
|
+
| `buybackHookOf` view | `buybackHookOf(uint256) returns (IJBRulesetDataHook)` | Removed. Replaced by immutable `BUYBACK_HOOK()`. |
|
|
53
|
+
| `loansOf` view | `loansOf(uint256) returns (address)` | Removed. Replaced by immutable `LOANS()`. |
|
|
54
|
+
|
|
55
|
+
### 1.4 IREVLoans Interface Changes
|
|
56
|
+
|
|
57
|
+
| Change | v5 | v6 |
|
|
58
|
+
|--------|----|----|
|
|
59
|
+
| `REVNETS` view | `REVNETS() returns (IREVDeployer)` | Removed. The loans contract no longer stores a reference to the deployer. |
|
|
60
|
+
| `numberOfLoansFor` view | `numberOfLoansFor(uint256) returns (uint256)` | Renamed to `totalLoansBorrowedFor(uint256) returns (uint256)` |
|
|
61
|
+
| `reallocateCollateralFromLoan` mutability | `external payable` | `external` (not payable) |
|
|
62
|
+
| Constructor | `constructor(IREVDeployer, uint256, address, IPermit2, address)` | `constructor(IJBController, IJBProjects, uint256, address, IPermit2, address)` |
|
|
63
|
+
|
|
64
|
+
### 1.5 Removed Errors
|
|
65
|
+
|
|
66
|
+
| Contract | v5 Error | v6 Replacement |
|
|
67
|
+
|----------|----------|----------------|
|
|
68
|
+
| `REVDeployer` | `REVDeployer_LoanSourceDoesntMatchTerminalConfigurations(address, address)` | Removed. Loan sources are no longer validated against terminal configurations. |
|
|
69
|
+
| `REVLoans` | `REVLoans_RevnetsMismatch(address, address)` | Replaced by `REVLoans_InvalidTerminal(address, uint256)`. Terminal validation replaces deployer ownership check. |
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## 2. New Features
|
|
74
|
+
|
|
75
|
+
### 2.1 New Functions
|
|
76
|
+
|
|
77
|
+
#### IREVDeployer / REVDeployer
|
|
78
|
+
|
|
79
|
+
| Function | Description |
|
|
80
|
+
|----------|-------------|
|
|
81
|
+
| `burnHeldTokensOf(uint256 revnetId)` | Burns any of the revnet's tokens held by the deployer contract. Project tokens can accumulate here from reserved token distribution when splits do not sum to 100%. |
|
|
82
|
+
| `deployFor` (4-arg overload) | Convenience overload that deploys a revnet with a default empty 721 hook. Constructs an empty 721 config internally. Returns `(uint256, IJB721TiersHook)`. |
|
|
83
|
+
| `BUYBACK_HOOK()` | Returns the immutable `IJBBuybackHookRegistry` used as a data hook to route payments through buyback pools. |
|
|
84
|
+
| `LOANS()` | Returns the immutable address of the single loan contract shared by all revnets. |
|
|
85
|
+
| `DEFAULT_BUYBACK_POOL_FEE()` | Returns the default Uniswap pool fee tier (`10_000` = 1%) for auto-configured buyback pools. |
|
|
86
|
+
| `DEFAULT_BUYBACK_TWAP_WINDOW()` | Returns the default TWAP window (`2 days`) for auto-configured buyback pools. |
|
|
87
|
+
|
|
88
|
+
### 2.2 New Events
|
|
89
|
+
|
|
90
|
+
| Contract | Event |
|
|
91
|
+
|----------|-------|
|
|
92
|
+
| `IREVDeployer` | `BurnHeldTokens(uint256 indexed revnetId, uint256 count, address caller)` |
|
|
93
|
+
|
|
94
|
+
### 2.3 New Errors
|
|
95
|
+
|
|
96
|
+
| Contract | Error |
|
|
97
|
+
|----------|-------|
|
|
98
|
+
| `REVDeployer` | `REVDeployer_NothingToBurn()` |
|
|
99
|
+
| `REVLoans` | `REVLoans_InvalidTerminal(address terminal, uint256 revnetId)` |
|
|
100
|
+
| `REVLoans` | `REVLoans_NothingToRepay()` |
|
|
101
|
+
| `REVLoans` | `REVLoans_ZeroBorrowAmount()` |
|
|
102
|
+
| `REVLoans` | `REVLoans_SourceMismatch()` |
|
|
103
|
+
| `REVLoans` | `REVLoans_LoanIdOverflow()` |
|
|
104
|
+
|
|
105
|
+
### 2.4 New Constants
|
|
106
|
+
|
|
107
|
+
| Contract | Constant | Description |
|
|
108
|
+
|----------|----------|-------------|
|
|
109
|
+
| `REVDeployer` | `DEFAULT_BUYBACK_POOL_FEE = 10_000` | Default Uniswap pool fee tier (1%) for auto-configured buyback pools. |
|
|
110
|
+
| `REVDeployer` | `DEFAULT_BUYBACK_TICK_SPACING = 200` | Default tick spacing for buyback pools, aligned with `UniV4DeploymentSplitHook.TICK_SPACING`. |
|
|
111
|
+
| `REVDeployer` | `DEFAULT_BUYBACK_TWAP_WINDOW = 2 days` | Default TWAP window for buyback pools. |
|
|
112
|
+
|
|
113
|
+
### 2.5 New Structs
|
|
114
|
+
|
|
115
|
+
| Struct | Description |
|
|
116
|
+
|--------|-------------|
|
|
117
|
+
| `REVBaseline721HookConfig` | Replaces `JBDeploy721TiersHookConfig` as the type for `REVDeploy721TiersHookConfig.baseline721HookConfiguration`. Contains `name`, `symbol`, `baseUri`, `tokenUriResolver`, `contractUri`, `tiersConfig`, `reserveBeneficiary`, and `flags` (`REV721TiersHookFlags`). |
|
|
118
|
+
| `REV721TiersHookFlags` | A subset of `JB721TiersHookFlags` that omits `issueTokensForSplits` (revnets always force it to `false`). Contains `noNewTiersWithReserves`, `noNewTiersWithVotes`, `noNewTiersWithOwnerMinting`, `preventOverspending`. |
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## 3. Event Changes
|
|
123
|
+
|
|
124
|
+
### 3.1 Added Events
|
|
125
|
+
|
|
126
|
+
See section 2.2 above.
|
|
127
|
+
|
|
128
|
+
### 3.2 Removed Events
|
|
129
|
+
|
|
130
|
+
| Contract | Event | Notes |
|
|
131
|
+
|----------|-------|-------|
|
|
132
|
+
| `IREVDeployer` | `SetAdditionalOperator(uint256 revnetId, address additionalOperator, uint256[] permissionIds, address caller)` | Removed entirely. |
|
|
133
|
+
|
|
134
|
+
### 3.3 Modified Events
|
|
135
|
+
|
|
136
|
+
| Contract | Event | Change |
|
|
137
|
+
|----------|-------|--------|
|
|
138
|
+
| `IREVDeployer` | `DeployRevnet` | Removed `REVBuybackHookConfig buybackHookConfiguration` parameter. |
|
|
139
|
+
| `IREVLoans` | `ReallocateCollateral` | Typo fix: `removedcollateralCount` (lowercase 'c') renamed to `removedCollateralCount` (uppercase 'C'). |
|
|
140
|
+
|
|
141
|
+
### 3.4 NatSpec Documentation
|
|
142
|
+
|
|
143
|
+
All events in v6 interfaces gained comprehensive NatSpec documentation (`@notice`, `@param`). This is a documentation-only change that does not affect the ABI.
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## 4. Error Changes
|
|
148
|
+
|
|
149
|
+
### 4.1 Removed Errors
|
|
150
|
+
|
|
151
|
+
| Contract | Error | Notes |
|
|
152
|
+
|----------|-------|-------|
|
|
153
|
+
| `REVDeployer` | `REVDeployer_LoanSourceDoesntMatchTerminalConfigurations(address, address)` | Loan sources removed from `REVConfig`. |
|
|
154
|
+
| `REVLoans` | `REVLoans_RevnetsMismatch(address, address)` | Replaced by terminal validation via `DIRECTORY.isTerminalOf`. |
|
|
155
|
+
|
|
156
|
+
### 4.2 New Errors
|
|
157
|
+
|
|
158
|
+
See section 2.3 above.
|
|
159
|
+
|
|
160
|
+
### 4.3 Unchanged Errors
|
|
161
|
+
|
|
162
|
+
The following errors are identical between v5 and v6:
|
|
163
|
+
|
|
164
|
+
**REVDeployer:**
|
|
165
|
+
- `REVDeployer_AutoIssuanceBeneficiaryZeroAddress()`
|
|
166
|
+
- `REVDeployer_CashOutDelayNotFinished(uint256, uint256)`
|
|
167
|
+
- `REVDeployer_CashOutsCantBeTurnedOffCompletely(uint256, uint256)`
|
|
168
|
+
- `REVDeployer_MustHaveSplits()`
|
|
169
|
+
- `REVDeployer_NothingToAutoIssue()`
|
|
170
|
+
- `REVDeployer_RulesetDoesNotAllowDeployingSuckers()`
|
|
171
|
+
- `REVDeployer_StageNotStarted(uint256)`
|
|
172
|
+
- `REVDeployer_StagesRequired()`
|
|
173
|
+
- `REVDeployer_StageTimesMustIncrease()`
|
|
174
|
+
- `REVDeployer_Unauthorized(uint256, address)`
|
|
175
|
+
|
|
176
|
+
**REVLoans:**
|
|
177
|
+
- `REVLoans_CollateralExceedsLoan(uint256, uint256)`
|
|
178
|
+
- `REVLoans_InvalidPrepaidFeePercent(uint256, uint256, uint256)`
|
|
179
|
+
- `REVLoans_LoanExpired(uint256, uint256)`
|
|
180
|
+
- `REVLoans_NewBorrowAmountGreaterThanLoanAmount(uint256, uint256)`
|
|
181
|
+
- `REVLoans_NoMsgValueAllowed()`
|
|
182
|
+
- `REVLoans_NotEnoughCollateral()`
|
|
183
|
+
- `REVLoans_OverflowAlert(uint256, uint256)`
|
|
184
|
+
- `REVLoans_OverMaxRepayBorrowAmount(uint256, uint256)`
|
|
185
|
+
- `REVLoans_PermitAllowanceNotEnough(uint256, uint256)`
|
|
186
|
+
- `REVLoans_ReallocatingMoreCollateralThanBorrowedAmountAllows(uint256, uint256)`
|
|
187
|
+
- `REVLoans_Unauthorized(address, address)`
|
|
188
|
+
- `REVLoans_UnderMinBorrowAmount(uint256, uint256)`
|
|
189
|
+
- `REVLoans_ZeroCollateralLoanIsInvalid()`
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## 5. Struct Changes
|
|
194
|
+
|
|
195
|
+
### 5.1 Removed Structs
|
|
196
|
+
|
|
197
|
+
| Struct | Notes |
|
|
198
|
+
|--------|-------|
|
|
199
|
+
| `REVBuybackHookConfig` | Buyback hook is now an immutable on the deployer; configuration is automatic. |
|
|
200
|
+
| `REVBuybackPoolConfig` | Was used within `REVBuybackHookConfig`. |
|
|
201
|
+
|
|
202
|
+
### 5.2 New Structs
|
|
203
|
+
|
|
204
|
+
| Struct | Notes |
|
|
205
|
+
|--------|-------|
|
|
206
|
+
| `REVBaseline721HookConfig` | Replaces `JBDeploy721TiersHookConfig` in `REVDeploy721TiersHookConfig`. Provides a revnet-specific 721 config that uses `REV721TiersHookFlags` instead of `JB721TiersHookFlags`, omitting `issueTokensForSplits`. |
|
|
207
|
+
| `REV721TiersHookFlags` | A subset of `JB721TiersHookFlags` without `issueTokensForSplits` (always forced to `false` for revnets). |
|
|
208
|
+
|
|
209
|
+
### 5.3 Modified Structs
|
|
210
|
+
|
|
211
|
+
| Struct | Field | v5 | v6 |
|
|
212
|
+
|--------|-------|----|----|
|
|
213
|
+
| `REVConfig` | `loanSources` | `REVLoanSource[] loanSources` | Removed |
|
|
214
|
+
| `REVConfig` | `loans` | `address loans` | Removed |
|
|
215
|
+
| `REVCroptopAllowedPost` | `maximumSplitPercent` | Not present | `uint32 maximumSplitPercent` |
|
|
216
|
+
| `REVDeploy721TiersHookConfig` | `baseline721HookConfiguration` | `JBDeploy721TiersHookConfig` | `REVBaseline721HookConfig` |
|
|
217
|
+
| `REVDeploy721TiersHookConfig` | `splitOperatorCanAdjustTiers` | `bool splitOperatorCanAdjustTiers` | Renamed/inverted: `bool preventSplitOperatorAdjustingTiers` |
|
|
218
|
+
| `REVDeploy721TiersHookConfig` | `splitOperatorCanUpdateMetadata` | `bool splitOperatorCanUpdateMetadata` | Renamed/inverted: `bool preventSplitOperatorUpdatingMetadata` |
|
|
219
|
+
| `REVDeploy721TiersHookConfig` | `splitOperatorCanMint` | `bool splitOperatorCanMint` | Renamed/inverted: `bool preventSplitOperatorMinting` |
|
|
220
|
+
| `REVDeploy721TiersHookConfig` | `splitOperatorCanIncreaseDiscountPercent` | `bool splitOperatorCanIncreaseDiscountPercent` | Renamed/inverted: `bool preventSplitOperatorIncreasingDiscountPercent` |
|
|
221
|
+
|
|
222
|
+
### 5.4 Unchanged Structs
|
|
223
|
+
|
|
224
|
+
The following structs are identical between v5 and v6 (only `forge-lint` comments added):
|
|
225
|
+
- `REVAutoIssuance`
|
|
226
|
+
- `REVDescription`
|
|
227
|
+
- `REVLoan`
|
|
228
|
+
- `REVLoanSource`
|
|
229
|
+
- `REVStageConfig`
|
|
230
|
+
- `REVSuckerDeploymentConfig`
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## 6. Implementation Changes
|
|
235
|
+
|
|
236
|
+
### 6.1 REVDeployer
|
|
237
|
+
|
|
238
|
+
| Change | Description |
|
|
239
|
+
|--------|-------------|
|
|
240
|
+
| **Solidity version** | Upgraded from `0.8.23` to `0.8.26`. |
|
|
241
|
+
| **Buyback hook architecture** | Per-revnet `buybackHookOf` mapping replaced with a single immutable `BUYBACK_HOOK` (`IJBBuybackHookRegistry`). Pools are auto-initialized for each terminal token during deployment via `_tryInitializeBuybackPoolFor`. |
|
|
242
|
+
| **Loans architecture** | Per-revnet `loansOf` mapping replaced with a single immutable `LOANS` address. The deployer grants `USE_ALLOWANCE` permission to the loans contract for all revnets in the constructor (wildcard `revnetId=0`). |
|
|
243
|
+
| **Constructor permissions** | v6 constructor grants three wildcard permissions: `MAP_SUCKER_TOKEN` to the sucker registry, `USE_ALLOWANCE` to the loans contract, and `SET_BUYBACK_POOL` to the buyback hook. v5 only granted `MAP_SUCKER_TOKEN`. |
|
|
244
|
+
| **Deploy function consolidation** | `deployFor` and `deployWith721sFor` merged into two `deployFor` overloads: a 6-arg version (with 721 config and allowed posts) and a 4-arg convenience version (auto-creates empty 721 hook). Both return `(uint256, IJB721TiersHook)`. |
|
|
245
|
+
| **Every revnet gets a 721 hook** | The 4-arg `deployFor` overload auto-deploys a default empty 721 hook with all split operator permissions granted. In v5, the simple `deployFor` did not deploy any 721 hook. |
|
|
246
|
+
| **721 permission semantics inverted** | v5 used opt-in flags (`splitOperatorCanAdjustTiers` etc.) that conditionally pushed permissions. v6 uses opt-out flags (`preventSplitOperatorAdjustingTiers` etc.) that grant permissions by default unless prevented. |
|
|
247
|
+
| **`beforePayRecordedWith` rewrite** | v5 fetched the buyback hook from `buybackHookOf[revnetId]` and the 721 hook separately, passing the 721 hook as a zero-amount `JBPayHookSpecification`. v6 queries the 721 hook first as a data hook to determine its tier split amount, reduces the payment context amount for the buyback hook query, and scales the buyback weight proportionally (`weight * projectAmount / totalAmount`) to prevent minting tokens for the split portion of payments. |
|
|
248
|
+
| **`hasMintPermissionFor` updated** | v5 checked `loansOf[revnetId]`, `buybackHookOf[revnetId]`, and suckers. v6 checks the immutable `LOANS`, the immutable `BUYBACK_HOOK`, and delegates to `BUYBACK_HOOK.hasMintPermissionFor` for buyback delegates. |
|
|
249
|
+
| **Loan fund access limits simplified** | v5 derived fund access limits from `configuration.loanSources` and validated them against terminal configurations via `_matchingCurrencyOf`. v6 derives them from all terminal configurations directly (one unlimited surplus allowance per terminal+token pair). The `_matchingCurrencyOf` helper is removed. |
|
|
250
|
+
| **`burnHeldTokensOf` added** | New function to burn any project tokens held by the deployer. Reverts with `REVDeployer_NothingToBurn` if the balance is zero. |
|
|
251
|
+
| **Split operator permissions expanded** | Default permissions increased from 6 (v5) to 9 (v6). Added `SET_BUYBACK_HOOK`, `SET_ROUTER_TERMINAL`, and `SET_TOKEN_METADATA`. |
|
|
252
|
+
| **Encoded configuration hash** | v5 included `configuration.loans` in the encoded configuration. v6 does not, since loans are no longer per-revnet. |
|
|
253
|
+
| **Deploy ordering** | v6 `_deploy721RevnetFor` deploys the revnet first via `_deployRevnetFor`, then deploys the 721 hook and sets split operator permissions. v5 deployed the 721 hook then called `_deployRevnetFor`. |
|
|
254
|
+
| **Croptop `maximumSplitPercent`** | v6 passes the new `maximumSplitPercent` field from `REVCroptopAllowedPost` to `CTAllowedPost`. |
|
|
255
|
+
| **Auto-initialized buyback pools** | During deployment, `_tryInitializeBuybackPoolFor` is called for every terminal token to set up Uniswap V4 buyback pools at a generic 1:1 `sqrtPriceX96`. Failures (e.g., pool already initialized) are silently caught via try-catch. |
|
|
256
|
+
| **Feeless beneficiary cashout routing** | `beforeCashOutRecordedWith` now checks `context.beneficiaryIsFeeless` and skips the 2.5% revnet fee when the cashout is routed by a feeless address (e.g., the router terminal routing value between projects). v5 did not have this check. The cash out tax rate still applies -- only the protocol fee is waived. |
|
|
257
|
+
|
|
258
|
+
### 6.2 REVLoans
|
|
259
|
+
|
|
260
|
+
| Change | Description |
|
|
261
|
+
|--------|-------------|
|
|
262
|
+
| **Solidity version** | Upgraded from `0.8.23` to `0.8.26`. |
|
|
263
|
+
| **Deployer dependency removed** | v5 stored `REVNETS` (`IREVDeployer`) and validated that the revnet was owned by the expected deployer via `RevnetsMismatch`. v6 does not reference the deployer at all. Validation now checks the terminal directly via `DIRECTORY.isTerminalOf`. |
|
|
264
|
+
| **Constructor refactored** | v5 accepted `IREVDeployer revnets` and derived `CONTROLLER`, `DIRECTORY`, etc. from it. v6 accepts `IJBController controller` and `IJBProjects projects` directly. |
|
|
265
|
+
| **Terminal validation** | `borrowFrom` now validates that the source terminal is registered in the directory for the revnet, reverting with `REVLoans_InvalidTerminal` if not. v5 validated deployer ownership instead. |
|
|
266
|
+
| **`numberOfLoansFor` renamed** | Renamed to `totalLoansBorrowedFor` to clarify that it is a monotonically increasing counter, not a count of active loans. |
|
|
267
|
+
| **`reallocateCollateralFromLoan` not payable** | v5 marked this function as `external payable`. v6 removes `payable` since the function only moves existing collateral between loans and does not accept new funds. |
|
|
268
|
+
| **Source mismatch check** | `reallocateCollateralFromLoan` now validates that the provided source matches the existing loan's source, reverting with `REVLoans_SourceMismatch()` if they differ. |
|
|
269
|
+
| **Zero borrow amount check** | `borrowFrom` now reverts with `REVLoans_ZeroBorrowAmount()` if the bonding curve returns zero. v5 did not have this check and would create a zero-amount loan. |
|
|
270
|
+
| **Nothing to repay check** | `repayLoan` now reverts with `REVLoans_NothingToRepay()` if both `repayBorrowAmount` and `collateralCountToReturn` are zero, preventing unbounded `totalLoansBorrowedFor` inflation. |
|
|
271
|
+
| **Liquidation loop behavior** | v5 broke out of the loop when encountering a loan with `createdAt == 0` (`break`). v6 continues iterating (`continue`), skipping gaps from repaid or previously liquidated loans. |
|
|
272
|
+
| **Liquidation cleanup** | v6 adds `delete _loanOf[loanId]` after burning a liquidated loan, clearing stale loan data for a gas refund. v5 did not clean up the loan data. |
|
|
273
|
+
| **`_totalBorrowedFrom` decimal normalization** | v6 normalizes token amounts from the source's native decimals to the target decimals before currency conversion. v5 did not perform decimal normalization, which could cause mixed-decimal arithmetic errors for tokens with non-18 decimals (e.g., USDC with 6 decimals). |
|
|
274
|
+
| **`_totalBorrowedFrom` zero-price safety** | v6 skips sources with a zero price to prevent division-by-zero panics that would DoS all loan operations. v5 did not handle this case. |
|
|
275
|
+
| **`_determineSourceFeeAmount` boundary fix** | An intermediate v6 revision used `>=` for the liquidation check, which created a 1-second window where neither repay nor liquidate could execute. This was fixed back to `>` (matching v5) so the exact boundary second is still repayable, while the liquidation path uses `<=`. |
|
|
276
|
+
| **BURN_TOKENS permission prerequisite (NatSpec only)** | `borrowFrom` documents via a `@dev` NatSpec comment that callers must first grant `BURN_TOKENS` permission to the loans contract via `JBPermissions.setPermissionsFor()`. This is required because collateral posting burns the caller's tokens through the controller. However, there is no runtime `hasPermission` check in `borrowFrom` itself -- if the permission is missing, the transaction will revert later inside `JBController.burnTokensOf`. v5 did not document this requirement. |
|
|
277
|
+
| **Cross-revnet liquidation guard** | `liquidateExpiredLoansFrom` now validates that `startingLoanId + count` does not exceed `_ONE_TRILLION`, preventing callers from overflowing into a different revnet's loan ID namespace via `_generateLoanId`. Reverts with `REVLoans_LoanIdOverflow()`. v5 did not have this bounds check. |
|
|
278
|
+
| **Source fee try-catch hardening** | The source fee payment in `_adjust` is now wrapped in a try-catch block. If the source terminal's `pay` call reverts, the ERC-20 allowance is reclaimed and the fee amount is returned to the beneficiary instead of blocking the entire loan operation. v5 called `terminal.pay` directly without error handling. |
|
|
279
|
+
| **Timestamp cast fix** | `borrowFrom` now casts `block.timestamp` to `uint48` when setting `loan.createdAt`, matching the `REVLoan.createdAt` field width. v5 used `uint40`, which would silently truncate timestamps after the year 36812. |
|
|
280
|
+
| **`ReallocateCollateral` event typo fix** | v5 used `removedcollateralCount` (lowercase 'c'). v6 fixes it to `removedCollateralCount` (uppercase 'C'). |
|
|
281
|
+
| **NatSpec documentation** | Extensive NatSpec added to all functions, views, and internal helpers. Flash loan safety analysis documented in `_borrowableAmountFrom`. |
|
|
282
|
+
|
|
283
|
+
### 6.3 Named Arguments
|
|
284
|
+
|
|
285
|
+
Throughout the codebase, function calls were updated to use named argument syntax (e.g., `foo({bar: 1, baz: 2})`) for improved readability.
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## 7. Migration Table
|
|
290
|
+
|
|
291
|
+
### Interfaces
|
|
292
|
+
|
|
293
|
+
| v5 | v6 | Notes |
|
|
294
|
+
|----|----|-------|
|
|
295
|
+
| `IREVDeployer` | `IREVDeployer` | `deployWith721sFor` removed; two `deployFor` overloads (both return `IJB721TiersHook`). `buybackHookOf` and `loansOf` removed. `BUYBACK_HOOK`, `LOANS`, `DEFAULT_BUYBACK_POOL_FEE`, `DEFAULT_BUYBACK_TWAP_WINDOW`, `burnHeldTokensOf` added. `BurnHeldTokens` event added, `SetAdditionalOperator` event removed. `DeployRevnet` event lost `buybackHookConfiguration` param. NatSpec added. |
|
|
296
|
+
| `IREVLoans` | `IREVLoans` | `REVNETS` removed. `numberOfLoansFor` renamed to `totalLoansBorrowedFor`. `reallocateCollateralFromLoan` no longer payable. Constructor takes `IJBController` + `IJBProjects` instead of `IREVDeployer`. `ReallocateCollateral` event typo fixed. NatSpec added. |
|
|
297
|
+
|
|
298
|
+
### Contracts
|
|
299
|
+
|
|
300
|
+
| v5 | v6 | Notes |
|
|
301
|
+
|----|----|-------|
|
|
302
|
+
| `REVDeployer` | `REVDeployer` | Buyback hook architecture changed from per-revnet mapping to immutable registry. Loans changed from per-revnet to single immutable. Deploy functions consolidated. Every revnet gets a 721 hook. 721 permission flags inverted. `beforePayRecordedWith` rewritten for split-aware weight scaling. `burnHeldTokensOf` added. Split operator gains 3 new default permissions (`SET_BUYBACK_HOOK`, `SET_ROUTER_TERMINAL`, `SET_TOKEN_METADATA`). Feeless beneficiary cashout routing skips fee for feeless addresses. |
|
|
303
|
+
| `REVLoans` | `REVLoans` | Deployer dependency removed. Terminal validation replaces deployer ownership check. `numberOfLoansFor` renamed. `reallocateCollateralFromLoan` not payable. Source mismatch, zero borrow, and nothing-to-repay checks added. BURN_TOKENS permission requirement documented via NatSpec (no runtime check). Cross-revnet liquidation guard prevents loan ID namespace overflow. Liquidation loop uses `continue` instead of `break`. Stale loan data cleaned up on liquidation. Decimal normalization and zero-price safety in `_totalBorrowedFrom`. Source fee payment wrapped in try-catch. Timestamp cast fixed from `uint40` to `uint48`. |
|
|
304
|
+
|
|
305
|
+
### Structs
|
|
306
|
+
|
|
307
|
+
| v5 | v6 | Notes |
|
|
308
|
+
|----|----|-------|
|
|
309
|
+
| `REVAutoIssuance` | `REVAutoIssuance` | Identical (lint comment added) |
|
|
310
|
+
| `REVBuybackHookConfig` | (removed) | Buyback hook is now an immutable on the deployer |
|
|
311
|
+
| `REVBuybackPoolConfig` | (removed) | Was used within `REVBuybackHookConfig` |
|
|
312
|
+
| (not present) | `REVBaseline721HookConfig` | New struct for revnet-specific 721 hook configuration |
|
|
313
|
+
| (not present) | `REV721TiersHookFlags` | New subset of `JB721TiersHookFlags` without `issueTokensForSplits` |
|
|
314
|
+
| `REVConfig` | `REVConfig` | Removed `loanSources` and `loans` fields |
|
|
315
|
+
| `REVCroptopAllowedPost` | `REVCroptopAllowedPost` | Added `maximumSplitPercent` field |
|
|
316
|
+
| `REVDeploy721TiersHookConfig` | `REVDeploy721TiersHookConfig` | `baseline721HookConfiguration` type changed. Boolean flags inverted from opt-in to opt-out. |
|
|
317
|
+
| `REVDescription` | `REVDescription` | Identical (lint comment added) |
|
|
318
|
+
| `REVLoan` | `REVLoan` | Identical (lint comment added) |
|
|
319
|
+
| `REVLoanSource` | `REVLoanSource` | Identical (lint comment added) |
|
|
320
|
+
| `REVStageConfig` | `REVStageConfig` | Identical (lint comment added) |
|
|
321
|
+
| `REVSuckerDeploymentConfig` | `REVSuckerDeploymentConfig` | Identical (lint comment added) |
|
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ Revnets are autonomous Juicebox projects with predetermined economic stages. Eac
|
|
|
47
47
|
|
|
48
48
|
`REVLoans` lets participants borrow against their revnet tokens. Unlike traditional lending:
|
|
49
49
|
|
|
50
|
-
- **Collateral is burned, not held.** Tokens are destroyed on borrow and re-minted on repay. This keeps the token supply accurate -- collateral tokens don't exist during the loan.
|
|
50
|
+
- **Collateral is burned, not held.** Tokens are destroyed on borrow and re-minted on repay. This keeps the token supply accurate -- collateral tokens don't exist during the loan. Callers must first grant `BURN_TOKENS` permission to the loans contract via `JBPermissions.setPermissionsFor()`.
|
|
51
51
|
- **Borrowable amount = cash-out value.** The bonding curve determines how much you can borrow for a given amount of collateral.
|
|
52
52
|
- **Prepaid fee model.** Borrowers choose a prepaid fee (2.5%-50%) that buys an interest-free window. After that window, a time-proportional source fee accrues.
|
|
53
53
|
- **Each loan is an ERC-721 NFT.** Loans can be transferred, and expired loans (10 years) can be liquidated by anyone.
|
|
@@ -183,7 +183,7 @@ Plus optional from 721 hook config: `ADJUST_721_TIERS`, `SET_721_METADATA`, `MIN
|
|
|
183
183
|
- **Loan flash-loan exposure.** `borrowableAmountFrom` reads live surplus, which can be inflated via flash loans. A borrower could temporarily inflate the treasury to borrow more than the sustained value would support.
|
|
184
184
|
- **uint112 truncation.** `REVLoan.amount` and `REVLoan.collateral` are `uint112` -- values above ~5.19e33 truncate silently.
|
|
185
185
|
- **Cash-out fee stacking.** Cash outs incur both the Juicebox terminal fee (2.5%) and the revnet cash-out fee (2.5% to fee revnet). These compound.
|
|
186
|
-
- **Auto-issuance stage
|
|
186
|
+
- **Auto-issuance stage IDs.** Stage IDs are `block.timestamp + i` during deployment. These match the Juicebox ruleset IDs because `JBRulesets` assigns IDs the same way (`latestId >= block.timestamp ? latestId + 1 : block.timestamp`), producing identical sequential IDs when all stages are queued in a single `deployFor()` call.
|
|
187
187
|
- **NATIVE_TOKEN on non-ETH chains.** Using `JBConstants.NATIVE_TOKEN` on Celo or Polygon means CELO/MATIC, not ETH. Use ERC-20 WETH instead. The matching hash does NOT catch this -- it covers economic parameters but NOT terminal configurations.
|
|
188
188
|
- **30-day cash-out delay.** When deploying an existing revnet to a new chain where the first stage has already started, a 30-day delay is imposed before cash outs are allowed, preventing cross-chain liquidity arbitrage.
|
|
189
189
|
- **Loan source array growth.** `_loanSourcesOf[revnetId]` is unbounded. If an attacker creates loans from many different terminals/tokens, the array grows without limit.
|