@rev-net/core-v6 0.0.48 → 0.0.51

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.48",
3
+ "version": "0.0.51",
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",
@@ -436,7 +436,18 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
436
436
  terminalToken: terminalToken,
437
437
  sqrtPriceX96: sqrtPriceX96
438
438
  }) {}
439
- catch {} // Pool may already be initialized — that's OK.
439
+ catch {
440
+ // Two failure modes both end up here and both must NOT block the revnet deploy:
441
+ // 1. The V4 pool is already initialized at the expected price (idempotent re-deploy). The buyback
442
+ // hook's strict price check inside `initializePoolFor` would still call `_setPoolFor`, so the
443
+ // try-branch already covered this. We reach this catch only when the check rejected the existing
444
+ // price.
445
+ // 2. The V4 pool was front-run and pre-initialized at an attacker-chosen `sqrtPriceX96`. The buyback
446
+ // hook's strict price check reverts with `JBBuybackHook_PoolInitializedAtWrongPrice`, which lands
447
+ // here. We deliberately swallow that revert so an attacker cannot DoS the revnet deploy by
448
+ // squatting on the predictable pool address — the revnet ships without buyback configured and
449
+ // can be configured manually post-deploy.
450
+ }
440
451
  }
441
452
 
442
453
  //*********************************************************************//
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
@@ -222,20 +222,14 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook, IJBPeerChainAdjustedAcc
222
222
  uint256 feeCashOutCount = mulDiv({x: context.cashOutCount, y: FEE, denominator: JBConstants.MAX_FEE});
223
223
  uint256 nonFeeCashOutCount = context.cashOutCount - feeCashOutCount;
224
224
 
225
- // Calculate how much surplus the non-fee tokens can reclaim via the bonding curve.
226
- // Use effective (cross-chain) surplus; cap at local surplus.
225
+ // Compute the gross (effective-surplus) reclaim and fee amounts. The bonding curve uses cross-chain effective
226
+ // surplus, which can exceed what this chain's terminal actually holds.
227
227
  uint256 postFeeReclaimedAmount = JBCashOuts.cashOutFrom({
228
228
  surplus: effectiveSurplusValue,
229
229
  cashOutCount: nonFeeCashOutCount,
230
230
  totalSupply: totalSupply,
231
231
  cashOutTaxRate: context.cashOutTaxRate
232
232
  });
233
- // Cap at local surplus — the bonding curve uses cross-chain effective surplus which can exceed what this
234
- // chain's terminal actually holds.
235
- if (postFeeReclaimedAmount > context.surplus.value) postFeeReclaimedAmount = context.surplus.value;
236
-
237
- // Calculate how much the fee tokens reclaim from the remaining surplus after the non-fee reclaim.
238
- // Use remaining effective surplus; cap at remaining local surplus.
239
233
  uint256 feeAmount = JBCashOuts.cashOutFrom({
240
234
  surplus: effectiveSurplusValue > postFeeReclaimedAmount
241
235
  ? effectiveSurplusValue - postFeeReclaimedAmount
@@ -244,11 +238,21 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook, IJBPeerChainAdjustedAcc
244
238
  totalSupply: totalSupply - nonFeeCashOutCount,
245
239
  cashOutTaxRate: context.cashOutTaxRate
246
240
  });
247
- // Cap the fee reclaim at remaining local surplus. The bonding curve uses the cross-chain effective surplus,
248
- // which can exceed what's actually held locally. Without this cap, the terminal would try to send more than
249
- // it has.
250
- if (feeAmount > context.surplus.value - postFeeReclaimedAmount) {
251
- feeAmount = context.surplus.value - postFeeReclaimedAmount;
241
+
242
+ // If the gross outflow exceeds local terminal liquidity, scale reclaim AND fee proportionally so the fee
243
+ // is preserved instead of being capped to zero when the reclaim alone consumes all local surplus.
244
+ uint256 grossOutflow = postFeeReclaimedAmount + feeAmount;
245
+ if (grossOutflow > context.surplus.value) {
246
+ if (grossOutflow == 0) {
247
+ // Defensive — both grossOutflow > localSurplus and grossOutflow == 0 can't both hold, but keep
248
+ // the explicit branch so future edits do not divide by zero.
249
+ postFeeReclaimedAmount = 0;
250
+ feeAmount = 0;
251
+ } else {
252
+ uint256 localSurplus = context.surplus.value;
253
+ postFeeReclaimedAmount = mulDiv({x: postFeeReclaimedAmount, y: localSurplus, denominator: grossOutflow});
254
+ feeAmount = mulDiv({x: feeAmount, y: localSurplus, denominator: grossOutflow});
255
+ }
252
256
  }
253
257
 
254
258
  // Build a context for the buyback hook using the non-fee token count and cross-chain-adjusted values