@rev-net/core-v6 0.0.17 → 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 +14 -10
- package/AUDIT_INSTRUCTIONS.md +40 -17
- package/CHANGE_LOG.md +87 -0
- package/README.md +10 -5
- package/RISKS.md +15 -10
- package/SKILLS.md +31 -15
- package/USER_JOURNEYS.md +16 -12
- package/foundry.toml +1 -1
- package/package.json +8 -8
- package/script/Deploy.s.sol +60 -19
- package/src/REVDeployer.sol +21 -303
- package/src/REVLoans.sol +31 -0
- package/src/REVOwner.sol +430 -0
- package/src/interfaces/IREVDeployer.sol +4 -10
- package/src/interfaces/IREVOwner.sol +10 -0
- package/src/structs/REVBaseline721HookConfig.sol +0 -2
- 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 +482 -0
- 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 -3
- package/test/TestSplitWeightFork.t.sol +16 -2
- 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 -2
- package/test/fork/TestPermit2PaymentFork.t.sol +4 -3
- package/test/helpers/REVEmpty721Config.sol +0 -1
- 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
|
|
|
@@ -47,7 +49,7 @@ Deploy and manage Revnets -- autonomous, unowned Juicebox projects with staged i
|
|
|
47
49
|
|
|
48
50
|
| Function | Permissions | What it does |
|
|
49
51
|
|----------|------------|-------------|
|
|
50
|
-
| `REVLoans.borrowFrom(revnetId, source, minBorrowAmount, collateralCount, beneficiary, prepaidFeePercent)` | Permissionless (caller must grant BURN_TOKENS to REVLoans) | Open a loan: burn collateral tokens, pull funds from revnet via `useAllowanceOf`, pay REV fee (1%) + terminal fee (2.5%), transfer remainder to beneficiary, mint loan NFT. |
|
|
52
|
+
| `REVLoans.borrowFrom(revnetId, source, minBorrowAmount, collateralCount, beneficiary, prepaidFeePercent)` | Permissionless (caller must grant BURN_TOKENS to REVLoans) | Open a loan: enforce cash-out delay if set (cross-chain deployment protection), burn collateral tokens, pull funds from revnet via `useAllowanceOf`, pay REV fee (1%) + terminal fee (2.5%), transfer remainder to beneficiary, mint loan NFT. |
|
|
51
53
|
| `REVLoans.repayLoan(loanId, maxRepayBorrowAmount, collateralCountToReturn, beneficiary, allowance)` | Loan NFT owner | Repay fully or partially. Returns funds to revnet via `addToBalanceOf`, re-mints collateral tokens, burns/replaces the loan NFT. Supports permit2 signatures. |
|
|
52
54
|
| `REVLoans.reallocateCollateralFromLoan(loanId, collateralCountToTransfer, source, minBorrowAmount, collateralCountToAdd, beneficiary, prepaidFeePercent)` | Loan NFT owner | Refinance: remove excess collateral from an existing loan and open a new loan with the freed collateral. Burns original, mints two replacements. |
|
|
53
55
|
| `REVLoans.liquidateExpiredLoansFrom(revnetId, startingLoanId, count)` | Permissionless | Clean up loans past the 10-year liquidation duration. Burns NFTs and decrements accounting totals. Collateral is permanently lost. |
|
|
@@ -57,7 +59,7 @@ Deploy and manage Revnets -- autonomous, unowned Juicebox projects with staged i
|
|
|
57
59
|
|
|
58
60
|
| Function | What it does |
|
|
59
61
|
|----------|-------------|
|
|
60
|
-
| `REVLoans.borrowableAmountFrom(revnetId, collateralCount, decimals, currency)` | Calculate how much can be borrowed for a given collateral amount. Aggregates surplus from all terminals, applies bonding curve. |
|
|
62
|
+
| `REVLoans.borrowableAmountFrom(revnetId, collateralCount, decimals, currency)` | Calculate how much can be borrowed for a given collateral amount. Returns 0 during the cash-out delay period. Aggregates surplus from all terminals, applies bonding curve. |
|
|
61
63
|
| `REVLoans.determineSourceFeeAmount(loan, amount)` | Calculate the time-proportional source fee for a loan repayment. Zero during prepaid window, linear accrual after. |
|
|
62
64
|
| `REVLoans.loanOf(loanId)` | Returns the full `REVLoan` struct for a loan. |
|
|
63
65
|
| `REVLoans.loanSourcesOf(revnetId)` | Returns all `(terminal, token)` pairs used for loans by a revnet. |
|
|
@@ -139,6 +141,7 @@ Deploy and manage Revnets -- autonomous, unowned Juicebox projects with staged i
|
|
|
139
141
|
|
|
140
142
|
| Error | When It Fires |
|
|
141
143
|
|-------|---------------|
|
|
144
|
+
| `REVLoans_CashOutDelayNotFinished(cashOutDelay, blockTimestamp)` | When borrowing during the 30-day cash-out delay period (cross-chain deployment protection). |
|
|
142
145
|
| `REVLoans_CollateralExceedsLoan(collateralToReturn, loanCollateral)` | When trying to return more collateral than the loan holds. |
|
|
143
146
|
| `REVLoans_InvalidPrepaidFeePercent(prepaidFeePercent, min, max)` | When `prepaidFeePercent` is outside the allowed range (2.5%--50%). |
|
|
144
147
|
| `REVLoans_InvalidTerminal(terminal, revnetId)` | When the specified terminal is not registered for the revnet. |
|
|
@@ -187,11 +190,17 @@ Deploy and manage Revnets -- autonomous, unowned Juicebox projects with staged i
|
|
|
187
190
|
| Mapping | Visibility | Type | Purpose |
|
|
188
191
|
|---------|-----------|------|---------|
|
|
189
192
|
| `amountToAutoIssue` | `public` | `revnetId => stageId => beneficiary => uint256` | Premint tokens per stage per beneficiary |
|
|
190
|
-
| `cashOutDelayOf` | `public` | `revnetId => uint256` | Timestamp when cash outs unlock (0 = no delay) |
|
|
191
193
|
| `hashedEncodedConfigurationOf` | `public` | `revnetId => bytes32` | Config hash for cross-chain sucker validation |
|
|
192
|
-
| `tiered721HookOf` | `public` | `revnetId => address` | Deployed 721 hook address (if any) |
|
|
193
194
|
| `_extraOperatorPermissions` | `internal` | `revnetId => uint256[]` | Custom permissions for split operator (no auto-getter) |
|
|
194
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
|
+
|
|
195
204
|
### REVLoans
|
|
196
205
|
|
|
197
206
|
| Mapping | Visibility | Type | Purpose |
|
|
@@ -212,20 +221,21 @@ Deploy and manage Revnets -- autonomous, unowned Juicebox projects with staged i
|
|
|
212
221
|
4. **Loan ID encoding.** `loanId = revnetId * 1_000_000_000_000 + loanNumber`. Each revnet supports ~1 trillion loans. Use `revnetIdOfLoanWith(loanId)` to decode.
|
|
213
222
|
5. **uint112 truncation risk.** `REVLoan.amount` and `REVLoan.collateral` are `uint112`. Values above ~5.19e33 truncate silently.
|
|
214
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.
|
|
215
|
-
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.
|
|
216
|
-
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.
|
|
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.
|
|
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.
|
|
217
226
|
9. **`cashOutTaxRate` cannot be MAX.** Must be strictly less than `MAX_CASH_OUT_TAX_RATE` (10,000). Revnets cannot fully disable cash outs.
|
|
218
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.
|
|
219
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.
|
|
220
229
|
12. **Loan source array is unbounded.** `_loanSourcesOf[revnetId]` grows without limit. No validation that a terminal is actually registered for the project.
|
|
221
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.
|
|
222
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).
|
|
223
|
-
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.
|
|
224
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.
|
|
225
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.
|
|
226
235
|
18. **Permit2 fallback.** `REVLoans` uses permit2 for ERC-20 transfers as a fallback when standard allowance is insufficient. Wrapped in try-catch.
|
|
227
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`.
|
|
228
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()`).
|
|
229
239
|
|
|
230
240
|
### NATIVE_TOKEN Accounting on Non-ETH Chains
|
|
231
241
|
|
|
@@ -300,8 +310,14 @@ Quick-reference for common read operations. All functions are `view`/`pure` and
|
|
|
300
310
|
| What | Call | Returns |
|
|
301
311
|
|------|------|---------|
|
|
302
312
|
| Config hash (cross-chain matching) | `REVDeployer.hashedEncodedConfigurationOf(revnetId)` | `bytes32` |
|
|
303
|
-
|
|
|
304
|
-
|
|
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) |
|
|
305
321
|
|
|
306
322
|
## Example Integration
|
|
307
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.
|
|
@@ -163,6 +164,7 @@ This is a standard Juicebox payment, but REVDeployer intervenes as the data hook
|
|
|
163
164
|
**Prerequisites:**
|
|
164
165
|
- Caller must hold `collateralCount` revnet ERC-20 tokens
|
|
165
166
|
- Caller must grant `BURN_TOKENS` permission to the REVLoans contract for the revnet's project ID via `JBPermissions.setPermissionsFor()`. Without this, the transaction reverts in `JBController.burnTokensOf` with `JBPermissioned_Unauthorized`.
|
|
167
|
+
- The revnet's cash-out delay must have passed (if one was set during cross-chain deployment). `borrowableAmountFrom` returns 0 and `borrowFrom` reverts with `REVLoans_CashOutDelayNotFinished` until the 30-day delay expires.
|
|
166
168
|
|
|
167
169
|
**Key parameters:**
|
|
168
170
|
|
|
@@ -181,6 +183,7 @@ This is a standard Juicebox payment, but REVDeployer intervenes as the data hook
|
|
|
181
183
|
- `collateralCount > 0` (no zero-collateral loans)
|
|
182
184
|
- `source.terminal` is registered for the revnet in the directory
|
|
183
185
|
- `prepaidFeePercent` in range [25, 500]
|
|
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`.
|
|
184
187
|
2. **Loan ID generation:** `revnetId * 1_000_000_000_000 + (++totalLoansBorrowedFor[revnetId])`
|
|
185
188
|
3. **Loan creation in storage:**
|
|
186
189
|
- `source`, `createdAt = block.timestamp`, `prepaidFeePercent`, `prepaidDuration = mulDiv(prepaidFeePercent, 3650 days, 500)`
|
|
@@ -210,6 +213,7 @@ This is a standard Juicebox payment, but REVDeployer intervenes as the data hook
|
|
|
210
213
|
- `prepaidDuration` at minimum (25): `25 * 3650 days / 500 = 182.5 days`. At maximum (500): `500 * 3650 days / 500 = 3650 days`.
|
|
211
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.
|
|
212
215
|
- Loan NFT is minted to `_msgSender()`, not `beneficiary`. The caller owns the loan; the beneficiary receives the funds.
|
|
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.
|
|
213
217
|
|
|
214
218
|
---
|
|
215
219
|
|
package/foundry.toml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rev-net/core-v6",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.19",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
"artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'revnet-core-v6'"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@bananapus/721-hook-v6": "^0.0.
|
|
23
|
-
"@bananapus/buyback-hook-v6": "^0.0.
|
|
24
|
-
"@bananapus/core-v6": "^0.0.
|
|
25
|
-
"@bananapus/ownable-v6": "^0.0.
|
|
22
|
+
"@bananapus/721-hook-v6": "^0.0.22",
|
|
23
|
+
"@bananapus/buyback-hook-v6": "^0.0.22",
|
|
24
|
+
"@bananapus/core-v6": "^0.0.28",
|
|
25
|
+
"@bananapus/ownable-v6": "^0.0.15",
|
|
26
26
|
"@bananapus/permission-ids-v6": "^0.0.14",
|
|
27
|
-
"@bananapus/router-terminal-v6": "^0.0.
|
|
28
|
-
"@bananapus/suckers-v6": "^0.0.
|
|
29
|
-
"@croptop/core-v6": "^0.0.
|
|
27
|
+
"@bananapus/router-terminal-v6": "^0.0.21",
|
|
28
|
+
"@bananapus/suckers-v6": "^0.0.18",
|
|
29
|
+
"@croptop/core-v6": "^0.0.23",
|
|
30
30
|
"@openzeppelin/contracts": "^5.6.1",
|
|
31
31
|
"@uniswap/v4-core": "^1.0.2",
|
|
32
32
|
"@uniswap/v4-periphery": "^1.0.3"
|
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;
|
|
@@ -323,7 +327,6 @@ contract DeployScript is Script, Sphinx {
|
|
|
323
327
|
tiersConfig: JB721InitTiersConfig({
|
|
324
328
|
tiers: new JB721TierConfig[](0), currency: ETH_CURRENCY, decimals: 18
|
|
325
329
|
}),
|
|
326
|
-
reserveBeneficiary: address(0),
|
|
327
330
|
flags: REV721TiersHookFlags({
|
|
328
331
|
noNewTiersWithReserves: false,
|
|
329
332
|
noNewTiersWithVotes: false,
|
|
@@ -369,6 +372,8 @@ contract DeployScript is Script, Sphinx {
|
|
|
369
372
|
bool _singletonsExist;
|
|
370
373
|
// The address of the previously deployed REVLoans, if found.
|
|
371
374
|
address _existingRevloansAddr;
|
|
375
|
+
// The address of the previously deployed REVOwner, if found.
|
|
376
|
+
address _existingOwnerAddr;
|
|
372
377
|
// The address of the previously deployed REVDeployer, if found.
|
|
373
378
|
address _existingDeployerAddr;
|
|
374
379
|
|
|
@@ -392,6 +397,19 @@ contract DeployScript is Script, Sphinx {
|
|
|
392
397
|
// Flag that singletons were found.
|
|
393
398
|
_singletonsExist = true;
|
|
394
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
|
+
|
|
395
413
|
// Also predict and verify the deployer.
|
|
396
414
|
(_existingDeployerAddr,) = _isDeployed({
|
|
397
415
|
salt: DEPLOYER_SALT,
|
|
@@ -404,7 +422,8 @@ contract DeployScript is Script, Sphinx {
|
|
|
404
422
|
croptop.publisher,
|
|
405
423
|
IJBBuybackHookRegistry(address(buybackHook.registry)),
|
|
406
424
|
_candidateRevloansAddr,
|
|
407
|
-
TRUSTED_FORWARDER
|
|
425
|
+
TRUSTED_FORWARDER,
|
|
426
|
+
_existingOwnerAddr
|
|
408
427
|
)
|
|
409
428
|
});
|
|
410
429
|
// Stop searching — we found the deployed singletons.
|
|
@@ -431,7 +450,18 @@ contract DeployScript is Script, Sphinx {
|
|
|
431
450
|
trustedForwarder: TRUSTED_FORWARDER
|
|
432
451
|
});
|
|
433
452
|
|
|
434
|
-
// 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.
|
|
435
465
|
(address _deployerAddr, bool _deployerIsDeployed) = _isDeployed({
|
|
436
466
|
salt: DEPLOYER_SALT,
|
|
437
467
|
creationCode: type(REVDeployer).creationCode,
|
|
@@ -443,7 +473,8 @@ contract DeployScript is Script, Sphinx {
|
|
|
443
473
|
croptop.publisher,
|
|
444
474
|
IJBBuybackHookRegistry(address(buybackHook.registry)),
|
|
445
475
|
address(revloans),
|
|
446
|
-
TRUSTED_FORWARDER
|
|
476
|
+
TRUSTED_FORWARDER,
|
|
477
|
+
address(revOwner)
|
|
447
478
|
)
|
|
448
479
|
});
|
|
449
480
|
REVDeployer _basicDeployer = _deployerIsDeployed
|
|
@@ -456,24 +487,34 @@ contract DeployScript is Script, Sphinx {
|
|
|
456
487
|
publisher: croptop.publisher,
|
|
457
488
|
buybackHook: IJBBuybackHookRegistry(address(buybackHook.registry)),
|
|
458
489
|
loans: address(revloans),
|
|
459
|
-
trustedForwarder: TRUSTED_FORWARDER
|
|
490
|
+
trustedForwarder: TRUSTED_FORWARDER,
|
|
491
|
+
owner: address(revOwner)
|
|
460
492
|
});
|
|
461
493
|
|
|
462
|
-
//
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
FeeProjectConfig memory feeProjectConfig = getFeeProjectConfig();
|
|
494
|
+
// Link the REVOwner to the REVDeployer (can only be called once).
|
|
495
|
+
if (!_deployerIsDeployed) {
|
|
496
|
+
revOwner.initialize(IREVDeployer(address(_basicDeployer)));
|
|
497
|
+
}
|
|
467
498
|
|
|
468
|
-
//
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
499
|
+
// Only configure the fee project if singletons were freshly deployed. Re-running `deployFor` on an
|
|
500
|
+
// already-configured project would fail because the project is no longer blank.
|
|
501
|
+
if (!_singletonsExist) {
|
|
502
|
+
// Approve the basic deployer to configure the project.
|
|
503
|
+
core.projects.approve({to: address(_basicDeployer), tokenId: FEE_PROJECT_ID});
|
|
504
|
+
|
|
505
|
+
// Build the config.
|
|
506
|
+
FeeProjectConfig memory feeProjectConfig = getFeeProjectConfig();
|
|
507
|
+
|
|
508
|
+
// Configure the project.
|
|
509
|
+
_basicDeployer.deployFor({
|
|
510
|
+
revnetId: FEE_PROJECT_ID,
|
|
511
|
+
configuration: feeProjectConfig.configuration,
|
|
512
|
+
terminalConfigurations: feeProjectConfig.terminalConfigurations,
|
|
513
|
+
suckerDeploymentConfiguration: feeProjectConfig.suckerDeploymentConfiguration,
|
|
514
|
+
tiered721HookConfiguration: feeProjectConfig.tiered721HookConfiguration,
|
|
515
|
+
allowedPosts: feeProjectConfig.allowedPosts
|
|
516
|
+
});
|
|
517
|
+
}
|
|
477
518
|
}
|
|
478
519
|
|
|
479
520
|
/// @notice Check whether a contract has already been deployed at its deterministic address.
|