Skip to content

Commit

Permalink
feat: implement ratio lender (#51)
Browse files Browse the repository at this point in the history
* feat: implement ratio lender

* feat: tests for ratio lender
  • Loading branch information
dangerousfood authored May 31, 2024
1 parent 3796779 commit da69f76
Show file tree
Hide file tree
Showing 4 changed files with 470 additions and 53 deletions.
120 changes: 67 additions & 53 deletions .gas-snapshot
Original file line number Diff line number Diff line change
@@ -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)
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)
144 changes: 144 additions & 0 deletions src/enforcers/AstariaV1RatioLenderEnforcer.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
}
}
24 changes: 24 additions & 0 deletions test/AstariaV1Test.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit da69f76

Please sign in to comment.