@rev-net/core-v6 0.0.50 → 0.0.52

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/CHANGELOG.md CHANGED
@@ -21,6 +21,18 @@ This file describes the verified change from `revnet-core-v5` to the current `re
21
21
  - The v6 test tree is substantially broader than the v5 tree, with dedicated regression, fork, attack, and invariant coverage for loans, cash-outs, split weights, and lifecycle edges.
22
22
  - The repo moved from the v5 `0.8.23` baseline to `0.8.28`.
23
23
 
24
+ ## In-v6 changes
25
+
26
+ ### `0.0.52` — Cap reported surplus on `REVOwner.beforeCashOutRecordedWith` to fit local liquidity
27
+
28
+ PR #149 scaled the fee + reclaim proportionally when the gross global outflow exceeded local terminal liquidity, preserving a nonzero fee. But the data hook still returned the **unscaled** `effectiveSurplusValue` to `JBTerminalStore._cashOutWithDataHook`, which recomputes the beneficiary reclaim as `cashOutFrom(effSurplus, cashOutCount, totalSupply, taxRate)` and caps it at local surplus before adding the fee spec — so `balanceDiff = localSurplus + feeAmount > localSurplus` reverted with `InadequateTerminalStoreBalance`. Omnichain holders could not cash out locally when global surplus dominated.
29
+
30
+ `cashOutFrom` is linear in `surplus`. After the existing PR #149 scaling, `REVOwner` now lowers the reported `effectiveSurplusValue` proportionally so the store's recomputed reclaim is at most `localSurplus - feeAmount`, leaving exact room for the (preserved) fee spec. The buyback hook still receives the full pre-cap global surplus for its routing decision — only the store-facing return is capped.
31
+
32
+ The fee is **never** trimmed or zeroed: that was the regression PR #149 fixed.
33
+
34
+ Integrator impact: omnichain cash-outs that previously reverted with `InadequateTerminalStoreBalance` when local liquidity was the binding cap now settle. The beneficiary receives `localSurplus - feeAmount` and the fee revnet receives `feeAmount`. The user still burns the full `context.cashOutCount` tokens — semantics are the same as the pre-existing local-cap protocol behavior, just now reachable end-to-end.
35
+
24
36
  ## Operator delegation
25
37
 
26
38
  - Added new `JBPermissionIds` for operator delegation in `@bananapus/permission-ids-v6`:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rev-net/core-v6",
3
- "version": "0.0.50",
3
+ "version": "0.0.52",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -28,7 +28,7 @@
28
28
  "dependencies": {
29
29
  "@bananapus/721-hook-v6": "^0.0.47",
30
30
  "@bananapus/buyback-hook-v6": "^0.0.39",
31
- "@bananapus/core-v6": "^0.0.44",
31
+ "@bananapus/core-v6": "^0.0.48",
32
32
  "@bananapus/ownable-v6": "^0.0.24",
33
33
  "@bananapus/permission-ids-v6": "^0.0.24",
34
34
  "@bananapus/router-terminal-v6": "^0.0.37",
package/src/REVLoans.sol CHANGED
@@ -62,6 +62,7 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
62
62
  error REVLoans_InvalidTerminal(address terminal, uint256 revnetId);
63
63
  error REVLoans_LoanExpired(uint256 timeSinceLoanCreated, uint256 loanLiquidationDuration);
64
64
  error REVLoans_LoanIdOverflow(uint256 revnetId, uint256 loanNumber, uint256 maxLoanNumber);
65
+ error REVLoans_LoanOwnerChanged(uint256 loanId, address expectedOwner, address actualOwner);
65
66
  error REVLoans_NewBorrowAmountGreaterThanLoanAmount(uint256 newBorrowAmount, uint256 loanAmount);
66
67
  error REVLoans_NoMsgValueAllowed(uint256 msgValue, address token);
67
68
  error REVLoans_NotEnoughCollateral(uint256 collateralCountToRemove, uint256 loanCollateral);
@@ -884,6 +885,16 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
884
885
  maxRepayBorrowAmount =
885
886
  _acceptFundsFor({token: loan.source.token, amount: maxRepayBorrowAmount, allowance: allowance});
886
887
 
888
+ // Re-check ownership: an ERC-777/ERC-1363 source token can reenter during the transfer above and transfer
889
+ // the loan NFT to another account. Without this check, `_repayLoan` would burn the new owner's NFT while
890
+ // returning collateral to the stale cached owner.
891
+ {
892
+ address currentOwner = _ownerOf(loanId);
893
+ if (currentOwner != loanOwner) {
894
+ revert REVLoans_LoanOwnerChanged({loanId: loanId, expectedOwner: loanOwner, actualOwner: currentOwner});
895
+ }
896
+ }
897
+
887
898
  // Make sure the minimum borrow amount is met.
888
899
  if (repayBorrowAmount > maxRepayBorrowAmount) {
889
900
  revert REVLoans_OverMaxRepayBorrowAmount({
package/src/REVOwner.sol CHANGED
@@ -239,6 +239,12 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook, IJBPeerChainAdjustedAcc
239
239
  cashOutTaxRate: context.cashOutTaxRate
240
240
  });
241
241
 
242
+ // Snapshot the unscaled reclaim before the local-liquidity proportional scaling below mutates it. This is
243
+ // what `JBTerminalStore._cashOutWithDataHook` will recompute when it calls
244
+ // `JBCashOuts.cashOutFrom(effectiveSurplusValue, cashOutCount, totalSupply, cashOutTaxRate)` — same inputs,
245
+ // same output. Used to cap the surplus we report to the store so the recompute leaves room for the fee.
246
+ uint256 unscaledReclaim = postFeeReclaimedAmount;
247
+
242
248
  // If the gross outflow exceeds local terminal liquidity, scale reclaim AND fee proportionally so the fee
243
249
  // is preserved instead of being capped to zero when the reclaim alone consumes all local surplus.
244
250
  uint256 grossOutflow = postFeeReclaimedAmount + feeAmount;
@@ -272,6 +278,37 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook, IJBPeerChainAdjustedAcc
272
278
  return (cashOutTaxRate, cashOutCount, totalSupply, effectiveSurplusValue, buybackHookSpecifications);
273
279
  }
274
280
 
281
+ // The store will recompute the beneficiary reclaim as `cashOutFrom(effectiveSurplusValue, cashOutCount,
282
+ // totalSupply, cashOutTaxRate)` and add the fee spec on top. When local liquidity is the binding cap, that
283
+ // sum can exceed local surplus and revert. `cashOutFrom` is linear in `surplus`, so scale the surplus we
284
+ // report so the store-side reclaim is at most `localSurplus - feeAmount`, preserving room for the fee.
285
+ // The fee is NOT scaled here — it was already scaled by the PR #149 block above. Only the store-facing
286
+ // surplus is touched; the buyback hook already received the full pre-cap value for its routing decision.
287
+ //
288
+ // Worked example (local=10, global=100, 500 of 1000 tokens at 50% tax):
289
+ // above this block (PR #149):
290
+ // unscaledReclaim = cashOutFrom(100, 487.5, 1000, 5000) ≈ 36 ETH (global)
291
+ // feeAmount = cashOutFrom(64, 12.5, 512.5, 5000) ≈ 0.78 ETH (global)
292
+ // grossOutflow ≈ 36.78 > 10 → scale both proportionally to local liquidity:
293
+ // postFeeReclaimedAmount *= 10/36.78 ≈ 9.79 ETH
294
+ // feeAmount *= 10/36.78 ≈ 0.214 ETH (preserved, nonzero)
295
+ // this block:
296
+ // reclaimCap = 10 − 0.214 = 9.786 ETH
297
+ // unscaledReclaim (36) > reclaimCap (9.786) → cap the surplus we report:
298
+ // effectiveSurplusValue = 100 × 9.786 / 36 ≈ 27.18 ETH
299
+ // store recompute (linear in surplus):
300
+ // storeReclaim = 36 × (27.18 / 100) ≈ 9.786 ETH
301
+ // balanceDiff = 9.786 + 0.214 = 10 ETH = localSurplus ✓ no revert
302
+ //
303
+ // Underflow safety on `localSurplus − feeAmount`: after PR #149 the relation
304
+ // `feeAmount ≤ localSurplus` holds in both branches — in the scaling branch because
305
+ // `feeAmount ≤ grossOutflow` and the multiplier is `localSurplus / grossOutflow ≤ 1`;
306
+ // in the else branch because `feeAmount ≤ grossOutflow ≤ localSurplus` already.
307
+ uint256 reclaimCap = context.surplus.value - feeAmount;
308
+ if (unscaledReclaim > reclaimCap) {
309
+ effectiveSurplusValue = mulDiv({x: effectiveSurplusValue, y: reclaimCap, denominator: unscaledReclaim});
310
+ }
311
+
275
312
  // Build a hook spec that routes the fee amount to this contract's `afterCashOutRecordedWith` for processing.
276
313
  JBCashOutHookSpecification memory feeSpec = JBCashOutHookSpecification({
277
314
  hook: IJBCashOutHook(address(this)), noop: false, amount: feeAmount, metadata: abi.encode(feeTerminal)