@rev-net/core-v6 0.0.6 → 0.0.7
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/SKILLS.md +1 -1
- package/docs/book.toml +1 -1
- package/docs/src/README.md +151 -54
- package/docs/src/SUMMARY.md +0 -2
- package/docs/src/src/REVDeployer.sol/contract.REVDeployer.md +148 -117
- package/docs/src/src/REVLoans.sol/contract.REVLoans.md +120 -59
- package/docs/src/src/interfaces/IREVDeployer.sol/interface.IREVDeployer.md +296 -14
- package/docs/src/src/interfaces/IREVLoans.sol/interface.IREVLoans.md +318 -16
- package/docs/src/src/structs/README.md +0 -2
- package/docs/src/src/structs/REVAutoIssuance.sol/struct.REVAutoIssuance.md +4 -4
- package/docs/src/src/structs/REVConfig.sol/struct.REVConfig.md +5 -17
- package/docs/src/src/structs/REVCroptopAllowedPost.sol/struct.REVCroptopAllowedPost.md +10 -6
- package/docs/src/src/structs/REVDeploy721TiersHookConfig.sol/struct.REVDeploy721TiersHookConfig.md +7 -7
- package/docs/src/src/structs/REVDescription.sol/struct.REVDescription.md +5 -5
- package/docs/src/src/structs/REVLoan.sol/struct.REVLoan.md +7 -7
- package/docs/src/src/structs/REVLoanSource.sol/struct.REVLoanSource.md +3 -3
- package/docs/src/src/structs/REVStageConfig.sol/struct.REVStageConfig.md +10 -10
- package/docs/src/src/structs/REVSuckerDeploymentConfig.sol/struct.REVSuckerDeploymentConfig.md +3 -3
- package/package.json +6 -6
- package/slither-ci.config.json +1 -1
- package/src/REVLoans.sol +20 -9
- package/src/interfaces/IREVDeployer.sol +0 -2
- package/src/interfaces/IREVLoans.sol +10 -4
- package/test/TestPR27_CEIPattern.t.sol +2 -2
- package/test/TestPR32_MixedFixes.t.sol +1 -1
- package/test/regression/TestI20_CumulativeLoanCounter.t.sol +303 -0
- package/test/regression/TestL27_LiquidateGapHandling.t.sol +334 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# REVLoan
|
|
2
|
-
[Git Source](https://github.com/rev-net/revnet-core-
|
|
2
|
+
[Git Source](https://github.com/rev-net/revnet-core-v6/blob/94c003a3a16de2bd012d63cccedd6bd38d21f6e7/src/structs/REVLoan.sol)
|
|
3
3
|
|
|
4
4
|
**Notes:**
|
|
5
5
|
- member: borrowedAmount The amount that is being borrowed.
|
|
@@ -17,12 +17,12 @@
|
|
|
17
17
|
|
|
18
18
|
```solidity
|
|
19
19
|
struct REVLoan {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
uint112 amount;
|
|
21
|
+
uint112 collateral;
|
|
22
|
+
uint48 createdAt;
|
|
23
|
+
uint16 prepaidFeePercent;
|
|
24
|
+
uint32 prepaidDuration;
|
|
25
|
+
REVLoanSource source;
|
|
26
26
|
}
|
|
27
27
|
```
|
|
28
28
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# REVLoanSource
|
|
2
|
-
[Git Source](https://github.com/rev-net/revnet-core-
|
|
2
|
+
[Git Source](https://github.com/rev-net/revnet-core-v6/blob/94c003a3a16de2bd012d63cccedd6bd38d21f6e7/src/structs/REVLoanSource.sol)
|
|
3
3
|
|
|
4
4
|
**Notes:**
|
|
5
5
|
- member: token The token that is being loaned.
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
|
|
10
10
|
```solidity
|
|
11
11
|
struct REVLoanSource {
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
address token;
|
|
13
|
+
IJBPayoutTerminal terminal;
|
|
14
14
|
}
|
|
15
15
|
```
|
|
16
16
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# REVStageConfig
|
|
2
|
-
[Git Source](https://github.com/rev-net/revnet-core-
|
|
2
|
+
[Git Source](https://github.com/rev-net/revnet-core-v6/blob/94c003a3a16de2bd012d63cccedd6bd38d21f6e7/src/structs/REVStageConfig.sol)
|
|
3
3
|
|
|
4
4
|
**Notes:**
|
|
5
5
|
- member: startsAtOrAfter The timestamp to start a stage at the given rate at or after.
|
|
@@ -30,15 +30,15 @@ out.
|
|
|
30
30
|
|
|
31
31
|
```solidity
|
|
32
32
|
struct REVStageConfig {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
33
|
+
uint48 startsAtOrAfter;
|
|
34
|
+
REVAutoIssuance[] autoIssuances;
|
|
35
|
+
uint16 splitPercent;
|
|
36
|
+
JBSplit[] splits;
|
|
37
|
+
uint112 initialIssuance;
|
|
38
|
+
uint32 issuanceCutFrequency;
|
|
39
|
+
uint32 issuanceCutPercent;
|
|
40
|
+
uint16 cashOutTaxRate;
|
|
41
|
+
uint16 extraMetadata;
|
|
42
42
|
}
|
|
43
43
|
```
|
|
44
44
|
|
package/docs/src/src/structs/REVSuckerDeploymentConfig.sol/struct.REVSuckerDeploymentConfig.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# REVSuckerDeploymentConfig
|
|
2
|
-
[Git Source](https://github.com/rev-net/revnet-core-
|
|
2
|
+
[Git Source](https://github.com/rev-net/revnet-core-v6/blob/94c003a3a16de2bd012d63cccedd6bd38d21f6e7/src/structs/REVSuckerDeploymentConfig.sol)
|
|
3
3
|
|
|
4
4
|
**Notes:**
|
|
5
5
|
- member: deployerConfigurations The information for how to suck tokens to other chains.
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
|
|
10
10
|
```solidity
|
|
11
11
|
struct REVSuckerDeploymentConfig {
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
JBSuckerDeployerConfig[] deployerConfigurations;
|
|
13
|
+
bytes32 salt;
|
|
14
14
|
}
|
|
15
15
|
```
|
|
16
16
|
|
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.7",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -17,11 +17,11 @@
|
|
|
17
17
|
"artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'revnet-core-v6'"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@bananapus/721-hook-v6": "^0.0.
|
|
21
|
-
"@bananapus/buyback-hook-v6": "^0.0.
|
|
22
|
-
"@bananapus/core-v6": "^0.0.
|
|
23
|
-
"@bananapus/permission-ids-v6": "^0.0.
|
|
24
|
-
"@bananapus/suckers-v6": "^0.0.
|
|
20
|
+
"@bananapus/721-hook-v6": "^0.0.9",
|
|
21
|
+
"@bananapus/buyback-hook-v6": "^0.0.7",
|
|
22
|
+
"@bananapus/core-v6": "^0.0.9",
|
|
23
|
+
"@bananapus/permission-ids-v6": "^0.0.4",
|
|
24
|
+
"@bananapus/suckers-v6": "^0.0.6",
|
|
25
25
|
"@bananapus/router-terminal-v6": "^0.0.6",
|
|
26
26
|
"@croptop/core-v6": "^0.0.6",
|
|
27
27
|
"@openzeppelin/contracts": "^5.2.0"
|
package/slither-ci.config.json
CHANGED
package/src/REVLoans.sol
CHANGED
|
@@ -134,9 +134,12 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
134
134
|
public
|
|
135
135
|
override isLoanSourceOf;
|
|
136
136
|
|
|
137
|
-
/// @notice The
|
|
138
|
-
/// @
|
|
139
|
-
|
|
137
|
+
/// @notice The cumulative number of loans ever created for a revnet, used as a loan ID sequence counter.
|
|
138
|
+
/// @dev This counter only increments (on borrow, repay-with-new-loan, and reallocation) and never decrements.
|
|
139
|
+
/// It does NOT represent the number of currently active loans. Repaid and liquidated loans leave permanent gaps
|
|
140
|
+
/// in the ID sequence. Integrators should not use this to count active loans.
|
|
141
|
+
/// @custom:param revnetId The ID of the revnet to get the cumulative loan count from.
|
|
142
|
+
mapping(uint256 revnetId => uint256) public override totalLoansBorrowedFor;
|
|
140
143
|
|
|
141
144
|
/// @notice The contract resolving each project ID to its ERC721 URI.
|
|
142
145
|
IJBTokenUriResolver public override tokenUriResolver;
|
|
@@ -158,6 +161,10 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
158
161
|
//*********************************************************************//
|
|
159
162
|
|
|
160
163
|
/// @notice The sources of each revnet's loan.
|
|
164
|
+
/// @dev This array grows monotonically -- entries are appended when a new (terminal, token) pair is first used for
|
|
165
|
+
/// borrowing, but are never removed. The `isLoanSourceOf` mapping tracks whether a source has been registered.
|
|
166
|
+
/// Since the number of distinct (terminal, token) pairs per revnet is practically bounded (typically < 10),
|
|
167
|
+
/// the gas cost of iterating this array in `loanSourcesOf` remains manageable.
|
|
161
168
|
/// @custom:member revnetId The ID of the revnet issuing the loan.
|
|
162
169
|
mapping(uint256 revnetId => REVLoanSource[]) internal _loanSourcesOf;
|
|
163
170
|
|
|
@@ -231,6 +238,8 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
231
238
|
}
|
|
232
239
|
|
|
233
240
|
/// @notice The sources of each revnet's loan.
|
|
241
|
+
/// @dev This array only grows -- sources are never removed. The number of distinct sources is practically bounded
|
|
242
|
+
/// by the number of unique (terminal, token) pairs used for borrowing, which is typically small.
|
|
234
243
|
/// @custom:member revnetId The ID of the revnet issuing the loan.
|
|
235
244
|
function loanSourcesOf(uint256 revnetId) external view override returns (REVLoanSource[] memory) {
|
|
236
245
|
return _loanSourcesOf[revnetId];
|
|
@@ -557,7 +566,7 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
557
566
|
}
|
|
558
567
|
|
|
559
568
|
// Get a reference to the loan ID.
|
|
560
|
-
loanId = _generateLoanId({revnetId: revnetId, loanNumber: ++
|
|
569
|
+
loanId = _generateLoanId({revnetId: revnetId, loanNumber: ++totalLoansBorrowedFor[revnetId]});
|
|
561
570
|
|
|
562
571
|
// Get a reference to the loan being created.
|
|
563
572
|
REVLoan storage loan = _loanOf[loanId];
|
|
@@ -627,9 +636,9 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
627
636
|
// Get a reference to the loan being iterated on.
|
|
628
637
|
REVLoan memory loan = _loanOf[loanId];
|
|
629
638
|
|
|
630
|
-
// If the loan doesn't exist,
|
|
639
|
+
// If the loan doesn't exist (repaid or already liquidated), skip past this gap and continue.
|
|
631
640
|
// slither-disable-next-line incorrect-equality
|
|
632
|
-
if (loan.createdAt == 0)
|
|
641
|
+
if (loan.createdAt == 0) continue;
|
|
633
642
|
|
|
634
643
|
// Keep a reference to the loan's owner.
|
|
635
644
|
address owner = _ownerOf(loanId);
|
|
@@ -775,7 +784,7 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
775
784
|
|
|
776
785
|
// Revert if this repayment would do nothing — no borrow amount repaid and no collateral returned.
|
|
777
786
|
// Without this check, a zero-amount repayment would burn the old loan NFT and mint a new one,
|
|
778
|
-
// incrementing
|
|
787
|
+
// incrementing totalLoansBorrowedFor without limit.
|
|
779
788
|
if (repayBorrowAmount == 0 && collateralCountToReturn == 0) revert REVLoans_NothingToRepay();
|
|
780
789
|
|
|
781
790
|
// Keep a reference to the fee that'll be taken.
|
|
@@ -862,6 +871,8 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
862
871
|
internal
|
|
863
872
|
{
|
|
864
873
|
// Register the source if this is the first time its being used for this revnet.
|
|
874
|
+
// Note: Sources are only appended, never removed. This is acceptable because the number of distinct
|
|
875
|
+
// (terminal, token) pairs per revnet is practically bounded.
|
|
865
876
|
if (!isLoanSourceOf[revnetId][loan.source.terminal][loan.source.token]) {
|
|
866
877
|
isLoanSourceOf[revnetId][loan.source.terminal][loan.source.token] = true;
|
|
867
878
|
_loanSourcesOf[revnetId].push(REVLoanSource({token: loan.source.token, terminal: loan.source.terminal}));
|
|
@@ -1139,7 +1150,7 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
1139
1150
|
} else {
|
|
1140
1151
|
// Make a new loan with the remaining amount and collateral.
|
|
1141
1152
|
// Get a reference to the replacement loan ID.
|
|
1142
|
-
uint256 paidOffLoanId = _generateLoanId({revnetId: revnetId, loanNumber: ++
|
|
1153
|
+
uint256 paidOffLoanId = _generateLoanId({revnetId: revnetId, loanNumber: ++totalLoansBorrowedFor[revnetId]});
|
|
1143
1154
|
|
|
1144
1155
|
// Get a reference to the loan being paid off.
|
|
1145
1156
|
REVLoan storage paidOffLoan = _loanOf[paidOffLoanId];
|
|
@@ -1220,7 +1231,7 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
1220
1231
|
}
|
|
1221
1232
|
|
|
1222
1233
|
// Get a reference to the replacement loan ID.
|
|
1223
|
-
reallocatedLoanId = _generateLoanId({revnetId: revnetId, loanNumber: ++
|
|
1234
|
+
reallocatedLoanId = _generateLoanId({revnetId: revnetId, loanNumber: ++totalLoansBorrowedFor[revnetId]});
|
|
1224
1235
|
|
|
1225
1236
|
// Get a reference to the loan being created.
|
|
1226
1237
|
reallocatedLoan = _loanOf[reallocatedLoanId];
|
|
@@ -47,8 +47,6 @@ interface IREVDeployer {
|
|
|
47
47
|
uint256 indexed revnetId, uint256 indexed stageId, address indexed beneficiary, uint256 count, address caller
|
|
48
48
|
);
|
|
49
49
|
|
|
50
|
-
event SetAdditionalOperator(uint256 revnetId, address additionalOperator, uint256[] permissionIds, address caller);
|
|
51
|
-
|
|
52
50
|
event BurnHeldTokens(uint256 indexed revnetId, uint256 count, address caller);
|
|
53
51
|
|
|
54
52
|
/// @notice The number of seconds until a revnet's participants can cash out after deploying to a new network.
|
|
@@ -129,14 +129,20 @@ interface IREVLoans {
|
|
|
129
129
|
function loanOf(uint256 loanId) external view returns (REVLoan memory);
|
|
130
130
|
|
|
131
131
|
/// @notice The sources of each revnet's loans.
|
|
132
|
+
/// @dev This array only grows -- sources are appended when a new (terminal, token) pair is first used for
|
|
133
|
+
/// borrowing, but are never removed. Gas cost scales linearly with the number of distinct sources, though this is
|
|
134
|
+
/// practically bounded to a small number of unique (terminal, token) pairs.
|
|
132
135
|
/// @param revnetId The ID of the revnet to get the loan sources for.
|
|
133
136
|
/// @return The array of loan sources.
|
|
134
137
|
function loanSourcesOf(uint256 revnetId) external view returns (REVLoanSource[] memory);
|
|
135
138
|
|
|
136
|
-
/// @notice The number of loans
|
|
137
|
-
/// @
|
|
138
|
-
///
|
|
139
|
-
|
|
139
|
+
/// @notice The cumulative number of loans ever created for a revnet, used as a loan ID sequence counter.
|
|
140
|
+
/// @dev This counter only increments and never decrements. It does NOT represent the count of currently active
|
|
141
|
+
/// loans -- repaid and liquidated loans leave permanent gaps in the sequence. Do not use this value to determine
|
|
142
|
+
/// how many loans are currently outstanding.
|
|
143
|
+
/// @param revnetId The ID of the revnet to get the cumulative loan count for.
|
|
144
|
+
/// @return The cumulative number of loans ever created.
|
|
145
|
+
function totalLoansBorrowedFor(uint256 revnetId) external view returns (uint256);
|
|
140
146
|
|
|
141
147
|
/// @notice The revnet ID for the loan with the provided loan ID.
|
|
142
148
|
/// @param loanId The loan ID to get the revnet ID of.
|
|
@@ -335,8 +335,8 @@ contract TestPR27_CEIPattern is TestBaseWorkflow, JBTest {
|
|
|
335
335
|
REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
|
|
336
336
|
|
|
337
337
|
// Pre-compute the loanId so the attacker can read it during reentrancy.
|
|
338
|
-
// loanId = revnetId * 1_000_000_000_000 + (
|
|
339
|
-
uint256 expectedLoanId = REVNET_ID * 1_000_000_000_000 + (LOANS_CONTRACT.
|
|
338
|
+
// loanId = revnetId * 1_000_000_000_000 + (totalLoansBorrowedFor + 1)
|
|
339
|
+
uint256 expectedLoanId = REVNET_ID * 1_000_000_000_000 + (LOANS_CONTRACT.totalLoansBorrowedFor(REVNET_ID) + 1);
|
|
340
340
|
attacker.setTarget(expectedLoanId);
|
|
341
341
|
|
|
342
342
|
// Borrow with attacker as beneficiary — attacker's receive() will fire when ETH arrives.
|
|
@@ -516,7 +516,7 @@ contract TestPR32_MixedFixes is TestBaseWorkflow, JBTest {
|
|
|
516
516
|
assertTrue(tokenBorrowed > 0, "TOKEN borrow should be tracked");
|
|
517
517
|
|
|
518
518
|
// The total number of loans should be 2.
|
|
519
|
-
assertEq(LOANS_CONTRACT.
|
|
519
|
+
assertEq(LOANS_CONTRACT.totalLoansBorrowedFor(MIXED_REVNET_ID), 2, "Should have 2 loans");
|
|
520
520
|
|
|
521
521
|
// Query borrowable in 18-decimal ETH context. This exercises _totalBorrowedFrom's
|
|
522
522
|
// cross-decimal normalization: TOKEN borrows (6 dec) must be normalized to 18 dec before
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.26;
|
|
3
|
+
|
|
4
|
+
import "forge-std/Test.sol";
|
|
5
|
+
import /* {*} from */ "@bananapus/core-v6/test/helpers/TestBaseWorkflow.sol";
|
|
6
|
+
import /* {*} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
|
|
7
|
+
import /* {*} from */ "./../../src/REVDeployer.sol";
|
|
8
|
+
import "@croptop/core-v6/src/CTPublisher.sol";
|
|
9
|
+
import {MockBuybackDataHook} from "./../mock/MockBuybackDataHook.sol";
|
|
10
|
+
import "@bananapus/core-v6/script/helpers/CoreDeploymentLib.sol";
|
|
11
|
+
import "@bananapus/721-hook-v6/script/helpers/Hook721DeploymentLib.sol";
|
|
12
|
+
import "@bananapus/suckers-v6/script/helpers/SuckerDeploymentLib.sol";
|
|
13
|
+
import "@croptop/core-v6/script/helpers/CroptopDeploymentLib.sol";
|
|
14
|
+
import "@bananapus/router-terminal-v6/script/helpers/RouterTerminalDeploymentLib.sol";
|
|
15
|
+
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
16
|
+
import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
|
|
17
|
+
import {JBSingleAllowance} from "@bananapus/core-v6/src/structs/JBSingleAllowance.sol";
|
|
18
|
+
import {MockPriceFeed} from "@bananapus/core-v6/test/mock/MockPriceFeed.sol";
|
|
19
|
+
import {MockERC20} from "@bananapus/core-v6/test/mock/MockERC20.sol";
|
|
20
|
+
import {REVLoans} from "../../src/REVLoans.sol";
|
|
21
|
+
import {REVLoan} from "../../src/structs/REVLoan.sol";
|
|
22
|
+
import {REVStageConfig, REVAutoIssuance} from "../../src/structs/REVStageConfig.sol";
|
|
23
|
+
import {REVLoanSource} from "../../src/structs/REVLoanSource.sol";
|
|
24
|
+
import {REVDescription} from "../../src/structs/REVDescription.sol";
|
|
25
|
+
import {IREVLoans} from "../../src/interfaces/IREVLoans.sol";
|
|
26
|
+
import {JBSuckerDeployerConfig} from "@bananapus/suckers-v6/src/structs/JBSuckerDeployerConfig.sol";
|
|
27
|
+
import {JBSuckerRegistry} from "@bananapus/suckers-v6/src/JBSuckerRegistry.sol";
|
|
28
|
+
import {JB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
|
|
29
|
+
import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
|
|
30
|
+
import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
|
|
31
|
+
import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
|
|
32
|
+
import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
|
|
33
|
+
|
|
34
|
+
/// @notice Regression test for I-20: totalLoansBorrowedFor is a cumulative counter, not an active loan count.
|
|
35
|
+
/// @dev The rename from numberOfLoansFor to totalLoansBorrowedFor clarifies that the counter only increments
|
|
36
|
+
/// and never decrements. Repaying or liquidating a loan does NOT reduce the counter. This test verifies that
|
|
37
|
+
/// the counter remains at its high-water mark after loans are fully repaid and after loans are liquidated.
|
|
38
|
+
contract TestI20_CumulativeLoanCounter is TestBaseWorkflow, JBTest {
|
|
39
|
+
bytes32 REV_DEPLOYER_SALT = "REVDeployer";
|
|
40
|
+
|
|
41
|
+
REVDeployer REV_DEPLOYER;
|
|
42
|
+
JB721TiersHook EXAMPLE_HOOK;
|
|
43
|
+
IJB721TiersHookDeployer HOOK_DEPLOYER;
|
|
44
|
+
IJB721TiersHookStore HOOK_STORE;
|
|
45
|
+
IJBAddressRegistry ADDRESS_REGISTRY;
|
|
46
|
+
REVLoans LOANS_CONTRACT;
|
|
47
|
+
IJBSuckerRegistry SUCKER_REGISTRY;
|
|
48
|
+
CTPublisher PUBLISHER;
|
|
49
|
+
MockBuybackDataHook MOCK_BUYBACK;
|
|
50
|
+
|
|
51
|
+
uint256 FEE_PROJECT_ID;
|
|
52
|
+
uint256 REVNET_ID;
|
|
53
|
+
|
|
54
|
+
address USER1 = makeAddr("user1");
|
|
55
|
+
address USER2 = makeAddr("user2");
|
|
56
|
+
address USER3 = makeAddr("user3");
|
|
57
|
+
|
|
58
|
+
address private constant TRUSTED_FORWARDER = 0xB2b5841DBeF766d4b521221732F9B618fCf34A87;
|
|
59
|
+
|
|
60
|
+
function setUp() public override {
|
|
61
|
+
super.setUp();
|
|
62
|
+
FEE_PROJECT_ID = jbProjects().createFor(multisig());
|
|
63
|
+
SUCKER_REGISTRY = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
|
|
64
|
+
HOOK_STORE = new JB721TiersHookStore();
|
|
65
|
+
EXAMPLE_HOOK = new JB721TiersHook(jbDirectory(), jbPermissions(), jbRulesets(), HOOK_STORE, multisig());
|
|
66
|
+
ADDRESS_REGISTRY = new JBAddressRegistry();
|
|
67
|
+
HOOK_DEPLOYER = new JB721TiersHookDeployer(EXAMPLE_HOOK, HOOK_STORE, ADDRESS_REGISTRY, multisig());
|
|
68
|
+
PUBLISHER = new CTPublisher(jbDirectory(), jbPermissions(), FEE_PROJECT_ID, multisig());
|
|
69
|
+
MOCK_BUYBACK = new MockBuybackDataHook();
|
|
70
|
+
MockPriceFeed priceFeed = new MockPriceFeed(1e18, 18);
|
|
71
|
+
vm.prank(multisig());
|
|
72
|
+
jbPrices()
|
|
73
|
+
.addPriceFeedFor(
|
|
74
|
+
0, uint32(uint160(JBConstants.NATIVE_TOKEN)), uint32(uint160(JBConstants.NATIVE_TOKEN)), priceFeed
|
|
75
|
+
);
|
|
76
|
+
LOANS_CONTRACT = new REVLoans({
|
|
77
|
+
controller: jbController(),
|
|
78
|
+
projects: jbProjects(),
|
|
79
|
+
revId: FEE_PROJECT_ID,
|
|
80
|
+
owner: address(this),
|
|
81
|
+
permit2: permit2(),
|
|
82
|
+
trustedForwarder: TRUSTED_FORWARDER
|
|
83
|
+
});
|
|
84
|
+
REV_DEPLOYER = new REVDeployer{salt: REV_DEPLOYER_SALT}(
|
|
85
|
+
jbController(),
|
|
86
|
+
SUCKER_REGISTRY,
|
|
87
|
+
FEE_PROJECT_ID,
|
|
88
|
+
HOOK_DEPLOYER,
|
|
89
|
+
PUBLISHER,
|
|
90
|
+
IJBRulesetDataHook(address(MOCK_BUYBACK)),
|
|
91
|
+
address(LOANS_CONTRACT),
|
|
92
|
+
TRUSTED_FORWARDER
|
|
93
|
+
);
|
|
94
|
+
vm.prank(multisig());
|
|
95
|
+
jbProjects().approve(address(REV_DEPLOYER), FEE_PROJECT_ID);
|
|
96
|
+
_deployFeeProject();
|
|
97
|
+
_deployRevnet();
|
|
98
|
+
vm.deal(USER1, 100e18);
|
|
99
|
+
vm.deal(USER2, 100e18);
|
|
100
|
+
vm.deal(USER3, 100e18);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function _deployFeeProject() internal {
|
|
104
|
+
JBAccountingContext[] memory acc = new JBAccountingContext[](1);
|
|
105
|
+
acc[0] = JBAccountingContext({
|
|
106
|
+
token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
107
|
+
});
|
|
108
|
+
JBTerminalConfig[] memory tc = new JBTerminalConfig[](1);
|
|
109
|
+
tc[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: acc});
|
|
110
|
+
REVStageConfig[] memory stages = new REVStageConfig[](1);
|
|
111
|
+
JBSplit[] memory splits = new JBSplit[](1);
|
|
112
|
+
splits[0].beneficiary = payable(multisig());
|
|
113
|
+
splits[0].percent = 10_000;
|
|
114
|
+
REVAutoIssuance[] memory ai = new REVAutoIssuance[](1);
|
|
115
|
+
ai[0] = REVAutoIssuance({chainId: uint32(block.chainid), count: uint104(70_000e18), beneficiary: multisig()});
|
|
116
|
+
stages[0] = REVStageConfig({
|
|
117
|
+
startsAtOrAfter: uint40(block.timestamp),
|
|
118
|
+
autoIssuances: ai,
|
|
119
|
+
splitPercent: 2000,
|
|
120
|
+
splits: splits,
|
|
121
|
+
initialIssuance: uint112(1000e18),
|
|
122
|
+
issuanceCutFrequency: 90 days,
|
|
123
|
+
issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
|
|
124
|
+
cashOutTaxRate: 6000,
|
|
125
|
+
extraMetadata: 0
|
|
126
|
+
});
|
|
127
|
+
REVConfig memory cfg = REVConfig({
|
|
128
|
+
description: REVDescription("Revnet", "$REV", "ipfs://test", "REV_TOKEN"),
|
|
129
|
+
baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
130
|
+
splitOperator: multisig(),
|
|
131
|
+
stageConfigurations: stages
|
|
132
|
+
});
|
|
133
|
+
vm.prank(multisig());
|
|
134
|
+
REV_DEPLOYER.deployFor({
|
|
135
|
+
revnetId: FEE_PROJECT_ID,
|
|
136
|
+
configuration: cfg,
|
|
137
|
+
terminalConfigurations: tc,
|
|
138
|
+
suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
|
|
139
|
+
deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256("FEE")
|
|
140
|
+
})
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function _deployRevnet() internal {
|
|
145
|
+
JBAccountingContext[] memory acc = new JBAccountingContext[](1);
|
|
146
|
+
acc[0] = JBAccountingContext({
|
|
147
|
+
token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
148
|
+
});
|
|
149
|
+
JBTerminalConfig[] memory tc = new JBTerminalConfig[](1);
|
|
150
|
+
tc[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: acc});
|
|
151
|
+
REVStageConfig[] memory stages = new REVStageConfig[](1);
|
|
152
|
+
JBSplit[] memory splits = new JBSplit[](1);
|
|
153
|
+
splits[0].beneficiary = payable(multisig());
|
|
154
|
+
splits[0].percent = 10_000;
|
|
155
|
+
REVAutoIssuance[] memory ai = new REVAutoIssuance[](1);
|
|
156
|
+
ai[0] = REVAutoIssuance({chainId: uint32(block.chainid), count: uint104(70_000e18), beneficiary: multisig()});
|
|
157
|
+
stages[0] = REVStageConfig({
|
|
158
|
+
startsAtOrAfter: uint40(block.timestamp),
|
|
159
|
+
autoIssuances: ai,
|
|
160
|
+
splitPercent: 2000,
|
|
161
|
+
splits: splits,
|
|
162
|
+
initialIssuance: uint112(1000e18),
|
|
163
|
+
issuanceCutFrequency: 90 days,
|
|
164
|
+
issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
|
|
165
|
+
cashOutTaxRate: 6000,
|
|
166
|
+
extraMetadata: 0
|
|
167
|
+
});
|
|
168
|
+
REVConfig memory cfg = REVConfig({
|
|
169
|
+
description: REVDescription("NANA", "$NANA", "ipfs://test2", "NANA_TOKEN"),
|
|
170
|
+
baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
171
|
+
splitOperator: multisig(),
|
|
172
|
+
stageConfigurations: stages
|
|
173
|
+
});
|
|
174
|
+
REVNET_ID = REV_DEPLOYER.deployFor({
|
|
175
|
+
revnetId: 0,
|
|
176
|
+
configuration: cfg,
|
|
177
|
+
terminalConfigurations: tc,
|
|
178
|
+
suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
|
|
179
|
+
deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256("NANA")
|
|
180
|
+
})
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function _setupLoan(address user, uint256 ethAmount) internal returns (uint256 loanId, uint256 tokenCount) {
|
|
185
|
+
vm.prank(user);
|
|
186
|
+
tokenCount =
|
|
187
|
+
jbMultiTerminal().pay{value: ethAmount}(REVNET_ID, JBConstants.NATIVE_TOKEN, ethAmount, user, 0, "", "");
|
|
188
|
+
uint256 borrowAmount =
|
|
189
|
+
LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokenCount, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
|
|
190
|
+
require(borrowAmount > 0, "Borrow amount should be > 0");
|
|
191
|
+
mockExpect(
|
|
192
|
+
address(jbPermissions()),
|
|
193
|
+
abi.encodeCall(IJBPermissions.hasPermission, (address(LOANS_CONTRACT), user, REVNET_ID, 11, true, true)),
|
|
194
|
+
abi.encode(true)
|
|
195
|
+
);
|
|
196
|
+
REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
|
|
197
|
+
vm.prank(user);
|
|
198
|
+
(loanId,) = LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokenCount, payable(user), 25);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/// @notice Verifies totalLoansBorrowedFor never decrements after loan repayment.
|
|
202
|
+
/// @dev Creates 3 loans, fully repays 2, then verifies the counter stays at 3 (not 1).
|
|
203
|
+
/// This confirms the I-20 rename correctly reflects cumulative semantics.
|
|
204
|
+
function test_I20_counterNeverDecrementsAfterRepayment() public {
|
|
205
|
+
// Counter starts at 0
|
|
206
|
+
assertEq(LOANS_CONTRACT.totalLoansBorrowedFor(REVNET_ID), 0, "Counter should start at 0");
|
|
207
|
+
|
|
208
|
+
// Create 3 loans
|
|
209
|
+
(uint256 loanId1,) = _setupLoan(USER1, 3e18);
|
|
210
|
+
(uint256 loanId2,) = _setupLoan(USER2, 3e18);
|
|
211
|
+
(uint256 loanId3,) = _setupLoan(USER3, 3e18);
|
|
212
|
+
|
|
213
|
+
// Counter should be 3
|
|
214
|
+
assertEq(LOANS_CONTRACT.totalLoansBorrowedFor(REVNET_ID), 3, "Counter should be 3 after 3 loans");
|
|
215
|
+
|
|
216
|
+
// Fully repay loan 1
|
|
217
|
+
REVLoan memory loan1 = LOANS_CONTRACT.loanOf(loanId1);
|
|
218
|
+
JBSingleAllowance memory allowance;
|
|
219
|
+
vm.prank(USER1);
|
|
220
|
+
LOANS_CONTRACT.repayLoan{value: loan1.amount}(
|
|
221
|
+
loanId1, loan1.amount, loan1.collateral, payable(USER1), allowance
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
// Counter should still be 3 (NOT 2) -- repayment does not decrement
|
|
225
|
+
assertEq(
|
|
226
|
+
LOANS_CONTRACT.totalLoansBorrowedFor(REVNET_ID),
|
|
227
|
+
3,
|
|
228
|
+
"Counter should remain 3 after repaying loan 1 -- cumulative, never decrements"
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
// Fully repay loan 2
|
|
232
|
+
REVLoan memory loan2 = LOANS_CONTRACT.loanOf(loanId2);
|
|
233
|
+
vm.prank(USER2);
|
|
234
|
+
LOANS_CONTRACT.repayLoan{value: loan2.amount}(
|
|
235
|
+
loanId2, loan2.amount, loan2.collateral, payable(USER2), allowance
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
// Counter should still be 3 (NOT 1)
|
|
239
|
+
assertEq(
|
|
240
|
+
LOANS_CONTRACT.totalLoansBorrowedFor(REVNET_ID),
|
|
241
|
+
3,
|
|
242
|
+
"Counter should remain 3 after repaying loan 2 -- cumulative, never decrements"
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
// Verify the loans are actually deleted (createdAt == 0)
|
|
246
|
+
assertEq(LOANS_CONTRACT.loanOf(loanId1).createdAt, 0, "Loan 1 should be deleted");
|
|
247
|
+
assertEq(LOANS_CONTRACT.loanOf(loanId2).createdAt, 0, "Loan 2 should be deleted");
|
|
248
|
+
assertTrue(LOANS_CONTRACT.loanOf(loanId3).createdAt > 0, "Loan 3 should still exist");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/// @notice Verifies totalLoansBorrowedFor never decrements after loan liquidation.
|
|
252
|
+
/// @dev Creates 2 loans, liquidates both, then verifies the counter stays at 2.
|
|
253
|
+
function test_I20_counterNeverDecrementsAfterLiquidation() public {
|
|
254
|
+
// Create 2 loans
|
|
255
|
+
(uint256 loanId1,) = _setupLoan(USER1, 5e18);
|
|
256
|
+
(uint256 loanId2,) = _setupLoan(USER2, 5e18);
|
|
257
|
+
|
|
258
|
+
assertEq(LOANS_CONTRACT.totalLoansBorrowedFor(REVNET_ID), 2, "Counter should be 2 after 2 loans");
|
|
259
|
+
|
|
260
|
+
// Warp past liquidation duration
|
|
261
|
+
vm.warp(block.timestamp + LOANS_CONTRACT.LOAN_LIQUIDATION_DURATION() + 1);
|
|
262
|
+
|
|
263
|
+
// Liquidate both loans
|
|
264
|
+
LOANS_CONTRACT.liquidateExpiredLoansFrom(REVNET_ID, 1, 2);
|
|
265
|
+
|
|
266
|
+
// Both loans should be deleted
|
|
267
|
+
assertEq(LOANS_CONTRACT.loanOf(loanId1).createdAt, 0, "Loan 1 should be liquidated");
|
|
268
|
+
assertEq(LOANS_CONTRACT.loanOf(loanId2).createdAt, 0, "Loan 2 should be liquidated");
|
|
269
|
+
|
|
270
|
+
// Counter should still be 2 -- liquidation does not decrement
|
|
271
|
+
assertEq(
|
|
272
|
+
LOANS_CONTRACT.totalLoansBorrowedFor(REVNET_ID),
|
|
273
|
+
2,
|
|
274
|
+
"Counter should remain 2 after liquidating both loans -- cumulative, never decrements"
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/// @notice Verifies that partial repayment (which creates a new loan) increments the counter.
|
|
279
|
+
/// @dev When partially repaying, the old loan is burned and a new loan is minted for the remainder.
|
|
280
|
+
/// This should increment the counter by 1 since a new loan ID is generated.
|
|
281
|
+
function test_I20_partialRepaymentIncrementsCounter() public {
|
|
282
|
+
// Create 1 loan
|
|
283
|
+
(uint256 loanId1,) = _setupLoan(USER1, 5e18);
|
|
284
|
+
assertEq(LOANS_CONTRACT.totalLoansBorrowedFor(REVNET_ID), 1, "Counter should be 1 after 1 loan");
|
|
285
|
+
|
|
286
|
+
// Partially repay (repay half the borrow amount, return no collateral)
|
|
287
|
+
REVLoan memory loan1 = LOANS_CONTRACT.loanOf(loanId1);
|
|
288
|
+
uint256 halfAmount = loan1.amount / 2;
|
|
289
|
+
JBSingleAllowance memory allowance;
|
|
290
|
+
vm.prank(USER1);
|
|
291
|
+
LOANS_CONTRACT.repayLoan{value: halfAmount}(loanId1, halfAmount, 0, payable(USER1), allowance);
|
|
292
|
+
|
|
293
|
+
// Counter should be 2: original loan (burned) + replacement loan (new ID)
|
|
294
|
+
assertEq(
|
|
295
|
+
LOANS_CONTRACT.totalLoansBorrowedFor(REVNET_ID),
|
|
296
|
+
2,
|
|
297
|
+
"Counter should be 2 after partial repayment creates a replacement loan"
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
// Original loan should be deleted
|
|
301
|
+
assertEq(LOANS_CONTRACT.loanOf(loanId1).createdAt, 0, "Original loan should be deleted after partial repay");
|
|
302
|
+
}
|
|
303
|
+
}
|