@rev-net/core-v6 0.0.12 → 0.0.13
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/AUDIT_INSTRUCTIONS.md +295 -0
- package/CHANGE_LOG.md +316 -0
- package/README.md +2 -2
- package/RISKS.md +180 -35
- package/SKILLS.md +1 -1
- package/USER_JOURNEYS.md +489 -0
- package/package.json +9 -9
- package/script/Deploy.s.sol +40 -6
- package/script/helpers/RevnetCoreDeploymentLib.sol +7 -1
- package/src/REVDeployer.sol +63 -47
- package/src/REVLoans.sol +51 -15
- package/src/interfaces/IREVDeployer.sol +0 -1
- package/src/structs/REV721TiersHookFlags.sol +1 -0
- package/src/structs/REVAutoIssuance.sol +1 -0
- package/src/structs/REVBaseline721HookConfig.sol +1 -0
- package/src/structs/REVConfig.sol +1 -0
- package/src/structs/REVCroptopAllowedPost.sol +1 -0
- package/src/structs/REVDeploy721TiersHookConfig.sol +1 -0
- package/src/structs/REVDescription.sol +1 -0
- package/src/structs/REVLoan.sol +1 -0
- package/src/structs/REVLoanSource.sol +1 -0
- package/src/structs/REVStageConfig.sol +1 -0
- package/src/structs/REVSuckerDeploymentConfig.sol +1 -0
- package/test/REV.integrations.t.sol +132 -12
- package/test/REVAutoIssuanceFuzz.t.sol +23 -3
- package/test/REVDeployerRegressions.t.sol +35 -4
- package/test/REVInvincibility.t.sol +58 -8
- package/test/REVInvincibilityHandler.sol +29 -0
- package/test/REVLifecycle.t.sol +28 -3
- package/test/REVLoans.invariants.t.sol +52 -5
- package/test/REVLoansAttacks.t.sol +43 -5
- package/test/REVLoansFeeRecovery.t.sol +50 -11
- package/test/REVLoansFindings.t.sol +27 -3
- package/test/REVLoansRegressions.t.sol +25 -3
- package/test/REVLoansSourceFeeRecovery.t.sol +491 -0
- package/test/REVLoansSourced.t.sol +56 -7
- package/test/REVLoansUnSourced.t.sol +49 -5
- package/test/TestBurnHeldTokens.t.sol +32 -5
- package/test/TestCEIPattern.t.sol +26 -2
- package/test/TestCashOutCallerValidation.t.sol +30 -4
- package/test/TestConversionDocumentation.t.sol +26 -5
- package/test/TestCrossCurrencyReclaim.t.sol +584 -0
- package/test/TestCrossSourceReallocation.t.sol +26 -2
- package/test/TestERC2771MetaTx.t.sol +557 -0
- package/test/TestEmptyBuybackSpecs.t.sol +23 -3
- package/test/TestFlashLoanSurplus.t.sol +28 -3
- package/test/TestHookArrayOOB.t.sol +24 -4
- package/test/TestLiquidationBehavior.t.sol +26 -3
- package/test/TestLoanSourceRotation.t.sol +525 -0
- package/test/TestLongTailEconomics.t.sol +651 -0
- package/test/TestLowFindings.t.sol +65 -2
- package/test/TestMixedFixes.t.sol +28 -3
- package/test/TestPermit2Signatures.t.sol +657 -0
- package/test/TestReallocationSandwich.t.sol +384 -0
- package/test/TestRevnetRegressions.t.sol +324 -0
- package/test/TestSplitWeightAdjustment.t.sol +24 -2
- package/test/TestSplitWeightE2E.t.sol +29 -2
- package/test/TestSplitWeightFork.t.sol +46 -7
- package/test/TestStageTransitionBorrowable.t.sol +24 -2
- package/test/TestSwapTerminalPermission.t.sol +23 -3
- package/test/TestUint112Overflow.t.sol +28 -2
- package/test/TestZeroRepayment.t.sol +26 -2
- package/test/fork/ForkTestBase.sol +46 -3
- package/test/fork/TestCashOutFork.t.sol +1 -1
- package/test/fork/TestLoanBorrowFork.t.sol +1 -0
- package/test/fork/TestLoanCrossRulesetFork.t.sol +3 -1
- package/test/fork/TestLoanLiquidationFork.t.sol +1 -0
- package/test/fork/TestLoanReallocateFork.t.sol +1 -0
- package/test/fork/TestLoanRepayFork.t.sol +1 -0
- package/test/fork/TestLoanTransferFork.t.sol +133 -0
- package/test/fork/TestSplitWeightFork.t.sol +3 -0
- package/test/helpers/REVEmpty721Config.sol +1 -0
- package/test/mock/MockBuybackDataHook.sol +1 -0
- package/test/regression/TestBurnPermissionRequired.t.sol +267 -0
- package/test/regression/TestCrossRevnetLiquidation.t.sol +228 -0
- package/test/regression/TestCumulativeLoanCounter.t.sol +27 -4
- package/test/regression/TestLiquidateGapHandling.t.sol +29 -4
- package/test/regression/TestZeroPriceFeed.t.sol +396 -0
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
pragma solidity 0.8.26;
|
|
3
3
|
|
|
4
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
4
5
|
import "forge-std/Test.sol";
|
|
6
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
5
7
|
import /* {*} from */ "@bananapus/core-v6/test/helpers/TestBaseWorkflow.sol";
|
|
6
|
-
import /* {*} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
|
|
8
|
+
// import /* {*} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
|
|
9
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
7
10
|
import /* {*} from */ "./../../src/REVDeployer.sol";
|
|
11
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
8
12
|
import "@croptop/core-v6/src/CTPublisher.sol";
|
|
9
13
|
import {MockBuybackDataHook} from "./../mock/MockBuybackDataHook.sol";
|
|
14
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
10
15
|
import "@bananapus/core-v6/script/helpers/CoreDeploymentLib.sol";
|
|
16
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
11
17
|
import "@bananapus/721-hook-v6/script/helpers/Hook721DeploymentLib.sol";
|
|
18
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
12
19
|
import "@bananapus/suckers-v6/script/helpers/SuckerDeploymentLib.sol";
|
|
20
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
13
21
|
import "@croptop/core-v6/script/helpers/CroptopDeploymentLib.sol";
|
|
22
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
14
23
|
import "@bananapus/router-terminal-v6/script/helpers/RouterTerminalDeploymentLib.sol";
|
|
15
24
|
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
16
25
|
import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
|
|
@@ -22,7 +31,6 @@ import {REVLoan} from "../../src/structs/REVLoan.sol";
|
|
|
22
31
|
import {REVStageConfig, REVAutoIssuance} from "../../src/structs/REVStageConfig.sol";
|
|
23
32
|
import {REVLoanSource} from "../../src/structs/REVLoanSource.sol";
|
|
24
33
|
import {REVDescription} from "../../src/structs/REVDescription.sol";
|
|
25
|
-
import {IREVLoans} from "../../src/interfaces/IREVLoans.sol";
|
|
26
34
|
import {JBSuckerDeployerConfig} from "@bananapus/suckers-v6/src/structs/JBSuckerDeployerConfig.sol";
|
|
27
35
|
import {JBSuckerRegistry} from "@bananapus/suckers-v6/src/JBSuckerRegistry.sol";
|
|
28
36
|
import {JB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
|
|
@@ -31,32 +39,46 @@ import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStor
|
|
|
31
39
|
import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
|
|
32
40
|
import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
|
|
33
41
|
import {REVEmpty721Config} from "../helpers/REVEmpty721Config.sol";
|
|
34
|
-
import {REVCroptopAllowedPost} from "../../src/structs/REVCroptopAllowedPost.sol";
|
|
35
|
-
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
|
|
36
42
|
|
|
37
43
|
/// @notice liquidateExpiredLoansFrom halts on deleted loan gaps.
|
|
38
44
|
/// @dev Before the fix, the function used `break` when encountering a deleted loan (createdAt == 0),
|
|
39
45
|
/// which stopped the entire iteration. Expired loans after the gap were never liquidated.
|
|
40
46
|
/// After the fix, `continue` is used instead, so the loop skips gaps and keeps processing.
|
|
41
47
|
contract TestLiquidateGapHandling is TestBaseWorkflow {
|
|
48
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
42
49
|
bytes32 REV_DEPLOYER_SALT = "REVDeployer";
|
|
43
50
|
|
|
51
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
44
52
|
REVDeployer REV_DEPLOYER;
|
|
53
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
45
54
|
JB721TiersHook EXAMPLE_HOOK;
|
|
55
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
46
56
|
IJB721TiersHookDeployer HOOK_DEPLOYER;
|
|
57
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
47
58
|
IJB721TiersHookStore HOOK_STORE;
|
|
59
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
48
60
|
IJBAddressRegistry ADDRESS_REGISTRY;
|
|
61
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
49
62
|
REVLoans LOANS_CONTRACT;
|
|
63
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
50
64
|
MockERC20 TOKEN;
|
|
65
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
51
66
|
IJBSuckerRegistry SUCKER_REGISTRY;
|
|
67
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
52
68
|
CTPublisher PUBLISHER;
|
|
69
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
53
70
|
MockBuybackDataHook MOCK_BUYBACK;
|
|
54
71
|
|
|
72
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
55
73
|
uint256 FEE_PROJECT_ID;
|
|
74
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
56
75
|
uint256 REVNET_ID;
|
|
57
76
|
|
|
77
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
58
78
|
address USER1 = makeAddr("user1");
|
|
79
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
59
80
|
address USER2 = makeAddr("user2");
|
|
81
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
60
82
|
address USER3 = makeAddr("user3");
|
|
61
83
|
|
|
62
84
|
address private constant TRUSTED_FORWARDER = 0xB2b5841DBeF766d4b521221732F9B618fCf34A87;
|
|
@@ -131,6 +153,7 @@ contract TestLiquidateGapHandling is TestBaseWorkflow {
|
|
|
131
153
|
extraMetadata: 0
|
|
132
154
|
});
|
|
133
155
|
REVConfig memory cfg = REVConfig({
|
|
156
|
+
// forge-lint: disable-next-line(named-struct-fields)
|
|
134
157
|
description: REVDescription("Revnet", "$REV", "ipfs://test", "REV_TOKEN"),
|
|
135
158
|
baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
136
159
|
splitOperator: multisig(),
|
|
@@ -175,6 +198,7 @@ contract TestLiquidateGapHandling is TestBaseWorkflow {
|
|
|
175
198
|
extraMetadata: 0
|
|
176
199
|
});
|
|
177
200
|
REVConfig memory cfg = REVConfig({
|
|
201
|
+
// forge-lint: disable-next-line(named-struct-fields)
|
|
178
202
|
description: REVDescription("NANA", "$NANA", "ipfs://test2", "NANA_TOKEN"),
|
|
179
203
|
baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
180
204
|
splitOperator: multisig(),
|
|
@@ -295,6 +319,7 @@ contract TestLiquidateGapHandling is TestBaseWorkflow {
|
|
|
295
319
|
/// Loan 1 and 4 should both be liquidated despite the double gap.
|
|
296
320
|
function test_liquidationHandlesMultipleConsecutiveGaps() public {
|
|
297
321
|
// Create 4 loans from the same user (simpler)
|
|
322
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
298
323
|
address USER4 = makeAddr("user4");
|
|
299
324
|
vm.deal(USER4, 100e18);
|
|
300
325
|
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.26;
|
|
3
|
+
|
|
4
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
5
|
+
import "forge-std/Test.sol";
|
|
6
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
7
|
+
import /* {*} from */ "@bananapus/core-v6/test/helpers/TestBaseWorkflow.sol";
|
|
8
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
9
|
+
import /* {*} from */ "./../../src/REVDeployer.sol";
|
|
10
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
11
|
+
import "@croptop/core-v6/src/CTPublisher.sol";
|
|
12
|
+
import {MockBuybackDataHook} from "./../mock/MockBuybackDataHook.sol";
|
|
13
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
14
|
+
import "@bananapus/core-v6/script/helpers/CoreDeploymentLib.sol";
|
|
15
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
16
|
+
import "@bananapus/721-hook-v6/script/helpers/Hook721DeploymentLib.sol";
|
|
17
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
18
|
+
import "@bananapus/suckers-v6/script/helpers/SuckerDeploymentLib.sol";
|
|
19
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
20
|
+
import "@croptop/core-v6/script/helpers/CroptopDeploymentLib.sol";
|
|
21
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
22
|
+
import "@bananapus/router-terminal-v6/script/helpers/RouterTerminalDeploymentLib.sol";
|
|
23
|
+
|
|
24
|
+
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
25
|
+
import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
|
|
26
|
+
import {MockPriceFeed} from "@bananapus/core-v6/test/mock/MockPriceFeed.sol";
|
|
27
|
+
import {MockERC20} from "@bananapus/core-v6/test/mock/MockERC20.sol";
|
|
28
|
+
import {REVLoans} from "../../src/REVLoans.sol";
|
|
29
|
+
import {REVLoan} from "../../src/structs/REVLoan.sol";
|
|
30
|
+
import {REVStageConfig, REVAutoIssuance} from "../../src/structs/REVStageConfig.sol";
|
|
31
|
+
import {REVLoanSource} from "../../src/structs/REVLoanSource.sol";
|
|
32
|
+
import {REVDescription} from "../../src/structs/REVDescription.sol";
|
|
33
|
+
import {IREVLoans} from "../../src/interfaces/IREVLoans.sol";
|
|
34
|
+
import {JBSuckerDeployerConfig} from "@bananapus/suckers-v6/src/structs/JBSuckerDeployerConfig.sol";
|
|
35
|
+
import {JBSuckerRegistry} from "@bananapus/suckers-v6/src/JBSuckerRegistry.sol";
|
|
36
|
+
import {JB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
|
|
37
|
+
import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
|
|
38
|
+
import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
|
|
39
|
+
import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
|
|
40
|
+
import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
|
|
41
|
+
import {REVEmpty721Config} from "../helpers/REVEmpty721Config.sol";
|
|
42
|
+
import {IJBPrices} from "@bananapus/core-v6/src/interfaces/IJBPrices.sol";
|
|
43
|
+
|
|
44
|
+
/// @notice Verifies that `_totalBorrowedFrom` gracefully handles zero-price feeds.
|
|
45
|
+
/// @dev When a cross-currency price feed returns 0 (e.g., inverse truncation at low decimals), the affected source
|
|
46
|
+
/// is skipped rather than reverting with division-by-zero. This prevents a stale or misconfigured price feed from
|
|
47
|
+
/// DoS-ing all loan operations. The tradeoff is that total borrowed is intentionally understated for the affected
|
|
48
|
+
/// source, which is conservative (reduces borrowable amount rather than inflating it).
|
|
49
|
+
contract TestZeroPriceFeed is TestBaseWorkflow {
|
|
50
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
51
|
+
bytes32 REV_DEPLOYER_SALT = "REVDeployer";
|
|
52
|
+
|
|
53
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
54
|
+
REVDeployer REV_DEPLOYER;
|
|
55
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
56
|
+
JB721TiersHook EXAMPLE_HOOK;
|
|
57
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
58
|
+
IJB721TiersHookDeployer HOOK_DEPLOYER;
|
|
59
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
60
|
+
IJB721TiersHookStore HOOK_STORE;
|
|
61
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
62
|
+
IJBAddressRegistry ADDRESS_REGISTRY;
|
|
63
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
64
|
+
REVLoans LOANS_CONTRACT;
|
|
65
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
66
|
+
MockERC20 TOKEN;
|
|
67
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
68
|
+
IJBSuckerRegistry SUCKER_REGISTRY;
|
|
69
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
70
|
+
CTPublisher PUBLISHER;
|
|
71
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
72
|
+
MockBuybackDataHook MOCK_BUYBACK;
|
|
73
|
+
|
|
74
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
75
|
+
uint256 FEE_PROJECT_ID;
|
|
76
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
77
|
+
uint256 REVNET_ID;
|
|
78
|
+
|
|
79
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
80
|
+
address USER = makeAddr("user");
|
|
81
|
+
|
|
82
|
+
address private constant TRUSTED_FORWARDER = 0xB2b5841DBeF766d4b521221732F9B618fCf34A87;
|
|
83
|
+
|
|
84
|
+
/// @notice The price feed address, stored so we can mock it after initial setup.
|
|
85
|
+
MockPriceFeed priceFeed;
|
|
86
|
+
|
|
87
|
+
function setUp() public override {
|
|
88
|
+
super.setUp();
|
|
89
|
+
|
|
90
|
+
FEE_PROJECT_ID = jbProjects().createFor(multisig());
|
|
91
|
+
|
|
92
|
+
SUCKER_REGISTRY = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
|
|
93
|
+
HOOK_STORE = new JB721TiersHookStore();
|
|
94
|
+
EXAMPLE_HOOK = new JB721TiersHook(
|
|
95
|
+
jbDirectory(), jbPermissions(), jbPrices(), jbRulesets(), HOOK_STORE, jbSplits(), multisig()
|
|
96
|
+
);
|
|
97
|
+
ADDRESS_REGISTRY = new JBAddressRegistry();
|
|
98
|
+
HOOK_DEPLOYER = new JB721TiersHookDeployer(EXAMPLE_HOOK, HOOK_STORE, ADDRESS_REGISTRY, multisig());
|
|
99
|
+
PUBLISHER = new CTPublisher(jbDirectory(), jbPermissions(), FEE_PROJECT_ID, multisig());
|
|
100
|
+
MOCK_BUYBACK = new MockBuybackDataHook();
|
|
101
|
+
|
|
102
|
+
// Deploy a 6-decimal ERC-20 token.
|
|
103
|
+
TOKEN = new MockERC20("Stable Token", "STABLE");
|
|
104
|
+
|
|
105
|
+
// Price feed: TOKEN -> ETH. 1 TOKEN (6 dec) = 0.0005 ETH.
|
|
106
|
+
priceFeed = new MockPriceFeed(5e14, 18);
|
|
107
|
+
vm.prank(multisig());
|
|
108
|
+
jbPrices()
|
|
109
|
+
.addPriceFeedFor(0, uint32(uint160(address(TOKEN))), uint32(uint160(JBConstants.NATIVE_TOKEN)), priceFeed);
|
|
110
|
+
|
|
111
|
+
LOANS_CONTRACT = new REVLoans({
|
|
112
|
+
controller: jbController(),
|
|
113
|
+
projects: jbProjects(),
|
|
114
|
+
revId: FEE_PROJECT_ID,
|
|
115
|
+
owner: address(this),
|
|
116
|
+
permit2: permit2(),
|
|
117
|
+
trustedForwarder: TRUSTED_FORWARDER
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
REV_DEPLOYER = new REVDeployer{salt: REV_DEPLOYER_SALT}(
|
|
121
|
+
jbController(),
|
|
122
|
+
SUCKER_REGISTRY,
|
|
123
|
+
FEE_PROJECT_ID,
|
|
124
|
+
HOOK_DEPLOYER,
|
|
125
|
+
PUBLISHER,
|
|
126
|
+
IJBBuybackHookRegistry(address(MOCK_BUYBACK)),
|
|
127
|
+
address(LOANS_CONTRACT),
|
|
128
|
+
TRUSTED_FORWARDER
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
vm.prank(multisig());
|
|
132
|
+
jbProjects().approve(address(REV_DEPLOYER), FEE_PROJECT_ID);
|
|
133
|
+
|
|
134
|
+
_deployFeeProject();
|
|
135
|
+
_deployRevnet();
|
|
136
|
+
|
|
137
|
+
vm.deal(USER, 1000e18);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function _deployFeeProject() internal {
|
|
141
|
+
JBAccountingContext[] memory acc = new JBAccountingContext[](2);
|
|
142
|
+
acc[0] = JBAccountingContext({
|
|
143
|
+
token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
144
|
+
});
|
|
145
|
+
acc[1] = JBAccountingContext({token: address(TOKEN), decimals: 6, currency: uint32(uint160(address(TOKEN)))});
|
|
146
|
+
JBTerminalConfig[] memory tc = new JBTerminalConfig[](1);
|
|
147
|
+
tc[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: acc});
|
|
148
|
+
|
|
149
|
+
JBSplit[] memory splits = new JBSplit[](1);
|
|
150
|
+
splits[0].beneficiary = payable(multisig());
|
|
151
|
+
splits[0].percent = 10_000;
|
|
152
|
+
|
|
153
|
+
REVAutoIssuance[] memory ai = new REVAutoIssuance[](1);
|
|
154
|
+
ai[0] = REVAutoIssuance({chainId: uint32(block.chainid), count: uint104(70_000e18), beneficiary: multisig()});
|
|
155
|
+
|
|
156
|
+
REVStageConfig[] memory stages = new REVStageConfig[](1);
|
|
157
|
+
stages[0] = REVStageConfig({
|
|
158
|
+
startsAtOrAfter: uint40(block.timestamp),
|
|
159
|
+
autoIssuances: ai,
|
|
160
|
+
splitPercent: 2000,
|
|
161
|
+
splits: splits,
|
|
162
|
+
initialIssuance: uint112(1000e18),
|
|
163
|
+
issuanceCutFrequency: 90 days,
|
|
164
|
+
issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
|
|
165
|
+
cashOutTaxRate: 6000,
|
|
166
|
+
extraMetadata: 0
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
REVConfig memory cfg = REVConfig({
|
|
170
|
+
// forge-lint: disable-next-line(named-struct-fields)
|
|
171
|
+
description: REVDescription("Revnet", "$REV", "ipfs://test", "REV_TOKEN"),
|
|
172
|
+
baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
173
|
+
splitOperator: multisig(),
|
|
174
|
+
stageConfigurations: stages
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
vm.prank(multisig());
|
|
178
|
+
REV_DEPLOYER.deployFor({
|
|
179
|
+
revnetId: FEE_PROJECT_ID,
|
|
180
|
+
configuration: cfg,
|
|
181
|
+
terminalConfigurations: tc,
|
|
182
|
+
suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
|
|
183
|
+
deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256("FEE")
|
|
184
|
+
}),
|
|
185
|
+
tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
|
|
186
|
+
allowedPosts: REVEmpty721Config.emptyAllowedPosts()
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function _deployRevnet() internal {
|
|
191
|
+
JBAccountingContext[] memory acc = new JBAccountingContext[](2);
|
|
192
|
+
acc[0] = JBAccountingContext({
|
|
193
|
+
token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
194
|
+
});
|
|
195
|
+
acc[1] = JBAccountingContext({token: address(TOKEN), decimals: 6, currency: uint32(uint160(address(TOKEN)))});
|
|
196
|
+
JBTerminalConfig[] memory tc = new JBTerminalConfig[](1);
|
|
197
|
+
tc[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: acc});
|
|
198
|
+
|
|
199
|
+
JBSplit[] memory splits = new JBSplit[](1);
|
|
200
|
+
splits[0].beneficiary = payable(multisig());
|
|
201
|
+
splits[0].percent = 10_000;
|
|
202
|
+
|
|
203
|
+
REVAutoIssuance[] memory ai = new REVAutoIssuance[](1);
|
|
204
|
+
ai[0] = REVAutoIssuance({chainId: uint32(block.chainid), count: uint104(70_000e18), beneficiary: multisig()});
|
|
205
|
+
|
|
206
|
+
REVStageConfig[] memory stages = new REVStageConfig[](1);
|
|
207
|
+
stages[0] = REVStageConfig({
|
|
208
|
+
startsAtOrAfter: uint40(block.timestamp),
|
|
209
|
+
autoIssuances: ai,
|
|
210
|
+
splitPercent: 2000,
|
|
211
|
+
splits: splits,
|
|
212
|
+
initialIssuance: uint112(1000e18),
|
|
213
|
+
issuanceCutFrequency: 90 days,
|
|
214
|
+
issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
|
|
215
|
+
cashOutTaxRate: 6000,
|
|
216
|
+
extraMetadata: 0
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
REVConfig memory cfg = REVConfig({
|
|
220
|
+
// forge-lint: disable-next-line(named-struct-fields)
|
|
221
|
+
description: REVDescription("NANA", "$NANA", "ipfs://test2", "NANA_TOKEN"),
|
|
222
|
+
baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
223
|
+
splitOperator: multisig(),
|
|
224
|
+
stageConfigurations: stages
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
(REVNET_ID,) = REV_DEPLOYER.deployFor({
|
|
228
|
+
revnetId: 0,
|
|
229
|
+
configuration: cfg,
|
|
230
|
+
terminalConfigurations: tc,
|
|
231
|
+
suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
|
|
232
|
+
deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256("NANA")
|
|
233
|
+
}),
|
|
234
|
+
tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
|
|
235
|
+
allowedPosts: REVEmpty721Config.emptyAllowedPosts()
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/// @notice Helper: mock BURN_TOKENS permission for the loans contract.
|
|
240
|
+
function _mockBurnPermission() internal {
|
|
241
|
+
mockExpect(
|
|
242
|
+
address(jbPermissions()),
|
|
243
|
+
abi.encodeCall(IJBPermissions.hasPermission, (address(LOANS_CONTRACT), USER, REVNET_ID, 11, true, true)),
|
|
244
|
+
abi.encode(true)
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/// @notice Computes the storage slot for balanceOf[terminal][projectId][token] in JBTerminalStore.
|
|
249
|
+
/// @dev balanceOf is at slot 0: mapping(address => mapping(uint256 => mapping(address => uint256))).
|
|
250
|
+
function _terminalStoreBalanceSlot(
|
|
251
|
+
address terminal,
|
|
252
|
+
uint256 projectId,
|
|
253
|
+
address token
|
|
254
|
+
)
|
|
255
|
+
internal
|
|
256
|
+
pure
|
|
257
|
+
returns (bytes32)
|
|
258
|
+
{
|
|
259
|
+
return
|
|
260
|
+
keccak256(abi.encode(token, keccak256(abi.encode(projectId, keccak256(abi.encode(terminal, uint256(0)))))));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
//*********************************************************************//
|
|
264
|
+
// --- Zero Price Feed Tests ----------------------------------------- //
|
|
265
|
+
//*********************************************************************//
|
|
266
|
+
|
|
267
|
+
/// @notice When a cross-currency price feed returns 0, `_totalBorrowedFrom` skips the affected source
|
|
268
|
+
/// rather than reverting. This prevents DoS of all loan operations when a price feed is stale or
|
|
269
|
+
/// misconfigured. The result is an undercount of total borrowed (conservative: reduces borrowable amount).
|
|
270
|
+
///
|
|
271
|
+
/// @dev Methodology: after creating a TOKEN loan source (which registers the source and sets a nonzero
|
|
272
|
+
/// totalBorrowedFrom), we zero out the TOKEN balance in the terminal store via vm.store. This means:
|
|
273
|
+
/// - The surplus calculation skips TOKEN (balance = 0, no price conversion needed)
|
|
274
|
+
/// - But `_totalBorrowedFrom` still has the TOKEN entry and needs cross-currency conversion
|
|
275
|
+
/// When the price feed returns 0, `_totalBorrowedFrom` skips it rather than reverting.
|
|
276
|
+
function test_zeroPriceFeed_doesNotRevert_undercountsTotalBorrowed() public {
|
|
277
|
+
// Step 1: Pay ETH to get revnet tokens BEFORE adding TOKEN liquidity.
|
|
278
|
+
vm.prank(USER);
|
|
279
|
+
uint256 revnetTokens =
|
|
280
|
+
jbMultiTerminal().pay{value: 10e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 10e18, USER, 0, "", "");
|
|
281
|
+
assertGt(revnetTokens, 0, "should receive revnet tokens");
|
|
282
|
+
|
|
283
|
+
// Step 2: Take a small loan from the ETH source.
|
|
284
|
+
uint256 ethCollateral = revnetTokens / 10;
|
|
285
|
+
_mockBurnPermission();
|
|
286
|
+
REVLoanSource memory ethSource = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
|
|
287
|
+
vm.prank(USER);
|
|
288
|
+
LOANS_CONTRACT.borrowFrom(REVNET_ID, ethSource, 0, ethCollateral, payable(USER), 25);
|
|
289
|
+
|
|
290
|
+
// Step 3: Fund the terminal with TOKEN and borrow from TOKEN source.
|
|
291
|
+
uint256 tokenFunding = 1_000_000e6;
|
|
292
|
+
TOKEN.mint(address(this), tokenFunding);
|
|
293
|
+
TOKEN.approve(address(jbMultiTerminal()), tokenFunding);
|
|
294
|
+
jbMultiTerminal().addToBalanceOf(REVNET_ID, address(TOKEN), tokenFunding, false, "", "");
|
|
295
|
+
|
|
296
|
+
uint256 tokenCollateral = revnetTokens / 10;
|
|
297
|
+
_mockBurnPermission();
|
|
298
|
+
REVLoanSource memory tokenSource = REVLoanSource({token: address(TOKEN), terminal: jbMultiTerminal()});
|
|
299
|
+
vm.prank(USER);
|
|
300
|
+
LOANS_CONTRACT.borrowFrom(REVNET_ID, tokenSource, 0, tokenCollateral, payable(USER), 25);
|
|
301
|
+
|
|
302
|
+
// Verify both sources have nonzero totalBorrowedFrom.
|
|
303
|
+
uint256 borrowedFromEth =
|
|
304
|
+
LOANS_CONTRACT.totalBorrowedFrom(REVNET_ID, jbMultiTerminal(), JBConstants.NATIVE_TOKEN);
|
|
305
|
+
uint256 borrowedFromToken = LOANS_CONTRACT.totalBorrowedFrom(REVNET_ID, jbMultiTerminal(), address(TOKEN));
|
|
306
|
+
assertGt(borrowedFromEth, 0, "ETH source should have nonzero borrowed amount");
|
|
307
|
+
assertGt(borrowedFromToken, 0, "TOKEN source should have nonzero borrowed amount");
|
|
308
|
+
|
|
309
|
+
// Step 4: Zero out the TOKEN balance in the terminal store so the surplus calculation
|
|
310
|
+
// skips the TOKEN accounting context (balance == 0 -> no price conversion needed).
|
|
311
|
+
// This isolates the test to only exercise `_totalBorrowedFrom`'s zero-price guard.
|
|
312
|
+
bytes32 tokenBalanceSlot = _terminalStoreBalanceSlot(address(jbMultiTerminal()), REVNET_ID, address(TOKEN));
|
|
313
|
+
vm.store(address(jbTerminalStore()), tokenBalanceSlot, bytes32(uint256(0)));
|
|
314
|
+
|
|
315
|
+
// Verify the TOKEN balance is now 0.
|
|
316
|
+
assertEq(
|
|
317
|
+
jbTerminalStore().balanceOf(address(jbMultiTerminal()), REVNET_ID, address(TOKEN)),
|
|
318
|
+
0,
|
|
319
|
+
"TOKEN balance should be zeroed out"
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
// Step 5: Record the borrowable amount WITH a working price feed.
|
|
323
|
+
vm.prank(USER);
|
|
324
|
+
uint256 freshTokens =
|
|
325
|
+
jbMultiTerminal().pay{value: 1e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 1e18, USER, 0, "", "");
|
|
326
|
+
assertGt(freshTokens, 0, "should receive fresh tokens");
|
|
327
|
+
|
|
328
|
+
uint256 borrowableWithPrice =
|
|
329
|
+
LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, freshTokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
|
|
330
|
+
|
|
331
|
+
// Step 6: Mock the price feed to return 0 for the TOKEN -> ETH conversion.
|
|
332
|
+
// This simulates an inverse price feed truncation scenario where the conversion
|
|
333
|
+
// rounds down to zero (e.g., a feed returning 1e21 at 6 decimals inverts to 0).
|
|
334
|
+
vm.mockCall(address(priceFeed), abi.encodeWithSignature("currentUnitPrice(uint256)"), abi.encode(uint256(0)));
|
|
335
|
+
|
|
336
|
+
// Step 7: Verify borrowableAmountFrom still works (no revert).
|
|
337
|
+
uint256 borrowableWithZeroPrice =
|
|
338
|
+
LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, freshTokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
|
|
339
|
+
|
|
340
|
+
// The call should succeed (not revert), proving the DoS protection works.
|
|
341
|
+
// With zero price, the TOKEN-denominated borrowed amount is skipped in `_totalBorrowedFrom`.
|
|
342
|
+
// This means `totalBorrowed` is understated (only includes ETH source), so
|
|
343
|
+
// `totalSurplus + totalBorrowed` is lower, producing a lower borrowable amount.
|
|
344
|
+
//
|
|
345
|
+
// NOTE: borrowableWithZeroPrice <= borrowableWithPrice because the understated totalBorrowed
|
|
346
|
+
// reduces the effective surplus-plus-debt pool used in the bonding curve calculation.
|
|
347
|
+
// This is the "acceptable tradeoff vs. blocking every borrow/repay" documented in the source.
|
|
348
|
+
assertLe(
|
|
349
|
+
borrowableWithZeroPrice,
|
|
350
|
+
borrowableWithPrice,
|
|
351
|
+
"zero-price undercount should produce equal or lower borrowable amount (conservative)"
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
// Document the undercount: the two amounts should differ since TOKEN debt is omitted.
|
|
355
|
+
emit log_named_uint("borrowable with working price feed", borrowableWithPrice);
|
|
356
|
+
emit log_named_uint("borrowable with zero price feed", borrowableWithZeroPrice);
|
|
357
|
+
emit log_named_uint("undercount delta", borrowableWithPrice - borrowableWithZeroPrice);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/// @notice When only one source exists and it matches the target currency (same currency),
|
|
361
|
+
/// a zero price feed for OTHER currencies has no effect since no cross-currency conversion is needed.
|
|
362
|
+
function test_zeroPriceFeed_noEffectOnSameCurrencySource() public {
|
|
363
|
+
// Step 1: Pay ETH to get revnet tokens.
|
|
364
|
+
vm.prank(USER);
|
|
365
|
+
uint256 revnetTokens =
|
|
366
|
+
jbMultiTerminal().pay{value: 10e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 10e18, USER, 0, "", "");
|
|
367
|
+
|
|
368
|
+
// Step 2: Take a loan from the ETH source only (same currency as baseCurrency).
|
|
369
|
+
_mockBurnPermission();
|
|
370
|
+
REVLoanSource memory ethSource = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
|
|
371
|
+
vm.prank(USER);
|
|
372
|
+
LOANS_CONTRACT.borrowFrom(REVNET_ID, ethSource, 0, revnetTokens / 2, payable(USER), 25);
|
|
373
|
+
|
|
374
|
+
// Step 3: Get borrowable amount.
|
|
375
|
+
vm.prank(USER);
|
|
376
|
+
uint256 freshTokens =
|
|
377
|
+
jbMultiTerminal().pay{value: 1e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 1e18, USER, 0, "", "");
|
|
378
|
+
|
|
379
|
+
uint256 borrowableBefore =
|
|
380
|
+
LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, freshTokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
|
|
381
|
+
|
|
382
|
+
// Step 4: Mock the price feed to return 0 -- this should not affect anything
|
|
383
|
+
// since the only loan source is ETH (same currency, no cross-currency conversion).
|
|
384
|
+
vm.mockCall(address(priceFeed), abi.encodeWithSignature("currentUnitPrice(uint256)"), abi.encode(uint256(0)));
|
|
385
|
+
|
|
386
|
+
uint256 borrowableAfter =
|
|
387
|
+
LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, freshTokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
|
|
388
|
+
|
|
389
|
+
// Same-currency source does not use the price feed, so the amounts should be identical.
|
|
390
|
+
assertEq(
|
|
391
|
+
borrowableAfter,
|
|
392
|
+
borrowableBefore,
|
|
393
|
+
"zero price feed should not affect same-currency loan source calculations"
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
}
|