Skip to content

Commit

Permalink
feat: tests for ratio lender
Browse files Browse the repository at this point in the history
  • Loading branch information
dangerousfood committed May 10, 2024
1 parent c81527f commit 8176703
Show file tree
Hide file tree
Showing 2 changed files with 252 additions and 29 deletions.
46 changes: 17 additions & 29 deletions src/enforcers/AstariaV1RatioLenderEnforcer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,12 @@ 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 {ItemType} from "seaport-types/src/lib/ConsiderationEnums.sol";
import {SpentItem} from "seaport-types/src/lib/ConsiderationStructs.sol";
import {FixedPointMathLib} from "solady/src/utils/FixedPointMathLib.sol";

contract AstariaV1RatioLenderEnforcer is CaveatEnforcer {
using FixedPointMathLib for uint256;

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* CUSTOM ERRORS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
Expand All @@ -34,16 +31,10 @@ contract AstariaV1RatioLenderEnforcer is CaveatEnforcer {
error LoanRateLessThanCaveatRate();
error DebtBundlesNotSupported();
error CollateralBundlesNotSupported();
error DebtAmountExceedsDebtMax(uint256 maxDebt, uint256 loanAmount);
error DebtAmountExceedsDebtMax(uint256 maxDebt, uint256 debtAmount);
error BelowMinCollateralAmount();
error MaxDebtOrCollateralToDebtRatioZero();

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* CONSTANTS AND IMMUTABLES */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

uint256 constant MAX_DURATION = uint256(3 * 365 days); // 3 years

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* STRUCTS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
Expand All @@ -59,8 +50,10 @@ contract AstariaV1RatioLenderEnforcer is CaveatEnforcer {
/* PUBLIC FUNCTIONS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

/// @notice Validates a loan against a caveat, w/ a minimum rate and a maximum amount
/// @dev Bundle support is not implemented, and will revert
/// @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(
Expand All @@ -78,9 +71,9 @@ contract AstariaV1RatioLenderEnforcer is CaveatEnforcer {

Starport.Terms calldata loanTerms = loan.terms;
uint256 loanRate = abi.decode(loanTerms.pricingData, (BasePricing.Details)).rate;
uint256 loanAmount = loan.debt[0].amount;
uint256 debtAmount = loan.debt[0].amount;
AstariaV1Lib.validateCompoundInterest(
loanAmount,
debtAmount,
loanRate,
AstariaV1Lib.getBaseRecallMax(loanTerms.statusData),
AstariaV1Lib.getBasePricingDecimals(loanTerms.pricingData)
Expand All @@ -93,9 +86,9 @@ contract AstariaV1RatioLenderEnforcer is CaveatEnforcer {
revert BelowMinCollateralAmount();
}

uint256 maxDebt = (collateralAmount * details.collateralToDebtRatio) / AstariaV1Lib.WAD;
if (loanAmount > maxDebt) {
revert DebtAmountExceedsDebtMax(maxDebt, loanAmount);
uint256 maxDebt = collateralAmount.mulWad(details.collateralToDebtRatio);
if (debtAmount > maxDebt) {
revert DebtAmountExceedsDebtMax(maxDebt, debtAmount);
}

if (maxDebt == 0) {
Expand All @@ -112,21 +105,16 @@ contract AstariaV1RatioLenderEnforcer is CaveatEnforcer {
AstariaV1Lib.setBasePricingRate(caveatPricingData, loanRate);
Starport.Loan memory caveatLoan = details.loan;

// Update the caveat loan amount
caveatLoan.debt[0].amount = loanAmount;

if (!details.matchIdentifier) {
// Update the caveat loan identifier
uint256 i = 0;
for (; i < caveatLoan.collateral.length;) {
caveatLoan.collateral[i].identifier = loan.collateral[i].identifier;
unchecked {
++i;
}
}
caveatLoan.collateral[0].identifier = loan.collateral[0].identifier;
}

// Hash and match w/ expected borrower
// 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;
}
Expand Down
235 changes: 235 additions & 0 deletions test/TestV1RatioLenderEnforcer.sol
Original file line number Diff line number Diff line change
@@ -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));
}
}

0 comments on commit 8176703

Please sign in to comment.