@rev-net/core-v6 0.0.14 → 0.0.16
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/ADMINISTRATION.md +5 -1
- package/ARCHITECTURE.md +69 -11
- package/AUDIT_INSTRUCTIONS.md +90 -7
- package/CHANGE_LOG.md +16 -3
- package/README.md +32 -7
- package/RISKS.md +26 -14
- package/SKILLS.md +168 -46
- package/STYLE_GUIDE.md +1 -1
- package/USER_JOURNEYS.md +20 -6
- package/foundry.toml +7 -0
- package/package.json +9 -10
- package/script/Deploy.s.sol +80 -16
- package/script/helpers/RevnetCoreDeploymentLib.sol +1 -1
- package/src/REVDeployer.sol +73 -21
- package/src/REVLoans.sol +27 -6
- package/test/REV.integrations.t.sol +1 -1
- package/test/REVAutoIssuanceFuzz.t.sol +1 -1
- package/test/REVDeployerRegressions.t.sol +7 -4
- package/test/REVInvincibility.t.sol +7 -19
- package/test/REVInvincibilityHandler.sol +1 -1
- package/test/REVLifecycle.t.sol +1 -1
- package/test/REVLoans.invariants.t.sol +1 -1
- package/test/REVLoansAttacks.t.sol +20 -12
- package/test/REVLoansFeeRecovery.t.sol +20 -12
- package/test/REVLoansFindings.t.sol +20 -12
- package/test/REVLoansRegressions.t.sol +20 -12
- package/test/REVLoansSourceFeeRecovery.t.sol +1 -1
- package/test/REVLoansSourced.t.sol +1 -9
- package/test/REVLoansUnSourced.t.sol +1 -1
- package/test/TestBurnHeldTokens.t.sol +1 -1
- package/test/TestCEIPattern.t.sol +1 -1
- package/test/TestCashOutCallerValidation.t.sol +75 -1
- package/test/TestConversionDocumentation.t.sol +1 -1
- package/test/TestCrossCurrencyReclaim.t.sol +1 -1
- package/test/TestCrossSourceReallocation.t.sol +1 -1
- package/test/TestERC2771MetaTx.t.sol +1 -1
- package/test/TestEmptyBuybackSpecs.t.sol +1 -1
- package/test/TestFlashLoanSurplus.t.sol +1 -1
- package/test/TestHookArrayOOB.t.sol +1 -1
- package/test/TestLiquidationBehavior.t.sol +1 -1
- package/test/TestLoanSourceRotation.t.sol +1 -1
- package/test/TestLongTailEconomics.t.sol +1 -1
- package/test/TestLowFindings.t.sol +4 -2
- package/test/TestMixedFixes.t.sol +7 -5
- package/test/TestPermit2Signatures.t.sol +1 -1
- package/test/TestReallocationSandwich.t.sol +1 -1
- package/test/TestRevnetRegressions.t.sol +1 -1
- package/test/TestSplitWeightAdjustment.t.sol +11 -6
- package/test/TestSplitWeightE2E.t.sol +1 -1
- package/test/TestSplitWeightFork.t.sol +9 -10
- package/test/TestStageTransitionBorrowable.t.sol +1 -1
- package/test/TestSwapTerminalPermission.t.sol +1 -1
- package/test/TestUint112Overflow.t.sol +1 -1
- package/test/TestZeroRepayment.t.sol +1 -1
- package/test/audit/LoanIdOverflowGuard.t.sol +497 -0
- package/test/fork/ForkTestBase.sol +8 -11
- package/test/fork/TestAutoIssuanceFork.t.sol +148 -0
- package/test/fork/TestCashOutFork.t.sol +23 -22
- package/test/fork/TestIssuanceDecayFork.t.sol +158 -0
- package/test/fork/TestLoanBorrowFork.t.sol +1 -1
- package/test/fork/TestLoanCrossRulesetFork.t.sol +1 -1
- package/test/fork/TestLoanERC20Fork.t.sol +463 -0
- package/test/fork/TestLoanLiquidationFork.t.sol +1 -1
- package/test/fork/TestLoanReallocateFork.t.sol +1 -1
- package/test/fork/TestLoanRepayFork.t.sol +3 -3
- package/test/fork/TestLoanTransferFork.t.sol +1 -1
- package/test/fork/TestPermit2PaymentFork.t.sol +299 -0
- package/test/fork/TestSplitWeightFork.t.sol +1 -1
- package/test/helpers/MaliciousContracts.sol +37 -23
- package/test/mock/MockBuybackCashOutRecorder.sol +82 -0
- package/test/mock/MockBuybackDataHook.sol +51 -7
- package/test/mock/MockBuybackDataHookMintPath.sol +1 -1
- package/test/regression/TestBurnPermissionRequired.t.sol +1 -1
- package/test/regression/TestCashOutBuybackFeeLeak.t.sol +205 -0
- package/test/regression/TestCrossRevnetLiquidation.t.sol +1 -1
- package/test/regression/TestCumulativeLoanCounter.t.sol +1 -1
- package/test/regression/TestLiquidateGapHandling.t.sol +1 -1
- package/test/regression/TestZeroPriceFeed.t.sol +1 -1
package/src/REVDeployer.sol
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
pragma solidity 0.8.26;
|
|
2
|
+
pragma solidity ^0.8.26;
|
|
3
3
|
|
|
4
4
|
import {IJB721TiersHook} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHook.sol";
|
|
5
5
|
import {IJB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHookDeployer.sol";
|
|
@@ -41,7 +41,7 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
|
41
41
|
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
|
42
42
|
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
|
|
43
43
|
import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
|
|
44
|
-
import {mulDiv} from "@prb/math/src/Common.sol";
|
|
44
|
+
import {mulDiv, sqrt} from "@prb/math/src/Common.sol";
|
|
45
45
|
|
|
46
46
|
import {IREVDeployer} from "./interfaces/IREVDeployer.sol";
|
|
47
47
|
import {REVAutoIssuance} from "./structs/REVAutoIssuance.sol";
|
|
@@ -235,6 +235,10 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
|
|
|
235
235
|
/// @notice Determine how a cash out from a revnet should be processed.
|
|
236
236
|
/// @dev This function is part of `IJBRulesetDataHook`, and gets called before the revnet processes a cash out.
|
|
237
237
|
/// @dev If a sucker is cashing out, no taxes or fees are imposed.
|
|
238
|
+
/// @dev REVDeployer is intentionally not registered as a feeless address. The protocol fee (2.5%) applies on top
|
|
239
|
+
/// of the rev fee — this is by design. The fee hook spec amount sent to `afterCashOutRecordedWith` will have the
|
|
240
|
+
/// protocol fee deducted by the terminal before reaching this contract, so the rev fee is computed on the
|
|
241
|
+
/// post-protocol-fee amount.
|
|
238
242
|
/// @param context Standard Juicebox cash out context. See `JBBeforeCashOutRecordedContext`.
|
|
239
243
|
/// @return cashOutTaxRate The cash out tax rate, which influences the amount of terminal tokens which get cashed
|
|
240
244
|
/// out.
|
|
@@ -271,18 +275,19 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
|
|
|
271
275
|
IJBTerminal feeTerminal = DIRECTORY.primaryTerminalOf({projectId: FEE_REVNET_ID, token: context.surplus.token});
|
|
272
276
|
|
|
273
277
|
// If there's no cash out tax (100% cash out tax rate), if there's no fee terminal, or if the beneficiary is
|
|
274
|
-
// feeless (e.g. the router terminal routing value between projects),
|
|
278
|
+
// feeless (e.g. the router terminal routing value between projects), proxy directly to the buyback hook.
|
|
275
279
|
if (context.cashOutTaxRate == 0 || address(feeTerminal) == address(0) || context.beneficiaryIsFeeless) {
|
|
276
|
-
|
|
280
|
+
// slither-disable-next-line unused-return
|
|
281
|
+
return BUYBACK_HOOK.beforeCashOutRecordedWith(context);
|
|
277
282
|
}
|
|
278
283
|
|
|
279
|
-
//
|
|
284
|
+
// Split the cashed-out tokens into a fee portion and a non-fee portion.
|
|
280
285
|
// Micro cash outs (< 40 wei at 2.5% fee) round feeCashOutCount to zero, bypassing the fee.
|
|
281
286
|
// Economically insignificant: the gas cost of the transaction far exceeds the bypassed fee. No fix needed.
|
|
282
287
|
uint256 feeCashOutCount = mulDiv({x: context.cashOutCount, y: FEE, denominator: JBConstants.MAX_FEE});
|
|
283
288
|
uint256 nonFeeCashOutCount = context.cashOutCount - feeCashOutCount;
|
|
284
289
|
|
|
285
|
-
//
|
|
290
|
+
// Calculate how much surplus the non-fee tokens can reclaim via the bonding curve.
|
|
286
291
|
uint256 postFeeReclaimedAmount = JBCashOuts.cashOutFrom({
|
|
287
292
|
surplus: context.surplus.value,
|
|
288
293
|
cashOutCount: nonFeeCashOutCount,
|
|
@@ -290,7 +295,7 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
|
|
|
290
295
|
cashOutTaxRate: context.cashOutTaxRate
|
|
291
296
|
});
|
|
292
297
|
|
|
293
|
-
//
|
|
298
|
+
// Calculate how much the fee tokens reclaim from the remaining surplus after the non-fee reclaim.
|
|
294
299
|
uint256 feeAmount = JBCashOuts.cashOutFrom({
|
|
295
300
|
surplus: context.surplus.value - postFeeReclaimedAmount,
|
|
296
301
|
cashOutCount: feeCashOutCount,
|
|
@@ -298,15 +303,39 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
|
|
|
298
303
|
cashOutTaxRate: context.cashOutTaxRate
|
|
299
304
|
});
|
|
300
305
|
|
|
301
|
-
//
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
306
|
+
// Build a context for the buyback hook using only the non-fee token count.
|
|
307
|
+
JBBeforeCashOutRecordedContext memory buybackHookContext = context;
|
|
308
|
+
buybackHookContext.cashOutCount = nonFeeCashOutCount;
|
|
309
|
+
|
|
310
|
+
// Let the buyback hook adjust the cash out parameters and optionally return a hook specification.
|
|
311
|
+
JBCashOutHookSpecification[] memory buybackHookSpecifications;
|
|
312
|
+
(cashOutTaxRate, cashOutCount, totalSupply, buybackHookSpecifications) =
|
|
313
|
+
BUYBACK_HOOK.beforeCashOutRecordedWith(buybackHookContext);
|
|
314
|
+
|
|
315
|
+
// If the fee rounds down to zero, return the buyback hook's response directly — no fee to process.
|
|
316
|
+
if (feeAmount == 0) return (cashOutTaxRate, cashOutCount, totalSupply, buybackHookSpecifications);
|
|
317
|
+
|
|
318
|
+
// Build a hook spec that routes the fee amount to this contract's `afterCashOutRecordedWith` for processing.
|
|
319
|
+
JBCashOutHookSpecification memory feeSpec = JBCashOutHookSpecification({
|
|
320
|
+
hook: IJBCashOutHook(address(this)), noop: false, amount: feeAmount, metadata: abi.encode(feeTerminal)
|
|
305
321
|
});
|
|
306
322
|
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
|
|
323
|
+
// Compose the final hook specifications: buyback spec (if any) + fee spec.
|
|
324
|
+
// NOTE: Only buybackHookSpecifications[0] is used. If the buyback hook returns multiple
|
|
325
|
+
// specs, the additional ones are silently dropped. This is intentional — the buyback hook is
|
|
326
|
+
// expected to return at most one spec for the cash-out buyback swap.
|
|
327
|
+
if (buybackHookSpecifications.length > 0) {
|
|
328
|
+
// The buyback hook returned a spec — include it before the fee spec.
|
|
329
|
+
hookSpecifications = new JBCashOutHookSpecification[](2);
|
|
330
|
+
hookSpecifications[0] = buybackHookSpecifications[0];
|
|
331
|
+
hookSpecifications[1] = feeSpec;
|
|
332
|
+
} else {
|
|
333
|
+
// No buyback spec — only the fee spec.
|
|
334
|
+
hookSpecifications = new JBCashOutHookSpecification[](1);
|
|
335
|
+
hookSpecifications[0] = feeSpec;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return (cashOutTaxRate, cashOutCount, totalSupply, hookSpecifications);
|
|
310
339
|
}
|
|
311
340
|
|
|
312
341
|
/// @notice Before a revnet processes an incoming payment, determine the weight and pay hooks to use.
|
|
@@ -565,14 +594,33 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
|
|
|
565
594
|
}
|
|
566
595
|
}
|
|
567
596
|
|
|
568
|
-
/// @notice Try to initialize a Uniswap V4 buyback pool for a terminal token at
|
|
597
|
+
/// @notice Try to initialize a Uniswap V4 buyback pool for a terminal token at its fair issuance price.
|
|
569
598
|
/// @dev Called after the ERC-20 token is deployed so the pool can be initialized in the PoolManager.
|
|
599
|
+
/// Computes `sqrtPriceX96` from `initialIssuance` so the pool starts at the same price as the bonding curve.
|
|
570
600
|
/// Silently catches failures (e.g., if the pool is already initialized).
|
|
571
601
|
/// @param revnetId The ID of the revnet.
|
|
572
602
|
/// @param terminalToken The terminal token to initialize a buyback pool for.
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
603
|
+
/// @param initialIssuance The initial issuance rate (project tokens per terminal token, 18 decimals).
|
|
604
|
+
function _tryInitializeBuybackPoolFor(uint256 revnetId, address terminalToken, uint112 initialIssuance) internal {
|
|
605
|
+
uint160 sqrtPriceX96;
|
|
606
|
+
|
|
607
|
+
if (initialIssuance == 0) {
|
|
608
|
+
sqrtPriceX96 = uint160(1 << 96);
|
|
609
|
+
} else {
|
|
610
|
+
address normalizedTerminalToken = terminalToken == JBConstants.NATIVE_TOKEN ? address(0) : terminalToken;
|
|
611
|
+
address projectToken = address(CONTROLLER.TOKENS().tokenOf(revnetId));
|
|
612
|
+
|
|
613
|
+
if (projectToken == address(0) || projectToken == normalizedTerminalToken) {
|
|
614
|
+
sqrtPriceX96 = uint160(1 << 96);
|
|
615
|
+
} else if (normalizedTerminalToken < projectToken) {
|
|
616
|
+
// token0 = terminal, token1 = project → price = issuance / 1e18
|
|
617
|
+
sqrtPriceX96 = uint160(sqrt(mulDiv(uint256(initialIssuance), 1 << 192, 1e18)));
|
|
618
|
+
} else {
|
|
619
|
+
// token0 = project, token1 = terminal → price = 1e18 / issuance
|
|
620
|
+
sqrtPriceX96 = uint160(sqrt(mulDiv(1e18, 1 << 192, uint256(initialIssuance))));
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
576
624
|
// slither-disable-next-line calls-loop
|
|
577
625
|
try BUYBACK_HOOK.initializePoolFor({
|
|
578
626
|
projectId: revnetId,
|
|
@@ -580,7 +628,7 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
|
|
|
580
628
|
tickSpacing: DEFAULT_BUYBACK_TICK_SPACING,
|
|
581
629
|
twapWindow: DEFAULT_BUYBACK_TWAP_WINDOW,
|
|
582
630
|
terminalToken: terminalToken,
|
|
583
|
-
sqrtPriceX96:
|
|
631
|
+
sqrtPriceX96: sqrtPriceX96
|
|
584
632
|
}) {}
|
|
585
633
|
catch {} // Pool may already be initialized — that's OK.
|
|
586
634
|
}
|
|
@@ -838,8 +886,10 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
|
|
|
838
886
|
|
|
839
887
|
/// @notice Change a revnet's split operator.
|
|
840
888
|
/// @dev Only a revnet's current split operator can set a new split operator.
|
|
889
|
+
/// @dev Passing `address(0)` as `newSplitOperator` relinquishes operator powers permanently — the permissions
|
|
890
|
+
/// are granted to the zero address (which cannot execute transactions), effectively burning them.
|
|
841
891
|
/// @param revnetId The ID of the revnet to set the split operator of.
|
|
842
|
-
/// @param newSplitOperator The new split operator's address.
|
|
892
|
+
/// @param newSplitOperator The new split operator's address. Use `address(0)` to relinquish operator powers.
|
|
843
893
|
function setSplitOperatorOf(uint256 revnetId, address newSplitOperator) external override {
|
|
844
894
|
// Enforce permissions.
|
|
845
895
|
_checkIfIsSplitOperatorOf({revnetId: revnetId, operator: _msgSender()});
|
|
@@ -1067,7 +1117,9 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
|
|
|
1067
1117
|
for (uint256 j; j < terminalConfiguration.accountingContextsToAccept.length; j++) {
|
|
1068
1118
|
// slither-disable-next-line calls-loop
|
|
1069
1119
|
_tryInitializeBuybackPoolFor({
|
|
1070
|
-
revnetId: revnetId,
|
|
1120
|
+
revnetId: revnetId,
|
|
1121
|
+
terminalToken: terminalConfiguration.accountingContextsToAccept[j].token,
|
|
1122
|
+
initialIssuance: configuration.stageConfigurations[0].initialIssuance
|
|
1071
1123
|
});
|
|
1072
1124
|
}
|
|
1073
1125
|
}
|
package/src/REVLoans.sol
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
pragma solidity 0.8.26;
|
|
2
|
+
pragma solidity ^0.8.26;
|
|
3
3
|
|
|
4
4
|
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
|
|
5
5
|
import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol";
|
|
@@ -339,11 +339,7 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
339
339
|
|
|
340
340
|
// Get the surplus of all the revnet's terminals in terms of the native currency.
|
|
341
341
|
uint256 totalSurplus = JBSurplus.currentSurplusOf({
|
|
342
|
-
projectId: revnetId,
|
|
343
|
-
terminals: terminals,
|
|
344
|
-
accountingContexts: new JBAccountingContext[](0),
|
|
345
|
-
decimals: decimals,
|
|
346
|
-
currency: currency
|
|
342
|
+
projectId: revnetId, terminals: terminals, tokens: new address[](0), decimals: decimals, currency: currency
|
|
347
343
|
});
|
|
348
344
|
|
|
349
345
|
// Get the total amount the revnet currently has loaned out, in terms of the native currency with 18
|
|
@@ -442,6 +438,9 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
442
438
|
}
|
|
443
439
|
|
|
444
440
|
/// @notice Generate a ID for a loan given a revnet ID and a loan number within that revnet.
|
|
441
|
+
/// @dev The multiplication and addition can theoretically overflow a uint256 if revnetId or loanNumber are
|
|
442
|
+
/// astronomically large. In practice this is infeasible — it would require 2^256 loans or project IDs, far
|
|
443
|
+
/// beyond any realistic usage. No overflow check is needed.
|
|
445
444
|
/// @param revnetId The ID of the revnet to generate a loan ID for.
|
|
446
445
|
/// @param loanNumber The loan number of the loan within the revnet.
|
|
447
446
|
/// @return The token ID of the 721.
|
|
@@ -581,6 +580,9 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
581
580
|
);
|
|
582
581
|
}
|
|
583
582
|
|
|
583
|
+
// Prevent the loan number from exceeding the ID namespace for this revnet.
|
|
584
|
+
if (totalLoansBorrowedFor[revnetId] >= _ONE_TRILLION) revert REVLoans_LoanIdOverflow();
|
|
585
|
+
|
|
584
586
|
// Get a reference to the loan ID.
|
|
585
587
|
loanId = _generateLoanId({revnetId: revnetId, loanNumber: ++totalLoansBorrowedFor[revnetId]});
|
|
586
588
|
|
|
@@ -991,6 +993,10 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
991
993
|
_beforeTransferTo({to: address(feeTerminal), token: loan.source.token, amount: revFeeAmount});
|
|
992
994
|
|
|
993
995
|
// Pay the fee. Send the REV to the beneficiary. If fee payment fails, give the amount back to the borrower.
|
|
996
|
+
// NOTE: When terminal.pay() reverts (e.g. due to a misconfigured terminal or paused payments),
|
|
997
|
+
// the REV fee is refunded to the borrower, resulting in an effectively interest-free loan for the
|
|
998
|
+
// REV fee portion. This is acceptable — it requires a broken/misconfigured fee terminal and the
|
|
999
|
+
// borrower still pays the source fee and protocol fee.
|
|
994
1000
|
// slither-disable-next-line arbitrary-send-eth,unused-return
|
|
995
1001
|
try feeTerminal.pay{value: payValue}({
|
|
996
1002
|
projectId: REV_ID,
|
|
@@ -1026,6 +1032,11 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
1026
1032
|
|
|
1027
1033
|
/// @notice Allows the owner of a loan to pay it back, add more, or receive returned collateral no longer necessary
|
|
1028
1034
|
/// to support the loan.
|
|
1035
|
+
/// @dev CEI ordering note: `totalCollateralOf` is not incremented until `_addCollateralTo` executes,
|
|
1036
|
+
/// which happens after the external calls in `_addTo` (useAllowanceOf, fee payment, transfer). A reentrant
|
|
1037
|
+
/// `borrowFrom` during those calls would see a lower `totalCollateralOf`, potentially passing collateral
|
|
1038
|
+
/// checks that should fail. Practically infeasible — requires an adversarial pay hook on the revnet's own
|
|
1039
|
+
/// terminal that calls back into `borrowFrom`, which is not a realistic deployment configuration.
|
|
1029
1040
|
/// @param loan The loan being adjusted.
|
|
1030
1041
|
/// @param revnetId The ID of the revnet the loan is being adjusted in.
|
|
1031
1042
|
/// @param newBorrowAmount The new amount of the loan, denominated in the token of the source's accounting
|
|
@@ -1096,6 +1107,10 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
1096
1107
|
});
|
|
1097
1108
|
|
|
1098
1109
|
// Pay the fee. If it fails, reclaim the allowance and give the amount back to the borrower.
|
|
1110
|
+
// NOTE: When terminal.pay() reverts (e.g. due to a misconfigured terminal or paused payments),
|
|
1111
|
+
// the source fee is refunded to the borrower, resulting in an effectively interest-free loan for the
|
|
1112
|
+
// source fee portion. This is acceptable — it requires a broken/misconfigured source terminal and
|
|
1113
|
+
// the borrower still pays the REV fee and protocol fee.
|
|
1099
1114
|
// slither-disable-next-line unused-return,arbitrary-send-eth
|
|
1100
1115
|
try loan.source.terminal.pay{value: payValue}({
|
|
1101
1116
|
projectId: revnetId,
|
|
@@ -1167,6 +1182,9 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
1167
1182
|
revert REVLoans_ReallocatingMoreCollateralThanBorrowedAmountAllows(borrowAmount, loan.amount);
|
|
1168
1183
|
}
|
|
1169
1184
|
|
|
1185
|
+
// Prevent the loan number from exceeding the ID namespace for this revnet.
|
|
1186
|
+
if (totalLoansBorrowedFor[revnetId] >= _ONE_TRILLION) revert REVLoans_LoanIdOverflow();
|
|
1187
|
+
|
|
1170
1188
|
// Get a reference to the replacement loan ID.
|
|
1171
1189
|
reallocatedLoanId = _generateLoanId({revnetId: revnetId, loanNumber: ++totalLoansBorrowedFor[revnetId]});
|
|
1172
1190
|
|
|
@@ -1296,6 +1314,9 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
1296
1314
|
return (loanId, paidOffSnapshot);
|
|
1297
1315
|
} else {
|
|
1298
1316
|
// Make a new loan with the remaining amount and collateral.
|
|
1317
|
+
// Prevent the loan number from exceeding the ID namespace for this revnet.
|
|
1318
|
+
if (totalLoansBorrowedFor[revnetId] >= _ONE_TRILLION) revert REVLoans_LoanIdOverflow();
|
|
1319
|
+
|
|
1299
1320
|
// Get a reference to the replacement loan ID.
|
|
1300
1321
|
uint256 paidOffLoanId = _generateLoanId({revnetId: revnetId, loanNumber: ++totalLoansBorrowedFor[revnetId]});
|
|
1301
1322
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
pragma solidity 0.8.26;
|
|
2
|
+
pragma solidity ^0.8.26;
|
|
3
3
|
|
|
4
4
|
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
5
5
|
import "forge-std/Test.sol";
|
|
@@ -138,7 +138,8 @@ contract REVDeployerRegressions is TestBaseWorkflow {
|
|
|
138
138
|
assertEq(correctIndex, 0, "buyback hook should use index 0 when no tiered hook");
|
|
139
139
|
|
|
140
140
|
// Write to the correct index (no revert)
|
|
141
|
-
specs[correctIndex] =
|
|
141
|
+
specs[correctIndex] =
|
|
142
|
+
JBPayHookSpecification({hook: IJBPayHook(address(0xbeef)), noop: false, amount: 1 ether, metadata: ""});
|
|
142
143
|
}
|
|
143
144
|
|
|
144
145
|
/// @notice Verify both hooks present works fine (no OOB).
|
|
@@ -150,8 +151,10 @@ contract REVDeployerRegressions is TestBaseWorkflow {
|
|
|
150
151
|
assertEq(arraySize, 2, "array should be size 2");
|
|
151
152
|
|
|
152
153
|
JBPayHookSpecification[] memory specs = new JBPayHookSpecification[](arraySize);
|
|
153
|
-
specs[0] =
|
|
154
|
-
|
|
154
|
+
specs[0] =
|
|
155
|
+
JBPayHookSpecification({hook: IJBPayHook(address(0xdead)), noop: false, amount: 1 ether, metadata: ""});
|
|
156
|
+
specs[1] =
|
|
157
|
+
JBPayHookSpecification({hook: IJBPayHook(address(0xbeef)), noop: false, amount: 2 ether, metadata: ""});
|
|
155
158
|
}
|
|
156
159
|
|
|
157
160
|
//*********************************************************************//
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
pragma solidity 0.8.26;
|
|
2
|
+
pragma solidity ^0.8.26;
|
|
3
3
|
|
|
4
4
|
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
5
5
|
import "forge-std/Test.sol";
|
|
@@ -368,7 +368,8 @@ contract REVInvincibility_PropertyTests is TestBaseWorkflow {
|
|
|
368
368
|
|
|
369
369
|
// Verify safe write
|
|
370
370
|
JBPayHookSpecification[] memory specs = new JBPayHookSpecification[](arraySize);
|
|
371
|
-
specs[correctIndex] =
|
|
371
|
+
specs[correctIndex] =
|
|
372
|
+
JBPayHookSpecification({hook: IJBPayHook(address(0xbeef)), noop: false, amount: 1 ether, metadata: ""});
|
|
372
373
|
}
|
|
373
374
|
|
|
374
375
|
/// @notice Reentrancy — _adjust calls terminal.pay() BEFORE writing loan state.
|
|
@@ -909,12 +910,8 @@ contract REVInvincibility_PropertyTests is TestBaseWorkflow {
|
|
|
909
910
|
// Record fee project balance before cash-out
|
|
910
911
|
uint256 feeBalanceBefore;
|
|
911
912
|
{
|
|
912
|
-
JBAccountingContext[] memory feeCtx = new JBAccountingContext[](1);
|
|
913
|
-
feeCtx[0] = JBAccountingContext({
|
|
914
|
-
token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
915
|
-
});
|
|
916
913
|
feeBalanceBefore = jbMultiTerminal()
|
|
917
|
-
.currentSurplusOf(FEE_PROJECT_ID,
|
|
914
|
+
.currentSurplusOf(FEE_PROJECT_ID, new address[](0), 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
|
|
918
915
|
}
|
|
919
916
|
|
|
920
917
|
// Cash out
|
|
@@ -935,12 +932,8 @@ contract REVInvincibility_PropertyTests is TestBaseWorkflow {
|
|
|
935
932
|
// because both the terminal fee AND the revnet fee route to it
|
|
936
933
|
uint256 feeBalanceAfter;
|
|
937
934
|
{
|
|
938
|
-
JBAccountingContext[] memory feeCtx = new JBAccountingContext[](1);
|
|
939
|
-
feeCtx[0] = JBAccountingContext({
|
|
940
|
-
token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
941
|
-
});
|
|
942
935
|
feeBalanceAfter = jbMultiTerminal()
|
|
943
|
-
.currentSurplusOf(FEE_PROJECT_ID,
|
|
936
|
+
.currentSurplusOf(FEE_PROJECT_ID, new address[](0), 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
|
|
944
937
|
}
|
|
945
938
|
|
|
946
939
|
// Fee project should have received fees from the cash-out
|
|
@@ -1206,13 +1199,8 @@ contract REVInvincibility_Invariants is StdInvariant, TestBaseWorkflow {
|
|
|
1206
1199
|
function invariant_REV_1_surplusCoversLoans() public {
|
|
1207
1200
|
uint256 totalBorrowed = LOANS_CONTRACT.totalBorrowedFrom(REVNET_ID, jbMultiTerminal(), JBConstants.NATIVE_TOKEN);
|
|
1208
1201
|
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
1212
|
-
});
|
|
1213
|
-
|
|
1214
|
-
uint256 storeBalance =
|
|
1215
|
-
jbMultiTerminal().currentSurplusOf(REVNET_ID, ctxArray, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
|
|
1202
|
+
uint256 storeBalance = jbMultiTerminal()
|
|
1203
|
+
.currentSurplusOf(REVNET_ID, new address[](0), 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
|
|
1216
1204
|
|
|
1217
1205
|
// Note: storeBalance is surplus (after payout limits), but the terminal holds at least this much
|
|
1218
1206
|
// The total borrowed should not exceed what the terminal can cover
|
package/test/REVLifecycle.t.sol
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
pragma solidity 0.8.26;
|
|
2
|
+
pragma solidity ^0.8.26;
|
|
3
3
|
|
|
4
4
|
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
5
5
|
import "forge-std/Test.sol";
|
|
@@ -42,6 +42,8 @@ import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressReg
|
|
|
42
42
|
import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
|
|
43
43
|
import {REVEmpty721Config} from "./helpers/REVEmpty721Config.sol";
|
|
44
44
|
import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
|
|
45
|
+
import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
|
|
46
|
+
import {JBPayHookSpecification} from "@bananapus/core-v6/src/structs/JBPayHookSpecification.sol";
|
|
45
47
|
|
|
46
48
|
/// @notice A malicious terminal that re-enters REVLoans during fee payment in _adjust().
|
|
47
49
|
/// @dev Reentrancy during pay() callback in _adjust.
|
|
@@ -128,17 +130,7 @@ contract ReentrantTerminal is ERC165, IJBPayoutTerminal {
|
|
|
128
130
|
override
|
|
129
131
|
{}
|
|
130
132
|
|
|
131
|
-
function currentSurplusOf(
|
|
132
|
-
uint256,
|
|
133
|
-
JBAccountingContext[] memory,
|
|
134
|
-
uint256,
|
|
135
|
-
uint256
|
|
136
|
-
)
|
|
137
|
-
external
|
|
138
|
-
pure
|
|
139
|
-
override
|
|
140
|
-
returns (uint256)
|
|
141
|
-
{
|
|
133
|
+
function currentSurplusOf(uint256, address[] calldata, uint256, uint256) external pure override returns (uint256) {
|
|
142
134
|
return 0;
|
|
143
135
|
}
|
|
144
136
|
|
|
@@ -168,6 +160,22 @@ contract ReentrantTerminal is ERC165, IJBPayoutTerminal {
|
|
|
168
160
|
return 0;
|
|
169
161
|
}
|
|
170
162
|
|
|
163
|
+
function previewPayFor(
|
|
164
|
+
uint256,
|
|
165
|
+
address,
|
|
166
|
+
uint256,
|
|
167
|
+
address,
|
|
168
|
+
bytes calldata
|
|
169
|
+
)
|
|
170
|
+
external
|
|
171
|
+
pure
|
|
172
|
+
override
|
|
173
|
+
returns (JBRuleset memory, uint256, uint256, JBPayHookSpecification[] memory)
|
|
174
|
+
{
|
|
175
|
+
JBRuleset memory ruleset;
|
|
176
|
+
return (ruleset, 0, 0, new JBPayHookSpecification[](0));
|
|
177
|
+
}
|
|
178
|
+
|
|
171
179
|
function supportsInterface(bytes4 interfaceId) public view override(ERC165, IERC165) returns (bool) {
|
|
172
180
|
return interfaceId == type(IJBTerminal).interfaceId || interfaceId == type(IJBPayoutTerminal).interfaceId
|
|
173
181
|
|| super.supportsInterface(interfaceId);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
pragma solidity 0.8.26;
|
|
2
|
+
pragma solidity ^0.8.26;
|
|
3
3
|
|
|
4
4
|
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
5
5
|
import "forge-std/Test.sol";
|
|
@@ -42,6 +42,8 @@ import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStor
|
|
|
42
42
|
import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
|
|
43
43
|
import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
|
|
44
44
|
import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
|
|
45
|
+
import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
|
|
46
|
+
import {JBPayHookSpecification} from "@bananapus/core-v6/src/structs/JBPayHookSpecification.sol";
|
|
45
47
|
import {REVEmpty721Config} from "./helpers/REVEmpty721Config.sol";
|
|
46
48
|
|
|
47
49
|
/// @notice A terminal mock that always reverts on pay(), used to simulate fee payment failure.
|
|
@@ -87,17 +89,7 @@ contract RevertingFeeTerminal is ERC165, IJBPayoutTerminal {
|
|
|
87
89
|
override
|
|
88
90
|
{}
|
|
89
91
|
|
|
90
|
-
function currentSurplusOf(
|
|
91
|
-
uint256,
|
|
92
|
-
JBAccountingContext[] memory,
|
|
93
|
-
uint256,
|
|
94
|
-
uint256
|
|
95
|
-
)
|
|
96
|
-
external
|
|
97
|
-
pure
|
|
98
|
-
override
|
|
99
|
-
returns (uint256)
|
|
100
|
-
{
|
|
92
|
+
function currentSurplusOf(uint256, address[] calldata, uint256, uint256) external pure override returns (uint256) {
|
|
101
93
|
return 0;
|
|
102
94
|
}
|
|
103
95
|
|
|
@@ -127,6 +119,22 @@ contract RevertingFeeTerminal is ERC165, IJBPayoutTerminal {
|
|
|
127
119
|
return 0;
|
|
128
120
|
}
|
|
129
121
|
|
|
122
|
+
function previewPayFor(
|
|
123
|
+
uint256,
|
|
124
|
+
address,
|
|
125
|
+
uint256,
|
|
126
|
+
address,
|
|
127
|
+
bytes calldata
|
|
128
|
+
)
|
|
129
|
+
external
|
|
130
|
+
pure
|
|
131
|
+
override
|
|
132
|
+
returns (JBRuleset memory, uint256, uint256, JBPayHookSpecification[] memory)
|
|
133
|
+
{
|
|
134
|
+
JBRuleset memory ruleset;
|
|
135
|
+
return (ruleset, 0, 0, new JBPayHookSpecification[](0));
|
|
136
|
+
}
|
|
137
|
+
|
|
130
138
|
function supportsInterface(bytes4 interfaceId) public view override(ERC165, IERC165) returns (bool) {
|
|
131
139
|
return interfaceId == type(IJBTerminal).interfaceId || interfaceId == type(IJBPayoutTerminal).interfaceId
|
|
132
140
|
|| super.supportsInterface(interfaceId);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
pragma solidity 0.8.26;
|
|
2
|
+
pragma solidity ^0.8.26;
|
|
3
3
|
|
|
4
4
|
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
5
5
|
import "forge-std/Test.sol";
|
|
@@ -42,6 +42,8 @@ import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
|
|
|
42
42
|
import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
|
|
43
43
|
import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
|
|
44
44
|
import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
|
|
45
|
+
import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
|
|
46
|
+
import {JBPayHookSpecification} from "@bananapus/core-v6/src/structs/JBPayHookSpecification.sol";
|
|
45
47
|
import {REVEmpty721Config} from "./helpers/REVEmpty721Config.sol";
|
|
46
48
|
|
|
47
49
|
/// @notice A fake terminal that returns garbage accounting contexts.
|
|
@@ -91,17 +93,7 @@ contract GarbageTerminal is ERC165, IJBPayoutTerminal {
|
|
|
91
93
|
override
|
|
92
94
|
{}
|
|
93
95
|
|
|
94
|
-
function currentSurplusOf(
|
|
95
|
-
uint256,
|
|
96
|
-
JBAccountingContext[] memory,
|
|
97
|
-
uint256,
|
|
98
|
-
uint256
|
|
99
|
-
)
|
|
100
|
-
external
|
|
101
|
-
pure
|
|
102
|
-
override
|
|
103
|
-
returns (uint256)
|
|
104
|
-
{
|
|
96
|
+
function currentSurplusOf(uint256, address[] calldata, uint256, uint256) external pure override returns (uint256) {
|
|
105
97
|
return 0;
|
|
106
98
|
}
|
|
107
99
|
|
|
@@ -130,6 +122,22 @@ contract GarbageTerminal is ERC165, IJBPayoutTerminal {
|
|
|
130
122
|
return 0;
|
|
131
123
|
}
|
|
132
124
|
|
|
125
|
+
function previewPayFor(
|
|
126
|
+
uint256,
|
|
127
|
+
address,
|
|
128
|
+
uint256,
|
|
129
|
+
address,
|
|
130
|
+
bytes calldata
|
|
131
|
+
)
|
|
132
|
+
external
|
|
133
|
+
pure
|
|
134
|
+
override
|
|
135
|
+
returns (JBRuleset memory, uint256, uint256, JBPayHookSpecification[] memory)
|
|
136
|
+
{
|
|
137
|
+
JBRuleset memory ruleset;
|
|
138
|
+
return (ruleset, 0, 0, new JBPayHookSpecification[](0));
|
|
139
|
+
}
|
|
140
|
+
|
|
133
141
|
function supportsInterface(bytes4 interfaceId) public view override(ERC165, IERC165) returns (bool) {
|
|
134
142
|
return interfaceId == type(IJBTerminal).interfaceId || interfaceId == type(IJBPayoutTerminal).interfaceId
|
|
135
143
|
|| super.supportsInterface(interfaceId);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
pragma solidity 0.8.26;
|
|
2
|
+
pragma solidity ^0.8.26;
|
|
3
3
|
|
|
4
4
|
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
5
5
|
import "forge-std/Test.sol";
|
|
@@ -38,6 +38,8 @@ import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStor
|
|
|
38
38
|
import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
|
|
39
39
|
import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
|
|
40
40
|
import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
|
|
41
|
+
import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
|
|
42
|
+
import {JBPayHookSpecification} from "@bananapus/core-v6/src/structs/JBPayHookSpecification.sol";
|
|
41
43
|
import {REVEmpty721Config} from "./helpers/REVEmpty721Config.sol";
|
|
42
44
|
|
|
43
45
|
/// @notice A fake terminal that tracks whether useAllowanceOf was called.
|
|
@@ -92,17 +94,7 @@ contract FakeTerminal is ERC165, IJBPayoutTerminal {
|
|
|
92
94
|
override
|
|
93
95
|
{}
|
|
94
96
|
|
|
95
|
-
function currentSurplusOf(
|
|
96
|
-
uint256,
|
|
97
|
-
JBAccountingContext[] memory,
|
|
98
|
-
uint256,
|
|
99
|
-
uint256
|
|
100
|
-
)
|
|
101
|
-
external
|
|
102
|
-
pure
|
|
103
|
-
override
|
|
104
|
-
returns (uint256)
|
|
105
|
-
{
|
|
97
|
+
function currentSurplusOf(uint256, address[] calldata, uint256, uint256) external pure override returns (uint256) {
|
|
106
98
|
return 0;
|
|
107
99
|
}
|
|
108
100
|
|
|
@@ -131,6 +123,22 @@ contract FakeTerminal is ERC165, IJBPayoutTerminal {
|
|
|
131
123
|
return 0;
|
|
132
124
|
}
|
|
133
125
|
|
|
126
|
+
function previewPayFor(
|
|
127
|
+
uint256,
|
|
128
|
+
address,
|
|
129
|
+
uint256,
|
|
130
|
+
address,
|
|
131
|
+
bytes calldata
|
|
132
|
+
)
|
|
133
|
+
external
|
|
134
|
+
pure
|
|
135
|
+
override
|
|
136
|
+
returns (JBRuleset memory, uint256, uint256, JBPayHookSpecification[] memory)
|
|
137
|
+
{
|
|
138
|
+
JBRuleset memory ruleset;
|
|
139
|
+
return (ruleset, 0, 0, new JBPayHookSpecification[](0));
|
|
140
|
+
}
|
|
141
|
+
|
|
134
142
|
function supportsInterface(bytes4 interfaceId) public view override(ERC165, IERC165) returns (bool) {
|
|
135
143
|
return interfaceId == type(IJBTerminal).interfaceId || interfaceId == type(IJBPayoutTerminal).interfaceId
|
|
136
144
|
|| super.supportsInterface(interfaceId);
|