From c74febe2337e9afdc1933adab7c182401f59612b Mon Sep 17 00:00:00 2001 From: andy Date: Wed, 29 Apr 2026 11:24:36 -0400 Subject: [PATCH 1/6] add mppc to poolsv3 --- .../LenderCommitmentGroup_Pool_V3.sol | 26 +- ...LenderCommitmentGroup_Pool_V3_Override.sol | 10 +- .../LenderCommitmentGroup_Pool_V3_Test.sol | 98 ++++++ .../tests_fork/NFT_FlashSwapRollover_Test.sol | 324 ++++++++++++++++++ 4 files changed, 451 insertions(+), 7 deletions(-) create mode 100644 packages/contracts/tests_fork/NFT_FlashSwapRollover_Test.sol diff --git a/packages/contracts/contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Pool_V3.sol b/packages/contracts/contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Pool_V3.sol index f9473cbcc..9305f89cb 100644 --- a/packages/contracts/contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Pool_V3.sol +++ b/packages/contracts/contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Pool_V3.sol @@ -159,7 +159,7 @@ contract LenderCommitmentGroup_Pool_V3 is //configured by the owner. If 0 , not used. - uint256 public maxPrincipalPerCollateralAmount; // DEPRECATED FOR NOW + uint256 public maxPrincipalPerCollateralAmount; uint256 public lastUnpausedAt; @@ -718,14 +718,18 @@ contract LenderCommitmentGroup_Pool_V3 is // principalPerCollateralAmount uint256 priceRatioQ96 = IPriceAdapter( priceAdapter ) .getPriceRatioQ96(priceRouteHash); - - - + + uint256 principalPerCollateralAmount = maxPrincipalPerCollateralAmount == 0 + ? priceRatioQ96 + : MathUpgradeable.min( + priceRatioQ96, + maxPrincipalPerCollateralAmount + ); return getRequiredCollateral( principalAmount, - priceRatioQ96 // principalPerCollateralAmount + principalPerCollateralAmount ); } @@ -1100,7 +1104,17 @@ contract LenderCommitmentGroup_Pool_V3 is - // ------------------------ Pausing functions ------------ + /** + * @notice Sets an optional manual cap for principal/collateral price ratio. Only Pool Owner. + * @param _maxPrincipalPerCollateralAmount Price ratio expanded by Q96. If 0, only oracle price is used. + */ + function setMaxPrincipalPerCollateralAmount(uint256 _maxPrincipalPerCollateralAmount) + external + onlyOwner { + maxPrincipalPerCollateralAmount = _maxPrincipalPerCollateralAmount; + } + + // ------------------------ Pausing functions ------------ diff --git a/packages/contracts/tests/SmartCommitmentForwarder/LenderCommitmentGroup_Pool_V3_Override.sol b/packages/contracts/tests/SmartCommitmentForwarder/LenderCommitmentGroup_Pool_V3_Override.sol index 2457ae484..1a5c43593 100644 --- a/packages/contracts/tests/SmartCommitmentForwarder/LenderCommitmentGroup_Pool_V3_Override.sol +++ b/packages/contracts/tests/SmartCommitmentForwarder/LenderCommitmentGroup_Pool_V3_Override.sol @@ -8,6 +8,7 @@ import { LenderCommitmentGroup_Pool_V3 } from "../../contracts/LenderCommitmentF contract LenderCommitmentGroup_Pool_V3_Override is LenderCommitmentGroup_Pool_V3 { uint256 mockRequiredCollateralAmount; + bool useRealGetRequiredCollateral; uint256 mockSharesExchangeRate; int256 mockMinimumAmountDifferenceToCloseDefaultedLoan; @@ -101,6 +102,10 @@ contract LenderCommitmentGroup_Pool_V3_Override is LenderCommitmentGroup_Pool_V3 mockRequiredCollateralAmount = amt; } + function set_useRealGetRequiredCollateral(bool _use) public { + useRealGetRequiredCollateral = _use; + } + function force_mint_shares(address guy, uint256 wad) public { return super.mintShares(guy, wad); } @@ -134,8 +139,11 @@ contract LenderCommitmentGroup_Pool_V3_Override is LenderCommitmentGroup_Pool_V3 function getRequiredCollateral( uint256 _principalAmount, - uint256 maxPrincipalPerCollateralAmount + uint256 _maxPrincipalPerCollateralAmountQ96 ) internal view override returns (uint256) { + if (useRealGetRequiredCollateral) { + return super.getRequiredCollateral(_principalAmount, _maxPrincipalPerCollateralAmountQ96); + } return mockRequiredCollateralAmount; } diff --git a/packages/contracts/tests/SmartCommitmentForwarder/LenderCommitmentGroup_Pool_V3_Test.sol b/packages/contracts/tests/SmartCommitmentForwarder/LenderCommitmentGroup_Pool_V3_Test.sol index 960daa8a4..08dcb7a5b 100644 --- a/packages/contracts/tests/SmartCommitmentForwarder/LenderCommitmentGroup_Pool_V3_Test.sol +++ b/packages/contracts/tests/SmartCommitmentForwarder/LenderCommitmentGroup_Pool_V3_Test.sol @@ -1290,6 +1290,7 @@ contract LenderCommitmentGroup_Pool_V3_Test is Testable { function test_calculateCollateralTokensAmountEquivalentToPrincipalTokens() public { initialize_group_contract(); + lenderCommitmentGroupV3.set_useRealGetRequiredCollateral(true); // With Q96 price (1:1), 1000 principal should need 1000 collateral _mockPriceAdapter.setMockPriceRatioQ96(Q96); @@ -1300,6 +1301,7 @@ contract LenderCommitmentGroup_Pool_V3_Test is Testable { function test_calculateCollateralRequiredToBorrowPrincipal() public { initialize_group_contract(); + lenderCommitmentGroupV3.set_useRealGetRequiredCollateral(true); // collateralRatio = 10000 (100%), price is 1:1 _mockPriceAdapter.setMockPriceRatioQ96(Q96); @@ -1311,6 +1313,7 @@ contract LenderCommitmentGroup_Pool_V3_Test is Testable { function test_calculateCollateralTokensAmount_with_different_price() public { initialize_group_contract(); + lenderCommitmentGroupV3.set_useRealGetRequiredCollateral(true); // Set price to 2:1 (1 collateral = 2 principal), so Q96 * 2 _mockPriceAdapter.setMockPriceRatioQ96(Q96 * 2); @@ -1320,6 +1323,101 @@ contract LenderCommitmentGroup_Pool_V3_Test is Testable { assertEq(collateralNeeded, 500, "2:1 price should require half collateral"); } + // ============ setMaxPrincipalPerCollateralAmount Tests ============ + + function test_setMaxPrincipalPerCollateralAmount_onlyOwner() public { + initialize_group_contract(); + + // Non-owner should revert + vm.prank(address(borrower)); + vm.expectRevert("Ownable: caller is not the owner"); + lenderCommitmentGroupV3.setMaxPrincipalPerCollateralAmount(Q96); + + // Owner should succeed + lenderCommitmentGroupV3.setMaxPrincipalPerCollateralAmount(Q96); + assertEq(lenderCommitmentGroupV3.maxPrincipalPerCollateralAmount(), Q96); + } + + function test_setMaxPrincipalPerCollateralAmount_zero_uses_oracle_only() public { + initialize_group_contract(); + lenderCommitmentGroupV3.set_useRealGetRequiredCollateral(true); + + // Oracle price 2:1 (1 collateral = 2 principal) + _mockPriceAdapter.setMockPriceRatioQ96(Q96 * 2); + + // maxPrincipalPerCollateralAmount = 0 (default), should use oracle price + uint256 collateralNeeded = lenderCommitmentGroupV3.calculateCollateralTokensAmountEquivalentToPrincipalTokens(1000); + assertEq(collateralNeeded, 500, "With cap=0, should use oracle price only"); + } + + function test_setMaxPrincipalPerCollateralAmount_caps_oracle_price() public { + initialize_group_contract(); + lenderCommitmentGroupV3.set_useRealGetRequiredCollateral(true); + + // Oracle says 1 collateral = 4 principal (Q96 * 4) + _mockPriceAdapter.setMockPriceRatioQ96(Q96 * 4); + + // Without cap: 1000 principal needs 250 collateral + uint256 withoutCap = lenderCommitmentGroupV3.calculateCollateralTokensAmountEquivalentToPrincipalTokens(1000); + assertEq(withoutCap, 250, "Without cap: 4:1 price means 250 collateral"); + + // Set cap to 2:1 (Q96 * 2) — lower than oracle's 4:1 + // min(4*Q96, 2*Q96) = 2*Q96, so borrower needs MORE collateral + lenderCommitmentGroupV3.setMaxPrincipalPerCollateralAmount(Q96 * 2); + + uint256 withCap = lenderCommitmentGroupV3.calculateCollateralTokensAmountEquivalentToPrincipalTokens(1000); + assertEq(withCap, 500, "Cap at 2:1 should require 500 collateral"); + } + + function test_setMaxPrincipalPerCollateralAmount_ignored_when_oracle_lower() public { + initialize_group_contract(); + lenderCommitmentGroupV3.set_useRealGetRequiredCollateral(true); + + // Oracle says 1 collateral = 2 principal (Q96 * 2) + _mockPriceAdapter.setMockPriceRatioQ96(Q96 * 2); + + // Set cap to 4:1 (Q96 * 4) — higher than oracle's 2:1 + // min(2*Q96, 4*Q96) = 2*Q96, so cap has no effect + lenderCommitmentGroupV3.setMaxPrincipalPerCollateralAmount(Q96 * 4); + + uint256 collateralNeeded = lenderCommitmentGroupV3.calculateCollateralTokensAmountEquivalentToPrincipalTokens(1000); + assertEq(collateralNeeded, 500, "Cap higher than oracle should have no effect"); + } + + function test_setMaxPrincipalPerCollateralAmount_can_be_reset_to_zero() public { + initialize_group_contract(); + lenderCommitmentGroupV3.set_useRealGetRequiredCollateral(true); + + _mockPriceAdapter.setMockPriceRatioQ96(Q96 * 4); + + // Set cap, then clear it + lenderCommitmentGroupV3.setMaxPrincipalPerCollateralAmount(Q96 * 2); + uint256 withCap = lenderCommitmentGroupV3.calculateCollateralTokensAmountEquivalentToPrincipalTokens(1000); + assertEq(withCap, 500, "With cap should need 500"); + + lenderCommitmentGroupV3.setMaxPrincipalPerCollateralAmount(0); + uint256 afterReset = lenderCommitmentGroupV3.calculateCollateralTokensAmountEquivalentToPrincipalTokens(1000); + assertEq(afterReset, 250, "After reset to 0 should use oracle (250)"); + } + + function test_setMaxPrincipalPerCollateralAmount_affects_collateralRequired() public { + initialize_group_contract(); + lenderCommitmentGroupV3.set_useRealGetRequiredCollateral(true); + + // Oracle 1:1, collateralRatio = 10000 (100%) + _mockPriceAdapter.setMockPriceRatioQ96(Q96); + + uint256 required = lenderCommitmentGroupV3.calculateCollateralRequiredToBorrowPrincipal(1000); + assertEq(required, 1000, "Baseline: 1:1 price, 100% ratio = 1000"); + + // Cap at 0.5:1 (half Q96) — means 1 collateral = 0.5 principal + // So 1000 principal needs 2000 collateral base, * 100% ratio = 2000 + lenderCommitmentGroupV3.setMaxPrincipalPerCollateralAmount(Q96 / 2); + + required = lenderCommitmentGroupV3.calculateCollateralRequiredToBorrowPrincipal(1000); + assertEq(required, 2000, "Cap at 0.5:1 should double required collateral"); + } + // ============ Getters / Interface Tests ============ function test_getCollateralTokenAddress() public { diff --git a/packages/contracts/tests_fork/NFT_FlashSwapRollover_Test.sol b/packages/contracts/tests_fork/NFT_FlashSwapRollover_Test.sol new file mode 100644 index 000000000..a120a6f4b --- /dev/null +++ b/packages/contracts/tests_fork/NFT_FlashSwapRollover_Test.sol @@ -0,0 +1,324 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { Test } from "../tests/util/FoundryTest.sol"; +import "forge-std/console.sol"; +import "forge-std/StdJson.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; + +import { SwapRolloverLoan } from "../contracts/LenderCommitmentForwarder/extensions/rollover/SwapRolloverLoan.sol"; +import { SwapRolloverLoan_G2 } from "../contracts/LenderCommitmentForwarder/extensions/rollover/SwapRolloverLoan_G2.sol"; +import { ITellerV2 } from "../contracts/interfaces/ITellerV2.sol"; +import { Payment } from "../contracts/TellerV2Storage.sol"; + +interface ILenderCommitmentForwarder_Debug { + // Matches LenderCommitmentForwarder_G1.Commitment storage layout exactly + struct Commitment { + uint256 maxPrincipal; + uint32 expiration; + uint32 maxDuration; + uint16 minInterestRate; + address collateralTokenAddress; + uint256 collateralTokenId; + uint256 maxPrincipalPerCollateralAmount; + uint8 collateralTokenType; // CommitmentCollateralType enum + address lender; + uint256 marketId; + address principalTokenAddress; + } + + function commitments(uint256 _commitmentId) external view returns ( + uint256 maxPrincipal, + uint32 expiration, + uint32 maxDuration, + uint16 minInterestRate, + address collateralTokenAddress, + uint256 collateralTokenId, + uint256 maxPrincipalPerCollateralAmount, + uint8 collateralTokenType, + address lender, + uint256 marketId, + address principalTokenAddress + ); + function getCommitmentMarketId(uint256 _commitmentId) external view returns (uint256); +} + +interface IExtensionsContext { + function addExtension(address extension) external; +} + +/** + * @title NFT Flash Swap Rollover Fork Test + * @notice Tests SwapRolloverLoan (G4) rollover with ERC721 (ENS) collateral + * and a regular lender commitment on mainnet. + * + * Transaction being replayed: + * from: 0xBa758f9169Df248B764aA20bc835b5f1786dFC14 + * to: 0x7848585b707F54CcF7044F8C82CF53F43100dc83 (SwapRolloverLoan) + * chain: mainnet (1) + * + * Loan details: + * - loanId: 4752 + * - principal: WETH + * - collateral: ENS domain NFT (ERC721) + * - commitment: regular (LenderCommitmentForwarderStaging, id=201) + * - flash pool: USDC/WETH 0.05% on Uniswap V3 + * + * Run with: + * FOUNDRY_PROFILE=fork forge test --match-contract NFT_FlashSwapRollover_Test -vvvv \ + * --fork-url + */ +contract NFT_FlashSwapRollover_Test is Test { + + string constant NETWORK_NAME = "mainnet"; + + using stdJson for string; + + // Mainnet addresses + address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + address constant ENS_BASE_REGISTRAR = 0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85; + + // Transaction participants + address constant BORROWER = 0xBa758f9169Df248B764aA20bc835b5f1786dFC14; + address constant SWAP_ROLLOVER_ADDR = 0x7848585b707F54CcF7044F8C82CF53F43100dc83; + address constant LCF_STAGING = 0x5098102507Da3F71677C5d9e170f91779Fe888F4; + + // Loan params from the raw tx + uint256 constant LOAN_ID = 4752; // 0x1290 + uint256 constant BORROWER_AMOUNT = 0x01be3dac9f1235f8; + uint256 constant COMMITMENT_ID = 201; // 0xc9 + uint256 constant PRINCIPAL_AMOUNT = 0x1b7a5fb088fe3c00; + uint256 constant COLLATERAL_TOKEN_ID = 0xb2605082270870306ed4c304d0b1df22fad260b4f0aeaa593a619c44125232b1; + uint256 constant FLASH_AMOUNT = 0x1c8f813559a7f2a1; + uint16 constant INTEREST_RATE = 2400; // 0x960 + uint32 constant LOAN_DURATION = 5184000; // 0x4f1a00 = 60 days + + SwapRolloverLoan swapRolloverLoan; + + function getDeployedAddress(string memory contractName) internal view returns (address) { + string memory root = vm.projectRoot(); + string memory path = string.concat(root, "/deployments/", NETWORK_NAME, "/", contractName, ".json"); + string memory json = vm.readFile(path); + return json.readAddress(".address"); + } + + function setUp() public { + swapRolloverLoan = SwapRolloverLoan(payable(SWAP_ROLLOVER_ADDR)); + assertTrue(SWAP_ROLLOVER_ADDR.code.length > 0, "SwapRolloverLoan not deployed"); + assertTrue(LCF_STAGING.code.length > 0, "LenderCommitmentForwarderStaging not deployed"); + } + + // ============ Diagnostic: inspect commitment minInterestRate ============ + + function test_inspect_commitment() public { + ILenderCommitmentForwarder_Debug lcf = ILenderCommitmentForwarder_Debug(LCF_STAGING); + + ( + uint256 maxPrincipal, + uint32 expiration, + uint32 maxDuration, + uint16 minInterestRate, + address collateralTokenAddress, + uint256 collateralTokenId, + uint256 maxPrincipalPerCollateralAmount, + uint8 collateralTokenType, + address lender, + uint256 marketId, + address principalTokenAddress + ) = lcf.commitments(COMMITMENT_ID); + + console.log("=== Commitment #201 ==="); + console.log(" minInterestRate:", minInterestRate); + console.log(" maxPrincipal:", maxPrincipal); + console.log(" maxDuration:", maxDuration); + console.log(" expiration:", expiration); + console.log(" collateralTokenType:", collateralTokenType); + console.log(" collateralTokenAddress:", collateralTokenAddress); + console.log(" principalTokenAddress:", principalTokenAddress); + console.log(" lender:", lender); + console.log(" marketId:", marketId); + console.log(" collateralTokenId:"); + console.logBytes32(bytes32(collateralTokenId)); + console.log(" maxPrincipalPerCollateralAmount:", maxPrincipalPerCollateralAmount); + + console.log(""); + console.log(" TX interestRate:", INTEREST_RATE); + console.log(" Passes check?:", INTEREST_RATE >= minInterestRate); + + if (INTEREST_RATE < minInterestRate) { + console.log(" >>> WILL REVERT: interestRate", INTEREST_RATE, "< minInterestRate", minInterestRate); + } + } + + // ============ Inspect loan state ============ + + function test_inspect_loan() public { + address tellerV2Addr = getDeployedAddress("TellerV2"); + ITellerV2 tellerV2 = ITellerV2(tellerV2Addr); + + address borrower = tellerV2.getLoanBorrower(LOAN_ID); + address lendingToken = tellerV2.getLoanLendingToken(LOAN_ID); + + console.log("=== Loan #4752 ==="); + console.log(" borrower:", borrower); + console.log(" lendingToken:", lendingToken); + console.log(" expected borrower:", BORROWER); + console.log(" match:", borrower == BORROWER); + + Payment memory owed = tellerV2.calculateAmountOwed(LOAN_ID, block.timestamp); + console.log(" principal owed:", owed.principal); + console.log(" interest owed:", owed.interest); + console.log(" total owed:", owed.principal + owed.interest); + console.log(" flashAmount:", FLASH_AMOUNT); + + // Check NFT ownership + address nftOwner = IERC721(ENS_BASE_REGISTRAR).ownerOf(COLLATERAL_TOKEN_ID); + console.log(" ENS NFT owner:", nftOwner); + } + + // ============ Replay the rollover transaction ============ + + function test_nft_flashswap_rollover() public { + // Log pre-state + _logPreState(); + + SwapRolloverLoan_G2.FlashSwapArgs memory flashSwapArgs = SwapRolloverLoan_G2.FlashSwapArgs({ + token0: USDC, + token1: WETH, + fee: 500, + flashAmount: FLASH_AMOUNT, + borrowToken1: true + }); + + SwapRolloverLoan_G2.AcceptCommitmentArgs memory acceptCommitmentArgs = SwapRolloverLoan_G2.AcceptCommitmentArgs({ + commitmentId: COMMITMENT_ID, + smartCommitmentAddress: address(0), + principalAmount: PRINCIPAL_AMOUNT, + collateralAmount: 1, + collateralTokenId: COLLATERAL_TOKEN_ID, + collateralTokenAddress: ENS_BASE_REGISTRAR, + interestRate: INTEREST_RATE, + loanDuration: LOAN_DURATION, + merkleProof: new bytes32[](0) + }); + + vm.prank(BORROWER); + swapRolloverLoan.rolloverLoanWithFlashSwap( + LCF_STAGING, + LOAN_ID, + BORROWER_AMOUNT, + flashSwapArgs, + acceptCommitmentArgs + ); + + // Post-state verification + _logPostState(); + } + + // ============ Replay via raw calldata decode ============ + + function test_nft_flashswap_rollover_raw_calldata() public { + bytes memory tx_calldata = hex"0f29fee20000000000000000000000005098102507da3f71677c5d9e170f91779fe888f4000000000000000000000000000000000000000000000000000000000000129000000000000000000000000000000000000000000000000001be3dac9f1235f8000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc200000000000000000000000000000000000000000000000000000000000001f40000000000000000000000000000000000000000000000001c8f813559a7f2a10000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000c900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001b7a5fb088fe3c000000000000000000000000000000000000000000000000000000000000000001b2605082270870306ed4c304d0b1df22fad260b4f0aeaa593a619c44125232b100000000000000000000000057f1887a8bf19b14fc0df6fd9b2acc9af147ea85000000000000000000000000000000000000000000000000000000000000096000000000000000000000000000000000000000000000000000000000004f1a0000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000000"; + + // Strip 4-byte selector + bytes memory params = new bytes(tx_calldata.length - 4); + for (uint i = 4; i < tx_calldata.length; i++) { + params[i - 4] = tx_calldata[i]; + } + + ( + address decoded_lcf, + uint256 decoded_loanId, + uint256 decoded_borrowerAmount, + SwapRolloverLoan_G2.FlashSwapArgs memory decoded_flashSwapArgs, + SwapRolloverLoan_G2.AcceptCommitmentArgs memory decoded_acceptCommitmentArgs + ) = abi.decode(params, ( + address, + uint256, + uint256, + SwapRolloverLoan_G2.FlashSwapArgs, + SwapRolloverLoan_G2.AcceptCommitmentArgs + )); + + console.log("=== Decoded TX Parameters ==="); + console.log(" lenderCommitmentForwarder:", decoded_lcf); + console.log(" loanId:", decoded_loanId); + console.log(" borrowerAmount:", decoded_borrowerAmount); + console.log(" flash token0:", decoded_flashSwapArgs.token0); + console.log(" flash token1:", decoded_flashSwapArgs.token1); + console.log(" flash fee:", decoded_flashSwapArgs.fee); + console.log(" flashAmount:", decoded_flashSwapArgs.flashAmount); + console.log(" borrowToken1:", decoded_flashSwapArgs.borrowToken1); + console.log(" commitmentId:", decoded_acceptCommitmentArgs.commitmentId); + console.log(" smartCommitmentAddress:", decoded_acceptCommitmentArgs.smartCommitmentAddress); + console.log(" principalAmount:", decoded_acceptCommitmentArgs.principalAmount); + console.log(" collateralAmount:", decoded_acceptCommitmentArgs.collateralAmount); + console.log(" collateralTokenId:"); + console.logBytes32(bytes32(decoded_acceptCommitmentArgs.collateralTokenId)); + console.log(" collateralTokenAddress:", decoded_acceptCommitmentArgs.collateralTokenAddress); + console.log(" interestRate:", decoded_acceptCommitmentArgs.interestRate); + console.log(" loanDuration:", decoded_acceptCommitmentArgs.loanDuration); + + // Inspect commitment to diagnose minInterestRate issue + ILenderCommitmentForwarder_Debug lcf = ILenderCommitmentForwarder_Debug(decoded_lcf); + (,,,uint16 minInterestRate,,,,,,,) = lcf.commitments(decoded_acceptCommitmentArgs.commitmentId); + console.log(""); + console.log(" commitment.minInterestRate:", minInterestRate); + console.log(" tx interestRate:", decoded_acceptCommitmentArgs.interestRate); + if (decoded_acceptCommitmentArgs.interestRate < minInterestRate) { + console.log(" >>> MISMATCH: tx rate < commitment min rate!"); + } + + vm.prank(BORROWER); + swapRolloverLoan.rolloverLoanWithFlashSwap( + decoded_lcf, + decoded_loanId, + decoded_borrowerAmount, + decoded_flashSwapArgs, + decoded_acceptCommitmentArgs + ); + + console.log("Rollover succeeded!"); + } + + // ============ Helpers ============ + + function _logPreState() internal { + address tellerV2Addr = getDeployedAddress("TellerV2"); + ITellerV2 tellerV2 = ITellerV2(tellerV2Addr); + + console.log("=== Pre-Rollover State ==="); + + address borrower = tellerV2.getLoanBorrower(LOAN_ID); + console.log(" loan borrower:", borrower); + + Payment memory owed = tellerV2.calculateAmountOwed(LOAN_ID, block.timestamp); + console.log(" total owed:", owed.principal + owed.interest); + + uint256 wethBal = IERC20(WETH).balanceOf(BORROWER); + console.log(" borrower WETH balance:", wethBal); + + address nftOwner = IERC721(ENS_BASE_REGISTRAR).ownerOf(COLLATERAL_TOKEN_ID); + console.log(" ENS NFT owner:", nftOwner); + } + + function _logPostState() internal { + address tellerV2Addr = getDeployedAddress("TellerV2"); + ITellerV2 tellerV2 = ITellerV2(tellerV2Addr); + + console.log("=== Post-Rollover State ==="); + + Payment memory owed = tellerV2.calculateAmountOwed(LOAN_ID, block.timestamp); + console.log(" old loan owed:", owed.principal + owed.interest); + + uint256 wethBal = IERC20(WETH).balanceOf(BORROWER); + console.log(" borrower WETH balance:", wethBal); + + address nftOwner = IERC721(ENS_BASE_REGISTRAR).ownerOf(COLLATERAL_TOKEN_ID); + console.log(" ENS NFT owner:", nftOwner); + + console.log("NFT flash swap rollover SUCCEEDED!"); + } +} From 70e015bf50f79e8b076eac8ed28a1cffbcbdba29 Mon Sep 17 00:00:00 2001 From: andy Date: Wed, 29 Apr 2026 12:05:23 -0400 Subject: [PATCH 2/6] use 1e18 expansion in poolsv3 for consistency with poolsv2 - Convert Q96 price from adapter to STANDARD_EXPANSION_FACTOR (1e18) internally - Add getPrincipalPerCollateralAmount() view for ABI compatibility with v2 - Update maxPrincipalPerCollateralAmount to use 1e18 expansion - Update tests to use 1e18-based cap values Co-Authored-By: Claude Opus 4.6 --- .../LenderCommitmentGroup_Pool_V3.sol | 67 +++++++++++++------ .../LenderCommitmentGroup_Pool_V3_Test.sol | 28 ++++---- 2 files changed, 61 insertions(+), 34 deletions(-) diff --git a/packages/contracts/contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Pool_V3.sol b/packages/contracts/contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Pool_V3.sol index 9305f89cb..519cb8cbc 100644 --- a/packages/contracts/contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Pool_V3.sol +++ b/packages/contracts/contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Pool_V3.sol @@ -100,12 +100,10 @@ contract LenderCommitmentGroup_Pool_V3 is uint256 constant Q96 = 0x1000000000000000000000000; + uint256 public immutable STANDARD_EXPANSION_FACTOR = 1e18; - uint256 public immutable MIN_TWAP_INTERVAL = 3; - uint256 public immutable UNISWAP_EXPANSION_FACTOR = 2**96; - uint256 public immutable EXCHANGE_RATE_EXPANSION_FACTOR = 1e36; using SafeERC20 for IERC20; @@ -705,24 +703,28 @@ contract LenderCommitmentGroup_Pool_V3 is return baseAmount.percent(collateralRatio); } - /* - * @dev this is expanded by 10e18 - * @dev this logic is very similar to that used in LCFA + /* + * @dev this is expanded by STANDARD_EXPANSION_FACTOR (1e18) + * @dev this logic is very similar to that used in LCFA */ function calculateCollateralTokensAmountEquivalentToPrincipalTokens( - uint256 principalAmount + uint256 principalAmount ) public view virtual returns (uint256 collateralTokensAmountToMatchValue) { - - - // principalPerCollateralAmount - uint256 priceRatioQ96 = IPriceAdapter( priceAdapter ) + // Convert Q96 oracle price to 1e18 expansion + uint256 priceRatioQ96 = IPriceAdapter(priceAdapter) .getPriceRatioQ96(priceRouteHash); + uint256 pricRatio1e18 = MathUpgradeable.mulDiv( + priceRatioQ96, + STANDARD_EXPANSION_FACTOR, + Q96 + ); + uint256 principalPerCollateralAmount = maxPrincipalPerCollateralAmount == 0 - ? priceRatioQ96 + ? pricRatio1e18 : MathUpgradeable.min( - priceRatioQ96, + pricRatio1e18, maxPrincipalPerCollateralAmount ); @@ -736,27 +738,50 @@ contract LenderCommitmentGroup_Pool_V3 is + /** + * @notice Returns the effective principal-per-collateral price ratio (capped by maxPrincipalPerCollateralAmount if set) + * @dev Mirrors the V2 getPrincipalForCollateralForPoolRoutes ABI for compatibility + * @return The principal per collateral ratio, expanded by STANDARD_EXPANSION_FACTOR (1e18) + */ + function getPrincipalPerCollateralAmount() external view virtual returns (uint256) { + uint256 priceRatioQ96 = IPriceAdapter(priceAdapter) + .getPriceRatioQ96(priceRouteHash); + + uint256 priceRatio1e18 = MathUpgradeable.mulDiv( + priceRatioQ96, + STANDARD_EXPANSION_FACTOR, + Q96 + ); + + return maxPrincipalPerCollateralAmount == 0 + ? priceRatio1e18 + : MathUpgradeable.min( + priceRatio1e18, + maxPrincipalPerCollateralAmount + ); + } + /** * @notice Calculates the amount of collateral tokens required for a given principal amount * @dev Converts principal amount to equivalent collateral based on current price ratio * @dev Uses the Math.mulDiv function with rounding up to ensure sufficient collateral * @param _principalAmount The amount of principal tokens to be borrowed - * @param _maxPrincipalPerCollateralAmountQ96 The exchange rate between principal and collateral (expanded by Q96) + * @param _maxPrincipalPerCollateralAmount The exchange rate between principal and collateral (expanded by STANDARD_EXPANSION_FACTOR) * @return The required amount of collateral tokens, rounded up to ensure sufficient collateralization */ function getRequiredCollateral( uint256 _principalAmount, - uint256 _maxPrincipalPerCollateralAmountQ96 //price ratio Q96 - + uint256 _maxPrincipalPerCollateralAmount + ) internal view virtual returns (uint256) { - + return MathUpgradeable.mulDiv( _principalAmount, - Q96, - _maxPrincipalPerCollateralAmountQ96, + STANDARD_EXPANSION_FACTOR, + _maxPrincipalPerCollateralAmount, MathUpgradeable.Rounding.Up - ); + ); } @@ -1106,7 +1131,7 @@ contract LenderCommitmentGroup_Pool_V3 is /** * @notice Sets an optional manual cap for principal/collateral price ratio. Only Pool Owner. - * @param _maxPrincipalPerCollateralAmount Price ratio expanded by Q96. If 0, only oracle price is used. + * @param _maxPrincipalPerCollateralAmount Price ratio expanded by STANDARD_EXPANSION_FACTOR (1e18). If 0, only oracle price is used. */ function setMaxPrincipalPerCollateralAmount(uint256 _maxPrincipalPerCollateralAmount) external diff --git a/packages/contracts/tests/SmartCommitmentForwarder/LenderCommitmentGroup_Pool_V3_Test.sol b/packages/contracts/tests/SmartCommitmentForwarder/LenderCommitmentGroup_Pool_V3_Test.sol index 08dcb7a5b..71ac73c2b 100644 --- a/packages/contracts/tests/SmartCommitmentForwarder/LenderCommitmentGroup_Pool_V3_Test.sol +++ b/packages/contracts/tests/SmartCommitmentForwarder/LenderCommitmentGroup_Pool_V3_Test.sol @@ -46,8 +46,10 @@ contract LenderCommitmentGroup_Pool_V3_Test is Testable { ProtocolPausingManager _protocolPausingManager; - // Q96 = 2^96, used for price ratio + // Q96 = 2^96, used for price ratio from adapter uint256 constant Q96 = 0x1000000000000000000000000; + // STANDARD_EXPANSION_FACTOR used for maxPrincipalPerCollateralAmount + uint256 constant STANDARD_EXPANSION_FACTOR = 1e18; function setUp() public { borrower = new User(); @@ -1331,11 +1333,11 @@ contract LenderCommitmentGroup_Pool_V3_Test is Testable { // Non-owner should revert vm.prank(address(borrower)); vm.expectRevert("Ownable: caller is not the owner"); - lenderCommitmentGroupV3.setMaxPrincipalPerCollateralAmount(Q96); + lenderCommitmentGroupV3.setMaxPrincipalPerCollateralAmount(STANDARD_EXPANSION_FACTOR); // Owner should succeed - lenderCommitmentGroupV3.setMaxPrincipalPerCollateralAmount(Q96); - assertEq(lenderCommitmentGroupV3.maxPrincipalPerCollateralAmount(), Q96); + lenderCommitmentGroupV3.setMaxPrincipalPerCollateralAmount(STANDARD_EXPANSION_FACTOR); + assertEq(lenderCommitmentGroupV3.maxPrincipalPerCollateralAmount(), STANDARD_EXPANSION_FACTOR); } function test_setMaxPrincipalPerCollateralAmount_zero_uses_oracle_only() public { @@ -1361,9 +1363,9 @@ contract LenderCommitmentGroup_Pool_V3_Test is Testable { uint256 withoutCap = lenderCommitmentGroupV3.calculateCollateralTokensAmountEquivalentToPrincipalTokens(1000); assertEq(withoutCap, 250, "Without cap: 4:1 price means 250 collateral"); - // Set cap to 2:1 (Q96 * 2) — lower than oracle's 4:1 - // min(4*Q96, 2*Q96) = 2*Q96, so borrower needs MORE collateral - lenderCommitmentGroupV3.setMaxPrincipalPerCollateralAmount(Q96 * 2); + // Set cap to 2:1 (1e18 * 2) — lower than oracle's 4:1 + // min(4*1e18, 2*1e18) = 2*1e18, so borrower needs MORE collateral + lenderCommitmentGroupV3.setMaxPrincipalPerCollateralAmount(STANDARD_EXPANSION_FACTOR * 2); uint256 withCap = lenderCommitmentGroupV3.calculateCollateralTokensAmountEquivalentToPrincipalTokens(1000); assertEq(withCap, 500, "Cap at 2:1 should require 500 collateral"); @@ -1376,9 +1378,9 @@ contract LenderCommitmentGroup_Pool_V3_Test is Testable { // Oracle says 1 collateral = 2 principal (Q96 * 2) _mockPriceAdapter.setMockPriceRatioQ96(Q96 * 2); - // Set cap to 4:1 (Q96 * 4) — higher than oracle's 2:1 - // min(2*Q96, 4*Q96) = 2*Q96, so cap has no effect - lenderCommitmentGroupV3.setMaxPrincipalPerCollateralAmount(Q96 * 4); + // Set cap to 4:1 (1e18 * 4) — higher than oracle's 2:1 + // min(2*1e18, 4*1e18) = 2*1e18, so cap has no effect + lenderCommitmentGroupV3.setMaxPrincipalPerCollateralAmount(STANDARD_EXPANSION_FACTOR * 4); uint256 collateralNeeded = lenderCommitmentGroupV3.calculateCollateralTokensAmountEquivalentToPrincipalTokens(1000); assertEq(collateralNeeded, 500, "Cap higher than oracle should have no effect"); @@ -1391,7 +1393,7 @@ contract LenderCommitmentGroup_Pool_V3_Test is Testable { _mockPriceAdapter.setMockPriceRatioQ96(Q96 * 4); // Set cap, then clear it - lenderCommitmentGroupV3.setMaxPrincipalPerCollateralAmount(Q96 * 2); + lenderCommitmentGroupV3.setMaxPrincipalPerCollateralAmount(STANDARD_EXPANSION_FACTOR * 2); uint256 withCap = lenderCommitmentGroupV3.calculateCollateralTokensAmountEquivalentToPrincipalTokens(1000); assertEq(withCap, 500, "With cap should need 500"); @@ -1410,9 +1412,9 @@ contract LenderCommitmentGroup_Pool_V3_Test is Testable { uint256 required = lenderCommitmentGroupV3.calculateCollateralRequiredToBorrowPrincipal(1000); assertEq(required, 1000, "Baseline: 1:1 price, 100% ratio = 1000"); - // Cap at 0.5:1 (half Q96) — means 1 collateral = 0.5 principal + // Cap at 0.5:1 (half 1e18) — means 1 collateral = 0.5 principal // So 1000 principal needs 2000 collateral base, * 100% ratio = 2000 - lenderCommitmentGroupV3.setMaxPrincipalPerCollateralAmount(Q96 / 2); + lenderCommitmentGroupV3.setMaxPrincipalPerCollateralAmount(STANDARD_EXPANSION_FACTOR / 2); required = lenderCommitmentGroupV3.calculateCollateralRequiredToBorrowPrincipal(1000); assertEq(required, 2000, "Cap at 0.5:1 should double required collateral"); From 81596ee9565004df3a8faf71dcb8511ce3b4b0c0 Mon Sep 17 00:00:00 2001 From: andy Date: Tue, 5 May 2026 21:02:18 -0400 Subject: [PATCH 3/6] init rreentrancy guard --- .../.openzeppelin/unknown-33139.json | 409 ++++++++++++++++++ .../LenderCommitmentGroup_Pool_V3.sol | 3 +- ...2_upgrade_lender_pools_v3_max_principal.ts | 70 +++ .../deployments/apechain/.migrations.json | 3 +- .../LenderCommitmentGroupBeaconV3.json | 37 +- 5 files changed, 515 insertions(+), 7 deletions(-) create mode 100644 packages/contracts/deploy/upgrades/42_upgrade_lender_pools_v3_max_principal.ts diff --git a/packages/contracts/.openzeppelin/unknown-33139.json b/packages/contracts/.openzeppelin/unknown-33139.json index 1443ba344..05fbb8319 100644 --- a/packages/contracts/.openzeppelin/unknown-33139.json +++ b/packages/contracts/.openzeppelin/unknown-33139.json @@ -3718,6 +3718,415 @@ "types": {}, "namespaces": {} } + }, + "5ebb103336e684bcf93390a00a58ee269366ac8705b47af83a516fe5bc0acfb1": { + "address": "0x920F45C59334B3676976F0473C37c5e2368a148B", + "txHash": "0x82020d19f1172cd91bf6772bf0bdb689d22cc772c9f97f452b44204380c6773a", + "layout": { + "solcVersion": "0.8.24", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:40" + }, + { + "label": "_owner", + "offset": 0, + "slot": "51", + "type": "t_address", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" + }, + { + "label": "__gap", + "offset": 0, + "slot": "52", + "type": "t_array(t_uint256)49_storage", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" + }, + { + "label": "_status", + "offset": 0, + "slot": "101", + "type": "t_uint256", + "contract": "ReentrancyGuardUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol:38" + }, + { + "label": "__gap", + "offset": 0, + "slot": "102", + "type": "t_array(t_uint256)49_storage", + "contract": "ReentrancyGuardUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol:88" + }, + { + "label": "_balances", + "offset": 0, + "slot": "151", + "type": "t_mapping(t_address,t_uint256)", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:40" + }, + { + "label": "_allowances", + "offset": 0, + "slot": "152", + "type": "t_mapping(t_address,t_mapping(t_address,t_uint256))", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:42" + }, + { + "label": "_totalSupply", + "offset": 0, + "slot": "153", + "type": "t_uint256", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:44" + }, + { + "label": "_name", + "offset": 0, + "slot": "154", + "type": "t_string_storage", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:46" + }, + { + "label": "_symbol", + "offset": 0, + "slot": "155", + "type": "t_string_storage", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:47" + }, + { + "label": "__gap", + "offset": 0, + "slot": "156", + "type": "t_array(t_uint256)45_storage", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:376" + }, + { + "label": "poolSharesLastTransferredAt", + "offset": 0, + "slot": "201", + "type": "t_mapping(t_address,t_uint256)", + "contract": "LenderCommitmentGroupSharesIntegrated", + "src": "contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroupSharesIntegrated.sol:31" + }, + { + "label": "__gap", + "offset": 0, + "slot": "202", + "type": "t_array(t_uint256)50_storage", + "contract": "LenderCommitmentGroupSharesIntegrated", + "src": "contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroupSharesIntegrated.sol:123" + }, + { + "label": "priceAdapter", + "offset": 0, + "slot": "252", + "type": "t_address", + "contract": "LenderCommitmentGroup_Pool_V3", + "src": "contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Pool_V3.sol:114" + }, + { + "label": "principalToken", + "offset": 0, + "slot": "253", + "type": "t_contract(IERC20)3383", + "contract": "LenderCommitmentGroup_Pool_V3", + "src": "contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Pool_V3.sol:118" + }, + { + "label": "collateralToken", + "offset": 0, + "slot": "254", + "type": "t_contract(IERC20)3383", + "contract": "LenderCommitmentGroup_Pool_V3", + "src": "contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Pool_V3.sol:119" + }, + { + "label": "marketId", + "offset": 0, + "slot": "255", + "type": "t_uint256", + "contract": "LenderCommitmentGroup_Pool_V3", + "src": "contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Pool_V3.sol:121" + }, + { + "label": "totalPrincipalTokensCommitted", + "offset": 0, + "slot": "256", + "type": "t_uint256", + "contract": "LenderCommitmentGroup_Pool_V3", + "src": "contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Pool_V3.sol:124" + }, + { + "label": "totalPrincipalTokensWithdrawn", + "offset": 0, + "slot": "257", + "type": "t_uint256", + "contract": "LenderCommitmentGroup_Pool_V3", + "src": "contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Pool_V3.sol:125" + }, + { + "label": "totalPrincipalTokensLended", + "offset": 0, + "slot": "258", + "type": "t_uint256", + "contract": "LenderCommitmentGroup_Pool_V3", + "src": "contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Pool_V3.sol:127" + }, + { + "label": "totalPrincipalTokensRepaid", + "offset": 0, + "slot": "259", + "type": "t_uint256", + "contract": "LenderCommitmentGroup_Pool_V3", + "src": "contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Pool_V3.sol:128" + }, + { + "label": "excessivePrincipalTokensRepaid", + "offset": 0, + "slot": "260", + "type": "t_uint256", + "contract": "LenderCommitmentGroup_Pool_V3", + "src": "contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Pool_V3.sol:129" + }, + { + "label": "totalInterestCollected", + "offset": 0, + "slot": "261", + "type": "t_uint256", + "contract": "LenderCommitmentGroup_Pool_V3", + "src": "contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Pool_V3.sol:131" + }, + { + "label": "liquidityThresholdPercent", + "offset": 0, + "slot": "262", + "type": "t_uint16", + "contract": "LenderCommitmentGroup_Pool_V3", + "src": "contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Pool_V3.sol:133" + }, + { + "label": "collateralRatio", + "offset": 2, + "slot": "262", + "type": "t_uint16", + "contract": "LenderCommitmentGroup_Pool_V3", + "src": "contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Pool_V3.sol:134" + }, + { + "label": "maxLoanDuration", + "offset": 4, + "slot": "262", + "type": "t_uint32", + "contract": "LenderCommitmentGroup_Pool_V3", + "src": "contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Pool_V3.sol:136" + }, + { + "label": "interestRateLowerBound", + "offset": 8, + "slot": "262", + "type": "t_uint16", + "contract": "LenderCommitmentGroup_Pool_V3", + "src": "contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Pool_V3.sol:137" + }, + { + "label": "interestRateUpperBound", + "offset": 10, + "slot": "262", + "type": "t_uint16", + "contract": "LenderCommitmentGroup_Pool_V3", + "src": "contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Pool_V3.sol:138" + }, + { + "label": "activeBids", + "offset": 0, + "slot": "263", + "type": "t_mapping(t_uint256,t_bool)", + "contract": "LenderCommitmentGroup_Pool_V3", + "src": "contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Pool_V3.sol:146" + }, + { + "label": "activeBidsAmountDueRemaining", + "offset": 0, + "slot": "264", + "type": "t_mapping(t_uint256,t_uint256)", + "contract": "LenderCommitmentGroup_Pool_V3", + "src": "contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Pool_V3.sol:147" + }, + { + "label": "tokenDifferenceFromLiquidations", + "offset": 0, + "slot": "265", + "type": "t_int256", + "contract": "LenderCommitmentGroup_Pool_V3", + "src": "contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Pool_V3.sol:149" + }, + { + "label": "firstDepositMade", + "offset": 0, + "slot": "266", + "type": "t_bool", + "contract": "LenderCommitmentGroup_Pool_V3", + "src": "contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Pool_V3.sol:151" + }, + { + "label": "withdrawDelayTimeSeconds", + "offset": 0, + "slot": "267", + "type": "t_uint256", + "contract": "LenderCommitmentGroup_Pool_V3", + "src": "contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Pool_V3.sol:152" + }, + { + "label": "priceRouteHash", + "offset": 0, + "slot": "268", + "type": "t_bytes32", + "contract": "LenderCommitmentGroup_Pool_V3", + "src": "contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Pool_V3.sol:156" + }, + { + "label": "maxPrincipalPerCollateralAmount", + "offset": 0, + "slot": "269", + "type": "t_uint256", + "contract": "LenderCommitmentGroup_Pool_V3", + "src": "contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Pool_V3.sol:160" + }, + { + "label": "lastUnpausedAt", + "offset": 0, + "slot": "270", + "type": "t_uint256", + "contract": "LenderCommitmentGroup_Pool_V3", + "src": "contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Pool_V3.sol:163" + }, + { + "label": "paused", + "offset": 0, + "slot": "271", + "type": "t_bool", + "contract": "LenderCommitmentGroup_Pool_V3", + "src": "contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Pool_V3.sol:164" + }, + { + "label": "borrowingPaused", + "offset": 1, + "slot": "271", + "type": "t_bool", + "contract": "LenderCommitmentGroup_Pool_V3", + "src": "contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Pool_V3.sol:165" + }, + { + "label": "liquidationAuctionPaused", + "offset": 2, + "slot": "271", + "type": "t_bool", + "contract": "LenderCommitmentGroup_Pool_V3", + "src": "contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Pool_V3.sol:166" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_uint256)45_storage": { + "label": "uint256[45]", + "numberOfBytes": "1440" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_contract(IERC20)3383": { + "label": "contract IERC20", + "numberOfBytes": "20" + }, + "t_int256": { + "label": "int256", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_mapping(t_address,t_uint256))": { + "label": "mapping(address => mapping(address => uint256))", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_uint256)": { + "label": "mapping(address => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_bool)": { + "label": "mapping(uint256 => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_uint256)": { + "label": "mapping(uint256 => uint256)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_uint16": { + "label": "uint16", + "numberOfBytes": "2" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint32": { + "label": "uint32", + "numberOfBytes": "4" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + }, + "namespaces": {} + } } } } diff --git a/packages/contracts/contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Pool_V3.sol b/packages/contracts/contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Pool_V3.sol index 519cb8cbc..ec806f22e 100644 --- a/packages/contracts/contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Pool_V3.sol +++ b/packages/contracts/contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Pool_V3.sol @@ -296,7 +296,8 @@ contract LenderCommitmentGroup_Pool_V3 is ) external initializer { __Ownable_init(); - + __ReentrancyGuard_init(); + __Shares_init( _commitmentGroupConfig.principalTokenAddress, _commitmentGroupConfig.collateralTokenAddress diff --git a/packages/contracts/deploy/upgrades/42_upgrade_lender_pools_v3_max_principal.ts b/packages/contracts/deploy/upgrades/42_upgrade_lender_pools_v3_max_principal.ts new file mode 100644 index 000000000..2a3ba3efd --- /dev/null +++ b/packages/contracts/deploy/upgrades/42_upgrade_lender_pools_v3_max_principal.ts @@ -0,0 +1,70 @@ +import { DeployFunction } from 'hardhat-deploy/dist/types' + + +const deployFn: DeployFunction = async (hre) => { + hre.log('----------') + hre.log('') + hre.log('Lender Pools V3: Proposing upgrade...') + + const lenderCommitmentGroupV3Beacon = await hre.contracts.get('LenderCommitmentGroupBeaconV3') + + const tellerV2 = await hre.contracts.get('TellerV2') + const SmartCommitmentForwarder = await hre.contracts.get( + 'SmartCommitmentForwarder' + ) + const tellerV2Address = await tellerV2.getAddress() + const smartCommitmentForwarderAddress = + await SmartCommitmentForwarder.getAddress() + + await hre.upgrades.proposeBatchTimelock({ + title: 'Lender Pools V3: Re-enable setMaxPrincipalPerCollateralAmount', + description: ` +# Lender Pools V3 + +* Re-adds setMaxPrincipalPerCollateralAmount so pool owners can set a manual cap on the principal-per-collateral price ratio. +* When set (nonzero), the pool uses the lesser of the oracle price and the manual cap — protecting lenders from inflated oracle values. +* When zero (default), pricing uses the oracle only (no behavior change for existing pools). +`, + _steps: [ + { + beacon: lenderCommitmentGroupV3Beacon, + implFactory: await hre.ethers.getContractFactory('LenderCommitmentGroup_Pool_V3'), + + opts: { + unsafeAllow: [ + 'constructor', + 'state-variable-immutable', + ], + constructorArgs: [ + tellerV2Address, + smartCommitmentForwarderAddress, + ], + }, + }, + ], + }) + + hre.log('done.') + hre.log('') + hre.log('----------') + + return true +} + +// tags and deployment +deployFn.id = 'lender-commitment-group-beacon-v3:upgrade-max-principal' +deployFn.tags = [ + 'proposal', + 'upgrade', + 'lender-commitment-group-beacon-v3', + 'lender-commitment-group-beacon-v3:upgrade-max-principal', +] +deployFn.dependencies = [ + 'teller-v2:deploy', + 'smart-commitment-forwarder:deploy', + 'lender-commitment-group-beacon-v3:deploy', +] +deployFn.skip = async (hre) => { + return hre.network.name !== 'apechain' +} +export default deployFn diff --git a/packages/contracts/deployments/apechain/.migrations.json b/packages/contracts/deployments/apechain/.migrations.json index 0391717f7..49ac9671b 100644 --- a/packages/contracts/deployments/apechain/.migrations.json +++ b/packages/contracts/deployments/apechain/.migrations.json @@ -26,5 +26,6 @@ "apechain:transfer-timelock-ownership": 1771556073, "lender-commitment-group-beacon-v3:deploy": 1772162877, "lender-commitment-group-factory-v3:deploy": 1772162931, - "lender-commitment-forwarder:extensions:flash-swap-rollover:g4-upgrade-apechain": 1775158842 + "lender-commitment-forwarder:extensions:flash-swap-rollover:g4-upgrade-apechain": 1775158842, + "lender-commitment-group-beacon-v3:upgrade-max-principal": 1777644534 } \ No newline at end of file diff --git a/packages/contracts/deployments/apechain/LenderCommitmentGroupBeaconV3.json b/packages/contracts/deployments/apechain/LenderCommitmentGroupBeaconV3.json index 8c2a2f1ce..32ffd9324 100644 --- a/packages/contracts/deployments/apechain/LenderCommitmentGroupBeaconV3.json +++ b/packages/contracts/deployments/apechain/LenderCommitmentGroupBeaconV3.json @@ -483,28 +483,28 @@ }, { "type": "function", - "name": "TELLER_V2", + "name": "STANDARD_EXPANSION_FACTOR", "constant": true, "stateMutability": "view", "payable": false, "inputs": [], "outputs": [ { - "type": "address", + "type": "uint256", "name": "" } ] }, { "type": "function", - "name": "UNISWAP_EXPANSION_FACTOR", + "name": "TELLER_V2", "constant": true, "stateMutability": "view", "payable": false, "inputs": [], "outputs": [ { - "type": "uint256", + "type": "address", "name": "" } ] @@ -1015,6 +1015,20 @@ } ] }, + { + "type": "function", + "name": "getPrincipalPerCollateralAmount", + "constant": true, + "stateMutability": "view", + "payable": false, + "inputs": [], + "outputs": [ + { + "type": "uint256", + "name": "" + } + ] + }, { "type": "function", "name": "getPrincipalTokenAddress", @@ -1595,6 +1609,19 @@ ], "outputs": [] }, + { + "type": "function", + "name": "setMaxPrincipalPerCollateralAmount", + "constant": false, + "payable": false, + "inputs": [ + { + "type": "uint256", + "name": "_maxPrincipalPerCollateralAmount" + } + ], + "outputs": [] + }, { "type": "function", "name": "setWithdrawDelayTime", @@ -1888,6 +1915,6 @@ } ], "receipt": {}, - "numDeployments": 1, + "numDeployments": 2, "implementation": "0x688d3d7D900f38ce095914608AD069d5AA2331DB" } \ No newline at end of file From 3ec0d2a40aaea8a0bc2a6f6ffa970a8e49d0387e Mon Sep 17 00:00:00 2001 From: andy Date: Fri, 8 May 2026 10:29:03 -0400 Subject: [PATCH 4/6] deployed upgrade --- packages/subgraph-pool-v2/subgraph.yaml | 14 +++++++------- packages/subgraph/README.md | 1 + packages/subgraph/config/apechain.json | 2 +- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/subgraph-pool-v2/subgraph.yaml b/packages/subgraph-pool-v2/subgraph.yaml index 496ec64e4..61026d4c1 100644 --- a/packages/subgraph-pool-v2/subgraph.yaml +++ b/packages/subgraph-pool-v2/subgraph.yaml @@ -9,11 +9,11 @@ features: dataSources: - kind: ethereum/contract name: Factory - network: xdc + network: bsc source: abi: Factory - address: "0x0848E884b2DBb63727aa3216b921C279f6DC9a91" - startBlock: 100427353 + address: "0x0EfD3E33Ba2EdE028e50a3E7f81E084996d161aa" + startBlock: 81088797 mapping: kind: ethereum/events apiVersion: 0.0.7 @@ -29,11 +29,11 @@ dataSources: handler: handleLenderGroupDeployed - kind: ethereum/contract name: CollateralManager - network: xdc + network: bsc source: abi: CollateralManager - address: "0x6455F2E1CCb14bd0b675A309276FB5333Dec524f" - startBlock: 100427353 + address: "0x9D55Cf23D26a8C17670bb8Ee25D58481ff7aD295" + startBlock: 81088797 mapping: kind: ethereum/events apiVersion: 0.0.7 @@ -51,7 +51,7 @@ dataSources: templates: - kind: ethereum/contract name: Pool - network: xdc + network: bsc source: abi: Pool mapping: diff --git a/packages/subgraph/README.md b/packages/subgraph/README.md index 06d2c66f7..dfc7e2152 100644 --- a/packages/subgraph/README.md +++ b/packages/subgraph/README.md @@ -58,6 +58,7 @@ graph auth graph deploy tellerv2-mainnet --version-label 0.4.21-24 + graph deploy teller-v-2-base --version-label 0.4.21-26 diff --git a/packages/subgraph/config/apechain.json b/packages/subgraph/config/apechain.json index dea16d1b8..0a06e92b7 100644 --- a/packages/subgraph/config/apechain.json +++ b/packages/subgraph/config/apechain.json @@ -1 +1 @@ -{"enabled":true,"name":"teller-v2-apechain","network":"apechain-mainnet","export_network_name":"apechain","product":"studio","studio":{"owner":"0x1A2bAA2257343119FB03FD448622456a0c4f2190","network":"mainnet"},"grafting":{"enabled":false},"block_handler":{"enabled":false,"block":"33944573"},"contracts":{"teller_v2":{"enabled":true,"address":"0x3AF8DB041fcaFA539C2c78f73aa209383ba703ed","block":"33944573"},"market_registry":{"enabled":true,"address":"0x0708480670BdE591e275B06Cd19EcaDFC93A1f16","block":"33944590"},"lender_commitment":{"enabled":false,"address":"0x0000000000000000000000000000000000000000","block":"33944573"},"lender_commitment_staging":{"enabled":false,"address":"0x0000000000000000000000000000000000000000","block":"33944573"},"lender_commitment_alpha":{"enabled":true,"address":"0xdb2Ba4a2b90c6670f240D59AfcBeb35cd9Edd515","block":"33944592"},"collateral_manager":{"enabled":true,"address":"0x90D08f8Df66dFdE93801783FF7A36876453DAE75","block":"33944575"},"lender_manager":{"enabled":true,"address":"0x0AeeeD450EcCaFaA140222De43963B179B514540","block":"33944610"},"market_liquidity_rewards":{"enabled":true,"address":"0x7Ff158DcD62C4E112f374F14fF25C735E8E4684D","block":"33944631"}}} \ No newline at end of file +{"enabled":true,"name":"teller-v2-apechain","network":"apechain","export_network_name":"apechain","product":"studio","studio":{"owner":"0x1A2bAA2257343119FB03FD448622456a0c4f2190","network":"mainnet"},"grafting":{"enabled":false},"block_handler":{"enabled":false,"block":"33944573"},"contracts":{"teller_v2":{"enabled":true,"address":"0x3AF8DB041fcaFA539C2c78f73aa209383ba703ed","block":"33944573"},"market_registry":{"enabled":true,"address":"0x0708480670BdE591e275B06Cd19EcaDFC93A1f16","block":"33944590"},"lender_commitment":{"enabled":false,"address":"0x0000000000000000000000000000000000000000","block":"33944573"},"lender_commitment_staging":{"enabled":false,"address":"0x0000000000000000000000000000000000000000","block":"33944573"},"lender_commitment_alpha":{"enabled":true,"address":"0xdb2Ba4a2b90c6670f240D59AfcBeb35cd9Edd515","block":"33944592"},"collateral_manager":{"enabled":true,"address":"0x90D08f8Df66dFdE93801783FF7A36876453DAE75","block":"33944575"},"lender_manager":{"enabled":true,"address":"0x0AeeeD450EcCaFaA140222De43963B179B514540","block":"33944610"},"market_liquidity_rewards":{"enabled":true,"address":"0x7Ff158DcD62C4E112f374F14fF25C735E8E4684D","block":"33944631"}}} \ No newline at end of file From 13af5653dc9633f900655c0bf39aecc4888bd375 Mon Sep 17 00:00:00 2001 From: Ethereumdegen Date: Tue, 26 May 2026 09:48:14 -0400 Subject: [PATCH 5/6] add note to readme --- packages/contracts/README.md | 4 ++++ packages/contracts/package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/contracts/README.md b/packages/contracts/README.md index 1100d9369..d99cd220d 100644 --- a/packages/contracts/README.md +++ b/packages/contracts/README.md @@ -62,3 +62,7 @@ yarn contracts test_forked +### Package Version Warning + +Please do not install/use published package versions 3.1.52 or 3.1.53 as they are problematic. + diff --git a/packages/contracts/package.json b/packages/contracts/package.json index a12c830b3..228762cf9 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -1,6 +1,6 @@ { "name": "@teller-protocol/v2-contracts", - "version": "3.1.51", + "version": "3.1.61", "license": "MIT", "scripts": { "forge": "forge", From 9704022376f72a4c4efcdb98459152b1d50a85a7 Mon Sep 17 00:00:00 2001 From: Ethereumdegen Date: Tue, 26 May 2026 09:55:28 -0400 Subject: [PATCH 6/6] adding to npm package readme --- packages/contracts/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 228762cf9..f446ca109 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -32,7 +32,6 @@ "format:sol": "prettier --config .prettierrc.yml --resolve-plugins-relative-to . --write --loglevel error '{contracts,tests}/**/*.sol'", "git:future": "./scripts/git/future.sh", "git:portal": "./scripts/git/portal.sh", - "publish": "yarn npm publish", "clean": "yarn hh clean && ./scripts/clean.sh", "prepacklegacy": "./scripts/prepack.sh", "prepack": "node ./scripts/prepack.js",