diff --git a/.gas-snapshot b/.gas-snapshot index 8b16ebae..39342dff 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,4 +1,11 @@ -TestStarLite:testNewLoan() (gas: 2424939) -TestStarLite:testRepayLoan() (gas: 3078108) +TestAstariaV1Loan:testNewLoanERC721CollateralDefaultTermsRecall() (gas: 952394) +TestLoanManager:testStorage() (gas: 2283158165) +TestLoanManager:testSupportsInterface() (gas: 6937) +TestNewLoan:testBuyNowPayLater() (gas: 1141902) +TestNewLoan:testNewLoanERC721CollateralDefaultTerms2():((uint256,address,address,address,address,(uint8,address,uint256,uint256)[],(uint8,address,uint256,uint256)[],(address,bytes,address,bytes,address,bytes))) (gas: 987750) +TestNewLoan:testNewLoanERC721CollateralDefaultTermsRefinance() (gas: 648498) +TestNewLoan:testNewLoanERC721CollateralDefaultTermsWithMerkleProof():((uint256,address,address,address,address,(uint8,address,uint256,uint256)[],(uint8,address,uint256,uint256)[],(address,bytes,address,bytes,address,bytes))) (gas: 581604) +TestNewLoan:testSettleLoan() (gas: 1260468) +TestRepayLoan:testRepayLoan() (gas: 718985) TestStarLiteUtils:testEncodeReceivedWithRecipient() (gas: 17955) TestStarLiteUtils:testSpentToReceived() (gas: 17796) \ No newline at end of file diff --git a/src/LoanManager.sol b/src/LoanManager.sol index ea8b4f74..44e5fcef 100644 --- a/src/LoanManager.sol +++ b/src/LoanManager.sol @@ -469,20 +469,21 @@ contract LoanManager is ERC721, ContractOffererInterface, ConduitHelper { bytes32 caveatHash ) internal returns (SpentItem[] memory offer) { offer = new SpentItem[](debt.length + 1); - offer[0] = SpentItem({ - itemType: ItemType.ERC721, - token: address(this), - identifier: uint256(caveatHash), - amount: 1 - }); - uint256 i = 0; - for (; i < debt.length; ) { - offer[i + 1] = debt[i]; + + for (uint256 i; i < debt.length; ) { + offer[i] = debt[i]; _setDebtApprovals(debt[i]); unchecked { ++i; } } + + offer[debt.length] = SpentItem({ + itemType: ItemType.ERC721, + token: address(this), + identifier: uint256(caveatHash), + amount: 1 + }); } function transferFrom( @@ -546,6 +547,7 @@ contract LoanManager is ERC721, ContractOffererInterface, ConduitHelper { address conduit ) external { (, , address conduitController) = seaport.information(); + if ( ConduitControllerInterface(conduitController).ownerOf(conduit) != msg.sender @@ -575,16 +577,19 @@ contract LoanManager is ERC721, ContractOffererInterface, ConduitHelper { refinanceConsideration = _removeZeroAmounts(refinanceConsideration); // if for malicious or non-malicious the refinanceConsideration is zero - if (refinanceConsideration.length == 0) + if (refinanceConsideration.length == 0) { revert InvalidNoRefinanceConsideration(); + } + _settle(loan); - uint256 i = 0; - for (; i < loan.debt.length; ) { + + for (uint256 i; i < loan.debt.length; ) { loan.debt[i].amount = considerationPayment[i].amount; unchecked { ++i; } } + if ( ConduitInterface(conduit).execute( _packageTransfers(refinanceConsideration, msg.sender) diff --git a/src/hooks/BaseRecall.sol b/src/hooks/BaseRecall.sol index 7df88a99..86a1be7c 100644 --- a/src/hooks/BaseRecall.sol +++ b/src/hooks/BaseRecall.sol @@ -112,7 +112,9 @@ abstract contract BaseRecall is ConduitHelper { bytes memory encodedLoan = abi.encode(loan); uint256 loanId = uint256(keccak256(encodedLoan)); + if (!LM.active(loanId)) revert LoanDoesNotExist(); + recalls[loanId] = Recall(payable(msg.sender), uint64(block.timestamp)); emit Recalled(loanId, msg.sender, loan.start + details.recallWindow); } @@ -131,8 +133,10 @@ abstract contract BaseRecall is ConduitHelper { Recall storage recall = recalls[loanId]; // ensure that a recall exists for the provided tokenId, ensure that the recall - if (recall.start == 0 || recall.recaller == address(0)) + if (recall.start == 0 || recall.recaller == address(0)) { revert WithdrawDoesNotExist(); + } + ReceivedItem[] memory recallConsideration = _generateRecallConsideration( loan, 0, @@ -143,13 +147,15 @@ abstract contract BaseRecall is ConduitHelper { recall.recaller = payable(address(0)); recall.start = 0; - uint256 i = 0; - for (; i < recallConsideration.length; ) { + for (uint256 i; i < recallConsideration.length; ) { + if (loan.debt[i].itemType != ItemType.ERC20) revert InvalidStakeType(); + ERC20(loan.debt[i].token).transfer( receiver, recallConsideration[i].amount ); + unchecked { ++i; } @@ -168,17 +174,15 @@ abstract contract BaseRecall is ConduitHelper { (BasePricing.Details) ); recallStake = new uint256[](loan.debt.length); - uint256 i = 0; - for (; i < loan.debt.length; ) { - uint256 delta_t = end - start; - uint256 stake = BasePricing(loan.terms.pricing).getInterest( + for (uint256 i; i < loan.debt.length; ) { + recallStake[i] = BasePricing(loan.terms.pricing).getInterest( loan, details, start, end, i ); - recallStake[i] = stake; + unchecked { ++i; } @@ -210,14 +214,12 @@ abstract contract BaseRecall is ConduitHelper { ) internal view returns (ReceivedItem[] memory consideration) { uint256[] memory stake = _getRecallStake(loan, start, end); consideration = new ReceivedItem[](stake.length); - uint256 i = 0; - for (; i < consideration.length; ) { + + for (uint256 i; i < consideration.length; ) { consideration[i] = ReceivedItem({ itemType: loan.debt[i].itemType, identifier: loan.debt[i].identifier, - amount: stake.length == consideration.length - ? stake[i].mulWad(proportion) - : stake[0].mulWad(proportion), + amount: stake[i].mulWad(proportion), token: loan.debt[i].token, recipient: receiver }); diff --git a/test/StarPortTest.sol b/test/StarPortTest.sol index 351adc2d..55bc52fb 100644 --- a/test/StarPortTest.sol +++ b/test/StarPortTest.sol @@ -498,16 +498,9 @@ contract StarPortTest is BaseOrderTest { ) ); OfferItem[] memory offer = new OfferItem[](nlr.debt.length + 1); - offer[0] = OfferItem({ - itemType: ItemType.ERC721, - token: address(LM), - identifierOrCriteria: uint256(caveatHash), - startAmount: 1, - endAmount: 1 - }); - uint256 i = 0; - for (; i < debt.length; ) { - offer[i + 1] = OfferItem({ + + for (uint256 i; i < debt.length; ) { + offer[i] = OfferItem({ itemType: debt[i].itemType, token: debt[i].token, identifierOrCriteria: debt[i].identifier, @@ -519,6 +512,14 @@ contract StarPortTest is BaseOrderTest { } } + offer[nlr.debt.length] = OfferItem({ + itemType: ItemType.ERC721, + token: address(LM), + identifierOrCriteria: uint256(caveatHash), + startAmount: 1, + endAmount: 1 + }); + OfferItem[] memory zOffer = new OfferItem[](1); zOffer[0] = OfferItem({ itemType: nlr.debt[0].itemType, @@ -578,7 +579,7 @@ contract StarPortTest is BaseOrderTest { fill[0].offerComponents[0] = FulfillmentComponent({ orderIndex: 1, - itemIndex: 1 + itemIndex: 0 }); fill[0].considerationComponents[0] = FulfillmentComponent({ orderIndex: 0, @@ -593,6 +594,7 @@ contract StarPortTest is BaseOrderTest { orderIndex: 2, itemIndex: 0 }); + fill[1].considerationComponents[0] = FulfillmentComponent({ orderIndex: 0, itemIndex: 0 @@ -607,10 +609,12 @@ contract StarPortTest is BaseOrderTest { orderIndex: 0, itemIndex: 0 }); + fill[2].considerationComponents[0] = FulfillmentComponent({ orderIndex: 1, itemIndex: 0 }); + fill[3] = Fulfillment({ offerComponents: new FulfillmentComponent[](1), considerationComponents: new FulfillmentComponent[](1) @@ -618,8 +622,9 @@ contract StarPortTest is BaseOrderTest { fill[3].offerComponents[0] = FulfillmentComponent({ orderIndex: 1, - itemIndex: 0 + itemIndex: 1 }); + fill[3].considerationComponents[0] = FulfillmentComponent({ orderIndex: 2, itemIndex: 0 @@ -661,16 +666,9 @@ contract StarPortTest is BaseOrderTest { ) ); OfferItem[] memory offer = new OfferItem[](nlr.debt.length + 1); - offer[0] = OfferItem({ - itemType: ItemType.ERC721, - token: address(LM), - identifierOrCriteria: uint256(caveatHash), - startAmount: 1, - endAmount: 1 - }); - uint256 i = 0; - for (; i < debt.length; ) { - offer[i + 1] = OfferItem({ + + for (uint i; i < debt.length; ) { + offer[i] = OfferItem({ itemType: debt[i].itemType, token: debt[i].token, identifierOrCriteria: debt[i].identifier, @@ -681,6 +679,15 @@ contract StarPortTest is BaseOrderTest { ++i; } } + + offer[nlr.debt.length] = OfferItem({ + itemType: ItemType.ERC721, + token: address(LM), + identifierOrCriteria: uint256(caveatHash), + startAmount: 1, + endAmount: 1 + }); + OrderParameters memory op = _buildContractOrder( address(LM), nlr.caveats.length == 0 ? new OfferItem[](0) : offer, diff --git a/test/utils/Assertions.sol b/test/utils/Assertions.sol new file mode 100644 index 00000000..a111eafc --- /dev/null +++ b/test/utils/Assertions.sol @@ -0,0 +1,25 @@ + +//Assert balances are correct after new loan is created +modifier newLoanAssertions() { + + uint256 borrowerBalance; + + _; + + //Assert updated balances are correct + +} + + +//Assert balances are correct after loan is repaid +modifier repayAssertions() { + + uint256 borrowerBalance; + + _; + + //Assert updated balances are correct + +} + + diff --git a/test/utils/Bound.sol b/test/utils/Bound.sol new file mode 100644 index 00000000..1c5d39d5 --- /dev/null +++ b/test/utils/Bound.sol @@ -0,0 +1,64 @@ +pragma solidity =0.8.17; + +import { + ItemType, + SpentItem, + ReceivedItem +} from "seaport-types/src/lib/ConsiderationStructs.sol"; +import {Cast} from "test/utils/Cast.sol"; +import "test/utils/FuzzStructs.sol" as Fuzz; +import "forge-std/Test.sol"; + +abstract contract Bound is StdUtils { + using Cast for *; + + function _boundItemType(uint8 itemType) internal pure returns (ItemType) { + return + _bound( + itemType, + uint8(ItemType.NATIVE), + uint8(ItemType.ERC1155_WITH_CRITERIA) + ).toItemType(); + } + + function _boundSpentItem( + Fuzz.SpentItem memory input + ) internal pure returns (SpentItem memory ret) { + ret = SpentItem({ + itemType: _boundItemType(input.itemType), + token: input.token, + identifier: input.identifier, + amount: input.amount + }); + } + + function _boundSpentItems( + Fuzz.SpentItem[] memory input + ) internal pure returns (SpentItem[] memory ret) { + ret = new SpentItem[](input.length); + for (uint256 i = 0; i < input.length; i++) { + ret[i] = _boundSpentItem(input[i]); + } + } + + function _boundReceivedItem( + Fuzz.ReceivedItem memory input + ) internal pure returns (ReceivedItem memory ret) { + ret = ReceivedItem({ + itemType: _boundItemType(input.itemType), + token: input.token, + identifier: input.identifier, + amount: input.amount, + recipient: input.recipient + }); + } + + function _boundReceivedItems( + Fuzz.ReceivedItem[] memory input + ) internal pure returns (ReceivedItem[] memory ret) { + ret = new ReceivedItem[](input.length); + for (uint256 i = 0; i < input.length; i++) { + ret[i] = _boundReceivedItem(input[i]); + } + } +} diff --git a/test/utils/Cast.sol b/test/utils/Cast.sol new file mode 100644 index 00000000..cbd67fec --- /dev/null +++ b/test/utils/Cast.sol @@ -0,0 +1,29 @@ +pragma solidity =0.8.17; + +import { + ItemType, + SpentItem, + ReceivedItem +} from "seaport-types/src/lib/ConsiderationStructs.sol"; +import "test/utils/FuzzStructs.sol" as Fuzz; +import "forge-std/Test.sol"; + +library Cast { + function toUint(uint8 input) internal pure returns (uint256 ret) { + assembly { + ret := input + } + } + + function toUint(address input) internal pure returns (uint256 ret) { + assembly { + ret := input + } + } + + function toItemType(uint256 input) internal pure returns (ItemType ret) { + assembly { + ret := input + } + } +} diff --git a/test/utils/DeepEq.sol b/test/utils/DeepEq.sol new file mode 100644 index 00000000..97e910b8 --- /dev/null +++ b/test/utils/DeepEq.sol @@ -0,0 +1,34 @@ +import { + ItemType, + SpentItem, + ReceivedItem +} from "seaport-types/src/lib/ConsiderationStructs.sol"; +import {Cast} from "test/utils/Cast.sol"; +import "test/utils/FuzzStructs.sol" as Fuzz; +import "forge-std/Test.sol"; + +abstract contract DeepEq { + function _deepEq( + ReceivedItem[] memory a, + ReceivedItem[] memory b + ) internal pure { + assert(a.length == b.length); + for (uint256 i = 0; i < a.length; i++) { + assert(a[i].itemType == b[i].itemType); + assert(a[i].token == b[i].token); + assert(a[i].identifier == b[i].identifier); + assert(a[i].amount == b[i].amount); + assert(a[i].recipient == b[i].recipient); + } + } + + function _deepEq(SpentItem[] memory a, SpentItem[] memory b) internal pure { + assert(a.length == b.length); + for (uint256 i = 0; i < a.length; i++) { + assert(a[i].itemType == b[i].itemType); + assert(a[i].token == b[i].token); + assert(a[i].identifier == b[i].identifier); + assert(a[i].amount == b[i].amount); + } + } +} diff --git a/test/utils/Events.sol b/test/utils/Events.sol new file mode 100644 index 00000000..276cc541 --- /dev/null +++ b/test/utils/Events.sol @@ -0,0 +1,8 @@ + + bytes32 lienOpenTopic = bytes32(0x57cb72d73c48fadf55428537f6c9efbe080ae111339b0c5af42d9027ed20ba17); + for (uint256 i = 0; i < logs.length; i++) { + if (logs[i].topics[0] == lienOpenTopic) { + (loanId, loan) = abi.decode(logs[i].data, (uint256, LoanManager.Loan)); + break; + } + } diff --git a/test/utils/Expect.sol b/test/utils/Expect.sol new file mode 100644 index 00000000..c5beaf45 --- /dev/null +++ b/test/utils/Expect.sol @@ -0,0 +1,34 @@ +import { + ItemType, + SpentItem, + ReceivedItem +} from "seaport-types/src/lib/ConsiderationStructs.sol"; +import "forge-std/Test.sol"; + +abstract contract Expect is Test { + ItemType MAX_ITEM_TYPE = ItemType.ERC1155_WITH_CRITERIA; + + function _expectRevert(SpentItem[] calldata items) internal { + bool expectRevert; + ItemType max = type(ItemType).max; + assembly { + let e := add(items.offset, mul(items.length, 0x80)) + + for { + let i := items.offset + } lt(i, e) { + i := add(i, 0x80) + } { + let item := calldataload(i) + if gt(item, max) { + expectRevert := 1 + break + } + } + } + + if (expectRevert) { + vm.expectRevert(); + } + } +} diff --git a/test/utils/FuzzStructs.sol b/test/utils/FuzzStructs.sol new file mode 100644 index 00000000..f72cc0c6 --- /dev/null +++ b/test/utils/FuzzStructs.sol @@ -0,0 +1,16 @@ +pragma solidity =0.8.17; + +struct ReceivedItem { + uint8 itemType; + address token; + uint256 identifier; + uint256 amount; + address payable recipient; +} + +struct SpentItem { + uint8 itemType; + address token; + uint256 identifier; + uint256 amount; +}