@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,448 @@
|
|
|
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
|
+
/// @notice Contract that reenters REVLoans when it receives ETH during a borrow payout.
|
|
35
|
+
/// Records the loan state it observes during reentrancy to verify CEI correctness.
|
|
36
|
+
contract ReentrantBorrower {
|
|
37
|
+
IREVLoans public loans;
|
|
38
|
+
uint256 public targetLoanId;
|
|
39
|
+
uint256 public observedAmount;
|
|
40
|
+
uint256 public observedCollateral;
|
|
41
|
+
bool public reentered;
|
|
42
|
+
|
|
43
|
+
constructor(IREVLoans _loans) {
|
|
44
|
+
loans = _loans;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function setTarget(uint256 _loanId) external {
|
|
48
|
+
targetLoanId = _loanId;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
receive() external payable {
|
|
52
|
+
if (!reentered) {
|
|
53
|
+
reentered = true;
|
|
54
|
+
// During ETH receipt, read loan state. With CEI, state should already be finalized.
|
|
55
|
+
REVLoan memory loan = loans.loanOf(targetLoanId);
|
|
56
|
+
observedAmount = loan.amount;
|
|
57
|
+
observedCollateral = loan.collateral;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// @title TestPR27_CEIPattern
|
|
63
|
+
/// @notice Tests for PR #27 — C-3 CEI pattern fix in REVLoans._adjust()
|
|
64
|
+
///
|
|
65
|
+
/// SOURCE VERIFICATION (confirmed by reading _addTo/_removeFrom/_addCollateralTo/_returnCollateralFrom):
|
|
66
|
+
/// - _addTo(REVLoan memory, ..., uint256 addedBorrowAmount, ...) — memory copy, uses delta param
|
|
67
|
+
/// - _removeFrom(REVLoan memory, ..., uint256 repaidBorrowAmount) — memory copy, uses delta param
|
|
68
|
+
/// - _addCollateralTo(uint256 revnetId, uint256 amount) — no loan reference at all
|
|
69
|
+
/// - _returnCollateralFrom(uint256 revnetId, uint256 collateralCount, ...) — no loan reference
|
|
70
|
+
/// None of the four helpers read loan.amount or loan.collateral — they all use pre-computed deltas.
|
|
71
|
+
/// The CEI fix writes loan.amount and loan.collateral BEFORE calling any of these helpers.
|
|
72
|
+
contract TestPR27_CEIPattern is TestBaseWorkflow, JBTest {
|
|
73
|
+
bytes32 REV_DEPLOYER_SALT = "REVDeployer";
|
|
74
|
+
|
|
75
|
+
REVDeployer REV_DEPLOYER;
|
|
76
|
+
JB721TiersHook EXAMPLE_HOOK;
|
|
77
|
+
IJB721TiersHookDeployer HOOK_DEPLOYER;
|
|
78
|
+
IJB721TiersHookStore HOOK_STORE;
|
|
79
|
+
IJBAddressRegistry ADDRESS_REGISTRY;
|
|
80
|
+
IREVLoans LOANS_CONTRACT;
|
|
81
|
+
MockERC20 TOKEN;
|
|
82
|
+
IJBSuckerRegistry SUCKER_REGISTRY;
|
|
83
|
+
CTPublisher PUBLISHER;
|
|
84
|
+
MockBuybackDataHook MOCK_BUYBACK;
|
|
85
|
+
|
|
86
|
+
uint256 FEE_PROJECT_ID;
|
|
87
|
+
uint256 REVNET_ID;
|
|
88
|
+
|
|
89
|
+
address USER = makeAddr("user");
|
|
90
|
+
address private constant TRUSTED_FORWARDER = 0xB2b5841DBeF766d4b521221732F9B618fCf34A87;
|
|
91
|
+
|
|
92
|
+
function setUp() public override {
|
|
93
|
+
super.setUp();
|
|
94
|
+
FEE_PROJECT_ID = jbProjects().createFor(multisig());
|
|
95
|
+
SUCKER_REGISTRY = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
|
|
96
|
+
HOOK_STORE = new JB721TiersHookStore();
|
|
97
|
+
EXAMPLE_HOOK = new JB721TiersHook(jbDirectory(), jbPermissions(), jbRulesets(), HOOK_STORE, multisig());
|
|
98
|
+
ADDRESS_REGISTRY = new JBAddressRegistry();
|
|
99
|
+
HOOK_DEPLOYER = new JB721TiersHookDeployer(EXAMPLE_HOOK, HOOK_STORE, ADDRESS_REGISTRY, multisig());
|
|
100
|
+
PUBLISHER = new CTPublisher(jbDirectory(), jbPermissions(), FEE_PROJECT_ID, multisig());
|
|
101
|
+
MOCK_BUYBACK = new MockBuybackDataHook();
|
|
102
|
+
TOKEN = new MockERC20("1/2 ETH", "1/2");
|
|
103
|
+
MockPriceFeed priceFeed = new MockPriceFeed(1e21, 6);
|
|
104
|
+
vm.prank(multisig());
|
|
105
|
+
jbPrices()
|
|
106
|
+
.addPriceFeedFor(0, uint32(uint160(address(TOKEN))), uint32(uint160(JBConstants.NATIVE_TOKEN)), priceFeed);
|
|
107
|
+
LOANS_CONTRACT = new REVLoans({
|
|
108
|
+
controller: jbController(),
|
|
109
|
+
projects: jbProjects(),
|
|
110
|
+
revId: FEE_PROJECT_ID,
|
|
111
|
+
owner: address(this),
|
|
112
|
+
permit2: permit2(),
|
|
113
|
+
trustedForwarder: TRUSTED_FORWARDER
|
|
114
|
+
});
|
|
115
|
+
REV_DEPLOYER = new REVDeployer{salt: REV_DEPLOYER_SALT}(
|
|
116
|
+
jbController(),
|
|
117
|
+
SUCKER_REGISTRY,
|
|
118
|
+
FEE_PROJECT_ID,
|
|
119
|
+
HOOK_DEPLOYER,
|
|
120
|
+
PUBLISHER,
|
|
121
|
+
IJBRulesetDataHook(address(MOCK_BUYBACK)),
|
|
122
|
+
address(LOANS_CONTRACT),
|
|
123
|
+
TRUSTED_FORWARDER
|
|
124
|
+
);
|
|
125
|
+
vm.prank(multisig());
|
|
126
|
+
jbProjects().approve(address(REV_DEPLOYER), FEE_PROJECT_ID);
|
|
127
|
+
_deployFeeProject();
|
|
128
|
+
_deployRevnet();
|
|
129
|
+
vm.deal(USER, 1000e18);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function _deployFeeProject() internal {
|
|
133
|
+
JBAccountingContext[] memory acc = new JBAccountingContext[](2);
|
|
134
|
+
acc[0] = JBAccountingContext({
|
|
135
|
+
token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
136
|
+
});
|
|
137
|
+
acc[1] = JBAccountingContext({token: address(TOKEN), decimals: 6, currency: uint32(uint160(address(TOKEN)))});
|
|
138
|
+
JBTerminalConfig[] memory tc = new JBTerminalConfig[](1);
|
|
139
|
+
tc[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: acc});
|
|
140
|
+
REVStageConfig[] memory stages = new REVStageConfig[](1);
|
|
141
|
+
JBSplit[] memory splits = new JBSplit[](1);
|
|
142
|
+
splits[0].beneficiary = payable(multisig());
|
|
143
|
+
splits[0].percent = 10_000;
|
|
144
|
+
REVAutoIssuance[] memory ai = new REVAutoIssuance[](1);
|
|
145
|
+
ai[0] = REVAutoIssuance({chainId: uint32(block.chainid), count: uint104(70_000e18), beneficiary: multisig()});
|
|
146
|
+
stages[0] = REVStageConfig({
|
|
147
|
+
startsAtOrAfter: uint40(block.timestamp),
|
|
148
|
+
autoIssuances: ai,
|
|
149
|
+
splitPercent: 2000,
|
|
150
|
+
splits: splits,
|
|
151
|
+
initialIssuance: uint112(1000e18),
|
|
152
|
+
issuanceCutFrequency: 90 days,
|
|
153
|
+
issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
|
|
154
|
+
cashOutTaxRate: 6000,
|
|
155
|
+
extraMetadata: 0
|
|
156
|
+
});
|
|
157
|
+
REVConfig memory cfg = REVConfig({
|
|
158
|
+
description: REVDescription("Revnet", "$REV", "ipfs://test", "REV_TOKEN"),
|
|
159
|
+
baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
160
|
+
splitOperator: multisig(),
|
|
161
|
+
stageConfigurations: stages
|
|
162
|
+
});
|
|
163
|
+
vm.prank(multisig());
|
|
164
|
+
REV_DEPLOYER.deployFor({
|
|
165
|
+
revnetId: FEE_PROJECT_ID,
|
|
166
|
+
configuration: cfg,
|
|
167
|
+
terminalConfigurations: tc,
|
|
168
|
+
suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
|
|
169
|
+
deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256("FEE")
|
|
170
|
+
})
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function _deployRevnet() internal {
|
|
175
|
+
JBAccountingContext[] memory acc = new JBAccountingContext[](2);
|
|
176
|
+
acc[0] = JBAccountingContext({
|
|
177
|
+
token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
178
|
+
});
|
|
179
|
+
acc[1] = JBAccountingContext({token: address(TOKEN), decimals: 6, currency: uint32(uint160(address(TOKEN)))});
|
|
180
|
+
JBTerminalConfig[] memory tc = new JBTerminalConfig[](1);
|
|
181
|
+
tc[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: acc});
|
|
182
|
+
REVStageConfig[] memory stages = new REVStageConfig[](1);
|
|
183
|
+
JBSplit[] memory splits = new JBSplit[](1);
|
|
184
|
+
splits[0].beneficiary = payable(multisig());
|
|
185
|
+
splits[0].percent = 10_000;
|
|
186
|
+
REVAutoIssuance[] memory ai = new REVAutoIssuance[](1);
|
|
187
|
+
ai[0] = REVAutoIssuance({chainId: uint32(block.chainid), count: uint104(70_000e18), beneficiary: multisig()});
|
|
188
|
+
stages[0] = REVStageConfig({
|
|
189
|
+
startsAtOrAfter: uint40(block.timestamp),
|
|
190
|
+
autoIssuances: ai,
|
|
191
|
+
splitPercent: 2000,
|
|
192
|
+
splits: splits,
|
|
193
|
+
initialIssuance: uint112(1000e18),
|
|
194
|
+
issuanceCutFrequency: 90 days,
|
|
195
|
+
issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
|
|
196
|
+
cashOutTaxRate: 6000,
|
|
197
|
+
extraMetadata: 0
|
|
198
|
+
});
|
|
199
|
+
REVLoanSource[] memory ls = new REVLoanSource[](1);
|
|
200
|
+
ls[0] = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
|
|
201
|
+
REVConfig memory cfg = REVConfig({
|
|
202
|
+
description: REVDescription("NANA", "$NANA", "ipfs://test2", "NANA_TOKEN"),
|
|
203
|
+
baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
204
|
+
splitOperator: multisig(),
|
|
205
|
+
stageConfigurations: stages
|
|
206
|
+
});
|
|
207
|
+
REVNET_ID = REV_DEPLOYER.deployFor({
|
|
208
|
+
revnetId: 0,
|
|
209
|
+
configuration: cfg,
|
|
210
|
+
terminalConfigurations: tc,
|
|
211
|
+
suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
|
|
212
|
+
deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256("NANA")
|
|
213
|
+
})
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function _setupLoan(
|
|
218
|
+
address user,
|
|
219
|
+
uint256 ethAmount,
|
|
220
|
+
uint256 prepaidFee
|
|
221
|
+
)
|
|
222
|
+
internal
|
|
223
|
+
returns (uint256 loanId, uint256 tokenCount, uint256 borrowAmount)
|
|
224
|
+
{
|
|
225
|
+
vm.prank(user);
|
|
226
|
+
tokenCount =
|
|
227
|
+
jbMultiTerminal().pay{value: ethAmount}(REVNET_ID, JBConstants.NATIVE_TOKEN, ethAmount, user, 0, "", "");
|
|
228
|
+
borrowAmount =
|
|
229
|
+
LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokenCount, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
|
|
230
|
+
if (borrowAmount == 0) return (0, tokenCount, 0);
|
|
231
|
+
mockExpect(
|
|
232
|
+
address(jbPermissions()),
|
|
233
|
+
abi.encodeCall(IJBPermissions.hasPermission, (address(LOANS_CONTRACT), user, REVNET_ID, 11, true, true)),
|
|
234
|
+
abi.encode(true)
|
|
235
|
+
);
|
|
236
|
+
REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
|
|
237
|
+
vm.prank(user);
|
|
238
|
+
(loanId,) = LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokenCount, payable(user), prepaidFee);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/// @notice After borrowing, loan.amount and loan.collateral are set correctly (CEI: state written before external
|
|
242
|
+
/// calls).
|
|
243
|
+
function test_normalBorrow_stateConsistent() public {
|
|
244
|
+
(uint256 loanId,, uint256 borrowAmount) = _setupLoan(USER, 10e18, 25);
|
|
245
|
+
assertTrue(borrowAmount > 0, "Should borrow nonzero");
|
|
246
|
+
|
|
247
|
+
REVLoan memory loan = LOANS_CONTRACT.loanOf(loanId);
|
|
248
|
+
// The amount should reflect the actual borrow minus any fee
|
|
249
|
+
assertTrue(loan.amount > 0, "Loan amount should be positive");
|
|
250
|
+
assertTrue(loan.collateral > 0, "Loan collateral should be positive");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/// @notice Repay a loan and verify state is consistent afterwards.
|
|
254
|
+
function test_repayLoan_stateConsistent() public {
|
|
255
|
+
(uint256 loanId, uint256 tokenCount, uint256 borrowAmount) = _setupLoan(USER, 10e18, 500);
|
|
256
|
+
assertTrue(borrowAmount > 0, "Should borrow nonzero");
|
|
257
|
+
|
|
258
|
+
REVLoan memory loan = LOANS_CONTRACT.loanOf(loanId);
|
|
259
|
+
|
|
260
|
+
// Immediately repay — within prepaid duration so no source fee
|
|
261
|
+
vm.prank(USER);
|
|
262
|
+
LOANS_CONTRACT.repayLoan{value: loan.amount}({
|
|
263
|
+
loanId: loanId,
|
|
264
|
+
maxRepayBorrowAmount: loan.amount,
|
|
265
|
+
collateralCountToReturn: loan.collateral,
|
|
266
|
+
beneficiary: payable(USER),
|
|
267
|
+
allowance: JBSingleAllowance({sigDeadline: 0, amount: 0, expiration: 0, nonce: 0, signature: ""})
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// After repayment, total collateral should be 0
|
|
271
|
+
uint256 totalCollateral = LOANS_CONTRACT.totalCollateralOf(REVNET_ID);
|
|
272
|
+
assertEq(totalCollateral, 0, "All collateral should be returned after full repay");
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/// @notice Multiple sequential borrows produce correct aggregate state.
|
|
276
|
+
function test_multipleBorrows_stateAccumulates() public {
|
|
277
|
+
vm.deal(USER, 2000e18);
|
|
278
|
+
|
|
279
|
+
// First borrow
|
|
280
|
+
(uint256 loanId1,, uint256 borrow1) = _setupLoan(USER, 10e18, 25);
|
|
281
|
+
assertTrue(borrow1 > 0, "First borrow should succeed");
|
|
282
|
+
|
|
283
|
+
REVLoan memory loan1 = LOANS_CONTRACT.loanOf(loanId1);
|
|
284
|
+
uint256 collateral1 = loan1.collateral;
|
|
285
|
+
|
|
286
|
+
// Second borrow (need more tokens)
|
|
287
|
+
vm.prank(USER);
|
|
288
|
+
uint256 tokens2 =
|
|
289
|
+
jbMultiTerminal().pay{value: 10e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 10e18, USER, 0, "", "");
|
|
290
|
+
uint256 borrowable2 =
|
|
291
|
+
LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokens2, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
|
|
292
|
+
if (borrowable2 > 0) {
|
|
293
|
+
mockExpect(
|
|
294
|
+
address(jbPermissions()),
|
|
295
|
+
abi.encodeCall(
|
|
296
|
+
IJBPermissions.hasPermission, (address(LOANS_CONTRACT), USER, REVNET_ID, 11, true, true)
|
|
297
|
+
),
|
|
298
|
+
abi.encode(true)
|
|
299
|
+
);
|
|
300
|
+
REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
|
|
301
|
+
vm.prank(USER);
|
|
302
|
+
LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokens2, payable(USER), 25);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Total collateral should equal sum of both loans' collateral
|
|
306
|
+
uint256 totalCollateral = LOANS_CONTRACT.totalCollateralOf(REVNET_ID);
|
|
307
|
+
assertTrue(totalCollateral >= collateral1, "Total collateral should include both loans");
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/// @notice A reentrant beneficiary reads loan state during ETH receipt.
|
|
311
|
+
/// With CEI, the loan state is already finalized when external calls execute.
|
|
312
|
+
function test_reentrantBeneficiary_seesUpdatedState() public {
|
|
313
|
+
ReentrantBorrower attacker = new ReentrantBorrower(LOANS_CONTRACT);
|
|
314
|
+
vm.deal(address(attacker), 100e18);
|
|
315
|
+
|
|
316
|
+
// Pay into revnet as the attacker contract to get tokens.
|
|
317
|
+
vm.prank(address(attacker));
|
|
318
|
+
uint256 tokens = jbMultiTerminal().pay{value: 10e18}(
|
|
319
|
+
REVNET_ID, JBConstants.NATIVE_TOKEN, 10e18, address(attacker), 0, "", ""
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
uint256 borrowable =
|
|
323
|
+
LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
|
|
324
|
+
vm.assume(borrowable > 0);
|
|
325
|
+
|
|
326
|
+
// Mock BURN permission for attacker.
|
|
327
|
+
mockExpect(
|
|
328
|
+
address(jbPermissions()),
|
|
329
|
+
abi.encodeCall(
|
|
330
|
+
IJBPermissions.hasPermission, (address(LOANS_CONTRACT), address(attacker), REVNET_ID, 11, true, true)
|
|
331
|
+
),
|
|
332
|
+
abi.encode(true)
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
|
|
336
|
+
|
|
337
|
+
// Pre-compute the loanId so the attacker can read it during reentrancy.
|
|
338
|
+
// loanId = revnetId * 1_000_000_000_000 + (numberOfLoansFor + 1)
|
|
339
|
+
uint256 expectedLoanId = REVNET_ID * 1_000_000_000_000 + (LOANS_CONTRACT.numberOfLoansFor(REVNET_ID) + 1);
|
|
340
|
+
attacker.setTarget(expectedLoanId);
|
|
341
|
+
|
|
342
|
+
// Borrow with attacker as beneficiary — attacker's receive() will fire when ETH arrives.
|
|
343
|
+
vm.prank(address(attacker));
|
|
344
|
+
(uint256 loanId,) = LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokens, payable(address(attacker)), 25);
|
|
345
|
+
|
|
346
|
+
assertEq(loanId, expectedLoanId, "LoanId should match pre-computed value");
|
|
347
|
+
|
|
348
|
+
// Verify loan state is finalized.
|
|
349
|
+
REVLoan memory loan = LOANS_CONTRACT.loanOf(loanId);
|
|
350
|
+
assertGt(loan.amount, 0, "Loan should have amount");
|
|
351
|
+
assertGt(loan.collateral, 0, "Loan should have collateral");
|
|
352
|
+
|
|
353
|
+
// The attacker's receive() fired during the ETH transfer. With CEI, it should have
|
|
354
|
+
// observed the correct (finalized) loan state.
|
|
355
|
+
if (attacker.reentered()) {
|
|
356
|
+
assertEq(attacker.observedAmount(), loan.amount, "Reentrant read should see finalized loan amount");
|
|
357
|
+
assertEq(
|
|
358
|
+
attacker.observedCollateral(), loan.collateral, "Reentrant read should see finalized loan collateral"
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/// @notice Verify atomic consistency: loan state matches global accounting after every operation.
|
|
364
|
+
/// If _adjust wrote state AFTER external calls (old code), a reentrant observer between
|
|
365
|
+
/// the external calls and the state write could see totalBorrowedFrom updated but loan.amount stale.
|
|
366
|
+
function test_CEI_atomicConsistency_borrowAndRepay() public {
|
|
367
|
+
vm.deal(USER, 2000e18);
|
|
368
|
+
|
|
369
|
+
// Borrow.
|
|
370
|
+
(uint256 loanId,, uint256 borrowAmount) = _setupLoan(USER, 10e18, 25);
|
|
371
|
+
assertTrue(borrowAmount > 0, "Should borrow nonzero");
|
|
372
|
+
|
|
373
|
+
REVLoan memory loan = LOANS_CONTRACT.loanOf(loanId);
|
|
374
|
+
|
|
375
|
+
// Verify loan.amount matches what totalBorrowedFrom tracks.
|
|
376
|
+
uint256 totalBorrowed = LOANS_CONTRACT.totalBorrowedFrom(REVNET_ID, jbMultiTerminal(), JBConstants.NATIVE_TOKEN);
|
|
377
|
+
assertEq(totalBorrowed, loan.amount, "totalBorrowedFrom should equal loan.amount after single borrow");
|
|
378
|
+
|
|
379
|
+
// Verify collateral accounting.
|
|
380
|
+
uint256 totalCollateral = LOANS_CONTRACT.totalCollateralOf(REVNET_ID);
|
|
381
|
+
assertEq(totalCollateral, loan.collateral, "totalCollateralOf should equal loan.collateral after single borrow");
|
|
382
|
+
|
|
383
|
+
// Repay fully.
|
|
384
|
+
vm.prank(USER);
|
|
385
|
+
LOANS_CONTRACT.repayLoan{value: loan.amount * 2}({
|
|
386
|
+
loanId: loanId,
|
|
387
|
+
maxRepayBorrowAmount: loan.amount * 2,
|
|
388
|
+
collateralCountToReturn: loan.collateral,
|
|
389
|
+
beneficiary: payable(USER),
|
|
390
|
+
allowance: JBSingleAllowance({sigDeadline: 0, amount: 0, expiration: 0, nonce: 0, signature: ""})
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
// After full repay, both should be zero atomically.
|
|
394
|
+
uint256 totalBorrowedAfter =
|
|
395
|
+
LOANS_CONTRACT.totalBorrowedFrom(REVNET_ID, jbMultiTerminal(), JBConstants.NATIVE_TOKEN);
|
|
396
|
+
uint256 totalCollateralAfter = LOANS_CONTRACT.totalCollateralOf(REVNET_ID);
|
|
397
|
+
assertEq(totalBorrowedAfter, 0, "totalBorrowedFrom should be 0 after full repay");
|
|
398
|
+
assertEq(totalCollateralAfter, 0, "totalCollateralOf should be 0 after full repay");
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/// @notice Rapid sequential borrows and repays can't create inconsistent state.
|
|
402
|
+
/// Exercises _adjust's CEI pattern under repeated state transitions.
|
|
403
|
+
function test_CEI_rapidBorrowRepaySequence() public {
|
|
404
|
+
vm.deal(USER, 5000e18);
|
|
405
|
+
|
|
406
|
+
for (uint256 i; i < 3; i++) {
|
|
407
|
+
// Borrow.
|
|
408
|
+
vm.prank(USER);
|
|
409
|
+
uint256 tokens =
|
|
410
|
+
jbMultiTerminal().pay{value: 10e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 10e18, USER, 0, "", "");
|
|
411
|
+
|
|
412
|
+
uint256 borrowable =
|
|
413
|
+
LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
|
|
414
|
+
if (borrowable == 0) continue;
|
|
415
|
+
|
|
416
|
+
mockExpect(
|
|
417
|
+
address(jbPermissions()),
|
|
418
|
+
abi.encodeCall(
|
|
419
|
+
IJBPermissions.hasPermission, (address(LOANS_CONTRACT), USER, REVNET_ID, 11, true, true)
|
|
420
|
+
),
|
|
421
|
+
abi.encode(true)
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
|
|
425
|
+
|
|
426
|
+
vm.prank(USER);
|
|
427
|
+
(uint256 loanId,) = LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokens, payable(USER), 25);
|
|
428
|
+
|
|
429
|
+
REVLoan memory loan = LOANS_CONTRACT.loanOf(loanId);
|
|
430
|
+
|
|
431
|
+
// Immediately repay.
|
|
432
|
+
vm.prank(USER);
|
|
433
|
+
LOANS_CONTRACT.repayLoan{value: loan.amount * 2}({
|
|
434
|
+
loanId: loanId,
|
|
435
|
+
maxRepayBorrowAmount: loan.amount * 2,
|
|
436
|
+
collateralCountToReturn: loan.collateral,
|
|
437
|
+
beneficiary: payable(USER),
|
|
438
|
+
allowance: JBSingleAllowance({sigDeadline: 0, amount: 0, expiration: 0, nonce: 0, signature: ""})
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// After all borrows repaid, accounting should be clean.
|
|
443
|
+
uint256 totalBorrowed = LOANS_CONTRACT.totalBorrowedFrom(REVNET_ID, jbMultiTerminal(), JBConstants.NATIVE_TOKEN);
|
|
444
|
+
uint256 totalCollateral = LOANS_CONTRACT.totalCollateralOf(REVNET_ID);
|
|
445
|
+
assertEq(totalBorrowed, 0, "totalBorrowedFrom should be 0 after all repaid");
|
|
446
|
+
assertEq(totalCollateral, 0, "totalCollateralOf should be 0 after all repaid");
|
|
447
|
+
}
|
|
448
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
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
|
+
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/swap-terminal-v6/script/helpers/SwapTerminalDeploymentLib.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 {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
|
|
18
|
+
import {REVLoans} from "../src/REVLoans.sol";
|
|
19
|
+
import {REVStageConfig, REVAutoIssuance} from "../src/structs/REVStageConfig.sol";
|
|
20
|
+
import {REVLoanSource} from "../src/structs/REVLoanSource.sol";
|
|
21
|
+
import {REVDescription} from "../src/structs/REVDescription.sol";
|
|
22
|
+
import {IREVLoans} from "./../src/interfaces/IREVLoans.sol";
|
|
23
|
+
import {JBSuckerDeployerConfig} from "@bananapus/suckers-v6/src/structs/JBSuckerDeployerConfig.sol";
|
|
24
|
+
import {JBSuckerRegistry} from "@bananapus/suckers-v6/src/JBSuckerRegistry.sol";
|
|
25
|
+
import {JB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
|
|
26
|
+
import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
|
|
27
|
+
import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
|
|
28
|
+
import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
|
|
29
|
+
import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
|
|
30
|
+
|
|
31
|
+
/// @notice Tests for PR #29: fix/l27-swap-terminal-permission
|
|
32
|
+
/// Verifies that ADD_SWAP_TERMINAL_POOL (permission ID 26) is included in the default
|
|
33
|
+
/// split operator permissions. The fix adds this as the 7th default permission.
|
|
34
|
+
contract TestPR29_SwapTerminalPermission is TestBaseWorkflow, JBTest {
|
|
35
|
+
bytes32 REV_DEPLOYER_SALT = "REVDeployer";
|
|
36
|
+
|
|
37
|
+
REVDeployer REV_DEPLOYER;
|
|
38
|
+
JB721TiersHook EXAMPLE_HOOK;
|
|
39
|
+
IJB721TiersHookDeployer HOOK_DEPLOYER;
|
|
40
|
+
IJB721TiersHookStore HOOK_STORE;
|
|
41
|
+
IJBAddressRegistry ADDRESS_REGISTRY;
|
|
42
|
+
IREVLoans LOANS_CONTRACT;
|
|
43
|
+
IJBSuckerRegistry SUCKER_REGISTRY;
|
|
44
|
+
CTPublisher PUBLISHER;
|
|
45
|
+
MockBuybackDataHook MOCK_BUYBACK;
|
|
46
|
+
|
|
47
|
+
uint256 FEE_PROJECT_ID;
|
|
48
|
+
uint256 TEST_REVNET_ID;
|
|
49
|
+
|
|
50
|
+
address private constant TRUSTED_FORWARDER = 0xB2b5841DBeF766d4b521221732F9B618fCf34A87;
|
|
51
|
+
|
|
52
|
+
function setUp() public override {
|
|
53
|
+
super.setUp();
|
|
54
|
+
FEE_PROJECT_ID = jbProjects().createFor(multisig());
|
|
55
|
+
SUCKER_REGISTRY = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
|
|
56
|
+
HOOK_STORE = new JB721TiersHookStore();
|
|
57
|
+
EXAMPLE_HOOK = new JB721TiersHook(jbDirectory(), jbPermissions(), jbRulesets(), HOOK_STORE, multisig());
|
|
58
|
+
ADDRESS_REGISTRY = new JBAddressRegistry();
|
|
59
|
+
HOOK_DEPLOYER = new JB721TiersHookDeployer(EXAMPLE_HOOK, HOOK_STORE, ADDRESS_REGISTRY, multisig());
|
|
60
|
+
PUBLISHER = new CTPublisher(jbDirectory(), jbPermissions(), FEE_PROJECT_ID, multisig());
|
|
61
|
+
MOCK_BUYBACK = new MockBuybackDataHook();
|
|
62
|
+
LOANS_CONTRACT = new REVLoans({
|
|
63
|
+
controller: jbController(),
|
|
64
|
+
projects: jbProjects(),
|
|
65
|
+
revId: FEE_PROJECT_ID,
|
|
66
|
+
owner: address(this),
|
|
67
|
+
permit2: permit2(),
|
|
68
|
+
trustedForwarder: TRUSTED_FORWARDER
|
|
69
|
+
});
|
|
70
|
+
REV_DEPLOYER = new REVDeployer{salt: REV_DEPLOYER_SALT}(
|
|
71
|
+
jbController(),
|
|
72
|
+
SUCKER_REGISTRY,
|
|
73
|
+
FEE_PROJECT_ID,
|
|
74
|
+
HOOK_DEPLOYER,
|
|
75
|
+
PUBLISHER,
|
|
76
|
+
IJBRulesetDataHook(address(MOCK_BUYBACK)),
|
|
77
|
+
address(LOANS_CONTRACT),
|
|
78
|
+
TRUSTED_FORWARDER
|
|
79
|
+
);
|
|
80
|
+
vm.prank(multisig());
|
|
81
|
+
jbProjects().approve(address(REV_DEPLOYER), FEE_PROJECT_ID);
|
|
82
|
+
|
|
83
|
+
// Deploy the fee project as a revnet
|
|
84
|
+
(REVConfig memory feeCfg, JBTerminalConfig[] memory feeTc, REVSuckerDeploymentConfig memory feeSdc) =
|
|
85
|
+
_buildConfig("FeeProject", "FEE", "FEE_SALT");
|
|
86
|
+
|
|
87
|
+
vm.prank(multisig());
|
|
88
|
+
REV_DEPLOYER.deployFor({
|
|
89
|
+
revnetId: FEE_PROJECT_ID,
|
|
90
|
+
configuration: feeCfg,
|
|
91
|
+
terminalConfigurations: feeTc,
|
|
92
|
+
suckerDeploymentConfiguration: feeSdc
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Deploy the test revnet
|
|
96
|
+
(REVConfig memory cfg, JBTerminalConfig[] memory tc, REVSuckerDeploymentConfig memory sdc) =
|
|
97
|
+
_buildConfig("TestRevnet", "TST", "TST_SALT");
|
|
98
|
+
|
|
99
|
+
TEST_REVNET_ID = REV_DEPLOYER.deployFor({
|
|
100
|
+
revnetId: 0, configuration: cfg, terminalConfigurations: tc, suckerDeploymentConfiguration: sdc
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function _buildConfig(
|
|
105
|
+
string memory name,
|
|
106
|
+
string memory ticker,
|
|
107
|
+
bytes32 salt
|
|
108
|
+
)
|
|
109
|
+
internal
|
|
110
|
+
view
|
|
111
|
+
returns (REVConfig memory cfg, JBTerminalConfig[] memory tc, REVSuckerDeploymentConfig memory sdc)
|
|
112
|
+
{
|
|
113
|
+
JBAccountingContext[] memory acc = new JBAccountingContext[](1);
|
|
114
|
+
acc[0] = JBAccountingContext({
|
|
115
|
+
token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
116
|
+
});
|
|
117
|
+
tc = new JBTerminalConfig[](1);
|
|
118
|
+
tc[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: acc});
|
|
119
|
+
|
|
120
|
+
REVStageConfig[] memory stages = new REVStageConfig[](1);
|
|
121
|
+
JBSplit[] memory splits = new JBSplit[](1);
|
|
122
|
+
splits[0].beneficiary = payable(multisig());
|
|
123
|
+
splits[0].percent = 10_000;
|
|
124
|
+
stages[0] = REVStageConfig({
|
|
125
|
+
startsAtOrAfter: uint40(block.timestamp),
|
|
126
|
+
autoIssuances: new REVAutoIssuance[](0),
|
|
127
|
+
splitPercent: 0,
|
|
128
|
+
splits: splits,
|
|
129
|
+
initialIssuance: uint112(1000e18),
|
|
130
|
+
issuanceCutFrequency: 0,
|
|
131
|
+
issuanceCutPercent: 0,
|
|
132
|
+
cashOutTaxRate: 5000,
|
|
133
|
+
extraMetadata: 0
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
cfg = REVConfig({
|
|
137
|
+
description: REVDescription(name, ticker, "ipfs://test", salt),
|
|
138
|
+
baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
139
|
+
splitOperator: multisig(),
|
|
140
|
+
stageConfigurations: stages
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
sdc = REVSuckerDeploymentConfig({
|
|
144
|
+
deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256(abi.encodePacked("TEST"))
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/// @notice Verify the split operator has SET_BUYBACK_HOOK and SET_SWAP_TERMINAL permissions.
|
|
149
|
+
function test_splitOperator_hasRegistryPermissions() public view {
|
|
150
|
+
bool hasBuybackHook = jbPermissions()
|
|
151
|
+
.hasPermission({
|
|
152
|
+
operator: multisig(),
|
|
153
|
+
account: address(REV_DEPLOYER),
|
|
154
|
+
projectId: TEST_REVNET_ID,
|
|
155
|
+
permissionId: JBPermissionIds.SET_BUYBACK_HOOK,
|
|
156
|
+
includeRoot: false,
|
|
157
|
+
includeWildcardProjectId: false
|
|
158
|
+
});
|
|
159
|
+
assertTrue(hasBuybackHook, "Split operator should have SET_BUYBACK_HOOK permission");
|
|
160
|
+
|
|
161
|
+
bool hasSwapTerminal = jbPermissions()
|
|
162
|
+
.hasPermission({
|
|
163
|
+
operator: multisig(),
|
|
164
|
+
account: address(REV_DEPLOYER),
|
|
165
|
+
projectId: TEST_REVNET_ID,
|
|
166
|
+
permissionId: JBPermissionIds.SET_SWAP_TERMINAL,
|
|
167
|
+
includeRoot: false,
|
|
168
|
+
includeWildcardProjectId: false
|
|
169
|
+
});
|
|
170
|
+
assertTrue(hasSwapTerminal, "Split operator should have SET_SWAP_TERMINAL permission");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/// @notice Verify all 9 default permissions are present for the split operator.
|
|
174
|
+
function test_allDefaultPermissionsPresent() public view {
|
|
175
|
+
// All 9 default permissions that should be granted
|
|
176
|
+
uint256[9] memory expectedPermissions = [
|
|
177
|
+
uint256(JBPermissionIds.SET_SPLIT_GROUPS),
|
|
178
|
+
uint256(JBPermissionIds.SET_BUYBACK_POOL),
|
|
179
|
+
uint256(JBPermissionIds.SET_BUYBACK_TWAP),
|
|
180
|
+
uint256(JBPermissionIds.SET_PROJECT_URI),
|
|
181
|
+
uint256(JBPermissionIds.ADD_PRICE_FEED),
|
|
182
|
+
uint256(JBPermissionIds.SUCKER_SAFETY),
|
|
183
|
+
uint256(JBPermissionIds.ADD_SWAP_TERMINAL_POOL),
|
|
184
|
+
uint256(JBPermissionIds.SET_BUYBACK_HOOK),
|
|
185
|
+
uint256(JBPermissionIds.SET_SWAP_TERMINAL)
|
|
186
|
+
];
|
|
187
|
+
|
|
188
|
+
for (uint256 i = 0; i < expectedPermissions.length; i++) {
|
|
189
|
+
bool hasPermission = jbPermissions()
|
|
190
|
+
.hasPermission({
|
|
191
|
+
operator: multisig(),
|
|
192
|
+
account: address(REV_DEPLOYER),
|
|
193
|
+
projectId: TEST_REVNET_ID,
|
|
194
|
+
permissionId: expectedPermissions[i],
|
|
195
|
+
includeRoot: false,
|
|
196
|
+
includeWildcardProjectId: false
|
|
197
|
+
});
|
|
198
|
+
assertTrue(
|
|
199
|
+
hasPermission,
|
|
200
|
+
string.concat(
|
|
201
|
+
"Missing permission at index ", vm.toString(i), " (ID ", vm.toString(expectedPermissions[i]), ")"
|
|
202
|
+
)
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|