@rev-net/core-v6 0.0.17 → 0.0.18
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/ARCHITECTURE.md +1 -0
- package/AUDIT_INSTRUCTIONS.md +2 -0
- package/CHANGE_LOG.md +9 -0
- package/RISKS.md +4 -0
- package/SKILLS.md +5 -4
- package/USER_JOURNEYS.md +3 -0
- package/package.json +8 -8
- package/script/Deploy.s.sol +19 -16
- package/src/REVDeployer.sol +4 -1
- package/src/REVLoans.sol +36 -0
- package/src/structs/REVBaseline721HookConfig.sol +0 -2
- package/test/TestLoansCashOutDelay.t.sol +467 -0
- package/test/TestSplitWeightE2E.t.sol +0 -1
- package/test/TestSplitWeightFork.t.sol +0 -1
- package/test/fork/ForkTestBase.sol +0 -1
- package/test/helpers/REVEmpty721Config.sol +0 -1
package/ARCHITECTURE.md
CHANGED
|
@@ -76,6 +76,7 @@ Cash Out → REVDeployer.beforeCashOutRecordedWith()
|
|
|
76
76
|
### Loan Flow
|
|
77
77
|
```
|
|
78
78
|
Borrower → REVLoans.borrowFrom()
|
|
79
|
+
→ Enforce cash-out delay if set (cross-chain deployment protection)
|
|
79
80
|
→ Burn borrower's revnet tokens as collateral
|
|
80
81
|
→ Calculate borrow amount from bonding curve value of collateral
|
|
81
82
|
→ Pull funds from treasury via USE_ALLOWANCE
|
package/AUDIT_INSTRUCTIONS.md
CHANGED
|
@@ -93,6 +93,7 @@ User cashes out via terminal
|
|
|
93
93
|
```
|
|
94
94
|
Borrower calls REVLoans.borrowFrom()
|
|
95
95
|
-> Prerequisite: caller must have granted BURN_TOKENS permission to REVLoans via JBPermissions
|
|
96
|
+
-> Enforce cash-out delay: resolve deployer from ruleset dataHook, check cashOutDelayOf(revnetId)
|
|
96
97
|
-> Validate: collateral > 0, terminal registered, prepaidFeePercent in range
|
|
97
98
|
-> Generate loan ID: revnetId * 1T + loanNumber
|
|
98
99
|
-> Create loan in storage
|
|
@@ -332,6 +333,7 @@ No prior formal audit with finding IDs has been conducted on this codebase. All
|
|
|
332
333
|
| `REVDeployer_StagesRequired` | REVDeployer | `deployFor` / `launchChainsFor` called with empty `stageConfigurations` array |
|
|
333
334
|
| `REVDeployer_StageTimesMustIncrease` | REVDeployer | Stage `startsAtOrAfter` timestamps are not strictly increasing |
|
|
334
335
|
| `REVDeployer_Unauthorized` | REVDeployer | Caller is not the split operator (for operator-gated functions) or not the project owner (for `launchChainsFor`) |
|
|
336
|
+
| `REVLoans_CashOutDelayNotFinished` | REVLoans | `borrowFrom` called during the 30-day cash-out delay period (cross-chain deployment protection) |
|
|
335
337
|
| `REVLoans_CollateralExceedsLoan` | REVLoans | `reallocateCollateralFromLoan` called with `collateralCountToReturn > loan.collateral` |
|
|
336
338
|
| `REVLoans_InvalidPrepaidFeePercent` | REVLoans | `prepaidFeePercent` outside `[MIN_PREPAID_FEE_PERCENT, MAX_PREPAID_FEE_PERCENT]` range (25-500) |
|
|
337
339
|
| `REVLoans_InvalidTerminal` | REVLoans | Loan source references a terminal not registered in `JBDirectory` for the revnet |
|
package/CHANGE_LOG.md
CHANGED
|
@@ -108,6 +108,7 @@ The boolean semantics are **inverted**: v5 used opt-in flags (`splitOperatorCan*
|
|
|
108
108
|
| `REVLoans` | `REVLoans_NothingToRepay()` |
|
|
109
109
|
| `REVLoans` | `REVLoans_ZeroBorrowAmount()` |
|
|
110
110
|
| `REVLoans` | `REVLoans_SourceMismatch()` |
|
|
111
|
+
| `REVLoans` | `REVLoans_CashOutDelayNotFinished(uint256 cashOutDelay, uint256 blockTimestamp)` |
|
|
111
112
|
| `REVLoans` | `REVLoans_LoanIdOverflow()` |
|
|
112
113
|
|
|
113
114
|
### 2.4 New Constants
|
|
@@ -129,6 +130,13 @@ The boolean semantics are **inverted**: v5 used opt-in flags (`splitOperatorCan*
|
|
|
129
130
|
|
|
130
131
|
## 3. Event Changes
|
|
131
132
|
|
|
133
|
+
### 3.0 Indexer Notes
|
|
134
|
+
|
|
135
|
+
For revnet-focused subgraphs:
|
|
136
|
+
- both deployment flows now correlate to `deployFor` rather than a split `deployFor`/`deployWith721sFor` model;
|
|
137
|
+
- revnet deployment entities should expect an associated 721 hook by default;
|
|
138
|
+
- any entity that previously depended on caller-supplied buyback-hook config should be updated for the v6 auto-configured buyback path.
|
|
139
|
+
|
|
132
140
|
### 3.1 Added Events
|
|
133
141
|
|
|
134
142
|
See section 2.2 above.
|
|
@@ -289,6 +297,7 @@ The following structs are identical between v5 and v6 (only `forge-lint` comment
|
|
|
289
297
|
| **Source fee try-catch hardening** | The source fee payment in `_adjust` is now wrapped in a try-catch block. If the source terminal's `pay` call reverts, the ERC-20 allowance is reclaimed and the fee amount is returned to the beneficiary instead of blocking the entire loan operation. v5 called `terminal.pay` directly without error handling. |
|
|
290
298
|
| **Timestamp cast fix** | `borrowFrom` now casts `block.timestamp` to `uint48` when setting `loan.createdAt`, matching the `REVLoan.createdAt` field width. v5 used `uint40`, which would silently truncate timestamps after the year 36812. |
|
|
291
299
|
| **`ReallocateCollateral` event typo fix** | v5 used `removedcollateralCount` (lowercase 'c'). v6 fixes it to `removedCollateralCount` (uppercase 'C'). |
|
|
300
|
+
| **Cash out delay enforced in loans** | `borrowFrom` now resolves the `REVDeployer` from the ruleset's `dataHook` and checks `cashOutDelayOf(revnetId)`. If the 30-day cross-chain deployment delay hasn't passed, `borrowFrom` reverts with `REVLoans_CashOutDelayNotFinished`. `borrowableAmountFrom` returns 0 during the delay so UIs reflect the restriction. v5 did not enforce the cash out delay in the loans contract. |
|
|
292
301
|
| **NatSpec documentation** | Extensive NatSpec added to all functions, views, and internal helpers. Flash loan safety analysis documented in `_borrowableAmountFrom`. |
|
|
293
302
|
|
|
294
303
|
### 6.3 Named Arguments
|
package/RISKS.md
CHANGED
|
@@ -81,6 +81,10 @@ Read [ARCHITECTURE.md](./ARCHITECTURE.md) and [SKILLS.md](./SKILLS.md) for proto
|
|
|
81
81
|
- **Source mismatch check.** `reallocateCollateralFromLoan` enforces that the new loan's source matches the existing loan's source (`source.token == existingSource.token && source.terminal == existingSource.terminal`). This prevents cross-source value extraction.
|
|
82
82
|
- **MEV opportunity at stage boundaries.** If a borrower knows a stage transition will decrease `cashOutTaxRate`, they can wait until just after the transition and `reallocateCollateralFromLoan` to extract more borrowed funds from the same collateral. This is predictable and not preventable by design.
|
|
83
83
|
|
|
84
|
+
### Cross-chain cash-out delay enforcement
|
|
85
|
+
|
|
86
|
+
- **Loans enforce the same 30-day cash-out delay as direct cash outs.** When a revnet is deployed to a new chain where its first stage has already started, `REVDeployer._setCashOutDelayIfNeeded()` sets a 30-day delay. `borrowFrom` resolves the deployer from the current ruleset's `dataHook` and checks `cashOutDelayOf(revnetId)`, reverting with `REVLoans_CashOutDelayNotFinished` if the delay hasn't passed. `borrowableAmountFrom` returns 0 during the delay. This prevents cross-chain arbitrage via the loan system (bridging tokens to a new chain and immediately borrowing against them before prices equilibrate).
|
|
87
|
+
|
|
84
88
|
### BURN_TOKENS permission prerequisite
|
|
85
89
|
|
|
86
90
|
- **Borrowers must grant BURN_TOKENS permission before calling `borrowFrom`.** The loans contract burns the caller's tokens as collateral via `JBController.burnTokensOf`, which requires the caller to have granted `BURN_TOKENS` permission to the loans contract for the revnet's project ID. Without this, the transaction reverts deep in `JBController` with `JBPermissioned_Unauthorized`. The prerequisite is documented in `borrowFrom`'s NatSpec.
|
package/SKILLS.md
CHANGED
|
@@ -47,7 +47,7 @@ Deploy and manage Revnets -- autonomous, unowned Juicebox projects with staged i
|
|
|
47
47
|
|
|
48
48
|
| Function | Permissions | What it does |
|
|
49
49
|
|----------|------------|-------------|
|
|
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. |
|
|
50
|
+
| `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
51
|
| `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
52
|
| `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
53
|
| `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 +57,7 @@ Deploy and manage Revnets -- autonomous, unowned Juicebox projects with staged i
|
|
|
57
57
|
|
|
58
58
|
| Function | What it does |
|
|
59
59
|
|----------|-------------|
|
|
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. |
|
|
60
|
+
| `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
61
|
| `REVLoans.determineSourceFeeAmount(loan, amount)` | Calculate the time-proportional source fee for a loan repayment. Zero during prepaid window, linear accrual after. |
|
|
62
62
|
| `REVLoans.loanOf(loanId)` | Returns the full `REVLoan` struct for a loan. |
|
|
63
63
|
| `REVLoans.loanSourcesOf(revnetId)` | Returns all `(terminal, token)` pairs used for loans by a revnet. |
|
|
@@ -139,6 +139,7 @@ Deploy and manage Revnets -- autonomous, unowned Juicebox projects with staged i
|
|
|
139
139
|
|
|
140
140
|
| Error | When It Fires |
|
|
141
141
|
|-------|---------------|
|
|
142
|
+
| `REVLoans_CashOutDelayNotFinished(cashOutDelay, blockTimestamp)` | When borrowing during the 30-day cash-out delay period (cross-chain deployment protection). |
|
|
142
143
|
| `REVLoans_CollateralExceedsLoan(collateralToReturn, loanCollateral)` | When trying to return more collateral than the loan holds. |
|
|
143
144
|
| `REVLoans_InvalidPrepaidFeePercent(prepaidFeePercent, min, max)` | When `prepaidFeePercent` is outside the allowed range (2.5%--50%). |
|
|
144
145
|
| `REVLoans_InvalidTerminal(terminal, revnetId)` | When the specified terminal is not registered for the revnet. |
|
|
@@ -212,8 +213,8 @@ Deploy and manage Revnets -- autonomous, unowned Juicebox projects with staged i
|
|
|
212
213
|
4. **Loan ID encoding.** `loanId = revnetId * 1_000_000_000_000 + loanNumber`. Each revnet supports ~1 trillion loans. Use `revnetIdOfLoanWith(loanId)` to decode.
|
|
213
214
|
5. **uint112 truncation risk.** `REVLoan.amount` and `REVLoan.collateral` are `uint112`. Values above ~5.19e33 truncate silently.
|
|
214
215
|
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.
|
|
216
|
+
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 resolved via the ruleset's `dataHook` (the REVDeployer) and `cashOutDelayOf(revnetId)`.
|
|
217
218
|
9. **`cashOutTaxRate` cannot be MAX.** Must be strictly less than `MAX_CASH_OUT_TAX_RATE` (10,000). Revnets cannot fully disable cash outs.
|
|
218
219
|
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
220
|
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.
|
package/USER_JOURNEYS.md
CHANGED
|
@@ -163,6 +163,7 @@ This is a standard Juicebox payment, but REVDeployer intervenes as the data hook
|
|
|
163
163
|
**Prerequisites:**
|
|
164
164
|
- Caller must hold `collateralCount` revnet ERC-20 tokens
|
|
165
165
|
- 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`.
|
|
166
|
+
- 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
167
|
|
|
167
168
|
**Key parameters:**
|
|
168
169
|
|
|
@@ -181,6 +182,7 @@ This is a standard Juicebox payment, but REVDeployer intervenes as the data hook
|
|
|
181
182
|
- `collateralCount > 0` (no zero-collateral loans)
|
|
182
183
|
- `source.terminal` is registered for the revnet in the directory
|
|
183
184
|
- `prepaidFeePercent` in range [25, 500]
|
|
185
|
+
- Cash-out delay has passed: resolves the `REVDeployer` from the current ruleset's `dataHook`, checks `cashOutDelayOf(revnetId)`. Reverts with `REVLoans_CashOutDelayNotFinished(cashOutDelay, block.timestamp)` if `cashOutDelay > block.timestamp`.
|
|
184
186
|
2. **Loan ID generation:** `revnetId * 1_000_000_000_000 + (++totalLoansBorrowedFor[revnetId])`
|
|
185
187
|
3. **Loan creation in storage:**
|
|
186
188
|
- `source`, `createdAt = block.timestamp`, `prepaidFeePercent`, `prepaidDuration = mulDiv(prepaidFeePercent, 3650 days, 500)`
|
|
@@ -210,6 +212,7 @@ This is a standard Juicebox payment, but REVDeployer intervenes as the data hook
|
|
|
210
212
|
- `prepaidDuration` at minimum (25): `25 * 3650 days / 500 = 182.5 days`. At maximum (500): `500 * 3650 days / 500 = 3650 days`.
|
|
211
213
|
- 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
214
|
- 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 deployer from the current ruleset's `dataHook` and checking `cashOutDelayOf`. This prevents cross-chain arbitrage via loans during the delay window.
|
|
213
216
|
|
|
214
217
|
---
|
|
215
218
|
|
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.18",
|
|
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
|
@@ -323,7 +323,6 @@ contract DeployScript is Script, Sphinx {
|
|
|
323
323
|
tiersConfig: JB721InitTiersConfig({
|
|
324
324
|
tiers: new JB721TierConfig[](0), currency: ETH_CURRENCY, decimals: 18
|
|
325
325
|
}),
|
|
326
|
-
reserveBeneficiary: address(0),
|
|
327
326
|
flags: REV721TiersHookFlags({
|
|
328
327
|
noNewTiersWithReserves: false,
|
|
329
328
|
noNewTiersWithVotes: false,
|
|
@@ -459,21 +458,25 @@ contract DeployScript is Script, Sphinx {
|
|
|
459
458
|
trustedForwarder: TRUSTED_FORWARDER
|
|
460
459
|
});
|
|
461
460
|
|
|
462
|
-
//
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
461
|
+
// Only configure the fee project if singletons were freshly deployed. Re-running `deployFor` on an
|
|
462
|
+
// already-configured project would fail because the project is no longer blank.
|
|
463
|
+
if (!_singletonsExist) {
|
|
464
|
+
// Approve the basic deployer to configure the project.
|
|
465
|
+
core.projects.approve({to: address(_basicDeployer), tokenId: FEE_PROJECT_ID});
|
|
466
|
+
|
|
467
|
+
// Build the config.
|
|
468
|
+
FeeProjectConfig memory feeProjectConfig = getFeeProjectConfig();
|
|
469
|
+
|
|
470
|
+
// Configure the project.
|
|
471
|
+
_basicDeployer.deployFor({
|
|
472
|
+
revnetId: FEE_PROJECT_ID,
|
|
473
|
+
configuration: feeProjectConfig.configuration,
|
|
474
|
+
terminalConfigurations: feeProjectConfig.terminalConfigurations,
|
|
475
|
+
suckerDeploymentConfiguration: feeProjectConfig.suckerDeploymentConfiguration,
|
|
476
|
+
tiered721HookConfiguration: feeProjectConfig.tiered721HookConfiguration,
|
|
477
|
+
allowedPosts: feeProjectConfig.allowedPosts
|
|
478
|
+
});
|
|
479
|
+
}
|
|
477
480
|
}
|
|
478
481
|
|
|
479
482
|
/// @notice Check whether a contract has already been deployed at its deterministic address.
|
package/src/REVDeployer.sol
CHANGED
|
@@ -282,6 +282,9 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
|
|
|
282
282
|
}
|
|
283
283
|
|
|
284
284
|
// Split the cashed-out tokens into a fee portion and a non-fee portion.
|
|
285
|
+
// The fee is applied to TOKEN COUNT (2.5% of tokens), not to value. The fee revnet receives the bonding-curve
|
|
286
|
+
// reclaim of its 2.5% token share regardless of whether the remaining 97.5% routes through a buyback pool at
|
|
287
|
+
// a better price. This is by design.
|
|
285
288
|
// Micro cash outs (< 40 wei at 2.5% fee) round feeCashOutCount to zero, bypassing the fee.
|
|
286
289
|
// Economically insignificant: the gas cost of the transaction far exceeds the bypassed fee. No fix needed.
|
|
287
290
|
uint256 feeCashOutCount = mulDiv({x: context.cashOutCount, y: FEE, denominator: JBConstants.MAX_FEE});
|
|
@@ -608,6 +611,7 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
|
|
|
608
611
|
sqrtPriceX96 = uint160(1 << 96);
|
|
609
612
|
} else {
|
|
610
613
|
address normalizedTerminalToken = terminalToken == JBConstants.NATIVE_TOKEN ? address(0) : terminalToken;
|
|
614
|
+
// slither-disable-next-line calls-loop
|
|
611
615
|
address projectToken = address(CONTROLLER.TOKENS().tokenOf(revnetId));
|
|
612
616
|
|
|
613
617
|
if (projectToken == address(0) || projectToken == normalizedTerminalToken) {
|
|
@@ -955,7 +959,6 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
|
|
|
955
959
|
tokenUriResolver: tiered721HookConfiguration.baseline721HookConfiguration.tokenUriResolver,
|
|
956
960
|
contractUri: tiered721HookConfiguration.baseline721HookConfiguration.contractUri,
|
|
957
961
|
tiersConfig: tiered721HookConfiguration.baseline721HookConfiguration.tiersConfig,
|
|
958
|
-
reserveBeneficiary: tiered721HookConfiguration.baseline721HookConfiguration.reserveBeneficiary,
|
|
959
962
|
flags: JB721TiersHookFlags({
|
|
960
963
|
noNewTiersWithReserves: tiered721HookConfiguration.baseline721HookConfiguration.flags
|
|
961
964
|
.noNewTiersWithReserves,
|
package/src/REVLoans.sol
CHANGED
|
@@ -27,6 +27,7 @@ import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingCo
|
|
|
27
27
|
import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
|
|
28
28
|
import {JBSingleAllowance} from "@bananapus/core-v6/src/structs/JBSingleAllowance.sol";
|
|
29
29
|
|
|
30
|
+
import {IREVDeployer} from "./interfaces/IREVDeployer.sol";
|
|
30
31
|
import {IREVLoans} from "./interfaces/IREVLoans.sol";
|
|
31
32
|
import {REVLoan} from "./structs/REVLoan.sol";
|
|
32
33
|
import {REVLoanSource} from "./structs/REVLoanSource.sol";
|
|
@@ -55,6 +56,7 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
55
56
|
// --------------------------- custom errors ------------------------- //
|
|
56
57
|
//*********************************************************************//
|
|
57
58
|
|
|
59
|
+
error REVLoans_CashOutDelayNotFinished(uint256 cashOutDelay, uint256 blockTimestamp);
|
|
58
60
|
error REVLoans_CollateralExceedsLoan(uint256 collateralToReturn, uint256 loanCollateral);
|
|
59
61
|
error REVLoans_InvalidPrepaidFeePercent(uint256 prepaidFeePercent, uint256 min, uint256 max);
|
|
60
62
|
error REVLoans_InvalidTerminal(address terminal, uint256 revnetId);
|
|
@@ -225,6 +227,22 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
225
227
|
view
|
|
226
228
|
returns (uint256)
|
|
227
229
|
{
|
|
230
|
+
// Get the current ruleset to resolve the deployer from its data hook.
|
|
231
|
+
// slither-disable-next-line unused-return
|
|
232
|
+
(JBRuleset memory currentRuleset,) = CONTROLLER.currentRulesetOf(revnetId);
|
|
233
|
+
|
|
234
|
+
// The ruleset's data hook is the REVDeployer that configured this revnet.
|
|
235
|
+
address deployer = currentRuleset.dataHook();
|
|
236
|
+
|
|
237
|
+
// Only check the delay if a deployer is set.
|
|
238
|
+
if (deployer != address(0)) {
|
|
239
|
+
// Get the timestamp after which cash outs (and loans) are allowed.
|
|
240
|
+
uint256 cashOutDelay = IREVDeployer(deployer).cashOutDelayOf(revnetId);
|
|
241
|
+
|
|
242
|
+
// If the delay hasn't passed yet, no amount is borrowable.
|
|
243
|
+
if (cashOutDelay > block.timestamp) return 0;
|
|
244
|
+
}
|
|
245
|
+
|
|
228
246
|
return _borrowableAmountFrom({
|
|
229
247
|
revnetId: revnetId,
|
|
230
248
|
collateralCount: collateralCount,
|
|
@@ -580,6 +598,24 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
580
598
|
);
|
|
581
599
|
}
|
|
582
600
|
|
|
601
|
+
// Get the current ruleset to resolve the deployer from its data hook.
|
|
602
|
+
// slither-disable-next-line unused-return
|
|
603
|
+
(JBRuleset memory currentRuleset,) = CONTROLLER.currentRulesetOf(revnetId);
|
|
604
|
+
|
|
605
|
+
// The ruleset's data hook is the REVDeployer that configured this revnet.
|
|
606
|
+
address deployer = currentRuleset.dataHook();
|
|
607
|
+
|
|
608
|
+
// Only check the delay if a deployer is set.
|
|
609
|
+
if (deployer != address(0)) {
|
|
610
|
+
// Get the timestamp after which cash outs (and loans) are allowed.
|
|
611
|
+
uint256 cashOutDelay = IREVDeployer(deployer).cashOutDelayOf(revnetId);
|
|
612
|
+
|
|
613
|
+
// Revert if the delay hasn't passed yet.
|
|
614
|
+
if (cashOutDelay > block.timestamp) {
|
|
615
|
+
revert REVLoans_CashOutDelayNotFinished(cashOutDelay, block.timestamp);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
583
619
|
// Prevent the loan number from exceeding the ID namespace for this revnet.
|
|
584
620
|
if (totalLoansBorrowedFor[revnetId] >= _ONE_TRILLION) revert REVLoans_LoanIdOverflow();
|
|
585
621
|
|
|
@@ -12,7 +12,6 @@ import {REV721TiersHookFlags} from "./REV721TiersHookFlags.sol";
|
|
|
12
12
|
/// @custom:member tokenUriResolver The token URI resolver for the NFT collection.
|
|
13
13
|
/// @custom:member contractUri The contract URI for the NFT collection.
|
|
14
14
|
/// @custom:member tiersConfig The tier configuration for the NFT collection.
|
|
15
|
-
/// @custom:member reserveBeneficiary The default reserve beneficiary for the NFT collection.
|
|
16
15
|
/// @custom:member flags A set of flags that configure the 721 hook. Omits `issueTokensForSplits` since revnets
|
|
17
16
|
/// always force it to `false`.
|
|
18
17
|
// forge-lint: disable-next-line(pascal-case-struct)
|
|
@@ -23,6 +22,5 @@ struct REVBaseline721HookConfig {
|
|
|
23
22
|
IJB721TokenUriResolver tokenUriResolver;
|
|
24
23
|
string contractUri;
|
|
25
24
|
JB721InitTiersConfig tiersConfig;
|
|
26
|
-
address reserveBeneficiary;
|
|
27
25
|
REV721TiersHookFlags flags;
|
|
28
26
|
}
|
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.28;
|
|
3
|
+
|
|
4
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
5
|
+
import "forge-std/Test.sol";
|
|
6
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
7
|
+
import /* {*} from */ "@bananapus/core-v6/test/helpers/TestBaseWorkflow.sol";
|
|
8
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
9
|
+
import /* {*} from */ "./../src/REVDeployer.sol";
|
|
10
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
11
|
+
import "@croptop/core-v6/src/CTPublisher.sol";
|
|
12
|
+
import {MockBuybackDataHook} from "./mock/MockBuybackDataHook.sol";
|
|
13
|
+
|
|
14
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
15
|
+
import "@bananapus/core-v6/script/helpers/CoreDeploymentLib.sol";
|
|
16
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
17
|
+
import "@bananapus/721-hook-v6/script/helpers/Hook721DeploymentLib.sol";
|
|
18
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
19
|
+
import "@bananapus/suckers-v6/script/helpers/SuckerDeploymentLib.sol";
|
|
20
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
21
|
+
import "@croptop/core-v6/script/helpers/CroptopDeploymentLib.sol";
|
|
22
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
23
|
+
import "@bananapus/router-terminal-v6/script/helpers/RouterTerminalDeploymentLib.sol";
|
|
24
|
+
|
|
25
|
+
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
26
|
+
import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
|
|
27
|
+
import {REVLoans} from "../src/REVLoans.sol";
|
|
28
|
+
import {REVStageConfig, REVAutoIssuance} from "../src/structs/REVStageConfig.sol";
|
|
29
|
+
import {REVLoanSource} from "../src/structs/REVLoanSource.sol";
|
|
30
|
+
import {REVDescription} from "../src/structs/REVDescription.sol";
|
|
31
|
+
import {IREVLoans} from "./../src/interfaces/IREVLoans.sol";
|
|
32
|
+
import {JBSuckerDeployerConfig} from "@bananapus/suckers-v6/src/structs/JBSuckerDeployerConfig.sol";
|
|
33
|
+
import {JBSuckerRegistry} from "@bananapus/suckers-v6/src/JBSuckerRegistry.sol";
|
|
34
|
+
import {JB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
|
|
35
|
+
import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
|
|
36
|
+
import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
|
|
37
|
+
import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
|
|
38
|
+
import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
|
|
39
|
+
import {REVEmpty721Config} from "./helpers/REVEmpty721Config.sol";
|
|
40
|
+
|
|
41
|
+
struct FeeProjectConfig {
|
|
42
|
+
REVConfig configuration;
|
|
43
|
+
JBTerminalConfig[] terminalConfigurations;
|
|
44
|
+
REVSuckerDeploymentConfig suckerDeploymentConfiguration;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// @notice Tests that REVLoans enforces the cash out delay set by REVDeployer for cross-chain deployments.
|
|
48
|
+
contract TestLoansCashOutDelay is TestBaseWorkflow {
|
|
49
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
50
|
+
bytes32 REV_DEPLOYER_SALT = "REVDeployer";
|
|
51
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
52
|
+
bytes32 ERC20_SALT = "REV_TOKEN";
|
|
53
|
+
|
|
54
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
55
|
+
REVDeployer REV_DEPLOYER;
|
|
56
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
57
|
+
JB721TiersHook EXAMPLE_HOOK;
|
|
58
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
59
|
+
IJB721TiersHookDeployer HOOK_DEPLOYER;
|
|
60
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
61
|
+
IJB721TiersHookStore HOOK_STORE;
|
|
62
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
63
|
+
IJBAddressRegistry ADDRESS_REGISTRY;
|
|
64
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
65
|
+
IREVLoans LOANS_CONTRACT;
|
|
66
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
67
|
+
IJBSuckerRegistry SUCKER_REGISTRY;
|
|
68
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
69
|
+
CTPublisher PUBLISHER;
|
|
70
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
71
|
+
MockBuybackDataHook MOCK_BUYBACK;
|
|
72
|
+
|
|
73
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
74
|
+
uint256 FEE_PROJECT_ID;
|
|
75
|
+
|
|
76
|
+
/// @notice Revnet deployed with startsAtOrAfter in the past (triggers cash out delay).
|
|
77
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
78
|
+
uint256 DELAYED_REVNET_ID;
|
|
79
|
+
|
|
80
|
+
/// @notice Revnet deployed with startsAtOrAfter == block.timestamp (no delay).
|
|
81
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
82
|
+
uint256 NORMAL_REVNET_ID;
|
|
83
|
+
|
|
84
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
85
|
+
address USER = makeAddr("user");
|
|
86
|
+
|
|
87
|
+
address private constant TRUSTED_FORWARDER = 0xB2b5841DBeF766d4b521221732F9B618fCf34A87;
|
|
88
|
+
|
|
89
|
+
function getFeeProjectConfig() internal view returns (FeeProjectConfig memory) {
|
|
90
|
+
JBAccountingContext[] memory accountingContextsToAccept = new JBAccountingContext[](1);
|
|
91
|
+
accountingContextsToAccept[0] = JBAccountingContext({
|
|
92
|
+
token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
JBTerminalConfig[] memory terminalConfigurations = new JBTerminalConfig[](1);
|
|
96
|
+
terminalConfigurations[0] =
|
|
97
|
+
JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: accountingContextsToAccept});
|
|
98
|
+
|
|
99
|
+
REVStageConfig[] memory stageConfigurations = new REVStageConfig[](1);
|
|
100
|
+
JBSplit[] memory splits = new JBSplit[](1);
|
|
101
|
+
splits[0].beneficiary = payable(multisig());
|
|
102
|
+
splits[0].percent = 10_000;
|
|
103
|
+
|
|
104
|
+
stageConfigurations[0] = REVStageConfig({
|
|
105
|
+
startsAtOrAfter: uint40(block.timestamp),
|
|
106
|
+
autoIssuances: new REVAutoIssuance[](0),
|
|
107
|
+
splitPercent: 2000,
|
|
108
|
+
splits: splits,
|
|
109
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
110
|
+
initialIssuance: uint112(1000e18),
|
|
111
|
+
issuanceCutFrequency: 90 days,
|
|
112
|
+
issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
|
|
113
|
+
cashOutTaxRate: 6000,
|
|
114
|
+
extraMetadata: 0
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return FeeProjectConfig({
|
|
118
|
+
configuration: REVConfig({
|
|
119
|
+
// forge-lint: disable-next-line(named-struct-fields)
|
|
120
|
+
description: REVDescription("Revnet", "$REV", "ipfs://fee", ERC20_SALT),
|
|
121
|
+
baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
122
|
+
splitOperator: multisig(),
|
|
123
|
+
stageConfigurations: stageConfigurations
|
|
124
|
+
}),
|
|
125
|
+
terminalConfigurations: terminalConfigurations,
|
|
126
|
+
suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
|
|
127
|
+
deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256(abi.encodePacked("REV"))
|
|
128
|
+
})
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/// @notice Returns a revnet config. When `pastStart` is true, `startsAtOrAfter` is set to 1 second ago,
|
|
133
|
+
/// triggering the 30-day cash out delay in REVDeployer._setCashOutDelayIfNeeded.
|
|
134
|
+
function _getRevnetConfig(
|
|
135
|
+
bool pastStart,
|
|
136
|
+
string memory name,
|
|
137
|
+
string memory symbol,
|
|
138
|
+
bytes32 salt
|
|
139
|
+
)
|
|
140
|
+
internal
|
|
141
|
+
view
|
|
142
|
+
returns (FeeProjectConfig memory)
|
|
143
|
+
{
|
|
144
|
+
JBAccountingContext[] memory accountingContextsToAccept = new JBAccountingContext[](1);
|
|
145
|
+
accountingContextsToAccept[0] = JBAccountingContext({
|
|
146
|
+
token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
JBTerminalConfig[] memory terminalConfigurations = new JBTerminalConfig[](1);
|
|
150
|
+
terminalConfigurations[0] =
|
|
151
|
+
JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: accountingContextsToAccept});
|
|
152
|
+
|
|
153
|
+
REVStageConfig[] memory stageConfigurations = new REVStageConfig[](1);
|
|
154
|
+
JBSplit[] memory splits = new JBSplit[](1);
|
|
155
|
+
splits[0].beneficiary = payable(multisig());
|
|
156
|
+
splits[0].percent = 10_000;
|
|
157
|
+
|
|
158
|
+
// If pastStart, set startsAtOrAfter to 1 second ago — simulates cross-chain deployment
|
|
159
|
+
// where the stage is already active on another chain.
|
|
160
|
+
uint40 startsAt = pastStart ? uint40(block.timestamp - 1) : uint40(block.timestamp);
|
|
161
|
+
|
|
162
|
+
stageConfigurations[0] = REVStageConfig({
|
|
163
|
+
startsAtOrAfter: startsAt,
|
|
164
|
+
autoIssuances: new REVAutoIssuance[](0),
|
|
165
|
+
splitPercent: 2000,
|
|
166
|
+
splits: splits,
|
|
167
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
168
|
+
initialIssuance: uint112(1000e18),
|
|
169
|
+
issuanceCutFrequency: 90 days,
|
|
170
|
+
issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
|
|
171
|
+
cashOutTaxRate: 6000,
|
|
172
|
+
extraMetadata: 0
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
return FeeProjectConfig({
|
|
176
|
+
configuration: REVConfig({
|
|
177
|
+
// forge-lint: disable-next-line(named-struct-fields)
|
|
178
|
+
description: REVDescription(name, symbol, "ipfs://test", salt),
|
|
179
|
+
baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
180
|
+
splitOperator: multisig(),
|
|
181
|
+
stageConfigurations: stageConfigurations
|
|
182
|
+
}),
|
|
183
|
+
terminalConfigurations: terminalConfigurations,
|
|
184
|
+
suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
|
|
185
|
+
deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: salt
|
|
186
|
+
})
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function setUp() public override {
|
|
191
|
+
super.setUp();
|
|
192
|
+
|
|
193
|
+
// Warp to a realistic timestamp so startsAtOrAfter - 1 doesn't underflow.
|
|
194
|
+
vm.warp(1_700_000_000);
|
|
195
|
+
|
|
196
|
+
FEE_PROJECT_ID = jbProjects().createFor(multisig());
|
|
197
|
+
|
|
198
|
+
SUCKER_REGISTRY = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
|
|
199
|
+
HOOK_STORE = new JB721TiersHookStore();
|
|
200
|
+
EXAMPLE_HOOK = new JB721TiersHook(
|
|
201
|
+
jbDirectory(), jbPermissions(), jbPrices(), jbRulesets(), HOOK_STORE, jbSplits(), multisig()
|
|
202
|
+
);
|
|
203
|
+
ADDRESS_REGISTRY = new JBAddressRegistry();
|
|
204
|
+
HOOK_DEPLOYER = new JB721TiersHookDeployer(EXAMPLE_HOOK, HOOK_STORE, ADDRESS_REGISTRY, multisig());
|
|
205
|
+
PUBLISHER = new CTPublisher(jbDirectory(), jbPermissions(), FEE_PROJECT_ID, multisig());
|
|
206
|
+
MOCK_BUYBACK = new MockBuybackDataHook();
|
|
207
|
+
|
|
208
|
+
LOANS_CONTRACT = new REVLoans({
|
|
209
|
+
controller: jbController(),
|
|
210
|
+
projects: jbProjects(),
|
|
211
|
+
revId: FEE_PROJECT_ID,
|
|
212
|
+
owner: address(this),
|
|
213
|
+
permit2: permit2(),
|
|
214
|
+
trustedForwarder: TRUSTED_FORWARDER
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
REV_DEPLOYER = new REVDeployer{salt: REV_DEPLOYER_SALT}(
|
|
218
|
+
jbController(),
|
|
219
|
+
SUCKER_REGISTRY,
|
|
220
|
+
FEE_PROJECT_ID,
|
|
221
|
+
HOOK_DEPLOYER,
|
|
222
|
+
PUBLISHER,
|
|
223
|
+
IJBBuybackHookRegistry(address(MOCK_BUYBACK)),
|
|
224
|
+
address(LOANS_CONTRACT),
|
|
225
|
+
TRUSTED_FORWARDER
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
// Approve the deployer to configure the fee project.
|
|
229
|
+
vm.prank(multisig());
|
|
230
|
+
jbProjects().approve(address(REV_DEPLOYER), FEE_PROJECT_ID);
|
|
231
|
+
|
|
232
|
+
// Deploy the fee project.
|
|
233
|
+
FeeProjectConfig memory feeProjectConfig = getFeeProjectConfig();
|
|
234
|
+
vm.prank(multisig());
|
|
235
|
+
REV_DEPLOYER.deployFor({
|
|
236
|
+
revnetId: FEE_PROJECT_ID,
|
|
237
|
+
configuration: feeProjectConfig.configuration,
|
|
238
|
+
terminalConfigurations: feeProjectConfig.terminalConfigurations,
|
|
239
|
+
suckerDeploymentConfiguration: feeProjectConfig.suckerDeploymentConfiguration,
|
|
240
|
+
tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
|
|
241
|
+
allowedPosts: REVEmpty721Config.emptyAllowedPosts()
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// Deploy a revnet with startsAtOrAfter in the past (triggers 30-day cash out delay).
|
|
245
|
+
FeeProjectConfig memory delayedConfig =
|
|
246
|
+
_getRevnetConfig(true, "Delayed", "$DLY", keccak256(abi.encodePacked("DELAYED")));
|
|
247
|
+
(DELAYED_REVNET_ID,) = REV_DEPLOYER.deployFor({
|
|
248
|
+
revnetId: 0,
|
|
249
|
+
configuration: delayedConfig.configuration,
|
|
250
|
+
terminalConfigurations: delayedConfig.terminalConfigurations,
|
|
251
|
+
suckerDeploymentConfiguration: delayedConfig.suckerDeploymentConfiguration,
|
|
252
|
+
tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
|
|
253
|
+
allowedPosts: REVEmpty721Config.emptyAllowedPosts()
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// Deploy a normal revnet with no delay.
|
|
257
|
+
FeeProjectConfig memory normalConfig =
|
|
258
|
+
_getRevnetConfig(false, "Normal", "$NRM", keccak256(abi.encodePacked("NORMAL")));
|
|
259
|
+
(NORMAL_REVNET_ID,) = REV_DEPLOYER.deployFor({
|
|
260
|
+
revnetId: 0,
|
|
261
|
+
configuration: normalConfig.configuration,
|
|
262
|
+
terminalConfigurations: normalConfig.terminalConfigurations,
|
|
263
|
+
suckerDeploymentConfiguration: normalConfig.suckerDeploymentConfiguration,
|
|
264
|
+
tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
|
|
265
|
+
allowedPosts: REVEmpty721Config.emptyAllowedPosts()
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
vm.deal(USER, 100 ether);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ------------------------------------------------------------------
|
|
272
|
+
// Helpers
|
|
273
|
+
// ------------------------------------------------------------------
|
|
274
|
+
|
|
275
|
+
/// @notice Pay ETH into a revnet and return the number of project tokens received.
|
|
276
|
+
function _payAndGetTokens(uint256 revnetId, uint256 amount) internal returns (uint256 tokenCount) {
|
|
277
|
+
vm.prank(USER);
|
|
278
|
+
tokenCount = jbMultiTerminal().pay{value: amount}(revnetId, JBConstants.NATIVE_TOKEN, amount, USER, 0, "", "");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/// @notice Mock the permissions check so LOANS_CONTRACT can burn tokens on behalf of USER.
|
|
282
|
+
function _mockBorrowPermission(uint256 projectId) internal {
|
|
283
|
+
mockExpect(
|
|
284
|
+
address(jbPermissions()),
|
|
285
|
+
abi.encodeCall(IJBPermissions.hasPermission, (address(LOANS_CONTRACT), USER, projectId, 11, true, true)),
|
|
286
|
+
abi.encode(true)
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ------------------------------------------------------------------
|
|
291
|
+
// Tests: delayed revnet (startsAtOrAfter in the past → 30-day delay)
|
|
292
|
+
// ------------------------------------------------------------------
|
|
293
|
+
|
|
294
|
+
/// @notice Verify the deployer actually set a cash out delay for the delayed revnet.
|
|
295
|
+
function test_delayedRevnet_hasCashOutDelay() public view {
|
|
296
|
+
uint256 cashOutDelay = REV_DEPLOYER.cashOutDelayOf(DELAYED_REVNET_ID);
|
|
297
|
+
assertGt(cashOutDelay, block.timestamp, "Cash out delay should be in the future");
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/// @notice Verify the normal revnet has no cash out delay.
|
|
301
|
+
function test_normalRevnet_noCashOutDelay() public view {
|
|
302
|
+
uint256 cashOutDelay = REV_DEPLOYER.cashOutDelayOf(NORMAL_REVNET_ID);
|
|
303
|
+
assertEq(cashOutDelay, 0, "Normal revnet should have no cash out delay");
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/// @notice borrowableAmountFrom should return 0 during the delay period.
|
|
307
|
+
function test_borrowableAmountFrom_returnsZeroDuringDelay() public {
|
|
308
|
+
// Pay into the delayed revnet to get tokens.
|
|
309
|
+
uint256 tokenCount = _payAndGetTokens(DELAYED_REVNET_ID, 1 ether);
|
|
310
|
+
assertGt(tokenCount, 0, "Should have tokens");
|
|
311
|
+
|
|
312
|
+
// Query borrowable amount — should be 0 during the delay.
|
|
313
|
+
uint256 borrowable = LOANS_CONTRACT.borrowableAmountFrom(
|
|
314
|
+
DELAYED_REVNET_ID, tokenCount, 18, uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
315
|
+
);
|
|
316
|
+
assertEq(borrowable, 0, "Borrowable amount should be 0 during cash out delay");
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/// @notice borrowFrom should revert during the delay period.
|
|
320
|
+
function test_borrowFrom_revertsDuringDelay() public {
|
|
321
|
+
// Pay into the delayed revnet to get tokens.
|
|
322
|
+
uint256 tokenCount = _payAndGetTokens(DELAYED_REVNET_ID, 1 ether);
|
|
323
|
+
assertGt(tokenCount, 0, "Should have tokens");
|
|
324
|
+
|
|
325
|
+
// No permission mock needed — the function reverts before reaching the permission check.
|
|
326
|
+
|
|
327
|
+
REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
|
|
328
|
+
|
|
329
|
+
// Attempt to borrow — should revert with CashOutDelayNotFinished.
|
|
330
|
+
uint256 cashOutDelay = REV_DEPLOYER.cashOutDelayOf(DELAYED_REVNET_ID);
|
|
331
|
+
vm.expectRevert(
|
|
332
|
+
abi.encodeWithSelector(REVLoans.REVLoans_CashOutDelayNotFinished.selector, cashOutDelay, block.timestamp)
|
|
333
|
+
);
|
|
334
|
+
vm.prank(USER);
|
|
335
|
+
LOANS_CONTRACT.borrowFrom(DELAYED_REVNET_ID, source, 1, tokenCount, payable(USER), 25);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/// @notice After warping past the delay, borrowableAmountFrom should return a non-zero value.
|
|
339
|
+
function test_borrowableAmountFrom_nonZeroAfterDelay() public {
|
|
340
|
+
// Pay into the delayed revnet.
|
|
341
|
+
uint256 tokenCount = _payAndGetTokens(DELAYED_REVNET_ID, 1 ether);
|
|
342
|
+
|
|
343
|
+
// Still in delay — should be 0.
|
|
344
|
+
uint256 borrowableBefore = LOANS_CONTRACT.borrowableAmountFrom(
|
|
345
|
+
DELAYED_REVNET_ID, tokenCount, 18, uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
346
|
+
);
|
|
347
|
+
assertEq(borrowableBefore, 0, "Should be 0 during delay");
|
|
348
|
+
|
|
349
|
+
// Warp past the delay.
|
|
350
|
+
vm.warp(block.timestamp + REV_DEPLOYER.CASH_OUT_DELAY() + 1);
|
|
351
|
+
|
|
352
|
+
// Now should be > 0.
|
|
353
|
+
uint256 borrowableAfter = LOANS_CONTRACT.borrowableAmountFrom(
|
|
354
|
+
DELAYED_REVNET_ID, tokenCount, 18, uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
355
|
+
);
|
|
356
|
+
assertGt(borrowableAfter, 0, "Should be > 0 after delay expires");
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/// @notice After warping past the delay, borrowFrom should succeed.
|
|
360
|
+
function test_borrowFrom_succeedsAfterDelay() public {
|
|
361
|
+
// Pay into the delayed revnet.
|
|
362
|
+
uint256 tokenCount = _payAndGetTokens(DELAYED_REVNET_ID, 1 ether);
|
|
363
|
+
|
|
364
|
+
// Warp past the delay.
|
|
365
|
+
vm.warp(block.timestamp + REV_DEPLOYER.CASH_OUT_DELAY() + 1);
|
|
366
|
+
|
|
367
|
+
// Get the borrowable amount.
|
|
368
|
+
uint256 borrowable = LOANS_CONTRACT.borrowableAmountFrom(
|
|
369
|
+
DELAYED_REVNET_ID, tokenCount, 18, uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
370
|
+
);
|
|
371
|
+
assertGt(borrowable, 0, "Should be borrowable after delay");
|
|
372
|
+
|
|
373
|
+
// Mock permission.
|
|
374
|
+
_mockBorrowPermission(DELAYED_REVNET_ID);
|
|
375
|
+
|
|
376
|
+
REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
|
|
377
|
+
|
|
378
|
+
// Borrow — should succeed.
|
|
379
|
+
vm.prank(USER);
|
|
380
|
+
(uint256 loanId,) =
|
|
381
|
+
LOANS_CONTRACT.borrowFrom(DELAYED_REVNET_ID, source, borrowable, tokenCount, payable(USER), 25);
|
|
382
|
+
assertGt(loanId, 0, "Should have created a loan");
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// ------------------------------------------------------------------
|
|
386
|
+
// Tests: normal revnet (no delay)
|
|
387
|
+
// ------------------------------------------------------------------
|
|
388
|
+
|
|
389
|
+
/// @notice A normal revnet (no delay) should allow borrowing immediately.
|
|
390
|
+
function test_normalRevnet_borrowableImmediately() public {
|
|
391
|
+
// Pay into the normal revnet.
|
|
392
|
+
uint256 tokenCount = _payAndGetTokens(NORMAL_REVNET_ID, 1 ether);
|
|
393
|
+
|
|
394
|
+
// Should have a non-zero borrowable amount immediately.
|
|
395
|
+
uint256 borrowable = LOANS_CONTRACT.borrowableAmountFrom(
|
|
396
|
+
NORMAL_REVNET_ID, tokenCount, 18, uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
397
|
+
);
|
|
398
|
+
assertGt(borrowable, 0, "Normal revnet should be borrowable immediately");
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/// @notice A normal revnet (no delay) should allow borrowFrom immediately.
|
|
402
|
+
function test_normalRevnet_borrowFromImmediately() public {
|
|
403
|
+
// Pay into the normal revnet.
|
|
404
|
+
uint256 tokenCount = _payAndGetTokens(NORMAL_REVNET_ID, 1 ether);
|
|
405
|
+
|
|
406
|
+
uint256 borrowable = LOANS_CONTRACT.borrowableAmountFrom(
|
|
407
|
+
NORMAL_REVNET_ID, tokenCount, 18, uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
408
|
+
);
|
|
409
|
+
assertGt(borrowable, 0, "Should be borrowable");
|
|
410
|
+
|
|
411
|
+
// Mock permission.
|
|
412
|
+
_mockBorrowPermission(NORMAL_REVNET_ID);
|
|
413
|
+
|
|
414
|
+
REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
|
|
415
|
+
|
|
416
|
+
// Borrow — should succeed without any delay.
|
|
417
|
+
vm.prank(USER);
|
|
418
|
+
(uint256 loanId,) =
|
|
419
|
+
LOANS_CONTRACT.borrowFrom(NORMAL_REVNET_ID, source, borrowable, tokenCount, payable(USER), 25);
|
|
420
|
+
assertGt(loanId, 0, "Should have created a loan");
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ------------------------------------------------------------------
|
|
424
|
+
// Tests: boundary conditions
|
|
425
|
+
// ------------------------------------------------------------------
|
|
426
|
+
|
|
427
|
+
/// @notice borrowFrom should revert at exactly the delay timestamp (not yet expired).
|
|
428
|
+
function test_borrowFrom_revertsAtExactDelayTimestamp() public {
|
|
429
|
+
uint256 tokenCount = _payAndGetTokens(DELAYED_REVNET_ID, 1 ether);
|
|
430
|
+
|
|
431
|
+
// Warp to exactly the delay timestamp (not past it).
|
|
432
|
+
uint256 cashOutDelay = REV_DEPLOYER.cashOutDelayOf(DELAYED_REVNET_ID);
|
|
433
|
+
vm.warp(cashOutDelay);
|
|
434
|
+
|
|
435
|
+
// borrowableAmountFrom should still return 0 (cashOutDelay > block.timestamp is false, but == is not >).
|
|
436
|
+
// Actually cashOutDelay == block.timestamp means cashOutDelay > block.timestamp is false → should pass.
|
|
437
|
+
// Let's verify: at exact boundary, the delay is NOT enforced (delay == timestamp passes).
|
|
438
|
+
uint256 borrowable = LOANS_CONTRACT.borrowableAmountFrom(
|
|
439
|
+
DELAYED_REVNET_ID, tokenCount, 18, uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
440
|
+
);
|
|
441
|
+
assertGt(borrowable, 0, "At exact delay timestamp, borrowing should be allowed");
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/// @notice borrowFrom should revert 1 second before the delay expires.
|
|
445
|
+
function test_borrowFrom_revertsOneSecondBeforeDelay() public {
|
|
446
|
+
uint256 tokenCount = _payAndGetTokens(DELAYED_REVNET_ID, 1 ether);
|
|
447
|
+
|
|
448
|
+
// Warp to 1 second before the delay expires.
|
|
449
|
+
uint256 cashOutDelay = REV_DEPLOYER.cashOutDelayOf(DELAYED_REVNET_ID);
|
|
450
|
+
vm.warp(cashOutDelay - 1);
|
|
451
|
+
|
|
452
|
+
// borrowableAmountFrom should return 0.
|
|
453
|
+
uint256 borrowable = LOANS_CONTRACT.borrowableAmountFrom(
|
|
454
|
+
DELAYED_REVNET_ID, tokenCount, 18, uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
455
|
+
);
|
|
456
|
+
assertEq(borrowable, 0, "Should be 0 one second before delay expires");
|
|
457
|
+
|
|
458
|
+
// borrowFrom should revert before reaching the permission check.
|
|
459
|
+
REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
|
|
460
|
+
|
|
461
|
+
vm.expectRevert(
|
|
462
|
+
abi.encodeWithSelector(REVLoans.REVLoans_CashOutDelayNotFinished.selector, cashOutDelay, block.timestamp)
|
|
463
|
+
);
|
|
464
|
+
vm.prank(USER);
|
|
465
|
+
LOANS_CONTRACT.borrowFrom(DELAYED_REVNET_ID, source, 1, tokenCount, payable(USER), 25);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
@@ -218,7 +218,6 @@ contract TestSplitWeightE2E is TestBaseWorkflow {
|
|
|
218
218
|
tiersConfig: JB721InitTiersConfig({
|
|
219
219
|
tiers: tiers, currency: uint32(uint160(JBConstants.NATIVE_TOKEN)), decimals: 18
|
|
220
220
|
}),
|
|
221
|
-
reserveBeneficiary: address(0),
|
|
222
221
|
flags: REV721TiersHookFlags({
|
|
223
222
|
noNewTiersWithReserves: false,
|
|
224
223
|
noNewTiersWithVotes: false,
|
|
@@ -429,7 +429,6 @@ contract TestSplitWeightFork is TestBaseWorkflow {
|
|
|
429
429
|
tiersConfig: JB721InitTiersConfig({
|
|
430
430
|
tiers: tiers, currency: uint32(uint160(JBConstants.NATIVE_TOKEN)), decimals: 18
|
|
431
431
|
}),
|
|
432
|
-
reserveBeneficiary: address(0),
|
|
433
432
|
flags: REV721TiersHookFlags({
|
|
434
433
|
noNewTiersWithReserves: false,
|
|
435
434
|
noNewTiersWithVotes: false,
|
|
@@ -435,7 +435,6 @@ abstract contract ForkTestBase is TestBaseWorkflow {
|
|
|
435
435
|
tiersConfig: JB721InitTiersConfig({
|
|
436
436
|
tiers: tiers, currency: uint32(uint160(JBConstants.NATIVE_TOKEN)), decimals: 18
|
|
437
437
|
}),
|
|
438
|
-
reserveBeneficiary: address(0),
|
|
439
438
|
flags: REV721TiersHookFlags({
|
|
440
439
|
noNewTiersWithReserves: false,
|
|
441
440
|
noNewTiersWithVotes: false,
|
|
@@ -24,7 +24,6 @@ library REVEmpty721Config {
|
|
|
24
24
|
tiersConfig: JB721InitTiersConfig({
|
|
25
25
|
tiers: new JB721TierConfig[](0), currency: baseCurrency, decimals: 18
|
|
26
26
|
}),
|
|
27
|
-
reserveBeneficiary: address(0),
|
|
28
27
|
flags: REV721TiersHookFlags({
|
|
29
28
|
noNewTiersWithReserves: false,
|
|
30
29
|
noNewTiersWithVotes: false,
|