@rev-net/core-v6 0.0.7 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ADMINISTRATION.md +186 -0
- package/ARCHITECTURE.md +87 -0
- package/README.md +4 -2
- package/RISKS.md +49 -0
- package/SKILLS.md +22 -2
- package/STYLE_GUIDE.md +482 -0
- package/foundry.toml +6 -6
- package/package.json +13 -10
- package/script/Deploy.s.sol +3 -2
- package/src/REVDeployer.sol +129 -72
- package/src/REVLoans.sol +174 -165
- package/src/interfaces/IREVDeployer.sol +111 -72
- package/src/interfaces/IREVLoans.sol +116 -76
- package/src/structs/REV721TiersHookFlags.sol +14 -0
- package/src/structs/REVBaseline721HookConfig.sol +27 -0
- package/src/structs/REVDeploy721TiersHookConfig.sol +2 -2
- package/test/REV.integrations.t.sol +4 -3
- package/test/REVAutoIssuanceFuzz.t.sol +12 -8
- package/test/REVDeployerAuditRegressions.t.sol +4 -3
- package/test/REVInvincibility.t.sol +8 -6
- package/test/REVInvincibilityHandler.sol +1 -0
- package/test/REVLifecycle.t.sol +4 -3
- package/test/REVLoans.invariants.t.sol +5 -3
- package/test/REVLoansAttacks.t.sol +4 -3
- package/test/REVLoansAuditRegressions.t.sol +13 -24
- package/test/REVLoansFeeRecovery.t.sol +4 -3
- package/test/REVLoansSourced.t.sol +4 -3
- package/test/REVLoansUnSourced.t.sol +4 -3
- package/test/REVLoans_AuditFindings.t.sol +644 -0
- package/test/TestEmptyBuybackSpecs.t.sol +4 -3
- package/test/TestPR09_ConversionDocumentation.t.sol +4 -3
- package/test/TestPR10_LiquidationBehavior.t.sol +4 -3
- package/test/TestPR11_LowFindings.t.sol +4 -3
- package/test/TestPR12_FlashLoanSurplus.t.sol +4 -3
- package/test/TestPR13_CrossSourceReallocation.t.sol +4 -3
- package/test/TestPR15_CashOutCallerValidation.t.sol +4 -3
- package/test/TestPR16_ZeroRepayment.t.sol +4 -3
- package/test/TestPR21_Uint112Overflow.t.sol +4 -3
- package/test/TestPR22_HookArrayOOB.t.sol +4 -3
- package/test/TestPR26_BurnHeldTokens.t.sol +4 -3
- package/test/TestPR27_CEIPattern.t.sol +4 -3
- package/test/TestPR29_SwapTerminalPermission.t.sol +4 -3
- package/test/TestPR32_MixedFixes.t.sol +4 -3
- package/test/TestSplitWeightAdjustment.t.sol +445 -0
- package/test/TestSplitWeightE2E.t.sol +528 -0
- package/test/TestSplitWeightFork.t.sol +821 -0
- package/test/TestStageTransitionBorrowable.t.sol +4 -3
- package/test/fork/ForkTestBase.sol +617 -0
- package/test/fork/TestCashOutFork.t.sol +245 -0
- package/test/fork/TestLoanBorrowFork.t.sol +163 -0
- package/test/fork/TestLoanLiquidationFork.t.sol +129 -0
- package/test/fork/TestLoanReallocateFork.t.sol +103 -0
- package/test/fork/TestLoanRepayFork.t.sol +184 -0
- package/test/fork/TestSplitWeightFork.t.sol +186 -0
- package/test/mock/MockBuybackDataHook.sol +11 -4
- package/test/mock/MockBuybackDataHookMintPath.sol +11 -3
- package/test/regression/TestI20_CumulativeLoanCounter.t.sol +6 -5
- package/test/regression/TestL27_LiquidateGapHandling.t.sol +7 -6
- package/SECURITY.md +0 -68
|
@@ -0,0 +1,644 @@
|
|
|
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
|
+
|
|
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/router-terminal-v6/script/helpers/RouterTerminalDeploymentLib.sol";
|
|
16
|
+
|
|
17
|
+
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
18
|
+
import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
|
|
19
|
+
import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
|
|
20
|
+
import {JBRulesetMetadata} from "@bananapus/core-v6/src/structs/JBRulesetMetadata.sol";
|
|
21
|
+
import {JBSingleAllowance} from "@bananapus/core-v6/src/structs/JBSingleAllowance.sol";
|
|
22
|
+
import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
|
|
23
|
+
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
|
|
24
|
+
import {REVLoans} from "../src/REVLoans.sol";
|
|
25
|
+
import {REVLoan} from "../src/structs/REVLoan.sol";
|
|
26
|
+
import {REVStageConfig, REVAutoIssuance} from "../src/structs/REVStageConfig.sol";
|
|
27
|
+
import {REVLoanSource} from "../src/structs/REVLoanSource.sol";
|
|
28
|
+
import {REVDescription} from "../src/structs/REVDescription.sol";
|
|
29
|
+
import {IREVLoans} from "./../src/interfaces/IREVLoans.sol";
|
|
30
|
+
import {JBSuckerDeployerConfig} from "@bananapus/suckers-v6/src/structs/JBSuckerDeployerConfig.sol";
|
|
31
|
+
import {JBSuckerRegistry} from "@bananapus/suckers-v6/src/JBSuckerRegistry.sol";
|
|
32
|
+
import {JB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
|
|
33
|
+
import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
|
|
34
|
+
import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
|
|
35
|
+
import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
|
|
36
|
+
import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
|
|
37
|
+
|
|
38
|
+
/// @notice A fake terminal that returns garbage accounting contexts.
|
|
39
|
+
/// Used to test unvalidated loan source terminal rejection.
|
|
40
|
+
contract GarbageTerminal is ERC165, IJBPayoutTerminal {
|
|
41
|
+
function useAllowanceOf(
|
|
42
|
+
uint256,
|
|
43
|
+
address,
|
|
44
|
+
uint256,
|
|
45
|
+
uint256,
|
|
46
|
+
uint256,
|
|
47
|
+
address payable,
|
|
48
|
+
address payable,
|
|
49
|
+
string calldata
|
|
50
|
+
)
|
|
51
|
+
external
|
|
52
|
+
pure
|
|
53
|
+
override
|
|
54
|
+
returns (uint256)
|
|
55
|
+
{
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function accountingContextForTokenOf(uint256, address) external pure override returns (JBAccountingContext memory) {
|
|
60
|
+
// Return garbage values to demonstrate the danger.
|
|
61
|
+
return JBAccountingContext({token: address(0xdead), decimals: 42, currency: 999_999});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function accountingContextsOf(uint256) external pure override returns (JBAccountingContext[] memory) {
|
|
65
|
+
JBAccountingContext[] memory contexts = new JBAccountingContext[](1);
|
|
66
|
+
contexts[0] = JBAccountingContext({token: address(0xdead), decimals: 42, currency: 999_999});
|
|
67
|
+
return contexts;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function addAccountingContextsFor(uint256, JBAccountingContext[] calldata) external override {}
|
|
71
|
+
|
|
72
|
+
function addToBalanceOf(
|
|
73
|
+
uint256,
|
|
74
|
+
address,
|
|
75
|
+
uint256,
|
|
76
|
+
bool,
|
|
77
|
+
string calldata,
|
|
78
|
+
bytes calldata
|
|
79
|
+
)
|
|
80
|
+
external
|
|
81
|
+
payable
|
|
82
|
+
override
|
|
83
|
+
{}
|
|
84
|
+
|
|
85
|
+
function currentSurplusOf(
|
|
86
|
+
uint256,
|
|
87
|
+
JBAccountingContext[] memory,
|
|
88
|
+
uint256,
|
|
89
|
+
uint256
|
|
90
|
+
)
|
|
91
|
+
external
|
|
92
|
+
pure
|
|
93
|
+
override
|
|
94
|
+
returns (uint256)
|
|
95
|
+
{
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function migrateBalanceOf(uint256, address, IJBTerminal) external pure override returns (uint256) {
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function pay(
|
|
104
|
+
uint256,
|
|
105
|
+
address,
|
|
106
|
+
uint256,
|
|
107
|
+
address,
|
|
108
|
+
uint256,
|
|
109
|
+
string calldata,
|
|
110
|
+
bytes calldata
|
|
111
|
+
)
|
|
112
|
+
external
|
|
113
|
+
payable
|
|
114
|
+
override
|
|
115
|
+
returns (uint256)
|
|
116
|
+
{
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function sendPayoutsOf(uint256, address, uint256, uint256, uint256) external pure override returns (uint256) {
|
|
121
|
+
return 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function supportsInterface(bytes4 interfaceId) public view override(ERC165, IERC165) returns (bool) {
|
|
125
|
+
return interfaceId == type(IJBTerminal).interfaceId || interfaceId == type(IJBPayoutTerminal).interfaceId
|
|
126
|
+
|| super.supportsInterface(interfaceId);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
receive() external payable {}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/// @notice Regression tests for nemesis audit findings.
|
|
133
|
+
/// Unvalidated loan source terminal
|
|
134
|
+
/// RepayLoan event emits zeroed values
|
|
135
|
+
/// Auto-issuance timing guard bypass (false positive)
|
|
136
|
+
/// repayLoan revert on excess collateral (false positive)
|
|
137
|
+
contract REVLoans_AuditFindings is TestBaseWorkflow {
|
|
138
|
+
bytes32 REV_DEPLOYER_SALT = "REVDeployer";
|
|
139
|
+
bytes32 ERC20_SALT = "REV_TOKEN";
|
|
140
|
+
|
|
141
|
+
REVDeployer REV_DEPLOYER;
|
|
142
|
+
JB721TiersHook EXAMPLE_HOOK;
|
|
143
|
+
IJB721TiersHookDeployer HOOK_DEPLOYER;
|
|
144
|
+
IJB721TiersHookStore HOOK_STORE;
|
|
145
|
+
IJBAddressRegistry ADDRESS_REGISTRY;
|
|
146
|
+
IREVLoans LOANS_CONTRACT;
|
|
147
|
+
IJBSuckerRegistry SUCKER_REGISTRY;
|
|
148
|
+
CTPublisher PUBLISHER;
|
|
149
|
+
MockBuybackDataHook MOCK_BUYBACK;
|
|
150
|
+
|
|
151
|
+
uint256 FEE_PROJECT_ID;
|
|
152
|
+
uint256 REVNET_ID;
|
|
153
|
+
|
|
154
|
+
address USER = makeAddr("user");
|
|
155
|
+
|
|
156
|
+
address private constant TRUSTED_FORWARDER = 0xB2b5841DBeF766d4b521221732F9B618fCf34A87;
|
|
157
|
+
|
|
158
|
+
function setUp() public override {
|
|
159
|
+
super.setUp();
|
|
160
|
+
|
|
161
|
+
FEE_PROJECT_ID = jbProjects().createFor(multisig());
|
|
162
|
+
|
|
163
|
+
SUCKER_REGISTRY = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
|
|
164
|
+
HOOK_STORE = new JB721TiersHookStore();
|
|
165
|
+
EXAMPLE_HOOK =
|
|
166
|
+
new JB721TiersHook(jbDirectory(), jbPermissions(), jbRulesets(), HOOK_STORE, jbSplits(), multisig());
|
|
167
|
+
ADDRESS_REGISTRY = new JBAddressRegistry();
|
|
168
|
+
HOOK_DEPLOYER = new JB721TiersHookDeployer(EXAMPLE_HOOK, HOOK_STORE, ADDRESS_REGISTRY, multisig());
|
|
169
|
+
PUBLISHER = new CTPublisher(jbDirectory(), jbPermissions(), FEE_PROJECT_ID, multisig());
|
|
170
|
+
MOCK_BUYBACK = new MockBuybackDataHook();
|
|
171
|
+
|
|
172
|
+
LOANS_CONTRACT = new REVLoans({
|
|
173
|
+
controller: jbController(),
|
|
174
|
+
projects: jbProjects(),
|
|
175
|
+
revId: FEE_PROJECT_ID,
|
|
176
|
+
owner: address(this),
|
|
177
|
+
permit2: permit2(),
|
|
178
|
+
trustedForwarder: TRUSTED_FORWARDER
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
REV_DEPLOYER = new REVDeployer{salt: REV_DEPLOYER_SALT}(
|
|
182
|
+
jbController(),
|
|
183
|
+
SUCKER_REGISTRY,
|
|
184
|
+
FEE_PROJECT_ID,
|
|
185
|
+
HOOK_DEPLOYER,
|
|
186
|
+
PUBLISHER,
|
|
187
|
+
IJBBuybackHookRegistry(address(MOCK_BUYBACK)),
|
|
188
|
+
address(LOANS_CONTRACT),
|
|
189
|
+
TRUSTED_FORWARDER
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
vm.prank(multisig());
|
|
193
|
+
jbProjects().approve(address(REV_DEPLOYER), FEE_PROJECT_ID);
|
|
194
|
+
|
|
195
|
+
// Deploy the fee revnet (project ID 1).
|
|
196
|
+
_deployFeeRevnet();
|
|
197
|
+
|
|
198
|
+
// Deploy a second revnet to borrow from.
|
|
199
|
+
_deployBorrowableRevnet();
|
|
200
|
+
|
|
201
|
+
// Give user ETH.
|
|
202
|
+
vm.deal(USER, 100e18);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function _deployFeeRevnet() internal {
|
|
206
|
+
JBAccountingContext[] memory accountingContextsToAccept = new JBAccountingContext[](1);
|
|
207
|
+
accountingContextsToAccept[0] = JBAccountingContext({
|
|
208
|
+
token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
JBTerminalConfig[] memory terminalConfigurations = new JBTerminalConfig[](1);
|
|
212
|
+
terminalConfigurations[0] =
|
|
213
|
+
JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: accountingContextsToAccept});
|
|
214
|
+
|
|
215
|
+
REVStageConfig[] memory stageConfigurations = new REVStageConfig[](1);
|
|
216
|
+
JBSplit[] memory splits = new JBSplit[](1);
|
|
217
|
+
splits[0].beneficiary = payable(multisig());
|
|
218
|
+
splits[0].percent = 10_000;
|
|
219
|
+
|
|
220
|
+
REVAutoIssuance[] memory issuanceConfs = new REVAutoIssuance[](1);
|
|
221
|
+
issuanceConfs[0] =
|
|
222
|
+
REVAutoIssuance({chainId: uint32(block.chainid), count: uint104(70_000e18), beneficiary: multisig()});
|
|
223
|
+
|
|
224
|
+
stageConfigurations[0] = REVStageConfig({
|
|
225
|
+
startsAtOrAfter: uint40(block.timestamp),
|
|
226
|
+
autoIssuances: issuanceConfs,
|
|
227
|
+
splitPercent: 2000,
|
|
228
|
+
splits: splits,
|
|
229
|
+
initialIssuance: uint112(1000e18),
|
|
230
|
+
issuanceCutFrequency: 90 days,
|
|
231
|
+
issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
|
|
232
|
+
cashOutTaxRate: 6000,
|
|
233
|
+
extraMetadata: 0
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
REVConfig memory revnetConfiguration = REVConfig({
|
|
237
|
+
description: REVDescription("Revnet", "$REV", "ipfs://test", ERC20_SALT),
|
|
238
|
+
baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
239
|
+
splitOperator: multisig(),
|
|
240
|
+
stageConfigurations: stageConfigurations
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
vm.prank(multisig());
|
|
244
|
+
REV_DEPLOYER.deployFor({
|
|
245
|
+
revnetId: FEE_PROJECT_ID,
|
|
246
|
+
configuration: revnetConfiguration,
|
|
247
|
+
terminalConfigurations: terminalConfigurations,
|
|
248
|
+
suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
|
|
249
|
+
deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256(abi.encodePacked("REV"))
|
|
250
|
+
})
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function _deployBorrowableRevnet() internal {
|
|
255
|
+
JBAccountingContext[] memory accountingContextsToAccept = new JBAccountingContext[](1);
|
|
256
|
+
accountingContextsToAccept[0] = JBAccountingContext({
|
|
257
|
+
token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
JBTerminalConfig[] memory terminalConfigurations = new JBTerminalConfig[](1);
|
|
261
|
+
terminalConfigurations[0] =
|
|
262
|
+
JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: accountingContextsToAccept});
|
|
263
|
+
|
|
264
|
+
REVStageConfig[] memory stageConfigurations = new REVStageConfig[](1);
|
|
265
|
+
JBSplit[] memory splits = new JBSplit[](1);
|
|
266
|
+
splits[0].beneficiary = payable(multisig());
|
|
267
|
+
splits[0].percent = 10_000;
|
|
268
|
+
|
|
269
|
+
stageConfigurations[0] = REVStageConfig({
|
|
270
|
+
startsAtOrAfter: uint40(block.timestamp),
|
|
271
|
+
autoIssuances: new REVAutoIssuance[](0),
|
|
272
|
+
splitPercent: 0,
|
|
273
|
+
splits: splits,
|
|
274
|
+
initialIssuance: uint112(1000e18),
|
|
275
|
+
issuanceCutFrequency: 0,
|
|
276
|
+
issuanceCutPercent: 0,
|
|
277
|
+
cashOutTaxRate: 5000,
|
|
278
|
+
extraMetadata: 0
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
REVConfig memory revnetConfiguration = REVConfig({
|
|
282
|
+
description: REVDescription("Borrowable", "BRW", "ipfs://brw", "BRW_TOKEN"),
|
|
283
|
+
baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
284
|
+
splitOperator: multisig(),
|
|
285
|
+
stageConfigurations: stageConfigurations
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
REVNET_ID = REV_DEPLOYER.deployFor({
|
|
289
|
+
revnetId: 0,
|
|
290
|
+
configuration: revnetConfiguration,
|
|
291
|
+
terminalConfigurations: terminalConfigurations,
|
|
292
|
+
suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
|
|
293
|
+
deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256(abi.encodePacked("BRW"))
|
|
294
|
+
})
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/// @notice Helper: pay into the revnet and get tokens.
|
|
299
|
+
function _payAndGetTokens(uint256 amount) internal returns (uint256 tokens) {
|
|
300
|
+
vm.prank(USER);
|
|
301
|
+
tokens = jbMultiTerminal().pay{value: amount}(REVNET_ID, JBConstants.NATIVE_TOKEN, amount, USER, 0, "", "");
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/// @notice Helper: mock permission for LOANS_CONTRACT to burn user tokens.
|
|
305
|
+
function _mockBurnPermission() internal {
|
|
306
|
+
mockExpect(
|
|
307
|
+
address(jbPermissions()),
|
|
308
|
+
abi.encodeCall(IJBPermissions.hasPermission, (address(LOANS_CONTRACT), USER, REVNET_ID, 11, true, true)),
|
|
309
|
+
abi.encode(true)
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/// @notice Helper: borrow against tokens.
|
|
314
|
+
function _borrow(uint256 tokens) internal returns (uint256 loanId, REVLoan memory loan, uint256 loanable) {
|
|
315
|
+
loanable = LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
|
|
316
|
+
|
|
317
|
+
_mockBurnPermission();
|
|
318
|
+
|
|
319
|
+
REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
|
|
320
|
+
|
|
321
|
+
vm.prank(USER);
|
|
322
|
+
(loanId, loan) = LOANS_CONTRACT.borrowFrom(REVNET_ID, source, loanable, tokens, payable(USER), 25);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
//*********************************************************************//
|
|
326
|
+
// --------- Unvalidated Loan Source Terminal ------------------- //
|
|
327
|
+
//*********************************************************************//
|
|
328
|
+
|
|
329
|
+
/// @notice borrowFrom rejects a fake terminal not registered in the directory.
|
|
330
|
+
function test_H1_borrowFromRejectsUnregisteredTerminal() public {
|
|
331
|
+
// Step 1: User pays into the revnet to get tokens.
|
|
332
|
+
uint256 tokens = _payAndGetTokens(1e18);
|
|
333
|
+
assertGt(tokens, 0, "user should receive tokens");
|
|
334
|
+
|
|
335
|
+
// Step 2: Create a fake terminal that returns garbage accounting contexts.
|
|
336
|
+
GarbageTerminal fakeTerminal = new GarbageTerminal();
|
|
337
|
+
|
|
338
|
+
// Step 3: Verify the fake terminal is NOT in the directory.
|
|
339
|
+
assertFalse(
|
|
340
|
+
jbDirectory().isTerminalOf(REVNET_ID, IJBTerminal(address(fakeTerminal))),
|
|
341
|
+
"fake terminal should NOT be registered"
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
// Step 4: Attempt to borrow using the fake terminal.
|
|
345
|
+
uint256 loanable =
|
|
346
|
+
LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
|
|
347
|
+
assertGt(loanable, 0, "should have borrowable amount");
|
|
348
|
+
|
|
349
|
+
// NOTE: Do NOT mock burn permission here. The call should revert
|
|
350
|
+
// at the terminal validation check before it ever reaches the burn step.
|
|
351
|
+
|
|
352
|
+
REVLoanSource memory fakeSource =
|
|
353
|
+
REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: IJBPayoutTerminal(address(fakeTerminal))});
|
|
354
|
+
|
|
355
|
+
// Step 5: Expect revert with the new REVLoans_InvalidTerminal error.
|
|
356
|
+
vm.expectRevert(
|
|
357
|
+
abi.encodeWithSelector(REVLoans.REVLoans_InvalidTerminal.selector, address(fakeTerminal), REVNET_ID)
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
vm.prank(USER);
|
|
361
|
+
LOANS_CONTRACT.borrowFrom(REVNET_ID, fakeSource, loanable, tokens, payable(USER), 25);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
//*********************************************************************//
|
|
365
|
+
// ------- RepayLoan Event Emits Zeroed Values ----------------- //
|
|
366
|
+
//*********************************************************************//
|
|
367
|
+
|
|
368
|
+
/// @notice RepayLoan event emits non-zero loan amount and collateral
|
|
369
|
+
/// when fully repaying a loan.
|
|
370
|
+
function test_L1_repayLoanEventEmitsNonZeroValues() public {
|
|
371
|
+
// Step 1: Pay in and borrow.
|
|
372
|
+
uint256 tokens = _payAndGetTokens(1e18);
|
|
373
|
+
(uint256 loanId, REVLoan memory loan, uint256 loanable) = _borrow(tokens);
|
|
374
|
+
|
|
375
|
+
assertGt(loan.amount, 0, "loan amount should be non-zero");
|
|
376
|
+
assertGt(loan.collateral, 0, "loan collateral should be non-zero");
|
|
377
|
+
|
|
378
|
+
// Step 2: Calculate the repay amount (loan amount + source fee).
|
|
379
|
+
uint256 sourceFee = LOANS_CONTRACT.determineSourceFeeAmount(loan, loan.amount);
|
|
380
|
+
uint256 totalRepay = loan.amount + sourceFee;
|
|
381
|
+
|
|
382
|
+
// Step 3: Give user enough ETH for repayment.
|
|
383
|
+
vm.deal(USER, totalRepay);
|
|
384
|
+
|
|
385
|
+
// Step 4: Expect the RepayLoan event with non-zero loan values.
|
|
386
|
+
// The `loan` field (4th param) should contain the original pre-repay loan data.
|
|
387
|
+
// We check that the event is emitted by looking for the indexed fields.
|
|
388
|
+
vm.expectEmit(true, true, true, false);
|
|
389
|
+
emit IREVLoans.RepayLoan({
|
|
390
|
+
loanId: loanId,
|
|
391
|
+
revnetId: REVNET_ID,
|
|
392
|
+
paidOffLoanId: loanId,
|
|
393
|
+
// These fields are the ones we care about -- they should be non-zero.
|
|
394
|
+
loan: loan,
|
|
395
|
+
paidOffLoan: loan, // placeholder, we only check the `loan` field
|
|
396
|
+
repayBorrowAmount: totalRepay,
|
|
397
|
+
sourceFeeAmount: sourceFee,
|
|
398
|
+
collateralCountToReturn: loan.collateral,
|
|
399
|
+
beneficiary: payable(USER),
|
|
400
|
+
caller: USER
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// Step 5: Fully repay the loan (return all collateral).
|
|
404
|
+
JBSingleAllowance memory allowance;
|
|
405
|
+
vm.prank(USER);
|
|
406
|
+
LOANS_CONTRACT.repayLoan{value: totalRepay}(loanId, totalRepay, loan.collateral, payable(USER), allowance);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/// @notice Secondary check: verify the original loan data in the emitted event
|
|
410
|
+
/// has the expected non-zero amount and collateral by recording logs.
|
|
411
|
+
function test_L1_repayLoanEventLoanFieldIsNonZero() public {
|
|
412
|
+
// Step 1: Pay in and borrow.
|
|
413
|
+
uint256 tokens = _payAndGetTokens(1e18);
|
|
414
|
+
(uint256 loanId, REVLoan memory loan,) = _borrow(tokens);
|
|
415
|
+
|
|
416
|
+
uint256 originalAmount = loan.amount;
|
|
417
|
+
uint256 originalCollateral = loan.collateral;
|
|
418
|
+
|
|
419
|
+
// Step 2: Calculate repay amount.
|
|
420
|
+
uint256 sourceFee = LOANS_CONTRACT.determineSourceFeeAmount(loan, loan.amount);
|
|
421
|
+
uint256 totalRepay = loan.amount + sourceFee;
|
|
422
|
+
vm.deal(USER, totalRepay);
|
|
423
|
+
|
|
424
|
+
// Step 3: Record logs to inspect the event data.
|
|
425
|
+
vm.recordLogs();
|
|
426
|
+
|
|
427
|
+
JBSingleAllowance memory allowance;
|
|
428
|
+
vm.prank(USER);
|
|
429
|
+
LOANS_CONTRACT.repayLoan{value: totalRepay}(loanId, totalRepay, loan.collateral, payable(USER), allowance);
|
|
430
|
+
|
|
431
|
+
// Step 4: Find the RepayLoan event and decode the loan struct.
|
|
432
|
+
Vm.Log[] memory entries = vm.getRecordedLogs();
|
|
433
|
+
bytes32 repayLoanSig = keccak256(
|
|
434
|
+
"RepayLoan(uint256,uint256,uint256,(uint112,uint112,uint48,uint16,uint32,(address,address)),(uint112,uint112,uint48,uint16,uint32,(address,address)),uint256,uint256,uint256,address,address)"
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
bool foundEvent = false;
|
|
438
|
+
for (uint256 i = 0; i < entries.length; i++) {
|
|
439
|
+
if (entries[i].topics[0] == repayLoanSig) {
|
|
440
|
+
foundEvent = true;
|
|
441
|
+
// Decode the non-indexed data.
|
|
442
|
+
// The data contains: loan, paidOffLoan, repayBorrowAmount, sourceFeeAmount,
|
|
443
|
+
// collateralCountToReturn, beneficiary, caller
|
|
444
|
+
(REVLoan memory emittedLoan,,,,,,) =
|
|
445
|
+
abi.decode(entries[i].data, (REVLoan, REVLoan, uint256, uint256, uint256, address, address));
|
|
446
|
+
|
|
447
|
+
// The emitted loan should have the ORIGINAL non-zero values.
|
|
448
|
+
assertEq(emittedLoan.amount, originalAmount, "emitted loan.amount should match original");
|
|
449
|
+
assertEq(emittedLoan.collateral, originalCollateral, "emitted loan.collateral should match original");
|
|
450
|
+
assertGt(emittedLoan.amount, 0, "emitted loan.amount must be non-zero");
|
|
451
|
+
assertGt(emittedLoan.collateral, 0, "emitted loan.collateral must be non-zero");
|
|
452
|
+
break;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
assertTrue(foundEvent, "RepayLoan event should have been emitted");
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
//*********************************************************************//
|
|
460
|
+
// --- Auto-Issuance Timing Guard Works Correctly ------------- //
|
|
461
|
+
//*********************************************************************//
|
|
462
|
+
|
|
463
|
+
/// @notice Proves that block.timestamp + i matches actual ruleset IDs,
|
|
464
|
+
/// and the timing guard in autoIssueFor correctly prevents premature issuance.
|
|
465
|
+
function test_autoIssueTimingGuardWorksCorrectly() public {
|
|
466
|
+
// Step 1: Deploy a revnet with 2 stages where stage 2 starts far in the future
|
|
467
|
+
// and has auto-issuance.
|
|
468
|
+
JBAccountingContext[] memory accountingContextsToAccept = new JBAccountingContext[](1);
|
|
469
|
+
accountingContextsToAccept[0] = JBAccountingContext({
|
|
470
|
+
token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
JBTerminalConfig[] memory terminalConfigurations = new JBTerminalConfig[](1);
|
|
474
|
+
terminalConfigurations[0] =
|
|
475
|
+
JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: accountingContextsToAccept});
|
|
476
|
+
|
|
477
|
+
REVStageConfig[] memory stages = new REVStageConfig[](2);
|
|
478
|
+
JBSplit[] memory splits = new JBSplit[](1);
|
|
479
|
+
splits[0].beneficiary = payable(multisig());
|
|
480
|
+
splits[0].percent = 10_000;
|
|
481
|
+
|
|
482
|
+
// Stage 1: starts now, no auto-issuance.
|
|
483
|
+
stages[0] = REVStageConfig({
|
|
484
|
+
startsAtOrAfter: uint40(block.timestamp),
|
|
485
|
+
autoIssuances: new REVAutoIssuance[](0),
|
|
486
|
+
splitPercent: 2000,
|
|
487
|
+
splits: splits,
|
|
488
|
+
initialIssuance: uint112(1000e18),
|
|
489
|
+
issuanceCutFrequency: 0,
|
|
490
|
+
issuanceCutPercent: 0,
|
|
491
|
+
cashOutTaxRate: 5000,
|
|
492
|
+
extraMetadata: 0
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
// Stage 2: starts 365 days in the future, HAS auto-issuance.
|
|
496
|
+
REVAutoIssuance[] memory stage2AutoIssuances = new REVAutoIssuance[](1);
|
|
497
|
+
stage2AutoIssuances[0] =
|
|
498
|
+
REVAutoIssuance({chainId: uint32(block.chainid), count: uint104(50_000e18), beneficiary: multisig()});
|
|
499
|
+
|
|
500
|
+
stages[1] = REVStageConfig({
|
|
501
|
+
startsAtOrAfter: uint40(block.timestamp + 365 days),
|
|
502
|
+
autoIssuances: stage2AutoIssuances,
|
|
503
|
+
splitPercent: 1000,
|
|
504
|
+
splits: splits,
|
|
505
|
+
initialIssuance: uint112(500e18),
|
|
506
|
+
issuanceCutFrequency: 0,
|
|
507
|
+
issuanceCutPercent: 0,
|
|
508
|
+
cashOutTaxRate: 3000,
|
|
509
|
+
extraMetadata: 0
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
REVConfig memory config = REVConfig({
|
|
513
|
+
description: REVDescription("FP1Test", "FP1", "ipfs://fp1", "FP1_TOKEN"),
|
|
514
|
+
baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
515
|
+
splitOperator: multisig(),
|
|
516
|
+
stageConfigurations: stages
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
// Record the deploy timestamp -- this is used for stage ID calculation.
|
|
520
|
+
uint256 deployTimestamp = block.timestamp;
|
|
521
|
+
|
|
522
|
+
uint256 fp1RevnetId = REV_DEPLOYER.deployFor({
|
|
523
|
+
revnetId: 0,
|
|
524
|
+
configuration: config,
|
|
525
|
+
terminalConfigurations: terminalConfigurations,
|
|
526
|
+
suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
|
|
527
|
+
deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256("FP1")
|
|
528
|
+
})
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
// Step 2: Verify the second ruleset ID matches deployTimestamp + 1.
|
|
532
|
+
// JBRulesets assigns IDs as: block.timestamp, block.timestamp+1, etc. when queued in one tx.
|
|
533
|
+
uint256 stage2RulesetId = deployTimestamp + 1;
|
|
534
|
+
|
|
535
|
+
(JBRuleset memory ruleset,) = jbController().getRulesetOf({projectId: fp1RevnetId, rulesetId: stage2RulesetId});
|
|
536
|
+
|
|
537
|
+
// The ruleset should exist and have the correct startsAtOrAfter.
|
|
538
|
+
assertGt(ruleset.id, 0, "stage 2 ruleset should exist");
|
|
539
|
+
assertEq(ruleset.start, deployTimestamp + 365 days, "stage 2 should start 365 days from deploy");
|
|
540
|
+
|
|
541
|
+
// Step 3: Verify amountToAutoIssue was stored with the correct stage ID.
|
|
542
|
+
uint256 storedAutoIssue = REV_DEPLOYER.amountToAutoIssue(fp1RevnetId, stage2RulesetId, multisig());
|
|
543
|
+
assertEq(storedAutoIssue, 50_000e18, "auto-issuance amount should be stored at deployTimestamp + 1");
|
|
544
|
+
|
|
545
|
+
// Step 4: Call autoIssueFor now -- it should revert because stage 2 hasn't started yet.
|
|
546
|
+
vm.expectRevert(abi.encodeWithSelector(REVDeployer.REVDeployer_StageNotStarted.selector, stage2RulesetId));
|
|
547
|
+
REV_DEPLOYER.autoIssueFor(fp1RevnetId, stage2RulesetId, multisig());
|
|
548
|
+
|
|
549
|
+
// Step 5: Warp to after stage 2 starts and verify auto-issuance works.
|
|
550
|
+
vm.warp(deployTimestamp + 365 days + 1);
|
|
551
|
+
|
|
552
|
+
REV_DEPLOYER.autoIssueFor(fp1RevnetId, stage2RulesetId, multisig());
|
|
553
|
+
|
|
554
|
+
// Verify the tokens were minted.
|
|
555
|
+
uint256 balance = jbController().TOKENS().totalBalanceOf({holder: multisig(), projectId: fp1RevnetId});
|
|
556
|
+
assertGe(balance, 50_000e18, "multisig should have received the auto-issued tokens");
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
//*********************************************************************//
|
|
560
|
+
// --- repayLoan Correctly Reverts On Excess Collateral ------- //
|
|
561
|
+
//*********************************************************************//
|
|
562
|
+
|
|
563
|
+
/// @notice When collateral value exceeds the loan amount (e.g. from price appreciation
|
|
564
|
+
/// or surplus growth), partial repayment correctly reverts because the remaining
|
|
565
|
+
/// collateral supports more than the loan amount. reallocateCollateralFromLoan
|
|
566
|
+
/// is the correct alternative.
|
|
567
|
+
function test_repayLoanCorrectlyRevertsOnExcessCollateral() public {
|
|
568
|
+
// Step 1: User pays in and borrows.
|
|
569
|
+
uint256 tokens = _payAndGetTokens(1e18);
|
|
570
|
+
(uint256 loanId, REVLoan memory loan,) = _borrow(tokens);
|
|
571
|
+
|
|
572
|
+
uint256 loanAmount = loan.amount;
|
|
573
|
+
uint256 loanCollateral = loan.collateral;
|
|
574
|
+
|
|
575
|
+
assertGt(loanAmount, 0, "loan should have non-zero amount");
|
|
576
|
+
assertGt(loanCollateral, 0, "loan should have non-zero collateral");
|
|
577
|
+
|
|
578
|
+
// Step 2: Simulate surplus growth by adding balance directly (no token minting).
|
|
579
|
+
// Using addToBalanceOf increases surplus without increasing supply, so each token
|
|
580
|
+
// is now backed by more surplus and the collateral value exceeds the loan amount.
|
|
581
|
+
{
|
|
582
|
+
address whale = makeAddr("whale");
|
|
583
|
+
vm.deal(whale, 50e18);
|
|
584
|
+
vm.prank(whale);
|
|
585
|
+
jbMultiTerminal().addToBalanceOf{value: 50e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 50e18, false, "", "");
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Step 3: Verify the collateral value has increased.
|
|
589
|
+
{
|
|
590
|
+
uint256 newBorrowable = LOANS_CONTRACT.borrowableAmountFrom(
|
|
591
|
+
REVNET_ID, loanCollateral, 18, uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
592
|
+
);
|
|
593
|
+
assertGt(
|
|
594
|
+
newBorrowable, loanAmount, "collateral value should exceed original loan amount after surplus growth"
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Step 4: Try to repay returning only SOME collateral such that remaining collateral
|
|
599
|
+
// supports more than the loan amount. This should revert with
|
|
600
|
+
// REVLoans_NewBorrowAmountGreaterThanLoanAmount.
|
|
601
|
+
{
|
|
602
|
+
uint256 sourceFee = LOANS_CONTRACT.determineSourceFeeAmount(loan, loanAmount);
|
|
603
|
+
uint256 totalRepay = loanAmount + sourceFee;
|
|
604
|
+
vm.deal(USER, totalRepay);
|
|
605
|
+
|
|
606
|
+
JBSingleAllowance memory allowance;
|
|
607
|
+
|
|
608
|
+
vm.prank(USER);
|
|
609
|
+
vm.expectRevert(); // REVLoans_NewBorrowAmountGreaterThanLoanAmount
|
|
610
|
+
LOANS_CONTRACT.repayLoan{value: totalRepay}(loanId, totalRepay, 1, payable(USER), allowance);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Step 5: Show that reallocateCollateralFromLoan is the correct alternative.
|
|
614
|
+
// The user can reallocate excess collateral to a new loan instead.
|
|
615
|
+
_mockBurnPermission();
|
|
616
|
+
|
|
617
|
+
uint256 collateralToTransfer = loanCollateral / 10;
|
|
618
|
+
|
|
619
|
+
uint256 minBorrow = LOANS_CONTRACT.borrowableAmountFrom(
|
|
620
|
+
REVNET_ID, collateralToTransfer, 18, uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
621
|
+
);
|
|
622
|
+
|
|
623
|
+
REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
|
|
624
|
+
|
|
625
|
+
vm.prank(USER);
|
|
626
|
+
(,, REVLoan memory reallocatedLoan, REVLoan memory newLoan) = LOANS_CONTRACT.reallocateCollateralFromLoan({
|
|
627
|
+
loanId: loanId,
|
|
628
|
+
collateralCountToTransfer: collateralToTransfer,
|
|
629
|
+
source: source,
|
|
630
|
+
minBorrowAmount: minBorrow,
|
|
631
|
+
collateralCountToAdd: 0,
|
|
632
|
+
beneficiary: payable(USER),
|
|
633
|
+
prepaidFeePercent: 25
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
// Verify the reallocation succeeded.
|
|
637
|
+
assertEq(
|
|
638
|
+
reallocatedLoan.collateral,
|
|
639
|
+
loanCollateral - collateralToTransfer,
|
|
640
|
+
"reallocated loan should have reduced collateral"
|
|
641
|
+
);
|
|
642
|
+
assertGt(newLoan.collateral, 0, "new loan should have collateral from transfer");
|
|
643
|
+
}
|
|
644
|
+
}
|
|
@@ -34,7 +34,7 @@ import {JBTokenAmount} from "@bananapus/core-v6/src/structs/JBTokenAmount.sol";
|
|
|
34
34
|
/// When JBBuybackHook determines minting is cheaper than swapping, it returns an empty
|
|
35
35
|
/// hookSpecifications array. Before the fix, REVDeployer.beforePayRecordedWith would
|
|
36
36
|
/// Panic(0x32) (array out-of-bounds) when accessing buybackHookSpecifications[0].
|
|
37
|
-
contract TestEmptyBuybackSpecs is TestBaseWorkflow
|
|
37
|
+
contract TestEmptyBuybackSpecs is TestBaseWorkflow {
|
|
38
38
|
bytes32 REV_DEPLOYER_SALT = "REVDeployer";
|
|
39
39
|
|
|
40
40
|
REVDeployer REV_DEPLOYER;
|
|
@@ -57,7 +57,8 @@ contract TestEmptyBuybackSpecs is TestBaseWorkflow, JBTest {
|
|
|
57
57
|
FEE_PROJECT_ID = jbProjects().createFor(multisig());
|
|
58
58
|
SUCKER_REGISTRY = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
|
|
59
59
|
HOOK_STORE = new JB721TiersHookStore();
|
|
60
|
-
EXAMPLE_HOOK =
|
|
60
|
+
EXAMPLE_HOOK =
|
|
61
|
+
new JB721TiersHook(jbDirectory(), jbPermissions(), jbRulesets(), HOOK_STORE, jbSplits(), multisig());
|
|
61
62
|
ADDRESS_REGISTRY = new JBAddressRegistry();
|
|
62
63
|
HOOK_DEPLOYER = new JB721TiersHookDeployer(EXAMPLE_HOOK, HOOK_STORE, ADDRESS_REGISTRY, multisig());
|
|
63
64
|
PUBLISHER = new CTPublisher(jbDirectory(), jbPermissions(), FEE_PROJECT_ID, multisig());
|
|
@@ -76,7 +77,7 @@ contract TestEmptyBuybackSpecs is TestBaseWorkflow, JBTest {
|
|
|
76
77
|
FEE_PROJECT_ID,
|
|
77
78
|
HOOK_DEPLOYER,
|
|
78
79
|
PUBLISHER,
|
|
79
|
-
|
|
80
|
+
IJBBuybackHookRegistry(address(MOCK_BUYBACK_MINT_PATH)),
|
|
80
81
|
address(LOANS_CONTRACT),
|
|
81
82
|
TRUSTED_FORWARDER
|
|
82
83
|
);
|