@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,704 @@
|
|
|
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
|
+
|
|
17
|
+
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
18
|
+
import {JBFees} from "@bananapus/core-v6/src/libraries/JBFees.sol";
|
|
19
|
+
import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
|
|
20
|
+
import {MockPriceFeed} from "@bananapus/core-v6/test/mock/MockPriceFeed.sol";
|
|
21
|
+
import {MockERC20} from "@bananapus/core-v6/test/mock/MockERC20.sol";
|
|
22
|
+
import {REVLoans} from "../src/REVLoans.sol";
|
|
23
|
+
import {REVLoan} from "../src/structs/REVLoan.sol";
|
|
24
|
+
import {REVStageConfig, REVAutoIssuance} from "../src/structs/REVStageConfig.sol";
|
|
25
|
+
import {REVLoanSource} from "../src/structs/REVLoanSource.sol";
|
|
26
|
+
import {REVDescription} from "../src/structs/REVDescription.sol";
|
|
27
|
+
import {IREVLoans} from "./../src/interfaces/IREVLoans.sol";
|
|
28
|
+
import {JBSuckerDeployerConfig} from "@bananapus/suckers-v6/src/structs/JBSuckerDeployerConfig.sol";
|
|
29
|
+
import {JBSuckerRegistry} from "@bananapus/suckers-v6/src/JBSuckerRegistry.sol";
|
|
30
|
+
import {JB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
|
|
31
|
+
import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
|
|
32
|
+
import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
|
|
33
|
+
import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
|
|
34
|
+
import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
|
|
35
|
+
import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
|
|
36
|
+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
37
|
+
|
|
38
|
+
/// @notice A terminal mock that always reverts on pay(), used to simulate fee payment failure.
|
|
39
|
+
contract RevertingFeeTerminal is ERC165, IJBPayoutTerminal {
|
|
40
|
+
function pay(
|
|
41
|
+
uint256,
|
|
42
|
+
address,
|
|
43
|
+
uint256,
|
|
44
|
+
address,
|
|
45
|
+
uint256,
|
|
46
|
+
string calldata,
|
|
47
|
+
bytes calldata
|
|
48
|
+
)
|
|
49
|
+
external
|
|
50
|
+
payable
|
|
51
|
+
override
|
|
52
|
+
returns (uint256)
|
|
53
|
+
{
|
|
54
|
+
revert("Fee payment failed");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function accountingContextForTokenOf(uint256, address) external pure override returns (JBAccountingContext memory) {
|
|
58
|
+
return JBAccountingContext({
|
|
59
|
+
token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function accountingContextsOf(uint256) external pure override returns (JBAccountingContext[] memory) {
|
|
64
|
+
return new JBAccountingContext[](0);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function addAccountingContextsFor(uint256, JBAccountingContext[] calldata) external override {}
|
|
68
|
+
function addToBalanceOf(
|
|
69
|
+
uint256,
|
|
70
|
+
address,
|
|
71
|
+
uint256,
|
|
72
|
+
bool,
|
|
73
|
+
string calldata,
|
|
74
|
+
bytes calldata
|
|
75
|
+
)
|
|
76
|
+
external
|
|
77
|
+
payable
|
|
78
|
+
override
|
|
79
|
+
{}
|
|
80
|
+
|
|
81
|
+
function currentSurplusOf(
|
|
82
|
+
uint256,
|
|
83
|
+
JBAccountingContext[] memory,
|
|
84
|
+
uint256,
|
|
85
|
+
uint256
|
|
86
|
+
)
|
|
87
|
+
external
|
|
88
|
+
pure
|
|
89
|
+
override
|
|
90
|
+
returns (uint256)
|
|
91
|
+
{
|
|
92
|
+
return 0;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function migrateBalanceOf(uint256, address, IJBTerminal) external pure override returns (uint256) {
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function sendPayoutsOf(uint256, address, uint256, uint256, uint256) external pure override returns (uint256) {
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function useAllowanceOf(
|
|
104
|
+
uint256,
|
|
105
|
+
address,
|
|
106
|
+
uint256,
|
|
107
|
+
uint256,
|
|
108
|
+
uint256,
|
|
109
|
+
address payable,
|
|
110
|
+
address payable,
|
|
111
|
+
string calldata
|
|
112
|
+
)
|
|
113
|
+
external
|
|
114
|
+
pure
|
|
115
|
+
override
|
|
116
|
+
returns (uint256)
|
|
117
|
+
{
|
|
118
|
+
return 0;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function supportsInterface(bytes4 interfaceId) public view override(ERC165, IERC165) returns (bool) {
|
|
122
|
+
return interfaceId == type(IJBTerminal).interfaceId || interfaceId == type(IJBPayoutTerminal).interfaceId
|
|
123
|
+
|| super.supportsInterface(interfaceId);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
receive() external payable {}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
struct FeeRecoveryProjectConfig {
|
|
130
|
+
REVConfig configuration;
|
|
131
|
+
JBTerminalConfig[] terminalConfigurations;
|
|
132
|
+
REVSuckerDeploymentConfig suckerDeploymentConfiguration;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/// @title REVLoansFeeRecovery
|
|
136
|
+
/// @notice Tests for the fee payment error recovery in REVLoans._addTo().
|
|
137
|
+
/// @dev When feeTerminal.pay() reverts, the borrower should receive the fee amount back
|
|
138
|
+
/// instead of losing it. For ERC-20 tokens, the dangling allowance must also be cleaned up.
|
|
139
|
+
contract REVLoansFeeRecovery is TestBaseWorkflow, JBTest {
|
|
140
|
+
bytes32 REV_DEPLOYER_SALT = "REVDeployer";
|
|
141
|
+
bytes32 ERC20_SALT = "REV_TOKEN";
|
|
142
|
+
|
|
143
|
+
REVDeployer REV_DEPLOYER;
|
|
144
|
+
JB721TiersHook EXAMPLE_HOOK;
|
|
145
|
+
IJB721TiersHookDeployer HOOK_DEPLOYER;
|
|
146
|
+
IJB721TiersHookStore HOOK_STORE;
|
|
147
|
+
IJBAddressRegistry ADDRESS_REGISTRY;
|
|
148
|
+
IREVLoans LOANS_CONTRACT;
|
|
149
|
+
MockERC20 TOKEN;
|
|
150
|
+
IJBSuckerRegistry SUCKER_REGISTRY;
|
|
151
|
+
CTPublisher PUBLISHER;
|
|
152
|
+
MockBuybackDataHook MOCK_BUYBACK;
|
|
153
|
+
RevertingFeeTerminal REVERTING_TERMINAL;
|
|
154
|
+
|
|
155
|
+
uint256 FEE_PROJECT_ID;
|
|
156
|
+
uint256 REVNET_ID;
|
|
157
|
+
|
|
158
|
+
address USER = makeAddr("user");
|
|
159
|
+
address private constant TRUSTED_FORWARDER = 0xB2b5841DBeF766d4b521221732F9B618fCf34A87;
|
|
160
|
+
|
|
161
|
+
function _getFeeProjectConfig() internal view returns (FeeRecoveryProjectConfig memory) {
|
|
162
|
+
uint8 decimals = 18;
|
|
163
|
+
uint256 decimalMultiplier = 10 ** decimals;
|
|
164
|
+
|
|
165
|
+
JBAccountingContext[] memory accountingContextsToAccept = new JBAccountingContext[](2);
|
|
166
|
+
accountingContextsToAccept[0] = JBAccountingContext({
|
|
167
|
+
token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
168
|
+
});
|
|
169
|
+
accountingContextsToAccept[1] =
|
|
170
|
+
JBAccountingContext({token: address(TOKEN), decimals: 6, currency: uint32(uint160(address(TOKEN)))});
|
|
171
|
+
|
|
172
|
+
JBTerminalConfig[] memory terminalConfigurations = new JBTerminalConfig[](1);
|
|
173
|
+
terminalConfigurations[0] =
|
|
174
|
+
JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: accountingContextsToAccept});
|
|
175
|
+
|
|
176
|
+
REVStageConfig[] memory stageConfigurations = new REVStageConfig[](1);
|
|
177
|
+
JBSplit[] memory splits = new JBSplit[](1);
|
|
178
|
+
splits[0].beneficiary = payable(multisig());
|
|
179
|
+
splits[0].percent = 10_000;
|
|
180
|
+
|
|
181
|
+
REVAutoIssuance[] memory issuanceConfs = new REVAutoIssuance[](1);
|
|
182
|
+
issuanceConfs[0] = REVAutoIssuance({
|
|
183
|
+
chainId: uint32(block.chainid), count: uint104(70_000 * decimalMultiplier), beneficiary: multisig()
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
stageConfigurations[0] = REVStageConfig({
|
|
187
|
+
startsAtOrAfter: uint40(block.timestamp),
|
|
188
|
+
autoIssuances: issuanceConfs,
|
|
189
|
+
splitPercent: 2000,
|
|
190
|
+
splits: splits,
|
|
191
|
+
initialIssuance: uint112(1000 * decimalMultiplier),
|
|
192
|
+
issuanceCutFrequency: 90 days,
|
|
193
|
+
issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
|
|
194
|
+
cashOutTaxRate: 6000,
|
|
195
|
+
extraMetadata: 0
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
REVLoanSource[] memory _loanSources = new REVLoanSource[](0);
|
|
199
|
+
|
|
200
|
+
REVConfig memory revnetConfiguration = REVConfig({
|
|
201
|
+
description: REVDescription(
|
|
202
|
+
"Revnet", "$REV", "ipfs://QmNRHT91HcDgMcenebYX7rJigt77cgNcosvuhX21wkF3tx", ERC20_SALT
|
|
203
|
+
),
|
|
204
|
+
baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
205
|
+
splitOperator: multisig(),
|
|
206
|
+
stageConfigurations: stageConfigurations
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
return FeeRecoveryProjectConfig({
|
|
210
|
+
configuration: revnetConfiguration,
|
|
211
|
+
terminalConfigurations: terminalConfigurations,
|
|
212
|
+
suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
|
|
213
|
+
deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256(abi.encodePacked("REV"))
|
|
214
|
+
})
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function _getRevnetConfig() internal view returns (FeeRecoveryProjectConfig memory) {
|
|
219
|
+
uint8 decimals = 18;
|
|
220
|
+
uint256 decimalMultiplier = 10 ** decimals;
|
|
221
|
+
|
|
222
|
+
JBAccountingContext[] memory accountingContextsToAccept = new JBAccountingContext[](2);
|
|
223
|
+
accountingContextsToAccept[0] = JBAccountingContext({
|
|
224
|
+
token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
225
|
+
});
|
|
226
|
+
accountingContextsToAccept[1] =
|
|
227
|
+
JBAccountingContext({token: address(TOKEN), decimals: 6, currency: uint32(uint160(address(TOKEN)))});
|
|
228
|
+
|
|
229
|
+
JBTerminalConfig[] memory terminalConfigurations = new JBTerminalConfig[](1);
|
|
230
|
+
terminalConfigurations[0] =
|
|
231
|
+
JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: accountingContextsToAccept});
|
|
232
|
+
|
|
233
|
+
JBSplit[] memory splits = new JBSplit[](1);
|
|
234
|
+
splits[0].beneficiary = payable(multisig());
|
|
235
|
+
splits[0].percent = 10_000;
|
|
236
|
+
|
|
237
|
+
REVStageConfig[] memory stageConfigurations = new REVStageConfig[](1);
|
|
238
|
+
REVAutoIssuance[] memory issuanceConfs = new REVAutoIssuance[](1);
|
|
239
|
+
issuanceConfs[0] = REVAutoIssuance({
|
|
240
|
+
chainId: uint32(block.chainid), count: uint104(70_000 * decimalMultiplier), beneficiary: multisig()
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
stageConfigurations[0] = REVStageConfig({
|
|
244
|
+
startsAtOrAfter: uint40(block.timestamp),
|
|
245
|
+
autoIssuances: issuanceConfs,
|
|
246
|
+
splitPercent: 2000,
|
|
247
|
+
splits: splits,
|
|
248
|
+
initialIssuance: uint112(1000 * decimalMultiplier),
|
|
249
|
+
issuanceCutFrequency: 90 days,
|
|
250
|
+
issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
|
|
251
|
+
cashOutTaxRate: 6000,
|
|
252
|
+
extraMetadata: 0
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
REVLoanSource[] memory _loanSources = new REVLoanSource[](2);
|
|
256
|
+
_loanSources[0] = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
|
|
257
|
+
_loanSources[1] = REVLoanSource({token: address(TOKEN), terminal: jbMultiTerminal()});
|
|
258
|
+
|
|
259
|
+
REVConfig memory revnetConfiguration = REVConfig({
|
|
260
|
+
description: REVDescription(
|
|
261
|
+
"NANA", "$NANA", "ipfs://QmNRHT91HcDgMcenebYX7rJigt77cgNxosvuhX21wkF3tx", "NANA_TOKEN"
|
|
262
|
+
),
|
|
263
|
+
baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
264
|
+
splitOperator: multisig(),
|
|
265
|
+
stageConfigurations: stageConfigurations
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
return FeeRecoveryProjectConfig({
|
|
269
|
+
configuration: revnetConfiguration,
|
|
270
|
+
terminalConfigurations: terminalConfigurations,
|
|
271
|
+
suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
|
|
272
|
+
deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256(abi.encodePacked("NANA"))
|
|
273
|
+
})
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function setUp() public override {
|
|
278
|
+
super.setUp();
|
|
279
|
+
|
|
280
|
+
FEE_PROJECT_ID = jbProjects().createFor(multisig());
|
|
281
|
+
|
|
282
|
+
SUCKER_REGISTRY = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
|
|
283
|
+
HOOK_STORE = new JB721TiersHookStore();
|
|
284
|
+
EXAMPLE_HOOK = new JB721TiersHook(jbDirectory(), jbPermissions(), jbRulesets(), HOOK_STORE, multisig());
|
|
285
|
+
ADDRESS_REGISTRY = new JBAddressRegistry();
|
|
286
|
+
HOOK_DEPLOYER = new JB721TiersHookDeployer(EXAMPLE_HOOK, HOOK_STORE, ADDRESS_REGISTRY, multisig());
|
|
287
|
+
PUBLISHER = new CTPublisher(jbDirectory(), jbPermissions(), FEE_PROJECT_ID, multisig());
|
|
288
|
+
MOCK_BUYBACK = new MockBuybackDataHook();
|
|
289
|
+
TOKEN = new MockERC20("1/2 ETH", "1/2");
|
|
290
|
+
REVERTING_TERMINAL = new RevertingFeeTerminal();
|
|
291
|
+
|
|
292
|
+
MockPriceFeed priceFeed = new MockPriceFeed(1e21, 6);
|
|
293
|
+
vm.prank(multisig());
|
|
294
|
+
jbPrices()
|
|
295
|
+
.addPriceFeedFor(0, uint32(uint160(address(TOKEN))), uint32(uint160(JBConstants.NATIVE_TOKEN)), priceFeed);
|
|
296
|
+
|
|
297
|
+
LOANS_CONTRACT = new REVLoans({
|
|
298
|
+
controller: jbController(),
|
|
299
|
+
projects: jbProjects(),
|
|
300
|
+
revId: FEE_PROJECT_ID,
|
|
301
|
+
owner: address(this),
|
|
302
|
+
permit2: permit2(),
|
|
303
|
+
trustedForwarder: TRUSTED_FORWARDER
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
REV_DEPLOYER = new REVDeployer{salt: REV_DEPLOYER_SALT}(
|
|
307
|
+
jbController(),
|
|
308
|
+
SUCKER_REGISTRY,
|
|
309
|
+
FEE_PROJECT_ID,
|
|
310
|
+
HOOK_DEPLOYER,
|
|
311
|
+
PUBLISHER,
|
|
312
|
+
IJBRulesetDataHook(address(MOCK_BUYBACK)),
|
|
313
|
+
address(LOANS_CONTRACT),
|
|
314
|
+
TRUSTED_FORWARDER
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
// Deploy fee project.
|
|
318
|
+
vm.prank(multisig());
|
|
319
|
+
jbProjects().approve(address(REV_DEPLOYER), FEE_PROJECT_ID);
|
|
320
|
+
|
|
321
|
+
FeeRecoveryProjectConfig memory feeProjectConfig = _getFeeProjectConfig();
|
|
322
|
+
vm.prank(multisig());
|
|
323
|
+
REV_DEPLOYER.deployFor({
|
|
324
|
+
revnetId: FEE_PROJECT_ID,
|
|
325
|
+
configuration: feeProjectConfig.configuration,
|
|
326
|
+
terminalConfigurations: feeProjectConfig.terminalConfigurations,
|
|
327
|
+
suckerDeploymentConfiguration: feeProjectConfig.suckerDeploymentConfiguration
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// Deploy revnet with loans enabled.
|
|
331
|
+
FeeRecoveryProjectConfig memory revnetConfig = _getRevnetConfig();
|
|
332
|
+
REVNET_ID = REV_DEPLOYER.deployFor({
|
|
333
|
+
revnetId: 0,
|
|
334
|
+
configuration: revnetConfig.configuration,
|
|
335
|
+
terminalConfigurations: revnetConfig.terminalConfigurations,
|
|
336
|
+
suckerDeploymentConfiguration: revnetConfig.suckerDeploymentConfiguration
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
vm.deal(USER, 1000e18);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// =========================================================================
|
|
343
|
+
// Helpers
|
|
344
|
+
// =========================================================================
|
|
345
|
+
|
|
346
|
+
/// @notice Mock loan permissions for a user.
|
|
347
|
+
function _mockLoanPermission(address user) internal {
|
|
348
|
+
mockExpect(
|
|
349
|
+
address(jbPermissions()),
|
|
350
|
+
abi.encodeCall(IJBPermissions.hasPermission, (address(LOANS_CONTRACT), user, REVNET_ID, 11, true, true)),
|
|
351
|
+
abi.encode(true)
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/// @notice Make the directory return the reverting terminal as the fee terminal for the REV project.
|
|
356
|
+
function _mockRevertingFeeTerminal(address token) internal {
|
|
357
|
+
vm.mockCall(
|
|
358
|
+
address(jbDirectory()),
|
|
359
|
+
abi.encodeWithSelector(IJBDirectory.primaryTerminalOf.selector, FEE_PROJECT_ID, token),
|
|
360
|
+
abi.encode(address(REVERTING_TERMINAL))
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/// @notice Borrow against native ETH and return the borrower's balance change.
|
|
365
|
+
function _borrowNative(
|
|
366
|
+
address user,
|
|
367
|
+
uint256 ethAmount,
|
|
368
|
+
uint256 prepaidFee
|
|
369
|
+
)
|
|
370
|
+
internal
|
|
371
|
+
returns (uint256 loanId, uint256 borrowerBalanceBefore, uint256 borrowerBalanceAfter)
|
|
372
|
+
{
|
|
373
|
+
// Pay into revnet to get tokens.
|
|
374
|
+
vm.prank(user);
|
|
375
|
+
uint256 tokenCount =
|
|
376
|
+
jbMultiTerminal().pay{value: ethAmount}(REVNET_ID, JBConstants.NATIVE_TOKEN, ethAmount, user, 0, "", "");
|
|
377
|
+
|
|
378
|
+
_mockLoanPermission(user);
|
|
379
|
+
|
|
380
|
+
REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
|
|
381
|
+
|
|
382
|
+
borrowerBalanceBefore = user.balance;
|
|
383
|
+
|
|
384
|
+
vm.prank(user);
|
|
385
|
+
(loanId,) = LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokenCount, payable(user), prepaidFee);
|
|
386
|
+
|
|
387
|
+
borrowerBalanceAfter = user.balance;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// =========================================================================
|
|
391
|
+
// Test: Normal fee payment succeeds (regression — confirm existing behavior)
|
|
392
|
+
// =========================================================================
|
|
393
|
+
|
|
394
|
+
/// @notice When the fee terminal is healthy, the REV fee is deducted from the borrower's payout.
|
|
395
|
+
function test_feePaymentSuccess_nativeToken() public {
|
|
396
|
+
(, uint256 balanceBefore, uint256 balanceAfter) = _borrowNative(USER, 10e18, 25);
|
|
397
|
+
|
|
398
|
+
uint256 received = balanceAfter - balanceBefore;
|
|
399
|
+
|
|
400
|
+
// The borrower should have received something (net of both source fee + REV fee).
|
|
401
|
+
assertGt(received, 0, "Borrower should receive ETH");
|
|
402
|
+
|
|
403
|
+
// No ETH should be stuck in the loans contract.
|
|
404
|
+
assertEq(address(LOANS_CONTRACT).balance, 0, "No ETH stuck in loans contract");
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// =========================================================================
|
|
408
|
+
// Test: Fee terminal reverts with native ETH — borrower gets fee back
|
|
409
|
+
// =========================================================================
|
|
410
|
+
|
|
411
|
+
/// @notice When feeTerminal.pay() reverts, the borrower receives the REV fee amount back.
|
|
412
|
+
function test_feePaymentFailure_nativeToken_borrowerGetsMoreETH() public {
|
|
413
|
+
// Pay into revnet first so both borrow attempts start from identical state.
|
|
414
|
+
vm.prank(USER);
|
|
415
|
+
uint256 tokenCount =
|
|
416
|
+
jbMultiTerminal().pay{value: 10e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 10e18, USER, 0, "", "");
|
|
417
|
+
|
|
418
|
+
_mockLoanPermission(USER);
|
|
419
|
+
REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
|
|
420
|
+
|
|
421
|
+
// Snapshot state before borrow.
|
|
422
|
+
uint256 snap = vm.snapshotState();
|
|
423
|
+
|
|
424
|
+
// Normal borrow.
|
|
425
|
+
uint256 balBefore = USER.balance;
|
|
426
|
+
vm.prank(USER);
|
|
427
|
+
LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokenCount, payable(USER), 25);
|
|
428
|
+
uint256 normalReceived = USER.balance - balBefore;
|
|
429
|
+
|
|
430
|
+
// Revert to snapshot — identical state.
|
|
431
|
+
vm.revertToState(snap);
|
|
432
|
+
|
|
433
|
+
// Mock the fee terminal to revert.
|
|
434
|
+
_mockRevertingFeeTerminal(JBConstants.NATIVE_TOKEN);
|
|
435
|
+
_mockLoanPermission(USER);
|
|
436
|
+
|
|
437
|
+
balBefore = USER.balance;
|
|
438
|
+
vm.prank(USER);
|
|
439
|
+
LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokenCount, payable(USER), 25);
|
|
440
|
+
uint256 failReceived = USER.balance - balBefore;
|
|
441
|
+
|
|
442
|
+
// The borrower with a failed fee terminal should receive MORE than the normal borrower,
|
|
443
|
+
// because the REV fee (1% of borrow amount) is returned to them.
|
|
444
|
+
assertGt(failReceived, normalReceived, "Failed-fee borrower should receive more ETH than normal borrower");
|
|
445
|
+
|
|
446
|
+
// No ETH should be stuck in the loans contract.
|
|
447
|
+
assertEq(address(LOANS_CONTRACT).balance, 0, "No ETH stuck in loans contract after fee failure");
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// =========================================================================
|
|
451
|
+
// Test: Fee terminal reverts with native ETH — amount difference matches REV fee
|
|
452
|
+
// =========================================================================
|
|
453
|
+
|
|
454
|
+
/// @notice The extra ETH the borrower receives when the fee terminal reverts matches
|
|
455
|
+
/// the expected REV fee amount (1% of borrow amount).
|
|
456
|
+
function test_feePaymentFailure_nativeToken_exactFeeRecovery() public {
|
|
457
|
+
REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
|
|
458
|
+
|
|
459
|
+
// Pay into revnet.
|
|
460
|
+
vm.prank(USER);
|
|
461
|
+
uint256 tokens =
|
|
462
|
+
jbMultiTerminal().pay{value: 10e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 10e18, USER, 0, "", "");
|
|
463
|
+
|
|
464
|
+
_mockLoanPermission(USER);
|
|
465
|
+
|
|
466
|
+
// Snapshot.
|
|
467
|
+
uint256 snap = vm.snapshotState();
|
|
468
|
+
|
|
469
|
+
// Normal borrow.
|
|
470
|
+
uint256 balBefore = USER.balance;
|
|
471
|
+
vm.prank(USER);
|
|
472
|
+
LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokens, payable(USER), 25);
|
|
473
|
+
uint256 normalReceived = USER.balance - balBefore;
|
|
474
|
+
|
|
475
|
+
// Get the actual borrow amount from the loan to compute expected REV fee.
|
|
476
|
+
// Loan ID = revnetId * 1e12 + loanNumber (first loan = 1).
|
|
477
|
+
REVLoan memory loan = LOANS_CONTRACT.loanOf(REVNET_ID * 1_000_000_000_000 + 1);
|
|
478
|
+
uint256 expectedRevFee =
|
|
479
|
+
JBFees.feeAmountFrom({amountBeforeFee: loan.amount, feePercent: LOANS_CONTRACT.REV_PREPAID_FEE_PERCENT()});
|
|
480
|
+
|
|
481
|
+
// Revert to snapshot.
|
|
482
|
+
vm.revertToState(snap);
|
|
483
|
+
|
|
484
|
+
// Mock fee terminal to revert.
|
|
485
|
+
_mockRevertingFeeTerminal(JBConstants.NATIVE_TOKEN);
|
|
486
|
+
_mockLoanPermission(USER);
|
|
487
|
+
|
|
488
|
+
balBefore = USER.balance;
|
|
489
|
+
vm.prank(USER);
|
|
490
|
+
LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokens, payable(USER), 25);
|
|
491
|
+
uint256 failReceived = USER.balance - balBefore;
|
|
492
|
+
|
|
493
|
+
// The difference should be the REV fee amount.
|
|
494
|
+
uint256 difference = failReceived - normalReceived;
|
|
495
|
+
assertEq(difference, expectedRevFee, "Difference should equal the REV fee amount");
|
|
496
|
+
|
|
497
|
+
// Verify no funds stuck.
|
|
498
|
+
assertEq(address(LOANS_CONTRACT).balance, 0, "No ETH stuck");
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// =========================================================================
|
|
502
|
+
// Test: Fee terminal reverts with ERC-20 — allowance is cleaned up
|
|
503
|
+
// =========================================================================
|
|
504
|
+
|
|
505
|
+
/// @notice When feeTerminal.pay() reverts for an ERC-20 loan, the dangling allowance
|
|
506
|
+
/// to the fee terminal is removed via safeDecreaseAllowance.
|
|
507
|
+
function test_feePaymentFailure_erc20_allowanceCleaned() public {
|
|
508
|
+
// Mock the fee terminal to revert for the TOKEN.
|
|
509
|
+
_mockRevertingFeeTerminal(address(TOKEN));
|
|
510
|
+
|
|
511
|
+
// Fund user with ERC-20 tokens.
|
|
512
|
+
uint256 payAmount = 1_000_000; // 6 decimals
|
|
513
|
+
deal(address(TOKEN), USER, payAmount);
|
|
514
|
+
|
|
515
|
+
// Pay into revnet with ERC-20.
|
|
516
|
+
vm.startPrank(USER);
|
|
517
|
+
TOKEN.approve(address(jbMultiTerminal()), payAmount);
|
|
518
|
+
uint256 tokenCount = jbMultiTerminal().pay(REVNET_ID, address(TOKEN), payAmount, USER, 0, "", "");
|
|
519
|
+
vm.stopPrank();
|
|
520
|
+
|
|
521
|
+
_mockLoanPermission(USER);
|
|
522
|
+
REVLoanSource memory source = REVLoanSource({token: address(TOKEN), terminal: jbMultiTerminal()});
|
|
523
|
+
|
|
524
|
+
// Check allowance to reverting terminal BEFORE borrow.
|
|
525
|
+
uint256 allowanceBefore = TOKEN.allowance(address(LOANS_CONTRACT), address(REVERTING_TERMINAL));
|
|
526
|
+
assertEq(allowanceBefore, 0, "No pre-existing allowance");
|
|
527
|
+
|
|
528
|
+
vm.prank(USER);
|
|
529
|
+
LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokenCount, payable(USER), 25);
|
|
530
|
+
|
|
531
|
+
// After the borrow, the allowance to the reverting terminal should still be 0
|
|
532
|
+
// (the catch block decreased it).
|
|
533
|
+
uint256 allowanceAfter = TOKEN.allowance(address(LOANS_CONTRACT), address(REVERTING_TERMINAL));
|
|
534
|
+
assertEq(allowanceAfter, 0, "Allowance should be cleaned up after fee failure");
|
|
535
|
+
|
|
536
|
+
// No tokens stuck in the loans contract.
|
|
537
|
+
assertEq(TOKEN.balanceOf(address(LOANS_CONTRACT)), 0, "No ERC-20 stuck in loans contract");
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// =========================================================================
|
|
541
|
+
// Test: Fee terminal reverts with ERC-20 — borrower gets fee back
|
|
542
|
+
// =========================================================================
|
|
543
|
+
|
|
544
|
+
/// @notice When feeTerminal.pay() reverts for an ERC-20 loan, the borrower receives
|
|
545
|
+
/// the fee amount that would have gone to the REV project.
|
|
546
|
+
function test_feePaymentFailure_erc20_borrowerGetsMoreTokens() public {
|
|
547
|
+
uint256 payAmount = 1_000_000; // 6 decimals
|
|
548
|
+
REVLoanSource memory source = REVLoanSource({token: address(TOKEN), terminal: jbMultiTerminal()});
|
|
549
|
+
|
|
550
|
+
// Pay into revnet with ERC-20.
|
|
551
|
+
deal(address(TOKEN), USER, payAmount);
|
|
552
|
+
vm.startPrank(USER);
|
|
553
|
+
TOKEN.approve(address(jbMultiTerminal()), payAmount);
|
|
554
|
+
uint256 tokens = jbMultiTerminal().pay(REVNET_ID, address(TOKEN), payAmount, USER, 0, "", "");
|
|
555
|
+
vm.stopPrank();
|
|
556
|
+
|
|
557
|
+
_mockLoanPermission(USER);
|
|
558
|
+
|
|
559
|
+
// Snapshot.
|
|
560
|
+
uint256 snap = vm.snapshotState();
|
|
561
|
+
|
|
562
|
+
// Normal borrow.
|
|
563
|
+
uint256 tokenBalBefore = TOKEN.balanceOf(USER);
|
|
564
|
+
vm.prank(USER);
|
|
565
|
+
LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokens, payable(USER), 25);
|
|
566
|
+
uint256 normalReceived = TOKEN.balanceOf(USER) - tokenBalBefore;
|
|
567
|
+
|
|
568
|
+
// Revert to snapshot.
|
|
569
|
+
vm.revertToState(snap);
|
|
570
|
+
|
|
571
|
+
// Mock fee terminal to revert.
|
|
572
|
+
_mockRevertingFeeTerminal(address(TOKEN));
|
|
573
|
+
_mockLoanPermission(USER);
|
|
574
|
+
|
|
575
|
+
tokenBalBefore = TOKEN.balanceOf(USER);
|
|
576
|
+
vm.prank(USER);
|
|
577
|
+
LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokens, payable(USER), 25);
|
|
578
|
+
uint256 failReceived = TOKEN.balanceOf(USER) - tokenBalBefore;
|
|
579
|
+
|
|
580
|
+
// Failed-fee borrower should receive more tokens.
|
|
581
|
+
assertGt(failReceived, normalReceived, "Failed-fee ERC-20 borrower should receive more tokens");
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// =========================================================================
|
|
585
|
+
// Test: No fee terminal (address(0)) — revFeeAmount is zero, no try/catch
|
|
586
|
+
// =========================================================================
|
|
587
|
+
|
|
588
|
+
/// @notice When no fee terminal exists for the token, revFeeAmount is 0 and no fee is attempted.
|
|
589
|
+
function test_noFeeTerminal_borrowStillWorks() public {
|
|
590
|
+
// Mock the directory to return address(0) for the fee terminal.
|
|
591
|
+
vm.mockCall(
|
|
592
|
+
address(jbDirectory()),
|
|
593
|
+
abi.encodeWithSelector(IJBDirectory.primaryTerminalOf.selector, FEE_PROJECT_ID, JBConstants.NATIVE_TOKEN),
|
|
594
|
+
abi.encode(address(0))
|
|
595
|
+
);
|
|
596
|
+
|
|
597
|
+
// Borrow should still work — no fee is taken.
|
|
598
|
+
(, uint256 balanceBefore, uint256 balanceAfter) = _borrowNative(USER, 10e18, 25);
|
|
599
|
+
uint256 received = balanceAfter - balanceBefore;
|
|
600
|
+
assertGt(received, 0, "Borrower should receive ETH even without fee terminal");
|
|
601
|
+
|
|
602
|
+
// No ETH stuck.
|
|
603
|
+
assertEq(address(LOANS_CONTRACT).balance, 0, "No ETH stuck");
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// =========================================================================
|
|
607
|
+
// Test: Multiple borrows with fee failure — no cumulative stuck funds
|
|
608
|
+
// =========================================================================
|
|
609
|
+
|
|
610
|
+
/// @notice After multiple borrows where the fee terminal reverts, no funds accumulate
|
|
611
|
+
/// in the loans contract.
|
|
612
|
+
function test_feePaymentFailure_multipleBorrows_noStuckFunds() public {
|
|
613
|
+
_mockRevertingFeeTerminal(JBConstants.NATIVE_TOKEN);
|
|
614
|
+
|
|
615
|
+
for (uint256 i; i < 3; i++) {
|
|
616
|
+
address borrower = makeAddr(string(abi.encodePacked("borrower", i)));
|
|
617
|
+
vm.deal(borrower, 100e18);
|
|
618
|
+
|
|
619
|
+
vm.prank(borrower);
|
|
620
|
+
uint256 tokens =
|
|
621
|
+
jbMultiTerminal().pay{value: 5e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 5e18, borrower, 0, "", "");
|
|
622
|
+
|
|
623
|
+
mockExpect(
|
|
624
|
+
address(jbPermissions()),
|
|
625
|
+
abi.encodeCall(
|
|
626
|
+
IJBPermissions.hasPermission, (address(LOANS_CONTRACT), borrower, REVNET_ID, 11, true, true)
|
|
627
|
+
),
|
|
628
|
+
abi.encode(true)
|
|
629
|
+
);
|
|
630
|
+
|
|
631
|
+
REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
|
|
632
|
+
|
|
633
|
+
vm.prank(borrower);
|
|
634
|
+
LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokens, payable(borrower), 25);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// After 3 borrows with fee failures, no ETH should be stuck.
|
|
638
|
+
assertEq(address(LOANS_CONTRACT).balance, 0, "No ETH stuck after multiple fee-failed borrows");
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// =========================================================================
|
|
642
|
+
// Fuzz: Fee recovery always returns correct amount to borrower
|
|
643
|
+
// =========================================================================
|
|
644
|
+
|
|
645
|
+
/// @notice Fuzz test: regardless of the borrow amount, when the fee terminal reverts,
|
|
646
|
+
/// the borrower always receives the full netAmountPaidOut minus only the source fee.
|
|
647
|
+
function test_fuzz_feeRecovery_nativeToken(uint256 payAmount) public {
|
|
648
|
+
// Bound to reasonable range. Need enough to get a nonzero borrow.
|
|
649
|
+
payAmount = bound(payAmount, 1e16, 100e18);
|
|
650
|
+
|
|
651
|
+
_mockRevertingFeeTerminal(JBConstants.NATIVE_TOKEN);
|
|
652
|
+
|
|
653
|
+
address borrower = makeAddr("fuzzBorrower");
|
|
654
|
+
vm.deal(borrower, payAmount + 1e18);
|
|
655
|
+
|
|
656
|
+
vm.prank(borrower);
|
|
657
|
+
uint256 tokens = jbMultiTerminal().pay{value: payAmount}(
|
|
658
|
+
REVNET_ID, JBConstants.NATIVE_TOKEN, payAmount, borrower, 0, "", ""
|
|
659
|
+
);
|
|
660
|
+
|
|
661
|
+
uint256 borrowable =
|
|
662
|
+
LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
|
|
663
|
+
|
|
664
|
+
// Skip if not enough surplus to borrow.
|
|
665
|
+
if (borrowable == 0) return;
|
|
666
|
+
|
|
667
|
+
_mockLoanPermission(borrower);
|
|
668
|
+
REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
|
|
669
|
+
|
|
670
|
+
uint256 balanceBefore = borrower.balance;
|
|
671
|
+
vm.prank(borrower);
|
|
672
|
+
LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokens, payable(borrower), 25);
|
|
673
|
+
uint256 received = borrower.balance - balanceBefore;
|
|
674
|
+
|
|
675
|
+
// The borrower should always receive something.
|
|
676
|
+
assertGt(received, 0, "Borrower should receive ETH in fuzz");
|
|
677
|
+
|
|
678
|
+
// No funds stuck.
|
|
679
|
+
assertEq(address(LOANS_CONTRACT).balance, 0, "No ETH stuck in fuzz");
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// =========================================================================
|
|
683
|
+
// Test: Fee recovery on native token — ETH returned from failed call
|
|
684
|
+
// =========================================================================
|
|
685
|
+
|
|
686
|
+
/// @notice Verifies that when a native-token fee terminal call reverts, the ETH sent
|
|
687
|
+
/// with the call is returned to REVLoans and forwarded to the borrower.
|
|
688
|
+
/// The reverting terminal should NOT hold any ETH.
|
|
689
|
+
function test_feePaymentFailure_nativeToken_revertingTerminalHoldsNoETH() public {
|
|
690
|
+
_mockRevertingFeeTerminal(JBConstants.NATIVE_TOKEN);
|
|
691
|
+
|
|
692
|
+
uint256 revertingTerminalBalanceBefore = address(REVERTING_TERMINAL).balance;
|
|
693
|
+
|
|
694
|
+
_borrowNative(USER, 10e18, 25);
|
|
695
|
+
|
|
696
|
+
// The reverting terminal should not have received any ETH.
|
|
697
|
+
assertEq(
|
|
698
|
+
address(REVERTING_TERMINAL).balance, revertingTerminalBalanceBefore, "Reverting terminal should hold no ETH"
|
|
699
|
+
);
|
|
700
|
+
|
|
701
|
+
// No ETH stuck in loans contract.
|
|
702
|
+
assertEq(address(LOANS_CONTRACT).balance, 0, "No ETH stuck in loans contract");
|
|
703
|
+
}
|
|
704
|
+
}
|