@rev-net/core-v6 0.0.11 → 0.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ADMINISTRATION.md +7 -7
- package/ARCHITECTURE.md +11 -11
- package/AUDIT_INSTRUCTIONS.md +295 -0
- package/CHANGE_LOG.md +316 -0
- package/README.md +9 -6
- package/RISKS.md +180 -35
- package/SKILLS.md +9 -11
- package/STYLE_GUIDE.md +14 -1
- package/USER_JOURNEYS.md +489 -0
- package/package.json +9 -9
- package/script/Deploy.s.sol +124 -40
- package/script/helpers/RevnetCoreDeploymentLib.sol +19 -6
- package/src/REVDeployer.sol +183 -175
- package/src/REVLoans.sol +65 -28
- package/src/interfaces/IREVDeployer.sol +25 -23
- package/src/structs/REV721TiersHookFlags.sol +1 -0
- package/src/structs/REVAutoIssuance.sol +1 -0
- package/src/structs/REVBaseline721HookConfig.sol +1 -0
- package/src/structs/REVConfig.sol +1 -0
- package/src/structs/REVCroptopAllowedPost.sol +1 -0
- package/src/structs/REVDeploy721TiersHookConfig.sol +13 -14
- package/src/structs/REVDescription.sol +1 -0
- package/src/structs/REVLoan.sol +1 -0
- package/src/structs/REVLoanSource.sol +1 -0
- package/src/structs/REVStageConfig.sol +1 -0
- package/src/structs/REVSuckerDeploymentConfig.sol +1 -0
- package/test/REV.integrations.t.sol +148 -19
- package/test/REVAutoIssuanceFuzz.t.sol +31 -6
- package/test/REVDeployerRegressions.t.sol +47 -9
- package/test/REVInvincibility.t.sol +83 -19
- package/test/REVInvincibilityHandler.sol +29 -0
- package/test/REVLifecycle.t.sol +36 -6
- package/test/REVLoans.invariants.t.sol +64 -10
- package/test/REVLoansAttacks.t.sol +54 -9
- package/test/REVLoansFeeRecovery.t.sol +61 -15
- package/test/REVLoansFindings.t.sol +42 -9
- package/test/REVLoansRegressions.t.sol +33 -6
- package/test/REVLoansSourceFeeRecovery.t.sol +491 -0
- package/test/REVLoansSourced.t.sol +79 -17
- package/test/REVLoansUnSourced.t.sol +61 -10
- package/test/TestBurnHeldTokens.t.sol +47 -11
- package/test/TestCEIPattern.t.sol +37 -6
- package/test/TestCashOutCallerValidation.t.sol +41 -8
- package/test/TestConversionDocumentation.t.sol +50 -13
- package/test/TestCrossCurrencyReclaim.t.sol +584 -0
- package/test/TestCrossSourceReallocation.t.sol +37 -6
- package/test/TestERC2771MetaTx.t.sol +557 -0
- package/test/TestEmptyBuybackSpecs.t.sol +45 -10
- package/test/TestFlashLoanSurplus.t.sol +39 -7
- package/test/TestHookArrayOOB.t.sol +42 -13
- package/test/TestLiquidationBehavior.t.sol +37 -7
- package/test/TestLoanSourceRotation.t.sol +525 -0
- package/test/TestLongTailEconomics.t.sol +651 -0
- package/test/TestLowFindings.t.sol +80 -8
- package/test/TestMixedFixes.t.sol +43 -9
- package/test/TestPermit2Signatures.t.sol +657 -0
- package/test/TestReallocationSandwich.t.sol +384 -0
- package/test/TestRevnetRegressions.t.sol +324 -0
- package/test/TestSplitWeightAdjustment.t.sol +52 -13
- package/test/TestSplitWeightE2E.t.sol +53 -18
- package/test/TestSplitWeightFork.t.sol +66 -21
- package/test/TestStageTransitionBorrowable.t.sol +38 -6
- package/test/TestSwapTerminalPermission.t.sol +37 -7
- package/test/TestUint112Overflow.t.sol +39 -6
- package/test/TestZeroRepayment.t.sol +37 -6
- package/test/fork/ForkTestBase.sol +66 -17
- package/test/fork/TestCashOutFork.t.sol +9 -3
- package/test/fork/TestLoanBorrowFork.t.sol +1 -0
- package/test/fork/TestLoanCrossRulesetFork.t.sol +11 -3
- package/test/fork/TestLoanLiquidationFork.t.sol +1 -0
- package/test/fork/TestLoanReallocateFork.t.sol +1 -0
- package/test/fork/TestLoanRepayFork.t.sol +1 -0
- package/test/fork/TestLoanTransferFork.t.sol +133 -0
- package/test/fork/TestSplitWeightFork.t.sol +3 -0
- package/test/helpers/REVEmpty721Config.sol +46 -0
- package/test/mock/MockBuybackDataHook.sol +1 -0
- package/test/regression/TestBurnPermissionRequired.t.sol +267 -0
- package/test/regression/TestCrossRevnetLiquidation.t.sol +228 -0
- package/test/regression/TestCumulativeLoanCounter.t.sol +38 -8
- package/test/regression/TestLiquidateGapHandling.t.sol +40 -8
- package/test/regression/TestZeroPriceFeed.t.sol +396 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
pragma solidity 0.8.26;
|
|
3
3
|
|
|
4
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
4
5
|
import "./ForkTestBase.sol";
|
|
5
|
-
import {
|
|
6
|
+
import {REVEmpty721Config} from "../helpers/REVEmpty721Config.sol";
|
|
6
7
|
|
|
7
8
|
/// @notice Fork tests for revnet cash-out scenarios with real Uniswap V4 buyback hook.
|
|
8
9
|
///
|
|
@@ -203,8 +204,13 @@ contract TestCashOutFork is ForkTestBase {
|
|
|
203
204
|
(REVConfig memory cfg, JBTerminalConfig[] memory tc, REVSuckerDeploymentConfig memory sdc) =
|
|
204
205
|
_buildMinimalConfig(5000);
|
|
205
206
|
cfg.stageConfigurations[0].startsAtOrAfter = uint40(block.timestamp - 1);
|
|
206
|
-
uint256 delayRevnet = REV_DEPLOYER.deployFor({
|
|
207
|
-
revnetId: 0,
|
|
207
|
+
(uint256 delayRevnet,) = REV_DEPLOYER.deployFor({
|
|
208
|
+
revnetId: 0,
|
|
209
|
+
configuration: cfg,
|
|
210
|
+
terminalConfigurations: tc,
|
|
211
|
+
suckerDeploymentConfiguration: sdc,
|
|
212
|
+
tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
|
|
213
|
+
allowedPosts: REVEmpty721Config.emptyAllowedPosts()
|
|
208
214
|
});
|
|
209
215
|
_setupPool(delayRevnet, 10_000 ether);
|
|
210
216
|
_payRevnet(delayRevnet, PAYER, 1 ether);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
pragma solidity 0.8.26;
|
|
3
3
|
|
|
4
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
4
5
|
import "./ForkTestBase.sol";
|
|
5
|
-
import {
|
|
6
|
+
import {REVEmpty721Config} from "../helpers/REVEmpty721Config.sol";
|
|
6
7
|
|
|
7
8
|
/// @notice Fork tests for loan lifecycle spanning multiple revnet stages (rulesets).
|
|
8
9
|
///
|
|
@@ -52,6 +53,7 @@ contract TestLoanCrossRulesetFork is ForkTestBase {
|
|
|
52
53
|
|
|
53
54
|
// Stage 2: low tax — starts after STAGE_DURATION.
|
|
54
55
|
stages[1] = REVStageConfig({
|
|
56
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
55
57
|
startsAtOrAfter: uint40(block.timestamp + STAGE_DURATION),
|
|
56
58
|
autoIssuances: new REVAutoIssuance[](0),
|
|
57
59
|
splitPercent: 0,
|
|
@@ -64,6 +66,7 @@ contract TestLoanCrossRulesetFork is ForkTestBase {
|
|
|
64
66
|
});
|
|
65
67
|
|
|
66
68
|
cfg = REVConfig({
|
|
69
|
+
// forge-lint: disable-next-line(named-struct-fields)
|
|
67
70
|
description: REVDescription("CrossStage", "XSTG", "ipfs://xstage", "XSTG_SALT"),
|
|
68
71
|
baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
69
72
|
splitOperator: multisig(),
|
|
@@ -85,8 +88,13 @@ contract TestLoanCrossRulesetFork is ForkTestBase {
|
|
|
85
88
|
(REVConfig memory cfg, JBTerminalConfig[] memory tc, REVSuckerDeploymentConfig memory sdc) =
|
|
86
89
|
_buildTwoStageConfig(7000, 2000);
|
|
87
90
|
|
|
88
|
-
revnetId = REV_DEPLOYER.deployFor({
|
|
89
|
-
revnetId: 0,
|
|
91
|
+
(revnetId,) = REV_DEPLOYER.deployFor({
|
|
92
|
+
revnetId: 0,
|
|
93
|
+
configuration: cfg,
|
|
94
|
+
terminalConfigurations: tc,
|
|
95
|
+
suckerDeploymentConfiguration: sdc,
|
|
96
|
+
tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
|
|
97
|
+
allowedPosts: REVEmpty721Config.emptyAllowedPosts()
|
|
90
98
|
});
|
|
91
99
|
|
|
92
100
|
// Set up pool at 1:1 (mint path wins).
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.26;
|
|
3
|
+
|
|
4
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
5
|
+
import "./ForkTestBase.sol";
|
|
6
|
+
|
|
7
|
+
/// @notice Fork tests for transferring loan NFTs and repaying from the new owner.
|
|
8
|
+
///
|
|
9
|
+
/// Covers: transfer + repay by new owner, original owner rejection after transfer, transfer + partial repay.
|
|
10
|
+
///
|
|
11
|
+
/// Run with: FOUNDRY_PROFILE=fork forge test --match-contract TestLoanTransferFork -vvv
|
|
12
|
+
contract TestLoanTransferFork is ForkTestBase {
|
|
13
|
+
uint256 revnetId;
|
|
14
|
+
uint256 loanId;
|
|
15
|
+
REVLoan loan;
|
|
16
|
+
uint256 borrowerTokens;
|
|
17
|
+
|
|
18
|
+
address newOwner = makeAddr("newOwner");
|
|
19
|
+
|
|
20
|
+
function setUp() public override {
|
|
21
|
+
super.setUp();
|
|
22
|
+
|
|
23
|
+
// Deploy fee project + revnet.
|
|
24
|
+
_deployFeeProject(5000);
|
|
25
|
+
revnetId = _deployRevnet(5000);
|
|
26
|
+
_setupPool(revnetId, 10_000 ether);
|
|
27
|
+
|
|
28
|
+
// Pay to create surplus.
|
|
29
|
+
_payRevnet(revnetId, PAYER, 10 ether);
|
|
30
|
+
_payRevnet(revnetId, BORROWER, 5 ether);
|
|
31
|
+
|
|
32
|
+
borrowerTokens = jbTokens().totalBalanceOf(BORROWER, revnetId);
|
|
33
|
+
|
|
34
|
+
// Create a loan with min prepaid fee.
|
|
35
|
+
(loanId, loan) = _createLoan(revnetId, BORROWER, borrowerTokens, LOANS_CONTRACT.MIN_PREPAID_FEE_PERCENT());
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// @notice Transfer loan NFT to a new owner, who then fully repays the loan.
|
|
39
|
+
function test_fork_transferLoan_newOwnerCanRepay() public {
|
|
40
|
+
// Transfer the loan NFT from BORROWER to newOwner.
|
|
41
|
+
vm.prank(BORROWER);
|
|
42
|
+
REVLoans(payable(address(LOANS_CONTRACT))).safeTransferFrom(BORROWER, newOwner, loanId);
|
|
43
|
+
|
|
44
|
+
// Verify newOwner is the loan NFT owner.
|
|
45
|
+
assertEq(_loanOwnerOf(loanId), newOwner, "newOwner should own the loan NFT after transfer");
|
|
46
|
+
|
|
47
|
+
// Fund newOwner with ETH for repayment.
|
|
48
|
+
vm.deal(newOwner, 100 ether);
|
|
49
|
+
|
|
50
|
+
JBSingleAllowance memory allowance;
|
|
51
|
+
|
|
52
|
+
// newOwner repays the loan in full.
|
|
53
|
+
vm.prank(newOwner);
|
|
54
|
+
LOANS_CONTRACT.repayLoan{value: loan.amount * 2}({
|
|
55
|
+
loanId: loanId,
|
|
56
|
+
maxRepayBorrowAmount: loan.amount * 2,
|
|
57
|
+
collateralCountToReturn: loan.collateral,
|
|
58
|
+
beneficiary: payable(newOwner),
|
|
59
|
+
allowance: allowance
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Loan NFT should be burned after full repay.
|
|
63
|
+
vm.expectRevert();
|
|
64
|
+
_loanOwnerOf(loanId);
|
|
65
|
+
|
|
66
|
+
// Collateral tokens should be minted to newOwner (the beneficiary).
|
|
67
|
+
uint256 newOwnerTokens = jbTokens().totalBalanceOf(newOwner, revnetId);
|
|
68
|
+
assertEq(newOwnerTokens, borrowerTokens, "collateral should be returned to newOwner");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// @notice After transferring the loan NFT, the original borrower cannot repay.
|
|
72
|
+
function test_fork_transferLoan_originalBorrowerCannotRepay() public {
|
|
73
|
+
// Transfer the loan NFT from BORROWER to newOwner.
|
|
74
|
+
vm.prank(BORROWER);
|
|
75
|
+
REVLoans(payable(address(LOANS_CONTRACT))).safeTransferFrom(BORROWER, newOwner, loanId);
|
|
76
|
+
|
|
77
|
+
// Fund BORROWER with ETH for the attempted repayment.
|
|
78
|
+
vm.deal(BORROWER, 100 ether);
|
|
79
|
+
|
|
80
|
+
JBSingleAllowance memory allowance;
|
|
81
|
+
|
|
82
|
+
// Original borrower tries to repay — should revert with REVLoans_Unauthorized.
|
|
83
|
+
vm.prank(BORROWER);
|
|
84
|
+
vm.expectRevert(abi.encodeWithSelector(REVLoans.REVLoans_Unauthorized.selector, BORROWER, newOwner));
|
|
85
|
+
LOANS_CONTRACT.repayLoan{value: loan.amount * 2}({
|
|
86
|
+
loanId: loanId,
|
|
87
|
+
maxRepayBorrowAmount: loan.amount * 2,
|
|
88
|
+
collateralCountToReturn: loan.collateral,
|
|
89
|
+
beneficiary: payable(BORROWER),
|
|
90
|
+
allowance: allowance
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/// @notice Transfer loan NFT, new owner does a partial repay — old loan burned, new loan minted to new owner.
|
|
95
|
+
function test_fork_transferLoan_newOwnerPartialRepay() public {
|
|
96
|
+
// Transfer the loan NFT from BORROWER to newOwner.
|
|
97
|
+
vm.prank(BORROWER);
|
|
98
|
+
REVLoans(payable(address(LOANS_CONTRACT))).safeTransferFrom(BORROWER, newOwner, loanId);
|
|
99
|
+
|
|
100
|
+
// Fund newOwner with ETH for repayment.
|
|
101
|
+
vm.deal(newOwner, 100 ether);
|
|
102
|
+
|
|
103
|
+
uint256 halfCollateral = loan.collateral / 2;
|
|
104
|
+
|
|
105
|
+
JBSingleAllowance memory allowance;
|
|
106
|
+
|
|
107
|
+
// newOwner partially repays the loan (return half the collateral).
|
|
108
|
+
vm.prank(newOwner);
|
|
109
|
+
(uint256 newLoanId, REVLoan memory newLoan) = LOANS_CONTRACT.repayLoan{value: loan.amount * 2}({
|
|
110
|
+
loanId: loanId,
|
|
111
|
+
maxRepayBorrowAmount: loan.amount * 2,
|
|
112
|
+
collateralCountToReturn: halfCollateral,
|
|
113
|
+
beneficiary: payable(newOwner),
|
|
114
|
+
allowance: allowance
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Original loan NFT should be burned.
|
|
118
|
+
vm.expectRevert();
|
|
119
|
+
_loanOwnerOf(loanId);
|
|
120
|
+
|
|
121
|
+
// New loan should exist with reduced collateral.
|
|
122
|
+
assertGt(newLoanId, 0, "new loan should be created");
|
|
123
|
+
assertEq(newLoan.collateral, loan.collateral - halfCollateral, "new loan collateral should be reduced");
|
|
124
|
+
assertLt(newLoan.amount, loan.amount, "new loan borrow amount should be less");
|
|
125
|
+
|
|
126
|
+
// New loan NFT should be owned by newOwner.
|
|
127
|
+
assertEq(_loanOwnerOf(newLoanId), newOwner, "new loan NFT should be owned by newOwner");
|
|
128
|
+
|
|
129
|
+
// Half collateral should be returned to newOwner.
|
|
130
|
+
uint256 newOwnerTokens = jbTokens().totalBalanceOf(newOwner, revnetId);
|
|
131
|
+
assertEq(newOwnerTokens, halfCollateral, "half collateral should be returned to newOwner");
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
pragma solidity 0.8.26;
|
|
3
3
|
|
|
4
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
4
5
|
import "./ForkTestBase.sol";
|
|
5
6
|
|
|
6
7
|
/// @notice Fork tests verifying that revnet 721 tier splits + real Uniswap V4 buyback hook produce correct token
|
|
@@ -50,6 +51,7 @@ contract TestSplitWeightFork is ForkTestBase {
|
|
|
50
51
|
vm.stopPrank();
|
|
51
52
|
|
|
52
53
|
// Add full-range liquidity at tick 0 (1:1 price).
|
|
54
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
53
55
|
int256 liquidityDelta = int256(ethLiq / 4);
|
|
54
56
|
vm.prank(address(liqHelper));
|
|
55
57
|
liqHelper.addLiquidity{value: ethLiq}(key, TICK_LOWER, TICK_UPPER, liquidityDelta);
|
|
@@ -66,6 +68,7 @@ contract TestSplitWeightFork is ForkTestBase {
|
|
|
66
68
|
uint160 sqrtPriceLimit = TickMath.getSqrtPriceAtTick(76_000);
|
|
67
69
|
|
|
68
70
|
vm.prank(address(liqHelper));
|
|
71
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
69
72
|
liqHelper.swap(key, zeroForOne, -int256(swapAmount), sqrtPriceLimit);
|
|
70
73
|
|
|
71
74
|
// Read the post-swap tick for the oracle mock.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.0;
|
|
3
|
+
|
|
4
|
+
import {IJB721TokenUriResolver} from "@bananapus/721-hook-v6/src/interfaces/IJB721TokenUriResolver.sol";
|
|
5
|
+
// forge-lint: disable-next-line(unused-import)
|
|
6
|
+
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
7
|
+
import {JB721InitTiersConfig} from "@bananapus/721-hook-v6/src/structs/JB721InitTiersConfig.sol";
|
|
8
|
+
import {JB721TierConfig} from "@bananapus/721-hook-v6/src/structs/JB721TierConfig.sol";
|
|
9
|
+
import {REVBaseline721HookConfig} from "../../src/structs/REVBaseline721HookConfig.sol";
|
|
10
|
+
import {REVDeploy721TiersHookConfig} from "../../src/structs/REVDeploy721TiersHookConfig.sol";
|
|
11
|
+
import {REV721TiersHookFlags} from "../../src/structs/REV721TiersHookFlags.sol";
|
|
12
|
+
import {REVCroptopAllowedPost} from "../../src/structs/REVCroptopAllowedPost.sol";
|
|
13
|
+
|
|
14
|
+
/// @notice Helpers for constructing empty 721 hook configs in tests.
|
|
15
|
+
library REVEmpty721Config {
|
|
16
|
+
function empty721Config(uint32 baseCurrency) internal pure returns (REVDeploy721TiersHookConfig memory) {
|
|
17
|
+
return REVDeploy721TiersHookConfig({
|
|
18
|
+
baseline721HookConfiguration: REVBaseline721HookConfig({
|
|
19
|
+
name: "",
|
|
20
|
+
symbol: "",
|
|
21
|
+
baseUri: "",
|
|
22
|
+
tokenUriResolver: IJB721TokenUriResolver(address(0)),
|
|
23
|
+
contractUri: "",
|
|
24
|
+
tiersConfig: JB721InitTiersConfig({
|
|
25
|
+
tiers: new JB721TierConfig[](0), currency: baseCurrency, decimals: 18
|
|
26
|
+
}),
|
|
27
|
+
reserveBeneficiary: address(0),
|
|
28
|
+
flags: REV721TiersHookFlags({
|
|
29
|
+
noNewTiersWithReserves: false,
|
|
30
|
+
noNewTiersWithVotes: false,
|
|
31
|
+
noNewTiersWithOwnerMinting: false,
|
|
32
|
+
preventOverspending: false
|
|
33
|
+
})
|
|
34
|
+
}),
|
|
35
|
+
salt: bytes32(0),
|
|
36
|
+
preventSplitOperatorAdjustingTiers: false,
|
|
37
|
+
preventSplitOperatorUpdatingMetadata: false,
|
|
38
|
+
preventSplitOperatorMinting: false,
|
|
39
|
+
preventSplitOperatorIncreasingDiscountPercent: false
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function emptyAllowedPosts() internal pure returns (REVCroptopAllowedPost[] memory) {
|
|
44
|
+
return new REVCroptopAllowedPost[](0);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -3,6 +3,7 @@ pragma solidity 0.8.26;
|
|
|
3
3
|
|
|
4
4
|
import {IJBRulesetDataHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetDataHook.sol";
|
|
5
5
|
import {IJBPayHook} from "@bananapus/core-v6/src/interfaces/IJBPayHook.sol";
|
|
6
|
+
// forge-lint: disable-next-line(unused-import)
|
|
6
7
|
import {IJBBuybackHook} from "@bananapus/buyback-hook-v6/src/interfaces/IJBBuybackHook.sol";
|
|
7
8
|
import {JBBeforePayRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforePayRecordedContext.sol";
|
|
8
9
|
import {JBBeforeCashOutRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforeCashOutRecordedContext.sol";
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.26;
|
|
3
|
+
|
|
4
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
5
|
+
import "forge-std/Test.sol";
|
|
6
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
7
|
+
import /* {*} from */ "@bananapus/core-v6/test/helpers/TestBaseWorkflow.sol";
|
|
8
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
9
|
+
import /* {*} from */ "./../../src/REVDeployer.sol";
|
|
10
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
11
|
+
import "@croptop/core-v6/src/CTPublisher.sol";
|
|
12
|
+
import {MockBuybackDataHook} from "./../mock/MockBuybackDataHook.sol";
|
|
13
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
14
|
+
import "@bananapus/core-v6/script/helpers/CoreDeploymentLib.sol";
|
|
15
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
16
|
+
import "@bananapus/721-hook-v6/script/helpers/Hook721DeploymentLib.sol";
|
|
17
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
18
|
+
import "@bananapus/suckers-v6/script/helpers/SuckerDeploymentLib.sol";
|
|
19
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
20
|
+
import "@croptop/core-v6/script/helpers/CroptopDeploymentLib.sol";
|
|
21
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
22
|
+
import "@bananapus/router-terminal-v6/script/helpers/RouterTerminalDeploymentLib.sol";
|
|
23
|
+
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
24
|
+
import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
|
|
25
|
+
import {JBSingleAllowance} from "@bananapus/core-v6/src/structs/JBSingleAllowance.sol";
|
|
26
|
+
import {MockPriceFeed} from "@bananapus/core-v6/test/mock/MockPriceFeed.sol";
|
|
27
|
+
import {MockERC20} from "@bananapus/core-v6/test/mock/MockERC20.sol";
|
|
28
|
+
import {REVLoans} from "../../src/REVLoans.sol";
|
|
29
|
+
import {REVLoan} from "../../src/structs/REVLoan.sol";
|
|
30
|
+
import {REVStageConfig, REVAutoIssuance} from "../../src/structs/REVStageConfig.sol";
|
|
31
|
+
import {REVLoanSource} from "../../src/structs/REVLoanSource.sol";
|
|
32
|
+
import {REVDescription} from "../../src/structs/REVDescription.sol";
|
|
33
|
+
import {JBSuckerDeployerConfig} from "@bananapus/suckers-v6/src/structs/JBSuckerDeployerConfig.sol";
|
|
34
|
+
import {JBSuckerRegistry} from "@bananapus/suckers-v6/src/JBSuckerRegistry.sol";
|
|
35
|
+
import {JB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
|
|
36
|
+
import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
|
|
37
|
+
import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
|
|
38
|
+
import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
|
|
39
|
+
import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
|
|
40
|
+
import {REVEmpty721Config} from "../helpers/REVEmpty721Config.sol";
|
|
41
|
+
import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
|
|
42
|
+
import {JBPermissioned} from "@bananapus/core-v6/src/abstract/JBPermissioned.sol";
|
|
43
|
+
|
|
44
|
+
/// @notice Validates that borrowFrom() reverts with a clear error when the caller hasn't granted BURN_TOKENS
|
|
45
|
+
/// permission to the REVLoans contract.
|
|
46
|
+
/// @dev Without this upfront check, the transaction would revert deep in JBController.burnTokensOf with a
|
|
47
|
+
/// less informative JBPermissioned_Unauthorized error.
|
|
48
|
+
contract TestBurnPermissionRequired is TestBaseWorkflow {
|
|
49
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
50
|
+
bytes32 REV_DEPLOYER_SALT = "REVDeployer";
|
|
51
|
+
|
|
52
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
53
|
+
REVDeployer REV_DEPLOYER;
|
|
54
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
55
|
+
JB721TiersHook EXAMPLE_HOOK;
|
|
56
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
57
|
+
IJB721TiersHookDeployer HOOK_DEPLOYER;
|
|
58
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
59
|
+
IJB721TiersHookStore HOOK_STORE;
|
|
60
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
61
|
+
IJBAddressRegistry ADDRESS_REGISTRY;
|
|
62
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
63
|
+
REVLoans LOANS_CONTRACT;
|
|
64
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
65
|
+
MockERC20 TOKEN;
|
|
66
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
67
|
+
IJBSuckerRegistry SUCKER_REGISTRY;
|
|
68
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
69
|
+
CTPublisher PUBLISHER;
|
|
70
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
71
|
+
MockBuybackDataHook MOCK_BUYBACK;
|
|
72
|
+
|
|
73
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
74
|
+
uint256 FEE_PROJECT_ID;
|
|
75
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
76
|
+
uint256 REVNET_ID;
|
|
77
|
+
|
|
78
|
+
address user = makeAddr("user");
|
|
79
|
+
|
|
80
|
+
address private constant TRUSTED_FORWARDER = 0xB2b5841DBeF766d4b521221732F9B618fCf34A87;
|
|
81
|
+
|
|
82
|
+
function setUp() public override {
|
|
83
|
+
super.setUp();
|
|
84
|
+
FEE_PROJECT_ID = jbProjects().createFor(multisig());
|
|
85
|
+
SUCKER_REGISTRY = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
|
|
86
|
+
HOOK_STORE = new JB721TiersHookStore();
|
|
87
|
+
EXAMPLE_HOOK = new JB721TiersHook(
|
|
88
|
+
jbDirectory(), jbPermissions(), jbPrices(), jbRulesets(), HOOK_STORE, jbSplits(), multisig()
|
|
89
|
+
);
|
|
90
|
+
ADDRESS_REGISTRY = new JBAddressRegistry();
|
|
91
|
+
HOOK_DEPLOYER = new JB721TiersHookDeployer(EXAMPLE_HOOK, HOOK_STORE, ADDRESS_REGISTRY, multisig());
|
|
92
|
+
PUBLISHER = new CTPublisher(jbDirectory(), jbPermissions(), FEE_PROJECT_ID, multisig());
|
|
93
|
+
MOCK_BUYBACK = new MockBuybackDataHook();
|
|
94
|
+
TOKEN = new MockERC20("1/2 ETH", "1/2");
|
|
95
|
+
MockPriceFeed priceFeed = new MockPriceFeed(1e21, 6);
|
|
96
|
+
vm.prank(multisig());
|
|
97
|
+
jbPrices()
|
|
98
|
+
.addPriceFeedFor(0, uint32(uint160(address(TOKEN))), uint32(uint160(JBConstants.NATIVE_TOKEN)), priceFeed);
|
|
99
|
+
LOANS_CONTRACT = new REVLoans({
|
|
100
|
+
controller: jbController(),
|
|
101
|
+
projects: jbProjects(),
|
|
102
|
+
revId: FEE_PROJECT_ID,
|
|
103
|
+
owner: address(this),
|
|
104
|
+
permit2: permit2(),
|
|
105
|
+
trustedForwarder: TRUSTED_FORWARDER
|
|
106
|
+
});
|
|
107
|
+
REV_DEPLOYER = new REVDeployer{salt: REV_DEPLOYER_SALT}(
|
|
108
|
+
jbController(),
|
|
109
|
+
SUCKER_REGISTRY,
|
|
110
|
+
FEE_PROJECT_ID,
|
|
111
|
+
HOOK_DEPLOYER,
|
|
112
|
+
PUBLISHER,
|
|
113
|
+
IJBBuybackHookRegistry(address(MOCK_BUYBACK)),
|
|
114
|
+
address(LOANS_CONTRACT),
|
|
115
|
+
TRUSTED_FORWARDER
|
|
116
|
+
);
|
|
117
|
+
vm.prank(multisig());
|
|
118
|
+
jbProjects().approve(address(REV_DEPLOYER), FEE_PROJECT_ID);
|
|
119
|
+
_deployFeeProject();
|
|
120
|
+
_deployRevnet();
|
|
121
|
+
vm.deal(user, 100e18);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function _deployFeeProject() internal {
|
|
125
|
+
JBAccountingContext[] memory acc = new JBAccountingContext[](2);
|
|
126
|
+
acc[0] = JBAccountingContext({
|
|
127
|
+
token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
128
|
+
});
|
|
129
|
+
acc[1] = JBAccountingContext({token: address(TOKEN), decimals: 6, currency: uint32(uint160(address(TOKEN)))});
|
|
130
|
+
JBTerminalConfig[] memory tc = new JBTerminalConfig[](1);
|
|
131
|
+
tc[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: acc});
|
|
132
|
+
REVStageConfig[] memory stages = new REVStageConfig[](1);
|
|
133
|
+
JBSplit[] memory splits = new JBSplit[](1);
|
|
134
|
+
splits[0].beneficiary = payable(multisig());
|
|
135
|
+
splits[0].percent = 10_000;
|
|
136
|
+
REVAutoIssuance[] memory ai = new REVAutoIssuance[](1);
|
|
137
|
+
ai[0] = REVAutoIssuance({chainId: uint32(block.chainid), count: uint104(70_000e18), beneficiary: multisig()});
|
|
138
|
+
stages[0] = REVStageConfig({
|
|
139
|
+
startsAtOrAfter: uint40(block.timestamp),
|
|
140
|
+
autoIssuances: ai,
|
|
141
|
+
splitPercent: 2000,
|
|
142
|
+
splits: splits,
|
|
143
|
+
initialIssuance: uint112(1000e18),
|
|
144
|
+
issuanceCutFrequency: 90 days,
|
|
145
|
+
issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
|
|
146
|
+
cashOutTaxRate: 6000,
|
|
147
|
+
extraMetadata: 0
|
|
148
|
+
});
|
|
149
|
+
REVConfig memory cfg = REVConfig({
|
|
150
|
+
// forge-lint: disable-next-line(named-struct-fields)
|
|
151
|
+
description: REVDescription("Revnet", "$REV", "ipfs://test", "REV_TOKEN"),
|
|
152
|
+
baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
153
|
+
splitOperator: multisig(),
|
|
154
|
+
stageConfigurations: stages
|
|
155
|
+
});
|
|
156
|
+
vm.prank(multisig());
|
|
157
|
+
REV_DEPLOYER.deployFor({
|
|
158
|
+
revnetId: FEE_PROJECT_ID,
|
|
159
|
+
configuration: cfg,
|
|
160
|
+
terminalConfigurations: tc,
|
|
161
|
+
suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
|
|
162
|
+
deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256("FEE")
|
|
163
|
+
}),
|
|
164
|
+
tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
|
|
165
|
+
allowedPosts: REVEmpty721Config.emptyAllowedPosts()
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function _deployRevnet() internal {
|
|
170
|
+
JBAccountingContext[] memory acc = new JBAccountingContext[](2);
|
|
171
|
+
acc[0] = JBAccountingContext({
|
|
172
|
+
token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
173
|
+
});
|
|
174
|
+
acc[1] = JBAccountingContext({token: address(TOKEN), decimals: 6, currency: uint32(uint160(address(TOKEN)))});
|
|
175
|
+
JBTerminalConfig[] memory tc = new JBTerminalConfig[](1);
|
|
176
|
+
tc[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: acc});
|
|
177
|
+
REVStageConfig[] memory stages = new REVStageConfig[](1);
|
|
178
|
+
JBSplit[] memory splits = new JBSplit[](1);
|
|
179
|
+
splits[0].beneficiary = payable(multisig());
|
|
180
|
+
splits[0].percent = 10_000;
|
|
181
|
+
REVAutoIssuance[] memory ai = new REVAutoIssuance[](1);
|
|
182
|
+
ai[0] = REVAutoIssuance({chainId: uint32(block.chainid), count: uint104(70_000e18), beneficiary: multisig()});
|
|
183
|
+
stages[0] = REVStageConfig({
|
|
184
|
+
startsAtOrAfter: uint40(block.timestamp),
|
|
185
|
+
autoIssuances: ai,
|
|
186
|
+
splitPercent: 2000,
|
|
187
|
+
splits: splits,
|
|
188
|
+
initialIssuance: uint112(1000e18),
|
|
189
|
+
issuanceCutFrequency: 90 days,
|
|
190
|
+
issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
|
|
191
|
+
cashOutTaxRate: 6000,
|
|
192
|
+
extraMetadata: 0
|
|
193
|
+
});
|
|
194
|
+
REVConfig memory cfg = REVConfig({
|
|
195
|
+
// forge-lint: disable-next-line(named-struct-fields)
|
|
196
|
+
description: REVDescription("NANA", "$NANA", "ipfs://test2", "NANA_TOKEN"),
|
|
197
|
+
baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
198
|
+
splitOperator: multisig(),
|
|
199
|
+
stageConfigurations: stages
|
|
200
|
+
});
|
|
201
|
+
(REVNET_ID,) = REV_DEPLOYER.deployFor({
|
|
202
|
+
revnetId: 0,
|
|
203
|
+
configuration: cfg,
|
|
204
|
+
terminalConfigurations: tc,
|
|
205
|
+
suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
|
|
206
|
+
deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256("NANA")
|
|
207
|
+
}),
|
|
208
|
+
tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
|
|
209
|
+
allowedPosts: REVEmpty721Config.emptyAllowedPosts()
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/// @notice borrowFrom should revert when the caller hasn't granted BURN_TOKENS permission.
|
|
214
|
+
/// @dev The controller enforces this with JBPermissioned_Unauthorized when burnTokensOf is called.
|
|
215
|
+
function test_borrowFrom_revertsWithoutBurnPermission() public {
|
|
216
|
+
// Pay into the revnet to get tokens.
|
|
217
|
+
vm.prank(user);
|
|
218
|
+
uint256 tokenCount =
|
|
219
|
+
jbMultiTerminal().pay{value: 5e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 5e18, user, 0, "", "");
|
|
220
|
+
require(tokenCount > 0, "Should have received tokens");
|
|
221
|
+
|
|
222
|
+
// Attempt to borrow WITHOUT granting BURN_TOKENS permission → should revert.
|
|
223
|
+
REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
|
|
224
|
+
vm.prank(user);
|
|
225
|
+
vm.expectRevert(
|
|
226
|
+
abi.encodeWithSelector(
|
|
227
|
+
JBPermissioned.JBPermissioned_Unauthorized.selector,
|
|
228
|
+
user, // account (the token holder)
|
|
229
|
+
address(LOANS_CONTRACT), // sender (the contract trying to burn)
|
|
230
|
+
REVNET_ID, // projectId
|
|
231
|
+
JBPermissionIds.BURN_TOKENS // permissionId
|
|
232
|
+
)
|
|
233
|
+
);
|
|
234
|
+
LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokenCount, payable(user), 25);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/// @notice borrowFrom should succeed when the caller has granted BURN_TOKENS permission.
|
|
238
|
+
function test_borrowFrom_succeedsWithBurnPermission() public {
|
|
239
|
+
// Pay into the revnet to get tokens.
|
|
240
|
+
vm.prank(user);
|
|
241
|
+
uint256 tokenCount =
|
|
242
|
+
jbMultiTerminal().pay{value: 5e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 5e18, user, 0, "", "");
|
|
243
|
+
require(tokenCount > 0, "Should have received tokens");
|
|
244
|
+
|
|
245
|
+
// Grant BURN_TOKENS permission to the loans contract via the real permissions system.
|
|
246
|
+
uint8[] memory permissionIds = new uint8[](1);
|
|
247
|
+
permissionIds[0] = JBPermissionIds.BURN_TOKENS;
|
|
248
|
+
vm.prank(user);
|
|
249
|
+
jbPermissions()
|
|
250
|
+
.setPermissionsFor({
|
|
251
|
+
account: user,
|
|
252
|
+
permissionsData: JBPermissionsData({
|
|
253
|
+
operator: address(LOANS_CONTRACT), projectId: uint64(REVNET_ID), permissionIds: permissionIds
|
|
254
|
+
})
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// Borrow should now succeed.
|
|
258
|
+
REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
|
|
259
|
+
vm.prank(user);
|
|
260
|
+
(uint256 loanId, REVLoan memory loan) =
|
|
261
|
+
LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokenCount, payable(user), 25);
|
|
262
|
+
|
|
263
|
+
assertTrue(loanId > 0, "Loan ID should be non-zero");
|
|
264
|
+
assertTrue(loan.createdAt > 0, "Loan should be created");
|
|
265
|
+
assertTrue(loan.amount > 0, "Loan amount should be non-zero");
|
|
266
|
+
}
|
|
267
|
+
}
|