@rev-net/core-v6 0.0.11 → 0.0.13
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 +7 -7
- package/ARCHITECTURE.md +11 -11
- package/AUDIT_INSTRUCTIONS.md +295 -0
- package/CHANGE_LOG.md +316 -0
- package/README.md +9 -6
- package/RISKS.md +180 -35
- package/SKILLS.md +9 -11
- package/STYLE_GUIDE.md +14 -1
- package/USER_JOURNEYS.md +489 -0
- package/package.json +9 -9
- package/script/Deploy.s.sol +124 -40
- package/script/helpers/RevnetCoreDeploymentLib.sol +19 -6
- package/src/REVDeployer.sol +183 -175
- package/src/REVLoans.sol +65 -28
- package/src/interfaces/IREVDeployer.sol +25 -23
- 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 +13 -14
- 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 +148 -19
- package/test/REVAutoIssuanceFuzz.t.sol +31 -6
- package/test/REVDeployerRegressions.t.sol +47 -9
- package/test/REVInvincibility.t.sol +83 -19
- package/test/REVInvincibilityHandler.sol +29 -0
- package/test/REVLifecycle.t.sol +36 -6
- package/test/REVLoans.invariants.t.sol +64 -10
- package/test/REVLoansAttacks.t.sol +54 -9
- package/test/REVLoansFeeRecovery.t.sol +61 -15
- package/test/REVLoansFindings.t.sol +42 -9
- package/test/REVLoansRegressions.t.sol +33 -6
- package/test/REVLoansSourceFeeRecovery.t.sol +491 -0
- package/test/REVLoansSourced.t.sol +79 -17
- package/test/REVLoansUnSourced.t.sol +61 -10
- package/test/TestBurnHeldTokens.t.sol +47 -11
- package/test/TestCEIPattern.t.sol +37 -6
- package/test/TestCashOutCallerValidation.t.sol +41 -8
- package/test/TestConversionDocumentation.t.sol +50 -13
- package/test/TestCrossCurrencyReclaim.t.sol +584 -0
- package/test/TestCrossSourceReallocation.t.sol +37 -6
- package/test/TestERC2771MetaTx.t.sol +557 -0
- package/test/TestEmptyBuybackSpecs.t.sol +45 -10
- package/test/TestFlashLoanSurplus.t.sol +39 -7
- package/test/TestHookArrayOOB.t.sol +42 -13
- package/test/TestLiquidationBehavior.t.sol +37 -7
- package/test/TestLoanSourceRotation.t.sol +525 -0
- package/test/TestLongTailEconomics.t.sol +651 -0
- package/test/TestLowFindings.t.sol +80 -8
- package/test/TestMixedFixes.t.sol +43 -9
- 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 +52 -13
- package/test/TestSplitWeightE2E.t.sol +53 -18
- package/test/TestSplitWeightFork.t.sol +66 -21
- package/test/TestStageTransitionBorrowable.t.sol +38 -6
- package/test/TestSwapTerminalPermission.t.sol +37 -7
- package/test/TestUint112Overflow.t.sol +39 -6
- package/test/TestZeroRepayment.t.sol +37 -6
- package/test/fork/ForkTestBase.sol +66 -17
- package/test/fork/TestCashOutFork.t.sol +9 -3
- package/test/fork/TestLoanBorrowFork.t.sol +1 -0
- package/test/fork/TestLoanCrossRulesetFork.t.sol +11 -3
- 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 +46 -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 +38 -8
- package/test/regression/TestLiquidateGapHandling.t.sol +40 -8
- package/test/regression/TestZeroPriceFeed.t.sol +396 -0
package/ADMINISTRATION.md
CHANGED
|
@@ -35,8 +35,7 @@ Admin privileges and their scope in revnet-core-v6. Revnets are designed to be a
|
|
|
35
35
|
|
|
36
36
|
| Function | Required Role | Permission ID | What It Does |
|
|
37
37
|
|----------|--------------|---------------|-------------|
|
|
38
|
-
| `deployFor()` | Anyone (new revnet) or Juicebox project owner (existing project) | None | Deploys a new revnet or irreversibly converts an existing Juicebox project into a revnet. |
|
|
39
|
-
| `deployWith721sFor()` | Anyone (new revnet) or Juicebox project owner (existing project) | None | Same as `deployFor()` but also deploys a tiered ERC-721 hook and optional croptop posting rules. |
|
|
38
|
+
| `deployFor()` | Anyone (new revnet) or Juicebox project owner (existing project) | None | Deploys a new revnet or irreversibly converts an existing Juicebox project into a revnet. Both variants deploy a tiered ERC-721 hook: the 4-arg variant deploys a default empty hook; the 6-arg variant deploys a hook with pre-configured tiers and optional croptop posting rules. |
|
|
40
39
|
| `deploySuckersFor()` | Split Operator | Checked via `_checkIfIsSplitOperatorOf()` | Deploys new cross-chain suckers for an existing revnet. Also requires the current ruleset's `extraMetadata` bit 2 to be set (allows deploying suckers). |
|
|
41
40
|
| `setSplitOperatorOf()` | Split Operator | Checked via `_checkIfIsSplitOperatorOf()` | Replaces the current split operator with a new address. Revokes all operator permissions from the caller and grants them to the new address. |
|
|
42
41
|
| `autoIssueFor()` | Anyone | None | Mints pre-configured auto-issuance tokens for a beneficiary once the relevant stage has started. Amounts are set at deployment and can only be claimed once. |
|
|
@@ -57,15 +56,16 @@ The split operator receives the following Juicebox permission IDs, scoped to its
|
|
|
57
56
|
| `SUCKER_SAFETY` | Manage sucker safety settings (e.g., emergency hatch). |
|
|
58
57
|
| `SET_BUYBACK_HOOK` | Configure the buyback hook. |
|
|
59
58
|
| `SET_ROUTER_TERMINAL` | Set the router terminal. |
|
|
59
|
+
| `SET_TOKEN_METADATA` | Update the revnet token's name and symbol. |
|
|
60
60
|
|
|
61
61
|
Optional 721 permissions (granted only if enabled at deployment via `REVDeploy721TiersHookConfig`):
|
|
62
62
|
|
|
63
63
|
| Permission ID | Deployment Flag | What It Allows |
|
|
64
64
|
|---------------|----------------|----------------|
|
|
65
|
-
| `ADJUST_721_TIERS` | `
|
|
66
|
-
| `SET_721_METADATA` | `
|
|
67
|
-
| `MINT_721` | `
|
|
68
|
-
| `SET_721_DISCOUNT_PERCENT` | `
|
|
65
|
+
| `ADJUST_721_TIERS` | `preventSplitOperatorAdjustingTiers` | Add or remove ERC-721 tiers. Allowed unless prevented. |
|
|
66
|
+
| `SET_721_METADATA` | `preventSplitOperatorUpdatingMetadata` | Update ERC-721 tier metadata. Allowed unless prevented. |
|
|
67
|
+
| `MINT_721` | `preventSplitOperatorMinting` | Mint ERC-721s without payment from tiers with `allowOwnerMint`. Allowed unless prevented. |
|
|
68
|
+
| `SET_721_DISCOUNT_PERCENT` | `preventSplitOperatorIncreasingDiscountPercent` | Increase the discount percentage of a tier. Allowed unless prevented. |
|
|
69
69
|
|
|
70
70
|
### REVLoans
|
|
71
71
|
|
|
@@ -112,7 +112,7 @@ The `REVLoans` contract has minimal admin surface by design:
|
|
|
112
112
|
|
|
113
113
|
The following parameters are set at deployment and can never be changed:
|
|
114
114
|
|
|
115
|
-
### REVDeployer (per-revnet, set at `deployFor`
|
|
115
|
+
### REVDeployer (per-revnet, set at `deployFor` time)
|
|
116
116
|
- Stage schedule (start times, issuance rates, cut frequencies, cut percentages)
|
|
117
117
|
- Cash-out tax rates per stage
|
|
118
118
|
- Split percentages per stage
|
package/ARCHITECTURE.md
CHANGED
|
@@ -9,7 +9,7 @@ Autonomous revenue networks ("revnets") built on Juicebox V6. REVDeployer create
|
|
|
9
9
|
```
|
|
10
10
|
src/
|
|
11
11
|
├── REVDeployer.sol — Deploys revnets: stages → rulesets, data hook, buyback, suckers, 721 tiers
|
|
12
|
-
├── REVLoans.sol — Borrow against
|
|
12
|
+
├── REVLoans.sol — Borrow against burned revnet tokens (10-year max, permissionless liquidation)
|
|
13
13
|
├── interfaces/
|
|
14
14
|
│ ├── IREVDeployer.sol
|
|
15
15
|
│ └── IREVLoans.sol
|
|
@@ -28,7 +28,7 @@ Deployer → REVDeployer.deployFor()
|
|
|
28
28
|
→ Set REVDeployer as data hook (controls pay + cashout behavior)
|
|
29
29
|
→ Initialize buyback pools at 1:1 price, configure buyback hook
|
|
30
30
|
→ Deploy suckers for cross-chain operation
|
|
31
|
-
→ Deploy 721
|
|
31
|
+
→ Deploy tiered ERC-721 hook (always — empty by default, pre-configured if specified)
|
|
32
32
|
→ Compute matching hash for cross-chain deployment verification
|
|
33
33
|
```
|
|
34
34
|
|
|
@@ -47,18 +47,18 @@ Cash Out → REVDeployer.beforeCashOutRecordedWith()
|
|
|
47
47
|
### Loan Flow
|
|
48
48
|
```
|
|
49
49
|
Borrower → REVLoans.borrowFrom()
|
|
50
|
-
→
|
|
50
|
+
→ Burn borrower's revnet tokens as collateral
|
|
51
51
|
→ Calculate max borrow based on bonding curve value
|
|
52
|
-
→
|
|
53
|
-
→
|
|
52
|
+
→ Pull funds from treasury via USE_ALLOWANCE
|
|
53
|
+
→ Mint loan ERC-721 NFT to borrower
|
|
54
54
|
|
|
55
55
|
Repay → REVLoans.repayLoan()
|
|
56
|
-
→ Accept repayment (principal +
|
|
57
|
-
→
|
|
56
|
+
→ Accept repayment (principal + prepaid fee)
|
|
57
|
+
→ Re-mint collateral tokens to borrower
|
|
58
58
|
|
|
59
|
-
Liquidate → REVLoans.
|
|
60
|
-
→ After
|
|
61
|
-
→
|
|
59
|
+
Liquidate → REVLoans.liquidateExpiredLoansFrom()
|
|
60
|
+
→ After 10-year term, anyone can liquidate
|
|
61
|
+
→ Collateral permanently destroyed (was burned at borrow time)
|
|
62
62
|
```
|
|
63
63
|
|
|
64
64
|
## Extension Points
|
|
@@ -84,4 +84,4 @@ Liquidate → REVLoans.liquidateLoan()
|
|
|
84
84
|
- Stages are immutable after deployment — no owner can change ruleset parameters
|
|
85
85
|
- Matching hash ensures cross-chain deployments have identical economic parameters
|
|
86
86
|
- REVDeployer is the data hook for all revnets it deploys — centralizes behavioral control
|
|
87
|
-
- Loans use bonding curve value, not market price —
|
|
87
|
+
- Loans use bonding curve value, not market price — independent of external DEX pricing
|
|
@@ -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. |
|