@rev-net/core-v6 0.0.71 → 0.0.73
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/package.json +1 -1
- package/references/operations.md +2 -2
- package/references/runtime.md +1 -1
- package/src/REVDeployer.sol +34 -4
- package/src/REVLoans.sol +38 -11
- package/src/REVOwner.sol +11 -11
package/package.json
CHANGED
package/references/operations.md
CHANGED
|
@@ -91,7 +91,7 @@ Use this file when you need revnet-specific risks, state reads, constants, or ex
|
|
|
91
91
|
| Constant | Value | Purpose |
|
|
92
92
|
|----------|-------|---------|
|
|
93
93
|
| `CASH_OUT_DELAY` | 2,592,000 (30 days) | Prevents cross-chain liquidity arbitrage on new chain deployments |
|
|
94
|
-
| `FEE` | 25 (of MAX_FEE=1000) | 2.5% cash-out fee paid to fee revnet |
|
|
94
|
+
| `FEE` | 25 (of MAX_FEE=1000) | 2.5% cash-out fee paid to fee revnet for non-zero-tax ordinary cash-outs |
|
|
95
95
|
| `DEFAULT_BUYBACK_POOL_FEE` | 10,000 | 1% Uniswap fee tier for default buyback pools |
|
|
96
96
|
| `DEFAULT_BUYBACK_TWAP_WINDOW` | 2 days | TWAP observation window for buyback price |
|
|
97
97
|
| `DEFAULT_BUYBACK_TICK_SPACING` | 200 | Tick spacing for default buyback V4 pools |
|
|
@@ -147,7 +147,7 @@ Use this file when you need revnet-specific risks, state reads, constants, or ex
|
|
|
147
147
|
4. **Loan ID encoding.** `loanId = revnetId * 1_000_000_000_000 + loanNumber`. Each revnet supports ~1 trillion loans. Use `revnetIdOfLoanWith(loanId)` to decode.
|
|
148
148
|
5. **uint112 truncation risk.** `REVLoan.amount` and `REVLoan.collateral` are `uint112`. Values above ~5.19e33 truncate silently.
|
|
149
149
|
6. **Auto-issuance stage IDs.** Computed as `block.timestamp + i` during deployment. These match the Juicebox ruleset IDs because `JBRulesets` assigns IDs the same way (`latestId >= block.timestamp ? latestId + 1 : block.timestamp`), producing identical sequential IDs when all stages are queued in a single `deployFor()` call.
|
|
150
|
-
7. **Cash-out fee stacking.**
|
|
150
|
+
7. **Cash-out fee stacking.** Non-zero-tax ordinary cash-outs incur both the Juicebox terminal fee (2.5%) and the revnet cash-out fee (2.5% to fee revnet). These compound. The 2.5% revnet fee is deducted from the TOKEN AMOUNT being cashed out, not from the reclaim value. 2.5% of the tokens are redirected to the fee revnet, which then redeems them at the bonding curve independently. The net reclaim to the caller is based on 97.5% of the tokens, not 97.5% of the computed ETH value. Zero-tax ordinary cash-outs route through the buyback hook without adding the revnet fee hook, matching current code behavior. This is by design.
|
|
151
151
|
8. **30-day cash-out delay.** Applied when deploying an existing revnet to a new chain where the first stage has already started. Prevents cross-chain liquidity arbitrage. Enforced in both `beforeCashOutRecordedWith` (direct cash outs) and `REVLoans.borrowFrom` / `borrowableAmountFrom` (loans). The delay is stored on REVOwner (`cashOutDelayOf(revnetId)`) and populated by REVDeployer during deployment via the bundled `initializeRevnet()` call. REVLoans imports IREVOwner (not IREVDeployer) to read it.
|
|
152
152
|
9. **`cashOutTaxRate` cannot be MAX.** Must be strictly less than `MAX_CASH_OUT_TAX_RATE` (10,000). Revnets cannot fully disable cash outs.
|
|
153
153
|
10. **Split operator is singular.** Only ONE address can be operator at a time. The operator can replace itself via `setOperatorOf` but cannot delegate or multi-sig.
|
package/references/runtime.md
CHANGED
|
@@ -29,7 +29,7 @@ Deploy and manage Revnets -- autonomous, unowned Juicebox projects with staged i
|
|
|
29
29
|
| Function | Permissions | What it does |
|
|
30
30
|
|----------|------------|-------------|
|
|
31
31
|
| `REVOwner.beforePayRecordedWith(context)` | Terminal callback | Calls the 721 hook first for split specs, then calls the buyback hook with a reduced amount context (payment minus split amount). Adjusts the returned weight proportionally for splits (`weight = mulDiv(weight, amount - splitAmount, amount)`) so the terminal only mints tokens for the amount entering the project. Assembles pay hook specs (721 hook specs first, then buyback spec). Reads `tiered721HookOf` from REVOwner storage. |
|
|
32
|
-
| `REVOwner.beforeCashOutRecordedWith(context)` | Terminal callback | If sucker: returns full amount with 0 tax (fee exempt). Otherwise:
|
|
32
|
+
| `REVOwner.beforeCashOutRecordedWith(context)` | Terminal callback | If sucker: returns full amount with 0 tax (fee exempt). Otherwise: enforces the 30-day cash-out delay, routes zero-tax ordinary cash-outs through the buyback hook without a revnet fee, and only calculates the 2.5% revnet fee when `cashOutTaxRate != 0`. |
|
|
33
33
|
| `REVOwner.afterCashOutRecordedWith(context)` | Permissionless | Cash-out hook callback. Receives fee amount and pays it to the fee revnet's terminal. Falls back to returning funds if fee payment fails. |
|
|
34
34
|
| `REVOwner.hasMintPermissionFor(revnetId, ruleset, addr)` | View | Returns `true` for: loans contract, buyback hook, buyback hook delegates, or suckers. |
|
|
35
35
|
| `REVOwner.cashOutDelayOf(revnetId)` | View | Returns the cash-out delay timestamp from REVOwner storage. Exposed for REVLoans compatibility (REVLoans imports IREVOwner for this). |
|
package/src/REVDeployer.sol
CHANGED
|
@@ -24,6 +24,7 @@ import {JBRulesetConfig} from "@bananapus/core-v6/src/structs/JBRulesetConfig.so
|
|
|
24
24
|
import {JBRulesetMetadata} from "@bananapus/core-v6/src/structs/JBRulesetMetadata.sol";
|
|
25
25
|
import {JBSplitGroup} from "@bananapus/core-v6/src/structs/JBSplitGroup.sol";
|
|
26
26
|
import {JBTerminalConfig} from "@bananapus/core-v6/src/structs/JBTerminalConfig.sol";
|
|
27
|
+
import {JBOwnable} from "@bananapus/ownable-v6/src/JBOwnable.sol";
|
|
27
28
|
import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
|
|
28
29
|
import {IJBSuckerRegistry} from "@bananapus/suckers-v6/src/interfaces/IJBSuckerRegistry.sol";
|
|
29
30
|
import {CTPublisher} from "@croptop/core-v6/src/CTPublisher.sol";
|
|
@@ -345,6 +346,27 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
345
346
|
}
|
|
346
347
|
}
|
|
347
348
|
|
|
349
|
+
/// @notice Calculate a Uniswap V4 sqrt price, returning zero if the ratio is out of range.
|
|
350
|
+
/// @param numerator The numerator of the raw token price ratio.
|
|
351
|
+
/// @param denominator The denominator of the raw token price ratio.
|
|
352
|
+
/// @return sqrtPriceX96 The encoded sqrt price, or zero when it cannot be represented.
|
|
353
|
+
function _sqrtPriceX96From(uint256 numerator, uint256 denominator) internal pure returns (uint160 sqrtPriceX96) {
|
|
354
|
+
// Q192 is the fixed-point scale Uniswap uses before taking the square root.
|
|
355
|
+
uint256 q192 = 1 << 192;
|
|
356
|
+
|
|
357
|
+
// `mulDiv` reverts if `numerator * Q192 / denominator` exceeds uint256.
|
|
358
|
+
uint256 maxRatio = type(uint256).max / q192;
|
|
359
|
+
|
|
360
|
+
// Cap the numerator at a conservative bound that keeps the scaled ratio representable.
|
|
361
|
+
uint256 maxNumerator = denominator > type(uint256).max / maxRatio ? type(uint256).max : maxRatio * denominator;
|
|
362
|
+
|
|
363
|
+
// A zero denominator is invalid, and an out-of-range numerator means this pool price should be skipped.
|
|
364
|
+
if (denominator == 0 || numerator > maxNumerator) return 0;
|
|
365
|
+
|
|
366
|
+
// The bounded ratio fits in uint256, and its square root always fits in uint160.
|
|
367
|
+
sqrtPriceX96 = uint160(sqrt(mulDiv({x: numerator, y: q192, denominator: denominator})));
|
|
368
|
+
}
|
|
369
|
+
|
|
348
370
|
/// @notice Try to initialize a Uniswap V4 buyback pool for a terminal token at its fair issuance price.
|
|
349
371
|
/// @dev Called after the ERC-20 token is deployed so the pool can be initialized in the PoolManager.
|
|
350
372
|
/// Computes `sqrtPriceX96` from `initialIssuance` so the pool starts at the same price as the bonding curve.
|
|
@@ -406,13 +428,15 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
406
428
|
sqrtPriceX96 = uint160(1 << 96);
|
|
407
429
|
} else if (normalizedTerminalToken < projectToken) {
|
|
408
430
|
// token0 = terminal, token1 = project → price = adjustedIssuance / terminalTokenUnit
|
|
409
|
-
sqrtPriceX96 =
|
|
410
|
-
uint160(sqrt(mulDiv({x: adjustedInitialIssuance, y: 1 << 192, denominator: terminalTokenUnit})));
|
|
431
|
+
sqrtPriceX96 = _sqrtPriceX96From({numerator: adjustedInitialIssuance, denominator: terminalTokenUnit});
|
|
411
432
|
} else {
|
|
412
433
|
// token0 = project, token1 = terminal → price = terminalTokenUnit / adjustedIssuance
|
|
413
|
-
sqrtPriceX96 =
|
|
414
|
-
uint160(sqrt(mulDiv({x: terminalTokenUnit, y: 1 << 192, denominator: adjustedInitialIssuance})));
|
|
434
|
+
sqrtPriceX96 = _sqrtPriceX96From({numerator: terminalTokenUnit, denominator: adjustedInitialIssuance});
|
|
415
435
|
}
|
|
436
|
+
|
|
437
|
+
// Some extreme cross-currency prices are outside Uniswap's usable sqrt-price range. In those cases,
|
|
438
|
+
// leave the pool uninitialized instead of reverting the whole revnet deployment.
|
|
439
|
+
if (sqrtPriceX96 == 0) return;
|
|
416
440
|
}
|
|
417
441
|
|
|
418
442
|
try BUYBACK_HOOK.initializePoolFor({
|
|
@@ -528,6 +552,9 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
528
552
|
});
|
|
529
553
|
}
|
|
530
554
|
|
|
555
|
+
// Scope the hook's permissions to REVOwner, where the operator permissions are granted.
|
|
556
|
+
JBOwnable(address(hook)).transferOwnership(OWNER);
|
|
557
|
+
|
|
531
558
|
// Grant the operator all 721 permissions (no prevent* flags for default config).
|
|
532
559
|
ownerInit.extraOperatorPermissionIds = new uint256[](4);
|
|
533
560
|
ownerInit.extraOperatorPermissionIds[0] = JBPermissionIds.ADJUST_721_TIERS;
|
|
@@ -630,6 +657,9 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
630
657
|
salt: keccak256(abi.encode(tiered721HookConfiguration.salt, encodedConfigurationHash, _msgSender()))
|
|
631
658
|
});
|
|
632
659
|
|
|
660
|
+
// Scope the hook's permissions to REVOwner, where the operator permissions are granted.
|
|
661
|
+
JBOwnable(address(hook)).transferOwnership(OWNER);
|
|
662
|
+
|
|
633
663
|
// Build the 721 permission additions based on the deployer's `preventOperator*` flags.
|
|
634
664
|
{
|
|
635
665
|
uint256 extraCount;
|
package/src/REVLoans.sol
CHANGED
|
@@ -4,6 +4,7 @@ pragma solidity 0.8.28;
|
|
|
4
4
|
import {JBPermissioned} from "@bananapus/core-v6/src/abstract/JBPermissioned.sol";
|
|
5
5
|
import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
|
|
6
6
|
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
7
|
+
import {IJBMultiTerminal} from "@bananapus/core-v6/src/interfaces/IJBMultiTerminal.sol";
|
|
7
8
|
import {IJBPayoutTerminal} from "@bananapus/core-v6/src/interfaces/IJBPayoutTerminal.sol";
|
|
8
9
|
import {IJBPermissioned} from "@bananapus/core-v6/src/interfaces/IJBPermissioned.sol";
|
|
9
10
|
import {IJBPrices} from "@bananapus/core-v6/src/interfaces/IJBPrices.sol";
|
|
@@ -58,6 +59,7 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
58
59
|
|
|
59
60
|
error REVLoans_CashOutDelayNotFinished(uint256 cashOutDelay, uint256 blockTimestamp);
|
|
60
61
|
error REVLoans_CollateralExceedsLoan(uint256 collateralToReturn, uint256 loanCollateral);
|
|
62
|
+
error REVLoans_FeeOnTransferSourceUnsupported(address token, uint256 expectedAmount, uint256 creditedAmount);
|
|
61
63
|
error REVLoans_InvalidAccountingContext(uint256 revnetId, address token);
|
|
62
64
|
error REVLoans_InvalidPrepaidFeePercent(uint256 prepaidFeePercent, uint256 min, uint256 max);
|
|
63
65
|
error REVLoans_LoanExpired(uint256 timeSinceLoanCreated, uint256 loanLiquidationDuration);
|
|
@@ -682,11 +684,20 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
682
684
|
override
|
|
683
685
|
nonReentrantLoanAction
|
|
684
686
|
{
|
|
685
|
-
//
|
|
686
|
-
|
|
687
|
-
|
|
687
|
+
// No loans are checked when count is zero.
|
|
688
|
+
if (count == 0) return;
|
|
689
|
+
|
|
690
|
+
// Prevent cross-revnet accounting corruption: every iterated loan number must stay within the revnet's ID
|
|
691
|
+
// namespace.
|
|
692
|
+
if (startingLoanId > _ONE_TRILLION) {
|
|
688
693
|
revert REVLoans_LoanIdOverflow({
|
|
689
|
-
revnetId: revnetId, loanNumber:
|
|
694
|
+
revnetId: revnetId, loanNumber: startingLoanId, maxLoanNumber: _ONE_TRILLION
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
uint256 maxCount = _ONE_TRILLION - startingLoanId + 1;
|
|
698
|
+
if (count > maxCount) {
|
|
699
|
+
revert REVLoans_LoanIdOverflow({
|
|
700
|
+
revnetId: revnetId, loanNumber: _ONE_TRILLION + 1, maxLoanNumber: _ONE_TRILLION
|
|
690
701
|
});
|
|
691
702
|
}
|
|
692
703
|
|
|
@@ -1196,6 +1207,12 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1196
1207
|
|
|
1197
1208
|
// INTERACTIONS: Execute external calls with pre-computed deltas.
|
|
1198
1209
|
|
|
1210
|
+
// Burn newly added collateral before pulling source funds so fee-project token mints from the borrow cannot
|
|
1211
|
+
// be used as same-transaction collateral.
|
|
1212
|
+
if (addedCollateralCount > 0) {
|
|
1213
|
+
_addCollateralTo({revnetId: revnetId, amount: addedCollateralCount, holder: holder});
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1199
1216
|
// Add to the loan if needed...
|
|
1200
1217
|
if (addedBorrowAmount > 0) {
|
|
1201
1218
|
_addTo({
|
|
@@ -1210,11 +1227,8 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1210
1227
|
_removeFrom({loan: loan, revnetId: revnetId, repaidBorrowAmount: repaidBorrowAmount});
|
|
1211
1228
|
}
|
|
1212
1229
|
|
|
1213
|
-
//
|
|
1214
|
-
if (
|
|
1215
|
-
_addCollateralTo({revnetId: revnetId, amount: addedCollateralCount, holder: holder});
|
|
1216
|
-
// ... or return collateral if needed.
|
|
1217
|
-
} else if (returnedCollateralCount > 0) {
|
|
1230
|
+
// Return collateral if needed.
|
|
1231
|
+
if (returnedCollateralCount > 0) {
|
|
1218
1232
|
_returnCollateralFrom({
|
|
1219
1233
|
revnetId: revnetId, collateralCount: returnedCollateralCount, beneficiary: beneficiary
|
|
1220
1234
|
});
|
|
@@ -1477,8 +1491,10 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1477
1491
|
function _removeFrom(REVLoan memory loan, uint256 revnetId, uint256 repaidBorrowAmount) internal {
|
|
1478
1492
|
address sourceToken = loan.sourceToken;
|
|
1479
1493
|
|
|
1480
|
-
|
|
1481
|
-
|
|
1494
|
+
IJBMultiTerminal terminal = IJBMultiTerminal(address(TERMINAL));
|
|
1495
|
+
|
|
1496
|
+
// Snapshot the credited terminal balance so fee-on-transfer source tokens cannot under-repay the revnet.
|
|
1497
|
+
uint256 balanceBefore = terminal.STORE().balanceOf(address(TERMINAL), revnetId, sourceToken);
|
|
1482
1498
|
|
|
1483
1499
|
// Increase the allowance for the beneficiary.
|
|
1484
1500
|
uint256 payValue = _beforeTransferTo({to: address(TERMINAL), token: sourceToken, amount: repaidBorrowAmount});
|
|
@@ -1493,6 +1509,17 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1493
1509
|
metadata: bytes(abi.encodePacked(REV_ID))
|
|
1494
1510
|
});
|
|
1495
1511
|
|
|
1512
|
+
uint256 creditedAmount = terminal.STORE().balanceOf(address(TERMINAL), revnetId, sourceToken) - balanceBefore;
|
|
1513
|
+
|
|
1514
|
+
if (creditedAmount != repaidBorrowAmount) {
|
|
1515
|
+
revert REVLoans_FeeOnTransferSourceUnsupported({
|
|
1516
|
+
token: sourceToken, expectedAmount: repaidBorrowAmount, creditedAmount: creditedAmount
|
|
1517
|
+
});
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
// Decrement the total amount of a token being loaned out by the revnet.
|
|
1521
|
+
totalBorrowedFrom[revnetId][sourceToken] -= repaidBorrowAmount;
|
|
1522
|
+
|
|
1496
1523
|
_afterTransferTo({to: address(TERMINAL), token: sourceToken});
|
|
1497
1524
|
}
|
|
1498
1525
|
|
package/src/REVOwner.sol
CHANGED
|
@@ -41,8 +41,8 @@ import {REVOwnerRevnetInit} from "./structs/REVOwnerRevnetInit.sol";
|
|
|
41
41
|
/// @notice The runtime hook for all revnets — set as every revnet's `dataHook` in ruleset metadata. At pay time, it
|
|
42
42
|
/// coordinates the 721 hook (NFT tier minting) with the buyback hook (secondary market swap routing) and scales weight
|
|
43
43
|
/// for split deductions. At cash-out time, it aggregates cross-chain total supply and surplus (including outstanding
|
|
44
|
-
/// loan debt and collateral), grants suckers 0% tax, splits a 2.5% fee from non-sucker cash outs
|
|
45
|
-
/// proceeds to the fee revnet via `afterCashOutRecordedWith`.
|
|
44
|
+
/// loan debt and collateral), grants suckers 0% tax, splits a 2.5% fee from non-sucker cash outs with a non-zero
|
|
45
|
+
/// cash-out tax, and routes fee proceeds to the fee revnet via `afterCashOutRecordedWith`.
|
|
46
46
|
/// @dev Separated from `REVDeployer` to stay within the EIP-170 contract size limit. Also implements
|
|
47
47
|
/// `IJBPeerChainAdjustedAccounts` to expose loan state to peer-chain supply/surplus snapshots.
|
|
48
48
|
contract REVOwner is IREVOwner, IJBRulesetDataHook, IJBCashOutHook, IJBPeerChainAdjustedAccounts, IERC721Receiver {
|
|
@@ -167,12 +167,12 @@ contract REVOwner is IREVOwner, IJBRulesetDataHook, IJBCashOutHook, IJBPeerChain
|
|
|
167
167
|
|
|
168
168
|
/// @notice Called before a cash out is recorded. Suckers get 0% tax (bridged tokens redeem at face value). For
|
|
169
169
|
/// regular holders, aggregates cross-chain total supply and surplus (including outstanding loan debt/collateral),
|
|
170
|
-
/// splits a 2.5% fee from the cashed-out token count
|
|
171
|
-
/// and the fee portion, then delegates to the buyback hook for potential
|
|
172
|
-
///
|
|
173
|
-
///
|
|
174
|
-
/// fee (2.5%) applies on top of the rev fee. The fee hook spec amount sent to
|
|
175
|
-
/// the protocol fee deducted by the terminal before reaching this contract.
|
|
170
|
+
/// splits a 2.5% fee from the cashed-out token count when cash-out tax is non-zero, computes bonding curve
|
|
171
|
+
/// reclaims for both the holder's portion and the fee portion, then delegates to the buyback hook for potential
|
|
172
|
+
/// swap routing.
|
|
173
|
+
/// @dev Part of `IJBRulesetDataHook`. In the non-zero-tax fee path, REVOwner is intentionally not registered as a
|
|
174
|
+
/// feeless address — the protocol fee (2.5%) applies on top of the rev fee. The fee hook spec amount sent to
|
|
175
|
+
/// `afterCashOutRecordedWith` will have the protocol fee deducted by the terminal before reaching this contract.
|
|
176
176
|
/// @param context Standard Juicebox cash out context. See `JBBeforeCashOutRecordedContext`.
|
|
177
177
|
/// @return cashOutTaxRate The cash out tax rate, which influences the amount of terminal tokens reclaimed.
|
|
178
178
|
/// @return cashOutCount The number of revnet tokens to cash out.
|
|
@@ -232,9 +232,9 @@ contract REVOwner is IREVOwner, IJBRulesetDataHook, IJBCashOutHook, IJBPeerChain
|
|
|
232
232
|
});
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
-
// If there's no cash out tax
|
|
236
|
-
//
|
|
237
|
-
//
|
|
235
|
+
// If there's no cash out tax, if there's no fee terminal, or if the beneficiary is feeless (e.g. the router
|
|
236
|
+
// terminal routing value between projects), proxy to the buyback hook with our totalSupply and
|
|
237
|
+
// effectiveSurplusValue. Zero-tax ordinary cash-outs do not add the revnet fee hook.
|
|
238
238
|
if (context.cashOutTaxRate == 0 || address(feeTerminal) == address(0) || context.beneficiaryIsFeeless) {
|
|
239
239
|
// Build a modified context with cross-chain-adjusted values so the buyback hook sees the global state
|
|
240
240
|
// for its swap-vs-passthrough routing decision.
|