@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.
Files changed (81) hide show
  1. package/ADMINISTRATION.md +7 -7
  2. package/ARCHITECTURE.md +11 -11
  3. package/AUDIT_INSTRUCTIONS.md +295 -0
  4. package/CHANGE_LOG.md +316 -0
  5. package/README.md +9 -6
  6. package/RISKS.md +180 -35
  7. package/SKILLS.md +9 -11
  8. package/STYLE_GUIDE.md +14 -1
  9. package/USER_JOURNEYS.md +489 -0
  10. package/package.json +9 -9
  11. package/script/Deploy.s.sol +124 -40
  12. package/script/helpers/RevnetCoreDeploymentLib.sol +19 -6
  13. package/src/REVDeployer.sol +183 -175
  14. package/src/REVLoans.sol +65 -28
  15. package/src/interfaces/IREVDeployer.sol +25 -23
  16. package/src/structs/REV721TiersHookFlags.sol +1 -0
  17. package/src/structs/REVAutoIssuance.sol +1 -0
  18. package/src/structs/REVBaseline721HookConfig.sol +1 -0
  19. package/src/structs/REVConfig.sol +1 -0
  20. package/src/structs/REVCroptopAllowedPost.sol +1 -0
  21. package/src/structs/REVDeploy721TiersHookConfig.sol +13 -14
  22. package/src/structs/REVDescription.sol +1 -0
  23. package/src/structs/REVLoan.sol +1 -0
  24. package/src/structs/REVLoanSource.sol +1 -0
  25. package/src/structs/REVStageConfig.sol +1 -0
  26. package/src/structs/REVSuckerDeploymentConfig.sol +1 -0
  27. package/test/REV.integrations.t.sol +148 -19
  28. package/test/REVAutoIssuanceFuzz.t.sol +31 -6
  29. package/test/REVDeployerRegressions.t.sol +47 -9
  30. package/test/REVInvincibility.t.sol +83 -19
  31. package/test/REVInvincibilityHandler.sol +29 -0
  32. package/test/REVLifecycle.t.sol +36 -6
  33. package/test/REVLoans.invariants.t.sol +64 -10
  34. package/test/REVLoansAttacks.t.sol +54 -9
  35. package/test/REVLoansFeeRecovery.t.sol +61 -15
  36. package/test/REVLoansFindings.t.sol +42 -9
  37. package/test/REVLoansRegressions.t.sol +33 -6
  38. package/test/REVLoansSourceFeeRecovery.t.sol +491 -0
  39. package/test/REVLoansSourced.t.sol +79 -17
  40. package/test/REVLoansUnSourced.t.sol +61 -10
  41. package/test/TestBurnHeldTokens.t.sol +47 -11
  42. package/test/TestCEIPattern.t.sol +37 -6
  43. package/test/TestCashOutCallerValidation.t.sol +41 -8
  44. package/test/TestConversionDocumentation.t.sol +50 -13
  45. package/test/TestCrossCurrencyReclaim.t.sol +584 -0
  46. package/test/TestCrossSourceReallocation.t.sol +37 -6
  47. package/test/TestERC2771MetaTx.t.sol +557 -0
  48. package/test/TestEmptyBuybackSpecs.t.sol +45 -10
  49. package/test/TestFlashLoanSurplus.t.sol +39 -7
  50. package/test/TestHookArrayOOB.t.sol +42 -13
  51. package/test/TestLiquidationBehavior.t.sol +37 -7
  52. package/test/TestLoanSourceRotation.t.sol +525 -0
  53. package/test/TestLongTailEconomics.t.sol +651 -0
  54. package/test/TestLowFindings.t.sol +80 -8
  55. package/test/TestMixedFixes.t.sol +43 -9
  56. package/test/TestPermit2Signatures.t.sol +657 -0
  57. package/test/TestReallocationSandwich.t.sol +384 -0
  58. package/test/TestRevnetRegressions.t.sol +324 -0
  59. package/test/TestSplitWeightAdjustment.t.sol +52 -13
  60. package/test/TestSplitWeightE2E.t.sol +53 -18
  61. package/test/TestSplitWeightFork.t.sol +66 -21
  62. package/test/TestStageTransitionBorrowable.t.sol +38 -6
  63. package/test/TestSwapTerminalPermission.t.sol +37 -7
  64. package/test/TestUint112Overflow.t.sol +39 -6
  65. package/test/TestZeroRepayment.t.sol +37 -6
  66. package/test/fork/ForkTestBase.sol +66 -17
  67. package/test/fork/TestCashOutFork.t.sol +9 -3
  68. package/test/fork/TestLoanBorrowFork.t.sol +1 -0
  69. package/test/fork/TestLoanCrossRulesetFork.t.sol +11 -3
  70. package/test/fork/TestLoanLiquidationFork.t.sol +1 -0
  71. package/test/fork/TestLoanReallocateFork.t.sol +1 -0
  72. package/test/fork/TestLoanRepayFork.t.sol +1 -0
  73. package/test/fork/TestLoanTransferFork.t.sol +133 -0
  74. package/test/fork/TestSplitWeightFork.t.sol +3 -0
  75. package/test/helpers/REVEmpty721Config.sol +46 -0
  76. package/test/mock/MockBuybackDataHook.sol +1 -0
  77. package/test/regression/TestBurnPermissionRequired.t.sol +267 -0
  78. package/test/regression/TestCrossRevnetLiquidation.t.sol +228 -0
  79. package/test/regression/TestCumulativeLoanCounter.t.sol +38 -8
  80. package/test/regression/TestLiquidateGapHandling.t.sol +40 -8
  81. 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` | `splitOperatorCanAdjustTiers` | Add or remove ERC-721 tiers. |
66
- | `SET_721_METADATA` | `splitOperatorCanUpdateMetadata` | Update ERC-721 tier metadata. |
67
- | `MINT_721` | `splitOperatorCanMint` | Mint ERC-721s without payment from tiers with `allowOwnerMint`. |
68
- | `SET_721_DISCOUNT_PERCENT` | `splitOperatorCanIncreaseDiscountPercent` | Increase the discount percentage of a tier. |
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` / `deployWith721sFor` time)
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 locked revnet tokens (10-year max, gradual liquidation)
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 tiers if specified
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
- Lock borrower's revnet tokens as collateral
50
+ Burn borrower's revnet tokens as collateral
51
51
  → Calculate max borrow based on bonding curve value
52
- Transfer borrowed funds to borrower
53
- Create loan with 10-year max term
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 + prepay fee)
57
- Return locked collateral tokens to borrower
56
+ → Accept repayment (principal + prepaid fee)
57
+ Re-mint collateral tokens to borrower
58
58
 
59
- Liquidate → REVLoans.liquidateLoan()
60
- → After loan term, gradually release collateral
61
- Liquidation schedule spreads over time
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 — immune to external price manipulation
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. |