@rev-net/core-v6 0.0.37 → 0.0.39

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 (107) 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 +60 -65
  9. package/src/REVHiddenTokens.sol +2 -2
  10. package/src/REVLoans.sol +17 -10
  11. package/src/REVOwner.sol +121 -14
  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/ADMINISTRATION.md +0 -73
  16. package/ARCHITECTURE.md +0 -116
  17. package/AUDIT_INSTRUCTIONS.md +0 -90
  18. package/RISKS.md +0 -107
  19. package/SKILLS.md +0 -46
  20. package/STYLE_GUIDE.md +0 -610
  21. package/USER_JOURNEYS.md +0 -195
  22. package/foundry.lock +0 -11
  23. package/slither-ci.config.json +0 -10
  24. package/sphinx.lock +0 -507
  25. package/test/REV.integrations.t.sol +0 -573
  26. package/test/REVAutoIssuanceFuzz.t.sol +0 -328
  27. package/test/REVDeployerRegressions.t.sol +0 -396
  28. package/test/REVInvincibility.t.sol +0 -1371
  29. package/test/REVInvincibilityHandler.sol +0 -387
  30. package/test/REVLifecycle.t.sol +0 -420
  31. package/test/REVLoans.invariants.t.sol +0 -724
  32. package/test/REVLoansAttacks.t.sol +0 -816
  33. package/test/REVLoansFeeRecovery.t.sol +0 -783
  34. package/test/REVLoansFindings.t.sol +0 -711
  35. package/test/REVLoansRegressions.t.sol +0 -364
  36. package/test/REVLoansSourceFeeRecovery.t.sol +0 -517
  37. package/test/REVLoansSourced.t.sol +0 -1839
  38. package/test/REVLoansUnSourced.t.sol +0 -409
  39. package/test/TestAuditFixVerification.t.sol +0 -675
  40. package/test/TestBurnHeldTokens.t.sol +0 -394
  41. package/test/TestCEIPattern.t.sol +0 -508
  42. package/test/TestCashOutCallerValidation.t.sol +0 -452
  43. package/test/TestConversionDocumentation.t.sol +0 -365
  44. package/test/TestCrossCurrencyReclaim.t.sol +0 -610
  45. package/test/TestCrossSourceReallocation.t.sol +0 -361
  46. package/test/TestERC2771MetaTx.t.sol +0 -585
  47. package/test/TestEmptyBuybackSpecs.t.sol +0 -300
  48. package/test/TestFlashLoanSurplus.t.sol +0 -365
  49. package/test/TestHiddenTokens.t.sol +0 -474
  50. package/test/TestHookArrayOOB.t.sol +0 -278
  51. package/test/TestLiquidationBehavior.t.sol +0 -398
  52. package/test/TestLoanSourceRotation.t.sol +0 -553
  53. package/test/TestLoansCashOutDelay.t.sol +0 -493
  54. package/test/TestLongTailEconomics.t.sol +0 -677
  55. package/test/TestLowFindings.t.sol +0 -677
  56. package/test/TestMixedFixes.t.sol +0 -593
  57. package/test/TestPermit2Signatures.t.sol +0 -683
  58. package/test/TestReallocationSandwich.t.sol +0 -412
  59. package/test/TestRevnetRegressions.t.sol +0 -350
  60. package/test/TestSplitWeightAdjustment.t.sol +0 -527
  61. package/test/TestSplitWeightE2E.t.sol +0 -605
  62. package/test/TestSplitWeightFork.t.sol +0 -855
  63. package/test/TestStageTransitionBorrowable.t.sol +0 -301
  64. package/test/TestSwapTerminalPermission.t.sol +0 -262
  65. package/test/TestTerminalEncodingInHash.t.sol +0 -326
  66. package/test/TestUint112Overflow.t.sol +0 -311
  67. package/test/TestZeroAmountLoanGuard.t.sol +0 -378
  68. package/test/TestZeroRepayment.t.sol +0 -354
  69. package/test/audit/CrossChainBuybackRouteMismatch.t.sol +0 -184
  70. package/test/audit/HiddenSupplyCashout.t.sol +0 -61
  71. package/test/audit/LoanIdOverflowGuard.t.sol +0 -523
  72. package/test/audit/NemesisVerification.t.sol +0 -97
  73. package/test/audit/OperatorDelegation.t.sol +0 -356
  74. package/test/audit/PhantomSurplusTerminal.t.sol +0 -367
  75. package/test/audit/REVOwnerCurrencyMismatch.t.sol +0 -188
  76. package/test/audit/REVOwnerRemoteSurplusCurrencyMismatch.t.sol +0 -140
  77. package/test/audit/ReallocatePermission.t.sol +0 -363
  78. package/test/audit/RemoteLoanAccountingGap.t.sol +0 -74
  79. package/test/audit/SupportsInterfaceTest.t.sol +0 -51
  80. package/test/audit/TestFeeAllowanceLeak.t.sol +0 -197
  81. package/test/audit/TestLoansAndDeployerFixes.t.sol +0 -576
  82. package/test/fork/ForkTestBase.sol +0 -727
  83. package/test/fork/TestAutoIssuanceFork.t.sol +0 -148
  84. package/test/fork/TestCashOutFork.t.sol +0 -253
  85. package/test/fork/TestIssuanceDecayFork.t.sol +0 -158
  86. package/test/fork/TestLoanAdversarialFork.t.sol +0 -744
  87. package/test/fork/TestLoanBorrowFork.t.sol +0 -163
  88. package/test/fork/TestLoanCrossRulesetFork.t.sol +0 -308
  89. package/test/fork/TestLoanERC20Fork.t.sol +0 -459
  90. package/test/fork/TestLoanLiquidationFork.t.sol +0 -135
  91. package/test/fork/TestLoanReallocateFork.t.sol +0 -113
  92. package/test/fork/TestLoanRepayFork.t.sol +0 -188
  93. package/test/fork/TestLoanTransferFork.t.sol +0 -143
  94. package/test/fork/TestPermit2PaymentFork.t.sol +0 -300
  95. package/test/fork/TestSplitWeightFork.t.sol +0 -189
  96. package/test/helpers/MaliciousContracts.sol +0 -247
  97. package/test/helpers/REVEmpty721Config.sol +0 -45
  98. package/test/mock/MockBuybackCashOutRecorder.sol +0 -84
  99. package/test/mock/MockBuybackDataHook.sol +0 -112
  100. package/test/mock/MockBuybackDataHookMintPath.sol +0 -68
  101. package/test/mock/MockSuckerRegistry.sol +0 -17
  102. package/test/regression/TestBurnPermissionRequired.t.sol +0 -294
  103. package/test/regression/TestCashOutBuybackFeeLeak.t.sol +0 -232
  104. package/test/regression/TestCrossRevnetLiquidation.t.sol +0 -255
  105. package/test/regression/TestCumulativeLoanCounter.t.sol +0 -361
  106. package/test/regression/TestLiquidateGapHandling.t.sol +0 -394
  107. 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
- }