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