@rev-net/core-v6 0.0.1
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/LICENSE +21 -0
- package/README.md +65 -0
- package/REVNET_SECURITY_CHECKLIST.md +164 -0
- package/SECURITY.md +68 -0
- package/SKILLS.md +166 -0
- package/deployments/revnet-core-v5/arbitrum/REVDeployer.json +2821 -0
- package/deployments/revnet-core-v5/arbitrum/REVLoans.json +2260 -0
- package/deployments/revnet-core-v5/arbitrum_sepolia/REVDeployer.json +2821 -0
- package/deployments/revnet-core-v5/arbitrum_sepolia/REVLoans.json +2260 -0
- package/deployments/revnet-core-v5/base/REVDeployer.json +2825 -0
- package/deployments/revnet-core-v5/base/REVLoans.json +2264 -0
- package/deployments/revnet-core-v5/base_sepolia/REVDeployer.json +2825 -0
- package/deployments/revnet-core-v5/base_sepolia/REVLoans.json +2264 -0
- package/deployments/revnet-core-v5/ethereum/REVDeployer.json +2825 -0
- package/deployments/revnet-core-v5/ethereum/REVLoans.json +2264 -0
- package/deployments/revnet-core-v5/optimism/REVDeployer.json +2821 -0
- package/deployments/revnet-core-v5/optimism/REVLoans.json +2260 -0
- package/deployments/revnet-core-v5/optimism_sepolia/REVDeployer.json +2825 -0
- package/deployments/revnet-core-v5/optimism_sepolia/REVLoans.json +2264 -0
- package/deployments/revnet-core-v5/sepolia/REVDeployer.json +2825 -0
- package/deployments/revnet-core-v5/sepolia/REVLoans.json +2264 -0
- package/docs/book.css +13 -0
- package/docs/book.toml +13 -0
- package/docs/solidity.min.js +74 -0
- package/docs/src/README.md +88 -0
- package/docs/src/SUMMARY.md +20 -0
- package/docs/src/src/README.md +7 -0
- package/docs/src/src/REVDeployer.sol/contract.REVDeployer.md +968 -0
- package/docs/src/src/REVLoans.sol/contract.REVLoans.md +1047 -0
- package/docs/src/src/interfaces/IREVDeployer.sol/interface.IREVDeployer.md +243 -0
- package/docs/src/src/interfaces/IREVLoans.sol/interface.IREVLoans.md +296 -0
- package/docs/src/src/interfaces/README.md +5 -0
- package/docs/src/src/structs/README.md +14 -0
- package/docs/src/src/structs/REVAutoIssuance.sol/struct.REVAutoIssuance.md +19 -0
- package/docs/src/src/structs/REVBuybackHookConfig.sol/struct.REVBuybackHookConfig.md +19 -0
- package/docs/src/src/structs/REVBuybackPoolConfig.sol/struct.REVBuybackPoolConfig.md +21 -0
- package/docs/src/src/structs/REVConfig.sol/struct.REVConfig.md +35 -0
- package/docs/src/src/structs/REVCroptopAllowedPost.sol/struct.REVCroptopAllowedPost.md +28 -0
- package/docs/src/src/structs/REVDeploy721TiersHookConfig.sol/struct.REVDeploy721TiersHookConfig.md +34 -0
- package/docs/src/src/structs/REVDescription.sol/struct.REVDescription.md +23 -0
- package/docs/src/src/structs/REVLoan.sol/struct.REVLoan.md +28 -0
- package/docs/src/src/structs/REVLoanSource.sol/struct.REVLoanSource.md +16 -0
- package/docs/src/src/structs/REVStageConfig.sol/struct.REVStageConfig.md +44 -0
- package/docs/src/src/structs/REVSuckerDeploymentConfig.sol/struct.REVSuckerDeploymentConfig.md +16 -0
- package/foundry.lock +11 -0
- package/foundry.toml +23 -0
- package/package.json +31 -0
- package/remappings.txt +1 -0
- package/script/Deploy.s.sol +350 -0
- package/script/helpers/RevnetCoreDeploymentLib.sol +72 -0
- package/slither-ci.config.json +10 -0
- package/sphinx.lock +507 -0
- package/src/REVDeployer.sol +1257 -0
- package/src/REVLoans.sol +1333 -0
- package/src/interfaces/IREVDeployer.sol +198 -0
- package/src/interfaces/IREVLoans.sol +241 -0
- package/src/structs/REVAutoIssuance.sol +11 -0
- package/src/structs/REVConfig.sol +17 -0
- package/src/structs/REVCroptopAllowedPost.sol +20 -0
- package/src/structs/REVDeploy721TiersHookConfig.sol +25 -0
- package/src/structs/REVDescription.sol +14 -0
- package/src/structs/REVLoan.sol +19 -0
- package/src/structs/REVLoanSource.sol +11 -0
- package/src/structs/REVStageConfig.sol +34 -0
- package/src/structs/REVSuckerDeploymentConfig.sol +11 -0
- package/test/REV.integrations.t.sol +420 -0
- package/test/REVAutoIssuanceFuzz.t.sol +276 -0
- package/test/REVDeployerAuditRegressions.t.sol +328 -0
- package/test/REVInvincibility.t.sol +1275 -0
- package/test/REVInvincibilityHandler.sol +357 -0
- package/test/REVLifecycle.t.sol +364 -0
- package/test/REVLoans.invariants.t.sol +642 -0
- package/test/REVLoansAttacks.t.sol +739 -0
- package/test/REVLoansAuditRegressions.t.sol +314 -0
- package/test/REVLoansFeeRecovery.t.sol +704 -0
- package/test/REVLoansSourced.t.sol +1732 -0
- package/test/REVLoansUnSourced.t.sol +331 -0
- package/test/TestPR09_ConversionDocumentation.t.sol +304 -0
- package/test/TestPR10_LiquidationBehavior.t.sol +340 -0
- package/test/TestPR11_LowFindings.t.sol +571 -0
- package/test/TestPR12_FlashLoanSurplus.t.sol +305 -0
- package/test/TestPR13_CrossSourceReallocation.t.sol +302 -0
- package/test/TestPR15_CashOutCallerValidation.t.sol +320 -0
- package/test/TestPR16_ZeroRepayment.t.sol +297 -0
- package/test/TestPR21_Uint112Overflow.t.sol +251 -0
- package/test/TestPR22_HookArrayOOB.t.sol +221 -0
- package/test/TestPR26_BurnHeldTokens.t.sol +331 -0
- package/test/TestPR27_CEIPattern.t.sol +448 -0
- package/test/TestPR29_SwapTerminalPermission.t.sol +206 -0
- package/test/TestPR32_MixedFixes.t.sol +529 -0
- package/test/helpers/MaliciousContracts.sol +233 -0
- package/test/mock/MockBuybackDataHook.sol +61 -0
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.23;
|
|
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
|
+
|
|
11
|
+
import "@bananapus/core-v6/script/helpers/CoreDeploymentLib.sol";
|
|
12
|
+
import "@bananapus/721-hook-v6/script/helpers/Hook721DeploymentLib.sol";
|
|
13
|
+
import "@bananapus/suckers-v6/script/helpers/SuckerDeploymentLib.sol";
|
|
14
|
+
import "@croptop/core-v6/script/helpers/CroptopDeploymentLib.sol";
|
|
15
|
+
import "@bananapus/swap-terminal-v6/script/helpers/SwapTerminalDeploymentLib.sol";
|
|
16
|
+
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
17
|
+
import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.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
|
+
struct FeeProjectConfig {
|
|
35
|
+
REVConfig configuration;
|
|
36
|
+
JBTerminalConfig[] terminalConfigurations;
|
|
37
|
+
REVSuckerDeploymentConfig suckerDeploymentConfiguration;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
contract TestPR11_LowFindings is TestBaseWorkflow, JBTest {
|
|
41
|
+
bytes32 REV_DEPLOYER_SALT = "REVDeployer";
|
|
42
|
+
bytes32 ERC20_SALT = "REV_TOKEN";
|
|
43
|
+
|
|
44
|
+
REVDeployer REV_DEPLOYER;
|
|
45
|
+
JB721TiersHook EXAMPLE_HOOK;
|
|
46
|
+
IJB721TiersHookDeployer HOOK_DEPLOYER;
|
|
47
|
+
IJB721TiersHookStore HOOK_STORE;
|
|
48
|
+
IJBAddressRegistry ADDRESS_REGISTRY;
|
|
49
|
+
IREVLoans LOANS_CONTRACT;
|
|
50
|
+
MockERC20 TOKEN;
|
|
51
|
+
IJBSuckerRegistry SUCKER_REGISTRY;
|
|
52
|
+
CTPublisher PUBLISHER;
|
|
53
|
+
MockBuybackDataHook MOCK_BUYBACK;
|
|
54
|
+
|
|
55
|
+
uint256 FEE_PROJECT_ID;
|
|
56
|
+
uint256 REVNET_ID;
|
|
57
|
+
|
|
58
|
+
address USER = makeAddr("user");
|
|
59
|
+
|
|
60
|
+
address private constant TRUSTED_FORWARDER = 0xB2b5841DBeF766d4b521221732F9B618fCf34A87;
|
|
61
|
+
|
|
62
|
+
function getFeeProjectConfig() internal view returns (FeeProjectConfig memory) {
|
|
63
|
+
uint8 decimals = 18;
|
|
64
|
+
uint256 decimalMultiplier = 10 ** decimals;
|
|
65
|
+
|
|
66
|
+
JBAccountingContext[] memory accountingContextsToAccept = new JBAccountingContext[](2);
|
|
67
|
+
accountingContextsToAccept[0] = JBAccountingContext({
|
|
68
|
+
token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
69
|
+
});
|
|
70
|
+
accountingContextsToAccept[1] =
|
|
71
|
+
JBAccountingContext({token: address(TOKEN), decimals: 6, currency: uint32(uint160(address(TOKEN)))});
|
|
72
|
+
|
|
73
|
+
JBTerminalConfig[] memory terminalConfigurations = new JBTerminalConfig[](1);
|
|
74
|
+
terminalConfigurations[0] =
|
|
75
|
+
JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: accountingContextsToAccept});
|
|
76
|
+
|
|
77
|
+
REVStageConfig[] memory stageConfigurations = new REVStageConfig[](1);
|
|
78
|
+
JBSplit[] memory splits = new JBSplit[](1);
|
|
79
|
+
splits[0].beneficiary = payable(multisig());
|
|
80
|
+
splits[0].percent = 10_000;
|
|
81
|
+
|
|
82
|
+
stageConfigurations[0] = REVStageConfig({
|
|
83
|
+
startsAtOrAfter: uint40(block.timestamp),
|
|
84
|
+
autoIssuances: new REVAutoIssuance[](0),
|
|
85
|
+
splitPercent: 2000,
|
|
86
|
+
splits: splits,
|
|
87
|
+
initialIssuance: uint112(1000 * decimalMultiplier),
|
|
88
|
+
issuanceCutFrequency: 90 days,
|
|
89
|
+
issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
|
|
90
|
+
cashOutTaxRate: 6000,
|
|
91
|
+
extraMetadata: 0
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
REVConfig memory revnetConfiguration = REVConfig({
|
|
95
|
+
description: REVDescription("Revnet", "$REV", "ipfs://test", ERC20_SALT),
|
|
96
|
+
baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
97
|
+
splitOperator: multisig(),
|
|
98
|
+
stageConfigurations: stageConfigurations
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return FeeProjectConfig({
|
|
102
|
+
configuration: revnetConfiguration,
|
|
103
|
+
terminalConfigurations: terminalConfigurations,
|
|
104
|
+
suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
|
|
105
|
+
deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256(abi.encodePacked("REV"))
|
|
106
|
+
})
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/// @notice Deploy a revnet with two stages for testing stage transitions.
|
|
111
|
+
/// Stage 0: cashOutTaxRate=2000 (20%), starts now
|
|
112
|
+
/// Stage 1: cashOutTaxRate=6000 (60%), starts after 30 days
|
|
113
|
+
function _deployTwoStageRevnet() internal returns (uint256 revnetId) {
|
|
114
|
+
uint8 decimals = 18;
|
|
115
|
+
uint256 decimalMultiplier = 10 ** decimals;
|
|
116
|
+
|
|
117
|
+
JBAccountingContext[] memory accountingContextsToAccept = new JBAccountingContext[](2);
|
|
118
|
+
accountingContextsToAccept[0] = JBAccountingContext({
|
|
119
|
+
token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
120
|
+
});
|
|
121
|
+
accountingContextsToAccept[1] =
|
|
122
|
+
JBAccountingContext({token: address(TOKEN), decimals: 6, currency: uint32(uint160(address(TOKEN)))});
|
|
123
|
+
|
|
124
|
+
JBTerminalConfig[] memory terminalConfigurations = new JBTerminalConfig[](1);
|
|
125
|
+
terminalConfigurations[0] =
|
|
126
|
+
JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: accountingContextsToAccept});
|
|
127
|
+
|
|
128
|
+
REVStageConfig[] memory stageConfigurations = new REVStageConfig[](2);
|
|
129
|
+
JBSplit[] memory splits = new JBSplit[](1);
|
|
130
|
+
splits[0].beneficiary = payable(multisig());
|
|
131
|
+
splits[0].percent = 10_000;
|
|
132
|
+
|
|
133
|
+
// Stage 0: low tax rate (20%).
|
|
134
|
+
stageConfigurations[0] = REVStageConfig({
|
|
135
|
+
startsAtOrAfter: uint40(block.timestamp),
|
|
136
|
+
autoIssuances: new REVAutoIssuance[](0),
|
|
137
|
+
splitPercent: 2000,
|
|
138
|
+
splits: splits,
|
|
139
|
+
initialIssuance: uint112(1000 * decimalMultiplier),
|
|
140
|
+
issuanceCutFrequency: 0,
|
|
141
|
+
issuanceCutPercent: 0,
|
|
142
|
+
cashOutTaxRate: 2000, // 20%
|
|
143
|
+
extraMetadata: 0
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Stage 1: high tax rate (60%), starts after 30 days.
|
|
147
|
+
stageConfigurations[1] = REVStageConfig({
|
|
148
|
+
startsAtOrAfter: uint40(block.timestamp + 30 days),
|
|
149
|
+
autoIssuances: new REVAutoIssuance[](0),
|
|
150
|
+
splitPercent: 2000,
|
|
151
|
+
splits: splits,
|
|
152
|
+
initialIssuance: 0, // inherit
|
|
153
|
+
issuanceCutFrequency: 0,
|
|
154
|
+
issuanceCutPercent: 0,
|
|
155
|
+
cashOutTaxRate: 6000, // 60%
|
|
156
|
+
extraMetadata: 0
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
REVLoanSource[] memory loanSources = new REVLoanSource[](1);
|
|
160
|
+
loanSources[0] = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
|
|
161
|
+
|
|
162
|
+
REVConfig memory revnetConfiguration = REVConfig({
|
|
163
|
+
description: REVDescription("TwoStage", "$TWO", "ipfs://test", "TWO_TOKEN"),
|
|
164
|
+
baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
165
|
+
splitOperator: multisig(),
|
|
166
|
+
stageConfigurations: stageConfigurations
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
revnetId = REV_DEPLOYER.deployFor({
|
|
170
|
+
revnetId: 0,
|
|
171
|
+
configuration: revnetConfiguration,
|
|
172
|
+
terminalConfigurations: terminalConfigurations,
|
|
173
|
+
suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
|
|
174
|
+
deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256(abi.encodePacked("TWO"))
|
|
175
|
+
})
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/// @notice Deploy a single-stage revnet with loans enabled.
|
|
180
|
+
function _deploySingleStageRevnet() internal returns (uint256 revnetId) {
|
|
181
|
+
uint8 decimals = 18;
|
|
182
|
+
uint256 decimalMultiplier = 10 ** decimals;
|
|
183
|
+
|
|
184
|
+
JBAccountingContext[] memory accountingContextsToAccept = new JBAccountingContext[](2);
|
|
185
|
+
accountingContextsToAccept[0] = JBAccountingContext({
|
|
186
|
+
token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
187
|
+
});
|
|
188
|
+
accountingContextsToAccept[1] =
|
|
189
|
+
JBAccountingContext({token: address(TOKEN), decimals: 6, currency: uint32(uint160(address(TOKEN)))});
|
|
190
|
+
|
|
191
|
+
JBTerminalConfig[] memory terminalConfigurations = new JBTerminalConfig[](1);
|
|
192
|
+
terminalConfigurations[0] =
|
|
193
|
+
JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: accountingContextsToAccept});
|
|
194
|
+
|
|
195
|
+
REVStageConfig[] memory stageConfigurations = new REVStageConfig[](1);
|
|
196
|
+
JBSplit[] memory splits = new JBSplit[](1);
|
|
197
|
+
splits[0].beneficiary = payable(multisig());
|
|
198
|
+
splits[0].percent = 10_000;
|
|
199
|
+
|
|
200
|
+
stageConfigurations[0] = REVStageConfig({
|
|
201
|
+
startsAtOrAfter: uint40(block.timestamp),
|
|
202
|
+
autoIssuances: new REVAutoIssuance[](0),
|
|
203
|
+
splitPercent: 2000,
|
|
204
|
+
splits: splits,
|
|
205
|
+
initialIssuance: uint112(1000 * decimalMultiplier),
|
|
206
|
+
issuanceCutFrequency: 0,
|
|
207
|
+
issuanceCutPercent: 0,
|
|
208
|
+
cashOutTaxRate: 0, // 0% tax for simplicity
|
|
209
|
+
extraMetadata: 0
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
REVLoanSource[] memory loanSources = new REVLoanSource[](1);
|
|
213
|
+
loanSources[0] = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
|
|
214
|
+
|
|
215
|
+
REVConfig memory revnetConfiguration = REVConfig({
|
|
216
|
+
description: REVDescription("Single", "$SGL", "ipfs://test", "SGL_TOKEN"),
|
|
217
|
+
baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
218
|
+
splitOperator: multisig(),
|
|
219
|
+
stageConfigurations: stageConfigurations
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
revnetId = REV_DEPLOYER.deployFor({
|
|
223
|
+
revnetId: 0,
|
|
224
|
+
configuration: revnetConfiguration,
|
|
225
|
+
terminalConfigurations: terminalConfigurations,
|
|
226
|
+
suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
|
|
227
|
+
deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256(abi.encodePacked("SGL"))
|
|
228
|
+
})
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function setUp() public override {
|
|
233
|
+
super.setUp();
|
|
234
|
+
|
|
235
|
+
FEE_PROJECT_ID = jbProjects().createFor(multisig());
|
|
236
|
+
SUCKER_REGISTRY = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
|
|
237
|
+
HOOK_STORE = new JB721TiersHookStore();
|
|
238
|
+
EXAMPLE_HOOK = new JB721TiersHook(jbDirectory(), jbPermissions(), jbRulesets(), HOOK_STORE, multisig());
|
|
239
|
+
ADDRESS_REGISTRY = new JBAddressRegistry();
|
|
240
|
+
HOOK_DEPLOYER = new JB721TiersHookDeployer(EXAMPLE_HOOK, HOOK_STORE, ADDRESS_REGISTRY, multisig());
|
|
241
|
+
PUBLISHER = new CTPublisher(jbDirectory(), jbPermissions(), FEE_PROJECT_ID, multisig());
|
|
242
|
+
MOCK_BUYBACK = new MockBuybackDataHook();
|
|
243
|
+
TOKEN = new MockERC20("1/2 ETH", "1/2");
|
|
244
|
+
|
|
245
|
+
MockPriceFeed priceFeed = new MockPriceFeed(1e21, 6);
|
|
246
|
+
vm.prank(multisig());
|
|
247
|
+
jbPrices()
|
|
248
|
+
.addPriceFeedFor(0, uint32(uint160(address(TOKEN))), uint32(uint160(JBConstants.NATIVE_TOKEN)), priceFeed);
|
|
249
|
+
|
|
250
|
+
LOANS_CONTRACT = new REVLoans({
|
|
251
|
+
controller: jbController(),
|
|
252
|
+
projects: jbProjects(),
|
|
253
|
+
revId: FEE_PROJECT_ID,
|
|
254
|
+
owner: address(this),
|
|
255
|
+
permit2: permit2(),
|
|
256
|
+
trustedForwarder: TRUSTED_FORWARDER
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
REV_DEPLOYER = new REVDeployer{salt: REV_DEPLOYER_SALT}(
|
|
260
|
+
jbController(),
|
|
261
|
+
SUCKER_REGISTRY,
|
|
262
|
+
FEE_PROJECT_ID,
|
|
263
|
+
HOOK_DEPLOYER,
|
|
264
|
+
PUBLISHER,
|
|
265
|
+
IJBRulesetDataHook(address(MOCK_BUYBACK)),
|
|
266
|
+
address(LOANS_CONTRACT),
|
|
267
|
+
TRUSTED_FORWARDER
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
// Deploy fee project.
|
|
271
|
+
vm.prank(multisig());
|
|
272
|
+
jbProjects().approve(address(REV_DEPLOYER), FEE_PROJECT_ID);
|
|
273
|
+
|
|
274
|
+
FeeProjectConfig memory feeProjectConfig = getFeeProjectConfig();
|
|
275
|
+
vm.prank(multisig());
|
|
276
|
+
REV_DEPLOYER.deployFor({
|
|
277
|
+
revnetId: FEE_PROJECT_ID,
|
|
278
|
+
configuration: feeProjectConfig.configuration,
|
|
279
|
+
terminalConfigurations: feeProjectConfig.terminalConfigurations,
|
|
280
|
+
suckerDeploymentConfiguration: feeProjectConfig.suckerDeploymentConfiguration
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
vm.deal(USER, 1000 ether);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/// @notice Stage transition reduces borrowable amount due to higher cashOutTaxRate.
|
|
287
|
+
function test_stageTransition_reducesLoanHealth() public {
|
|
288
|
+
uint256 revnetId = _deployTwoStageRevnet();
|
|
289
|
+
|
|
290
|
+
// Pay ETH into revnet to create surplus and get tokens.
|
|
291
|
+
vm.prank(USER);
|
|
292
|
+
uint256 tokens =
|
|
293
|
+
jbMultiTerminal().pay{value: 10 ether}(revnetId, JBConstants.NATIVE_TOKEN, 10 ether, USER, 0, "", "");
|
|
294
|
+
assertGt(tokens, 0, "Should have received tokens");
|
|
295
|
+
|
|
296
|
+
// Check borrowable amount in stage 0 (20% tax).
|
|
297
|
+
uint256 borrowableStage0 =
|
|
298
|
+
LOANS_CONTRACT.borrowableAmountFrom(revnetId, tokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
|
|
299
|
+
assertGt(borrowableStage0, 0, "Should have borrowable amount in stage 0");
|
|
300
|
+
|
|
301
|
+
// Warp to stage 1 (60% tax).
|
|
302
|
+
vm.warp(block.timestamp + 30 days + 1);
|
|
303
|
+
|
|
304
|
+
// Check borrowable amount in stage 1 — should be lower due to higher tax.
|
|
305
|
+
uint256 borrowableStage1 =
|
|
306
|
+
LOANS_CONTRACT.borrowableAmountFrom(revnetId, tokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
|
|
307
|
+
|
|
308
|
+
assertLt(borrowableStage1, borrowableStage0, "Borrowable amount should decrease when cashOutTaxRate increases");
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/// @notice After full repayment, loan data is deleted (storage cleared).
|
|
312
|
+
function test_loanDataDeletedAfterRepay() public {
|
|
313
|
+
uint256 revnetId = _deploySingleStageRevnet();
|
|
314
|
+
|
|
315
|
+
// Pay ETH into revnet to get tokens.
|
|
316
|
+
vm.prank(USER);
|
|
317
|
+
uint256 tokens =
|
|
318
|
+
jbMultiTerminal().pay{value: 10 ether}(revnetId, JBConstants.NATIVE_TOKEN, 10 ether, USER, 0, "", "");
|
|
319
|
+
|
|
320
|
+
uint256 loanable =
|
|
321
|
+
LOANS_CONTRACT.borrowableAmountFrom(revnetId, tokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
|
|
322
|
+
// Skip if nothing borrowable.
|
|
323
|
+
vm.assume(loanable > 0);
|
|
324
|
+
|
|
325
|
+
// Mock permission for BURN (permission ID 10).
|
|
326
|
+
mockExpect(
|
|
327
|
+
address(jbPermissions()),
|
|
328
|
+
abi.encodeCall(IJBPermissions.hasPermission, (address(LOANS_CONTRACT), USER, revnetId, 11, true, true)),
|
|
329
|
+
abi.encode(true)
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
|
|
333
|
+
|
|
334
|
+
uint256 minPrepaid = LOANS_CONTRACT.MIN_PREPAID_FEE_PERCENT();
|
|
335
|
+
|
|
336
|
+
vm.prank(USER);
|
|
337
|
+
(uint256 loanId,) = LOANS_CONTRACT.borrowFrom(revnetId, source, loanable, tokens, payable(USER), minPrepaid);
|
|
338
|
+
|
|
339
|
+
REVLoan memory loanBefore = LOANS_CONTRACT.loanOf(loanId);
|
|
340
|
+
assertGt(loanBefore.amount, 0, "Loan should have an amount");
|
|
341
|
+
assertGt(loanBefore.collateral, 0, "Loan should have collateral");
|
|
342
|
+
|
|
343
|
+
// Fully repay the loan — return all collateral.
|
|
344
|
+
JBSingleAllowance memory allowance;
|
|
345
|
+
|
|
346
|
+
vm.prank(USER);
|
|
347
|
+
LOANS_CONTRACT.repayLoan{value: loanBefore.amount * 2}(
|
|
348
|
+
loanId, loanBefore.amount * 2, loanBefore.collateral, payable(USER), allowance
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
// After repayment, loan storage should be cleared.
|
|
352
|
+
REVLoan memory loanAfter = LOANS_CONTRACT.loanOf(loanId);
|
|
353
|
+
assertEq(loanAfter.amount, 0, "Loan amount should be 0 after repay");
|
|
354
|
+
assertEq(loanAfter.collateral, 0, "Loan collateral should be 0 after repay");
|
|
355
|
+
assertEq(loanAfter.createdAt, 0, "Loan createdAt should be 0 after repay");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/// @notice After liquidation, loan data is deleted (storage cleared).
|
|
359
|
+
function test_loanDataDeletedAfterLiquidation() public {
|
|
360
|
+
uint256 revnetId = _deploySingleStageRevnet();
|
|
361
|
+
|
|
362
|
+
// Pay ETH into revnet to get tokens.
|
|
363
|
+
vm.prank(USER);
|
|
364
|
+
uint256 tokens =
|
|
365
|
+
jbMultiTerminal().pay{value: 10 ether}(revnetId, JBConstants.NATIVE_TOKEN, 10 ether, USER, 0, "", "");
|
|
366
|
+
|
|
367
|
+
uint256 loanable =
|
|
368
|
+
LOANS_CONTRACT.borrowableAmountFrom(revnetId, tokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
|
|
369
|
+
// Skip if nothing borrowable.
|
|
370
|
+
vm.assume(loanable > 0);
|
|
371
|
+
|
|
372
|
+
// Mock permission for BURN (permission ID 10).
|
|
373
|
+
mockExpect(
|
|
374
|
+
address(jbPermissions()),
|
|
375
|
+
abi.encodeCall(IJBPermissions.hasPermission, (address(LOANS_CONTRACT), USER, revnetId, 11, true, true)),
|
|
376
|
+
abi.encode(true)
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
|
|
380
|
+
|
|
381
|
+
uint256 minPrepaid = LOANS_CONTRACT.MIN_PREPAID_FEE_PERCENT();
|
|
382
|
+
|
|
383
|
+
vm.prank(USER);
|
|
384
|
+
(uint256 loanId,) = LOANS_CONTRACT.borrowFrom(revnetId, source, loanable, tokens, payable(USER), minPrepaid);
|
|
385
|
+
|
|
386
|
+
REVLoan memory loanBefore = LOANS_CONTRACT.loanOf(loanId);
|
|
387
|
+
assertGt(loanBefore.amount, 0, "Loan should exist before liquidation");
|
|
388
|
+
|
|
389
|
+
// Warp past LOAN_LIQUIDATION_DURATION (3650 days).
|
|
390
|
+
vm.warp(block.timestamp + 3650 days + 1);
|
|
391
|
+
|
|
392
|
+
// Get the loan number from the ID (loanId = revnetId * 1_000_000_000_000 + loanNumber).
|
|
393
|
+
// For the first loan, loanNumber is 1.
|
|
394
|
+
LOANS_CONTRACT.liquidateExpiredLoansFrom(revnetId, 1, 1);
|
|
395
|
+
|
|
396
|
+
// After liquidation, loan storage should be cleared.
|
|
397
|
+
REVLoan memory loanAfter = LOANS_CONTRACT.loanOf(loanId);
|
|
398
|
+
assertEq(loanAfter.amount, 0, "Loan amount should be 0 after liquidation");
|
|
399
|
+
assertEq(loanAfter.collateral, 0, "Loan collateral should be 0 after liquidation");
|
|
400
|
+
assertEq(loanAfter.createdAt, 0, "Loan createdAt should be 0 after liquidation");
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/// @notice Partial repay (return some but not all collateral) clears old loan storage
|
|
404
|
+
/// and creates a replacement loan. Exercises the `else` branch in `_repayLoan`.
|
|
405
|
+
function test_partialRepay_clearsOldLoanStorage() public {
|
|
406
|
+
uint256 revnetId = _deploySingleStageRevnet();
|
|
407
|
+
|
|
408
|
+
// Pay ETH into revnet to get tokens.
|
|
409
|
+
vm.prank(USER);
|
|
410
|
+
uint256 tokens =
|
|
411
|
+
jbMultiTerminal().pay{value: 10 ether}(revnetId, JBConstants.NATIVE_TOKEN, 10 ether, USER, 0, "", "");
|
|
412
|
+
|
|
413
|
+
uint256 loanable =
|
|
414
|
+
LOANS_CONTRACT.borrowableAmountFrom(revnetId, tokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
|
|
415
|
+
vm.assume(loanable > 0);
|
|
416
|
+
|
|
417
|
+
// Mock permission for BURN (permission ID 10).
|
|
418
|
+
mockExpect(
|
|
419
|
+
address(jbPermissions()),
|
|
420
|
+
abi.encodeCall(IJBPermissions.hasPermission, (address(LOANS_CONTRACT), USER, revnetId, 11, true, true)),
|
|
421
|
+
abi.encode(true)
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
|
|
425
|
+
uint256 minPrepaid = LOANS_CONTRACT.MIN_PREPAID_FEE_PERCENT();
|
|
426
|
+
|
|
427
|
+
vm.prank(USER);
|
|
428
|
+
(uint256 loanId,) = LOANS_CONTRACT.borrowFrom(revnetId, source, loanable, tokens, payable(USER), minPrepaid);
|
|
429
|
+
|
|
430
|
+
REVLoan memory loanBefore = LOANS_CONTRACT.loanOf(loanId);
|
|
431
|
+
assertGt(loanBefore.collateral, 1, "Need >1 collateral for partial return");
|
|
432
|
+
|
|
433
|
+
// Partial repay: return HALF the collateral (triggers else branch in _repayLoan).
|
|
434
|
+
uint256 halfCollateral = loanBefore.collateral / 2;
|
|
435
|
+
JBSingleAllowance memory allowance;
|
|
436
|
+
|
|
437
|
+
// Send loan.amount as maxRepay — more than enough for partial repay.
|
|
438
|
+
vm.prank(USER);
|
|
439
|
+
(uint256 newLoanId, REVLoan memory newLoan) = LOANS_CONTRACT.repayLoan{value: loanBefore.amount}(
|
|
440
|
+
loanId, loanBefore.amount, halfCollateral, payable(USER), allowance
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
// Old loan storage should be cleared (the delete we're testing).
|
|
444
|
+
REVLoan memory oldLoan = LOANS_CONTRACT.loanOf(loanId);
|
|
445
|
+
assertEq(oldLoan.amount, 0, "Old loan amount should be 0 after partial repay");
|
|
446
|
+
assertEq(oldLoan.collateral, 0, "Old loan collateral should be 0 after partial repay");
|
|
447
|
+
assertEq(oldLoan.createdAt, 0, "Old loan createdAt should be 0 after partial repay");
|
|
448
|
+
|
|
449
|
+
// New replacement loan should exist with remaining values.
|
|
450
|
+
assertGt(newLoan.amount, 0, "New loan should have amount");
|
|
451
|
+
assertGt(newLoan.collateral, 0, "New loan should have collateral");
|
|
452
|
+
assertLt(newLoan.amount, loanBefore.amount, "New loan amount should be less than original");
|
|
453
|
+
|
|
454
|
+
// Verify via storage read too (not just return value).
|
|
455
|
+
REVLoan memory newLoanFromStorage = LOANS_CONTRACT.loanOf(newLoanId);
|
|
456
|
+
assertEq(newLoanFromStorage.amount, newLoan.amount, "Storage should match return value");
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/// @notice Repaying with excess ETH correctly refunds the difference.
|
|
460
|
+
/// This tests the sourceToken caching fix — before the fix, `loan.source.token` was read
|
|
461
|
+
/// after `_repayLoan` deleted the storage, yielding `address(0)` and reverting.
|
|
462
|
+
function test_repayLoan_refundsExcessWithCorrectToken() public {
|
|
463
|
+
uint256 revnetId = _deploySingleStageRevnet();
|
|
464
|
+
|
|
465
|
+
// Pay ETH into revnet to get tokens.
|
|
466
|
+
vm.prank(USER);
|
|
467
|
+
uint256 tokens =
|
|
468
|
+
jbMultiTerminal().pay{value: 10 ether}(revnetId, JBConstants.NATIVE_TOKEN, 10 ether, USER, 0, "", "");
|
|
469
|
+
|
|
470
|
+
uint256 loanable =
|
|
471
|
+
LOANS_CONTRACT.borrowableAmountFrom(revnetId, tokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
|
|
472
|
+
vm.assume(loanable > 0);
|
|
473
|
+
|
|
474
|
+
// Mock permission for BURN (permission ID 10).
|
|
475
|
+
mockExpect(
|
|
476
|
+
address(jbPermissions()),
|
|
477
|
+
abi.encodeCall(IJBPermissions.hasPermission, (address(LOANS_CONTRACT), USER, revnetId, 11, true, true)),
|
|
478
|
+
abi.encode(true)
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
|
|
482
|
+
uint256 minPrepaid = LOANS_CONTRACT.MIN_PREPAID_FEE_PERCENT();
|
|
483
|
+
|
|
484
|
+
vm.prank(USER);
|
|
485
|
+
(uint256 loanId,) = LOANS_CONTRACT.borrowFrom(revnetId, source, loanable, tokens, payable(USER), minPrepaid);
|
|
486
|
+
|
|
487
|
+
REVLoan memory loan = LOANS_CONTRACT.loanOf(loanId);
|
|
488
|
+
|
|
489
|
+
// Partial repay returning half collateral. Send 2x the loan amount — guaranteed excess.
|
|
490
|
+
uint256 halfCollateral = loan.collateral / 2;
|
|
491
|
+
uint256 excessivePayment = loan.amount * 2;
|
|
492
|
+
|
|
493
|
+
uint256 balBefore = USER.balance;
|
|
494
|
+
JBSingleAllowance memory allowance;
|
|
495
|
+
|
|
496
|
+
vm.prank(USER);
|
|
497
|
+
LOANS_CONTRACT.repayLoan{value: excessivePayment}(
|
|
498
|
+
loanId, excessivePayment, halfCollateral, payable(USER), allowance
|
|
499
|
+
);
|
|
500
|
+
|
|
501
|
+
uint256 balAfter = USER.balance;
|
|
502
|
+
|
|
503
|
+
// User sent `excessivePayment` but should get excess back.
|
|
504
|
+
// Net cost = repayBorrowAmount (includes fee). Must be less than loan.amount.
|
|
505
|
+
uint256 netCost = balBefore - balAfter;
|
|
506
|
+
assertLt(netCost, excessivePayment, "User should have been refunded excess ETH");
|
|
507
|
+
assertGt(netCost, 0, "User should have paid something");
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/// @notice Reallocation clears old loan storage after creating replacement.
|
|
511
|
+
/// Exercises the `delete _loanOf[loanId]` in `_reallocateCollateralFromLoan`.
|
|
512
|
+
function test_reallocateCollateral_clearsOldLoanStorage() public {
|
|
513
|
+
uint256 revnetId = _deploySingleStageRevnet();
|
|
514
|
+
|
|
515
|
+
// Pay ETH into revnet to get tokens.
|
|
516
|
+
vm.prank(USER);
|
|
517
|
+
uint256 tokens =
|
|
518
|
+
jbMultiTerminal().pay{value: 10 ether}(revnetId, JBConstants.NATIVE_TOKEN, 10 ether, USER, 0, "", "");
|
|
519
|
+
|
|
520
|
+
uint256 loanable =
|
|
521
|
+
LOANS_CONTRACT.borrowableAmountFrom(revnetId, tokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
|
|
522
|
+
vm.assume(loanable > 0);
|
|
523
|
+
|
|
524
|
+
// Mock permission for BURN (permission ID 10).
|
|
525
|
+
mockExpect(
|
|
526
|
+
address(jbPermissions()),
|
|
527
|
+
abi.encodeCall(IJBPermissions.hasPermission, (address(LOANS_CONTRACT), USER, revnetId, 11, true, true)),
|
|
528
|
+
abi.encode(true)
|
|
529
|
+
);
|
|
530
|
+
|
|
531
|
+
REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
|
|
532
|
+
uint256 minPrepaid = LOANS_CONTRACT.MIN_PREPAID_FEE_PERCENT();
|
|
533
|
+
|
|
534
|
+
// Borrow the full max against all tokens.
|
|
535
|
+
vm.prank(USER);
|
|
536
|
+
(uint256 loanId,) = LOANS_CONTRACT.borrowFrom(revnetId, source, loanable, tokens, payable(USER), minPrepaid);
|
|
537
|
+
|
|
538
|
+
REVLoan memory loanBefore = LOANS_CONTRACT.loanOf(loanId);
|
|
539
|
+
assertGt(loanBefore.collateral, 0, "Loan should have collateral");
|
|
540
|
+
|
|
541
|
+
// Increase surplus WITHOUT minting new tokens — makes existing collateral worth more.
|
|
542
|
+
// This allows reallocating collateral since borrowable(reduced collateral) > loan.amount.
|
|
543
|
+
address DONOR = makeAddr("donor");
|
|
544
|
+
vm.deal(DONOR, 100 ether);
|
|
545
|
+
vm.prank(DONOR);
|
|
546
|
+
jbMultiTerminal().addToBalanceOf{value: 50 ether}(revnetId, JBConstants.NATIVE_TOKEN, 50 ether, false, "", "");
|
|
547
|
+
|
|
548
|
+
// Transfer a small amount of collateral (10%) to a new loan.
|
|
549
|
+
uint256 collateralToTransfer = loanBefore.collateral / 10;
|
|
550
|
+
assertGt(collateralToTransfer, 0, "Must transfer some collateral");
|
|
551
|
+
|
|
552
|
+
vm.prank(USER);
|
|
553
|
+
(uint256 reallocatedLoanId, uint256 newLoanId, REVLoan memory reallocatedLoan,) = LOANS_CONTRACT.reallocateCollateralFromLoan(
|
|
554
|
+
loanId, collateralToTransfer, source, 0, 0, payable(USER), minPrepaid
|
|
555
|
+
);
|
|
556
|
+
|
|
557
|
+
// Old loan storage should be cleared.
|
|
558
|
+
REVLoan memory oldLoan = LOANS_CONTRACT.loanOf(loanId);
|
|
559
|
+
assertEq(oldLoan.amount, 0, "Old loan amount should be 0 after reallocation");
|
|
560
|
+
assertEq(oldLoan.collateral, 0, "Old loan collateral should be 0 after reallocation");
|
|
561
|
+
assertEq(oldLoan.createdAt, 0, "Old loan createdAt should be 0 after reallocation");
|
|
562
|
+
|
|
563
|
+
// Reallocated loan should have reduced collateral but same amount.
|
|
564
|
+
assertEq(reallocatedLoan.amount, loanBefore.amount, "Reallocated loan should keep original amount");
|
|
565
|
+
assertLt(reallocatedLoan.collateral, loanBefore.collateral, "Reallocated loan should have less collateral");
|
|
566
|
+
|
|
567
|
+
// New loan should exist.
|
|
568
|
+
assertTrue(newLoanId != loanId, "New loan should have different ID");
|
|
569
|
+
assertTrue(newLoanId != reallocatedLoanId, "New loan should differ from reallocated loan");
|
|
570
|
+
}
|
|
571
|
+
}
|