@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
|
@@ -0,0 +1,205 @@
|
|
|
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
|
+
|
|
13
|
+
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
14
|
+
import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
|
|
15
|
+
import {JBTerminalConfig} from "@bananapus/core-v6/src/structs/JBTerminalConfig.sol";
|
|
16
|
+
import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
|
|
17
|
+
import {JBSuckerDeployerConfig} from "@bananapus/suckers-v6/src/structs/JBSuckerDeployerConfig.sol";
|
|
18
|
+
import {JBBeforeCashOutRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforeCashOutRecordedContext.sol";
|
|
19
|
+
import {JBCashOutHookSpecification} from "@bananapus/core-v6/src/structs/JBCashOutHookSpecification.sol";
|
|
20
|
+
import {JBTokenAmount} from "@bananapus/core-v6/src/structs/JBTokenAmount.sol";
|
|
21
|
+
import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
|
|
22
|
+
import {JB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
|
|
23
|
+
import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
|
|
24
|
+
import {IJB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHookDeployer.sol";
|
|
25
|
+
import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
|
|
26
|
+
import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
|
|
27
|
+
import {JBSuckerRegistry} from "@bananapus/suckers-v6/src/JBSuckerRegistry.sol";
|
|
28
|
+
import {IJBBuybackHookRegistry} from "@bananapus/buyback-hook-v6/src/interfaces/IJBBuybackHookRegistry.sol";
|
|
29
|
+
|
|
30
|
+
import {REVConfig} from "../../src/structs/REVConfig.sol";
|
|
31
|
+
import {REVDescription} from "../../src/structs/REVDescription.sol";
|
|
32
|
+
import {REVStageConfig} from "../../src/structs/REVStageConfig.sol";
|
|
33
|
+
import {REVAutoIssuance} from "../../src/structs/REVAutoIssuance.sol";
|
|
34
|
+
import {REVSuckerDeploymentConfig} from "../../src/structs/REVSuckerDeploymentConfig.sol";
|
|
35
|
+
import {REVLoans} from "../../src/REVLoans.sol";
|
|
36
|
+
import {REVEmpty721Config} from "../helpers/REVEmpty721Config.sol";
|
|
37
|
+
import {MockBuybackCashOutRecorder} from "../mock/MockBuybackCashOutRecorder.sol";
|
|
38
|
+
|
|
39
|
+
/// @title TestCashOutBuybackFeeLeak
|
|
40
|
+
/// @notice Proves the buyback hook callback receives only the non-fee cashOutCount (not the full count).
|
|
41
|
+
/// Before the fix, the buyback hook reminted and sold `context.cashOutCount` tokens — more than REVDeployer
|
|
42
|
+
/// intended. The fee portion was monetized through the pool sale AND the fee was also extracted from treasury.
|
|
43
|
+
contract TestCashOutBuybackFeeLeak is TestBaseWorkflow {
|
|
44
|
+
bytes32 private constant REV_DEPLOYER_SALT = "REVDeployer";
|
|
45
|
+
bytes32 private constant ERC20_SALT = "REV_TOKEN";
|
|
46
|
+
address private constant TRUSTED_FORWARDER = 0xB2b5841DBeF766d4b521221732F9B618fCf34A87;
|
|
47
|
+
|
|
48
|
+
REVDeployer internal revDeployer;
|
|
49
|
+
MockBuybackCashOutRecorder internal mockBuyback;
|
|
50
|
+
JB721TiersHook internal exampleHook;
|
|
51
|
+
IJB721TiersHookDeployer internal hookDeployer;
|
|
52
|
+
IJBAddressRegistry internal addressRegistry;
|
|
53
|
+
JB721TiersHookStore internal hookStore;
|
|
54
|
+
JBSuckerRegistry internal suckerRegistry;
|
|
55
|
+
CTPublisher internal publisher;
|
|
56
|
+
REVLoans internal loans;
|
|
57
|
+
|
|
58
|
+
uint256 internal feeProjectId;
|
|
59
|
+
uint256 internal revnetId;
|
|
60
|
+
address internal user = makeAddr("user");
|
|
61
|
+
|
|
62
|
+
function setUp() public override {
|
|
63
|
+
super.setUp();
|
|
64
|
+
|
|
65
|
+
feeProjectId = jbProjects().createFor(multisig());
|
|
66
|
+
suckerRegistry = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
|
|
67
|
+
hookStore = new JB721TiersHookStore();
|
|
68
|
+
exampleHook = new JB721TiersHook(
|
|
69
|
+
jbDirectory(), jbPermissions(), jbPrices(), jbRulesets(), hookStore, jbSplits(), multisig()
|
|
70
|
+
);
|
|
71
|
+
addressRegistry = new JBAddressRegistry();
|
|
72
|
+
hookDeployer = new JB721TiersHookDeployer(exampleHook, hookStore, addressRegistry, multisig());
|
|
73
|
+
publisher = new CTPublisher(jbDirectory(), jbPermissions(), feeProjectId, multisig());
|
|
74
|
+
mockBuyback = new MockBuybackCashOutRecorder();
|
|
75
|
+
loans = new REVLoans({
|
|
76
|
+
controller: jbController(),
|
|
77
|
+
projects: jbProjects(),
|
|
78
|
+
revId: feeProjectId,
|
|
79
|
+
owner: address(this),
|
|
80
|
+
permit2: permit2(),
|
|
81
|
+
trustedForwarder: TRUSTED_FORWARDER
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
revDeployer = new REVDeployer{salt: REV_DEPLOYER_SALT}(
|
|
85
|
+
jbController(),
|
|
86
|
+
suckerRegistry,
|
|
87
|
+
feeProjectId,
|
|
88
|
+
hookDeployer,
|
|
89
|
+
publisher,
|
|
90
|
+
IJBBuybackHookRegistry(address(mockBuyback)),
|
|
91
|
+
address(loans),
|
|
92
|
+
TRUSTED_FORWARDER
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
vm.prank(multisig());
|
|
96
|
+
jbProjects().approve(address(revDeployer), feeProjectId);
|
|
97
|
+
|
|
98
|
+
// Deploy fee project.
|
|
99
|
+
vm.prank(multisig());
|
|
100
|
+
_deployRevnet("Fee", "FEE", "ipfs://fee", "FEE_SALT", 6000, feeProjectId);
|
|
101
|
+
|
|
102
|
+
// Deploy test revnet.
|
|
103
|
+
(revnetId,) = _deployRevnet("Revnet", "REV", "ipfs://rev", ERC20_SALT, 6000, 0);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/// @notice The invariant: buyback hook should only process the non-fee token count.
|
|
107
|
+
/// This test FAILS before the fix (proving the bug) and PASSES after.
|
|
108
|
+
function test_buybackHookReceivesOnlyNonFeeCount() external {
|
|
109
|
+
// Fund the user and pay into the revnet.
|
|
110
|
+
vm.deal(user, 10 ether);
|
|
111
|
+
vm.prank(user);
|
|
112
|
+
jbMultiTerminal().pay{value: 5 ether}({
|
|
113
|
+
projectId: revnetId,
|
|
114
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
115
|
+
amount: 5 ether,
|
|
116
|
+
beneficiary: user,
|
|
117
|
+
minReturnedTokens: 0,
|
|
118
|
+
memo: "",
|
|
119
|
+
metadata: ""
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
uint256 fullCashOutCount = jbTokens().totalBalanceOf(user, revnetId) / 2;
|
|
123
|
+
assertGt(fullCashOutCount, 0, "user should have tokens");
|
|
124
|
+
|
|
125
|
+
// The non-fee count: what the buyback hook SHOULD process.
|
|
126
|
+
uint256 feeCashOutCount = fullCashOutCount * revDeployer.FEE() / JBConstants.MAX_FEE;
|
|
127
|
+
uint256 expectedNonFeeCount = fullCashOutCount - feeCashOutCount;
|
|
128
|
+
|
|
129
|
+
// Perform the cash out.
|
|
130
|
+
vm.prank(user);
|
|
131
|
+
jbMultiTerminal()
|
|
132
|
+
.cashOutTokensOf({
|
|
133
|
+
holder: user,
|
|
134
|
+
projectId: revnetId,
|
|
135
|
+
cashOutCount: fullCashOutCount,
|
|
136
|
+
tokenToReclaim: JBConstants.NATIVE_TOKEN,
|
|
137
|
+
minTokensReclaimed: 0,
|
|
138
|
+
beneficiary: payable(user),
|
|
139
|
+
metadata: ""
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// THE INVARIANT: The buyback hook callback should receive nonFeeCashOutCount.
|
|
143
|
+
// The real buyback hook remints `context.cashOutCount` tokens and sells them on the pool.
|
|
144
|
+
// If it receives fullCashOutCount, it sells the fee portion too — the fee is bypassed.
|
|
145
|
+
assertEq(
|
|
146
|
+
mockBuyback.afterCashOutCount(),
|
|
147
|
+
expectedNonFeeCount,
|
|
148
|
+
"BUG: buyback hook received full count instead of non-fee count"
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function _deployRevnet(
|
|
153
|
+
string memory name,
|
|
154
|
+
string memory symbol,
|
|
155
|
+
string memory projectUri,
|
|
156
|
+
bytes32 salt,
|
|
157
|
+
uint16 cashOutTaxRate,
|
|
158
|
+
uint256 existingProjectId
|
|
159
|
+
)
|
|
160
|
+
internal
|
|
161
|
+
returns (uint256 id, IJB721TiersHook hook)
|
|
162
|
+
{
|
|
163
|
+
JBAccountingContext[] memory accountingContextsToAccept = new JBAccountingContext[](1);
|
|
164
|
+
accountingContextsToAccept[0] = JBAccountingContext({
|
|
165
|
+
token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
JBTerminalConfig[] memory terminalConfigurations = new JBTerminalConfig[](1);
|
|
169
|
+
terminalConfigurations[0] =
|
|
170
|
+
JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: accountingContextsToAccept});
|
|
171
|
+
|
|
172
|
+
JBSplit[] memory splits = new JBSplit[](1);
|
|
173
|
+
splits[0].beneficiary = payable(multisig());
|
|
174
|
+
splits[0].percent = 10_000;
|
|
175
|
+
|
|
176
|
+
REVStageConfig[] memory stageConfigurations = new REVStageConfig[](1);
|
|
177
|
+
stageConfigurations[0] = REVStageConfig({
|
|
178
|
+
startsAtOrAfter: uint40(block.timestamp),
|
|
179
|
+
autoIssuances: new REVAutoIssuance[](0),
|
|
180
|
+
splitPercent: 2000,
|
|
181
|
+
splits: splits,
|
|
182
|
+
initialIssuance: uint112(1000e18),
|
|
183
|
+
issuanceCutFrequency: 90 days,
|
|
184
|
+
issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
|
|
185
|
+
cashOutTaxRate: cashOutTaxRate,
|
|
186
|
+
extraMetadata: 0
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
return revDeployer.deployFor({
|
|
190
|
+
revnetId: existingProjectId,
|
|
191
|
+
configuration: REVConfig({
|
|
192
|
+
description: REVDescription(name, symbol, projectUri, salt),
|
|
193
|
+
baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
194
|
+
splitOperator: multisig(),
|
|
195
|
+
stageConfigurations: stageConfigurations
|
|
196
|
+
}),
|
|
197
|
+
terminalConfigurations: terminalConfigurations,
|
|
198
|
+
suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
|
|
199
|
+
deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256(abi.encodePacked(symbol))
|
|
200
|
+
}),
|
|
201
|
+
tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
|
|
202
|
+
allowedPosts: REVEmpty721Config.emptyAllowedPosts()
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|