@rev-net/core-v6 0.0.18 → 0.0.20
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 +14 -4
- package/ARCHITECTURE.md +13 -10
- package/AUDIT_INSTRUCTIONS.md +39 -18
- package/CHANGE_LOG.md +78 -1
- package/README.md +10 -5
- package/RISKS.md +12 -11
- package/SKILLS.md +27 -12
- package/USER_JOURNEYS.md +15 -14
- package/foundry.toml +1 -1
- package/package.json +1 -1
- package/script/Deploy.s.sol +37 -4
- package/src/REVDeployer.sol +23 -305
- package/src/REVLoans.sol +24 -29
- package/src/REVOwner.sol +429 -0
- package/src/interfaces/IREVDeployer.sol +4 -10
- package/src/interfaces/IREVOwner.sol +10 -0
- package/test/REV.integrations.t.sol +12 -1
- package/test/REVAutoIssuanceFuzz.t.sol +12 -1
- package/test/REVDeployerRegressions.t.sol +15 -2
- package/test/REVInvincibility.t.sol +27 -3
- package/test/REVLifecycle.t.sol +14 -1
- package/test/REVLoans.invariants.t.sol +14 -1
- package/test/REVLoansAttacks.t.sol +14 -1
- package/test/REVLoansFeeRecovery.t.sol +14 -1
- package/test/REVLoansFindings.t.sol +14 -1
- package/test/REVLoansRegressions.t.sol +14 -1
- package/test/REVLoansSourceFeeRecovery.t.sol +14 -1
- package/test/REVLoansSourced.t.sol +14 -1
- package/test/REVLoansUnSourced.t.sol +14 -1
- package/test/TestBurnHeldTokens.t.sol +14 -1
- package/test/TestCEIPattern.t.sol +15 -1
- package/test/TestCashOutCallerValidation.t.sol +17 -4
- package/test/TestConversionDocumentation.t.sol +14 -1
- package/test/TestCrossCurrencyReclaim.t.sol +14 -1
- package/test/TestCrossSourceReallocation.t.sol +15 -1
- package/test/TestERC2771MetaTx.t.sol +14 -1
- package/test/TestEmptyBuybackSpecs.t.sol +17 -3
- package/test/TestFlashLoanSurplus.t.sol +15 -1
- package/test/TestHookArrayOOB.t.sol +16 -2
- package/test/TestLiquidationBehavior.t.sol +15 -1
- package/test/TestLoanSourceRotation.t.sol +14 -1
- package/test/TestLoansCashOutDelay.t.sol +19 -6
- package/test/TestLongTailEconomics.t.sol +14 -1
- package/test/TestLowFindings.t.sol +14 -1
- package/test/TestMixedFixes.t.sol +15 -1
- package/test/TestPermit2Signatures.t.sol +14 -1
- package/test/TestReallocationSandwich.t.sol +15 -1
- package/test/TestRevnetRegressions.t.sol +14 -1
- package/test/TestSplitWeightAdjustment.t.sol +41 -19
- package/test/TestSplitWeightE2E.t.sol +23 -2
- package/test/TestSplitWeightFork.t.sol +14 -1
- package/test/TestStageTransitionBorrowable.t.sol +15 -1
- package/test/TestSwapTerminalPermission.t.sol +15 -1
- package/test/TestUint112Overflow.t.sol +14 -1
- package/test/TestZeroRepayment.t.sol +15 -1
- package/test/audit/LoanIdOverflowGuard.t.sol +14 -1
- package/test/fork/ForkTestBase.sol +14 -1
- package/test/fork/TestPermit2PaymentFork.t.sol +4 -3
- package/test/regression/TestBurnPermissionRequired.t.sol +15 -1
- package/test/regression/TestCashOutBuybackFeeLeak.t.sol +13 -1
- package/test/regression/TestCrossRevnetLiquidation.t.sol +15 -1
- package/test/regression/TestCumulativeLoanCounter.t.sol +15 -1
- package/test/regression/TestLiquidateGapHandling.t.sol +15 -1
- package/test/regression/TestZeroPriceFeed.t.sol +14 -1
package/src/REVOwner.sol
ADDED
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.28;
|
|
3
|
+
|
|
4
|
+
import {IJB721TiersHook} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHook.sol";
|
|
5
|
+
import {IJBBuybackHookRegistry} from "@bananapus/buyback-hook-v6/src/interfaces/IJBBuybackHookRegistry.sol";
|
|
6
|
+
import {IJBCashOutHook} from "@bananapus/core-v6/src/interfaces/IJBCashOutHook.sol";
|
|
7
|
+
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
8
|
+
import {IJBRulesetDataHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetDataHook.sol";
|
|
9
|
+
import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
|
|
10
|
+
import {JBCashOuts} from "@bananapus/core-v6/src/libraries/JBCashOuts.sol";
|
|
11
|
+
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
12
|
+
import {JBAfterCashOutRecordedContext} from "@bananapus/core-v6/src/structs/JBAfterCashOutRecordedContext.sol";
|
|
13
|
+
import {JBBeforeCashOutRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforeCashOutRecordedContext.sol";
|
|
14
|
+
import {JBBeforePayRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforePayRecordedContext.sol";
|
|
15
|
+
import {JBCashOutHookSpecification} from "@bananapus/core-v6/src/structs/JBCashOutHookSpecification.sol";
|
|
16
|
+
import {JBPayHookSpecification} from "@bananapus/core-v6/src/structs/JBPayHookSpecification.sol";
|
|
17
|
+
import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
|
|
18
|
+
import {IJBSuckerRegistry} from "@bananapus/suckers-v6/src/interfaces/IJBSuckerRegistry.sol";
|
|
19
|
+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
20
|
+
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
|
21
|
+
import {mulDiv} from "@prb/math/src/Common.sol";
|
|
22
|
+
|
|
23
|
+
import {IREVDeployer} from "./interfaces/IREVDeployer.sol";
|
|
24
|
+
|
|
25
|
+
/// @notice Handles the runtime data hook and cash out hook behavior for revnets.
|
|
26
|
+
/// @dev Separated from `REVDeployer` to stay within the EIP-170 contract size limit.
|
|
27
|
+
/// This contract is set as the `dataHook` in each revnet's ruleset metadata.
|
|
28
|
+
contract REVOwner is IJBRulesetDataHook, IJBCashOutHook {
|
|
29
|
+
// A library that adds default safety checks to ERC20 functionality.
|
|
30
|
+
using SafeERC20 for IERC20;
|
|
31
|
+
|
|
32
|
+
//*********************************************************************//
|
|
33
|
+
// --------------------------- custom errors ------------------------- //
|
|
34
|
+
//*********************************************************************//
|
|
35
|
+
|
|
36
|
+
error REVOwner_AlreadyInitialized();
|
|
37
|
+
error REVOwner_CashOutDelayNotFinished(uint256 cashOutDelay, uint256 blockTimestamp);
|
|
38
|
+
error REVOwner_Unauthorized();
|
|
39
|
+
|
|
40
|
+
//*********************************************************************//
|
|
41
|
+
// ------------------------- public constants ------------------------ //
|
|
42
|
+
//*********************************************************************//
|
|
43
|
+
|
|
44
|
+
/// @notice The cash out fee (as a fraction out of `JBConstants.MAX_FEE`).
|
|
45
|
+
/// @dev Cashout fees are paid to the revnet with the `FEE_REVNET_ID`.
|
|
46
|
+
/// @dev When suckers withdraw funds, they do not pay cash out fees.
|
|
47
|
+
uint256 public constant FEE = 25; // 2.5%
|
|
48
|
+
|
|
49
|
+
//*********************************************************************//
|
|
50
|
+
// --------------- public immutable stored properties ---------------- //
|
|
51
|
+
//*********************************************************************//
|
|
52
|
+
|
|
53
|
+
/// @notice The buyback hook used as a data hook to route payments through buyback pools.
|
|
54
|
+
IJBBuybackHookRegistry public immutable BUYBACK_HOOK;
|
|
55
|
+
|
|
56
|
+
/// @notice The directory of terminals and controllers for Juicebox projects.
|
|
57
|
+
IJBDirectory public immutable DIRECTORY;
|
|
58
|
+
|
|
59
|
+
/// @notice The Juicebox project ID of the revnet that receives cash out fees.
|
|
60
|
+
uint256 public immutable FEE_REVNET_ID;
|
|
61
|
+
|
|
62
|
+
/// @notice The loan contract used by all revnets.
|
|
63
|
+
address public immutable LOANS;
|
|
64
|
+
|
|
65
|
+
/// @notice Deploys and tracks suckers for revnets.
|
|
66
|
+
IJBSuckerRegistry public immutable SUCKER_REGISTRY;
|
|
67
|
+
|
|
68
|
+
//*********************************************************************//
|
|
69
|
+
// --------------------- public stored properties -------------------- //
|
|
70
|
+
//*********************************************************************//
|
|
71
|
+
|
|
72
|
+
/// @notice The timestamp of when cashouts will become available to a specific revnet's participants.
|
|
73
|
+
/// @dev Only applies to existing revnets which are deploying onto a new network.
|
|
74
|
+
/// @custom:param revnetId The ID of the revnet to get the cash out delay for.
|
|
75
|
+
mapping(uint256 revnetId => uint256 cashOutDelay) public cashOutDelayOf;
|
|
76
|
+
|
|
77
|
+
/// @notice Each revnet's tiered ERC-721 hook.
|
|
78
|
+
/// @custom:param revnetId The ID of the revnet to get the tiered ERC-721 hook for.
|
|
79
|
+
// slither-disable-next-line uninitialized-state
|
|
80
|
+
mapping(uint256 revnetId => IJB721TiersHook tiered721Hook) public tiered721HookOf;
|
|
81
|
+
|
|
82
|
+
/// @notice The deployer that manages revnet state.
|
|
83
|
+
/// @dev Set once via `setDeployer()` from the REVDeployer's constructor. Reverts if called again.
|
|
84
|
+
IREVDeployer public DEPLOYER;
|
|
85
|
+
|
|
86
|
+
//*********************************************************************//
|
|
87
|
+
// -------------------------- constructor ---------------------------- //
|
|
88
|
+
//*********************************************************************//
|
|
89
|
+
|
|
90
|
+
/// @param buybackHook The buyback hook used to route payments.
|
|
91
|
+
/// @param directory The directory of terminals and controllers.
|
|
92
|
+
/// @param feeRevnetId The Juicebox project ID of the fee revnet.
|
|
93
|
+
/// @param suckerRegistry The sucker registry.
|
|
94
|
+
/// @param loans The loan contract address.
|
|
95
|
+
constructor(
|
|
96
|
+
IJBBuybackHookRegistry buybackHook,
|
|
97
|
+
IJBDirectory directory,
|
|
98
|
+
uint256 feeRevnetId,
|
|
99
|
+
IJBSuckerRegistry suckerRegistry,
|
|
100
|
+
address loans
|
|
101
|
+
) {
|
|
102
|
+
BUYBACK_HOOK = buybackHook;
|
|
103
|
+
DIRECTORY = directory;
|
|
104
|
+
FEE_REVNET_ID = feeRevnetId;
|
|
105
|
+
SUCKER_REGISTRY = suckerRegistry;
|
|
106
|
+
// slither-disable-next-line missing-zero-check
|
|
107
|
+
LOANS = loans;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
//*********************************************************************//
|
|
111
|
+
// ------------------------- external views -------------------------- //
|
|
112
|
+
//*********************************************************************//
|
|
113
|
+
|
|
114
|
+
/// @notice Determine how a cash out from a revnet should be processed.
|
|
115
|
+
/// @dev This function is part of `IJBRulesetDataHook`, and gets called before the revnet processes a cash out.
|
|
116
|
+
/// @dev If a sucker is cashing out, no taxes or fees are imposed.
|
|
117
|
+
/// @dev REVOwner is intentionally not registered as a feeless address. The protocol fee (2.5%) applies on top
|
|
118
|
+
/// of the rev fee — this is by design. The fee hook spec amount sent to `afterCashOutRecordedWith` will have the
|
|
119
|
+
/// protocol fee deducted by the terminal before reaching this contract, so the rev fee is computed on the
|
|
120
|
+
/// post-protocol-fee amount.
|
|
121
|
+
/// @param context Standard Juicebox cash out context. See `JBBeforeCashOutRecordedContext`.
|
|
122
|
+
/// @return cashOutTaxRate The cash out tax rate, which influences the amount of terminal tokens which get cashed
|
|
123
|
+
/// out.
|
|
124
|
+
/// @return cashOutCount The number of revnet tokens that are cashed out.
|
|
125
|
+
/// @return totalSupply The total revnet token supply.
|
|
126
|
+
/// @return hookSpecifications The amount of funds and the data to send to cash out hooks (this contract).
|
|
127
|
+
function beforeCashOutRecordedWith(JBBeforeCashOutRecordedContext calldata context)
|
|
128
|
+
external
|
|
129
|
+
view
|
|
130
|
+
override
|
|
131
|
+
returns (
|
|
132
|
+
uint256 cashOutTaxRate,
|
|
133
|
+
uint256 cashOutCount,
|
|
134
|
+
uint256 totalSupply,
|
|
135
|
+
JBCashOutHookSpecification[] memory hookSpecifications
|
|
136
|
+
)
|
|
137
|
+
{
|
|
138
|
+
// If the cash out is from a sucker, return the full cash out amount without taxes or fees.
|
|
139
|
+
// This relies on the sucker registry to only contain trusted sucker contracts deployed via
|
|
140
|
+
// the registry's own deploySuckersFor flow — external addresses cannot register as suckers.
|
|
141
|
+
if (_isSuckerOf({revnetId: context.projectId, addr: context.holder})) {
|
|
142
|
+
return (0, context.cashOutCount, context.totalSupply, hookSpecifications);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Keep a reference to the cash out delay of the revnet.
|
|
146
|
+
uint256 cashOutDelay = cashOutDelayOf[context.projectId];
|
|
147
|
+
|
|
148
|
+
// Enforce the cash out delay.
|
|
149
|
+
if (cashOutDelay > block.timestamp) {
|
|
150
|
+
revert REVOwner_CashOutDelayNotFinished(cashOutDelay, block.timestamp);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Get the terminal that will receive the cash out fee.
|
|
154
|
+
IJBTerminal feeTerminal = DIRECTORY.primaryTerminalOf({projectId: FEE_REVNET_ID, token: context.surplus.token});
|
|
155
|
+
|
|
156
|
+
// If there's no cash out tax (100% cash out tax rate), if there's no fee terminal, or if the beneficiary is
|
|
157
|
+
// feeless (e.g. the router terminal routing value between projects), proxy directly to the buyback hook.
|
|
158
|
+
if (context.cashOutTaxRate == 0 || address(feeTerminal) == address(0) || context.beneficiaryIsFeeless) {
|
|
159
|
+
// slither-disable-next-line unused-return
|
|
160
|
+
return BUYBACK_HOOK.beforeCashOutRecordedWith(context);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Split the cashed-out tokens into a fee portion and a non-fee portion.
|
|
164
|
+
// The fee is applied to TOKEN COUNT (2.5% of tokens), not to value. The fee revnet receives the bonding-curve
|
|
165
|
+
// reclaim of its 2.5% token share regardless of whether the remaining 97.5% routes through a buyback pool at
|
|
166
|
+
// a better price. This is by design.
|
|
167
|
+
// Micro cash outs (< 40 wei at 2.5% fee) round feeCashOutCount to zero, bypassing the fee.
|
|
168
|
+
// Economically insignificant: the gas cost of the transaction far exceeds the bypassed fee. No fix needed.
|
|
169
|
+
uint256 feeCashOutCount = mulDiv({x: context.cashOutCount, y: FEE, denominator: JBConstants.MAX_FEE});
|
|
170
|
+
uint256 nonFeeCashOutCount = context.cashOutCount - feeCashOutCount;
|
|
171
|
+
|
|
172
|
+
// Calculate how much surplus the non-fee tokens can reclaim via the bonding curve.
|
|
173
|
+
uint256 postFeeReclaimedAmount = JBCashOuts.cashOutFrom({
|
|
174
|
+
surplus: context.surplus.value,
|
|
175
|
+
cashOutCount: nonFeeCashOutCount,
|
|
176
|
+
totalSupply: context.totalSupply,
|
|
177
|
+
cashOutTaxRate: context.cashOutTaxRate
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Calculate how much the fee tokens reclaim from the remaining surplus after the non-fee reclaim.
|
|
181
|
+
uint256 feeAmount = JBCashOuts.cashOutFrom({
|
|
182
|
+
surplus: context.surplus.value - postFeeReclaimedAmount,
|
|
183
|
+
cashOutCount: feeCashOutCount,
|
|
184
|
+
totalSupply: context.totalSupply - nonFeeCashOutCount,
|
|
185
|
+
cashOutTaxRate: context.cashOutTaxRate
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Build a context for the buyback hook using only the non-fee token count.
|
|
189
|
+
JBBeforeCashOutRecordedContext memory buybackHookContext = context;
|
|
190
|
+
buybackHookContext.cashOutCount = nonFeeCashOutCount;
|
|
191
|
+
|
|
192
|
+
// Let the buyback hook adjust the cash out parameters and optionally return a hook specification.
|
|
193
|
+
JBCashOutHookSpecification[] memory buybackHookSpecifications;
|
|
194
|
+
(cashOutTaxRate, cashOutCount, totalSupply, buybackHookSpecifications) =
|
|
195
|
+
BUYBACK_HOOK.beforeCashOutRecordedWith(buybackHookContext);
|
|
196
|
+
|
|
197
|
+
// If the fee rounds down to zero, return the buyback hook's response directly — no fee to process.
|
|
198
|
+
if (feeAmount == 0) return (cashOutTaxRate, cashOutCount, totalSupply, buybackHookSpecifications);
|
|
199
|
+
|
|
200
|
+
// Build a hook spec that routes the fee amount to this contract's `afterCashOutRecordedWith` for processing.
|
|
201
|
+
JBCashOutHookSpecification memory feeSpec = JBCashOutHookSpecification({
|
|
202
|
+
hook: IJBCashOutHook(address(this)), noop: false, amount: feeAmount, metadata: abi.encode(feeTerminal)
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Compose the final hook specifications: buyback spec (if any) + fee spec.
|
|
206
|
+
// NOTE: Only buybackHookSpecifications[0] is used. If the buyback hook returns multiple
|
|
207
|
+
// specs, the additional ones are silently dropped. This is intentional — the buyback hook is
|
|
208
|
+
// expected to return at most one spec for the cash-out buyback swap.
|
|
209
|
+
if (buybackHookSpecifications.length > 0) {
|
|
210
|
+
// The buyback hook returned a spec — include it before the fee spec.
|
|
211
|
+
hookSpecifications = new JBCashOutHookSpecification[](2);
|
|
212
|
+
hookSpecifications[0] = buybackHookSpecifications[0];
|
|
213
|
+
hookSpecifications[1] = feeSpec;
|
|
214
|
+
} else {
|
|
215
|
+
// No buyback spec — only the fee spec.
|
|
216
|
+
hookSpecifications = new JBCashOutHookSpecification[](1);
|
|
217
|
+
hookSpecifications[0] = feeSpec;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return (cashOutTaxRate, cashOutCount, totalSupply, hookSpecifications);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/// @notice Before a revnet processes an incoming payment, determine the weight and pay hooks to use.
|
|
224
|
+
/// @dev This function is part of `IJBRulesetDataHook`, and gets called before the revnet processes a payment.
|
|
225
|
+
/// @param context Standard Juicebox payment context. See `JBBeforePayRecordedContext`.
|
|
226
|
+
/// @return weight The weight which revnet tokens are minted relative to. This can be used to customize how many
|
|
227
|
+
/// tokens get minted by a payment.
|
|
228
|
+
/// @return hookSpecifications Amounts (out of what's being paid in) to be sent to pay hooks instead of being paid
|
|
229
|
+
/// into the revnet. Useful for automatically routing funds from a treasury as payments come in.
|
|
230
|
+
function beforePayRecordedWith(JBBeforePayRecordedContext calldata context)
|
|
231
|
+
external
|
|
232
|
+
view
|
|
233
|
+
override
|
|
234
|
+
returns (uint256 weight, JBPayHookSpecification[] memory hookSpecifications)
|
|
235
|
+
{
|
|
236
|
+
// Get the 721 hook's spec and total split amount.
|
|
237
|
+
IJB721TiersHook tiered721Hook = tiered721HookOf[context.projectId];
|
|
238
|
+
JBPayHookSpecification memory tiered721HookSpec;
|
|
239
|
+
uint256 totalSplitAmount;
|
|
240
|
+
bool usesTiered721Hook = address(tiered721Hook) != address(0);
|
|
241
|
+
if (usesTiered721Hook) {
|
|
242
|
+
JBPayHookSpecification[] memory specs;
|
|
243
|
+
// slither-disable-next-line unused-return
|
|
244
|
+
(, specs) = IJBRulesetDataHook(address(tiered721Hook)).beforePayRecordedWith(context);
|
|
245
|
+
// The 721 hook returns a single spec (itself) whose amount is the total split amount.
|
|
246
|
+
if (specs.length > 0) {
|
|
247
|
+
tiered721HookSpec = specs[0];
|
|
248
|
+
totalSplitAmount = tiered721HookSpec.amount;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// The amount entering the project after tier splits.
|
|
253
|
+
uint256 projectAmount = totalSplitAmount >= context.amount.value ? 0 : context.amount.value - totalSplitAmount;
|
|
254
|
+
|
|
255
|
+
// Get the buyback hook's weight and specs. Reduce the amount so it only considers funds entering the project.
|
|
256
|
+
JBPayHookSpecification[] memory buybackHookSpecs;
|
|
257
|
+
{
|
|
258
|
+
JBBeforePayRecordedContext memory buybackHookContext = context;
|
|
259
|
+
buybackHookContext.amount.value = projectAmount;
|
|
260
|
+
(weight, buybackHookSpecs) = BUYBACK_HOOK.beforePayRecordedWith(buybackHookContext);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Scale the buyback hook's weight for splits so the terminal mints tokens only for the project's share.
|
|
264
|
+
// The terminal uses the full context.amount.value for minting (tokenCount = amount * weight / weightRatio),
|
|
265
|
+
// but only projectAmount actually enters the project. Without scaling, payers get token credit for the split
|
|
266
|
+
// portion too. Preserves weight=0 from the buyback hook (buying back, not minting).
|
|
267
|
+
if (projectAmount == 0) {
|
|
268
|
+
weight = 0;
|
|
269
|
+
} else if (projectAmount < context.amount.value) {
|
|
270
|
+
weight = mulDiv({x: weight, y: projectAmount, denominator: context.amount.value});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Merge hook specifications: 721 hook spec first, then buyback hook spec.
|
|
274
|
+
bool usesBuybackHook = buybackHookSpecs.length > 0;
|
|
275
|
+
hookSpecifications = new JBPayHookSpecification[]((usesTiered721Hook ? 1 : 0) + (usesBuybackHook ? 1 : 0));
|
|
276
|
+
|
|
277
|
+
if (usesTiered721Hook) hookSpecifications[0] = tiered721HookSpec;
|
|
278
|
+
if (usesBuybackHook) hookSpecifications[usesTiered721Hook ? 1 : 0] = buybackHookSpecs[0];
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/// @notice A flag indicating whether an address has permission to mint a revnet's tokens on-demand.
|
|
282
|
+
/// @dev Required by the `IJBRulesetDataHook` interface.
|
|
283
|
+
/// @param revnetId The ID of the revnet to check permissions for.
|
|
284
|
+
/// @param ruleset The ruleset to check the mint permission for.
|
|
285
|
+
/// @param addr The address to check the mint permission of.
|
|
286
|
+
/// @return flag A flag indicating whether the address has permission to mint the revnet's tokens on-demand.
|
|
287
|
+
function hasMintPermissionFor(
|
|
288
|
+
uint256 revnetId,
|
|
289
|
+
JBRuleset calldata ruleset,
|
|
290
|
+
address addr
|
|
291
|
+
)
|
|
292
|
+
external
|
|
293
|
+
view
|
|
294
|
+
override
|
|
295
|
+
returns (bool)
|
|
296
|
+
{
|
|
297
|
+
// The loans contract, buyback hook (and its delegates), and suckers are allowed to mint the revnet's tokens.
|
|
298
|
+
return addr == LOANS || addr == address(BUYBACK_HOOK)
|
|
299
|
+
|| BUYBACK_HOOK.hasMintPermissionFor({projectId: revnetId, ruleset: ruleset, addr: addr})
|
|
300
|
+
|| _isSuckerOf({revnetId: revnetId, addr: addr});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
//*********************************************************************//
|
|
304
|
+
// --------------------- external transactions ----------------------- //
|
|
305
|
+
//*********************************************************************//
|
|
306
|
+
|
|
307
|
+
/// @notice Processes the fee from a cash out.
|
|
308
|
+
/// @param context Cash out context passed in by the terminal.
|
|
309
|
+
function afterCashOutRecordedWith(JBAfterCashOutRecordedContext calldata context) external payable override {
|
|
310
|
+
// No caller validation needed — this hook only pays fees to the fee project using funds forwarded by the
|
|
311
|
+
// caller. A non-terminal caller would just be donating their own funds as fees. There's nothing to exploit.
|
|
312
|
+
|
|
313
|
+
// If there's sufficient approval, transfer normally.
|
|
314
|
+
if (context.forwardedAmount.token != JBConstants.NATIVE_TOKEN) {
|
|
315
|
+
IERC20(context.forwardedAmount.token)
|
|
316
|
+
.safeTransferFrom({from: msg.sender, to: address(this), value: context.forwardedAmount.value});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Parse the metadata forwarded from the data hook to get the fee terminal.
|
|
320
|
+
// See `beforeCashOutRecordedWith(…)`.
|
|
321
|
+
(IJBTerminal feeTerminal) = abi.decode(context.hookMetadata, (IJBTerminal));
|
|
322
|
+
|
|
323
|
+
// Determine how much to pay in `msg.value` (in the native currency).
|
|
324
|
+
uint256 payValue = _beforeTransferTo({
|
|
325
|
+
to: address(feeTerminal), token: context.forwardedAmount.token, amount: context.forwardedAmount.value
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// Pay the fee.
|
|
329
|
+
// slither-disable-next-line arbitrary-send-eth,unused-return
|
|
330
|
+
try feeTerminal.pay{value: payValue}({
|
|
331
|
+
projectId: FEE_REVNET_ID,
|
|
332
|
+
token: context.forwardedAmount.token,
|
|
333
|
+
amount: context.forwardedAmount.value,
|
|
334
|
+
beneficiary: context.holder,
|
|
335
|
+
minReturnedTokens: 0,
|
|
336
|
+
memo: "",
|
|
337
|
+
metadata: bytes(abi.encodePacked(context.projectId))
|
|
338
|
+
}) {}
|
|
339
|
+
catch (bytes memory) {
|
|
340
|
+
// Decrease the allowance for the fee terminal if the token is not the native token.
|
|
341
|
+
if (context.forwardedAmount.token != JBConstants.NATIVE_TOKEN) {
|
|
342
|
+
IERC20(context.forwardedAmount.token)
|
|
343
|
+
.safeDecreaseAllowance({
|
|
344
|
+
spender: address(feeTerminal), requestedDecrease: context.forwardedAmount.value
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// If the fee can't be processed, return the funds to the project.
|
|
349
|
+
payValue = _beforeTransferTo({
|
|
350
|
+
to: msg.sender, token: context.forwardedAmount.token, amount: context.forwardedAmount.value
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
// slither-disable-next-line arbitrary-send-eth
|
|
354
|
+
IJBTerminal(msg.sender).addToBalanceOf{value: payValue}({
|
|
355
|
+
projectId: context.projectId,
|
|
356
|
+
token: context.forwardedAmount.token,
|
|
357
|
+
amount: context.forwardedAmount.value,
|
|
358
|
+
shouldReturnHeldFees: false,
|
|
359
|
+
memo: "",
|
|
360
|
+
metadata: bytes(abi.encodePacked(FEE_REVNET_ID))
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/// @notice Set the caller as this contract's deployer.
|
|
366
|
+
/// @dev Called by the REVDeployer's constructor. Reverts if a deployer is already set.
|
|
367
|
+
function setDeployer() external {
|
|
368
|
+
if (address(DEPLOYER) != address(0)) revert REVOwner_AlreadyInitialized();
|
|
369
|
+
DEPLOYER = IREVDeployer(msg.sender);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/// @notice Store the cash out delay for a revnet.
|
|
373
|
+
/// @dev Only callable by the deployer.
|
|
374
|
+
/// @param revnetId The ID of the revnet.
|
|
375
|
+
/// @param cashOutDelay The timestamp after which cash outs are allowed.
|
|
376
|
+
function setCashOutDelayOf(uint256 revnetId, uint256 cashOutDelay) external {
|
|
377
|
+
if (msg.sender != address(DEPLOYER)) revert REVOwner_Unauthorized();
|
|
378
|
+
cashOutDelayOf[revnetId] = cashOutDelay;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/// @notice Store the tiered ERC-721 hook for a revnet.
|
|
382
|
+
/// @dev Only callable by the deployer.
|
|
383
|
+
/// @param revnetId The ID of the revnet.
|
|
384
|
+
/// @param hook The tiered ERC-721 hook.
|
|
385
|
+
function setTiered721HookOf(uint256 revnetId, IJB721TiersHook hook) external {
|
|
386
|
+
if (msg.sender != address(DEPLOYER)) revert REVOwner_Unauthorized();
|
|
387
|
+
tiered721HookOf[revnetId] = hook;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
//*********************************************************************//
|
|
391
|
+
// -------------------------- public views --------------------------- //
|
|
392
|
+
//*********************************************************************//
|
|
393
|
+
|
|
394
|
+
/// @notice Indicates if this contract adheres to the specified interface.
|
|
395
|
+
/// @dev See `IERC165.supportsInterface`.
|
|
396
|
+
/// @return A flag indicating if the provided interface ID is supported.
|
|
397
|
+
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
|
|
398
|
+
return interfaceId == type(IJBRulesetDataHook).interfaceId || interfaceId == type(IJBCashOutHook).interfaceId;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
//*********************************************************************//
|
|
402
|
+
// -------------------------- internal views ------------------------- //
|
|
403
|
+
//*********************************************************************//
|
|
404
|
+
|
|
405
|
+
/// @notice A flag indicating whether an address is a revnet's sucker.
|
|
406
|
+
/// @param revnetId The ID of the revnet to check sucker status for.
|
|
407
|
+
/// @param addr The address being checked.
|
|
408
|
+
/// @return isSucker A flag indicating whether the address is one of the revnet's suckers.
|
|
409
|
+
function _isSuckerOf(uint256 revnetId, address addr) internal view returns (bool) {
|
|
410
|
+
return SUCKER_REGISTRY.isSuckerOf({projectId: revnetId, addr: addr});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
//*********************************************************************//
|
|
414
|
+
// --------------------- internal transactions ----------------------- //
|
|
415
|
+
//*********************************************************************//
|
|
416
|
+
|
|
417
|
+
/// @notice Logic to be triggered before transferring tokens from this contract.
|
|
418
|
+
/// @param to The address the transfer is going to.
|
|
419
|
+
/// @param token The token being transferred.
|
|
420
|
+
/// @param amount The number of tokens being transferred, as a fixed point number with the same number of decimals
|
|
421
|
+
/// as the token specifies.
|
|
422
|
+
/// @return payValue The value to attach to the transaction being sent.
|
|
423
|
+
function _beforeTransferTo(address to, address token, uint256 amount) internal returns (uint256) {
|
|
424
|
+
// If the token is the native token, no allowance needed.
|
|
425
|
+
if (token == JBConstants.NATIVE_TOKEN) return amount;
|
|
426
|
+
IERC20(token).safeIncreaseAllowance({spender: to, value: amount});
|
|
427
|
+
return 0;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
@@ -102,11 +102,6 @@ interface IREVDeployer {
|
|
|
102
102
|
/// @return The cash out delay in seconds.
|
|
103
103
|
function CASH_OUT_DELAY() external view returns (uint256);
|
|
104
104
|
|
|
105
|
-
/// @notice The timestamp when cash outs become available for a revnet's participants.
|
|
106
|
-
/// @param revnetId The ID of the revnet.
|
|
107
|
-
/// @return The cash out delay timestamp.
|
|
108
|
-
function cashOutDelayOf(uint256 revnetId) external view returns (uint256);
|
|
109
|
-
|
|
110
105
|
/// @notice The controller used to create and manage Juicebox projects for revnets.
|
|
111
106
|
/// @return The controller contract.
|
|
112
107
|
function CONTROLLER() external view returns (IJBController);
|
|
@@ -150,6 +145,10 @@ interface IREVDeployer {
|
|
|
150
145
|
/// @return The loans contract address.
|
|
151
146
|
function LOANS() external view returns (address);
|
|
152
147
|
|
|
148
|
+
/// @notice The runtime data hook contract that handles pay and cash out callbacks for revnets.
|
|
149
|
+
/// @return The owner contract address.
|
|
150
|
+
function OWNER() external view returns (address);
|
|
151
|
+
|
|
153
152
|
/// @notice The contract that stores Juicebox project access permissions.
|
|
154
153
|
/// @return The permissions contract.
|
|
155
154
|
function PERMISSIONS() external view returns (IJBPermissions);
|
|
@@ -166,11 +165,6 @@ interface IREVDeployer {
|
|
|
166
165
|
/// @return The sucker registry contract.
|
|
167
166
|
function SUCKER_REGISTRY() external view returns (IJBSuckerRegistry);
|
|
168
167
|
|
|
169
|
-
/// @notice Each revnet's tiered ERC-721 hook.
|
|
170
|
-
/// @param revnetId The ID of the revnet.
|
|
171
|
-
/// @return The tiered ERC-721 hook.
|
|
172
|
-
function tiered721HookOf(uint256 revnetId) external view returns (IJB721TiersHook);
|
|
173
|
-
|
|
174
168
|
/// @notice Auto-mint a revnet's tokens from a stage for a beneficiary.
|
|
175
169
|
/// @param revnetId The ID of the revnet to auto-mint tokens from.
|
|
176
170
|
/// @param stageId The ID of the stage auto-mint tokens are available from.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.0;
|
|
3
|
+
|
|
4
|
+
/// @notice Interface for the REVOwner contract that handles runtime data hook and cash out hook behavior for revnets.
|
|
5
|
+
interface IREVOwner {
|
|
6
|
+
/// @notice The timestamp of when cashouts will become available to a specific revnet's participants.
|
|
7
|
+
/// @param revnetId The ID of the revnet.
|
|
8
|
+
/// @return The cash out delay timestamp.
|
|
9
|
+
function cashOutDelayOf(uint256 revnetId) external view returns (uint256);
|
|
10
|
+
}
|
|
@@ -12,6 +12,8 @@ import /* {*} from */ "./../src/REVDeployer.sol";
|
|
|
12
12
|
import "@croptop/core-v6/src/CTPublisher.sol";
|
|
13
13
|
import {MockBuybackDataHook} from "./mock/MockBuybackDataHook.sol";
|
|
14
14
|
import {REVEmpty721Config} from "./helpers/REVEmpty721Config.sol";
|
|
15
|
+
import {REVOwner} from "../src/REVOwner.sol";
|
|
16
|
+
import {IREVDeployer} from "../src/interfaces/IREVDeployer.sol";
|
|
15
17
|
|
|
16
18
|
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
17
19
|
import "@bananapus/core-v6/script/helpers/CoreDeploymentLib.sol";
|
|
@@ -219,6 +221,14 @@ contract REVnet_Integrations is TestBaseWorkflow {
|
|
|
219
221
|
PUBLISHER = new CTPublisher(jbDirectory(), jbPermissions(), FEE_PROJECT_ID, multisig());
|
|
220
222
|
MOCK_BUYBACK = new MockBuybackDataHook();
|
|
221
223
|
|
|
224
|
+
REVOwner revOwner = new REVOwner(
|
|
225
|
+
IJBBuybackHookRegistry(address(MOCK_BUYBACK)),
|
|
226
|
+
jbDirectory(),
|
|
227
|
+
FEE_PROJECT_ID,
|
|
228
|
+
SUCKER_REGISTRY,
|
|
229
|
+
makeAddr("loans")
|
|
230
|
+
);
|
|
231
|
+
|
|
222
232
|
REV_DEPLOYER = new REVDeployer{salt: REV_DEPLOYER_SALT}(
|
|
223
233
|
jbController(),
|
|
224
234
|
SUCKER_REGISTRY,
|
|
@@ -227,7 +237,8 @@ contract REVnet_Integrations is TestBaseWorkflow {
|
|
|
227
237
|
PUBLISHER,
|
|
228
238
|
IJBBuybackHookRegistry(address(MOCK_BUYBACK)),
|
|
229
239
|
makeAddr("loans"),
|
|
230
|
-
TRUSTED_FORWARDER
|
|
240
|
+
TRUSTED_FORWARDER,
|
|
241
|
+
address(revOwner)
|
|
231
242
|
);
|
|
232
243
|
|
|
233
244
|
// Deploy the ARB sucker deployer.
|
|
@@ -35,6 +35,8 @@ import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStor
|
|
|
35
35
|
import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
|
|
36
36
|
import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
|
|
37
37
|
import {REVEmpty721Config} from "./helpers/REVEmpty721Config.sol";
|
|
38
|
+
import {REVOwner} from "../src/REVOwner.sol";
|
|
39
|
+
import {IREVDeployer} from "../src/interfaces/IREVDeployer.sol";
|
|
38
40
|
|
|
39
41
|
/// @notice Fuzz tests for REVDeployer multi-stage auto-issuance.
|
|
40
42
|
/// Tests stage ID computation consistency and multi-stage claiming behavior.
|
|
@@ -82,6 +84,14 @@ contract REVAutoIssuanceFuzz_Local is TestBaseWorkflow {
|
|
|
82
84
|
PUBLISHER = new CTPublisher(jbDirectory(), jbPermissions(), FEE_PROJECT_ID, multisig());
|
|
83
85
|
MOCK_BUYBACK = new MockBuybackDataHook();
|
|
84
86
|
|
|
87
|
+
REVOwner revOwner = new REVOwner(
|
|
88
|
+
IJBBuybackHookRegistry(address(MOCK_BUYBACK)),
|
|
89
|
+
jbDirectory(),
|
|
90
|
+
FEE_PROJECT_ID,
|
|
91
|
+
SUCKER_REGISTRY,
|
|
92
|
+
makeAddr("loans")
|
|
93
|
+
);
|
|
94
|
+
|
|
85
95
|
REV_DEPLOYER = new REVDeployer{salt: REV_DEPLOYER_SALT}(
|
|
86
96
|
jbController(),
|
|
87
97
|
SUCKER_REGISTRY,
|
|
@@ -90,7 +100,8 @@ contract REVAutoIssuanceFuzz_Local is TestBaseWorkflow {
|
|
|
90
100
|
PUBLISHER,
|
|
91
101
|
IJBBuybackHookRegistry(address(MOCK_BUYBACK)),
|
|
92
102
|
makeAddr("loans"),
|
|
93
|
-
TRUSTED_FORWARDER
|
|
103
|
+
TRUSTED_FORWARDER,
|
|
104
|
+
address(revOwner)
|
|
94
105
|
);
|
|
95
106
|
|
|
96
107
|
vm.prank(multisig());
|
|
@@ -37,6 +37,8 @@ import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
|
|
|
37
37
|
import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
|
|
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
|
+
import {REVOwner} from "../src/REVOwner.sol";
|
|
41
|
+
import {IREVDeployer} from "../src/interfaces/IREVDeployer.sol";
|
|
40
42
|
|
|
41
43
|
/// @notice Regression tests for REVDeployer.
|
|
42
44
|
contract REVDeployerRegressions is TestBaseWorkflow {
|
|
@@ -65,6 +67,8 @@ contract REVDeployerRegressions is TestBaseWorkflow {
|
|
|
65
67
|
CTPublisher PUBLISHER;
|
|
66
68
|
// forge-lint: disable-next-line(mixed-case-variable)
|
|
67
69
|
MockBuybackDataHook MOCK_BUYBACK;
|
|
70
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
71
|
+
REVOwner REV_OWNER;
|
|
68
72
|
|
|
69
73
|
// forge-lint: disable-next-line(mixed-case-variable)
|
|
70
74
|
uint256 FEE_PROJECT_ID;
|
|
@@ -95,6 +99,14 @@ contract REVDeployerRegressions is TestBaseWorkflow {
|
|
|
95
99
|
trustedForwarder: TRUSTED_FORWARDER
|
|
96
100
|
});
|
|
97
101
|
|
|
102
|
+
REV_OWNER = new REVOwner(
|
|
103
|
+
IJBBuybackHookRegistry(address(MOCK_BUYBACK)),
|
|
104
|
+
jbDirectory(),
|
|
105
|
+
FEE_PROJECT_ID,
|
|
106
|
+
SUCKER_REGISTRY,
|
|
107
|
+
address(LOANS_CONTRACT)
|
|
108
|
+
);
|
|
109
|
+
|
|
98
110
|
REV_DEPLOYER = new REVDeployer{salt: REV_DEPLOYER_SALT}(
|
|
99
111
|
jbController(),
|
|
100
112
|
SUCKER_REGISTRY,
|
|
@@ -103,7 +115,8 @@ contract REVDeployerRegressions is TestBaseWorkflow {
|
|
|
103
115
|
PUBLISHER,
|
|
104
116
|
IJBBuybackHookRegistry(address(MOCK_BUYBACK)),
|
|
105
117
|
address(LOANS_CONTRACT),
|
|
106
|
-
TRUSTED_FORWARDER
|
|
118
|
+
TRUSTED_FORWARDER,
|
|
119
|
+
address(REV_OWNER)
|
|
107
120
|
);
|
|
108
121
|
|
|
109
122
|
vm.prank(multisig());
|
|
@@ -220,7 +233,7 @@ contract REVDeployerRegressions is TestBaseWorkflow {
|
|
|
220
233
|
|
|
221
234
|
// With buyback hook removed, hasMintPermissionFor should return false
|
|
222
235
|
// for addresses that are not the loans contract or a sucker.
|
|
223
|
-
bool hasPerm =
|
|
236
|
+
bool hasPerm = REV_OWNER.hasMintPermissionFor(revnetId, currentRuleset, someRandomAddr);
|
|
224
237
|
assertFalse(hasPerm, "random address should not have mint permission");
|
|
225
238
|
}
|
|
226
239
|
|