@rev-net/core-v6 0.0.37 → 0.0.40

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