Skip to content

Commit

Permalink
feat/exotic repayment settlement tests (#76)
Browse files Browse the repository at this point in the history
* update for simple interest pricing to ensure old and new decimals are the same, update _settleLoan helper, add exotic repayment/settlement testing with custom pricing and settlement modules

* update fuzzers with random erc20 and decimals, update applyRefinanceConsideration to just override the debt based on incoming payment data from getRefinanceConsideration

* simply return from applyRefinanceConsideration

* chore: snapshot

* update order of operations to account for changes to the applyRefinanceConsiderationToLoan method accessing memory directly
  • Loading branch information
androolloyd authored Nov 20, 2023
1 parent 92c45f0 commit 4e0b967
Show file tree
Hide file tree
Showing 8 changed files with 551 additions and 177 deletions.
222 changes: 112 additions & 110 deletions .gas-snapshot

Large diffs are not rendered by default.

34 changes: 20 additions & 14 deletions src/Starport.sol
Original file line number Diff line number Diff line change
Expand Up @@ -209,12 +209,13 @@ contract Starport is PausableNonReentrant {

_settle(loan);
_postRepaymentExecute(loan, msg.sender);
loan = applyRefinanceConsiderationToLoan(loan, considerationPayment, carryPayment, pricingData);

StarportLib.transferSpentItems(considerationPayment, lender, loan.issuer, false);
if (carryPayment.length > 0) {
StarportLib.transferSpentItems(carryPayment, lender, loan.originator, false);
}
loan.debt = applyRefinanceConsiderationToLoan(considerationPayment, carryPayment);
loan.terms.pricingData = pricingData;

loan.issuer = lender;
loan.originator = address(0);
Expand Down Expand Up @@ -245,39 +246,44 @@ contract Starport is PausableNonReentrant {
}
}

function applyRefinanceConsiderationToLoan(
Starport.Loan memory loan,
SpentItem[] memory considerationPayment,
SpentItem[] memory carryPayment,
bytes calldata pricingData
) public pure returns (Starport.Loan memory) {
function applyRefinanceConsiderationToLoan(SpentItem[] memory considerationPayment, SpentItem[] memory carryPayment)
public
pure
returns (SpentItem[] memory newDebt)
{
if (
considerationPayment.length == 0
|| (carryPayment.length != 0 && considerationPayment.length != carryPayment.length)
|| considerationPayment.length != loan.debt.length
) {
revert MalformedRefinance();
}

uint256 i = 0;
if (carryPayment.length > 0) {
SpentItem[] memory newDebt = new SpentItem[](considerationPayment.length);
uint256 i = 0;
for (; i < considerationPayment.length;) {
loan.debt[i].amount = considerationPayment[i].amount + carryPayment[i].amount;

newDebt[i] = considerationPayment[i];
newDebt[i].amount += carryPayment[i].amount;
if (newDebt[i].itemType == ItemType.ERC721 && newDebt[i].amount > 1) {
revert MalformedRefinance();
}
unchecked {
++i;
}
}
return newDebt;
} else {
uint256 i = 0;
for (; i < considerationPayment.length;) {
loan.debt[i].amount = considerationPayment[i].amount;
if (considerationPayment[i].itemType == ItemType.ERC721 && considerationPayment[i].amount > 1) {
revert MalformedRefinance();
}
unchecked {
++i;
}
}
return considerationPayment;
}
loan.terms.pricingData = pricingData;
return loan;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/pricing/SimpleInterestPricing.sol
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ contract SimpleInterestPricing is BasePricing {
Details memory oldDetails = abi.decode(loan.terms.pricingData, (Details));
Details memory newDetails = abi.decode(newPricingData, (Details));

if ((newDetails.rate < oldDetails.rate)) {
if (oldDetails.decimals == newDetails.decimals && (newDetails.rate < oldDetails.rate)) {
(repayConsideration, carryConsideration) = getPaymentConsideration(loan);
additionalConsideration = new AdditionalTransfer[](0);
} else {
Expand Down
16 changes: 5 additions & 11 deletions test/StarportTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -483,16 +483,6 @@ contract StarportTest is BaseOrderTest {
return refinanceLoan(loan, newPricingData, asWho, lenderCaveat, lender, "");
}

function getRefinanceCaveat(Starport.Loan memory loan, bytes memory pricingData, address fulfiller)
public
view
returns (Starport.Loan memory)
{
(SpentItem[] memory considerationPayment, SpentItem[] memory carryPayment,) =
Pricing(loan.terms.pricing).getRefinanceConsideration(loan, pricingData, fulfiller);
return SP.applyRefinanceConsiderationToLoan(loan, considerationPayment, carryPayment, pricingData);
}

function refinanceLoan(
Starport.Loan memory loan,
bytes memory pricingData,
Expand Down Expand Up @@ -584,7 +574,11 @@ contract StarportTest is BaseOrderTest {
(SpentItem[] memory offer, ReceivedItem[] memory paymentConsideration) = Custodian(
payable(activeLoan.custodian)
).previewOrder(
address(consideration), fulfiller, new SpentItem[](0), new SpentItem[](0), abi.encode(activeLoan)
address(consideration),
fulfiller,
new SpentItem[](0),
new SpentItem[](0),
abi.encode(Custodian.Command(Actions.Settlement, activeLoan, ""))
);

OrderParameters memory op = _buildContractOrder(
Expand Down
144 changes: 117 additions & 27 deletions test/fuzz-testing/TestFuzzStarport.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,63 @@ import "starport-test/utils/Bound.sol";
import {FixedPointMathLib} from "solady/src/utils/FixedPointMathLib.sol";
import {DeepEq} from "../utils/DeepEq.sol";
import {StarportLib} from "starport-core/lib/StarportLib.sol";
import {ERC20 as RariERC20} from "@rari-capital/solmate/src/tokens/ERC20.sol";

contract TestDebt is RariERC20 {
bool public blocked;

bool public noReturnData;

constructor(uint8 decimals) RariERC20("Test20", "TST20", decimals) {
blocked = false;
noReturnData = false;
}

function blockTransfer(bool blocking) external {
blocked = blocking;
}

function setNoReturnData(bool noReturn) external {
noReturnData = noReturn;
}

function mint(address to, uint256 amount) external returns (bool) {
_mint(to, amount);
return true;
}

function transferFrom(address from, address to, uint256 amount) public override returns (bool ok) {
if (blocked) {
return false;
}

uint256 allowed = allowance[from][msg.sender];

if (amount > allowed) {
revert("NOT_AUTHORIZED");
}

super.transferFrom(from, to, amount);

if (noReturnData) {
assembly {
return(0, 0)
}
}

ok = true;
}

function increaseAllowance(address spender, uint256 amount) external returns (bool) {
uint256 current = allowance[msg.sender][spender];
uint256 remaining = type(uint256).max - current;
if (amount > remaining) {
amount = remaining;
}
approve(spender, current + amount);
return true;
}
}

contract TestFuzzStarport is StarportTest, Bound, DeepEq {
using FixedPointMathLib for uint256;
Expand Down Expand Up @@ -55,7 +112,7 @@ contract TestFuzzStarport is StarportTest, Bound, DeepEq {
BasePricing.Details memory details = BasePricing.Details({
rate: _boundMax(min, (uint256(1e16) * 150)),
carryRate: _boundMax(0, uint256((1e16 * 100))),
decimals: 18
decimals: _boundMax(0, 18)
});
pricingData = abi.encode(details);
}
Expand Down Expand Up @@ -147,12 +204,24 @@ contract TestFuzzStarport is StarportTest, Bound, DeepEq {
}
loan.collateral = ret;
SpentItem[] memory debt = new SpentItem[](1);
debt[0] = SpentItem({
itemType: ItemType.ERC20,
identifier: 0,
amount: _boundMax(params.debtAmount, type(uint112).max),
token: address(erc20s[1])
});
BasePricing.Details memory pricingDetails = abi.decode(loan.terms.pricingData, (BasePricing.Details));
if (pricingDetails.decimals == 18) {
debt[0] = SpentItem({
itemType: ItemType.ERC20,
identifier: 0,
amount: _boundMax(params.debtAmount, type(uint128).max),
token: address(erc20s[1])
});
} else {
TestDebt newDebt = new TestDebt(uint8(pricingDetails.decimals));
debt[0] = SpentItem({
itemType: ItemType.ERC20,
identifier: 0,
amount: _boundMax(params.debtAmount, type(uint128).max),
token: address(newDebt)
});
}

loan.debt = debt;
loan.borrower = borrower.addr;
loan.custodian = SP.defaultCustodian();
Expand Down Expand Up @@ -216,17 +285,19 @@ contract TestFuzzStarport is StarportTest, Bound, DeepEq {
} else {
fulfiller = _toAddress(_boundMin(_toUint(params.fulfiller), 100));
}
uint256 borrowerDebtBalanceBefore = erc20s[1].balanceOf(loan.borrower);
uint256 borrowerDebtBalanceBefore = ERC20(loan.debt[0].token).balanceOf(loan.borrower);

goodLoan = newLoan(loan, borrowerSalt, lenderSalt, fulfiller);

if (params.feesOn) {
assert(
erc20s[1].balanceOf(loan.borrower)
ERC20(loan.debt[0].token).balanceOf(loan.borrower)
== (borrowerDebtBalanceBefore + (loan.debt[0].amount - loan.debt[0].amount.mulWad(feeRake)))
);
} else {
assert(erc20s[1].balanceOf(loan.borrower) == (borrowerDebtBalanceBefore + loan.debt[0].amount));
assert(
ERC20(loan.debt[0].token).balanceOf(loan.borrower) == (borrowerDebtBalanceBefore + loan.debt[0].amount)
);
}
}

Expand Down Expand Up @@ -349,16 +420,22 @@ contract TestFuzzStarport is StarportTest, Bound, DeepEq {
extraData: abi.encode(Custodian.Command(Actions.Repayment, badLoan, ""))
});

vm.startPrank(badLoan.borrower);
for (uint256 i = 0; i < paymentConsideration.length; i++) {
TestDebt token = TestDebt(paymentConsideration[i].token);
token.mint(goodLoan.borrower, paymentConsideration[i].amount);
token.approve(address(consideration), type(uint256).max);
}
if (keccak256(abi.encode(goodLoan)) != keccak256(abi.encode(badLoan))) {
vm.expectRevert();
}
vm.prank(badLoan.borrower);
consideration.fulfillAdvancedOrder({
advancedOrder: x,
criteriaResolvers: new CriteriaResolver[](0),
fulfillerConduitKey: bytes32(0),
recipient: address(badLoan.borrower)
});
vm.stopPrank();
}

function testFuzzRepaymentSuccess(FuzzRepaymentLoan memory params) public {
Expand All @@ -373,9 +450,6 @@ contract TestFuzzStarport is StarportTest, Bound, DeepEq {
new SpentItem[](0),
abi.encode(Custodian.Command(Actions.Repayment, goodLoan, ""))
);
for (uint256 i = 0; i < paymentConsideration.length; i++) {
erc20s[0].mint(goodLoan.borrower, paymentConsideration[i].amount);
}

OrderParameters memory op = _buildContractOrder(
address(goodLoan.custodian), _SpentItemsToOfferItems(offer), _toConsiderationItems(paymentConsideration)
Expand All @@ -389,7 +463,11 @@ contract TestFuzzStarport is StarportTest, Bound, DeepEq {
});

vm.startPrank(goodLoan.borrower);
erc20s[0].approve(address(consideration), type(uint256).max);
for (uint256 i = 0; i < paymentConsideration.length; i++) {
TestDebt token = TestDebt(paymentConsideration[i].token);
token.mint(goodLoan.borrower, paymentConsideration[i].amount);
token.approve(address(consideration), type(uint256).max);
}
consideration.fulfillAdvancedOrder({
advancedOrder: x,
criteriaResolvers: new CriteriaResolver[](0),
Expand Down Expand Up @@ -429,16 +507,22 @@ contract TestFuzzStarport is StarportTest, Bound, DeepEq {
extraData: abi.encode(Actions.Settlement, badLoan)
});

vm.startPrank(badLoan.borrower);
for (uint256 i = 0; i < paymentConsideration.length; i++) {
TestDebt token = TestDebt(paymentConsideration[i].token);
token.mint(goodLoan.borrower, paymentConsideration[i].amount);
token.approve(address(consideration), type(uint256).max);
}
if (keccak256(abi.encode(goodLoan)) != keccak256(abi.encode(badLoan))) {
vm.expectRevert();
}
vm.prank(badLoan.borrower);
consideration.fulfillAdvancedOrder({
advancedOrder: x,
criteriaResolvers: new CriteriaResolver[](0),
fulfillerConduitKey: bytes32(0),
recipient: address(badLoan.borrower)
});
vm.stopPrank();
}

function _generateGoodLoan(FuzzLoan memory params) internal virtual returns (Starport.Loan memory) {
Expand Down Expand Up @@ -487,7 +571,11 @@ contract TestFuzzStarport is StarportTest, Bound, DeepEq {
});

vm.startPrank(filler);
erc20s[1].approve(address(consideration), type(uint256).max);
for (uint256 i = 0; i < paymentConsideration.length; i++) {
TestDebt token = TestDebt(paymentConsideration[i].token);
token.mint(filler, paymentConsideration[i].amount);
token.approve(address(consideration), type(uint256).max);
}
consideration.fulfillAdvancedOrder({
advancedOrder: x,
criteriaResolvers: new CriteriaResolver[](0),
Expand All @@ -500,11 +588,14 @@ contract TestFuzzStarport is StarportTest, Bound, DeepEq {
function testFuzzRefinance(FuzzRefinanceLoan memory params) public virtual {
Starport.Loan memory goodLoan = fuzzNewLoanOrigination(params.origination, abi.encode(LoanBounds(1)));

uint256 oldRate = abi.decode(goodLoan.terms.pricingData, (BasePricing.Details)).rate;
BasePricing.Details memory oldDetails = abi.decode(goodLoan.terms.pricingData, (BasePricing.Details));

uint256 newRate = _boundMax(oldRate - 1, (uint256(1e16) * 1000) / (365 * 1 days));
BasePricing.Details memory newPricingDetails =
BasePricing.Details({rate: newRate, carryRate: _boundMax(0, uint256((1e16 * 100))), decimals: 18});
uint256 newRate = _boundMax(oldDetails.rate - 1, (uint256(1e16) * 1000) / (365 * 1 days));
BasePricing.Details memory newPricingDetails = BasePricing.Details({
rate: newRate,
carryRate: _boundMax(0, uint256((1e16 * 100))),
decimals: oldDetails.decimals
});
Account memory account = makeAndAllocateAccount(params.refiKey);

address refiFulfiller;
Expand All @@ -525,11 +616,10 @@ contract TestFuzzStarport is StarportTest, Bound, DeepEq {
refiFulfiller = _toAddress(_boundMin(params.skipTime, 100));
}
Starport.Loan memory goodLoan2 = goodLoan;
LenderEnforcer.Details memory details = LenderEnforcer.Details({
loan: SP.applyRefinanceConsiderationToLoan(
goodLoan2, considerationPayment, carryPayment, abi.encode(newPricingDetails)
)
});
Starport.Loan memory refiLoan = loanCopy(goodLoan);
refiLoan.terms.pricingData = abi.encode(newPricingDetails);
refiLoan.debt = SP.applyRefinanceConsiderationToLoan(considerationPayment, carryPayment);
LenderEnforcer.Details memory details = LenderEnforcer.Details({loan: refiLoan});
_issueAndApproveTarget(details.loan.debt, account.addr, address(SP));

details.loan.issuer = account.addr;
Expand All @@ -543,7 +633,7 @@ contract TestFuzzStarport is StarportTest, Bound, DeepEq {
enforcer: address(lenderEnforcer)
});
{
if (newRate > oldRate) {
if (newRate > oldDetails.rate) {
vm.expectRevert();
}
vm.prank(refiFulfiller);
Expand Down
14 changes: 7 additions & 7 deletions test/integration-testing/TestCaveats.sol
Original file line number Diff line number Diff line change
Expand Up @@ -185,10 +185,9 @@ contract IntegrationTestCaveats is StarportTest, DeepEq, MockCall {

function testRefinanceWCaveatsInvalidSalt() public {
Starport.Loan memory loan = newLoanWithDefaultTerms();

LenderEnforcer.Details memory details = LenderEnforcer.Details({
loan: SP.applyRefinanceConsiderationToLoan(loan, loan.debt, new SpentItem[](0), defaultPricingData)
});
Starport.Loan memory refiLoan = loanCopy(loan);
refiLoan.debt = SP.applyRefinanceConsiderationToLoan(loan.debt, new SpentItem[](0));
LenderEnforcer.Details memory details = LenderEnforcer.Details({loan: refiLoan});

details.loan.issuer = lender.addr;
details.loan.originator = address(0);
Expand Down Expand Up @@ -238,9 +237,10 @@ contract IntegrationTestCaveats is StarportTest, DeepEq, MockCall {

function testRefinanceUnapprovedFulfiller() public {
Starport.Loan memory loan = newLoanWithDefaultTerms();
LenderEnforcer.Details memory details = LenderEnforcer.Details({
loan: SP.applyRefinanceConsiderationToLoan(loan, loan.debt, new SpentItem[](0), defaultPricingData)
});
Starport.Loan memory refiLoan = loanCopy(loan);

refiLoan.debt = SP.applyRefinanceConsiderationToLoan(loan.debt, new SpentItem[](0));
LenderEnforcer.Details memory details = LenderEnforcer.Details({loan: refiLoan});

details.loan.issuer = lender.addr;
details.loan.originator = address(0);
Expand Down
Loading

0 comments on commit 4e0b967

Please sign in to comment.