@rev-net/core-v6 0.0.18 → 0.0.19
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 +14 -4
- package/ARCHITECTURE.md +13 -10
- package/AUDIT_INSTRUCTIONS.md +39 -18
- package/CHANGE_LOG.md +79 -1
- package/README.md +10 -5
- package/RISKS.md +12 -11
- package/SKILLS.md +27 -12
- package/USER_JOURNEYS.md +15 -14
- package/foundry.toml +1 -1
- package/package.json +1 -1
- package/script/Deploy.s.sol +42 -4
- package/src/REVDeployer.sol +20 -305
- package/src/REVLoans.sol +24 -29
- package/src/REVOwner.sol +430 -0
- package/src/interfaces/IREVDeployer.sol +4 -10
- package/src/interfaces/IREVOwner.sol +10 -0
- package/test/REV.integrations.t.sol +14 -1
- package/test/REVAutoIssuanceFuzz.t.sol +14 -1
- package/test/REVDeployerRegressions.t.sol +17 -2
- package/test/REVInvincibility.t.sol +31 -3
- package/test/REVLifecycle.t.sol +16 -1
- package/test/REVLoans.invariants.t.sol +16 -1
- package/test/REVLoansAttacks.t.sol +16 -1
- package/test/REVLoansFeeRecovery.t.sol +16 -1
- package/test/REVLoansFindings.t.sol +16 -1
- package/test/REVLoansRegressions.t.sol +16 -1
- package/test/REVLoansSourceFeeRecovery.t.sol +16 -1
- package/test/REVLoansSourced.t.sol +16 -1
- package/test/REVLoansUnSourced.t.sol +16 -1
- package/test/TestBurnHeldTokens.t.sol +16 -1
- package/test/TestCEIPattern.t.sol +16 -1
- package/test/TestCashOutCallerValidation.t.sol +19 -4
- package/test/TestConversionDocumentation.t.sol +16 -1
- package/test/TestCrossCurrencyReclaim.t.sol +16 -1
- package/test/TestCrossSourceReallocation.t.sol +16 -1
- package/test/TestERC2771MetaTx.t.sol +16 -1
- package/test/TestEmptyBuybackSpecs.t.sol +18 -3
- package/test/TestFlashLoanSurplus.t.sol +16 -1
- package/test/TestHookArrayOOB.t.sol +17 -2
- package/test/TestLiquidationBehavior.t.sol +16 -1
- package/test/TestLoanSourceRotation.t.sol +16 -1
- package/test/TestLoansCashOutDelay.t.sol +21 -6
- package/test/TestLongTailEconomics.t.sol +16 -1
- package/test/TestLowFindings.t.sol +16 -1
- package/test/TestMixedFixes.t.sol +16 -1
- package/test/TestPermit2Signatures.t.sol +16 -1
- package/test/TestReallocationSandwich.t.sol +16 -1
- package/test/TestRevnetRegressions.t.sol +16 -1
- package/test/TestSplitWeightAdjustment.t.sol +43 -19
- package/test/TestSplitWeightE2E.t.sol +26 -2
- package/test/TestSplitWeightFork.t.sol +16 -1
- package/test/TestStageTransitionBorrowable.t.sol +16 -1
- package/test/TestSwapTerminalPermission.t.sol +16 -1
- package/test/TestUint112Overflow.t.sol +16 -1
- package/test/TestZeroRepayment.t.sol +16 -1
- package/test/audit/LoanIdOverflowGuard.t.sol +16 -1
- package/test/fork/ForkTestBase.sol +16 -1
- package/test/fork/TestPermit2PaymentFork.t.sol +4 -3
- package/test/regression/TestBurnPermissionRequired.t.sol +16 -1
- package/test/regression/TestCashOutBuybackFeeLeak.t.sol +15 -1
- package/test/regression/TestCrossRevnetLiquidation.t.sol +16 -1
- package/test/regression/TestCumulativeLoanCounter.t.sol +16 -1
- package/test/regression/TestLiquidateGapHandling.t.sol +16 -1
- package/test/regression/TestZeroPriceFeed.t.sol +16 -1
package/SKILLS.md
CHANGED
|
@@ -8,7 +8,8 @@ Deploy and manage Revnets -- autonomous, unowned Juicebox projects with staged i
|
|
|
8
8
|
|
|
9
9
|
| Contract | Role |
|
|
10
10
|
|----------|------|
|
|
11
|
-
| `REVDeployer` | Deploys revnets, permanently owns the project NFT
|
|
11
|
+
| `REVDeployer` | Deploys revnets, permanently owns the project NFT. Manages stages, splits, auto-issuance, buyback hooks, suckers, split operators, and configuration state storage. Exposes `OWNER()` view returning the REVOwner address. Calls DEPLOYER-restricted setters on REVOwner during deployment to store `cashOutDelayOf` and `tiered721HookOf`. |
|
|
12
|
+
| `REVOwner` | Runtime hook contract for all revnets. Implements `IJBRulesetDataHook` + `IJBCashOutHook`. Set as the `dataHook` in each revnet's ruleset metadata. Handles pay hooks, cash-out hooks, mint permissions, and sucker verification. Stores `cashOutDelayOf` and `tiered721HookOf` mappings (set by REVDeployer via DEPLOYER-restricted setters `setCashOutDelayOf()` and `setTiered721HookOf()`). (~310 lines) |
|
|
12
13
|
| `REVLoans` | Issues token-collateralized loans from revnet treasuries. Each loan is an ERC-721 NFT. Burns collateral on borrow, re-mints on repay. Charges tiered fees (REV protocol fee + source fee + prepaid fee). (~1,359 lines) |
|
|
13
14
|
|
|
14
15
|
## Key Functions
|
|
@@ -21,14 +22,15 @@ Deploy and manage Revnets -- autonomous, unowned Juicebox projects with staged i
|
|
|
21
22
|
| `REVDeployer.deployFor(revnetId, config, terminals, suckerConfig, hookConfig, allowedPosts)` | Permissionless | Same as `deployFor` but deploys a tiered ERC-721 hook with pre-configured tiers. Optionally configures Croptop posting criteria and grants publisher permission to add tiers. |
|
|
22
23
|
| `REVDeployer.deploySuckersFor(revnetId, suckerConfig)` | Split operator | Deploy new cross-chain suckers post-launch. Validates ruleset allows sucker deployment (bit 2 of `extraMetadata`). Uses stored config hash for cross-chain matching. |
|
|
23
24
|
|
|
24
|
-
### Data Hooks
|
|
25
|
+
### Data Hooks (REVOwner)
|
|
25
26
|
|
|
26
27
|
| Function | Permissions | What it does |
|
|
27
28
|
|----------|------------|-------------|
|
|
28
|
-
| `
|
|
29
|
-
| `
|
|
30
|
-
| `
|
|
31
|
-
| `
|
|
29
|
+
| `REVOwner.beforePayRecordedWith(context)` | Terminal callback | Calls the 721 hook first for split specs, then calls the buyback hook with a reduced amount context (payment minus split amount). Adjusts the returned weight proportionally for splits (`weight = mulDiv(weight, amount - splitAmount, amount)`) so the terminal only mints tokens for the amount entering the project. Assembles pay hook specs (721 hook specs first, then buyback spec). Reads `tiered721HookOf` from REVOwner storage. |
|
|
30
|
+
| `REVOwner.beforeCashOutRecordedWith(context)` | Terminal callback | If sucker: returns full amount with 0 tax (fee exempt). Otherwise: calculates 2.5% fee, enforces 30-day cash-out delay (reads `cashOutDelayOf` from REVOwner storage), returns modified count + fee hook spec. |
|
|
31
|
+
| `REVOwner.afterCashOutRecordedWith(context)` | Permissionless | Cash-out hook callback. Receives fee amount and pays it to the fee revnet's terminal. Falls back to returning funds if fee payment fails. |
|
|
32
|
+
| `REVOwner.hasMintPermissionFor(revnetId, ruleset, addr)` | View | Returns `true` for: loans contract, buyback hook, buyback hook delegates, or suckers. |
|
|
33
|
+
| `REVOwner.cashOutDelayOf(revnetId)` | View | Returns the cash-out delay timestamp from REVOwner storage. Exposed for REVLoans compatibility (REVLoans imports IREVOwner for this). |
|
|
32
34
|
|
|
33
35
|
### Split Operator
|
|
34
36
|
|
|
@@ -188,11 +190,17 @@ Deploy and manage Revnets -- autonomous, unowned Juicebox projects with staged i
|
|
|
188
190
|
| Mapping | Visibility | Type | Purpose |
|
|
189
191
|
|---------|-----------|------|---------|
|
|
190
192
|
| `amountToAutoIssue` | `public` | `revnetId => stageId => beneficiary => uint256` | Premint tokens per stage per beneficiary |
|
|
191
|
-
| `cashOutDelayOf` | `public` | `revnetId => uint256` | Timestamp when cash outs unlock (0 = no delay) |
|
|
192
193
|
| `hashedEncodedConfigurationOf` | `public` | `revnetId => bytes32` | Config hash for cross-chain sucker validation |
|
|
193
|
-
| `tiered721HookOf` | `public` | `revnetId => address` | Deployed 721 hook address (if any) |
|
|
194
194
|
| `_extraOperatorPermissions` | `internal` | `revnetId => uint256[]` | Custom permissions for split operator (no auto-getter) |
|
|
195
195
|
|
|
196
|
+
### REVOwner
|
|
197
|
+
|
|
198
|
+
| Mapping | Visibility | Type | Purpose |
|
|
199
|
+
|---------|-----------|------|---------|
|
|
200
|
+
| `DEPLOYER` | `public` | `address` | REVDeployer address (storage variable, set once via `initialize()`) |
|
|
201
|
+
| `cashOutDelayOf` | `public` | `revnetId => uint256` | Timestamp when cash outs unlock (0 = no delay). Set by REVDeployer via `setCashOutDelayOf()`. |
|
|
202
|
+
| `tiered721HookOf` | `public` | `revnetId => address` | Deployed 721 hook address (if any). Set by REVDeployer via `setTiered721HookOf()`. |
|
|
203
|
+
|
|
196
204
|
### REVLoans
|
|
197
205
|
|
|
198
206
|
| Mapping | Visibility | Type | Purpose |
|
|
@@ -214,19 +222,20 @@ Deploy and manage Revnets -- autonomous, unowned Juicebox projects with staged i
|
|
|
214
222
|
5. **uint112 truncation risk.** `REVLoan.amount` and `REVLoan.collateral` are `uint112`. Values above ~5.19e33 truncate silently.
|
|
215
223
|
6. **Auto-issuance stage IDs.** Computed as `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.
|
|
216
224
|
7. **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. The 2.5% fee is deducted from the TOKEN AMOUNT being cashed out, not from the reclaim value. 2.5% of the tokens are redirected to the fee revnet, which then redeems them at the bonding curve independently. The net reclaim to the caller is based on 97.5% of the tokens, not 97.5% of the computed ETH value. This is by design.
|
|
217
|
-
8. **30-day cash-out delay.** Applied when deploying an existing revnet to a new chain where the first stage has already started. Prevents cross-chain liquidity arbitrage. Enforced in both `beforeCashOutRecordedWith` (direct cash outs) and `REVLoans.borrowFrom` / `borrowableAmountFrom` (loans). The delay is
|
|
225
|
+
8. **30-day cash-out delay.** Applied when deploying an existing revnet to a new chain where the first stage has already started. Prevents cross-chain liquidity arbitrage. Enforced in both `beforeCashOutRecordedWith` (direct cash outs) and `REVLoans.borrowFrom` / `borrowableAmountFrom` (loans). The delay is stored on REVOwner (`cashOutDelayOf(revnetId)`) and set by REVDeployer during deployment via `setCashOutDelayOf()`. REVLoans imports IREVOwner (not IREVDeployer) to read it.
|
|
218
226
|
9. **`cashOutTaxRate` cannot be MAX.** Must be strictly less than `MAX_CASH_OUT_TAX_RATE` (10,000). Revnets cannot fully disable cash outs.
|
|
219
227
|
10. **Split operator is singular.** Only ONE address can be split operator at a time. The operator can replace itself via `setSplitOperatorOf` but cannot delegate or multi-sig.
|
|
220
228
|
11. **NATIVE_TOKEN on non-ETH chains.** `JBConstants.NATIVE_TOKEN` on Celo means CELO, on Polygon means MATIC -- not ETH. Use ERC-20 WETH instead. The config matching hash does NOT catch terminal configuration differences.
|
|
221
229
|
12. **Loan source array is unbounded.** `_loanSourcesOf[revnetId]` grows without limit. No validation that a terminal is actually registered for the project.
|
|
222
230
|
13. **Flash-loan surplus exposure.** `borrowableAmountFrom` reads live surplus. A flash loan can temporarily inflate the treasury to borrow more than the sustained value supports.
|
|
223
231
|
14. **Fee revnet must have terminals.** Cash-out fees and loan protocol fees are paid to `FEE_REVNET_ID`. If that project has no terminal for the token, the fee silently fails (try-catch).
|
|
224
|
-
15. **Buyback hook is immutable per deployer.** `
|
|
232
|
+
15. **Buyback hook is immutable per deployer.** `BUYBACK_HOOK` is set at construction time on both REVDeployer and REVOwner. All revnets deployed by the same deployer share the same buyback hook.
|
|
225
233
|
16. **Cross-chain config matching.** `hashedEncodedConfigurationOf` covers economic parameters (baseCurrency, stages, auto-issuances) but NOT terminal configurations, accounting contexts, or sucker token mappings. Two deployments with identical hashes can have different terminal setups.
|
|
226
234
|
17. **Loan fee model has three layers.** See Constants table for exact values: REV protocol fee, terminal fee, and prepaid source fee (borrower-chosen, buys interest-free window). After the prepaid window, source fee accrues linearly over the remaining loan duration.
|
|
227
235
|
18. **Permit2 fallback.** `REVLoans` uses permit2 for ERC-20 transfers as a fallback when standard allowance is insufficient. Wrapped in try-catch.
|
|
228
236
|
19. **39.16% cash-out tax crossover.** Below ~39% cash-out tax, cashing out is more capital-efficient than borrowing. Above ~39%, loans become more efficient because they preserve upside while providing liquidity. Based on CryptoEconLab academic research. Design implication: revnets intended for active token trading should consider this threshold when setting `cashOutTaxRate`.
|
|
229
237
|
20. **REVDeployer always deploys a 721 hook** via `HOOK_DEPLOYER.deployHookFor` — even if `baseline721HookConfiguration` has empty tiers. This is correct by design: it lets the split operator add and sell NFTs later without migration. Non-revnet projects should follow the same pattern by using `JB721TiersHookProjectDeployer.launchProjectFor` (or `JBOmnichainDeployer.launchProjectFor`) instead of bare `launchProjectFor`.
|
|
238
|
+
21. **REVOwner circular dependency.** REVOwner and REVDeployer have a circular dependency broken by a one-shot `initialize()` call. `REVOwner.DEPLOYER` is a storage variable (not immutable) set via `initialize()`. If `initialize()` is never called or called with the wrong address, all runtime hook behavior breaks. Deploy order: REVOwner first, then REVDeployer(owner=REVOwner), then REVOwner.initialize(deployer). REVOwner stores `cashOutDelayOf` and `tiered721HookOf` mappings, which are set by REVDeployer via DEPLOYER-restricted setters (`setCashOutDelayOf()`, `setTiered721HookOf()`).
|
|
230
239
|
|
|
231
240
|
### NATIVE_TOKEN Accounting on Non-ETH Chains
|
|
232
241
|
|
|
@@ -301,8 +310,14 @@ Quick-reference for common read operations. All functions are `view`/`pure` and
|
|
|
301
310
|
| What | Call | Returns |
|
|
302
311
|
|------|------|---------|
|
|
303
312
|
| Config hash (cross-chain matching) | `REVDeployer.hashedEncodedConfigurationOf(revnetId)` | `bytes32` |
|
|
304
|
-
|
|
|
305
|
-
|
|
313
|
+
| REVOwner address | `REVDeployer.OWNER()` | `address` |
|
|
314
|
+
|
|
315
|
+
### REVOwner State
|
|
316
|
+
|
|
317
|
+
| What | Call | Returns |
|
|
318
|
+
|------|------|---------|
|
|
319
|
+
| 721 hook address | `REVOwner.tiered721HookOf(revnetId)` | `IJB721TiersHook` |
|
|
320
|
+
| Cash-out delay timestamp | `REVOwner.cashOutDelayOf(revnetId)` | `uint256` (0 = no delay) |
|
|
306
321
|
|
|
307
322
|
## Example Integration
|
|
308
323
|
|
package/USER_JOURNEYS.md
CHANGED
|
@@ -29,7 +29,7 @@ Or: `REVDeployer.deployFor(revnetId=0, configuration, terminalConfigurations, su
|
|
|
29
29
|
1. `revnetId = PROJECTS.count() + 1` (next available ID)
|
|
30
30
|
2. `_makeRulesetConfigurations` converts stages to JBRulesetConfigs:
|
|
31
31
|
- Validates: at least one stage, `startsAtOrAfter` strictly increasing, `cashOutTaxRate < MAX`, splits required if `splitPercent > 0`
|
|
32
|
-
- Each stage becomes a ruleset with: duration = `issuanceCutFrequency`, weight = `initialIssuance`, weightCutPercent = `issuanceCutPercent`, data hook =
|
|
32
|
+
- Each stage becomes a ruleset with: duration = `issuanceCutFrequency`, weight = `initialIssuance`, weightCutPercent = `issuanceCutPercent`, data hook = REVOwner address
|
|
33
33
|
- Fund access limits: unlimited surplus allowance per terminal/token (for loans)
|
|
34
34
|
- Encoded configuration hash computed from economic parameters
|
|
35
35
|
- Auto-issuance amounts stored: `amountToAutoIssue[revnetId][block.timestamp + i][beneficiary] += count`
|
|
@@ -91,12 +91,13 @@ Or: `REVDeployer.deployFor(revnetId=0, configuration, terminalConfigurations, su
|
|
|
91
91
|
|
|
92
92
|
**Entry point:** `JBMultiTerminal.pay(projectId, token, amount, beneficiary, minReturnedTokens, memo, metadata)`
|
|
93
93
|
|
|
94
|
-
This is a standard Juicebox payment, but
|
|
94
|
+
This is a standard Juicebox payment, but REVOwner intervenes as the data hook.
|
|
95
95
|
|
|
96
96
|
**What happens:**
|
|
97
97
|
|
|
98
98
|
1. Terminal records payment in store
|
|
99
|
-
2. Store calls `
|
|
99
|
+
2. Store calls `REVOwner.beforePayRecordedWith(context)`:
|
|
100
|
+
- Reads `tiered721HookOf` from REVOwner storage
|
|
100
101
|
- Calls 721 hook's `beforePayRecordedWith` for split specs (tier purchases)
|
|
101
102
|
- Computes `projectAmount = context.amount.value - totalSplitAmount`
|
|
102
103
|
- Calls buyback hook's `beforePayRecordedWith` with reduced amount context
|
|
@@ -108,14 +109,14 @@ This is a standard Juicebox payment, but REVDeployer intervenes as the data hook
|
|
|
108
109
|
- 721 hook processes tier purchases
|
|
109
110
|
- Buyback hook processes swap (if applicable)
|
|
110
111
|
|
|
111
|
-
**Preview**: Call `JBMultiTerminal.previewPayFor(revnetId, token, amount, beneficiary, metadata)` to simulate the full payment including
|
|
112
|
+
**Preview**: Call `JBMultiTerminal.previewPayFor(revnetId, token, amount, beneficiary, metadata)` to simulate the full payment including REVOwner's data hook effects (buyback routing, 721 tier splits, weight adjustment). Returns the expected token count and hook specifications. When the buyback hook is active, noop specs may carry routing diagnostics (TWAP tick, liquidity, pool ID) even when the protocol mint path wins.
|
|
112
113
|
|
|
113
|
-
**Events:** No revnet-specific events. The payment is handled by `JBMultiTerminal` and `JBController` (see nana-core-v6).
|
|
114
|
+
**Events:** No revnet-specific events. The payment is handled by `JBMultiTerminal` and `JBController` (see nana-core-v6). REVOwner's `beforePayRecordedWith` is a `view` function and emits nothing.
|
|
114
115
|
|
|
115
116
|
**Edge cases:**
|
|
116
117
|
- If the buyback hook determines a DEX swap is better, weight = 0 and the buyback hook spec receives the full project amount. The buyback hook buys tokens on the DEX and mints them to the payer.
|
|
117
118
|
- If `totalSplitAmount >= context.amount.value`, `projectAmount = 0`, weight = 0, and no tokens are minted by the terminal. All funds go to 721 tier splits.
|
|
118
|
-
- If no 721 hook is set (`tiered721HookOf[revnetId] == address(0)`), only the buyback hook is consulted.
|
|
119
|
+
- If no 721 hook is set (`tiered721HookOf[revnetId] == address(0)` on REVOwner), only the buyback hook is consulted.
|
|
119
120
|
|
|
120
121
|
---
|
|
121
122
|
|
|
@@ -126,9 +127,9 @@ This is a standard Juicebox payment, but REVDeployer intervenes as the data hook
|
|
|
126
127
|
**What happens:**
|
|
127
128
|
|
|
128
129
|
1. Terminal records cash-out in store
|
|
129
|
-
2. Store calls `
|
|
130
|
+
2. Store calls `REVOwner.beforeCashOutRecordedWith(context)`:
|
|
130
131
|
- **If sucker:** Returns 0% tax, full cash-out count, no hooks (fee exempt)
|
|
131
|
-
- **If cash-out delay active:**
|
|
132
|
+
- **If cash-out delay active:** Reads `cashOutDelayOf` from REVOwner storage, reverts with `REVDeployer_CashOutDelayNotFinished`
|
|
132
133
|
- **If no tax or no fee terminal:** Returns parameters unchanged
|
|
133
134
|
- **Otherwise:** Splits cash-out into fee portion (2.5%) and non-fee portion:
|
|
134
135
|
- `feeCashOutCount = mulDiv(cashOutCount, 25, 1000)`
|
|
@@ -138,17 +139,17 @@ This is a standard Juicebox payment, but REVDeployer intervenes as the data hook
|
|
|
138
139
|
- Returns `nonFeeCashOutCount` as the adjusted cash-out count + hook spec for fee
|
|
139
140
|
3. Terminal burns ALL of the user's specified token count
|
|
140
141
|
4. Terminal transfers the reclaimed amount to the beneficiary
|
|
141
|
-
5. Terminal calls `
|
|
142
|
+
5. Terminal calls `REVOwner.afterCashOutRecordedWith(context)`:
|
|
142
143
|
- Transfers fee amount from terminal to this contract
|
|
143
144
|
- Pays fee to fee revnet's terminal via `feeTerminal.pay`
|
|
144
145
|
- On failure: returns funds to the originating project via `addToBalanceOf`
|
|
145
146
|
|
|
146
|
-
**Preview**: Call `JBMultiTerminal.previewCashOutFrom(holder, revnetId, cashOutCount, tokenToReclaim, beneficiary, metadata)` to simulate the full cash out including
|
|
147
|
+
**Preview**: Call `JBMultiTerminal.previewCashOutFrom(holder, revnetId, cashOutCount, tokenToReclaim, beneficiary, metadata)` to simulate the full cash out including REVOwner's data hook effects (fee splitting, tax rate). Returns the expected reclaim amount and hook specifications. For a simpler estimate without data hook effects, use `JBTerminalStore.currentTotalReclaimableSurplusOf(revnetId, cashOutCount, decimals, currency)`.
|
|
147
148
|
|
|
148
|
-
**Events:** No revnet-specific events. Cash-out events are emitted by `JBMultiTerminal` and `JBController`.
|
|
149
|
+
**Events:** No revnet-specific events. Cash-out events are emitted by `JBMultiTerminal` and `JBController`. REVOwner's `beforeCashOutRecordedWith` is a `view` function. The `afterCashOutRecordedWith` hook on REVOwner processes fees but does not emit events.
|
|
149
150
|
|
|
150
151
|
**Edge cases:**
|
|
151
|
-
- Suckers bypass both the cash-out fee AND the cash-out delay. The `_isSuckerOf` check is the only gate.
|
|
152
|
+
- Suckers bypass both the cash-out fee AND the cash-out delay. The `REVOwner._isSuckerOf` check is the only gate.
|
|
152
153
|
- `cashOutTaxRate == 0` means no tax and no revnet fee. The terminal's 2.5% protocol fee only applies up to the `feeFreeSurplusOf` amount (round-trip prevention), not the full reclaim.
|
|
153
154
|
- Micro cash-outs (< 40 wei at 2.5%) round `feeCashOutCount` to 0, bypassing the fee. Gas cost far exceeds the bypassed fee.
|
|
154
155
|
- The fee is paid to `FEE_REVNET_ID`, not `REV_ID`. These may be different projects.
|
|
@@ -182,7 +183,7 @@ This is a standard Juicebox payment, but REVDeployer intervenes as the data hook
|
|
|
182
183
|
- `collateralCount > 0` (no zero-collateral loans)
|
|
183
184
|
- `source.terminal` is registered for the revnet in the directory
|
|
184
185
|
- `prepaidFeePercent` in range [25, 500]
|
|
185
|
-
- Cash-out delay has passed: resolves the `
|
|
186
|
+
- Cash-out delay has passed: resolves the `REVOwner` from the current ruleset's `dataHook`, checks `IREVOwner.cashOutDelayOf(revnetId)` (stored on REVOwner). Reverts with `REVLoans_CashOutDelayNotFinished(cashOutDelay, block.timestamp)` if `cashOutDelay > block.timestamp`.
|
|
186
187
|
2. **Loan ID generation:** `revnetId * 1_000_000_000_000 + (++totalLoansBorrowedFor[revnetId])`
|
|
187
188
|
3. **Loan creation in storage:**
|
|
188
189
|
- `source`, `createdAt = block.timestamp`, `prepaidFeePercent`, `prepaidDuration = mulDiv(prepaidFeePercent, 3650 days, 500)`
|
|
@@ -212,7 +213,7 @@ This is a standard Juicebox payment, but REVDeployer intervenes as the data hook
|
|
|
212
213
|
- `prepaidDuration` at minimum (25): `25 * 3650 days / 500 = 182.5 days`. At maximum (500): `500 * 3650 days / 500 = 3650 days`.
|
|
213
214
|
- Both the REV fee payment and the source fee payment failures are non-fatal. If either `feeTerminal.pay` or `source.terminal.pay` reverts, the fee amount is transferred to the beneficiary instead.
|
|
214
215
|
- Loan NFT is minted to `_msgSender()`, not `beneficiary`. The caller owns the loan; the beneficiary receives the funds.
|
|
215
|
-
- When a revnet deploys to a new chain with `startsAtOrAfter` in the past, `REVDeployer` sets a 30-day cash-out delay. Both `borrowFrom` and `borrowableAmountFrom` enforce this delay by resolving the
|
|
216
|
+
- When a revnet deploys to a new chain with `startsAtOrAfter` in the past, `REVDeployer` sets a 30-day cash-out delay via `REVOwner.setCashOutDelayOf()`. Both `borrowFrom` and `borrowableAmountFrom` enforce this delay by resolving the REVOwner from the current ruleset's `dataHook` and checking `IREVOwner.cashOutDelayOf(revnetId)` (stored on REVOwner). This prevents cross-chain arbitrage via loans during the delay window.
|
|
216
217
|
|
|
217
218
|
---
|
|
218
219
|
|
package/foundry.toml
CHANGED
package/package.json
CHANGED
package/script/Deploy.s.sol
CHANGED
|
@@ -30,6 +30,8 @@ import {IJBBuybackHookRegistry} from "@bananapus/buyback-hook-v6/src/interfaces/
|
|
|
30
30
|
import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
|
|
31
31
|
|
|
32
32
|
import {REVDeployer} from "./../src/REVDeployer.sol";
|
|
33
|
+
import {REVOwner} from "./../src/REVOwner.sol";
|
|
34
|
+
import {IREVDeployer} from "./../src/interfaces/IREVDeployer.sol";
|
|
33
35
|
import {REVAutoIssuance} from "../src/structs/REVAutoIssuance.sol";
|
|
34
36
|
import {REVConfig} from "../src/structs/REVConfig.sol";
|
|
35
37
|
import {REVDescription} from "../src/structs/REVDescription.sol";
|
|
@@ -91,6 +93,8 @@ contract DeployScript is Script, Sphinx {
|
|
|
91
93
|
// forge-lint: disable-next-line(mixed-case-variable)
|
|
92
94
|
bytes32 REVLOANS_SALT = "_REV_LOANS_SALT_V6_";
|
|
93
95
|
// forge-lint: disable-next-line(mixed-case-variable)
|
|
96
|
+
bytes32 REVOWNER_SALT = "_REV_OWNER_SALT_V6_";
|
|
97
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
94
98
|
address LOANS_OWNER;
|
|
95
99
|
// forge-lint: disable-next-line(mixed-case-variable)
|
|
96
100
|
address OPERATOR;
|
|
@@ -368,6 +372,8 @@ contract DeployScript is Script, Sphinx {
|
|
|
368
372
|
bool _singletonsExist;
|
|
369
373
|
// The address of the previously deployed REVLoans, if found.
|
|
370
374
|
address _existingRevloansAddr;
|
|
375
|
+
// The address of the previously deployed REVOwner, if found.
|
|
376
|
+
address _existingOwnerAddr;
|
|
371
377
|
// The address of the previously deployed REVDeployer, if found.
|
|
372
378
|
address _existingDeployerAddr;
|
|
373
379
|
|
|
@@ -391,6 +397,19 @@ contract DeployScript is Script, Sphinx {
|
|
|
391
397
|
// Flag that singletons were found.
|
|
392
398
|
_singletonsExist = true;
|
|
393
399
|
|
|
400
|
+
// Also predict and verify the owner.
|
|
401
|
+
(_existingOwnerAddr,) = _isDeployed({
|
|
402
|
+
salt: REVOWNER_SALT,
|
|
403
|
+
creationCode: type(REVOwner).creationCode,
|
|
404
|
+
arguments: abi.encode(
|
|
405
|
+
IJBBuybackHookRegistry(address(buybackHook.registry)),
|
|
406
|
+
core.controller.DIRECTORY(),
|
|
407
|
+
_candidateId,
|
|
408
|
+
suckers.registry,
|
|
409
|
+
_candidateRevloansAddr
|
|
410
|
+
)
|
|
411
|
+
});
|
|
412
|
+
|
|
394
413
|
// Also predict and verify the deployer.
|
|
395
414
|
(_existingDeployerAddr,) = _isDeployed({
|
|
396
415
|
salt: DEPLOYER_SALT,
|
|
@@ -403,7 +422,8 @@ contract DeployScript is Script, Sphinx {
|
|
|
403
422
|
croptop.publisher,
|
|
404
423
|
IJBBuybackHookRegistry(address(buybackHook.registry)),
|
|
405
424
|
_candidateRevloansAddr,
|
|
406
|
-
TRUSTED_FORWARDER
|
|
425
|
+
TRUSTED_FORWARDER,
|
|
426
|
+
_existingOwnerAddr
|
|
407
427
|
)
|
|
408
428
|
});
|
|
409
429
|
// Stop searching — we found the deployed singletons.
|
|
@@ -430,7 +450,18 @@ contract DeployScript is Script, Sphinx {
|
|
|
430
450
|
trustedForwarder: TRUSTED_FORWARDER
|
|
431
451
|
});
|
|
432
452
|
|
|
433
|
-
// Deploy
|
|
453
|
+
// Deploy REVOwner — the runtime data hook that handles pay and cash out callbacks.
|
|
454
|
+
REVOwner revOwner = _singletonsExist
|
|
455
|
+
? REVOwner(_existingOwnerAddr)
|
|
456
|
+
: new REVOwner{salt: REVOWNER_SALT}({
|
|
457
|
+
buybackHook: IJBBuybackHookRegistry(address(buybackHook.registry)),
|
|
458
|
+
directory: core.controller.DIRECTORY(),
|
|
459
|
+
feeRevnetId: FEE_PROJECT_ID,
|
|
460
|
+
suckerRegistry: suckers.registry,
|
|
461
|
+
loans: address(revloans)
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
// Deploy REVDeployer with the REVLoans, buyback hook, and REVOwner addresses.
|
|
434
465
|
(address _deployerAddr, bool _deployerIsDeployed) = _isDeployed({
|
|
435
466
|
salt: DEPLOYER_SALT,
|
|
436
467
|
creationCode: type(REVDeployer).creationCode,
|
|
@@ -442,7 +473,8 @@ contract DeployScript is Script, Sphinx {
|
|
|
442
473
|
croptop.publisher,
|
|
443
474
|
IJBBuybackHookRegistry(address(buybackHook.registry)),
|
|
444
475
|
address(revloans),
|
|
445
|
-
TRUSTED_FORWARDER
|
|
476
|
+
TRUSTED_FORWARDER,
|
|
477
|
+
address(revOwner)
|
|
446
478
|
)
|
|
447
479
|
});
|
|
448
480
|
REVDeployer _basicDeployer = _deployerIsDeployed
|
|
@@ -455,9 +487,15 @@ contract DeployScript is Script, Sphinx {
|
|
|
455
487
|
publisher: croptop.publisher,
|
|
456
488
|
buybackHook: IJBBuybackHookRegistry(address(buybackHook.registry)),
|
|
457
489
|
loans: address(revloans),
|
|
458
|
-
trustedForwarder: TRUSTED_FORWARDER
|
|
490
|
+
trustedForwarder: TRUSTED_FORWARDER,
|
|
491
|
+
owner: address(revOwner)
|
|
459
492
|
});
|
|
460
493
|
|
|
494
|
+
// Link the REVOwner to the REVDeployer (can only be called once).
|
|
495
|
+
if (!_deployerIsDeployed) {
|
|
496
|
+
revOwner.initialize(IREVDeployer(address(_basicDeployer)));
|
|
497
|
+
}
|
|
498
|
+
|
|
461
499
|
// Only configure the fee project if singletons were freshly deployed. Re-running `deployFor` on an
|
|
462
500
|
// already-configured project would fail because the project is no longer blank.
|
|
463
501
|
if (!_singletonsExist) {
|