@rev-net/core-v6 0.0.4 → 0.0.6
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/README.md +149 -29
- package/SKILLS.md +189 -101
- package/package.json +1 -1
- package/src/REVDeployer.sol +9 -4
- package/src/REVLoans.sol +5 -1
- package/test/REVAutoIssuanceFuzz.t.sol +5 -5
- package/test/REVDeployerAuditRegressions.t.sol +20 -20
- package/test/REVInvincibility.t.sol +48 -48
- package/test/REVLoans.invariants.t.sol +1 -1
- package/test/REVLoansAttacks.t.sol +11 -11
- package/test/REVLoansAuditRegressions.t.sol +12 -12
- package/test/REVLoansSourced.t.sol +1 -1
- package/test/TestEmptyBuybackSpecs.t.sol +237 -0
- package/test/TestPR12_FlashLoanSurplus.t.sol +1 -1
- package/test/TestPR16_ZeroRepayment.t.sol +1 -1
- package/test/TestPR21_Uint112Overflow.t.sol +1 -1
- package/test/TestPR22_HookArrayOOB.t.sol +1 -1
- package/test/TestPR27_CEIPattern.t.sol +2 -2
- package/test/TestStageTransitionBorrowable.t.sol +241 -0
- package/test/helpers/MaliciousContracts.sol +2 -2
- package/test/mock/MockBuybackDataHookMintPath.sol +61 -0
- package/REVNET_SECURITY_CHECKLIST.md +0 -164
|
@@ -0,0 +1,237 @@
|
|
|
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 */ "./../src/REVDeployer.sol";
|
|
7
|
+
import "@croptop/core-v6/src/CTPublisher.sol";
|
|
8
|
+
import {MockBuybackDataHookMintPath} from "./mock/MockBuybackDataHookMintPath.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 {REVLoans} from "../src/REVLoans.sol";
|
|
18
|
+
import {REVStageConfig, REVAutoIssuance} from "../src/structs/REVStageConfig.sol";
|
|
19
|
+
import {REVLoanSource} from "../src/structs/REVLoanSource.sol";
|
|
20
|
+
import {REVDescription} from "../src/structs/REVDescription.sol";
|
|
21
|
+
import {IREVLoans} from "./../src/interfaces/IREVLoans.sol";
|
|
22
|
+
import {JBSuckerDeployerConfig} from "@bananapus/suckers-v6/src/structs/JBSuckerDeployerConfig.sol";
|
|
23
|
+
import {JBSuckerRegistry} from "@bananapus/suckers-v6/src/JBSuckerRegistry.sol";
|
|
24
|
+
import {JB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
|
|
25
|
+
import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
|
|
26
|
+
import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
|
|
27
|
+
import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
|
|
28
|
+
import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
|
|
29
|
+
import {JBBeforePayRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforePayRecordedContext.sol";
|
|
30
|
+
import {JBPayHookSpecification} from "@bananapus/core-v6/src/structs/JBPayHookSpecification.sol";
|
|
31
|
+
import {JBTokenAmount} from "@bananapus/core-v6/src/structs/JBTokenAmount.sol";
|
|
32
|
+
|
|
33
|
+
/// @notice Regression tests for the empty buyback hook specifications fix.
|
|
34
|
+
/// When JBBuybackHook determines minting is cheaper than swapping, it returns an empty
|
|
35
|
+
/// hookSpecifications array. Before the fix, REVDeployer.beforePayRecordedWith would
|
|
36
|
+
/// Panic(0x32) (array out-of-bounds) when accessing buybackHookSpecifications[0].
|
|
37
|
+
contract TestEmptyBuybackSpecs is TestBaseWorkflow, JBTest {
|
|
38
|
+
bytes32 REV_DEPLOYER_SALT = "REVDeployer";
|
|
39
|
+
|
|
40
|
+
REVDeployer REV_DEPLOYER;
|
|
41
|
+
JB721TiersHook EXAMPLE_HOOK;
|
|
42
|
+
IJB721TiersHookDeployer HOOK_DEPLOYER;
|
|
43
|
+
IJB721TiersHookStore HOOK_STORE;
|
|
44
|
+
IJBAddressRegistry ADDRESS_REGISTRY;
|
|
45
|
+
IREVLoans LOANS_CONTRACT;
|
|
46
|
+
IJBSuckerRegistry SUCKER_REGISTRY;
|
|
47
|
+
CTPublisher PUBLISHER;
|
|
48
|
+
MockBuybackDataHookMintPath MOCK_BUYBACK_MINT_PATH;
|
|
49
|
+
|
|
50
|
+
uint256 FEE_PROJECT_ID;
|
|
51
|
+
|
|
52
|
+
address private constant TRUSTED_FORWARDER = 0xB2b5841DBeF766d4b521221732F9B618fCf34A87;
|
|
53
|
+
address USER = makeAddr("user");
|
|
54
|
+
|
|
55
|
+
function setUp() public override {
|
|
56
|
+
super.setUp();
|
|
57
|
+
FEE_PROJECT_ID = jbProjects().createFor(multisig());
|
|
58
|
+
SUCKER_REGISTRY = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
|
|
59
|
+
HOOK_STORE = new JB721TiersHookStore();
|
|
60
|
+
EXAMPLE_HOOK = new JB721TiersHook(jbDirectory(), jbPermissions(), jbRulesets(), HOOK_STORE, multisig());
|
|
61
|
+
ADDRESS_REGISTRY = new JBAddressRegistry();
|
|
62
|
+
HOOK_DEPLOYER = new JB721TiersHookDeployer(EXAMPLE_HOOK, HOOK_STORE, ADDRESS_REGISTRY, multisig());
|
|
63
|
+
PUBLISHER = new CTPublisher(jbDirectory(), jbPermissions(), FEE_PROJECT_ID, multisig());
|
|
64
|
+
MOCK_BUYBACK_MINT_PATH = new MockBuybackDataHookMintPath();
|
|
65
|
+
LOANS_CONTRACT = new REVLoans({
|
|
66
|
+
controller: jbController(),
|
|
67
|
+
projects: jbProjects(),
|
|
68
|
+
revId: FEE_PROJECT_ID,
|
|
69
|
+
owner: address(this),
|
|
70
|
+
permit2: permit2(),
|
|
71
|
+
trustedForwarder: TRUSTED_FORWARDER
|
|
72
|
+
});
|
|
73
|
+
REV_DEPLOYER = new REVDeployer{salt: REV_DEPLOYER_SALT}(
|
|
74
|
+
jbController(),
|
|
75
|
+
SUCKER_REGISTRY,
|
|
76
|
+
FEE_PROJECT_ID,
|
|
77
|
+
HOOK_DEPLOYER,
|
|
78
|
+
PUBLISHER,
|
|
79
|
+
IJBRulesetDataHook(address(MOCK_BUYBACK_MINT_PATH)),
|
|
80
|
+
address(LOANS_CONTRACT),
|
|
81
|
+
TRUSTED_FORWARDER
|
|
82
|
+
);
|
|
83
|
+
vm.prank(multisig());
|
|
84
|
+
jbProjects().approve(address(REV_DEPLOYER), FEE_PROJECT_ID);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function _buildMinimalConfig()
|
|
88
|
+
internal
|
|
89
|
+
view
|
|
90
|
+
returns (REVConfig memory cfg, JBTerminalConfig[] memory tc, REVSuckerDeploymentConfig memory sdc)
|
|
91
|
+
{
|
|
92
|
+
JBAccountingContext[] memory acc = new JBAccountingContext[](1);
|
|
93
|
+
acc[0] = JBAccountingContext({
|
|
94
|
+
token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
95
|
+
});
|
|
96
|
+
tc = new JBTerminalConfig[](1);
|
|
97
|
+
tc[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: acc});
|
|
98
|
+
|
|
99
|
+
REVStageConfig[] memory stages = new REVStageConfig[](1);
|
|
100
|
+
JBSplit[] memory splits = new JBSplit[](1);
|
|
101
|
+
splits[0].beneficiary = payable(multisig());
|
|
102
|
+
splits[0].percent = 10_000;
|
|
103
|
+
stages[0] = REVStageConfig({
|
|
104
|
+
startsAtOrAfter: uint40(block.timestamp),
|
|
105
|
+
autoIssuances: new REVAutoIssuance[](0),
|
|
106
|
+
splitPercent: 0,
|
|
107
|
+
splits: splits,
|
|
108
|
+
initialIssuance: uint112(1000e18),
|
|
109
|
+
issuanceCutFrequency: 0,
|
|
110
|
+
issuanceCutPercent: 0,
|
|
111
|
+
cashOutTaxRate: 5000,
|
|
112
|
+
extraMetadata: 0
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
cfg = REVConfig({
|
|
116
|
+
description: REVDescription("Test", "TST", "ipfs://test", "TEST_SALT"),
|
|
117
|
+
baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
118
|
+
splitOperator: multisig(),
|
|
119
|
+
stageConfigurations: stages
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
sdc = REVSuckerDeploymentConfig({
|
|
123
|
+
deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256(abi.encodePacked("TEST"))
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function _deployFeeAndRevnet() internal returns (uint256 revnetId) {
|
|
128
|
+
(REVConfig memory feeCfg, JBTerminalConfig[] memory feeTc, REVSuckerDeploymentConfig memory feeSdc) =
|
|
129
|
+
_buildMinimalConfig();
|
|
130
|
+
|
|
131
|
+
vm.prank(multisig());
|
|
132
|
+
REV_DEPLOYER.deployFor({
|
|
133
|
+
revnetId: FEE_PROJECT_ID,
|
|
134
|
+
configuration: feeCfg,
|
|
135
|
+
terminalConfigurations: feeTc,
|
|
136
|
+
suckerDeploymentConfiguration: feeSdc
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
(REVConfig memory cfg, JBTerminalConfig[] memory tc, REVSuckerDeploymentConfig memory sdc) =
|
|
140
|
+
_buildMinimalConfig();
|
|
141
|
+
cfg.description = REVDescription("Test2", "TS2", "ipfs://test2", "TEST_SALT_2");
|
|
142
|
+
|
|
143
|
+
revnetId = REV_DEPLOYER.deployFor({
|
|
144
|
+
revnetId: 0, configuration: cfg, terminalConfigurations: tc, suckerDeploymentConfiguration: sdc
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/// @notice REGRESSION: Payment to revnet must succeed when buyback hook returns empty specs (mint path).
|
|
149
|
+
/// Before the fix, this would Panic(0x32) due to accessing buybackHookSpecifications[0] on an empty array.
|
|
150
|
+
function test_payRevnet_emptyBuybackSpecs_succeeds() public {
|
|
151
|
+
uint256 revnetId = _deployFeeAndRevnet();
|
|
152
|
+
|
|
153
|
+
vm.deal(USER, 1 ether);
|
|
154
|
+
vm.prank(USER);
|
|
155
|
+
jbMultiTerminal().pay{value: 1 ether}({
|
|
156
|
+
projectId: revnetId,
|
|
157
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
158
|
+
amount: 1 ether,
|
|
159
|
+
beneficiary: USER,
|
|
160
|
+
minReturnedTokens: 0,
|
|
161
|
+
memo: "payment with mint path buyback",
|
|
162
|
+
metadata: ""
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
uint256 balance = jbTokens().totalBalanceOf(USER, revnetId);
|
|
166
|
+
assertGt(balance, 0, "Should have received tokens when buyback hook takes mint path");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/// @notice Payment with various amounts should work when buyback hook returns empty specs.
|
|
170
|
+
function test_payRevnet_emptyBuybackSpecs_variousAmounts(uint96 amount) public {
|
|
171
|
+
vm.assume(amount > 0.001 ether && amount < 100 ether);
|
|
172
|
+
uint256 revnetId = _deployFeeAndRevnet();
|
|
173
|
+
|
|
174
|
+
vm.deal(USER, amount);
|
|
175
|
+
vm.prank(USER);
|
|
176
|
+
jbMultiTerminal().pay{value: amount}({
|
|
177
|
+
projectId: revnetId,
|
|
178
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
179
|
+
amount: amount,
|
|
180
|
+
beneficiary: USER,
|
|
181
|
+
minReturnedTokens: 0,
|
|
182
|
+
memo: "",
|
|
183
|
+
metadata: ""
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
uint256 balance = jbTokens().totalBalanceOf(USER, revnetId);
|
|
187
|
+
assertGt(balance, 0, "Should have received tokens for any valid amount");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/// @notice Multiple sequential payments should work with empty buyback specs.
|
|
191
|
+
function test_payRevnet_emptyBuybackSpecs_multiplePayments() public {
|
|
192
|
+
uint256 revnetId = _deployFeeAndRevnet();
|
|
193
|
+
|
|
194
|
+
for (uint256 i; i < 5; i++) {
|
|
195
|
+
address payer = makeAddr(string(abi.encodePacked("payer", i)));
|
|
196
|
+
vm.deal(payer, 1 ether);
|
|
197
|
+
vm.prank(payer);
|
|
198
|
+
jbMultiTerminal().pay{value: 1 ether}({
|
|
199
|
+
projectId: revnetId,
|
|
200
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
201
|
+
amount: 1 ether,
|
|
202
|
+
beneficiary: payer,
|
|
203
|
+
minReturnedTokens: 0,
|
|
204
|
+
memo: "",
|
|
205
|
+
metadata: ""
|
|
206
|
+
});
|
|
207
|
+
assertGt(jbTokens().totalBalanceOf(payer, revnetId), 0, "Each payer should receive tokens");
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/// @notice Verify beforePayRecordedWith returns empty hookSpecifications when buyback returns empty.
|
|
212
|
+
function test_beforePayRecordedWith_emptyBuybackSpecs_returnsEmptyArray() public {
|
|
213
|
+
uint256 revnetId = _deployFeeAndRevnet();
|
|
214
|
+
|
|
215
|
+
JBBeforePayRecordedContext memory context = JBBeforePayRecordedContext({
|
|
216
|
+
terminal: address(jbMultiTerminal()),
|
|
217
|
+
payer: USER,
|
|
218
|
+
amount: JBTokenAmount({
|
|
219
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
220
|
+
value: 1 ether,
|
|
221
|
+
decimals: 18,
|
|
222
|
+
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
223
|
+
}),
|
|
224
|
+
projectId: revnetId,
|
|
225
|
+
rulesetId: 0,
|
|
226
|
+
beneficiary: USER,
|
|
227
|
+
weight: 1000e18,
|
|
228
|
+
reservedPercent: 0,
|
|
229
|
+
metadata: ""
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
(uint256 weight, JBPayHookSpecification[] memory specs) = REV_DEPLOYER.beforePayRecordedWith(context);
|
|
233
|
+
|
|
234
|
+
assertEq(weight, context.weight, "Weight should pass through from buyback hook");
|
|
235
|
+
assertEq(specs.length, 0, "Should return empty specs when buyback hook returns empty and no 721 hook");
|
|
236
|
+
}
|
|
237
|
+
}
|
|
@@ -30,7 +30,7 @@ import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStor
|
|
|
30
30
|
import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
|
|
31
31
|
import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
|
|
32
32
|
|
|
33
|
-
/// @notice Tests
|
|
33
|
+
/// @notice Tests showing that flash loan surplus manipulation is economically unprofitable.
|
|
34
34
|
contract TestPR12_FlashLoanSurplus is TestBaseWorkflow, JBTest {
|
|
35
35
|
bytes32 REV_DEPLOYER_SALT = "REVDeployer";
|
|
36
36
|
bytes32 ERC20_SALT = "REV_TOKEN";
|
|
@@ -227,7 +227,7 @@ contract TestPR16_ZeroRepayment is TestBaseWorkflow, JBTest {
|
|
|
227
227
|
|
|
228
228
|
// Try to repay with collateralCountToReturn = 0 and some maxRepayBorrowAmount.
|
|
229
229
|
// Since surplus was inflated, newBorrowAmount > loan.amount, which reverts with
|
|
230
|
-
// REVLoans_NewBorrowAmountGreaterThanLoanAmount. This
|
|
230
|
+
// REVLoans_NewBorrowAmountGreaterThanLoanAmount. This shows zero-repayment is blocked.
|
|
231
231
|
vm.prank(USER);
|
|
232
232
|
vm.expectRevert(); // Will revert with either NothingToRepay or NewBorrowAmountGreaterThanLoanAmount
|
|
233
233
|
LOANS_CONTRACT.repayLoan{value: 0}(
|
|
@@ -32,7 +32,7 @@ import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressReg
|
|
|
32
32
|
import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
|
|
33
33
|
|
|
34
34
|
/// @title TestPR21_Uint112Overflow
|
|
35
|
-
/// @notice Tests for
|
|
35
|
+
/// @notice Tests for uint112 truncation fix in REVLoans._adjust()
|
|
36
36
|
contract TestPR21_Uint112Overflow is TestBaseWorkflow, JBTest {
|
|
37
37
|
bytes32 REV_DEPLOYER_SALT = "REVDeployer";
|
|
38
38
|
bytes32 ERC20_SALT = "REV_TOKEN";
|
|
@@ -32,7 +32,7 @@ import {JBTokenAmount} from "@bananapus/core-v6/src/structs/JBTokenAmount.sol";
|
|
|
32
32
|
import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
|
|
33
33
|
|
|
34
34
|
/// @notice Tests for PR #22: fix/c2-hook-array-oob
|
|
35
|
-
/// Verifies that the fix for the
|
|
35
|
+
/// Verifies that the fix for the hook array out-of-bounds bug works correctly.
|
|
36
36
|
/// The bug: `hookSpecifications[1] = buybackHookSpecifications[0]` would revert with OOB
|
|
37
37
|
/// when there is no tiered 721 hook (array size is 1, not 2).
|
|
38
38
|
/// The fix: `hookSpecifications[usesTiered721Hook ? 1 : 0] = buybackHookSpecifications[0]`.
|
|
@@ -60,9 +60,9 @@ contract ReentrantBorrower {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
/// @title TestPR27_CEIPattern
|
|
63
|
-
/// @notice Tests for
|
|
63
|
+
/// @notice Tests for CEI pattern fix in REVLoans._adjust()
|
|
64
64
|
///
|
|
65
|
-
///
|
|
65
|
+
/// Source context (_addTo/_removeFrom/_addCollateralTo/_returnCollateralFrom):
|
|
66
66
|
/// - _addTo(REVLoan memory, ..., uint256 addedBorrowAmount, ...) — memory copy, uses delta param
|
|
67
67
|
/// - _removeFrom(REVLoan memory, ..., uint256 repaidBorrowAmount) — memory copy, uses delta param
|
|
68
68
|
/// - _addCollateralTo(uint256 revnetId, uint256 amount) — no loan reference at all
|
|
@@ -0,0 +1,241 @@
|
|
|
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 */ "./../src/REVDeployer.sol";
|
|
7
|
+
import "@croptop/core-v6/src/CTPublisher.sol";
|
|
8
|
+
import {MockBuybackDataHook} from "./mock/MockBuybackDataHook.sol";
|
|
9
|
+
import "@bananapus/core-v6/script/helpers/CoreDeploymentLib.sol";
|
|
10
|
+
import "@bananapus/721-hook-v6/script/helpers/Hook721DeploymentLib.sol";
|
|
11
|
+
import "@bananapus/suckers-v6/script/helpers/SuckerDeploymentLib.sol";
|
|
12
|
+
import "@croptop/core-v6/script/helpers/CroptopDeploymentLib.sol";
|
|
13
|
+
import "@bananapus/router-terminal-v6/script/helpers/RouterTerminalDeploymentLib.sol";
|
|
14
|
+
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
15
|
+
import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
|
|
16
|
+
import {REVLoans} from "../src/REVLoans.sol";
|
|
17
|
+
import {REVStageConfig, REVAutoIssuance} from "../src/structs/REVStageConfig.sol";
|
|
18
|
+
import {REVLoanSource} from "../src/structs/REVLoanSource.sol";
|
|
19
|
+
import {REVDescription} from "../src/structs/REVDescription.sol";
|
|
20
|
+
import {IREVLoans} from "./../src/interfaces/IREVLoans.sol";
|
|
21
|
+
import {JBSuckerDeployerConfig} from "@bananapus/suckers-v6/src/structs/JBSuckerDeployerConfig.sol";
|
|
22
|
+
import {JBSuckerRegistry} from "@bananapus/suckers-v6/src/JBSuckerRegistry.sol";
|
|
23
|
+
import {JB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
|
|
24
|
+
import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
|
|
25
|
+
import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
|
|
26
|
+
import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
|
|
27
|
+
import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
|
|
28
|
+
|
|
29
|
+
/// @notice Documents and verifies that stage transitions change the borrowable amount for the same collateral.
|
|
30
|
+
/// This is by design: loan value tracks the current bonding curve parameters (cashOutTaxRate),
|
|
31
|
+
/// just as cash-out value does.
|
|
32
|
+
contract TestStageTransitionBorrowable is TestBaseWorkflow, JBTest {
|
|
33
|
+
bytes32 REV_DEPLOYER_SALT = "REVDeployer";
|
|
34
|
+
|
|
35
|
+
REVDeployer REV_DEPLOYER;
|
|
36
|
+
JB721TiersHook EXAMPLE_HOOK;
|
|
37
|
+
IJB721TiersHookDeployer HOOK_DEPLOYER;
|
|
38
|
+
IJB721TiersHookStore HOOK_STORE;
|
|
39
|
+
IJBAddressRegistry ADDRESS_REGISTRY;
|
|
40
|
+
IREVLoans LOANS_CONTRACT;
|
|
41
|
+
IJBSuckerRegistry SUCKER_REGISTRY;
|
|
42
|
+
CTPublisher PUBLISHER;
|
|
43
|
+
MockBuybackDataHook MOCK_BUYBACK;
|
|
44
|
+
|
|
45
|
+
uint256 FEE_PROJECT_ID;
|
|
46
|
+
uint256 REVNET_ID;
|
|
47
|
+
|
|
48
|
+
address USER = makeAddr("user");
|
|
49
|
+
|
|
50
|
+
address private constant TRUSTED_FORWARDER = 0xB2b5841DBeF766d4b521221732F9B618fCf34A87;
|
|
51
|
+
|
|
52
|
+
/// @notice Stage 1 starts now with 60% cashOutTaxRate, stage 2 starts after 30 days with 20% cashOutTaxRate.
|
|
53
|
+
function _buildConfig()
|
|
54
|
+
internal
|
|
55
|
+
view
|
|
56
|
+
returns (REVConfig memory cfg, JBTerminalConfig[] memory tc, REVSuckerDeploymentConfig memory sdc)
|
|
57
|
+
{
|
|
58
|
+
JBAccountingContext[] memory acc = new JBAccountingContext[](1);
|
|
59
|
+
acc[0] = JBAccountingContext({
|
|
60
|
+
token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
61
|
+
});
|
|
62
|
+
tc = new JBTerminalConfig[](1);
|
|
63
|
+
tc[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: acc});
|
|
64
|
+
|
|
65
|
+
REVStageConfig[] memory stages = new REVStageConfig[](2);
|
|
66
|
+
JBSplit[] memory splits = new JBSplit[](1);
|
|
67
|
+
splits[0].beneficiary = payable(multisig());
|
|
68
|
+
splits[0].percent = 10_000;
|
|
69
|
+
|
|
70
|
+
// Stage 1: high cashOutTaxRate (60%)
|
|
71
|
+
stages[0] = REVStageConfig({
|
|
72
|
+
startsAtOrAfter: uint40(block.timestamp),
|
|
73
|
+
autoIssuances: new REVAutoIssuance[](0),
|
|
74
|
+
splitPercent: 0,
|
|
75
|
+
splits: splits,
|
|
76
|
+
initialIssuance: uint112(1000e18),
|
|
77
|
+
issuanceCutFrequency: 0,
|
|
78
|
+
issuanceCutPercent: 0,
|
|
79
|
+
cashOutTaxRate: 6000, // 60%
|
|
80
|
+
extraMetadata: 0
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Stage 2: low cashOutTaxRate (20%) — starts after 30 days
|
|
84
|
+
stages[1] = REVStageConfig({
|
|
85
|
+
startsAtOrAfter: uint40(block.timestamp + 30 days),
|
|
86
|
+
autoIssuances: new REVAutoIssuance[](0),
|
|
87
|
+
splitPercent: 0,
|
|
88
|
+
splits: splits,
|
|
89
|
+
initialIssuance: uint112(1000e18),
|
|
90
|
+
issuanceCutFrequency: 0,
|
|
91
|
+
issuanceCutPercent: 0,
|
|
92
|
+
cashOutTaxRate: 2000, // 20%
|
|
93
|
+
extraMetadata: 0
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
cfg = REVConfig({
|
|
97
|
+
description: REVDescription("StageTest", "STG", "ipfs://test", "STG_SALT"),
|
|
98
|
+
baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
99
|
+
splitOperator: multisig(),
|
|
100
|
+
stageConfigurations: stages
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
sdc = REVSuckerDeploymentConfig({
|
|
104
|
+
deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256(abi.encodePacked("STG"))
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function setUp() public override {
|
|
109
|
+
super.setUp();
|
|
110
|
+
FEE_PROJECT_ID = jbProjects().createFor(multisig());
|
|
111
|
+
SUCKER_REGISTRY = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
|
|
112
|
+
HOOK_STORE = new JB721TiersHookStore();
|
|
113
|
+
EXAMPLE_HOOK = new JB721TiersHook(jbDirectory(), jbPermissions(), jbRulesets(), HOOK_STORE, multisig());
|
|
114
|
+
ADDRESS_REGISTRY = new JBAddressRegistry();
|
|
115
|
+
HOOK_DEPLOYER = new JB721TiersHookDeployer(EXAMPLE_HOOK, HOOK_STORE, ADDRESS_REGISTRY, multisig());
|
|
116
|
+
PUBLISHER = new CTPublisher(jbDirectory(), jbPermissions(), FEE_PROJECT_ID, multisig());
|
|
117
|
+
MOCK_BUYBACK = new MockBuybackDataHook();
|
|
118
|
+
LOANS_CONTRACT = new REVLoans({
|
|
119
|
+
controller: jbController(),
|
|
120
|
+
projects: jbProjects(),
|
|
121
|
+
revId: FEE_PROJECT_ID,
|
|
122
|
+
owner: address(this),
|
|
123
|
+
permit2: permit2(),
|
|
124
|
+
trustedForwarder: TRUSTED_FORWARDER
|
|
125
|
+
});
|
|
126
|
+
REV_DEPLOYER = new REVDeployer{salt: REV_DEPLOYER_SALT}(
|
|
127
|
+
jbController(),
|
|
128
|
+
SUCKER_REGISTRY,
|
|
129
|
+
FEE_PROJECT_ID,
|
|
130
|
+
HOOK_DEPLOYER,
|
|
131
|
+
PUBLISHER,
|
|
132
|
+
IJBRulesetDataHook(address(MOCK_BUYBACK)),
|
|
133
|
+
address(LOANS_CONTRACT),
|
|
134
|
+
TRUSTED_FORWARDER
|
|
135
|
+
);
|
|
136
|
+
vm.prank(multisig());
|
|
137
|
+
jbProjects().approve(address(REV_DEPLOYER), FEE_PROJECT_ID);
|
|
138
|
+
|
|
139
|
+
// Deploy the fee project first.
|
|
140
|
+
(REVConfig memory feeCfg, JBTerminalConfig[] memory feeTc, REVSuckerDeploymentConfig memory feeSdc) =
|
|
141
|
+
_buildConfig();
|
|
142
|
+
vm.prank(multisig());
|
|
143
|
+
REV_DEPLOYER.deployFor({
|
|
144
|
+
revnetId: FEE_PROJECT_ID,
|
|
145
|
+
configuration: feeCfg,
|
|
146
|
+
terminalConfigurations: feeTc,
|
|
147
|
+
suckerDeploymentConfiguration: feeSdc
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Deploy the test revnet.
|
|
151
|
+
(REVConfig memory cfg, JBTerminalConfig[] memory tc, REVSuckerDeploymentConfig memory sdc) = _buildConfig();
|
|
152
|
+
cfg.description = REVDescription("StageTest2", "ST2", "ipfs://test2", "STG_SALT_2");
|
|
153
|
+
REVNET_ID = REV_DEPLOYER.deployFor({
|
|
154
|
+
revnetId: 0, configuration: cfg, terminalConfigurations: tc, suckerDeploymentConfiguration: sdc
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
vm.deal(USER, 100 ether);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/// @notice BY DESIGN: Borrowable amount increases when transitioning to a stage with lower cashOutTaxRate.
|
|
161
|
+
/// This documents that loan value tracks the current bonding curve, just as cash-out value does.
|
|
162
|
+
/// @dev The bonding curve only applies a tax discount when cashOutCount < totalSupply,
|
|
163
|
+
/// so we need multiple payers to see the effect.
|
|
164
|
+
function test_borrowableAmount_increasesWhenCashOutTaxRateDecreases() public {
|
|
165
|
+
// Two payers so the bonding curve tax rate has a visible effect (count < supply).
|
|
166
|
+
address otherPayer = makeAddr("otherPayer");
|
|
167
|
+
vm.deal(otherPayer, 10 ether);
|
|
168
|
+
vm.prank(otherPayer);
|
|
169
|
+
jbMultiTerminal().pay{value: 10 ether}({
|
|
170
|
+
projectId: REVNET_ID,
|
|
171
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
172
|
+
amount: 10 ether,
|
|
173
|
+
beneficiary: otherPayer,
|
|
174
|
+
minReturnedTokens: 0,
|
|
175
|
+
memo: "",
|
|
176
|
+
metadata: ""
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
vm.prank(USER);
|
|
180
|
+
uint256 tokens = jbMultiTerminal().pay{value: 10 ether}({
|
|
181
|
+
projectId: REVNET_ID,
|
|
182
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
183
|
+
amount: 10 ether,
|
|
184
|
+
beneficiary: USER,
|
|
185
|
+
minReturnedTokens: 0,
|
|
186
|
+
memo: "",
|
|
187
|
+
metadata: ""
|
|
188
|
+
});
|
|
189
|
+
assertGt(tokens, 0, "Should receive tokens");
|
|
190
|
+
|
|
191
|
+
// Check borrowable amount during stage 1 (60% cashOutTaxRate).
|
|
192
|
+
uint256 borrowableStage1 =
|
|
193
|
+
LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
|
|
194
|
+
assertGt(borrowableStage1, 0, "Borrowable amount should be positive in stage 1");
|
|
195
|
+
|
|
196
|
+
// Warp to stage 2 (20% cashOutTaxRate).
|
|
197
|
+
vm.warp(block.timestamp + 31 days);
|
|
198
|
+
|
|
199
|
+
// Check borrowable amount during stage 2.
|
|
200
|
+
uint256 borrowableStage2 =
|
|
201
|
+
LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
|
|
202
|
+
|
|
203
|
+
// Borrowable amount should be HIGHER with a lower cashOutTaxRate — by design.
|
|
204
|
+
assertGt(borrowableStage2, borrowableStage1, "Borrowable amount should increase with lower cashOutTaxRate");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/// @notice Verifies that the bonding curve formula applies the tax rate correctly when count < supply.
|
|
208
|
+
function test_borrowableAmount_taxRateReducesPartialCashOut() public {
|
|
209
|
+
// Two payers so USER holds a fraction of total supply.
|
|
210
|
+
address otherPayer = makeAddr("otherPayer");
|
|
211
|
+
vm.deal(otherPayer, 10 ether);
|
|
212
|
+
vm.prank(otherPayer);
|
|
213
|
+
jbMultiTerminal().pay{value: 10 ether}({
|
|
214
|
+
projectId: REVNET_ID,
|
|
215
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
216
|
+
amount: 10 ether,
|
|
217
|
+
beneficiary: otherPayer,
|
|
218
|
+
minReturnedTokens: 0,
|
|
219
|
+
memo: "",
|
|
220
|
+
metadata: ""
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
vm.prank(USER);
|
|
224
|
+
uint256 tokens = jbMultiTerminal().pay{value: 10 ether}({
|
|
225
|
+
projectId: REVNET_ID,
|
|
226
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
227
|
+
amount: 10 ether,
|
|
228
|
+
beneficiary: USER,
|
|
229
|
+
minReturnedTokens: 0,
|
|
230
|
+
memo: "",
|
|
231
|
+
metadata: ""
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// With 60% tax rate and ~50% of supply, borrowable should be meaningfully less than pro-rata share.
|
|
235
|
+
uint256 borrowable =
|
|
236
|
+
LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
|
|
237
|
+
assertGt(borrowable, 0, "Borrowable amount should be positive");
|
|
238
|
+
// Pro-rata share would be ~10 ether (half of 20 ether surplus). With 60% tax, it should be less.
|
|
239
|
+
assertLt(borrowable, 10 ether, "Borrowable should be less than pro-rata share due to tax rate");
|
|
240
|
+
}
|
|
241
|
+
}
|
|
@@ -11,7 +11,7 @@ import {IREVLoans} from "../../src/interfaces/IREVLoans.sol";
|
|
|
11
11
|
import {REVLoanSource} from "../../src/structs/REVLoanSource.sol";
|
|
12
12
|
|
|
13
13
|
/// @notice A terminal that reverts on both pay() and addToBalanceOf().
|
|
14
|
-
/// @dev
|
|
14
|
+
/// @dev If the fee terminal breaks, cash-outs brick because
|
|
15
15
|
/// afterCashOutRecordedWith's fallback addToBalanceOf also reverts.
|
|
16
16
|
contract BrokenFeeTerminal is ERC165, IJBPayoutTerminal {
|
|
17
17
|
bool public payReverts = true;
|
|
@@ -119,7 +119,7 @@ contract BrokenFeeTerminal is ERC165, IJBPayoutTerminal {
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
/// @notice A terminal that attempts to addToBalance + borrow in a single tx.
|
|
122
|
-
/// @dev
|
|
122
|
+
/// @dev Flash loan surplus inflation via live surplus read.
|
|
123
123
|
contract SurplusInflator is ERC165, IJBPayoutTerminal {
|
|
124
124
|
IREVLoans public loans;
|
|
125
125
|
uint256 public revnetId;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.26;
|
|
3
|
+
|
|
4
|
+
import {IJBRulesetDataHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetDataHook.sol";
|
|
5
|
+
import {IJBPayHook} from "@bananapus/core-v6/src/interfaces/IJBPayHook.sol";
|
|
6
|
+
import {JBBeforePayRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforePayRecordedContext.sol";
|
|
7
|
+
import {JBBeforeCashOutRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforeCashOutRecordedContext.sol";
|
|
8
|
+
import {JBPayHookSpecification} from "@bananapus/core-v6/src/structs/JBPayHookSpecification.sol";
|
|
9
|
+
import {JBCashOutHookSpecification} from "@bananapus/core-v6/src/structs/JBCashOutHookSpecification.sol";
|
|
10
|
+
import {JBAfterPayRecordedContext} from "@bananapus/core-v6/src/structs/JBAfterPayRecordedContext.sol";
|
|
11
|
+
import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
|
|
12
|
+
import {IUniswapV3Pool} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
|
|
13
|
+
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
|
|
14
|
+
|
|
15
|
+
/// @notice Mock buyback hook that simulates the "mint path" — returns EMPTY hookSpecifications.
|
|
16
|
+
/// This is what the real JBBuybackHook does when direct minting is cheaper than swapping
|
|
17
|
+
/// (i.e., tokenCountWithoutHook >= minimumSwapAmountOut).
|
|
18
|
+
contract MockBuybackDataHookMintPath is IJBRulesetDataHook, IJBPayHook {
|
|
19
|
+
function beforePayRecordedWith(JBBeforePayRecordedContext calldata context)
|
|
20
|
+
external
|
|
21
|
+
view
|
|
22
|
+
override
|
|
23
|
+
returns (uint256 weight, JBPayHookSpecification[] memory hookSpecifications)
|
|
24
|
+
{
|
|
25
|
+
weight = context.weight;
|
|
26
|
+
// Return EMPTY hookSpecifications — simulating the mint path where no swap is needed.
|
|
27
|
+
hookSpecifications = new JBPayHookSpecification[](0);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function beforeCashOutRecordedWith(JBBeforeCashOutRecordedContext calldata context)
|
|
31
|
+
external
|
|
32
|
+
view
|
|
33
|
+
override
|
|
34
|
+
returns (
|
|
35
|
+
uint256 cashOutTaxRate,
|
|
36
|
+
uint256 cashOutCount,
|
|
37
|
+
uint256 totalSupply,
|
|
38
|
+
JBCashOutHookSpecification[] memory hookSpecifications
|
|
39
|
+
)
|
|
40
|
+
{
|
|
41
|
+
cashOutTaxRate = context.cashOutTaxRate;
|
|
42
|
+
cashOutCount = context.cashOutCount;
|
|
43
|
+
totalSupply = context.totalSupply;
|
|
44
|
+
hookSpecifications = new JBCashOutHookSpecification[](0);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function hasMintPermissionFor(uint256, JBRuleset calldata, address) external pure override returns (bool) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function afterPayRecordedWith(JBAfterPayRecordedContext calldata) external payable override {}
|
|
52
|
+
|
|
53
|
+
function setPoolFor(uint256, uint24, uint256, address) external pure returns (IUniswapV3Pool) {
|
|
54
|
+
return IUniswapV3Pool(address(0));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function supportsInterface(bytes4 interfaceId) external pure override returns (bool) {
|
|
58
|
+
return interfaceId == type(IJBRulesetDataHook).interfaceId || interfaceId == type(IJBPayHook).interfaceId
|
|
59
|
+
|| interfaceId == type(IERC165).interfaceId;
|
|
60
|
+
}
|
|
61
|
+
}
|