@rev-net/core-v6 0.0.72 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rev-net/core-v6",
3
- "version": "0.0.72",
3
+ "version": "0.0.73",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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.** 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% 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. This is by design.
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.
@@ -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: calculates 2.5% fee, enforces 30-day cash-out delay (reads `cashOutDelayOf` from REVOwner storage), returns modified count + fee hook spec. |
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). |
@@ -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
- // Prevent cross-revnet accounting corruption: loan numbers must stay within the revnet's ID namespace.
686
- uint256 endLoanNumber = startingLoanId + count;
687
- if (endLoanNumber > _ONE_TRILLION) {
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: endLoanNumber, maxLoanNumber: _ONE_TRILLION
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
- // Add collateral if needed...
1214
- if (addedCollateralCount > 0) {
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
- // Decrement the total amount of a token being loaned out by the revnet.
1481
- totalBorrowedFrom[revnetId][sourceToken] -= repaidBorrowAmount;
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, and routes fee
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, computes bonding curve reclaims for both the holder's portion
171
- /// and the fee portion, then delegates to the buyback hook for potential swap routing.
172
- /// @dev Part of `IJBRulesetDataHook`. REVOwner is intentionally not registered as a feeless address — the
173
- /// protocol
174
- /// fee (2.5%) applies on top of the rev fee. The fee hook spec amount sent to `afterCashOutRecordedWith` will have
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 (100% cash out tax rate), if there's no fee terminal, or if the beneficiary is
236
- // feeless (e.g. the router terminal routing value between projects), proxy to the buyback hook with our
237
- // totalSupply and effectiveSurplusValue.
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.