@rev-net/core-v6 0.0.17 → 0.0.19
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 +14 -10
- package/AUDIT_INSTRUCTIONS.md +40 -17
- package/CHANGE_LOG.md +87 -0
- package/README.md +10 -5
- package/RISKS.md +15 -10
- package/SKILLS.md +31 -15
- package/USER_JOURNEYS.md +16 -12
- package/foundry.toml +1 -1
- package/package.json +8 -8
- package/script/Deploy.s.sol +60 -19
- package/src/REVDeployer.sol +21 -303
- package/src/REVLoans.sol +31 -0
- package/src/REVOwner.sol +430 -0
- package/src/interfaces/IREVDeployer.sol +4 -10
- package/src/interfaces/IREVOwner.sol +10 -0
- package/src/structs/REVBaseline721HookConfig.sol +0 -2
- package/test/REV.integrations.t.sol +14 -1
- package/test/REVAutoIssuanceFuzz.t.sol +14 -1
- package/test/REVDeployerRegressions.t.sol +17 -2
- package/test/REVInvincibility.t.sol +31 -3
- package/test/REVLifecycle.t.sol +16 -1
- package/test/REVLoans.invariants.t.sol +16 -1
- package/test/REVLoansAttacks.t.sol +16 -1
- package/test/REVLoansFeeRecovery.t.sol +16 -1
- package/test/REVLoansFindings.t.sol +16 -1
- package/test/REVLoansRegressions.t.sol +16 -1
- package/test/REVLoansSourceFeeRecovery.t.sol +16 -1
- package/test/REVLoansSourced.t.sol +16 -1
- package/test/REVLoansUnSourced.t.sol +16 -1
- package/test/TestBurnHeldTokens.t.sol +16 -1
- package/test/TestCEIPattern.t.sol +16 -1
- package/test/TestCashOutCallerValidation.t.sol +19 -4
- package/test/TestConversionDocumentation.t.sol +16 -1
- package/test/TestCrossCurrencyReclaim.t.sol +16 -1
- package/test/TestCrossSourceReallocation.t.sol +16 -1
- package/test/TestERC2771MetaTx.t.sol +16 -1
- package/test/TestEmptyBuybackSpecs.t.sol +18 -3
- package/test/TestFlashLoanSurplus.t.sol +16 -1
- package/test/TestHookArrayOOB.t.sol +17 -2
- package/test/TestLiquidationBehavior.t.sol +16 -1
- package/test/TestLoanSourceRotation.t.sol +16 -1
- package/test/TestLoansCashOutDelay.t.sol +482 -0
- package/test/TestLongTailEconomics.t.sol +16 -1
- package/test/TestLowFindings.t.sol +16 -1
- package/test/TestMixedFixes.t.sol +16 -1
- package/test/TestPermit2Signatures.t.sol +16 -1
- package/test/TestReallocationSandwich.t.sol +16 -1
- package/test/TestRevnetRegressions.t.sol +16 -1
- package/test/TestSplitWeightAdjustment.t.sol +43 -19
- package/test/TestSplitWeightE2E.t.sol +26 -3
- package/test/TestSplitWeightFork.t.sol +16 -2
- package/test/TestStageTransitionBorrowable.t.sol +16 -1
- package/test/TestSwapTerminalPermission.t.sol +16 -1
- package/test/TestUint112Overflow.t.sol +16 -1
- package/test/TestZeroRepayment.t.sol +16 -1
- package/test/audit/LoanIdOverflowGuard.t.sol +16 -1
- package/test/fork/ForkTestBase.sol +16 -2
- package/test/fork/TestPermit2PaymentFork.t.sol +4 -3
- package/test/helpers/REVEmpty721Config.sol +0 -1
- package/test/regression/TestBurnPermissionRequired.t.sol +16 -1
- package/test/regression/TestCashOutBuybackFeeLeak.t.sol +15 -1
- package/test/regression/TestCrossRevnetLiquidation.t.sol +16 -1
- package/test/regression/TestCumulativeLoanCounter.t.sol +16 -1
- package/test/regression/TestLiquidateGapHandling.t.sol +16 -1
- package/test/regression/TestZeroPriceFeed.t.sol +16 -1
package/src/REVDeployer.sol
CHANGED
|
@@ -6,26 +6,17 @@ import {IJB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/interfaces/IJB
|
|
|
6
6
|
import {JB721TiersHookFlags} from "@bananapus/721-hook-v6/src/structs/JB721TiersHookFlags.sol";
|
|
7
7
|
import {JBDeploy721TiersHookConfig} from "@bananapus/721-hook-v6/src/structs/JBDeploy721TiersHookConfig.sol";
|
|
8
8
|
import {IJBBuybackHookRegistry} from "@bananapus/buyback-hook-v6/src/interfaces/IJBBuybackHookRegistry.sol";
|
|
9
|
-
import {IJBCashOutHook} from "@bananapus/core-v6/src/interfaces/IJBCashOutHook.sol";
|
|
10
9
|
import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
|
|
11
10
|
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
12
11
|
import {IJBPermissioned} from "@bananapus/core-v6/src/interfaces/IJBPermissioned.sol";
|
|
13
12
|
import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
|
|
14
13
|
import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
|
|
15
14
|
import {IJBRulesetApprovalHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetApprovalHook.sol";
|
|
16
|
-
import {IJBRulesetDataHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetDataHook.sol";
|
|
17
|
-
import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
|
|
18
|
-
import {JBCashOuts} from "@bananapus/core-v6/src/libraries/JBCashOuts.sol";
|
|
19
15
|
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
20
16
|
import {JBSplitGroupIds} from "@bananapus/core-v6/src/libraries/JBSplitGroupIds.sol";
|
|
21
17
|
import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
|
|
22
|
-
import {JBAfterCashOutRecordedContext} from "@bananapus/core-v6/src/structs/JBAfterCashOutRecordedContext.sol";
|
|
23
|
-
import {JBBeforeCashOutRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforeCashOutRecordedContext.sol";
|
|
24
|
-
import {JBBeforePayRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforePayRecordedContext.sol";
|
|
25
|
-
import {JBCashOutHookSpecification} from "@bananapus/core-v6/src/structs/JBCashOutHookSpecification.sol";
|
|
26
18
|
import {JBCurrencyAmount} from "@bananapus/core-v6/src/structs/JBCurrencyAmount.sol";
|
|
27
19
|
import {JBFundAccessLimitGroup} from "@bananapus/core-v6/src/structs/JBFundAccessLimitGroup.sol";
|
|
28
|
-
import {JBPayHookSpecification} from "@bananapus/core-v6/src/structs/JBPayHookSpecification.sol";
|
|
29
20
|
import {JBPermissionsData} from "@bananapus/core-v6/src/structs/JBPermissionsData.sol";
|
|
30
21
|
import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
|
|
31
22
|
import {JBRulesetConfig} from "@bananapus/core-v6/src/structs/JBRulesetConfig.sol";
|
|
@@ -37,13 +28,12 @@ import {IJBSuckerRegistry} from "@bananapus/suckers-v6/src/interfaces/IJBSuckerR
|
|
|
37
28
|
import {CTPublisher} from "@croptop/core-v6/src/CTPublisher.sol";
|
|
38
29
|
import {CTAllowedPost} from "@croptop/core-v6/src/structs/CTAllowedPost.sol";
|
|
39
30
|
import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol";
|
|
40
|
-
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
41
|
-
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
|
42
31
|
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
|
|
43
32
|
import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
|
|
44
33
|
import {mulDiv, sqrt} from "@prb/math/src/Common.sol";
|
|
45
34
|
|
|
46
35
|
import {IREVDeployer} from "./interfaces/IREVDeployer.sol";
|
|
36
|
+
import {REVOwner} from "./REVOwner.sol";
|
|
47
37
|
import {REVAutoIssuance} from "./structs/REVAutoIssuance.sol";
|
|
48
38
|
import {REVConfig} from "./structs/REVConfig.sol";
|
|
49
39
|
import {REVCroptopAllowedPost} from "./structs/REVCroptopAllowedPost.sol";
|
|
@@ -53,16 +43,12 @@ import {REVSuckerDeploymentConfig} from "./structs/REVSuckerDeploymentConfig.sol
|
|
|
53
43
|
|
|
54
44
|
/// @notice `REVDeployer` deploys, manages, and operates Revnets.
|
|
55
45
|
/// @dev Revnets are unowned Juicebox projects which operate autonomously after deployment.
|
|
56
|
-
contract REVDeployer is ERC2771Context, IREVDeployer,
|
|
57
|
-
// A library that adds default safety checks to ERC20 functionality.
|
|
58
|
-
using SafeERC20 for IERC20;
|
|
59
|
-
|
|
46
|
+
contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
60
47
|
//*********************************************************************//
|
|
61
48
|
// --------------------------- custom errors ------------------------- //
|
|
62
49
|
//*********************************************************************//
|
|
63
50
|
|
|
64
51
|
error REVDeployer_AutoIssuanceBeneficiaryZeroAddress();
|
|
65
|
-
error REVDeployer_CashOutDelayNotFinished(uint256 cashOutDelay, uint256 blockTimestamp);
|
|
66
52
|
error REVDeployer_CashOutsCantBeTurnedOffCompletely(uint256 cashOutTaxRate, uint256 maxCashOutTaxRate);
|
|
67
53
|
error REVDeployer_MustHaveSplits();
|
|
68
54
|
error REVDeployer_NothingToAutoIssue();
|
|
@@ -126,6 +112,10 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
|
|
|
126
112
|
/// Participants can borrow up to the current cash out value of their tokens.
|
|
127
113
|
address public immutable override LOANS;
|
|
128
114
|
|
|
115
|
+
/// @notice The runtime data hook contract that handles pay and cash out callbacks for revnets.
|
|
116
|
+
/// @dev Set as `dataHook` in each revnet's ruleset metadata. Implements `IJBRulesetDataHook` and `IJBCashOutHook`.
|
|
117
|
+
address public immutable override OWNER;
|
|
118
|
+
|
|
129
119
|
/// @notice Stores Juicebox project (and revnet) access permissions.
|
|
130
120
|
IJBPermissions public immutable override PERMISSIONS;
|
|
131
121
|
|
|
@@ -152,22 +142,12 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
|
|
|
152
142
|
public
|
|
153
143
|
override amountToAutoIssue;
|
|
154
144
|
|
|
155
|
-
/// @notice The timestamp of when cashouts will become available to a specific revnet's participants.
|
|
156
|
-
/// @dev Only applies to existing revnets which are deploying onto a new network.
|
|
157
|
-
/// @custom:param revnetId The ID of the revnet to get the cash out delay for.
|
|
158
|
-
mapping(uint256 revnetId => uint256 cashOutDelay) public override cashOutDelayOf;
|
|
159
|
-
|
|
160
145
|
/// @notice The hashed encoded configuration of each revnet.
|
|
161
146
|
/// @dev This is used to ensure that the encoded configuration of a revnet is the same when deploying suckers for
|
|
162
147
|
/// omnichain operations.
|
|
163
148
|
/// @custom:param revnetId The ID of the revnet to get the hashed encoded configuration for.
|
|
164
149
|
mapping(uint256 revnetId => bytes32 hashedEncodedConfiguration) public override hashedEncodedConfigurationOf;
|
|
165
150
|
|
|
166
|
-
/// @notice Each revnet's tiered ERC-721 hook.
|
|
167
|
-
/// @custom:param revnetId The ID of the revnet to get the tiered ERC-721 hook for.
|
|
168
|
-
// slither-disable-next-line uninitialized-state
|
|
169
|
-
mapping(uint256 revnetId => IJB721TiersHook tiered721Hook) public override tiered721HookOf;
|
|
170
|
-
|
|
171
151
|
//*********************************************************************//
|
|
172
152
|
// ------------------- internal stored properties -------------------- //
|
|
173
153
|
//*********************************************************************//
|
|
@@ -190,6 +170,7 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
|
|
|
190
170
|
/// @param buybackHook The buyback hook used as a data hook to route payments through buyback pools.
|
|
191
171
|
/// @param loans The loan contract used by all revnets.
|
|
192
172
|
/// @param trustedForwarder The trusted forwarder for the ERC2771Context.
|
|
173
|
+
/// @param owner The runtime data hook contract (REVOwner) that handles pay and cash out callbacks.
|
|
193
174
|
constructor(
|
|
194
175
|
IJBController controller,
|
|
195
176
|
IJBSuckerRegistry suckerRegistry,
|
|
@@ -198,7 +179,8 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
|
|
|
198
179
|
CTPublisher publisher,
|
|
199
180
|
IJBBuybackHookRegistry buybackHook,
|
|
200
181
|
address loans,
|
|
201
|
-
address trustedForwarder
|
|
182
|
+
address trustedForwarder,
|
|
183
|
+
address owner
|
|
202
184
|
)
|
|
203
185
|
ERC2771Context(trustedForwarder)
|
|
204
186
|
{
|
|
@@ -213,6 +195,8 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
|
|
|
213
195
|
BUYBACK_HOOK = buybackHook;
|
|
214
196
|
// slither-disable-next-line missing-zero-check
|
|
215
197
|
LOANS = loans;
|
|
198
|
+
// slither-disable-next-line missing-zero-check
|
|
199
|
+
OWNER = owner;
|
|
216
200
|
|
|
217
201
|
// Give the sucker registry permission to map tokens for all revnets.
|
|
218
202
|
_setPermission({
|
|
@@ -232,192 +216,6 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
|
|
|
232
216
|
// ------------------------- external views -------------------------- //
|
|
233
217
|
//*********************************************************************//
|
|
234
218
|
|
|
235
|
-
/// @notice Determine how a cash out from a revnet should be processed.
|
|
236
|
-
/// @dev This function is part of `IJBRulesetDataHook`, and gets called before the revnet processes a cash out.
|
|
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.
|
|
242
|
-
/// @param context Standard Juicebox cash out context. See `JBBeforeCashOutRecordedContext`.
|
|
243
|
-
/// @return cashOutTaxRate The cash out tax rate, which influences the amount of terminal tokens which get cashed
|
|
244
|
-
/// out.
|
|
245
|
-
/// @return cashOutCount The number of revnet tokens that are cashed out.
|
|
246
|
-
/// @return totalSupply The total revnet token supply.
|
|
247
|
-
/// @return hookSpecifications The amount of funds and the data to send to cash out hooks (this contract).
|
|
248
|
-
function beforeCashOutRecordedWith(JBBeforeCashOutRecordedContext calldata context)
|
|
249
|
-
external
|
|
250
|
-
view
|
|
251
|
-
override
|
|
252
|
-
returns (
|
|
253
|
-
uint256 cashOutTaxRate,
|
|
254
|
-
uint256 cashOutCount,
|
|
255
|
-
uint256 totalSupply,
|
|
256
|
-
JBCashOutHookSpecification[] memory hookSpecifications
|
|
257
|
-
)
|
|
258
|
-
{
|
|
259
|
-
// If the cash out is from a sucker, return the full cash out amount without taxes or fees.
|
|
260
|
-
// This relies on the sucker registry to only contain trusted sucker contracts deployed via
|
|
261
|
-
// the registry's own deploySuckersFor flow — external addresses cannot register as suckers.
|
|
262
|
-
if (_isSuckerOf({revnetId: context.projectId, addr: context.holder})) {
|
|
263
|
-
return (0, context.cashOutCount, context.totalSupply, hookSpecifications);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// Keep a reference to the cash out delay of the revnet.
|
|
267
|
-
uint256 cashOutDelay = cashOutDelayOf[context.projectId];
|
|
268
|
-
|
|
269
|
-
// Enforce the cash out delay.
|
|
270
|
-
if (cashOutDelay > block.timestamp) {
|
|
271
|
-
revert REVDeployer_CashOutDelayNotFinished(cashOutDelay, block.timestamp);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// Get the terminal that will receive the cash out fee.
|
|
275
|
-
IJBTerminal feeTerminal = DIRECTORY.primaryTerminalOf({projectId: FEE_REVNET_ID, token: context.surplus.token});
|
|
276
|
-
|
|
277
|
-
// If there's no cash out tax (100% cash out tax rate), if there's no fee terminal, or if the beneficiary is
|
|
278
|
-
// feeless (e.g. the router terminal routing value between projects), proxy directly to the buyback hook.
|
|
279
|
-
if (context.cashOutTaxRate == 0 || address(feeTerminal) == address(0) || context.beneficiaryIsFeeless) {
|
|
280
|
-
// slither-disable-next-line unused-return
|
|
281
|
-
return BUYBACK_HOOK.beforeCashOutRecordedWith(context);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// Split the cashed-out tokens into a fee portion and a non-fee portion.
|
|
285
|
-
// Micro cash outs (< 40 wei at 2.5% fee) round feeCashOutCount to zero, bypassing the fee.
|
|
286
|
-
// Economically insignificant: the gas cost of the transaction far exceeds the bypassed fee. No fix needed.
|
|
287
|
-
uint256 feeCashOutCount = mulDiv({x: context.cashOutCount, y: FEE, denominator: JBConstants.MAX_FEE});
|
|
288
|
-
uint256 nonFeeCashOutCount = context.cashOutCount - feeCashOutCount;
|
|
289
|
-
|
|
290
|
-
// Calculate how much surplus the non-fee tokens can reclaim via the bonding curve.
|
|
291
|
-
uint256 postFeeReclaimedAmount = JBCashOuts.cashOutFrom({
|
|
292
|
-
surplus: context.surplus.value,
|
|
293
|
-
cashOutCount: nonFeeCashOutCount,
|
|
294
|
-
totalSupply: context.totalSupply,
|
|
295
|
-
cashOutTaxRate: context.cashOutTaxRate
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
// Calculate how much the fee tokens reclaim from the remaining surplus after the non-fee reclaim.
|
|
299
|
-
uint256 feeAmount = JBCashOuts.cashOutFrom({
|
|
300
|
-
surplus: context.surplus.value - postFeeReclaimedAmount,
|
|
301
|
-
cashOutCount: feeCashOutCount,
|
|
302
|
-
totalSupply: context.totalSupply - nonFeeCashOutCount,
|
|
303
|
-
cashOutTaxRate: context.cashOutTaxRate
|
|
304
|
-
});
|
|
305
|
-
|
|
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)
|
|
321
|
-
});
|
|
322
|
-
|
|
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);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
/// @notice Before a revnet processes an incoming payment, determine the weight and pay hooks to use.
|
|
342
|
-
/// @dev This function is part of `IJBRulesetDataHook`, and gets called before the revnet processes a payment.
|
|
343
|
-
/// @param context Standard Juicebox payment context. See `JBBeforePayRecordedContext`.
|
|
344
|
-
/// @return weight The weight which revnet tokens are minted relative to. This can be used to customize how many
|
|
345
|
-
/// tokens get minted by a payment.
|
|
346
|
-
/// @return hookSpecifications Amounts (out of what's being paid in) to be sent to pay hooks instead of being paid
|
|
347
|
-
/// into the revnet. Useful for automatically routing funds from a treasury as payments come in.
|
|
348
|
-
function beforePayRecordedWith(JBBeforePayRecordedContext calldata context)
|
|
349
|
-
external
|
|
350
|
-
view
|
|
351
|
-
override
|
|
352
|
-
returns (uint256 weight, JBPayHookSpecification[] memory hookSpecifications)
|
|
353
|
-
{
|
|
354
|
-
// Get the 721 hook's spec and total split amount.
|
|
355
|
-
IJB721TiersHook tiered721Hook = tiered721HookOf[context.projectId];
|
|
356
|
-
JBPayHookSpecification memory tiered721HookSpec;
|
|
357
|
-
uint256 totalSplitAmount;
|
|
358
|
-
bool usesTiered721Hook = address(tiered721Hook) != address(0);
|
|
359
|
-
if (usesTiered721Hook) {
|
|
360
|
-
JBPayHookSpecification[] memory specs;
|
|
361
|
-
// slither-disable-next-line unused-return
|
|
362
|
-
(, specs) = IJBRulesetDataHook(address(tiered721Hook)).beforePayRecordedWith(context);
|
|
363
|
-
// The 721 hook returns a single spec (itself) whose amount is the total split amount.
|
|
364
|
-
if (specs.length > 0) {
|
|
365
|
-
tiered721HookSpec = specs[0];
|
|
366
|
-
totalSplitAmount = tiered721HookSpec.amount;
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
// The amount entering the project after tier splits.
|
|
371
|
-
uint256 projectAmount = totalSplitAmount >= context.amount.value ? 0 : context.amount.value - totalSplitAmount;
|
|
372
|
-
|
|
373
|
-
// Get the buyback hook's weight and specs. Reduce the amount so it only considers funds entering the project.
|
|
374
|
-
JBPayHookSpecification[] memory buybackHookSpecs;
|
|
375
|
-
{
|
|
376
|
-
JBBeforePayRecordedContext memory buybackHookContext = context;
|
|
377
|
-
buybackHookContext.amount.value = projectAmount;
|
|
378
|
-
(weight, buybackHookSpecs) = BUYBACK_HOOK.beforePayRecordedWith(buybackHookContext);
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// Scale the buyback hook's weight for splits so the terminal mints tokens only for the project's share.
|
|
382
|
-
// The terminal uses the full context.amount.value for minting (tokenCount = amount * weight / weightRatio),
|
|
383
|
-
// but only projectAmount actually enters the project. Without scaling, payers get token credit for the split
|
|
384
|
-
// portion too. Preserves weight=0 from the buyback hook (buying back, not minting).
|
|
385
|
-
if (projectAmount == 0) {
|
|
386
|
-
weight = 0;
|
|
387
|
-
} else if (projectAmount < context.amount.value) {
|
|
388
|
-
weight = mulDiv({x: weight, y: projectAmount, denominator: context.amount.value});
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// Merge hook specifications: 721 hook spec first, then buyback hook spec.
|
|
392
|
-
bool usesBuybackHook = buybackHookSpecs.length > 0;
|
|
393
|
-
hookSpecifications = new JBPayHookSpecification[]((usesTiered721Hook ? 1 : 0) + (usesBuybackHook ? 1 : 0));
|
|
394
|
-
|
|
395
|
-
if (usesTiered721Hook) hookSpecifications[0] = tiered721HookSpec;
|
|
396
|
-
if (usesBuybackHook) hookSpecifications[usesTiered721Hook ? 1 : 0] = buybackHookSpecs[0];
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
/// @notice A flag indicating whether an address has permission to mint a revnet's tokens on-demand.
|
|
400
|
-
/// @dev Required by the `IJBRulesetDataHook` interface.
|
|
401
|
-
/// @param revnetId The ID of the revnet to check permissions for.
|
|
402
|
-
/// @param ruleset The ruleset to check the mint permission for.
|
|
403
|
-
/// @param addr The address to check the mint permission of.
|
|
404
|
-
/// @return flag A flag indicating whether the address has permission to mint the revnet's tokens on-demand.
|
|
405
|
-
function hasMintPermissionFor(
|
|
406
|
-
uint256 revnetId,
|
|
407
|
-
JBRuleset calldata ruleset,
|
|
408
|
-
address addr
|
|
409
|
-
)
|
|
410
|
-
external
|
|
411
|
-
view
|
|
412
|
-
override
|
|
413
|
-
returns (bool)
|
|
414
|
-
{
|
|
415
|
-
// The loans contract, buyback hook (and its delegates), and suckers are allowed to mint the revnet's tokens.
|
|
416
|
-
return addr == LOANS || addr == address(BUYBACK_HOOK)
|
|
417
|
-
|| BUYBACK_HOOK.hasMintPermissionFor({projectId: revnetId, ruleset: ruleset, addr: addr})
|
|
418
|
-
|| _isSuckerOf({revnetId: revnetId, addr: addr});
|
|
419
|
-
}
|
|
420
|
-
|
|
421
219
|
/// @dev Make sure this contract can only receive project NFTs from `JBProjects`.
|
|
422
220
|
function onERC721Received(address, address, uint256, bytes calldata) external view returns (bytes4) {
|
|
423
221
|
// Make sure the 721 received is from the `JBProjects` contract.
|
|
@@ -448,9 +246,8 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
|
|
|
448
246
|
/// @notice Indicates if this contract adheres to the specified interface.
|
|
449
247
|
/// @dev See `IERC165.supportsInterface`.
|
|
450
248
|
/// @return A flag indicating if the provided interface ID is supported.
|
|
451
|
-
function supportsInterface(bytes4 interfaceId) public view virtual
|
|
452
|
-
return interfaceId == type(IREVDeployer).interfaceId || interfaceId == type(
|
|
453
|
-
|| interfaceId == type(IJBCashOutHook).interfaceId || interfaceId == type(IERC721Receiver).interfaceId;
|
|
249
|
+
function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) {
|
|
250
|
+
return interfaceId == type(IREVDeployer).interfaceId || interfaceId == type(IERC721Receiver).interfaceId;
|
|
454
251
|
}
|
|
455
252
|
|
|
456
253
|
//*********************************************************************//
|
|
@@ -466,14 +263,6 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
|
|
|
466
263
|
}
|
|
467
264
|
}
|
|
468
265
|
|
|
469
|
-
/// @notice A flag indicating whether an address is a revnet's sucker.
|
|
470
|
-
/// @param revnetId The ID of the revnet to check sucker status for.
|
|
471
|
-
/// @param addr The address being checked.
|
|
472
|
-
/// @return isSucker A flag indicating whether the address is one of the revnet's suckers.
|
|
473
|
-
function _isSuckerOf(uint256 revnetId, address addr) internal view returns (bool) {
|
|
474
|
-
return SUCKER_REGISTRY.isSuckerOf({projectId: revnetId, addr: addr});
|
|
475
|
-
}
|
|
476
|
-
|
|
477
266
|
/// @notice Initialize fund access limits for the loan contract.
|
|
478
267
|
/// @dev Returns an unlimited surplus allowance for each terminal+token pair derived from the terminal
|
|
479
268
|
/// configurations.
|
|
@@ -539,7 +328,7 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
|
|
|
539
328
|
metadata.useDataHookForPay = true; // Call this contract's `beforePayRecordedWith(…)` callback on payments.
|
|
540
329
|
metadata.useDataHookForCashOut = true; // Call this contract's `beforeCashOutRecordedWith(…)` callback on cash
|
|
541
330
|
// outs.
|
|
542
|
-
metadata.dataHook =
|
|
331
|
+
metadata.dataHook = OWNER; // The REVOwner contract is the data hook.
|
|
543
332
|
metadata.metadata = stageConfiguration.extraMetadata;
|
|
544
333
|
|
|
545
334
|
// Package the reserved token splits.
|
|
@@ -608,6 +397,7 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
|
|
|
608
397
|
sqrtPriceX96 = uint160(1 << 96);
|
|
609
398
|
} else {
|
|
610
399
|
address normalizedTerminalToken = terminalToken == JBConstants.NATIVE_TOKEN ? address(0) : terminalToken;
|
|
400
|
+
// slither-disable-next-line calls-loop
|
|
611
401
|
address projectToken = address(CONTROLLER.TOKENS().tokenOf(revnetId));
|
|
612
402
|
|
|
613
403
|
if (projectToken == address(0) || projectToken == normalizedTerminalToken) {
|
|
@@ -637,64 +427,6 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
|
|
|
637
427
|
// --------------------- external transactions ----------------------- //
|
|
638
428
|
//*********************************************************************//
|
|
639
429
|
|
|
640
|
-
/// @notice Processes the fee from a cash out.
|
|
641
|
-
/// @param context Cash out context passed in by the terminal.
|
|
642
|
-
function afterCashOutRecordedWith(JBAfterCashOutRecordedContext calldata context) external payable {
|
|
643
|
-
// No caller validation needed — this hook only pays fees to the fee project using funds forwarded by the
|
|
644
|
-
// caller. A non-terminal caller would just be donating their own funds as fees. There's nothing to exploit.
|
|
645
|
-
|
|
646
|
-
// If there's sufficient approval, transfer normally.
|
|
647
|
-
if (context.forwardedAmount.token != JBConstants.NATIVE_TOKEN) {
|
|
648
|
-
IERC20(context.forwardedAmount.token)
|
|
649
|
-
.safeTransferFrom({from: msg.sender, to: address(this), value: context.forwardedAmount.value});
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
// Parse the metadata forwarded from the data hook to get the fee terminal.
|
|
653
|
-
// See `beforeCashOutRecordedWith(…)`.
|
|
654
|
-
(IJBTerminal feeTerminal) = abi.decode(context.hookMetadata, (IJBTerminal));
|
|
655
|
-
|
|
656
|
-
// Determine how much to pay in `msg.value` (in the native currency).
|
|
657
|
-
uint256 payValue = _beforeTransferTo({
|
|
658
|
-
to: address(feeTerminal), token: context.forwardedAmount.token, amount: context.forwardedAmount.value
|
|
659
|
-
});
|
|
660
|
-
|
|
661
|
-
// Pay the fee.
|
|
662
|
-
// slither-disable-next-line arbitrary-send-eth,unused-return
|
|
663
|
-
try feeTerminal.pay{value: payValue}({
|
|
664
|
-
projectId: FEE_REVNET_ID,
|
|
665
|
-
token: context.forwardedAmount.token,
|
|
666
|
-
amount: context.forwardedAmount.value,
|
|
667
|
-
beneficiary: context.holder,
|
|
668
|
-
minReturnedTokens: 0,
|
|
669
|
-
memo: "",
|
|
670
|
-
metadata: bytes(abi.encodePacked(context.projectId))
|
|
671
|
-
}) {}
|
|
672
|
-
catch (bytes memory) {
|
|
673
|
-
// Decrease the allowance for the fee terminal if the token is not the native token.
|
|
674
|
-
if (context.forwardedAmount.token != JBConstants.NATIVE_TOKEN) {
|
|
675
|
-
IERC20(context.forwardedAmount.token)
|
|
676
|
-
.safeDecreaseAllowance({
|
|
677
|
-
spender: address(feeTerminal), requestedDecrease: context.forwardedAmount.value
|
|
678
|
-
});
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
// If the fee can't be processed, return the funds to the project.
|
|
682
|
-
payValue = _beforeTransferTo({
|
|
683
|
-
to: msg.sender, token: context.forwardedAmount.token, amount: context.forwardedAmount.value
|
|
684
|
-
});
|
|
685
|
-
|
|
686
|
-
// slither-disable-next-line arbitrary-send-eth
|
|
687
|
-
IJBTerminal(msg.sender).addToBalanceOf{value: payValue}({
|
|
688
|
-
projectId: context.projectId,
|
|
689
|
-
token: context.forwardedAmount.token,
|
|
690
|
-
amount: context.forwardedAmount.value,
|
|
691
|
-
shouldReturnHeldFees: false,
|
|
692
|
-
memo: "",
|
|
693
|
-
metadata: bytes(abi.encodePacked(FEE_REVNET_ID))
|
|
694
|
-
});
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
|
|
698
430
|
/// @notice Auto-mint a revnet's tokens from a stage for a beneficiary.
|
|
699
431
|
/// @param revnetId The ID of the revnet to auto-mint tokens from.
|
|
700
432
|
/// @param stageId The ID of the stage auto-mint tokens are available from.
|
|
@@ -833,8 +565,8 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
|
|
|
833
565
|
});
|
|
834
566
|
}
|
|
835
567
|
|
|
836
|
-
// Store the tiered ERC-721 hook.
|
|
837
|
-
|
|
568
|
+
// Store the tiered ERC-721 hook in the owner contract.
|
|
569
|
+
REVOwner(OWNER).setTiered721HookOf(revnetId, hook);
|
|
838
570
|
|
|
839
571
|
// Grant the split operator all 721 permissions (no prevent* flags for default config).
|
|
840
572
|
_extraOperatorPermissions[revnetId].push(JBPermissionIds.ADJUST_721_TIERS);
|
|
@@ -909,19 +641,6 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
|
|
|
909
641
|
// --------------------- internal transactions ----------------------- //
|
|
910
642
|
//*********************************************************************//
|
|
911
643
|
|
|
912
|
-
/// @notice Logic to be triggered before transferring tokens from this contract.
|
|
913
|
-
/// @param to The address the transfer is going to.
|
|
914
|
-
/// @param token The token being transferred.
|
|
915
|
-
/// @param amount The number of tokens being transferred, as a fixed point number with the same number of decimals
|
|
916
|
-
/// as the token specifies.
|
|
917
|
-
/// @return payValue The value to attach to the transaction being sent.
|
|
918
|
-
function _beforeTransferTo(address to, address token, uint256 amount) internal returns (uint256) {
|
|
919
|
-
// If the token is the native token, no allowance needed.
|
|
920
|
-
if (token == JBConstants.NATIVE_TOKEN) return amount;
|
|
921
|
-
IERC20(token).safeIncreaseAllowance({spender: to, value: amount});
|
|
922
|
-
return 0;
|
|
923
|
-
}
|
|
924
|
-
|
|
925
644
|
/// @notice Deploy a revnet which sells tiered ERC-721s and (optionally) allows croptop posts to its ERC-721 tiers.
|
|
926
645
|
function _deploy721RevnetFor(
|
|
927
646
|
uint256 revnetId,
|
|
@@ -955,7 +674,6 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
|
|
|
955
674
|
tokenUriResolver: tiered721HookConfiguration.baseline721HookConfiguration.tokenUriResolver,
|
|
956
675
|
contractUri: tiered721HookConfiguration.baseline721HookConfiguration.contractUri,
|
|
957
676
|
tiersConfig: tiered721HookConfiguration.baseline721HookConfiguration.tiersConfig,
|
|
958
|
-
reserveBeneficiary: tiered721HookConfiguration.baseline721HookConfiguration.reserveBeneficiary,
|
|
959
677
|
flags: JB721TiersHookFlags({
|
|
960
678
|
noNewTiersWithReserves: tiered721HookConfiguration.baseline721HookConfiguration.flags
|
|
961
679
|
.noNewTiersWithReserves,
|
|
@@ -971,8 +689,8 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
|
|
|
971
689
|
salt: keccak256(abi.encode(tiered721HookConfiguration.salt, encodedConfigurationHash, _msgSender()))
|
|
972
690
|
});
|
|
973
691
|
|
|
974
|
-
// Store the tiered ERC-721 hook.
|
|
975
|
-
|
|
692
|
+
// Store the tiered ERC-721 hook in the owner contract.
|
|
693
|
+
REVOwner(OWNER).setTiered721HookOf(revnetId, hook);
|
|
976
694
|
|
|
977
695
|
// Give the split operator permission to add and remove tiers unless prevented.
|
|
978
696
|
if (!tiered721HookConfiguration.preventSplitOperatorAdjustingTiers) {
|
|
@@ -1320,8 +1038,8 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
|
|
|
1320
1038
|
// Calculate the timestamp at which the cash out delay ends.
|
|
1321
1039
|
uint256 cashOutDelay = block.timestamp + CASH_OUT_DELAY;
|
|
1322
1040
|
|
|
1323
|
-
// Store the cash out delay.
|
|
1324
|
-
|
|
1041
|
+
// Store the cash out delay in the owner contract.
|
|
1042
|
+
REVOwner(OWNER).setCashOutDelayOf(revnetId, cashOutDelay);
|
|
1325
1043
|
|
|
1326
1044
|
emit SetCashOutDelay({revnetId: revnetId, cashOutDelay: cashOutDelay, caller: _msgSender()});
|
|
1327
1045
|
}
|
package/src/REVLoans.sol
CHANGED
|
@@ -28,6 +28,7 @@ import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
|
|
|
28
28
|
import {JBSingleAllowance} from "@bananapus/core-v6/src/structs/JBSingleAllowance.sol";
|
|
29
29
|
|
|
30
30
|
import {IREVLoans} from "./interfaces/IREVLoans.sol";
|
|
31
|
+
import {IREVOwner} from "./interfaces/IREVOwner.sol";
|
|
31
32
|
import {REVLoan} from "./structs/REVLoan.sol";
|
|
32
33
|
import {REVLoanSource} from "./structs/REVLoanSource.sol";
|
|
33
34
|
|
|
@@ -55,6 +56,7 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
55
56
|
// --------------------------- custom errors ------------------------- //
|
|
56
57
|
//*********************************************************************//
|
|
57
58
|
|
|
59
|
+
error REVLoans_CashOutDelayNotFinished(uint256 cashOutDelay, uint256 blockTimestamp);
|
|
58
60
|
error REVLoans_CollateralExceedsLoan(uint256 collateralToReturn, uint256 loanCollateral);
|
|
59
61
|
error REVLoans_InvalidPrepaidFeePercent(uint256 prepaidFeePercent, uint256 min, uint256 max);
|
|
60
62
|
error REVLoans_InvalidTerminal(address terminal, uint256 revnetId);
|
|
@@ -225,6 +227,9 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
225
227
|
view
|
|
226
228
|
returns (uint256)
|
|
227
229
|
{
|
|
230
|
+
// If the cash out delay hasn't passed yet, no amount is borrowable.
|
|
231
|
+
if (_cashOutDelayOf(revnetId) > block.timestamp) return 0;
|
|
232
|
+
|
|
228
233
|
return _borrowableAmountFrom({
|
|
229
234
|
revnetId: revnetId,
|
|
230
235
|
collateralCount: collateralCount,
|
|
@@ -285,6 +290,24 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
285
290
|
// -------------------------- internal views ------------------------- //
|
|
286
291
|
//*********************************************************************//
|
|
287
292
|
|
|
293
|
+
/// @notice Returns the cash out delay timestamp for a revnet by resolving the data hook from the current ruleset.
|
|
294
|
+
/// @param revnetId The ID of the revnet.
|
|
295
|
+
/// @return The cash out delay timestamp. Returns 0 if no data hook is set or no delay exists.
|
|
296
|
+
function _cashOutDelayOf(uint256 revnetId) internal view returns (uint256) {
|
|
297
|
+
// Get the revnet's current ruleset to find its data hook (the REVOwner contract).
|
|
298
|
+
// slither-disable-next-line unused-return
|
|
299
|
+
(JBRuleset memory currentRuleset,) = CONTROLLER.currentRulesetOf(revnetId);
|
|
300
|
+
|
|
301
|
+
// Extract the data hook address from the ruleset's packed metadata.
|
|
302
|
+
address dataHook = currentRuleset.dataHook();
|
|
303
|
+
|
|
304
|
+
// If there's no data hook, this isn't a revnet — no cash out delay applies.
|
|
305
|
+
if (dataHook == address(0)) return 0;
|
|
306
|
+
|
|
307
|
+
// Read the cash out delay from the REVOwner contract (the data hook).
|
|
308
|
+
return IREVOwner(dataHook).cashOutDelayOf(revnetId);
|
|
309
|
+
}
|
|
310
|
+
|
|
288
311
|
/// @notice Checks this contract's balance of a specific token.
|
|
289
312
|
/// @param token The address of the token to get this contract's balance of.
|
|
290
313
|
/// @return This contract's balance.
|
|
@@ -580,6 +603,14 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
580
603
|
);
|
|
581
604
|
}
|
|
582
605
|
|
|
606
|
+
// Enforce the cash out delay.
|
|
607
|
+
{
|
|
608
|
+
uint256 cashOutDelay = _cashOutDelayOf(revnetId);
|
|
609
|
+
if (cashOutDelay > block.timestamp) {
|
|
610
|
+
revert REVLoans_CashOutDelayNotFinished(cashOutDelay, block.timestamp);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
583
614
|
// Prevent the loan number from exceeding the ID namespace for this revnet.
|
|
584
615
|
if (totalLoansBorrowedFor[revnetId] >= _ONE_TRILLION) revert REVLoans_LoanIdOverflow();
|
|
585
616
|
|