@rev-net/core-v6 0.0.13 → 0.0.15
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/CHANGE_LOG.md +8 -3
- package/SKILLS.md +1 -1
- package/foundry.toml +7 -0
- package/package.json +8 -9
- package/src/REVDeployer.sol +34 -12
- package/src/REVLoans.sol +1 -5
- package/test/REVDeployerRegressions.t.sol +6 -3
- package/test/REVInvincibility.t.sol +6 -18
- package/test/REVLoansAttacks.t.sol +19 -11
- package/test/REVLoansFeeRecovery.t.sol +19 -11
- package/test/REVLoansFindings.t.sol +19 -11
- package/test/REVLoansRegressions.t.sol +19 -11
- package/test/REVLoansSourced.t.sol +0 -8
- package/test/TestCashOutCallerValidation.t.sol +74 -0
- package/test/TestLowFindings.t.sol +3 -1
- package/test/TestMixedFixes.t.sol +6 -4
- package/test/TestSplitWeightAdjustment.t.sol +10 -5
- package/test/fork/TestAutoIssuanceFork.t.sol +148 -0
- package/test/fork/TestCashOutFork.t.sol +22 -21
- package/test/fork/TestIssuanceDecayFork.t.sol +158 -0
- package/test/fork/TestLoanERC20Fork.t.sol +463 -0
- package/test/fork/TestLoanRepayFork.t.sol +2 -2
- package/test/fork/TestPermit2PaymentFork.t.sol +299 -0
- package/test/helpers/MaliciousContracts.sol +36 -22
- package/test/mock/MockBuybackDataHook.sol +50 -6
- package/deployments/revnet-core-v5/arbitrum/REVDeployer.json +0 -2821
- package/deployments/revnet-core-v5/arbitrum/REVLoans.json +0 -2260
- package/deployments/revnet-core-v5/arbitrum_sepolia/REVDeployer.json +0 -2821
- package/deployments/revnet-core-v5/arbitrum_sepolia/REVLoans.json +0 -2260
- package/deployments/revnet-core-v5/base/REVDeployer.json +0 -2825
- package/deployments/revnet-core-v5/base/REVLoans.json +0 -2264
- package/deployments/revnet-core-v5/base_sepolia/REVDeployer.json +0 -2825
- package/deployments/revnet-core-v5/base_sepolia/REVLoans.json +0 -2264
- package/deployments/revnet-core-v5/ethereum/REVDeployer.json +0 -2825
- package/deployments/revnet-core-v5/ethereum/REVLoans.json +0 -2264
- package/deployments/revnet-core-v5/optimism/REVDeployer.json +0 -2821
- package/deployments/revnet-core-v5/optimism/REVLoans.json +0 -2260
- package/deployments/revnet-core-v5/optimism_sepolia/REVDeployer.json +0 -2825
- package/deployments/revnet-core-v5/optimism_sepolia/REVLoans.json +0 -2264
- package/deployments/revnet-core-v5/sepolia/REVDeployer.json +0 -2825
- package/deployments/revnet-core-v5/sepolia/REVLoans.json +0 -2264
- package/docs/book.css +0 -13
- package/docs/book.toml +0 -13
- package/docs/solidity.min.js +0 -74
- package/docs/src/README.md +0 -185
- package/docs/src/SUMMARY.md +0 -18
- package/docs/src/src/README.md +0 -7
- package/docs/src/src/REVDeployer.sol/contract.REVDeployer.md +0 -999
- package/docs/src/src/REVLoans.sol/contract.REVLoans.md +0 -1108
- package/docs/src/src/interfaces/IREVDeployer.sol/interface.IREVDeployer.md +0 -525
- package/docs/src/src/interfaces/IREVLoans.sol/interface.IREVLoans.md +0 -598
- package/docs/src/src/interfaces/README.md +0 -5
- package/docs/src/src/structs/README.md +0 -12
- package/docs/src/src/structs/REVAutoIssuance.sol/struct.REVAutoIssuance.md +0 -19
- package/docs/src/src/structs/REVBuybackHookConfig.sol/struct.REVBuybackHookConfig.md +0 -19
- package/docs/src/src/structs/REVBuybackPoolConfig.sol/struct.REVBuybackPoolConfig.md +0 -21
- package/docs/src/src/structs/REVConfig.sol/struct.REVConfig.md +0 -23
- package/docs/src/src/structs/REVCroptopAllowedPost.sol/struct.REVCroptopAllowedPost.md +0 -32
- package/docs/src/src/structs/REVDeploy721TiersHookConfig.sol/struct.REVDeploy721TiersHookConfig.md +0 -34
- package/docs/src/src/structs/REVDescription.sol/struct.REVDescription.md +0 -23
- package/docs/src/src/structs/REVLoan.sol/struct.REVLoan.md +0 -28
- package/docs/src/src/structs/REVLoanSource.sol/struct.REVLoanSource.md +0 -16
- package/docs/src/src/structs/REVStageConfig.sol/struct.REVStageConfig.md +0 -44
- package/docs/src/src/structs/REVSuckerDeploymentConfig.sol/struct.REVSuckerDeploymentConfig.md +0 -16
package/CHANGE_LOG.md
CHANGED
|
@@ -253,6 +253,7 @@ The following structs are identical between v5 and v6 (only `forge-lint` comment
|
|
|
253
253
|
| **Deploy ordering** | v6 `_deploy721RevnetFor` deploys the revnet first via `_deployRevnetFor`, then deploys the 721 hook and sets split operator permissions. v5 deployed the 721 hook then called `_deployRevnetFor`. |
|
|
254
254
|
| **Croptop `maximumSplitPercent`** | v6 passes the new `maximumSplitPercent` field from `REVCroptopAllowedPost` to `CTAllowedPost`. |
|
|
255
255
|
| **Auto-initialized buyback pools** | During deployment, `_tryInitializeBuybackPoolFor` is called for every terminal token to set up Uniswap V4 buyback pools at a generic 1:1 `sqrtPriceX96`. Failures (e.g., pool already initialized) are silently caught via try-catch. |
|
|
256
|
+
| **Feeless beneficiary cashout routing** | `beforeCashOutRecordedWith` now checks `context.beneficiaryIsFeeless` and skips the 2.5% revnet fee when the cashout is routed by a feeless address (e.g., the router terminal routing value between projects). v5 did not have this check. The cash out tax rate still applies -- only the protocol fee is waived. |
|
|
256
257
|
|
|
257
258
|
### 6.2 REVLoans
|
|
258
259
|
|
|
@@ -271,7 +272,11 @@ The following structs are identical between v5 and v6 (only `forge-lint` comment
|
|
|
271
272
|
| **Liquidation cleanup** | v6 adds `delete _loanOf[loanId]` after burning a liquidated loan, clearing stale loan data for a gas refund. v5 did not clean up the loan data. |
|
|
272
273
|
| **`_totalBorrowedFrom` decimal normalization** | v6 normalizes token amounts from the source's native decimals to the target decimals before currency conversion. v5 did not perform decimal normalization, which could cause mixed-decimal arithmetic errors for tokens with non-18 decimals (e.g., USDC with 6 decimals). |
|
|
273
274
|
| **`_totalBorrowedFrom` zero-price safety** | v6 skips sources with a zero price to prevent division-by-zero panics that would DoS all loan operations. v5 did not handle this case. |
|
|
274
|
-
| **`_determineSourceFeeAmount` boundary** | v6
|
|
275
|
+
| **`_determineSourceFeeAmount` boundary fix** | An intermediate v6 revision used `>=` for the liquidation check, which created a 1-second window where neither repay nor liquidate could execute. This was fixed back to `>` (matching v5) so the exact boundary second is still repayable, while the liquidation path uses `<=`. |
|
|
276
|
+
| **BURN_TOKENS permission prerequisite (NatSpec only)** | `borrowFrom` documents via a `@dev` NatSpec comment that callers must first grant `BURN_TOKENS` permission to the loans contract via `JBPermissions.setPermissionsFor()`. This is required because collateral posting burns the caller's tokens through the controller. However, there is no runtime `hasPermission` check in `borrowFrom` itself -- if the permission is missing, the transaction will revert later inside `JBController.burnTokensOf`. v5 did not document this requirement. |
|
|
277
|
+
| **Cross-revnet liquidation guard** | `liquidateExpiredLoansFrom` now validates that `startingLoanId + count` does not exceed `_ONE_TRILLION`, preventing callers from overflowing into a different revnet's loan ID namespace via `_generateLoanId`. Reverts with `REVLoans_LoanIdOverflow()`. v5 did not have this bounds check. |
|
|
278
|
+
| **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. |
|
|
279
|
+
| **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. |
|
|
275
280
|
| **`ReallocateCollateral` event typo fix** | v5 used `removedcollateralCount` (lowercase 'c'). v6 fixes it to `removedCollateralCount` (uppercase 'C'). |
|
|
276
281
|
| **NatSpec documentation** | Extensive NatSpec added to all functions, views, and internal helpers. Flash loan safety analysis documented in `_borrowableAmountFrom`. |
|
|
277
282
|
|
|
@@ -294,8 +299,8 @@ Throughout the codebase, function calls were updated to use named argument synta
|
|
|
294
299
|
|
|
295
300
|
| v5 | v6 | Notes |
|
|
296
301
|
|----|----|-------|
|
|
297
|
-
| `REVDeployer` | `REVDeployer` | Buyback hook architecture changed from per-revnet mapping to immutable registry. Loans changed from per-revnet to single immutable. Deploy functions consolidated. Every revnet gets a 721 hook. 721 permission flags inverted. `beforePayRecordedWith` rewritten for split-aware weight scaling. `burnHeldTokensOf` added. Split operator gains 3 new default permissions. |
|
|
298
|
-
| `REVLoans` | `REVLoans` | Deployer dependency removed. Terminal validation replaces deployer ownership check. `numberOfLoansFor` renamed. `reallocateCollateralFromLoan` not payable. Source mismatch, zero borrow, and nothing-to-repay checks added. Liquidation loop uses `continue` instead of `break`. Stale loan data cleaned up on liquidation. Decimal normalization and zero-price safety in `_totalBorrowedFrom`. |
|
|
302
|
+
| `REVDeployer` | `REVDeployer` | Buyback hook architecture changed from per-revnet mapping to immutable registry. Loans changed from per-revnet to single immutable. Deploy functions consolidated. Every revnet gets a 721 hook. 721 permission flags inverted. `beforePayRecordedWith` rewritten for split-aware weight scaling. `burnHeldTokensOf` added. Split operator gains 3 new default permissions (`SET_BUYBACK_HOOK`, `SET_ROUTER_TERMINAL`, `SET_TOKEN_METADATA`). Feeless beneficiary cashout routing skips fee for feeless addresses. |
|
|
303
|
+
| `REVLoans` | `REVLoans` | Deployer dependency removed. Terminal validation replaces deployer ownership check. `numberOfLoansFor` renamed. `reallocateCollateralFromLoan` not payable. Source mismatch, zero borrow, and nothing-to-repay checks added. BURN_TOKENS permission requirement documented via NatSpec (no runtime check). Cross-revnet liquidation guard prevents loan ID namespace overflow. Liquidation loop uses `continue` instead of `break`. Stale loan data cleaned up on liquidation. Decimal normalization and zero-price safety in `_totalBorrowedFrom`. Source fee payment wrapped in try-catch. Timestamp cast fixed from `uint40` to `uint48`. |
|
|
299
304
|
|
|
300
305
|
### Structs
|
|
301
306
|
|
package/SKILLS.md
CHANGED
|
@@ -157,7 +157,7 @@ Deploy and manage Revnets -- autonomous, unowned Juicebox projects with staged i
|
|
|
157
157
|
17. **Loan fee model.** Three layers: (1) REV protocol fee (1%) taken when funds pulled, (2) terminal fee (2.5%) charged by `useAllowanceOf`, (3) prepaid source fee (2.5%-50%, borrower-chosen) that buys an interest-free window. After the prepaid window, time-proportional source fee accrues linearly over the remaining 10-year loan duration.
|
|
158
158
|
18. **Permit2 fallback.** `REVLoans` uses permit2 for ERC-20 transfers as a fallback when standard allowance is insufficient. Wrapped in try-catch.
|
|
159
159
|
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`.
|
|
160
|
-
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.
|
|
160
|
+
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`.
|
|
161
161
|
|
|
162
162
|
### NATIVE_TOKEN Accounting on Non-ETH Chains
|
|
163
163
|
|
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.15",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
"node": ">=20.0.0"
|
|
11
11
|
},
|
|
12
12
|
"scripts": {
|
|
13
|
-
"postinstall": "find node_modules -name '*.sol' -type f | xargs grep -l 'pragma solidity 0.8.23;' 2>/dev/null | xargs sed -i '' 's/pragma solidity 0.8.23;/pragma solidity 0.8.26;/g' 2>/dev/null || true",
|
|
14
13
|
"test": "forge test",
|
|
15
14
|
"coverage": "forge coverage --match-path \"./src/*.sol\" --report lcov --report summary",
|
|
16
15
|
"deploy:mainnets": "source ./.env && export START_TIME=$(date +%s) && npx sphinx propose ./script/Deploy.s.sol --networks mainnets",
|
|
@@ -20,14 +19,14 @@
|
|
|
20
19
|
"artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'revnet-core-v6'"
|
|
21
20
|
},
|
|
22
21
|
"dependencies": {
|
|
23
|
-
"@bananapus/721-hook-v6": "^0.0.
|
|
24
|
-
"@bananapus/buyback-hook-v6": "^0.0.
|
|
25
|
-
"@bananapus/core-v6": "^0.0.
|
|
26
|
-
"@bananapus/ownable-v6": "^0.0.
|
|
22
|
+
"@bananapus/721-hook-v6": "^0.0.19",
|
|
23
|
+
"@bananapus/buyback-hook-v6": "^0.0.16",
|
|
24
|
+
"@bananapus/core-v6": "^0.0.24",
|
|
25
|
+
"@bananapus/ownable-v6": "^0.0.12",
|
|
27
26
|
"@bananapus/permission-ids-v6": "^0.0.10",
|
|
28
|
-
"@bananapus/router-terminal-v6": "^0.0.
|
|
29
|
-
"@bananapus/suckers-v6": "^0.0.
|
|
30
|
-
"@croptop/core-v6": "^0.0.
|
|
27
|
+
"@bananapus/router-terminal-v6": "^0.0.16",
|
|
28
|
+
"@bananapus/suckers-v6": "^0.0.13",
|
|
29
|
+
"@croptop/core-v6": "^0.0.20",
|
|
31
30
|
"@openzeppelin/contracts": "^5.6.1",
|
|
32
31
|
"@uniswap/v4-core": "^1.0.2",
|
|
33
32
|
"@uniswap/v4-periphery": "^1.0.3"
|
package/src/REVDeployer.sol
CHANGED
|
@@ -271,18 +271,19 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
|
|
|
271
271
|
IJBTerminal feeTerminal = DIRECTORY.primaryTerminalOf({projectId: FEE_REVNET_ID, token: context.surplus.token});
|
|
272
272
|
|
|
273
273
|
// If there's no cash out tax (100% cash out tax rate), if there's no fee terminal, or if the beneficiary is
|
|
274
|
-
// feeless (e.g. the router terminal routing value between projects),
|
|
274
|
+
// feeless (e.g. the router terminal routing value between projects), proxy directly to the buyback hook.
|
|
275
275
|
if (context.cashOutTaxRate == 0 || address(feeTerminal) == address(0) || context.beneficiaryIsFeeless) {
|
|
276
|
-
|
|
276
|
+
// slither-disable-next-line unused-return
|
|
277
|
+
return BUYBACK_HOOK.beforeCashOutRecordedWith(context);
|
|
277
278
|
}
|
|
278
279
|
|
|
279
|
-
//
|
|
280
|
+
// Split the cashed-out tokens into a fee portion and a non-fee portion.
|
|
280
281
|
// Micro cash outs (< 40 wei at 2.5% fee) round feeCashOutCount to zero, bypassing the fee.
|
|
281
282
|
// Economically insignificant: the gas cost of the transaction far exceeds the bypassed fee. No fix needed.
|
|
282
283
|
uint256 feeCashOutCount = mulDiv({x: context.cashOutCount, y: FEE, denominator: JBConstants.MAX_FEE});
|
|
283
284
|
uint256 nonFeeCashOutCount = context.cashOutCount - feeCashOutCount;
|
|
284
285
|
|
|
285
|
-
//
|
|
286
|
+
// Calculate how much surplus the non-fee tokens can reclaim via the bonding curve.
|
|
286
287
|
uint256 postFeeReclaimedAmount = JBCashOuts.cashOutFrom({
|
|
287
288
|
surplus: context.surplus.value,
|
|
288
289
|
cashOutCount: nonFeeCashOutCount,
|
|
@@ -290,7 +291,7 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
|
|
|
290
291
|
cashOutTaxRate: context.cashOutTaxRate
|
|
291
292
|
});
|
|
292
293
|
|
|
293
|
-
//
|
|
294
|
+
// Calculate how much the fee tokens reclaim from the remaining surplus after the non-fee reclaim.
|
|
294
295
|
uint256 feeAmount = JBCashOuts.cashOutFrom({
|
|
295
296
|
surplus: context.surplus.value - postFeeReclaimedAmount,
|
|
296
297
|
cashOutCount: feeCashOutCount,
|
|
@@ -298,15 +299,36 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
|
|
|
298
299
|
cashOutTaxRate: context.cashOutTaxRate
|
|
299
300
|
});
|
|
300
301
|
|
|
301
|
-
//
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
302
|
+
// Build a context for the buyback hook using only the non-fee token count.
|
|
303
|
+
JBBeforeCashOutRecordedContext memory buybackHookContext = context;
|
|
304
|
+
buybackHookContext.cashOutCount = nonFeeCashOutCount;
|
|
305
|
+
|
|
306
|
+
// Let the buyback hook adjust the cash out parameters and optionally return a hook specification.
|
|
307
|
+
JBCashOutHookSpecification[] memory buybackHookSpecifications;
|
|
308
|
+
(cashOutTaxRate, cashOutCount, totalSupply, buybackHookSpecifications) =
|
|
309
|
+
BUYBACK_HOOK.beforeCashOutRecordedWith(buybackHookContext);
|
|
310
|
+
|
|
311
|
+
// If the fee rounds down to zero, return the buyback hook's response directly — no fee to process.
|
|
312
|
+
if (feeAmount == 0) return (cashOutTaxRate, cashOutCount, totalSupply, buybackHookSpecifications);
|
|
313
|
+
|
|
314
|
+
// Build a hook spec that routes the fee amount to this contract's `afterCashOutRecordedWith` for processing.
|
|
315
|
+
JBCashOutHookSpecification memory feeSpec = JBCashOutHookSpecification({
|
|
316
|
+
hook: IJBCashOutHook(address(this)), noop: false, amount: feeAmount, metadata: abi.encode(feeTerminal)
|
|
305
317
|
});
|
|
306
318
|
|
|
307
|
-
//
|
|
308
|
-
|
|
309
|
-
|
|
319
|
+
// Compose the final hook specifications: buyback spec (if any) + fee spec.
|
|
320
|
+
if (buybackHookSpecifications.length > 0) {
|
|
321
|
+
// The buyback hook returned a spec — include it before the fee spec.
|
|
322
|
+
hookSpecifications = new JBCashOutHookSpecification[](2);
|
|
323
|
+
hookSpecifications[0] = buybackHookSpecifications[0];
|
|
324
|
+
hookSpecifications[1] = feeSpec;
|
|
325
|
+
} else {
|
|
326
|
+
// No buyback spec — only the fee spec.
|
|
327
|
+
hookSpecifications = new JBCashOutHookSpecification[](1);
|
|
328
|
+
hookSpecifications[0] = feeSpec;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return (cashOutTaxRate, cashOutCount, totalSupply, hookSpecifications);
|
|
310
332
|
}
|
|
311
333
|
|
|
312
334
|
/// @notice Before a revnet processes an incoming payment, determine the weight and pay hooks to use.
|
package/src/REVLoans.sol
CHANGED
|
@@ -339,11 +339,7 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
339
339
|
|
|
340
340
|
// Get the surplus of all the revnet's terminals in terms of the native currency.
|
|
341
341
|
uint256 totalSurplus = JBSurplus.currentSurplusOf({
|
|
342
|
-
projectId: revnetId,
|
|
343
|
-
terminals: terminals,
|
|
344
|
-
accountingContexts: new JBAccountingContext[](0),
|
|
345
|
-
decimals: decimals,
|
|
346
|
-
currency: currency
|
|
342
|
+
projectId: revnetId, terminals: terminals, tokens: new address[](0), decimals: decimals, currency: currency
|
|
347
343
|
});
|
|
348
344
|
|
|
349
345
|
// Get the total amount the revnet currently has loaned out, in terms of the native currency with 18
|
|
@@ -138,7 +138,8 @@ contract REVDeployerRegressions is TestBaseWorkflow {
|
|
|
138
138
|
assertEq(correctIndex, 0, "buyback hook should use index 0 when no tiered hook");
|
|
139
139
|
|
|
140
140
|
// Write to the correct index (no revert)
|
|
141
|
-
specs[correctIndex] =
|
|
141
|
+
specs[correctIndex] =
|
|
142
|
+
JBPayHookSpecification({hook: IJBPayHook(address(0xbeef)), noop: false, amount: 1 ether, metadata: ""});
|
|
142
143
|
}
|
|
143
144
|
|
|
144
145
|
/// @notice Verify both hooks present works fine (no OOB).
|
|
@@ -150,8 +151,10 @@ contract REVDeployerRegressions is TestBaseWorkflow {
|
|
|
150
151
|
assertEq(arraySize, 2, "array should be size 2");
|
|
151
152
|
|
|
152
153
|
JBPayHookSpecification[] memory specs = new JBPayHookSpecification[](arraySize);
|
|
153
|
-
specs[0] =
|
|
154
|
-
|
|
154
|
+
specs[0] =
|
|
155
|
+
JBPayHookSpecification({hook: IJBPayHook(address(0xdead)), noop: false, amount: 1 ether, metadata: ""});
|
|
156
|
+
specs[1] =
|
|
157
|
+
JBPayHookSpecification({hook: IJBPayHook(address(0xbeef)), noop: false, amount: 2 ether, metadata: ""});
|
|
155
158
|
}
|
|
156
159
|
|
|
157
160
|
//*********************************************************************//
|
|
@@ -368,7 +368,8 @@ contract REVInvincibility_PropertyTests is TestBaseWorkflow {
|
|
|
368
368
|
|
|
369
369
|
// Verify safe write
|
|
370
370
|
JBPayHookSpecification[] memory specs = new JBPayHookSpecification[](arraySize);
|
|
371
|
-
specs[correctIndex] =
|
|
371
|
+
specs[correctIndex] =
|
|
372
|
+
JBPayHookSpecification({hook: IJBPayHook(address(0xbeef)), noop: false, amount: 1 ether, metadata: ""});
|
|
372
373
|
}
|
|
373
374
|
|
|
374
375
|
/// @notice Reentrancy — _adjust calls terminal.pay() BEFORE writing loan state.
|
|
@@ -909,12 +910,8 @@ contract REVInvincibility_PropertyTests is TestBaseWorkflow {
|
|
|
909
910
|
// Record fee project balance before cash-out
|
|
910
911
|
uint256 feeBalanceBefore;
|
|
911
912
|
{
|
|
912
|
-
JBAccountingContext[] memory feeCtx = new JBAccountingContext[](1);
|
|
913
|
-
feeCtx[0] = JBAccountingContext({
|
|
914
|
-
token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
915
|
-
});
|
|
916
913
|
feeBalanceBefore = jbMultiTerminal()
|
|
917
|
-
.currentSurplusOf(FEE_PROJECT_ID,
|
|
914
|
+
.currentSurplusOf(FEE_PROJECT_ID, new address[](0), 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
|
|
918
915
|
}
|
|
919
916
|
|
|
920
917
|
// Cash out
|
|
@@ -935,12 +932,8 @@ contract REVInvincibility_PropertyTests is TestBaseWorkflow {
|
|
|
935
932
|
// because both the terminal fee AND the revnet fee route to it
|
|
936
933
|
uint256 feeBalanceAfter;
|
|
937
934
|
{
|
|
938
|
-
JBAccountingContext[] memory feeCtx = new JBAccountingContext[](1);
|
|
939
|
-
feeCtx[0] = JBAccountingContext({
|
|
940
|
-
token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
941
|
-
});
|
|
942
935
|
feeBalanceAfter = jbMultiTerminal()
|
|
943
|
-
.currentSurplusOf(FEE_PROJECT_ID,
|
|
936
|
+
.currentSurplusOf(FEE_PROJECT_ID, new address[](0), 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
|
|
944
937
|
}
|
|
945
938
|
|
|
946
939
|
// Fee project should have received fees from the cash-out
|
|
@@ -1206,13 +1199,8 @@ contract REVInvincibility_Invariants is StdInvariant, TestBaseWorkflow {
|
|
|
1206
1199
|
function invariant_REV_1_surplusCoversLoans() public {
|
|
1207
1200
|
uint256 totalBorrowed = LOANS_CONTRACT.totalBorrowedFrom(REVNET_ID, jbMultiTerminal(), JBConstants.NATIVE_TOKEN);
|
|
1208
1201
|
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
1212
|
-
});
|
|
1213
|
-
|
|
1214
|
-
uint256 storeBalance =
|
|
1215
|
-
jbMultiTerminal().currentSurplusOf(REVNET_ID, ctxArray, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
|
|
1202
|
+
uint256 storeBalance = jbMultiTerminal()
|
|
1203
|
+
.currentSurplusOf(REVNET_ID, new address[](0), 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
|
|
1216
1204
|
|
|
1217
1205
|
// Note: storeBalance is surplus (after payout limits), but the terminal holds at least this much
|
|
1218
1206
|
// The total borrowed should not exceed what the terminal can cover
|
|
@@ -42,6 +42,8 @@ import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressReg
|
|
|
42
42
|
import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
|
|
43
43
|
import {REVEmpty721Config} from "./helpers/REVEmpty721Config.sol";
|
|
44
44
|
import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
|
|
45
|
+
import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
|
|
46
|
+
import {JBPayHookSpecification} from "@bananapus/core-v6/src/structs/JBPayHookSpecification.sol";
|
|
45
47
|
|
|
46
48
|
/// @notice A malicious terminal that re-enters REVLoans during fee payment in _adjust().
|
|
47
49
|
/// @dev Reentrancy during pay() callback in _adjust.
|
|
@@ -128,17 +130,7 @@ contract ReentrantTerminal is ERC165, IJBPayoutTerminal {
|
|
|
128
130
|
override
|
|
129
131
|
{}
|
|
130
132
|
|
|
131
|
-
function currentSurplusOf(
|
|
132
|
-
uint256,
|
|
133
|
-
JBAccountingContext[] memory,
|
|
134
|
-
uint256,
|
|
135
|
-
uint256
|
|
136
|
-
)
|
|
137
|
-
external
|
|
138
|
-
pure
|
|
139
|
-
override
|
|
140
|
-
returns (uint256)
|
|
141
|
-
{
|
|
133
|
+
function currentSurplusOf(uint256, address[] calldata, uint256, uint256) external pure override returns (uint256) {
|
|
142
134
|
return 0;
|
|
143
135
|
}
|
|
144
136
|
|
|
@@ -168,6 +160,22 @@ contract ReentrantTerminal is ERC165, IJBPayoutTerminal {
|
|
|
168
160
|
return 0;
|
|
169
161
|
}
|
|
170
162
|
|
|
163
|
+
function previewPayFor(
|
|
164
|
+
uint256,
|
|
165
|
+
address,
|
|
166
|
+
uint256,
|
|
167
|
+
address,
|
|
168
|
+
bytes calldata
|
|
169
|
+
)
|
|
170
|
+
external
|
|
171
|
+
pure
|
|
172
|
+
override
|
|
173
|
+
returns (JBRuleset memory, uint256, uint256, JBPayHookSpecification[] memory)
|
|
174
|
+
{
|
|
175
|
+
JBRuleset memory ruleset;
|
|
176
|
+
return (ruleset, 0, 0, new JBPayHookSpecification[](0));
|
|
177
|
+
}
|
|
178
|
+
|
|
171
179
|
function supportsInterface(bytes4 interfaceId) public view override(ERC165, IERC165) returns (bool) {
|
|
172
180
|
return interfaceId == type(IJBTerminal).interfaceId || interfaceId == type(IJBPayoutTerminal).interfaceId
|
|
173
181
|
|| super.supportsInterface(interfaceId);
|
|
@@ -42,6 +42,8 @@ import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStor
|
|
|
42
42
|
import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
|
|
43
43
|
import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
|
|
44
44
|
import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
|
|
45
|
+
import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
|
|
46
|
+
import {JBPayHookSpecification} from "@bananapus/core-v6/src/structs/JBPayHookSpecification.sol";
|
|
45
47
|
import {REVEmpty721Config} from "./helpers/REVEmpty721Config.sol";
|
|
46
48
|
|
|
47
49
|
/// @notice A terminal mock that always reverts on pay(), used to simulate fee payment failure.
|
|
@@ -87,17 +89,7 @@ contract RevertingFeeTerminal is ERC165, IJBPayoutTerminal {
|
|
|
87
89
|
override
|
|
88
90
|
{}
|
|
89
91
|
|
|
90
|
-
function currentSurplusOf(
|
|
91
|
-
uint256,
|
|
92
|
-
JBAccountingContext[] memory,
|
|
93
|
-
uint256,
|
|
94
|
-
uint256
|
|
95
|
-
)
|
|
96
|
-
external
|
|
97
|
-
pure
|
|
98
|
-
override
|
|
99
|
-
returns (uint256)
|
|
100
|
-
{
|
|
92
|
+
function currentSurplusOf(uint256, address[] calldata, uint256, uint256) external pure override returns (uint256) {
|
|
101
93
|
return 0;
|
|
102
94
|
}
|
|
103
95
|
|
|
@@ -127,6 +119,22 @@ contract RevertingFeeTerminal is ERC165, IJBPayoutTerminal {
|
|
|
127
119
|
return 0;
|
|
128
120
|
}
|
|
129
121
|
|
|
122
|
+
function previewPayFor(
|
|
123
|
+
uint256,
|
|
124
|
+
address,
|
|
125
|
+
uint256,
|
|
126
|
+
address,
|
|
127
|
+
bytes calldata
|
|
128
|
+
)
|
|
129
|
+
external
|
|
130
|
+
pure
|
|
131
|
+
override
|
|
132
|
+
returns (JBRuleset memory, uint256, uint256, JBPayHookSpecification[] memory)
|
|
133
|
+
{
|
|
134
|
+
JBRuleset memory ruleset;
|
|
135
|
+
return (ruleset, 0, 0, new JBPayHookSpecification[](0));
|
|
136
|
+
}
|
|
137
|
+
|
|
130
138
|
function supportsInterface(bytes4 interfaceId) public view override(ERC165, IERC165) returns (bool) {
|
|
131
139
|
return interfaceId == type(IJBTerminal).interfaceId || interfaceId == type(IJBPayoutTerminal).interfaceId
|
|
132
140
|
|| super.supportsInterface(interfaceId);
|
|
@@ -42,6 +42,8 @@ import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
|
|
|
42
42
|
import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
|
|
43
43
|
import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
|
|
44
44
|
import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
|
|
45
|
+
import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
|
|
46
|
+
import {JBPayHookSpecification} from "@bananapus/core-v6/src/structs/JBPayHookSpecification.sol";
|
|
45
47
|
import {REVEmpty721Config} from "./helpers/REVEmpty721Config.sol";
|
|
46
48
|
|
|
47
49
|
/// @notice A fake terminal that returns garbage accounting contexts.
|
|
@@ -91,17 +93,7 @@ contract GarbageTerminal is ERC165, IJBPayoutTerminal {
|
|
|
91
93
|
override
|
|
92
94
|
{}
|
|
93
95
|
|
|
94
|
-
function currentSurplusOf(
|
|
95
|
-
uint256,
|
|
96
|
-
JBAccountingContext[] memory,
|
|
97
|
-
uint256,
|
|
98
|
-
uint256
|
|
99
|
-
)
|
|
100
|
-
external
|
|
101
|
-
pure
|
|
102
|
-
override
|
|
103
|
-
returns (uint256)
|
|
104
|
-
{
|
|
96
|
+
function currentSurplusOf(uint256, address[] calldata, uint256, uint256) external pure override returns (uint256) {
|
|
105
97
|
return 0;
|
|
106
98
|
}
|
|
107
99
|
|
|
@@ -130,6 +122,22 @@ contract GarbageTerminal is ERC165, IJBPayoutTerminal {
|
|
|
130
122
|
return 0;
|
|
131
123
|
}
|
|
132
124
|
|
|
125
|
+
function previewPayFor(
|
|
126
|
+
uint256,
|
|
127
|
+
address,
|
|
128
|
+
uint256,
|
|
129
|
+
address,
|
|
130
|
+
bytes calldata
|
|
131
|
+
)
|
|
132
|
+
external
|
|
133
|
+
pure
|
|
134
|
+
override
|
|
135
|
+
returns (JBRuleset memory, uint256, uint256, JBPayHookSpecification[] memory)
|
|
136
|
+
{
|
|
137
|
+
JBRuleset memory ruleset;
|
|
138
|
+
return (ruleset, 0, 0, new JBPayHookSpecification[](0));
|
|
139
|
+
}
|
|
140
|
+
|
|
133
141
|
function supportsInterface(bytes4 interfaceId) public view override(ERC165, IERC165) returns (bool) {
|
|
134
142
|
return interfaceId == type(IJBTerminal).interfaceId || interfaceId == type(IJBPayoutTerminal).interfaceId
|
|
135
143
|
|| super.supportsInterface(interfaceId);
|
|
@@ -38,6 +38,8 @@ import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStor
|
|
|
38
38
|
import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
|
|
39
39
|
import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
|
|
40
40
|
import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
|
|
41
|
+
import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
|
|
42
|
+
import {JBPayHookSpecification} from "@bananapus/core-v6/src/structs/JBPayHookSpecification.sol";
|
|
41
43
|
import {REVEmpty721Config} from "./helpers/REVEmpty721Config.sol";
|
|
42
44
|
|
|
43
45
|
/// @notice A fake terminal that tracks whether useAllowanceOf was called.
|
|
@@ -92,17 +94,7 @@ contract FakeTerminal is ERC165, IJBPayoutTerminal {
|
|
|
92
94
|
override
|
|
93
95
|
{}
|
|
94
96
|
|
|
95
|
-
function currentSurplusOf(
|
|
96
|
-
uint256,
|
|
97
|
-
JBAccountingContext[] memory,
|
|
98
|
-
uint256,
|
|
99
|
-
uint256
|
|
100
|
-
)
|
|
101
|
-
external
|
|
102
|
-
pure
|
|
103
|
-
override
|
|
104
|
-
returns (uint256)
|
|
105
|
-
{
|
|
97
|
+
function currentSurplusOf(uint256, address[] calldata, uint256, uint256) external pure override returns (uint256) {
|
|
106
98
|
return 0;
|
|
107
99
|
}
|
|
108
100
|
|
|
@@ -131,6 +123,22 @@ contract FakeTerminal is ERC165, IJBPayoutTerminal {
|
|
|
131
123
|
return 0;
|
|
132
124
|
}
|
|
133
125
|
|
|
126
|
+
function previewPayFor(
|
|
127
|
+
uint256,
|
|
128
|
+
address,
|
|
129
|
+
uint256,
|
|
130
|
+
address,
|
|
131
|
+
bytes calldata
|
|
132
|
+
)
|
|
133
|
+
external
|
|
134
|
+
pure
|
|
135
|
+
override
|
|
136
|
+
returns (JBRuleset memory, uint256, uint256, JBPayHookSpecification[] memory)
|
|
137
|
+
{
|
|
138
|
+
JBRuleset memory ruleset;
|
|
139
|
+
return (ruleset, 0, 0, new JBPayHookSpecification[](0));
|
|
140
|
+
}
|
|
141
|
+
|
|
134
142
|
function supportsInterface(bytes4 interfaceId) public view override(ERC165, IERC165) returns (bool) {
|
|
135
143
|
return interfaceId == type(IJBTerminal).interfaceId || interfaceId == type(IJBPayoutTerminal).interfaceId
|
|
136
144
|
|| super.supportsInterface(interfaceId);
|
|
@@ -756,14 +756,6 @@ contract REVLoansSourcedTests is TestBaseWorkflow {
|
|
|
756
756
|
|
|
757
757
|
uint256 balanceBefore = _balanceOf(token, USER);
|
|
758
758
|
|
|
759
|
-
// Ensure that the hook was called.
|
|
760
|
-
vm.expectCall(address(REV_DEPLOYER), abi.encode(REVDeployer.beforeCashOutRecordedWith.selector));
|
|
761
|
-
|
|
762
|
-
// It only adds itself as a `after` cashoutHook if there is a cashout tax rate.
|
|
763
|
-
if (cashOutTaxRate > 0) {
|
|
764
|
-
vm.expectCall(address(REV_DEPLOYER), abi.encode(REVDeployer.afterCashOutRecordedWith.selector));
|
|
765
|
-
}
|
|
766
|
-
|
|
767
759
|
// Perform a cashout.
|
|
768
760
|
vm.prank(USER);
|
|
769
761
|
jbMultiTerminal().cashOutTokensOf(USER, revnetProjectId, tokensToCashout, token, 0, payable(USER), bytes(""));
|
|
@@ -22,8 +22,12 @@ import "@bananapus/suckers-v6/script/helpers/SuckerDeploymentLib.sol";
|
|
|
22
22
|
import "@croptop/core-v6/script/helpers/CroptopDeploymentLib.sol";
|
|
23
23
|
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
24
24
|
import "@bananapus/router-terminal-v6/script/helpers/RouterTerminalDeploymentLib.sol";
|
|
25
|
+
import {JBCashOuts} from "@bananapus/core-v6/src/libraries/JBCashOuts.sol";
|
|
25
26
|
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
26
27
|
import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
|
|
28
|
+
import {JBBeforeCashOutRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforeCashOutRecordedContext.sol";
|
|
29
|
+
import {JBCashOutHookSpecification} from "@bananapus/core-v6/src/structs/JBCashOutHookSpecification.sol";
|
|
30
|
+
import {JBTokenAmount} from "@bananapus/core-v6/src/structs/JBTokenAmount.sol";
|
|
27
31
|
import {MockERC20} from "@bananapus/core-v6/test/mock/MockERC20.sol";
|
|
28
32
|
import {REVLoans} from "../src/REVLoans.sol";
|
|
29
33
|
import {REVStageConfig, REVAutoIssuance} from "../src/structs/REVStageConfig.sol";
|
|
@@ -302,6 +306,76 @@ contract TestCashOutCallerValidation is TestBaseWorkflow {
|
|
|
302
306
|
assertGt(feeBalanceAfter, feeBalanceBefore, "Fee project balance should increase from cash out fee");
|
|
303
307
|
}
|
|
304
308
|
|
|
309
|
+
/// @notice Revnet cash-out fees and buyback sell-side specs are composed together.
|
|
310
|
+
function test_beforeCashOutRecordedWith_proxiesIntoBuybackAndAppendsFeeSpec() public {
|
|
311
|
+
bytes memory buybackMetadata = abi.encode(uint256(123));
|
|
312
|
+
MOCK_BUYBACK.configureCashOutResult({
|
|
313
|
+
cashOutTaxRate: JBConstants.MAX_CASH_OUT_TAX_RATE,
|
|
314
|
+
cashOutCount: 0,
|
|
315
|
+
totalSupply: 0,
|
|
316
|
+
hookAmount: 0,
|
|
317
|
+
hookMetadata: buybackMetadata
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
JBBeforeCashOutRecordedContext memory context = JBBeforeCashOutRecordedContext({
|
|
321
|
+
terminal: address(jbMultiTerminal()),
|
|
322
|
+
holder: USER,
|
|
323
|
+
projectId: REVNET_ID,
|
|
324
|
+
rulesetId: 0,
|
|
325
|
+
cashOutCount: 1000,
|
|
326
|
+
totalSupply: 10_000,
|
|
327
|
+
surplus: JBTokenAmount({
|
|
328
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
329
|
+
value: 100 ether,
|
|
330
|
+
decimals: 18,
|
|
331
|
+
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
332
|
+
}),
|
|
333
|
+
useTotalSurplus: true,
|
|
334
|
+
cashOutTaxRate: 3000,
|
|
335
|
+
beneficiaryIsFeeless: false,
|
|
336
|
+
metadata: ""
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
(
|
|
340
|
+
uint256 cashOutTaxRate,
|
|
341
|
+
uint256 cashOutCount,
|
|
342
|
+
uint256 totalSupply,
|
|
343
|
+
JBCashOutHookSpecification[] memory hookSpecifications
|
|
344
|
+
) = REV_DEPLOYER.beforeCashOutRecordedWith(context);
|
|
345
|
+
|
|
346
|
+
uint256 feeCashOutCount = context.cashOutCount * REV_DEPLOYER.FEE() / JBConstants.MAX_FEE;
|
|
347
|
+
uint256 nonFeeCashOutCount = context.cashOutCount - feeCashOutCount;
|
|
348
|
+
uint256 postFeeReclaimedAmount = JBCashOuts.cashOutFrom({
|
|
349
|
+
surplus: context.surplus.value,
|
|
350
|
+
cashOutCount: nonFeeCashOutCount,
|
|
351
|
+
totalSupply: context.totalSupply,
|
|
352
|
+
cashOutTaxRate: context.cashOutTaxRate
|
|
353
|
+
});
|
|
354
|
+
uint256 feeAmount = JBCashOuts.cashOutFrom({
|
|
355
|
+
surplus: context.surplus.value - postFeeReclaimedAmount,
|
|
356
|
+
cashOutCount: feeCashOutCount,
|
|
357
|
+
totalSupply: context.totalSupply - nonFeeCashOutCount,
|
|
358
|
+
cashOutTaxRate: context.cashOutTaxRate
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
assertEq(cashOutTaxRate, JBConstants.MAX_CASH_OUT_TAX_RATE, "Buyback cash out tax rate should be forwarded");
|
|
362
|
+
assertEq(cashOutCount, nonFeeCashOutCount, "Buyback should receive the non-fee cash out count");
|
|
363
|
+
assertEq(totalSupply, context.totalSupply, "Total supply should pass through");
|
|
364
|
+
assertEq(hookSpecifications.length, 2, "Buyback spec and revnet fee spec should both be returned");
|
|
365
|
+
|
|
366
|
+
assertEq(address(hookSpecifications[0].hook), address(MOCK_BUYBACK), "First hook spec should come from buyback");
|
|
367
|
+
assertEq(hookSpecifications[0].amount, 0, "Buyback sell-side spec should preserve its forwarded amount");
|
|
368
|
+
assertEq(hookSpecifications[0].metadata, buybackMetadata, "Buyback metadata should be preserved");
|
|
369
|
+
|
|
370
|
+
assertEq(address(hookSpecifications[1].hook), address(REV_DEPLOYER), "Second hook spec should charge fee");
|
|
371
|
+
assertEq(hookSpecifications[1].amount, feeAmount, "Fee spec amount should match the revnet fee math");
|
|
372
|
+
assertEq(
|
|
373
|
+
hookSpecifications[1].metadata,
|
|
374
|
+
abi.encode(jbMultiTerminal()),
|
|
375
|
+
"Fee spec metadata should encode the fee terminal"
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
305
379
|
/// @notice Test that afterCashOutRecordedWith has no access control — anyone can call it.
|
|
306
380
|
/// A non-terminal caller would just be donating their own funds as fees.
|
|
307
381
|
function test_nonTerminalCaller_justDonatesOwnFunds() public {
|
|
@@ -627,7 +627,9 @@ contract TestLowFindings is TestBaseWorkflow {
|
|
|
627
627
|
assertEq(borrowable, 0, "Borrowable amount for 1 wei of collateral should be 0");
|
|
628
628
|
|
|
629
629
|
// Mock the BURN permission (permission ID 11) for the loans contract.
|
|
630
|
-
mockExpect
|
|
630
|
+
// Use vm.mockCall only (not mockExpect which also adds vm.expectCall) because
|
|
631
|
+
// borrowFrom reverts with REVLoans_ZeroBorrowAmount before the permission check is reached.
|
|
632
|
+
vm.mockCall(
|
|
631
633
|
address(jbPermissions()),
|
|
632
634
|
abi.encodeCall(IJBPermissions.hasPermission, (address(LOANS_CONTRACT), USER, revnetId, 11, true, true)),
|
|
633
635
|
abi.encode(true)
|
|
@@ -240,14 +240,16 @@ contract TestMixedFixes is TestBaseWorkflow {
|
|
|
240
240
|
|
|
241
241
|
REVLoan memory loan = LOANS_CONTRACT.loanOf(loanId);
|
|
242
242
|
|
|
243
|
-
// Warp to
|
|
244
|
-
|
|
243
|
+
// Warp to one second past LOAN_LIQUIDATION_DURATION after creation.
|
|
244
|
+
// The contract uses `>` (not `>=`) so the exact boundary is still repayable;
|
|
245
|
+
// we need to exceed the boundary by 1 second to trigger the revert.
|
|
246
|
+
vm.warp(loan.createdAt + LOANS_CONTRACT.LOAN_LIQUIDATION_DURATION() + 1);
|
|
245
247
|
|
|
246
|
-
//
|
|
248
|
+
// timeSinceLoanCreated > LOAN_LIQUIDATION_DURATION → revert
|
|
247
249
|
vm.expectRevert(
|
|
248
250
|
abi.encodeWithSelector(
|
|
249
251
|
REVLoans.REVLoans_LoanExpired.selector,
|
|
250
|
-
LOANS_CONTRACT.LOAN_LIQUIDATION_DURATION(),
|
|
252
|
+
LOANS_CONTRACT.LOAN_LIQUIDATION_DURATION() + 1,
|
|
251
253
|
LOANS_CONTRACT.LOAN_LIQUIDATION_DURATION()
|
|
252
254
|
)
|
|
253
255
|
);
|