diff --git a/.gas-snapshot b/.gas-snapshot index 837ece1..dea9b3b 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,54 +1,68 @@ -TestAstariaV1Loan:testNewLoanERC721CollateralDefaultTermsRecallAuctionFailLenderClaim() (gas: 569882) -TestAstariaV1Loan:testNewLoanERC721CollateralDefaultTermsRecallBase() (gas: 910054) -TestAstariaV1Loan:testNewLoanERC721CollateralDefaultTermsRecallLenderClaimRandomFulfiller() (gas: 567197) -TestAstariaV1Loan:testNewLoanERC721CollateralRecallNotBlockedBorrower() (gas: 473813) -TestAstariaV1Loan:testNewLoanERC721CollateralRecallNotBlockedLender() (gas: 473538) -TestAstariaV1Loan:testNewLoanERC721CollateralRecallerNotBorrowerOrLender() (gas: 446127) -TestAstariaV1Pricing:testGetPaymentConsiderationIncrementation() (gas: 57195) -TestAstariaV1Pricing:testGetRefinanceConsiderationAsBorrowerZeroRate() (gas: 60433) -TestAstariaV1Pricing:testGetRefinanceConsiderationInsufficientRefinance() (gas: 95727) -TestAstariaV1Pricing:testGetRefinanceConsiderationInvalidRefinance() (gas: 59300) -TestAstariaV1Pricing:testGetRefinanceConsiderationValidEqualRate() (gas: 97989) -TestAstariaV1Pricing:testGetRefinanceConsiderationValidHigherRate() (gas: 108970) -TestAstariaV1Pricing:testGetRefinanceConsiderationValidLowerRate() (gas: 104617) -TestAstariaV1Pricing:testGetRefinanceConsiderationZeroRate() (gas: 64686) -TestAstariaV1Pricing:testGetRefinanceNewDecimalMismatch() (gas: 64618) -TestAstariaV1Pricing:testV1PricingValidateInvalid() (gas: 56870) -TestAstariaV1Pricing:testV1PricingValidateValid() (gas: 57388) -TestAstariaV1Settlement:testGetSettlementConsideration() (gas: 440480) -TestAstariaV1Settlement:testV1SettlementHandlerValidate() (gas: 436565) -TestAstariaV1Settlement:testV1SettlementValidateInvalid() (gas: 51055) -TestAstariaV1Settlement:testV1SettlementValidateValid() (gas: 50398) -TestAstariaV1Status:testCannotRecallTwice() (gas: 530871) -TestAstariaV1Status:testInvalidRecallLoanDoesNotExist() (gas: 497581) -TestAstariaV1Status:testIsActive() (gas: 446151) -TestAstariaV1Status:testIsRecalledInsideWindow() (gas: 541272) -TestAstariaV1Status:testIsRecalledOutsideWindow() (gas: 538810) -TestAstariaV1Status:testRecallAndRefinanceInsideWindow() (gas: 683957) -TestAstariaV1Status:testRecallAndRefinanceWithLenderCaveat() (gas: 742794) -TestAstariaV1Status:testRecallPauseable() (gas: 19669) -TestAstariaV1Status:testRecallRateActiveRecall() (gas: 527199) -TestAstariaV1Status:testRecallRateEmptyRecall() (gas: 446889) -TestAstariaV1Status:testV1StatusValidateInValid() (gas: 58479) -TestAstariaV1Status:testV1StatusValidateValid() (gas: 51916) -TestCompoundInterest:testDecimalsTooHigh() (gas: 3318) -TestCompoundInterest:testInterestAccrual() (gas: 52624) -TestCompoundInterest:testMaxAmountDecimals() (gas: 7891) -TestCompoundInterest:testRateExceedsMaxRecallRate() (gas: 3297) +TestAstariaV1Loan:testNewLoanERC721CollateralDefaultTermsRecallAuctionFailLenderClaim() (gas: 569913) +TestAstariaV1Loan:testNewLoanERC721CollateralDefaultTermsRecallBase() (gas: 910085) +TestAstariaV1Loan:testNewLoanERC721CollateralDefaultTermsRecallLenderClaimRandomFulfiller() (gas: 567097) +TestAstariaV1Loan:testNewLoanERC721CollateralRecallNotBlockedBorrower() (gas: 473733) +TestAstariaV1Loan:testNewLoanERC721CollateralRecallNotBlockedLender() (gas: 473569) +TestAstariaV1Loan:testNewLoanERC721CollateralRecallerNotBorrowerOrLender() (gas: 446158) +TestAstariaV1Pricing:testGetPaymentConsiderationIncrementation() (gas: 57204) +TestAstariaV1Pricing:testGetRefinanceConsiderationAsBorrowerZeroRate() (gas: 60464) +TestAstariaV1Pricing:testGetRefinanceConsiderationInsufficientRefinance() (gas: 95736) +TestAstariaV1Pricing:testGetRefinanceConsiderationInvalidRefinance() (gas: 59309) +TestAstariaV1Pricing:testGetRefinanceConsiderationValidEqualRate() (gas: 97998) +TestAstariaV1Pricing:testGetRefinanceConsiderationValidHigherRate() (gas: 109001) +TestAstariaV1Pricing:testGetRefinanceConsiderationValidLowerRate() (gas: 104626) +TestAstariaV1Pricing:testGetRefinanceConsiderationZeroRate() (gas: 64695) +TestAstariaV1Pricing:testGetRefinanceNewDecimalMismatch() (gas: 64649) +TestAstariaV1Pricing:testV1PricingValidateInvalid() (gas: 56901) +TestAstariaV1Pricing:testV1PricingValidateValid() (gas: 57419) +TestAstariaV1Settlement:testGetSettlementConsideration() (gas: 440400) +TestAstariaV1Settlement:testV1SettlementHandlerValidate() (gas: 436596) +TestAstariaV1Settlement:testV1SettlementValidateInvalid() (gas: 51064) +TestAstariaV1Settlement:testV1SettlementValidateValid() (gas: 50429) +TestAstariaV1Status:testCannotRecallTwice() (gas: 530858) +TestAstariaV1Status:testInvalidRecallLoanDoesNotExist() (gas: 497612) +TestAstariaV1Status:testIsActive() (gas: 446160) +TestAstariaV1Status:testIsRecalledInsideWindow() (gas: 541303) +TestAstariaV1Status:testIsRecalledOutsideWindow() (gas: 538841) +TestAstariaV1Status:testRecallAndRefinanceInsideWindow() (gas: 683988) +TestAstariaV1Status:testRecallAndRefinanceWithLenderCaveat() (gas: 742715) +TestAstariaV1Status:testRecallPauseable() (gas: 19691) +TestAstariaV1Status:testRecallRateActiveRecall() (gas: 527208) +TestAstariaV1Status:testRecallRateEmptyRecall() (gas: 446920) +TestAstariaV1Status:testV1StatusValidateInValid() (gas: 58510) +TestAstariaV1Status:testV1StatusValidateValid() (gas: 51925) +TestCompoundInterest:testDecimalsTooHigh() (gas: 3340) +TestCompoundInterest:testInterestAccrual() (gas: 52646) +TestCompoundInterest:testMaxAmountDecimals() (gas: 7913) +TestCompoundInterest:testRateExceedsMaxRecallRate() (gas: 3319) TestCompoundInterest:testRateTooLowZero() (gas: 3413) -TestV1BorrowerEnforcer:testFuzzRateMethods((uint256,uint256,uint256),uint256) (runs: 10000, μ: 1223, ~: 1223) -TestV1BorrowerEnforcer:testRevertLocateCurrentRateAndAmount() (gas: 50278) -TestV1BorrowerEnforcer:testV1BorrowerEnforcerCollateralAmountOOB() (gas: 112121) -TestV1BorrowerEnforcer:testV1BorrowerEnforcerDebtAmountOOB() (gas: 99409) -TestV1BorrowerEnforcer:testV1BorrowerEnforcerDebtBundlesNotSupported() (gas: 57321) -TestV1BorrowerEnforcer:testV1BorrowerEnforcerEnd() (gas: 106805) -TestV1BorrowerEnforcer:testV1BorrowerEnforcerHalfway() (gas: 101968) -TestV1BorrowerEnforcer:testV1BorrowerEnforcerRateGTCurrent() (gas: 81834) -TestV1BorrowerEnforcer:testV1BorrowerEnforcerRateLTCurrent() (gas: 89323) -TestV1BorrowerEnforcer:testV1BorrowerEnforcerStart() (gas: 86476) -TestV1LenderEnforcer:testV1LenderEnforcerAdditionalTransfers() (gas: 105167) -TestV1LenderEnforcer:testV1LenderEnforcerAmount() (gas: 173982) -TestV1LenderEnforcer:testV1LenderEnforcerDebtBundlesNotSupported() (gas: 63352) -TestV1LenderEnforcer:testV1LenderEnforcerMatchIdentifier() (gas: 109746) -TestV1LenderEnforcer:testV1LenderEnforcerMinDebtExceedsMax() (gas: 77387) -TestV1LenderEnforcer:testV1LenderEnforcerRate() (gas: 102337) \ No newline at end of file +TestV1BorrowerEnforcer:testFuzzRateMethods((uint256,uint256,uint256),uint256) (runs: 10000, μ: 1245, ~: 1245) +TestV1BorrowerEnforcer:testRevertLocateCurrentRateAndAmount() (gas: 50309) +TestV1BorrowerEnforcer:testV1BorrowerEnforcerCollateralAmountOOB() (gas: 112152) +TestV1BorrowerEnforcer:testV1BorrowerEnforcerDebtAmountOOB() (gas: 99418) +TestV1BorrowerEnforcer:testV1BorrowerEnforcerDebtBundlesNotSupported() (gas: 57330) +TestV1BorrowerEnforcer:testV1BorrowerEnforcerEnd() (gas: 106836) +TestV1BorrowerEnforcer:testV1BorrowerEnforcerHalfway() (gas: 101999) +TestV1BorrowerEnforcer:testV1BorrowerEnforcerRateGTCurrent() (gas: 81865) +TestV1BorrowerEnforcer:testV1BorrowerEnforcerRateLTCurrent() (gas: 89310) +TestV1BorrowerEnforcer:testV1BorrowerEnforcerStart() (gas: 86485) +TestV1LenderEnforcer:testV1LenderEnforcerAdditionalTransfers() (gas: 105176) +TestV1LenderEnforcer:testV1LenderEnforcerAmount() (gas: 173991) +TestV1LenderEnforcer:testV1LenderEnforcerDebtBundlesNotSupported() (gas: 63361) +TestV1LenderEnforcer:testV1LenderEnforcerMatchIdentifier() (gas: 109755) +TestV1LenderEnforcer:testV1LenderEnforcerMinDebtExceedsMax() (gas: 77418) +TestV1LenderEnforcer:testV1LenderEnforcerRate() (gas: 102346) +TestV1RatioLenderEnforcer:testV1LenderEnforcerCopyBorrower() (gas: 82012) +TestV1RatioLenderEnforcer:testV1LenderEnforcerCopyCollateralAmount() (gas: 81517) +TestV1RatioLenderEnforcer:testV1LenderEnforcerCopyDebtAmount() (gas: 81325) +TestV1RatioLenderEnforcer:testV1LenderEnforcerCopyOriginator() (gas: 84119) +TestV1RatioLenderEnforcer:testV1RatioLenderCollateralBundle() (gas: 72008) +TestV1RatioLenderEnforcer:testV1RatioLenderDebtAmountExceedsDebtMax() (gas: 79937) +TestV1RatioLenderEnforcer:testV1RatioLenderDebtBundle() (gas: 71732) +TestV1RatioLenderEnforcer:testV1RatioLenderDefault() (gas: 83916) +TestV1RatioLenderEnforcer:testV1RatioLenderEnforcerAdditionalTransfers() (gas: 116076) +TestV1RatioLenderEnforcer:testV1RatioLenderEnforcerMatchIdentifier() (gas: 112205) +TestV1RatioLenderEnforcer:testV1RatioLenderEnforcerRate() (gas: 104962) +TestV1RatioLenderEnforcer:testV1RatioLenderLoanRateLessThanCaveatRate() (gas: 79672) +TestV1RatioLenderEnforcer:testV1RatioLenderMaxDebtOrCollateralToDebtRatioZero() (gas: 79502) +TestV1RatioLenderEnforcer:testV1RatioLenderMinCollateralAmount() (gas: 105956) \ No newline at end of file diff --git a/src/enforcers/AstariaV1RatioLenderEnforcer.sol b/src/enforcers/AstariaV1RatioLenderEnforcer.sol new file mode 100644 index 0000000..b581038 --- /dev/null +++ b/src/enforcers/AstariaV1RatioLenderEnforcer.sol @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: Apache-2.0 +// █████╗ ███████╗████████╗ █████╗ ██████╗ ██╗ █████╗ ██╗ ██╗ ██╗ +// ██╔══██╗██╔════╝╚══██╔══╝██╔══██╗██╔══██╗██║██╔══██╗ ██║ ██║███║ +// ███████║███████╗ ██║ ███████║██████╔╝██║███████║ ██║ ██║╚██║ +// ██╔══██║╚════██║ ██║ ██╔══██║██╔══██╗██║██╔══██║ ╚██╗ ██╔╝ ██║ +// ██║ ██║███████║ ██║ ██║ ██║██║ ██║██║██║ ██║ ╚████╔╝ ██║ +// ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚═╝ ╚═╝ ╚═══╝ ╚═╝ +// +// Astaria v1 Lending +// Built on Starport https://github.com/astariaXYZ/starport +// Designed with love by Astaria Labs, Inc + +pragma solidity ^0.8.17; + +import {Starport} from "starport-core/Starport.sol"; +import {CaveatEnforcer} from "starport-core/enforcers/CaveatEnforcer.sol"; +import {BasePricing} from "v1-core/pricing/BasePricing.sol"; +import {AdditionalTransfer} from "starport-core/lib/StarportLib.sol"; +import {AstariaV1Lib} from "v1-core/lib/AstariaV1Lib.sol"; +import {FixedPointMathLib} from "solady/src/utils/FixedPointMathLib.sol"; + +contract AstariaV1RatioLenderEnforcer is CaveatEnforcer { + using FixedPointMathLib for uint256; + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CUSTOM ERRORS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + error InvalidLoanTerms(); + error InvalidAdditionalTransfer(); + error LoanRateLessThanCaveatRate(); + error DebtBundlesNotSupported(); + error CollateralBundlesNotSupported(); + error DebtAmountExceedsDebtMax(uint256 maxDebt, uint256 debtAmount); + error BelowMinCollateralAmount(); + error MaxDebtOrCollateralToDebtRatioZero(); + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* STRUCTS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + struct Details { + bool matchIdentifier; + uint256 minCollateralAmount; + uint256 collateralToDebtRatio; // WAD + Starport.Loan loan; + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* PUBLIC FUNCTIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @notice Validates a loan against a caveat, w/ a minCollateralAmount, collateralToDebtRatio, and a matchIdentifier + /// @dev collateralToDebtRatio is a 1e18 value allowing a ratio conversion from collateral to debt, 1e18 was used to allow flexibility on the collateral units lower bound + /// @dev Collateral bundle support is not implemented, and will revert + /// @dev Debt bundle support is not implemented, and will revert + /// @dev matchIdentifier = false will allow the loan to have a different identifier than the caveat + /// @dev Only viable for use w/ AstariaV1Pricing and AstariaV1Status modules + function validate( + AdditionalTransfer[] calldata additionalTransfers, + Starport.Loan calldata loan, + bytes calldata caveatData + ) public view virtual override returns (bytes4 selector) { + if (loan.debt.length > 1) { + revert DebtBundlesNotSupported(); + } + + if (loan.collateral.length > 1) { + revert CollateralBundlesNotSupported(); + } + + Starport.Terms calldata loanTerms = loan.terms; + uint256 loanRate = abi.decode(loanTerms.pricingData, (BasePricing.Details)).rate; + uint256 debtAmount = loan.debt[0].amount; + AstariaV1Lib.validateCompoundInterest( + debtAmount, + loanRate, + AstariaV1Lib.getBaseRecallMax(loanTerms.statusData), + AstariaV1Lib.getBasePricingDecimals(loanTerms.pricingData) + ); + + Details memory details = abi.decode(caveatData, (Details)); + + uint256 collateralAmount = loan.collateral[0].amount; + if (details.minCollateralAmount > collateralAmount) { + revert BelowMinCollateralAmount(); + } + + uint256 maxDebt = collateralAmount.mulWad(details.collateralToDebtRatio); + if (debtAmount > maxDebt) { + revert DebtAmountExceedsDebtMax(maxDebt, debtAmount); + } + + if (maxDebt == 0) { + revert MaxDebtOrCollateralToDebtRatioZero(); + } + + bytes memory caveatPricingData = details.loan.terms.pricingData; + if (loanRate < AstariaV1Lib.getBasePricingRate(caveatPricingData)) { + // Loan rate is less than the caveatDebt rate + revert LoanRateLessThanCaveatRate(); + } + + // Update the caveat loan rate + AstariaV1Lib.setBasePricingRate(caveatPricingData, loanRate); + Starport.Loan memory caveatLoan = details.loan; + + if (!details.matchIdentifier) { + // Update the caveat loan identifier + caveatLoan.collateral[0].identifier = loan.collateral[0].identifier; + } + + // Update the caveat debt and collateral amounts + caveatLoan.debt[0].amount = debtAmount; + caveatLoan.collateral[0].amount = collateralAmount; + + // Hash and match w/ expected borrower and originator + _validate(additionalTransfers, loan, caveatLoan); + selector = CaveatEnforcer.validate.selector; + } + + function _validate( + AdditionalTransfer[] calldata additionalTransfers, + Starport.Loan calldata loan, + Starport.Loan memory caveatLoan + ) internal pure { + caveatLoan.borrower = loan.borrower; + caveatLoan.originator = loan.originator; + + if (keccak256(abi.encode(loan)) != keccak256(abi.encode(caveatLoan))) { + revert InvalidLoanTerms(); + } + + if (additionalTransfers.length > 0) { + uint256 i = 0; + for (; i < additionalTransfers.length;) { + if (additionalTransfers[i].from == loan.issuer) revert InvalidAdditionalTransfer(); + unchecked { + ++i; + } + } + } + } +} diff --git a/test/AstariaV1Test.sol b/test/AstariaV1Test.sol index e05d39b..64b7b2e 100644 --- a/test/AstariaV1Test.sol +++ b/test/AstariaV1Test.sol @@ -89,6 +89,30 @@ contract AstariaV1Test is StarportTest { return LenderEnforcer.Details({loan: refiLoan}); } + function generateDefaultERC20LoanTerms() public view virtual returns (Starport.Loan memory) { + SpentItem[] memory newCollateral = new SpentItem[](1); + newCollateral[0] = SpentItem({itemType: ItemType.ERC20, token: address(erc20s[1]), identifier: 0, amount: 1e6}); + SpentItem[] memory newDebt = new SpentItem[](1); + newDebt[0] = SpentItem({itemType: ItemType.ERC20, token: address(erc20s[0]), identifier: 0, amount: 1e18}); + return Starport.Loan({ + start: 0, + custodian: address(custodian), + borrower: borrower.addr, + issuer: lender.addr, + originator: address(0), + collateral: newCollateral, + debt: newDebt, + terms: Starport.Terms({ + status: address(status), + settlement: address(settlement), + pricing: address(pricing), + pricingData: defaultPricingData, + settlementData: defaultSettlementData, + statusData: defaultStatusData + }) + }); + } + // loan.borrower and signer.addr could be mismatched function _generateSignedCaveatBorrower(Starport.Loan memory loan, Account memory signer, bytes32 salt) public diff --git a/test/TestV1RatioLenderEnforcer.sol b/test/TestV1RatioLenderEnforcer.sol new file mode 100644 index 0000000..f41e45b --- /dev/null +++ b/test/TestV1RatioLenderEnforcer.sol @@ -0,0 +1,235 @@ +// SPDX-License-Identifier: Apache-2.0 +// █████╗ ███████╗████████╗ █████╗ ██████╗ ██╗ █████╗ ██╗ ██╗ ██╗ +// ██╔══██╗██╔════╝╚══██╔══╝██╔══██╗██╔══██╗██║██╔══██╗ ██║ ██║███║ +// ███████║███████╗ ██║ ███████║██████╔╝██║███████║ ██║ ██║╚██║ +// ██╔══██║╚════██║ ██║ ██╔══██║██╔══██╗██║██╔══██║ ╚██╗ ██╔╝ ██║ +// ██║ ██║███████║ ██║ ██║ ██║██║ ██║██║██║ ██║ ╚████╔╝ ██║ +// ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚═╝ ╚═╝ ╚═══╝ ╚═╝ +// +// Astaria v1 Lending +// Built on Starport https://github.com/astariaXYZ/starport +// Designed with love by Astaria Labs, Inc + +pragma solidity ^0.8.17; + +import "test/AstariaV1Test.sol"; + +import {Starport} from "starport-core/Starport.sol"; +import {StarportLib, AdditionalTransfer} from "starport-core/lib/StarportLib.sol"; + +import {AstariaV1RatioLenderEnforcer} from "v1-core/enforcers/AstariaV1RatioLenderEnforcer.sol"; +import {AstariaV1Lib} from "v1-core/lib/AstariaV1Lib.sol"; + +import {FixedPointMathLib} from "solady/src/utils/FixedPointMathLib.sol"; +import {BasePricing} from "v1-core/pricing/BasePricing.sol"; + +import {console2} from "forge-std/console2.sol"; + +contract TestV1RatioLenderEnforcer is AstariaV1Test, AstariaV1RatioLenderEnforcer { + using FixedPointMathLib for uint256; + + function setUp() public virtual override { + super.setUp(); + + lenderEnforcer = new AstariaV1RatioLenderEnforcer(); + } + + function getDefaultV1RatioLenderDetails(Starport.Loan memory loan) + public + pure + returns (AstariaV1RatioLenderEnforcer.Details memory details) + { + details = AstariaV1RatioLenderEnforcer.Details({ + matchIdentifier: false, + minCollateralAmount: loan.collateral[0].amount, + collateralToDebtRatio: loan.debt[0].amount.divWadUp(loan.collateral[0].amount), + loan: loanCopy(loan) + }); + } + + function testV1RatioLenderDefault() public { + Starport.Loan memory loan = generateDefaultLoanTerms(); + AstariaV1RatioLenderEnforcer.Details memory details = getDefaultV1RatioLenderDetails(loan); + + // Test general passing case + lenderEnforcer.validate(new AdditionalTransfer[](0), loan, abi.encode(details)); + } + + function testV1RatioLenderDebtBundle() public { + Starport.Loan memory loan = generateDefaultLoanTerms(); + AstariaV1RatioLenderEnforcer.Details memory details = getDefaultV1RatioLenderDetails(loan); + + // Test Debt Bundle + loan.debt = new SpentItem[](2); + vm.expectRevert(DebtBundlesNotSupported.selector); + lenderEnforcer.validate(new AdditionalTransfer[](0), loan, abi.encode(details)); + } + + function testV1RatioLenderCollateralBundle() public { + Starport.Loan memory loan = generateDefaultLoanTerms(); + AstariaV1RatioLenderEnforcer.Details memory details = getDefaultV1RatioLenderDetails(loan); + + // Test Collateral Bundle + loan.collateral = new SpentItem[](2); + vm.expectRevert(CollateralBundlesNotSupported.selector); + lenderEnforcer.validate(new AdditionalTransfer[](0), loan, abi.encode(details)); + } + + function testV1RatioLenderMinCollateralAmount() public { + Starport.Loan memory loan = generateDefaultLoanTerms(); + AstariaV1RatioLenderEnforcer.Details memory details = getDefaultV1RatioLenderDetails(loan); + + // Test below min collateral amount + loan.collateral[0].amount--; + vm.expectRevert(BelowMinCollateralAmount.selector); + lenderEnforcer.validate(new AdditionalTransfer[](0), loan, abi.encode(details)); + + // Test below min above collateral amount + loan.collateral[0].amount += 2; + lenderEnforcer.validate(new AdditionalTransfer[](0), loan, abi.encode(details)); + } + + function testV1RatioLenderDebtAmountExceedsDebtMax() public { + Starport.Loan memory loan = generateDefaultLoanTerms(); + AstariaV1RatioLenderEnforcer.Details memory details = getDefaultV1RatioLenderDetails(loan); + + // + loan.debt[0].amount++; + vm.expectRevert( + abi.encodeWithSelector( + DebtAmountExceedsDebtMax.selector, + loan.collateral[0].amount.mulWad(details.collateralToDebtRatio), + loan.debt[0].amount + ) + ); + lenderEnforcer.validate(new AdditionalTransfer[](0), loan, abi.encode(details)); + } + + function testV1RatioLenderMaxDebtOrCollateralToDebtRatioZero() public { + Starport.Loan memory loan = generateDefaultLoanTerms(); + AstariaV1RatioLenderEnforcer.Details memory details = getDefaultV1RatioLenderDetails(loan); + + loan.debt[0].amount = 0; + loan.collateral[0].amount = 0; + details.collateralToDebtRatio = 0; + details.minCollateralAmount = 0; + vm.expectRevert(abi.encodeWithSelector(MaxDebtOrCollateralToDebtRatioZero.selector)); + lenderEnforcer.validate(new AdditionalTransfer[](0), loan, abi.encode(details)); + } + + function testV1RatioLenderLoanRateLessThanCaveatRate() public { + Starport.Loan memory loan = generateDefaultLoanTerms(); + AstariaV1RatioLenderEnforcer.Details memory details = getDefaultV1RatioLenderDetails(loan); + + BasePricing.Details memory pricingDetails = abi.decode(loan.terms.pricingData, (BasePricing.Details)); + pricingDetails.rate--; + loan.terms.pricingData = abi.encode(pricingDetails); + vm.expectRevert(LoanRateLessThanCaveatRate.selector); + lenderEnforcer.validate(new AdditionalTransfer[](0), loan, abi.encode(details)); + } + + function testV1RatioLenderEnforcerRate() public { + Starport.Loan memory loan = generateDefaultLoanTerms(); + AstariaV1RatioLenderEnforcer.Details memory details = getDefaultV1RatioLenderDetails(loan); + + // Test malleable rate + AstariaV1Lib.setBasePricingRate( + loan.terms.pricingData, AstariaV1Lib.getBasePricingRate(details.loan.terms.pricingData) + 1 + ); + lenderEnforcer.validate(new AdditionalTransfer[](0), loan, abi.encode(details)); + + // Test insufficient rate + AstariaV1Lib.setBasePricingRate( + loan.terms.pricingData, AstariaV1Lib.getBasePricingRate(details.loan.terms.pricingData) - 1 + ); + vm.expectRevert(LoanRateLessThanCaveatRate.selector); + lenderEnforcer.validate(new AdditionalTransfer[](0), loan, abi.encode(details)); + } + + // Test matchIdentifier + function testV1RatioLenderEnforcerMatchIdentifier() public { + Starport.Loan memory loan = generateDefaultLoanTerms(); + AstariaV1RatioLenderEnforcer.Details memory details = getDefaultV1RatioLenderDetails(loan); + + loan.collateral[0].identifier += 1; + + lenderEnforcer.validate(new AdditionalTransfer[](0), loan, abi.encode(details)); + details.matchIdentifier = true; + + vm.expectRevert(LenderEnforcer.InvalidLoanTerms.selector); + lenderEnforcer.validate(new AdditionalTransfer[](0), loan, abi.encode(details)); + } + + function testV1RatioLenderEnforcerAdditionalTransfers() external { + Starport.Loan memory loan = generateDefaultLoanTerms(); + AstariaV1RatioLenderEnforcer.Details memory details = getDefaultV1RatioLenderDetails(loan); + // Test invalid additional transfer from lender + AdditionalTransfer[] memory additionalTransfers = new AdditionalTransfer[](1); + additionalTransfers[0] = AdditionalTransfer({ + token: address(0), + amount: 0, + to: address(0), + from: lender.addr, + identifier: 0, + itemType: ItemType.ERC20 + }); + + vm.expectRevert(LenderEnforcer.InvalidAdditionalTransfer.selector); + lenderEnforcer.validate(additionalTransfers, loan, abi.encode(details)); + + // Test valid additional transfer from other party + additionalTransfers[0].from = borrower.addr; + lenderEnforcer.validate(additionalTransfers, loan, abi.encode(details)); + } + + // ensures that a debt copy occurs at the end of validate + function testV1LenderEnforcerCopyDebtAmount() external { + Starport.Loan memory loan = generateDefaultERC20LoanTerms(); + AstariaV1RatioLenderEnforcer.Details memory details = AstariaV1RatioLenderEnforcer.Details({ + matchIdentifier: false, + minCollateralAmount: 1, + collateralToDebtRatio: loan.debt[0].amount.divWadUp(loan.collateral[0].amount), + loan: loanCopy(loan) + }); + + loan.debt[0].amount /= 2; + lenderEnforcer.validate(new AdditionalTransfer[](0), loan, abi.encode(details)); + } + + // ensures that a collateral copy occurs at the end of validate + function testV1LenderEnforcerCopyCollateralAmount() external { + Starport.Loan memory loan = generateDefaultERC20LoanTerms(); + AstariaV1RatioLenderEnforcer.Details memory details = AstariaV1RatioLenderEnforcer.Details({ + matchIdentifier: false, + minCollateralAmount: 1, + collateralToDebtRatio: loan.debt[0].amount.divWadUp(loan.collateral[0].amount), + loan: loanCopy(loan) + }); + + loan.collateral[0].amount = 10; + loan.debt[0].amount = loan.collateral[0].amount.mulWad(details.collateralToDebtRatio); + lenderEnforcer.validate(new AdditionalTransfer[](0), loan, abi.encode(details)); + } + + // ensures that a borrower copy occurs at the beginning of _validate + function testV1LenderEnforcerCopyBorrower() external { + Starport.Loan memory loan = generateDefaultERC20LoanTerms(); + AstariaV1RatioLenderEnforcer.Details memory details = getDefaultV1RatioLenderDetails(loan); + + // ensuring that the details.loan.borrower does not match the loan.borrower + details.loan.borrower = address(uint160(details.loan.borrower) << 1); + assert(details.loan.borrower != loan.borrower); + lenderEnforcer.validate(new AdditionalTransfer[](0), loan, abi.encode(details)); + } + + // ensures that a originator copy occurs at the beginning of _validate + function testV1LenderEnforcerCopyOriginator() external { + Starport.Loan memory loan = generateDefaultERC20LoanTerms(); + AstariaV1RatioLenderEnforcer.Details memory details = getDefaultV1RatioLenderDetails(loan); + + // ensuring that the details.loan.originator does not match the loan.originator + details.loan.originator = address(uint160(fulfiller.addr) << 1); + assert(details.loan.originator != loan.originator); + lenderEnforcer.validate(new AdditionalTransfer[](0), loan, abi.encode(details)); + } +}