diff --git a/.gas-snapshot b/.gas-snapshot index 670716fe..f5f74ed6 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,66 +1,103 @@ -EnforcerTest:testCollateralEnforcer() (gas: 950836) -EnforcerTest:testFailCollateralEnforcerDifferentCollateral() (gas: 832965) -EnforcerTest:testFailRateEnforcerMaxCarryRate() (gas: 788105) -EnforcerTest:testFailRateEnforcerMaxRate() (gas: 788077) -EnforcerTest:testFailRateEnforcerMaxRateAndMaxCarryRate() (gas: 787942) -EnforcerTest:testRateEnforcerBasic() (gas: 908012) -EnforcerTest:testTermEnforcerBasic() (gas: 980340) -TestAstariaV1Loan:testNewLoanERC721CollateralDefaultTermsRecall() (gas: 969012) -TestAstariaV1Loan:testNewLoanERC721CollateralDefaultTermsRecallLender() (gas: 859410) -TestAstariaV1Loan:testNewLoanERC721CollateralDefaultTermsRecallLiquidation() (gas: 906274) -TestCustodian:testCannotLazyMintTwice() (gas: 76591) -TestCustodian:testCannotMintInvalidLoanInvalidCustodian() (gas: 66811) -TestCustodian:testCannotMintInvalidLoanValidCustodian() (gas: 72394) -TestCustodian:testCustodySelector() (gas: 2790258) -TestCustodian:testDefaultCustodySelectorRevert() (gas: 11673) -TestCustodian:testGenerateOrderInvalidHandlerExecution() (gas: 147388) -TestCustodian:testGenerateOrderRepay() (gas: 189283) -TestCustodian:testGenerateOrderRepayAsRepayApprovedBorrower() (gas: 214857) -TestCustodian:testGenerateOrderRepayERC1155AndERC20AndNative() (gas: 1130044) -TestCustodian:testGenerateOrderRepayERC1155AndERC20AndNativeHandlerAuthorized() (gas: 1029768) -TestCustodian:testGenerateOrderRepayERC1155WithRevert() (gas: 530761) -TestCustodian:testGenerateOrderRepayInvalidHookAddress() (gas: 104841) -TestCustodian:testGenerateOrderRepayInvalidHookReturnType() (gas: 99244) -TestCustodian:testGenerateOrderRepayNotBorrower() (gas: 110879) -TestCustodian:testGenerateOrderSettlement() (gas: 164511) -TestCustodian:testGenerateOrderSettlementHandlerAuthorized() (gas: 173617) -TestCustodian:testGenerateOrderSettlementNoActiveLoan() (gas: 107403) -TestCustodian:testGenerateOrderSettlementUnauthorized() (gas: 108744) -TestCustodian:testGetBorrower() (gas: 76099) +EnforcerTest:testCollateralEnforcer() (gas: 954262) +EnforcerTest:testFailCollateralEnforcerDifferentCollateral() (gas: 834281) +EnforcerTest:testFailRateEnforcerMaxCarryRate() (gas: 789417) +EnforcerTest:testFailRateEnforcerMaxRate() (gas: 789389) +EnforcerTest:testFailRateEnforcerMaxRateAndMaxCarryRate() (gas: 789254) +EnforcerTest:testRateEnforcerBasic() (gas: 911435) +EnforcerTest:testTermEnforcerBasic() (gas: 983763) +TestAstariaV1Loan:testNewLoanERC721CollateralDefaultTermsRecallBase() (gas: 1216423) +TestAstariaV1Loan:testNewLoanERC721CollateralDefaultTermsRecallLender() (gas: 858666) +TestAstariaV1Loan:testNewLoanERC721CollateralDefaultTermsRecallLiquidation() (gas: 894915) +TestCustodian:testCannotLazyMintTwice() (gas: 76663) +TestCustodian:testCannotMintInvalidLoanInvalidCustodian() (gas: 66883) +TestCustodian:testCannotMintInvalidLoanValidCustodian() (gas: 72400) +TestCustodian:testCustodySelector() (gas: 2766980) +TestCustodian:testDefaultCustodySelectorRevert() (gas: 11672) +TestCustodian:testGenerateOrderInvalidHandlerExecution() (gas: 139413) +TestCustodian:testGenerateOrderRepay() (gas: 179743) +TestCustodian:testGenerateOrderRepayAsRepayApprovedBorrower() (gas: 205294) +TestCustodian:testGenerateOrderRepayERC1155AndERC20AndNative() (gas: 1117448) +TestCustodian:testGenerateOrderRepayERC1155AndERC20AndNativeHandlerAuthorized() (gas: 1031307) +TestCustodian:testGenerateOrderRepayERC1155WithRevert() (gas: 525098) +TestCustodian:testGenerateOrderRepayInvalidHookAddress() (gas: 96770) +TestCustodian:testGenerateOrderRepayInvalidHookReturnType() (gas: 91174) +TestCustodian:testGenerateOrderRepayNotBorrower() (gas: 102896) +TestCustodian:testGenerateOrderSettlement() (gas: 161026) +TestCustodian:testGenerateOrderSettlementHandlerAuthorized() (gas: 170156) +TestCustodian:testGenerateOrderSettlementNoActiveLoan() (gas: 162510) +TestCustodian:testGenerateOrderSettlementUnauthorized() (gas: 100813) +TestCustodian:testGetBorrower() (gas: 76114) +TestCustodian:testInvalidAction() (gas: 124444) +TestCustodian:testInvalidActionRepayInActiveLoan() (gas: 124454) +TestCustodian:testInvalidActionSettleActiveLoan() (gas: 124457) TestCustodian:testName() (gas: 7098) -TestCustodian:testNonPayableFunctions() (gas: 247767) -TestCustodian:testOnlySeaport() (gas: 17931) +TestCustodian:testNonPayableFunctions() (gas: 219009) +TestCustodian:testOnlySeaport() (gas: 17975) TestCustodian:testPayableFunctions() (gas: 43468) -TestCustodian:testPreviewOrderNoActiveLoan() (gas: 104915) -TestCustodian:testPreviewOrderRepay() (gas: 243798) -TestCustodian:testPreviewOrderSettlement() (gas: 195979) -TestCustodian:testPreviewOrderSettlementInvalidFufliller() (gas: 106672) -TestCustodian:testPreviewOrderSettlementInvalidRepayer() (gas: 112759) -TestCustodian:testRatifyOrder() (gas: 195401) -TestCustodian:testSafeTransferReceive() (gas: 159065) -TestCustodian:testSeaportMetadata() (gas: 8501) -TestCustodian:testSetRepayApproval() (gas: 37907) -TestCustodian:testSupportsInterface() (gas: 9428) -TestCustodian:testSymbol() (gas: 7105) -TestCustodian:testTokenURI() (gas: 64811) +TestCustodian:testPreviewOrderNoActiveLoan() (gas: 105228) +TestCustodian:testPreviewOrderRepay() (gas: 230735) +TestCustodian:testPreviewOrderSettlement() (gas: 192962) +TestCustodian:testPreviewOrderSettlementInvalidFufliller() (gas: 107146) +TestCustodian:testPreviewOrderSettlementInvalidRepayer() (gas: 113233) +TestCustodian:testRatifyOrder() (gas: 185928) +TestCustodian:testSafeTransfer1155Receive() (gas: 122944) +TestCustodian:testSeaportMetadata() (gas: 8589) +TestCustodian:testSetRepayApproval() (gas: 37839) +TestCustodian:testSupportsInterface() (gas: 9406) +TestCustodian:testSymbol() (gas: 7171) +TestCustodian:testTokenURI() (gas: 64839) TestCustodian:testTokenURIInvalidLoan() (gas: 13196) -TestExoticLoans:testSwap() (gas: 1353956) +TestExoticLoans:testSwap() (gas: 1354115) TestLoanCombinations:testLoan20For721SimpleInterestDutchFixedRepay() (gas: 164) -TestLoanCombinations:testLoan20for20SimpleInterestDutchFixedRepay() (gas: 539940) -TestLoanCombinations:testLoan721for20SimpleInterestDutchFixedRepay() (gas: 756253) -TestLoanCombinations:testLoanAstariaSettlementRepay() (gas: 575128) -TestLoanCombinations:testLoanSimpleInterestEnglishFixed() (gas: 747899) -TestLoanManager:testGenerateOrder() (gas: 1005642) -TestLoanManager:testGenerateOrderNotSeaport() (gas: 13074) -TestLoanManager:testName() (gas: 7206) -TestLoanManager:testSupportsInterface() (gas: 9579) -TestLoanManager:testSymbol() (gas: 7148) -TestNewLoan:testBuyNowPayLater() (gas: 1148671) -TestNewLoan:testNewLoanERC721CollateralDefaultTerms2():((uint256,address,address,address,address,(uint8,address,uint256,uint256)[],(uint8,address,uint256,uint256)[],(address,bytes,address,bytes,address,bytes))) (gas: 979121) -TestNewLoan:testNewLoanERC721CollateralDefaultTermsRefinance() (gas: 639892) -TestNewLoan:testNewLoanERC721CollateralLessDebtThanOffered():((uint256,address,address,address,address,(uint8,address,uint256,uint256)[],(uint8,address,uint256,uint256)[],(address,bytes,address,bytes,address,bytes))) (gas: 979455) -TestNewLoan:testSettleLoan() (gas: 1233583) -TestRepayLoan:testRepayLoan() (gas: 694754) +TestLoanCombinations:testLoan20for20SimpleInterestDutchFixedRepay() (gas: 518369) +TestLoanCombinations:testLoan721for20SimpleInterestDutchFixedRepay() (gas: 713011) +TestLoanCombinations:testLoanAstariaSettlementRepay() (gas: 542452) +TestLoanCombinations:testLoanSimpleInterestEnglishFixed() (gas: 702511) +TestLoanManager:testCannotIssueSameLoanTwice() (gas: 1529558) +TestLoanManager:testCannotSettleInvalidLoan() (gas: 72844) +TestLoanManager:testCannotSettleUnlessValidCustodian() (gas: 68990) +TestLoanManager:testCaveatEnforcerInvalidOrigination() (gas: 1796959) +TestLoanManager:testDefaultFeeRake() (gas: 404134) +TestLoanManager:testExoticDebtWithNoCaveatsAsBorrower() (gas: 1587200) +TestLoanManager:testExoticDebtWithNoCaveatsNotAsBorrower() (gas: 1677110) +TestLoanManager:testGenerateOrder() (gas: 1494995) +TestLoanManager:testGenerateOrderInvalidAction() (gas: 1354720) +TestLoanManager:testGenerateOrderNotSeaport() (gas: 13045) +TestLoanManager:testInitializedFlagSetProperly() (gas: 65216) +TestLoanManager:testInvalidDebtLength() (gas: 37065) +TestLoanManager:testInvalidDebtType() (gas: 1362217) +TestLoanManager:testInvalidMaximumSpentEmpty() (gas: 48076) +TestLoanManager:testIssued() (gas: 67041) +TestLoanManager:testName() (gas: 7209) +TestLoanManager:testNativeDebtWithNoCaveatsAsBorrower() (gas: 1500521) +TestLoanManager:testNativeDebtWithNoCaveatsNotAsBorrower() (gas: 1541448) +TestLoanManager:testNativeDebtWithNoCaveatsNotAsBorrowerFeesOn() (gas: 1604818) +TestLoanManager:testNonDefaultCustodianCustodyCallFails() (gas: 23953) +TestLoanManager:testNonDefaultCustodianCustodyCallSuccess() (gas: 25349) +TestLoanManager:testNonPayableFunctions() (gas: 109562) +TestLoanManager:testOverrideFeeRake() (gas: 405450) +TestLoanManager:testPayableFunctions() (gas: 60281) +TestLoanManager:testPreviewOrderInvalidAction() (gas: 1299017) +TestLoanManager:testPreviewOrderOriginationWithNoCaveatsSetAsBorrowerFeeOn() (gas: 1345487) +TestLoanManager:testPreviewOrderOriginationWithNoCaveatsSetAsBorrowerNoFee() (gas: 1313690) +TestLoanManager:testPreviewOrderOriginationWithNoCaveatsSetNotBorrowerFeeOn() (gas: 1354195) +TestLoanManager:testPreviewOrderOriginationWithNoCaveatsSetNotBorrowerNoFee() (gas: 1322064) +TestLoanManager:testPreviewOrderRefinanceAsRefinancerFeeOff() (gas: 1373981) +TestLoanManager:testPreviewOrderRefinanceAsRefinancerFeeOn() (gas: 1399308) +TestLoanManager:testRefinanceNoRefinanceConsideration() (gas: 1386397) +TestLoanManager:testSafeTransfer1155Receive() (gas: 54356) +TestLoanManager:testSeaportMetadata() (gas: 8667) +TestLoanManager:testSupportsInterface() (gas: 9602) +TestLoanManager:testSymbol() (gas: 7238) +TestLoanManager:testTokenURI() (gas: 64967) +TestLoanManager:testTokenURIInvalidLoan() (gas: 13312) +TestLoanManager:testTransferFromFailFromSeaport() (gas: 82187) +TestNewLoan:testBuyNowPayLater() (gas: 1150283) +TestNewLoan:testNewLoanERC721CollateralDefaultTerms2():((uint256,address,address,address,address,(uint8,address,uint256,uint256)[],(uint8,address,uint256,uint256)[],(address,bytes,address,bytes,address,bytes))) (gas: 982753) +TestNewLoan:testNewLoanERC721CollateralLessDebtThanOffered():((uint256,address,address,address,address,(uint8,address,uint256,uint256)[],(uint8,address,uint256,uint256)[],(address,bytes,address,bytes,address,bytes))) (gas: 983087) +TestNewLoan:testNewLoanRefinanceNew() (gas: 697393) +TestNewLoan:testSettleLoan() (gas: 1223956) +TestRepayLoan:testRepayLoan() (gas: 684213) TestStarLiteUtils:testEncodeReceivedWithRecipient() (gas: 17955) TestStarLiteUtils:testSpentToReceived() (gas: 17708) TestStarLiteUtils:testValidateSaltOpt(address,bytes32) (runs: 256, μ: 26479, ~: 26479) diff --git a/src/Custodian.sol b/src/Custodian.sol index d4b553d5..0bde54d5 100644 --- a/src/Custodian.sol +++ b/src/Custodian.sol @@ -28,16 +28,15 @@ import {ItemType, Schema, SpentItem, ReceivedItem} from "seaport-types/src/lib/C import {ContractOffererInterface} from "seaport-types/src/interfaces/ContractOffererInterface.sol"; import {ConduitHelper} from "starport-core/ConduitHelper.sol"; -import {TokenReceiverInterface} from "starport-core/interfaces/TokenReceiverInterface.sol"; import {FixedPointMathLib} from "solady/src/utils/FixedPointMathLib.sol"; import {Originator} from "starport-core/originators/Originator.sol"; import {SettlementHook} from "starport-core/hooks/SettlementHook.sol"; import {SettlementHandler} from "starport-core/handlers/SettlementHandler.sol"; import {Pricing} from "starport-core/pricing/Pricing.sol"; import {LoanManager} from "starport-core/LoanManager.sol"; -import {StarPortLib} from "starport-core/lib/StarPortLib.sol"; +import {StarPortLib, Actions} from "starport-core/lib/StarPortLib.sol"; -contract Custodian is ERC721, ContractOffererInterface, ConduitHelper, TokenReceiverInterface { +contract Custodian is ERC721, ContractOffererInterface, ConduitHelper { using {StarPortLib.getId} for LoanManager.Loan; LoanManager public immutable LM; @@ -48,13 +47,14 @@ contract Custodian is ERC721, ContractOffererInterface, ConduitHelper, TokenRece event RepayApproval(address borrower, address repayer, bool approved); event SeaportCompatibleContractDeployed(); - error NotSeaport(); - error NotLoanManager(); - error InvalidRepayer(); + error ImplementInChild(); + error InvalidAction(); error InvalidFulfiller(); error InvalidHandlerExecution(); error InvalidLoan(); - error ImplementInChild(); + error InvalidRepayer(); + error NotSeaport(); + error NotLoanManager(); constructor(LoanManager LM_, address seaport_) { seaport = seaport_; @@ -202,12 +202,13 @@ contract Custodian is ERC721, ContractOffererInterface, ConduitHelper, TokenRece SpentItem[] calldata maximumSpent, bytes calldata context // encoded based on the schemaID ) public view returns (SpentItem[] memory offer, ReceivedItem[] memory consideration) { - LoanManager.Loan memory loan = abi.decode(context, (LoanManager.Loan)); + (Actions action, LoanManager.Loan memory loan) = abi.decode(context, (Actions, LoanManager.Loan)); if (!LM.issued(loan.getId())) { revert InvalidLoan(); } - if (SettlementHook(loan.terms.hook).isActive(loan)) { + bool loanActive = SettlementHook(loan.terms.hook).isActive(loan); + if (action == Actions.Repayment && loanActive) { address borrower = getBorrower(loan); if (fulfiller != borrower && !repayApproval[borrower][fulfiller]) { revert InvalidRepayer(); @@ -219,7 +220,7 @@ contract Custodian is ERC721, ContractOffererInterface, ConduitHelper, TokenRece consideration = _mergeConsiderations(paymentConsiderations, carryFeeConsideration, new ReceivedItem[](0)); consideration = _removeZeroAmounts(consideration); - } else { + } else if (action == Actions.Settlement && !loanActive) { address authorized; (consideration, authorized) = SettlementHandler(loan.terms.handler).getSettlement(loan); @@ -228,34 +229,19 @@ contract Custodian is ERC721, ContractOffererInterface, ConduitHelper, TokenRece } else if (authorized == loan.terms.handler || authorized == loan.issuer) {} else { revert InvalidFulfiller(); } + } else { + revert InvalidAction(); } } - function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data) - public - pure - virtual - returns (bytes4) - { - return TokenReceiverInterface.onERC721Received.selector; - } - + //seaport doesn't call safe transfer on anything but 1155 and never batch function onERC1155Received(address, address, uint256, uint256, bytes calldata) public pure virtual returns (bytes4) { - return TokenReceiverInterface.onERC1155Received.selector; - } - - function onERC1155BatchReceived(address, address, uint256[] calldata, uint256[] calldata, bytes calldata) - public - pure - virtual - returns (bytes4) - { - return TokenReceiverInterface.onERC1155BatchReceived.selector; + return this.onERC1155Received.selector; } //INTERNAL FUNCTIONS @@ -264,11 +250,10 @@ contract Custodian is ERC721, ContractOffererInterface, ConduitHelper, TokenRece internal returns (SpentItem[] memory offer, ReceivedItem[] memory consideration) { - LoanManager.Loan memory loan = abi.decode(context, (LoanManager.Loan)); - if (!LM.issued(loan.getId())) { - revert InvalidLoan(); - } - if (SettlementHook(loan.terms.hook).isActive(loan)) { + (Actions action, LoanManager.Loan memory loan) = abi.decode(context, (Actions, LoanManager.Loan)); + + bool loanActive = SettlementHook(loan.terms.hook).isActive(loan); + if (action == Actions.Repayment && loanActive) { address borrower = getBorrower(loan); if (fulfiller != borrower && !repayApproval[borrower][fulfiller]) { revert InvalidRepayer(); @@ -285,7 +270,7 @@ contract Custodian is ERC721, ContractOffererInterface, ConduitHelper, TokenRece consideration = _removeZeroAmounts(consideration); _settleLoan(loan); - } else { + } else if (action == Actions.Settlement && !loanActive) { address authorized; //add in originator fee _beforeSettlementHandlerHook(loan); @@ -310,6 +295,8 @@ contract Custodian is ERC721, ContractOffererInterface, ConduitHelper, TokenRece } _settleLoan(loan); + } else { + revert InvalidAction(); } } diff --git a/src/LoanManager.sol b/src/LoanManager.sol index 98566cdd..80b46a10 100644 --- a/src/LoanManager.sol +++ b/src/LoanManager.sol @@ -33,7 +33,7 @@ import {SettlementHook} from "starport-core/hooks/SettlementHook.sol"; import {SettlementHandler} from "starport-core/handlers/SettlementHandler.sol"; import {Pricing} from "starport-core/pricing/Pricing.sol"; -import {StarPortLib} from "starport-core/lib/StarPortLib.sol"; +import {StarPortLib, Actions} from "starport-core/lib/StarPortLib.sol"; import {ConduitTransfer, ConduitItemType} from "seaport-types/src/conduit/lib/ConduitStructs.sol"; import {ConduitControllerInterface} from "seaport-types/src/interfaces/ConduitControllerInterface.sol"; import {ConduitInterface} from "seaport-types/src/interfaces/ConduitInterface.sol"; @@ -43,36 +43,40 @@ import {SignatureCheckerLib} from "solady/src/utils/SignatureCheckerLib.sol"; import {CaveatEnforcer} from "starport-core/enforcers/CaveatEnforcer.sol"; import {Ownable} from "solady/src/auth/Ownable.sol"; import {ConduitHelper} from "starport-core/ConduitHelper.sol"; +import "forge-std/console.sol"; interface LoanSettledCallback { function onLoanSettled(LoanManager.Loan calldata loan) external; } -contract LoanManager is ContractOffererInterface, ConduitHelper, Ownable, ERC721 { +contract LoanManager is ConduitHelper, Ownable, ERC721 { using FixedPointMathLib for uint256; using {StarPortLib.toReceivedItems} for SpentItem[]; using {StarPortLib.getId} for LoanManager.Loan; using {StarPortLib.validateSalt} for mapping(address => mapping(bytes32 => bool)); + bytes32 internal immutable _DOMAIN_SEPARATOR; + ConsiderationInterface public immutable seaport; + // bool public paused; //TODO: + address payable public immutable defaultCustodian; bytes32 public immutable DEFAULT_CUSTODIAN_CODE_HASH; - bytes32 internal immutable _DOMAIN_SEPARATOR; // Define the EIP712 domain and typehash constants for generating signatures - bytes32 constant EIP_DOMAIN = keccak256("EIP712Domain(string version,uint256 chainId,address verifyingContract)"); + bytes32 public constant EIP_DOMAIN = + keccak256("EIP712Domain(string version,uint256 chainId,address verifyingContract)"); bytes32 public constant INTENT_ORIGINATION_TYPEHASH = keccak256("IntentOrigination(bytes32 hash,bytes32 salt,uint256 nonce)"); - bytes32 constant VERSION = keccak256("0"); - + bytes32 public constant VERSION = keccak256("0"); + address public feeTo; + uint96 public defaultFeeRake; mapping(address => mapping(bytes32 => bool)) public usedSalts; mapping(address => uint256) public borrowerNonce; //needs to be invalidated - address public feeTo; - uint96 public defaultFeeRake; //contract to token //fee rake - mapping(address => Fee) public exoticFee; + mapping(address => Fee) public feeOverride; enum FieldFlags { INITIALIZED, @@ -117,9 +121,8 @@ contract LoanManager is ContractOffererInterface, ConduitHelper, Ownable, ERC721 } struct Fee { - ItemType itemType; - address token; - uint88 rake; + bool enabled; + uint96 amount; } event Close(uint256 loanId); @@ -128,16 +131,21 @@ contract LoanManager is ContractOffererInterface, ConduitHelper, Ownable, ERC721 error CannotTransferLoans(); error ConduitTransferError(); + error InvalidAction(); error InvalidConduit(); error InvalidRefinance(); - error NotSeaport(); - error NotLoanCustodian(); - error InvalidAction(); - error InvalidLoan(uint256); + error InvalidCustodian(); + error InvalidLoan(); error InvalidMaximumSpentEmpty(); - error InvalidDebt(); + error InvalidDebtLength(); + error InvalidDebtType(); error InvalidOrigination(); error InvalidNoRefinanceConsideration(); + error LoanExists(); + error NotLoanCustodian(); + error NotPayingFees(); + error NotSeaport(); + error NotEnteredViaSeaport(); constructor(ConsiderationInterface seaport_) { seaport = seaport_; @@ -154,7 +162,14 @@ contract LoanManager is ContractOffererInterface, ConduitHelper, Ownable, ERC721 emit SeaportCompatibleContractDeployed(); } - // Encode the data with the account's nonce for generating a signature + /** + * @dev previews the order for this contract offerer. + * + * @param borrower The address of the borrower + * @param salt The salt of the borrower's obligation + * @param caveatHash The hash of the abi.encoded obligation caveats + * @return The abi encode packed bytes that include the intent typehash with the salt and nonce and caveatHash + */ function encodeWithSaltAndBorrowerCounter(address borrower, bytes32 salt, bytes32 caveatHash) public view @@ -169,15 +184,25 @@ contract LoanManager is ContractOffererInterface, ConduitHelper, Ownable, ERC721 ); } + /** + * @dev the erc721 name of the contract + * @return The name of the contract as a string + */ function name() public pure override returns (string memory) { return "Starport Loan Manager"; } + /** + * @dev the erc721 symbol of the contract + * @return The symbol of the contract as a string + */ function symbol() public pure override returns (string memory) { return "SLM"; } - // MODIFIERS + /** + * @dev modifier to check if the caller is seaport + */ modifier onlySeaport() { if (msg.sender != address(seaport)) { revert NotSeaport(); @@ -185,39 +210,63 @@ contract LoanManager is ContractOffererInterface, ConduitHelper, Ownable, ERC721 _; } + /** + * @dev helper to check if a loan is active + * @param loanId The id of the loan + * @return True if the loan is active + */ function active(uint256 loanId) public view returns (bool) { return _getExtraData(loanId) == uint8(FieldFlags.ACTIVE); } + /** + * @dev helper to check if a loan is inactive + * @param loanId The id of the loan + * @return True if the loan is inactive + */ function inactive(uint256 loanId) public view returns (bool) { return _getExtraData(loanId) == uint8(FieldFlags.INACTIVE); } + /** + * @dev helper to check if a loan is initialized(ie. has never been opened) + * @param loanId The id of the loan + * @return True if the loan is initialized + */ function initialized(uint256 loanId) public view returns (bool) { return _getExtraData(loanId) == uint8(FieldFlags.INITIALIZED); } - function tokenURI(uint256 tokenId) public view override returns (string memory) { - if (!_exists(tokenId)) { - revert InvalidLoan(tokenId); + /** + * @dev erc721 tokenURI override + * @param loanId The id of the loan + * @return the string uri of the loan + */ + function tokenURI(uint256 loanId) public view override returns (string memory) { + if (!_issued(loanId)) { + revert InvalidLoan(); } return string(""); } - function _issued(uint256 tokenId) internal view returns (bool) { - return (_getExtraData(tokenId) > uint8(0)); - } - - function issued(uint256 tokenId) external view returns (bool) { - return _issued(tokenId); + function _issued(uint256 loanId) internal view returns (bool) { + return (_getExtraData(loanId) > uint8(0)); } - //break the revert of the ownerOf method, so we can ensure anyone calling it in the settlement pipeline wont halt - function ownerOf(uint256 loanId) public view override returns (address) { - //not hasn't been issued but exists if we own it - return _issued(loanId) && !_exists(loanId) ? address(this) : _ownerOf(loanId); + /** + * @dev helper to check if a loan was issued ever(getExtraData > 0) + * @param loanId The id of the loan + * @return True if the loan is initialized + */ + function issued(uint256 loanId) external view returns (bool) { + return _issued(loanId); } + /** + * @dev helper to check if a loan is initialized(ie. has never been opened) + * guarded to ensure only the loan.custodian can call it + * @param loan The entire loan struct + */ function settle(Loan memory loan) external { if (msg.sender != loan.custodian) { revert NotLoanCustodian(); @@ -228,7 +277,7 @@ contract LoanManager is ContractOffererInterface, ConduitHelper, Ownable, ERC721 function _settle(Loan memory loan) internal { uint256 tokenId = loan.getId(); if (!_issued(tokenId)) { - revert InvalidLoan(tokenId); + revert InvalidLoan(); } if (_exists(tokenId)) { _burn(tokenId); @@ -241,29 +290,32 @@ contract LoanManager is ContractOffererInterface, ConduitHelper, Ownable, ERC721 emit Close(tokenId); } + /** + * @dev internal method to call the custody selector of the custodian if it does not share + * the same codehash as the default custodian + * @param consideration the receivedItems[] + * @param orderHashes the order hashes of the seaport txn + * @param contractNonce the nonce of the current contract offerer + * @param context the abi encoded bytes data of the order + */ function _callCustody( ReceivedItem[] calldata consideration, bytes32[] calldata orderHashes, uint256 contractNonce, bytes calldata context - ) internal returns (bytes4 selector) { - address custodian; - - assembly { - custodian := calldataload(add(context.offset, 0x20)) // 0x20 offset for the first address 'custodian' - } + ) internal { + address custodian = StarPortLib.getCustodian(context); // Comparing the retrieved code hash with a known hash bytes32 codeHash; assembly { codeHash := extcodehash(custodian) } - if (codeHash != DEFAULT_CUSTODIAN_CODE_HASH) { - if ( - Custodian(payable(custodian)).custody(consideration, orderHashes, contractNonce, context) + if ( + codeHash != DEFAULT_CUSTODIAN_CODE_HASH + && Custodian(payable(custodian)).custody(consideration, orderHashes, contractNonce, context) != Custodian.custody.selector - ) { - revert InvalidAction(); - } + ) { + revert InvalidCustodian(); } } @@ -284,43 +336,113 @@ contract LoanManager is ContractOffererInterface, ConduitHelper, Ownable, ERC721 SpentItem[] calldata minimumReceivedFromBorrower, SpentItem[] calldata maximumSpentFromBorrower, bytes calldata context // encoded based on the schemaID - ) public view returns (SpentItem[] memory offer, ReceivedItem[] memory consideration) { - LoanManager.Obligation memory obligation = abi.decode(context, (LoanManager.Obligation)); - - if (obligation.debt.length == 0) { - revert InvalidDebt(); - } - if (maximumSpentFromBorrower.length == 0) { - revert InvalidMaximumSpentEmpty(); - } - consideration = maximumSpentFromBorrower.toReceivedItems(obligation.custodian); - if (feeTo != address(0)) { - consideration = _mergeFees(consideration, _feeRake(obligation.debt)); - } - address receiver = obligation.borrower; - - // we settle via seaport channels if caveats are present - if (fulfiller != receiver || obligation.caveats.length > 0) { - bytes32 caveatHash = keccak256( - encodeWithSaltAndBorrowerCounter( - obligation.borrower, obligation.salt, keccak256(abi.encode(obligation.caveats)) - ) - ); - SpentItem[] memory debt = obligation.debt; - offer = new SpentItem[](debt.length + 1); + ) public returns (SpentItem[] memory offer, ReceivedItem[] memory consideration) { + Actions action = StarPortLib.getAction(context); + if (action == Actions.Origination) { + (, LoanManager.Obligation memory obligation) = abi.decode(context, (Actions, LoanManager.Obligation)); + + bool feeOn; + if (obligation.debt.length == 0) { + revert InvalidDebtLength(); + } + if (maximumSpentFromBorrower.length == 0) { + revert InvalidMaximumSpentEmpty(); + } + consideration = maximumSpentFromBorrower.toReceivedItems(obligation.custodian); + if (feeTo != address(0)) { + feeOn = true; + } + address receiver = obligation.borrower; + + // we settle via seaport channels if caveats are present + if (fulfiller != receiver || obligation.caveats.length > 0) { + bytes32 caveatHash = keccak256( + encodeWithSaltAndBorrowerCounter( + obligation.borrower, obligation.salt, keccak256(abi.encode(obligation.caveats)) + ) + ); + SpentItem[] memory debt = obligation.debt; + offer = new SpentItem[](debt.length + 1); + SpentItem[] memory feeItems = !feeOn ? new SpentItem[](0) : _feeRake(debt); + + for (uint256 i; i < debt.length;) { + if ( + debt[i].itemType == ItemType.ERC721_WITH_CRITERIA + || debt[i].itemType == ItemType.ERC1155_WITH_CRITERIA + ) { + revert InvalidDebtType(); + } + offer[i] = SpentItem({ + itemType: debt[i].itemType, + token: debt[i].token, + identifier: debt[i].identifier, + amount: debt[i].amount + }); + if (feeOn && feeItems[i].amount > 0) { + offer[i].amount = debt[i].amount - feeItems[i].amount; + } + unchecked { + ++i; + } + } - for (uint256 i; i < debt.length;) { - offer[i] = debt[i]; - unchecked { - ++i; + offer[debt.length] = SpentItem({ + itemType: ItemType.ERC721, + token: address(this), + identifier: uint256(caveatHash), + amount: 1 + }); + } else if (feeOn) { + SpentItem[] memory debt = obligation.debt; + offer = new SpentItem[](debt.length); + + SpentItem[] memory feeItems = !feeOn ? new SpentItem[](0) : _feeRake(debt); + + for (uint256 i; i < debt.length;) { + offer[i] = SpentItem({ + itemType: debt[i].itemType, + token: debt[i].token, + identifier: debt[i].identifier, + amount: debt[i].amount + }); + if (feeItems[i].amount > 0) { + offer[i].amount = debt[i].amount - feeItems[i].amount; + } + unchecked { + ++i; + } } } - - offer[debt.length] = - SpentItem({itemType: ItemType.ERC721, token: address(this), identifier: uint256(caveatHash), amount: 1}); + } else if (action == Actions.Refinance) { + (, LoanManager.Loan memory loan, bytes memory newPricingData) = + abi.decode(context, (Actions, LoanManager.Loan, bytes)); + + consideration = _getRefinanceConsiderationsPreview(loan, newPricingData, fulfiller); + // if for malicious or non-malicious the refinanceConsideration is zero + if (consideration.length == 0) { + revert InvalidNoRefinanceConsideration(); + } + } else { + revert InvalidAction(); } } + function _getRefinanceConsiderationsPreview( + LoanManager.Loan memory loan, + bytes memory newPricingData, + address fulfiller + ) internal view returns (ReceivedItem[] memory consideration) { + ( + // used to update the new loan amount + ReceivedItem[] memory considerationPayment, + ReceivedItem[] memory carryPayment, + ReceivedItem[] memory additionalPayment + ) = Pricing(loan.terms.pricing).isValidRefinance(loan, newPricingData, fulfiller); + + consideration = _mergeConsiderations(considerationPayment, carryPayment, additionalPayment); + consideration = _removeZeroAmounts(consideration); + } + /** * @dev Gets the metadata for this contract offerer. * @@ -333,84 +455,87 @@ contract LoanManager is ContractOffererInterface, ConduitHelper, Ownable, ERC721 return ("Loans", schemas); } + /** + * @dev set's the default fee Data + * only owner can call + * @param feeTo_ The feeToAddress + * @param defaultFeeRake_ the default fee rake in WAD denomination(1e17 = 10%) + */ function setFeeData(address feeTo_, uint96 defaultFeeRake_) external onlyOwner { feeTo = feeTo_; defaultFeeRake = defaultFeeRake_; } - function setExoticFee(address exotic, Fee memory fee) external onlyOwner { - exoticFee[exotic] = fee; - } - - function getExoticFee(SpentItem memory exotic) public view returns (Fee memory fee) { - return exoticFee[exotic.token]; + /** + * @dev set's fee override's for specific tokens + * only owner can call + * @param token The token to override + * @param overrideValue the new value in WAD denomination to override(1e17 = 10%) + */ + function setFeeOverride(address token, uint96 overrideValue) external onlyOwner { + feeOverride[token].enabled = true; + feeOverride[token].amount = overrideValue; } - function _feeRake(SpentItem[] memory debt) internal view returns (ReceivedItem[] memory feeConsideration) { - uint256 i = 0; - feeConsideration = new ReceivedItem[](debt.length); - for (; i < debt.length;) { - feeConsideration[i].identifier = 0; //fees are native or erc20 - feeConsideration[i].recipient = payable(feeTo); + /** + * @dev set's fee override's for specific tokens + * only owner can call + * @param debt The debt to rake + * @return feeItems SpentItem[] of fee's + */ + function _feeRake(SpentItem[] memory debt) internal view returns (SpentItem[] memory feeItems) { + feeItems = new SpentItem[](debt.length); + uint256 totalDebtItems; + for (uint256 i = 0; i < debt.length;) { + Fee memory feeOverride = feeOverride[debt[i].token]; + feeItems[i].identifier = 0; //fees are native or erc20 if (debt[i].itemType == ItemType.NATIVE || debt[i].itemType == ItemType.ERC20) { - feeConsideration[i].amount = debt[i].amount.mulDiv( - defaultFeeRake, debt[i].itemType == ItemType.NATIVE ? 1e18 : 10 ** ERC20(debt[i].token).decimals() + feeItems[i].amount = debt[i].amount.mulDiv( + !feeOverride.enabled ? defaultFeeRake : feeOverride.amount, + (debt[i].itemType == ItemType.NATIVE) ? 1e18 : 10 ** ERC20(debt[i].token).decimals() ); - feeConsideration[i].token = debt[i].token; - feeConsideration[i].itemType = debt[i].itemType; - } else { - Fee memory fee = getExoticFee(debt[i]); - feeConsideration[i].itemType = fee.itemType; - feeConsideration[i].token = fee.token; - feeConsideration[i].amount = fee.rake; //flat fee + feeItems[i].token = debt[i].token; + feeItems[i].itemType = debt[i].itemType; + ++totalDebtItems; } unchecked { ++i; } } - } - - function _mergeFees(ReceivedItem[] memory first, ReceivedItem[] memory second) - internal - pure - returns (ReceivedItem[] memory consideration) - { - consideration = new ReceivedItem[](first.length + second.length); - uint256 i = 0; - for (; i < first.length;) { - consideration[i] = first[i]; - unchecked { - ++i; - } - } - for (i = first.length; i < second.length;) { - consideration[i] = second[i]; - unchecked { - ++i; - } + assembly { + mstore(feeItems, totalDebtItems) } } + /** + * @dev fills and verifies the incoming obligation + * + * @param fulfiller the new value in WAD denomination to override(1e17 = 10%) + * @param maximumSpentFromBorrower the maximum incoming items from the order + * @param context bytes encoded abi of the obligation + */ function _fillObligationAndVerify( address fulfiller, SpentItem[] calldata maximumSpentFromBorrower, bytes calldata context ) internal returns (SpentItem[] memory offer, ReceivedItem[] memory consideration) { - LoanManager.Obligation memory obligation = abi.decode(context, (LoanManager.Obligation)); + bool feesOn = false; + if (feeTo != address(0)) { + feesOn = true; + } + (, LoanManager.Obligation memory obligation) = abi.decode(context, (Actions, LoanManager.Obligation)); if (obligation.debt.length == 0) { - revert InvalidDebt(); + revert InvalidDebtLength(); } if (maximumSpentFromBorrower.length == 0) { revert InvalidMaximumSpentEmpty(); } consideration = maximumSpentFromBorrower.toReceivedItems(obligation.custodian); - if (feeTo != address(0)) { - consideration = _mergeFees(consideration, _feeRake(obligation.debt)); - } + address receiver = obligation.borrower; bool enforceCaveats = fulfiller != receiver || obligation.caveats.length > 0; - if (enforceCaveats) { + if (enforceCaveats || feesOn) { receiver = address(this); } Originator.Response memory response = Originator(obligation.originator).execute( @@ -452,16 +577,26 @@ contract LoanManager is ContractOffererInterface, ConduitHelper, Ownable, ERC721 ++i; } } - offer = _setOffer(loan.debt, caveatHash); + offer = _setOffer(loan.debt, caveatHash, feesOn); + } else if (feesOn) { + offer = _setOffer(loan.debt, bytes32(0), feesOn); } _issueLoanManager(loan, response.issuer.code.length > 0); } + /** + * @dev issues a LM token if needed + * only owner can call + * @param loan the loan to issue + * @param mint if true, mint the token + */ function _issueLoanManager(Loan memory loan, bool mint) internal { bytes memory encodedLoan = abi.encode(loan); uint256 loanId = loan.getId(); - + if (_issued(loanId)) { + revert LoanExists(); + } _setExtraData(loanId, uint8(FieldFlags.ACTIVE)); if (mint) { _safeMint(loan.issuer, loanId, encodedLoan); @@ -484,9 +619,34 @@ contract LoanManager is ContractOffererInterface, ConduitHelper, Ownable, ERC721 SpentItem[] calldata maximumSpent, bytes calldata context // encoded based on the schemaID ) external onlySeaport returns (SpentItem[] memory offer, ReceivedItem[] memory consideration) { - (offer, consideration) = _fillObligationAndVerify(fulfiller, maximumSpent, context); + Actions action = StarPortLib.getAction(context); + if (action == Actions.Origination) { + (offer, consideration) = _fillObligationAndVerify(fulfiller, maximumSpent, context); + } else if (action == Actions.Refinance) { + consideration = _refinance(fulfiller, context); + } else { + revert InvalidAction(); + } + } + + /** + * @dev set's fee override's for specific tokens + * only owner can call + * @param feeItem The feeItem to payout + */ + function _moveFeesToReceived(SpentItem memory feeItem) internal { + if (feeItem.itemType == ItemType.NATIVE) { + payable(feeTo).call{value: feeItem.amount}(""); + } else if (feeItem.itemType == ItemType.ERC20) { + ERC20(feeItem.token).transfer(feeTo, feeItem.amount); + } } + /** + * @dev set's fee override's for specific tokens + * only owner can call + * @param debt The item to make available to seaport + */ function _enableDebtWithSeaport(SpentItem memory debt) internal { //approve consideration based on item type if (debt.itemType == ItemType.NATIVE) { @@ -498,32 +658,41 @@ contract LoanManager is ContractOffererInterface, ConduitHelper, Ownable, ERC721 } else if (debt.itemType == ItemType.ERC20) { ERC20(debt.token).approve(address(seaport), debt.amount); } else { - revert InvalidDebt(); + revert InvalidDebtType(); } } - function _setOffer(SpentItem[] memory debt, bytes32 caveatHash) internal returns (SpentItem[] memory offer) { - offer = new SpentItem[](debt.length + 1); - + function _setOffer(SpentItem[] memory debt, bytes32 caveatHash, bool feesOn) + internal + returns (SpentItem[] memory offer) + { + uint256 caveatLength = (caveatHash == bytes32(0)) ? 0 : 1; + offer = new SpentItem[](debt.length + caveatLength); + SpentItem[] memory feeItems = !feesOn ? new SpentItem[](0) : _feeRake(debt); for (uint256 i; i < debt.length;) { - offer[i] = debt[i]; - _enableDebtWithSeaport(debt[i]); + offer[i] = SpentItem({ + itemType: debt[i].itemType, + token: debt[i].token, + identifier: debt[i].identifier, + amount: debt[i].amount + }); + if (feesOn) { + offer[i].amount = debt[i].amount - feeItems[i].amount; + _moveFeesToReceived(feeItems[i]); + } + _enableDebtWithSeaport(offer[i]); unchecked { ++i; } } - - offer[debt.length] = - SpentItem({itemType: ItemType.ERC721, token: address(this), identifier: uint256(caveatHash), amount: 1}); - } - - function transferFrom(address from, address to, uint256 tokenId) public payable override { - //active loans do nothing - if (from != address(this)) revert CannotTransferLoans(); + if (caveatHash != bytes32(0)) { + offer[debt.length] = + SpentItem({itemType: ItemType.ERC721, token: address(this), identifier: uint256(caveatHash), amount: 1}); + } } - function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) public payable override { - if (from != address(this)) revert CannotTransferLoans(); + function transferFrom(address from, address to, uint256 tokenId) public payable override onlySeaport { + if (address(this) != from) revert CannotTransferLoans(); } /** @@ -543,44 +712,35 @@ contract LoanManager is ContractOffererInterface, ConduitHelper, Ownable, ERC721 bytes32[] calldata orderHashes, uint256 contractNonce ) external onlySeaport returns (bytes4 ratifyOrderMagicValue) { - _callCustody(consideration, orderHashes, contractNonce, context); + Actions action = StarPortLib.getAction(context); + if (action == Actions.Origination) { + _callCustody(consideration, orderHashes, contractNonce, context); + } ratifyOrderMagicValue = ContractOffererInterface.ratifyOrder.selector; } - function supportsInterface(bytes4 interfaceId) - public - view - virtual - override(ERC721, ContractOffererInterface) - returns (bool) - { + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721) returns (bool) { return interfaceId == type(ContractOffererInterface).interfaceId || interfaceId == type(ERC721).interfaceId || super.supportsInterface(interfaceId); } - function refinance(LoanManager.Loan memory loan, bytes memory newPricingData, address conduit) external { - (,, address conduitController) = seaport.information(); + function _refinance(address fulfiller, bytes calldata context) + internal + returns (ReceivedItem[] memory consideration) + { + (, LoanManager.Loan memory loan, bytes memory newPricingData) = + abi.decode(context, (Actions, LoanManager.Loan, bytes)); - if (ConduitControllerInterface(conduitController).ownerOf(conduit) != msg.sender) { - revert InvalidConduit(); - } ( - // used to update the new loan amount ReceivedItem[] memory considerationPayment, - // used to pay the carry amount ReceivedItem[] memory carryPayment, - // note: considerationPayment - carryPayment = amount to pay lender - - // used for any additional payments beyond consideration and carry ReceivedItem[] memory additionalPayment - ) = Pricing(loan.terms.pricing).isValidRefinance(loan, newPricingData, msg.sender); - - ReceivedItem[] memory refinanceConsideration = - _mergeConsiderations(considerationPayment, carryPayment, additionalPayment); - refinanceConsideration = _removeZeroAmounts(refinanceConsideration); + ) = Pricing(loan.terms.pricing).isValidRefinance(loan, newPricingData, fulfiller); + consideration = _mergeConsiderations(considerationPayment, carryPayment, additionalPayment); + consideration = _removeZeroAmounts(consideration); // if for malicious or non-malicious the refinanceConsideration is zero - if (refinanceConsideration.length == 0) { + if (consideration.length == 0) { revert InvalidNoRefinanceConsideration(); } @@ -593,19 +753,23 @@ contract LoanManager is ContractOffererInterface, ConduitHelper, Ownable, ERC721 } } - if ( - ConduitInterface(conduit).execute(_packageTransfers(refinanceConsideration, msg.sender)) - != ConduitInterface.execute.selector - ) { - revert ConduitTransferError(); - } - loan.terms.pricingData = newPricingData; - loan.originator = msg.sender; - loan.issuer = msg.sender; + loan.originator = fulfiller; + loan.issuer = fulfiller; loan.start = block.timestamp; - _issueLoanManager(loan, msg.sender.code.length > 0); + _issueLoanManager(loan, fulfiller.code.length > 0); + } + + receive() external payable { + try seaport.incrementCounter() { + revert NotEnteredViaSeaport(); + } catch {} } - receive() external payable {} + function onERC1155Received(address, address, uint256, uint256, bytes calldata) external returns (bytes4) { + try seaport.incrementCounter() { + revert NotEnteredViaSeaport(); + } catch {} + return this.onERC1155Received.selector; + } } diff --git a/src/lib/StarPortLib.sol b/src/lib/StarPortLib.sol index 7894c40a..b3af8e2e 100644 --- a/src/lib/StarPortLib.sol +++ b/src/lib/StarPortLib.sol @@ -3,12 +3,36 @@ pragma solidity ^0.8.17; import {ItemType, ReceivedItem, SpentItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; import {LoanManager} from "starport-core/LoanManager.sol"; +import "forge-std/console.sol"; + +enum Actions { + Nothing, + Origination, + Refinance, + Repayment, + Settlement +} library StarPortLib { error InvalidSalt(); uint256 internal constant _INVALID_SALT = 0x81e69d9b00000000000000000000000000000000000000000000000000000000; + uint256 internal constant ONE_WORD = 0x20; + uint256 internal constant CUSTODIAN_WORD_OFFSET = 0x40; + + function getAction(bytes calldata data) internal pure returns (Actions action) { + assembly { + action := calldataload(data.offset) + } + } + + function getCustodian(bytes calldata data) internal pure returns (address custodian) { + assembly { + custodian := calldataload(add(data.offset, CUSTODIAN_WORD_OFFSET)) + } + } + function getId(LoanManager.Loan memory loan) internal pure returns (uint256 loanId) { loanId = uint256(keccak256(abi.encode(loan))); } diff --git a/src/pricing/BasePricing.sol b/src/pricing/BasePricing.sol index 5f960cbb..4d57af89 100644 --- a/src/pricing/BasePricing.sol +++ b/src/pricing/BasePricing.sol @@ -109,7 +109,6 @@ abstract contract BasePricing is Pricing { consideration = new ReceivedItem[](loan.debt.length); uint256[] memory owing = _getOwed(loan, details, loan.start, block.timestamp); - bool isActive = LM.active(loan.getId()); uint256 i = 0; for (; i < consideration.length;) { diff --git a/src/pricing/CompoundInterestPricing.sol b/src/pricing/CompoundInterestPricing.sol index 53d130cc..bc4ebef8 100644 --- a/src/pricing/CompoundInterestPricing.sol +++ b/src/pricing/CompoundInterestPricing.sol @@ -8,13 +8,6 @@ import {BaseRecallPricing} from "starport-core/pricing/BaseRecallPricing.sol"; abstract contract CompoundInterestPricing is BaseRecallPricing { using FixedPointMathLib for uint256; - // function getInterest( - // uint256 delta_t, - // uint256 amount, - // uint256 rate // expressed as SPR seconds per rate - // ) public pure override returns (uint256) { - // return amount.mulWad((2718281828459045235 ** rate.mulWad(delta_t)) / 1e18); - // } function calculateInterest( uint256 delta_t, uint256 amount, diff --git a/test/StarPortTest.sol b/test/StarPortTest.sol index 8e3ab641..6cd06779 100644 --- a/test/StarPortTest.sol +++ b/test/StarPortTest.sol @@ -64,6 +64,7 @@ import {ERC721} from "solady/src/tokens/ERC721.sol"; import {ContractOffererInterface} from "seaport-types/src/interfaces/ContractOffererInterface.sol"; import {TokenReceiverInterface} from "starport-core/interfaces/TokenReceiverInterface.sol"; import {LoanSettledCallback} from "starport-core/LoanManager.sol"; +import {Actions} from "starport-core/lib/StarPortLib.sol"; interface IWETH9 { function deposit() external payable; @@ -172,7 +173,6 @@ contract StarPortTest is BaseOrderTest { vm.label(address(erc721s[0]), "721 collateral 1"); vm.label(address(erc721s[1]), "721 collateral 2"); vm.label(address(erc1155s[0]), "1155 collateral 1"); - vm.label(address(erc1155s[1]), "1155 collateral 2"); // allocate funds and tokens to test addresses allocateTokensAndApprovals(address(this), uint128(MAX_INT)); @@ -197,6 +197,7 @@ contract StarPortTest is BaseOrderTest { vm.label(address(erc20s[1]), "Collateral ERC20"); vm.label(address(erc1155s[0]), "Collateral 1155"); vm.label(address(erc1155s[1]), "Debt 1155 "); + vm.label(address(erc721s[2]), "Debt 721 "); { erc721s[1].mint(seller.addr, 1); erc721s[0].mint(borrower.addr, 1); @@ -204,6 +205,9 @@ contract StarPortTest is BaseOrderTest { erc721s[0].mint(borrower.addr, 3); erc20s[1].mint(borrower.addr, 10000); erc1155s[0].mint(borrower.addr, 1, 1); + erc1155s[1].mint(lender.addr, 1, 10); + erc1155s[1].mint(lender.addr, 2, 10); + erc721s[2].mint(lender.addr, 1); } conduitKeyOne = bytes32(uint256(uint160(address(lender.addr))) << 96); conduitKeyRefinancer = bytes32(uint256(uint160(address(refinancer.addr))) << 96); @@ -213,6 +217,8 @@ contract StarPortTest is BaseOrderTest { conduitController.updateChannel(lenderConduit, address(UO), true); erc20s[0].approve(address(lenderConduit), 100000); + erc1155s[1].setApprovalForAll(lenderConduit, true); + erc721s[2].setApprovalForAll(lenderConduit, true); vm.stopPrank(); vm.prank(address(issuer)); erc20s[0].approve(address(lenderConduit), 100000); @@ -263,7 +269,15 @@ contract StarPortTest is BaseOrderTest { internal returns (LoanManager.Loan memory) { - bool isTrusted = loanData.caveats.length == 0; + return newLoan(loanData, originator, collateral, ""); + } + + function newLoan( + NewLoanData memory loanData, + Originator originator, + ConsiderationItem[] storage collateral, + bytes memory revertMessage + ) internal returns (LoanManager.Loan memory) { { bytes32 detailsHash = keccak256(originator.encodeWithAccountCounter(keccak256(loanData.details))); (uint8 v, bytes32 r, bytes32 s) = vm.sign(strategist.key, detailsHash); @@ -283,6 +297,87 @@ contract StarPortTest is BaseOrderTest { } } + function refinanceLoan(LoanManager.Loan memory loan, bytes memory newPricingData, address asWho) + internal + returns (LoanManager.Loan memory newLoan) + { + return refinanceLoan(loan, newPricingData, asWho, ""); + } + + function refinanceLoan( + LoanManager.Loan memory loan, + bytes memory newPricingData, + address asWho, + bytes memory revertMessage + ) internal returns (LoanManager.Loan memory newLoan) { + if (revertMessage.length > 0) { + vm.expectRevert(revertMessage); + } + (SpentItem[] memory offer, ReceivedItem[] memory requiredConsideration) = LM.previewOrder( + address(seaport), + asWho, + new SpentItem[](0), + new SpentItem[](0), + abi.encode(Actions.Refinance, loan, newPricingData) + ); + //OrderParameters parameters; + // uint120 numerator; + // uint120 denominator; + // bytes signature; + // bytes extraData; + OfferItem[] memory offerItems = new OfferItem[](offer.length); + for (uint256 i = 0; i < offer.length; i++) { + offerItems[i] = OfferItem({ + itemType: offer[i].itemType, + token: offer[i].token, + identifierOrCriteria: offer[i].identifier, + startAmount: offer[i].amount, + endAmount: offer[i].amount + }); + } + + ConsiderationItem[] memory considerationItems = new ConsiderationItem[](requiredConsideration.length); + for (uint256 i = 0; i < requiredConsideration.length; i++) { + considerationItems[i] = ConsiderationItem({ + itemType: requiredConsideration[i].itemType, + token: requiredConsideration[i].token, + identifierOrCriteria: requiredConsideration[i].identifier, + startAmount: requiredConsideration[i].amount, + endAmount: requiredConsideration[i].amount, + recipient: requiredConsideration[i].recipient + }); + } + AdvancedOrder memory refinanceOrder = AdvancedOrder({ + signature: "", + parameters: _buildContractOrder(address(LM), offerItems, considerationItems), + numerator: 1, + denominator: 1, + extraData: abi.encode(Actions.Refinance, loan, newPricingData) + }); + vm.recordLogs(); + vm.startPrank(asWho); + + if (revertMessage.length > 0) { + vm.expectRevert(); //reverts InvalidContractOfferer with an address an a contract nonce so expect general revert + } + consideration.fulfillAdvancedOrder({ + advancedOrder: refinanceOrder, + criteriaResolvers: new CriteriaResolver[](0), + fulfillerConduitKey: bytes32(0), + recipient: address(asWho) + }); + vm.stopPrank(); + + Vm.Log[] memory logs = vm.getRecordedLogs(); + + for (uint256 i = 0; i < logs.length; i++) { + if (logs[i].topics[0] == bytes32(0x57cb72d73c48fadf55428537f6c9efbe080ae111339b0c5af42d9027ed20ba17)) { + (, newLoan) = abi.decode(logs[i].data, (uint256, LoanManager.Loan)); + break; + } + } + } + function buyNowPayLater( AdvancedOrder memory thingToBuy, NewLoanData memory loanData, @@ -392,7 +487,7 @@ contract StarPortTest is BaseOrderTest { numerator: 1, denominator: 1, signature: "0x", - extraData: abi.encode(activeLoan) + extraData: abi.encode(Actions.Settlement, activeLoan) }); uint256 balanceBefore = erc20s[0].balanceOf(borrower.addr); @@ -464,7 +559,7 @@ contract StarPortTest is BaseOrderTest { numerator: 1, denominator: 1, signature: "0x", - extraData: abi.encode(activeLoan) + extraData: abi.encode(Actions.Repayment, activeLoan) }); uint256 balanceBefore = erc20s[0].balanceOf(borrower.addr); @@ -556,7 +651,7 @@ contract StarPortTest is BaseOrderTest { numerator: 1, denominator: 1, signature: "", - extraData: abi.encode(nlr) + extraData: abi.encode(Actions.Origination, nlr) }); orders[2] = z; @@ -621,6 +716,14 @@ contract StarPortTest is BaseOrderTest { internal returns (LoanManager.Loan memory loan) { + return _executeNLR(nlr, collateral, ""); + } + + function _executeNLR( + LoanManager.Obligation memory nlr, + ConsiderationItem[] memory collateral, + bytes memory revertReason + ) internal returns (LoanManager.Loan memory loan) { bytes32 caveatHash = keccak256(LM.encodeWithSaltAndBorrowerCounter(nlr.borrower, nlr.salt, keccak256(abi.encode(nlr.caveats)))); OfferItem[] memory offer = new OfferItem[](nlr.debt.length + 1); @@ -649,8 +752,13 @@ contract StarPortTest is BaseOrderTest { OrderParameters memory op = _buildContractOrder(address(LM), nlr.caveats.length == 0 ? new OfferItem[](0) : offer, collateral); - AdvancedOrder memory x = - AdvancedOrder({parameters: op, numerator: 1, denominator: 1, signature: "0x", extraData: abi.encode(nlr)}); + AdvancedOrder memory x = AdvancedOrder({ + parameters: op, + numerator: 1, + denominator: 1, + signature: "0x", + extraData: abi.encode(Actions.Origination, nlr) + }); uint256 balanceBefore; if (debt[0].token == address(0)) { @@ -660,6 +768,9 @@ contract StarPortTest is BaseOrderTest { } vm.recordLogs(); vm.startPrank(borrower.addr); + if (revertReason.length > 0) { + vm.expectRevert(revertReason); + } if (collateral[0].itemType == ItemType.NATIVE) { consideration.fulfillAdvancedOrder{value: collateral[0].endAmount}({ advancedOrder: x, @@ -694,7 +805,16 @@ contract StarPortTest is BaseOrderTest { balanceAfter = ERC20(debt[0].token).balanceOf(borrower.addr); } - assertEq(balanceAfter - balanceBefore, debt[0].amount); + uint256 feeReceiverBalance; + if (LM.feeTo() != address(0)) { + if (debt[0].token == address(0)) { + feeReceiverBalance = LM.feeTo().balance; + } else { + feeReceiverBalance = ERC20(debt[0].token).balanceOf(LM.feeTo()); + } + } + + assertEq(balanceAfter - balanceBefore + feeReceiverBalance, debt[0].amount); vm.stopPrank(); } @@ -787,6 +907,15 @@ contract StarPortTest is BaseOrderTest { ConsiderationItem memory collateral, SpentItem memory debtRequested, address incomingIssuer + ) internal returns (Originator.Details memory details) { + return _generateOriginationDetails(collateral, debtRequested, incomingIssuer, address(LM.defaultCustodian())); + } + + function _generateOriginationDetails( + ConsiderationItem memory collateral, + SpentItem memory debtRequested, + address incomingIssuer, + address incomingCustodian ) internal returns (Originator.Details memory details) { delete selectedCollateral; delete debt; @@ -802,7 +931,7 @@ contract StarPortTest is BaseOrderTest { }); details = Originator.Details({ conduit: address(lenderConduit), - custodian: address(custodian), + custodian: address(incomingCustodian), issuer: incomingIssuer, deadline: block.timestamp + 100, offer: Originator.Offer({ @@ -820,9 +949,6 @@ contract StarPortTest is BaseOrderTest { ConsiderationItem memory collateralItem, SpentItem memory debtItem ) internal returns (LoanManager.Loan memory loan) { - selectedCollateral.push(collateralItem); - debt.push(debtItem); - Originator.Details memory loanDetails = _generateOriginationDetails(collateralItem, debtItem, lender); loan = newLoan( @@ -890,6 +1016,21 @@ contract StarPortTest is BaseOrderTest { }); } + function _getERC721Consideration(TestERC721 token, uint256 tokenId) + internal + view + returns (ConsiderationItem memory) + { + return ConsiderationItem({ + token: address(token), + startAmount: 1, + endAmount: 1, + identifierOrCriteria: tokenId, + itemType: ItemType.ERC721, + recipient: payable(address(custodian)) + }); + } + function _getERC1155Consideration(TestERC1155 token) internal view returns (ConsiderationItem memory) { return ConsiderationItem({ token: address(token), diff --git a/test/integration-testing/TestAstariaV1Loan.sol b/test/integration-testing/TestAstariaV1Loan.sol index 8577fb3a..62ef4444 100644 --- a/test/integration-testing/TestAstariaV1Loan.sol +++ b/test/integration-testing/TestAstariaV1Loan.sol @@ -1,53 +1,28 @@ import "starport-test/AstariaV1Test.sol"; import {BaseRecall} from "starport-core/hooks/BaseRecall.sol"; -// import {Base} from "starport-core/pricing/CompoundInterestPricing.sol"; -// import {AstariaV1Pricing} from "starport-core/pricing/AstariaV1Pricing.sol"; import "forge-std/console2.sol"; -import {StarPortLib} from "starport-core/lib/StarPortLib.sol"; +import {StarPortLib, Actions} from "starport-core/lib/StarPortLib.sol"; contract TestAstariaV1Loan is AstariaV1Test { using {StarPortLib.getId} for LoanManager.Loan; - function testNewLoanERC721CollateralDefaultTermsRecall() public { - Custodian custody = Custodian(LM.defaultCustodian()); - - LoanManager.Terms memory terms = LoanManager.Terms({ - hook: address(hook), - handler: address(handler), - pricing: address(pricing), - pricingData: defaultPricingData, - handlerData: defaultHandlerData, - hookData: defaultHookData - }); - - selectedCollateral.push( + function testNewLoanERC721CollateralDefaultTermsRecallBase() public { + Originator.Details memory loanDetails = _generateOriginationDetails( ConsiderationItem({ token: address(erc721s[0]), startAmount: 1, endAmount: 1, identifierOrCriteria: 1, itemType: ItemType.ERC721, - recipient: payable(address(custody)) - }) + recipient: payable(address(custodian)) + }), + SpentItem({itemType: ItemType.ERC20, token: address(erc20s[0]), amount: 100, identifier: 0}), + lender.addr ); - debt.push(SpentItem({itemType: ItemType.ERC20, token: address(erc20s[0]), amount: 100, identifier: 0})); - Originator.Details memory loanDetails = Originator.Details({ - conduit: address(lenderConduit), - custodian: address(custody), - issuer: lender.addr, - deadline: block.timestamp + 100, - offer: Originator.Offer({ - salt: bytes32(0), - terms: terms, - collateral: ConsiderationItemLib.toSpentItemArray(selectedCollateral), - debt: debt - }) - }); - LoanManager.Loan memory loan = newLoan( - NewLoanData(address(custody), new LoanManager.Caveat[](0), abi.encode(loanDetails)), + NewLoanData(address(loanDetails.custodian), new LoanManager.Caveat[](0), abi.encode(loanDetails)), Originator(UO), selectedCollateral ); @@ -63,14 +38,12 @@ contract TestAstariaV1Loan is AstariaV1Test { } { // refinance with before recall is initiated - vm.startPrank(refinancer.addr); - vm.expectRevert(Pricing.InvalidRefinance.selector); - LM.refinance( + refinanceLoan( loan, abi.encode(BasePricing.Details({rate: (uint256(1e16) * 100) / (365 * 1 days), carryRate: 0})), - refinancerConduit + refinancer.addr, + abi.encodeWithSelector(Pricing.InvalidRefinance.selector) ); - vm.stopPrank(); } uint256 stake; { @@ -115,14 +88,12 @@ contract TestAstariaV1Loan is AstariaV1Test { } { // refinance with incorrect terms - vm.expectRevert(AstariaV1Pricing.InsufficientRefinance.selector); - vm.startPrank(refinancer.addr); - LM.refinance( + refinanceLoan( loan, abi.encode(BasePricing.Details({rate: (uint256(1e16) * 100) / (365 * 1 days), carryRate: 0})), - refinancerConduit + refinancer.addr, + abi.encodeWithSelector(AstariaV1Pricing.InsufficientRefinance.selector) ); - vm.stopPrank(); } { // refinance with correct terms @@ -130,12 +101,10 @@ contract TestAstariaV1Loan is AstariaV1Test { uint256 oldLenderBefore = erc20s[0].balanceOf(lender.addr); uint256 recallerBefore = erc20s[0].balanceOf(recaller.addr); BaseRecall.Details memory details = abi.decode(loan.terms.hookData, (BaseRecall.Details)); - vm.startPrank(refinancer.addr); vm.warp(block.timestamp + (details.recallWindow / 2)); - LM.refinance( - loan, abi.encode(BasePricing.Details({rate: details.recallMax / 2, carryRate: 0})), refinancerConduit + refinanceLoan( + loan, abi.encode(BasePricing.Details({rate: details.recallMax / 2, carryRate: 0})), refinancer.addr ); - vm.stopPrank(); uint256 delta_t = block.timestamp - loan.start; BasePricing.Details memory pricingDetails = abi.decode(loan.terms.pricingData, (BasePricing.Details)); uint256 interest = @@ -300,7 +269,7 @@ contract TestAstariaV1Loan is AstariaV1Test { numerator: 1, denominator: 1, parameters: op, - extraData: abi.encode(loan), + extraData: abi.encode(Actions.Settlement, loan), signature: "" }); @@ -443,7 +412,7 @@ contract TestAstariaV1Loan is AstariaV1Test { numerator: 1, denominator: 1, parameters: op, - extraData: abi.encode(loan), + extraData: abi.encode(Actions.Settlement, loan), signature: "" }); diff --git a/test/integration-testing/TestNewLoan.sol b/test/integration-testing/TestNewLoan.sol index c1b1cd3b..3fbb061e 100644 --- a/test/integration-testing/TestNewLoan.sol +++ b/test/integration-testing/TestNewLoan.sol @@ -1,5 +1,6 @@ import "starport-test/StarPortTest.sol"; import {AstariaV1Pricing} from "starport-core/pricing/AstariaV1Pricing.sol"; +import {Actions} from "starport-core/lib/StarPortLib.sol"; contract TestNewLoan is StarPortTest { function testNewLoanERC721CollateralDefaultTerms2() public returns (LoanManager.Loan memory) { @@ -101,10 +102,9 @@ contract TestNewLoan is StarPortTest { newLoan(NewLoanData(address(custody), caveats, abi.encode(loanDetails)), Originator(UO), selectedCollateral); } - function testNewLoanERC721CollateralDefaultTermsRefinance() public { + function testNewLoanRefinanceNew() public { Custodian custody = Custodian(LM.defaultCustodian()); - // pricing = new AstariaV1Pricing(LM); LoanManager.Terms memory terms = LoanManager.Terms({ hook: address(hook), handler: address(handler), @@ -144,13 +144,12 @@ contract TestNewLoan is StarPortTest { Originator(UO), selectedCollateral ); - vm.startPrank(refinancer.addr); - LM.refinance( + + refinanceLoan( loan, abi.encode(BasePricing.Details({rate: (uint256(1e16) * 100) / (365 * 1 days), carryRate: 0})), - refinancerConduit + refinancer.addr ); - vm.stopPrank(); } function testBuyNowPayLater() public { @@ -302,7 +301,7 @@ contract TestNewLoan is StarPortTest { numerator: 1, denominator: 1, parameters: op, - extraData: abi.encode(activeLoan), + extraData: abi.encode(Actions.Settlement, activeLoan), signature: "" }); diff --git a/test/unit-testing/TestCustodian.sol b/test/unit-testing/TestCustodian.sol index cd427e8d..6922a5c9 100644 --- a/test/unit-testing/TestCustodian.sol +++ b/test/unit-testing/TestCustodian.sol @@ -2,7 +2,7 @@ import "starport-test/StarPortTest.sol"; import {DeepEq} from "starport-test/utils/DeepEq.sol"; import {MockCall} from "starport-test/utils/MockCall.sol"; import "forge-std/Test.sol"; -import {StarPortLib} from "starport-core/lib/StarPortLib.sol"; +import {StarPortLib, Actions} from "starport-core/lib/StarPortLib.sol"; contract MockCustodian is Custodian { constructor(LoanManager LM_, address seaport_) Custodian(LM_, seaport_) {} @@ -120,23 +120,6 @@ contract TestCustodian is StarPortTest, DeepEq, MockCall { ) ); vm.expectRevert(); - payable(address(custodian)).call{value: 1 ether}( - abi.encodeWithSelector( - Custodian.onERC721Received.selector, address(0), address(0), uint256(0), new bytes(0) - ) - ); - vm.expectRevert(); - payable(address(custodian)).call{value: 1 ether}( - abi.encodeWithSelector( - Custodian.onERC1155BatchReceived.selector, - address(0), - address(0), - new uint256[](0), - new uint256[](0), - new bytes(0) - ) - ); - vm.expectRevert(); payable(address(custodian)).call{value: 1 ether}( abi.encodeWithSelector( Custodian.onERC1155Received.selector, address(0), address(0), uint256(0), uint256(0), new bytes(0) @@ -200,23 +183,11 @@ contract TestCustodian is StarPortTest, DeepEq, MockCall { custodian.generateOrder(address(this), new SpentItem[](0), new SpentItem[](0), new bytes(0)); } - function testSafeTransferReceive() public { + function testSafeTransfer1155Receive() public { erc721s[0].mint(address(this), 0x1a4); - erc721s[0].safeTransferFrom(address(this), address(custodian), 0x1a4); - erc1155s[0].mint(address(this), 1, 2); - erc1155s[0].mint(address(this), 2, 2); erc1155s[0].safeTransferFrom(address(this), address(custodian), 1, 1, new bytes(0)); - - uint256[] memory ids = new uint256[](2); - ids[0] = 1; - ids[1] = 2; - - uint256[] memory amounts = new uint256[](2); - amounts[0] = 1; - amounts[1] = 1; - erc1155s[0].safeBatchTransferFrom(address(this), address(custodian), ids, amounts, new bytes(0)); } //TODO: make this test meaningful @@ -246,7 +217,9 @@ contract TestCustodian is StarPortTest, DeepEq, MockCall { //TODO: add assertions function testGenerateOrderRepay() public { vm.prank(seaportAddr); - custodian.generateOrder(activeLoan.borrower, new SpentItem[](0), debt, abi.encode(activeLoan)); + custodian.generateOrder( + activeLoan.borrower, new SpentItem[](0), debt, abi.encode(Actions.Repayment, activeLoan) + ); } //TODO: add assertions @@ -254,7 +227,7 @@ contract TestCustodian is StarPortTest, DeepEq, MockCall { vm.prank(activeLoan.borrower); custodian.setRepayApproval(address(this), true); vm.prank(seaportAddr); - custodian.generateOrder(address(this), new SpentItem[](0), debt, abi.encode(activeLoan)); + custodian.generateOrder(address(this), new SpentItem[](0), debt, abi.encode(Actions.Repayment, activeLoan)); } //TODO: add assertions @@ -275,10 +248,14 @@ contract TestCustodian is StarPortTest, DeepEq, MockCall { //function mockCallRevert(address callee, bytes calldata data, bytes calldata revertData) external; vm.mockCallRevert( address(issuer), - abi.encodeWithSelector(LoanSettledCallback.onLoanSettled.selector, abi.encode(activeLoan)), + abi.encodeWithSelector( + LoanSettledCallback.onLoanSettled.selector, abi.encode(Actions.Repayment, activeLoan) + ), new bytes(0) ); - custodian.generateOrder(activeLoan.borrower, new SpentItem[](0), debt, abi.encode(activeLoan)); + custodian.generateOrder( + activeLoan.borrower, new SpentItem[](0), debt, abi.encode(Actions.Repayment, activeLoan) + ); } function testGenerateOrderRepayERC1155AndERC20AndNativeHandlerAuthorized() public { @@ -298,7 +275,9 @@ contract TestCustodian is StarPortTest, DeepEq, MockCall { mockHandlerCall(activeLoan.terms.handler, new ReceivedItem[](0), address(activeLoan.terms.handler)); vm.prank(seaportAddr); - custodian.generateOrder(activeLoan.borrower, new SpentItem[](0), debt, abi.encode(activeLoan)); + custodian.generateOrder( + activeLoan.borrower, new SpentItem[](0), debt, abi.encode(Actions.Settlement, activeLoan) + ); //ERC20 loanDetails = _generateOriginationDetails( @@ -314,7 +293,9 @@ contract TestCustodian is StarPortTest, DeepEq, MockCall { loan.toStorage(activeLoan); vm.prank(seaportAddr); - custodian.generateOrder(activeLoan.borrower, new SpentItem[](0), debt, abi.encode(activeLoan)); + custodian.generateOrder( + activeLoan.borrower, new SpentItem[](0), debt, abi.encode(Actions.Settlement, activeLoan) + ); //Native loanDetails = _generateOriginationDetails( @@ -330,7 +311,9 @@ contract TestCustodian is StarPortTest, DeepEq, MockCall { loan.toStorage(activeLoan); vm.prank(seaportAddr); - custodian.generateOrder(activeLoan.borrower, new SpentItem[](0), debt, abi.encode(activeLoan)); + custodian.generateOrder( + activeLoan.borrower, new SpentItem[](0), debt, abi.encode(Actions.Settlement, activeLoan) + ); } function testGenerateOrderRepayERC1155AndERC20AndNative() public { @@ -347,7 +330,9 @@ contract TestCustodian is StarPortTest, DeepEq, MockCall { loan.toStorage(activeLoan); vm.prank(seaportAddr); - custodian.generateOrder(activeLoan.borrower, new SpentItem[](0), debt, abi.encode(activeLoan)); + custodian.generateOrder( + activeLoan.borrower, new SpentItem[](0), debt, abi.encode(Actions.Repayment, activeLoan) + ); //ERC20 loanDetails = _generateOriginationDetails( @@ -362,7 +347,9 @@ contract TestCustodian is StarPortTest, DeepEq, MockCall { loan.toStorage(activeLoan); vm.prank(seaportAddr); - custodian.generateOrder(activeLoan.borrower, new SpentItem[](0), debt, abi.encode(activeLoan)); + custodian.generateOrder( + activeLoan.borrower, new SpentItem[](0), debt, abi.encode(Actions.Repayment, activeLoan) + ); //Native loanDetails = _generateOriginationDetails( @@ -378,13 +365,15 @@ contract TestCustodian is StarPortTest, DeepEq, MockCall { loan.toStorage(activeLoan); vm.prank(seaportAddr); - custodian.generateOrder(activeLoan.borrower, new SpentItem[](0), debt, abi.encode(activeLoan)); + custodian.generateOrder( + activeLoan.borrower, new SpentItem[](0), debt, abi.encode(Actions.Repayment, activeLoan) + ); } function testGenerateOrderRepayNotBorrower() public { vm.prank(seaportAddr); vm.expectRevert(abi.encodeWithSelector(Custodian.InvalidRepayer.selector)); - custodian.generateOrder(alice, new SpentItem[](0), debt, abi.encode(activeLoan)); + custodian.generateOrder(alice, new SpentItem[](0), debt, abi.encode(Actions.Repayment, activeLoan)); } function testGenerateOrderSettlement() public { @@ -394,7 +383,7 @@ contract TestCustodian is StarPortTest, DeepEq, MockCall { mockHandlerCall(activeLoan.terms.handler, new ReceivedItem[](0), address(0)); (SpentItem[] memory offer, ReceivedItem[] memory consideration) = - custodian.generateOrder(alice, new SpentItem[](0), debt, abi.encode(activeLoan)); + custodian.generateOrder(alice, new SpentItem[](0), debt, abi.encode(Actions.Settlement, activeLoan)); vm.stopPrank(); @@ -408,7 +397,7 @@ contract TestCustodian is StarPortTest, DeepEq, MockCall { mockHandlerCall(activeLoan.terms.handler, new ReceivedItem[](0), address(activeLoan.terms.handler)); (SpentItem[] memory offer, ReceivedItem[] memory consideration) = - custodian.generateOrder(alice, new SpentItem[](0), debt, abi.encode(activeLoan)); + custodian.generateOrder(alice, new SpentItem[](0), debt, abi.encode(Actions.Settlement, activeLoan)); vm.stopPrank(); @@ -421,7 +410,7 @@ contract TestCustodian is StarPortTest, DeepEq, MockCall { mockHandlerCall(activeLoan.terms.handler, new ReceivedItem[](0), alice); vm.expectRevert(abi.encodeWithSelector(Custodian.InvalidFulfiller.selector)); - custodian.generateOrder(borrower.addr, new SpentItem[](0), debt, abi.encode(activeLoan)); + custodian.generateOrder(borrower.addr, new SpentItem[](0), debt, abi.encode(Actions.Settlement, activeLoan)); } function testGenerateOrderSettlementNoActiveLoan() public { @@ -430,14 +419,14 @@ contract TestCustodian is StarPortTest, DeepEq, MockCall { mockHandlerCall(activeLoan.terms.handler, new ReceivedItem[](0), lender.addr); activeLoan.borrower = address(bob); - vm.expectRevert(abi.encodeWithSelector(Custodian.InvalidLoan.selector)); - custodian.generateOrder(borrower.addr, new SpentItem[](0), debt, abi.encode(activeLoan)); + vm.expectRevert(abi.encodeWithSelector(LoanManager.InvalidLoan.selector)); + custodian.generateOrder(borrower.addr, new SpentItem[](0), debt, abi.encode(Actions.Settlement, activeLoan)); } //TODO: add assertions function testRatifyOrder() public { vm.startPrank(seaportAddr); - bytes memory context = abi.encode(activeLoan); + bytes memory context = abi.encode(Actions.Repayment, activeLoan); (SpentItem[] memory offer, ReceivedItem[] memory consideration) = custodian.generateOrder(activeLoan.borrower, new SpentItem[](0), debt, context); @@ -449,7 +438,7 @@ contract TestCustodian is StarPortTest, DeepEq, MockCall { function testGenerateOrderInvalidHandlerExecution() public { vm.startPrank(seaportAddr); - bytes memory context = abi.encode(activeLoan); + bytes memory context = abi.encode(Actions.Settlement, activeLoan); mockHookCall(activeLoan.terms.hook, false); mockHandlerCall(activeLoan.terms.handler, new ReceivedItem[](0), address(activeLoan.terms.handler)); mockHandlerExecuteFail(activeLoan.terms.handler); @@ -465,14 +454,19 @@ contract TestCustodian is StarPortTest, DeepEq, MockCall { mockHookCall(activeLoan.terms.hook, true); mockHandlerCall(activeLoan.terms.handler, new ReceivedItem[](0), address(0)); - (SpentItem[] memory expectedOffer, ReceivedItem[] memory expectedConsideration) = - custodian.generateOrder(activeLoan.borrower, new SpentItem[](0), debt, abi.encode(activeLoan)); + (SpentItem[] memory expectedOffer, ReceivedItem[] memory expectedConsideration) = custodian.generateOrder( + activeLoan.borrower, new SpentItem[](0), debt, abi.encode(Actions.Repayment, activeLoan) + ); mockHookCall(activeLoan.terms.hook, true); mockHandlerCall(activeLoan.terms.handler, new ReceivedItem[](0), address(0)); (SpentItem[] memory receivedOffer, ReceivedItem[] memory receivedCosideration) = custodian.previewOrder( - activeLoan.borrower, activeLoan.borrower, new SpentItem[](0), debt, abi.encode(activeLoan) + activeLoan.borrower, + activeLoan.borrower, + new SpentItem[](0), + debt, + abi.encode(Actions.Repayment, activeLoan) ); _deepEq(receivedOffer, expectedOffer); @@ -485,8 +479,9 @@ contract TestCustodian is StarPortTest, DeepEq, MockCall { destroyAccount(activeLoan.terms.hook, address(0)); vm.expectRevert(); - (SpentItem[] memory expectedOffer, ReceivedItem[] memory expectedConsideration) = - custodian.generateOrder(activeLoan.borrower, new SpentItem[](0), debt, abi.encode(activeLoan)); + (SpentItem[] memory expectedOffer, ReceivedItem[] memory expectedConsideration) = custodian.generateOrder( + activeLoan.borrower, new SpentItem[](0), debt, abi.encode(Actions.Repayment, activeLoan) + ); } function testGenerateOrderRepayInvalidHookReturnType() public { @@ -499,8 +494,9 @@ contract TestCustodian is StarPortTest, DeepEq, MockCall { ); vm.expectRevert(); - (SpentItem[] memory expectedOffer, ReceivedItem[] memory expectedConsideration) = - custodian.generateOrder(activeLoan.borrower, new SpentItem[](0), debt, abi.encode(activeLoan)); + (SpentItem[] memory expectedOffer, ReceivedItem[] memory expectedConsideration) = custodian.generateOrder( + activeLoan.borrower, new SpentItem[](0), debt, abi.encode(Actions.Repayment, activeLoan) + ); } function testPreviewOrderSettlementInvalidFufliller() public { @@ -510,7 +506,7 @@ contract TestCustodian is StarPortTest, DeepEq, MockCall { mockHandlerCall(activeLoan.terms.handler, new ReceivedItem[](0), address(1)); vm.expectRevert(abi.encodeWithSelector(Custodian.InvalidFulfiller.selector)); (SpentItem[] memory receivedOffer, ReceivedItem[] memory receivedConsideration) = - custodian.previewOrder(alice, alice, new SpentItem[](0), debt, abi.encode(activeLoan)); + custodian.previewOrder(alice, alice, new SpentItem[](0), debt, abi.encode(Actions.Settlement, activeLoan)); } function testPreviewOrderSettlementInvalidRepayer() public { @@ -520,7 +516,7 @@ contract TestCustodian is StarPortTest, DeepEq, MockCall { mockHandlerCall(activeLoan.terms.handler, new ReceivedItem[](0), address(0)); vm.expectRevert(abi.encodeWithSelector(Custodian.InvalidRepayer.selector)); (SpentItem[] memory receivedOffer, ReceivedItem[] memory receivedCosideration) = - custodian.previewOrder(alice, bob, new SpentItem[](0), debt, abi.encode(activeLoan)); + custodian.previewOrder(alice, bob, new SpentItem[](0), debt, abi.encode(Actions.Repayment, activeLoan)); } function testPreviewOrderSettlement() public { @@ -530,13 +526,14 @@ contract TestCustodian is StarPortTest, DeepEq, MockCall { mockHandlerCall(activeLoan.terms.handler, new ReceivedItem[](0), address(0)); (SpentItem[] memory expectedOffer, ReceivedItem[] memory expectedConsideration) = - custodian.generateOrder(alice, new SpentItem[](0), debt, abi.encode(activeLoan)); + custodian.generateOrder(alice, new SpentItem[](0), debt, abi.encode(Actions.Settlement, activeLoan)); mockHookCall(activeLoan.terms.hook, false); mockHandlerCall(activeLoan.terms.handler, new ReceivedItem[](0), address(0)); - (SpentItem[] memory receivedOffer, ReceivedItem[] memory receivedCosideration) = - custodian.previewOrder(seaportAddr, alice, new SpentItem[](0), debt, abi.encode(activeLoan)); + (SpentItem[] memory receivedOffer, ReceivedItem[] memory receivedCosideration) = custodian.previewOrder( + seaportAddr, alice, new SpentItem[](0), debt, abi.encode(Actions.Settlement, activeLoan) + ); _deepEq(receivedOffer, expectedOffer); _deepEq(receivedCosideration, expectedConsideration); @@ -547,7 +544,43 @@ contract TestCustodian is StarPortTest, DeepEq, MockCall { mockHandlerCall(activeLoan.terms.handler, new ReceivedItem[](0), address(0)); activeLoan.borrower = address(bob); vm.expectRevert(abi.encodeWithSelector(Custodian.InvalidLoan.selector)); - (SpentItem[] memory receivedOffer, ReceivedItem[] memory receivedCosideration) = - custodian.previewOrder(seaportAddr, alice, new SpentItem[](0), debt, abi.encode(activeLoan)); + (SpentItem[] memory receivedOffer, ReceivedItem[] memory receivedCosideration) = custodian.previewOrder( + seaportAddr, alice, new SpentItem[](0), debt, abi.encode(Actions.Settlement, activeLoan) + ); + } + + function testInvalidActionSettleActiveLoan() public { + vm.prank(seaportAddr); + + mockHookCall(activeLoan.terms.hook, true); + vm.expectRevert(abi.encodeWithSelector(Custodian.InvalidAction.selector)); + custodian.generateOrder(alice, new SpentItem[](0), debt, abi.encode(Actions.Settlement, activeLoan)); + + vm.expectRevert(abi.encodeWithSelector(Custodian.InvalidAction.selector)); + custodian.previewOrder(seaportAddr, alice, new SpentItem[](0), debt, abi.encode(Actions.Settlement, activeLoan)); + } + + function testInvalidActionRepayInActiveLoan() public { + vm.prank(seaportAddr); + + mockHookCall(activeLoan.terms.hook, false); + vm.expectRevert(abi.encodeWithSelector(Custodian.InvalidAction.selector)); + custodian.generateOrder(alice, new SpentItem[](0), debt, abi.encode(Actions.Repayment, activeLoan)); + + vm.expectRevert(abi.encodeWithSelector(Custodian.InvalidAction.selector)); + custodian.previewOrder(seaportAddr, alice, new SpentItem[](0), debt, abi.encode(Actions.Repayment, activeLoan)); + } + + function testInvalidAction() public { + vm.prank(seaportAddr); + + mockHookCall(activeLoan.terms.hook, true); + vm.expectRevert(abi.encodeWithSelector(Custodian.InvalidAction.selector)); + custodian.generateOrder(alice, new SpentItem[](0), debt, abi.encode(Actions.Origination, activeLoan)); + + vm.expectRevert(abi.encodeWithSelector(Custodian.InvalidAction.selector)); + custodian.previewOrder( + seaportAddr, alice, new SpentItem[](0), debt, abi.encode(Actions.Origination, activeLoan) + ); } } diff --git a/test/unit-testing/TestLoanManager.sol b/test/unit-testing/TestLoanManager.sol index a87b6478..3cf7dda0 100644 --- a/test/unit-testing/TestLoanManager.sol +++ b/test/unit-testing/TestLoanManager.sol @@ -1,5 +1,9 @@ import "starport-test/StarPortTest.sol"; import {StarPortLib} from "starport-core/lib/StarPortLib.sol"; +import {DeepEq} from "starport-test/utils/DeepEq.sol"; +import {FixedPointMathLib} from "solady/src/utils/FixedPointMathLib.sol"; +import "forge-std/console2.sol"; +import {SpentItemLib} from "seaport-sol/src/lib/SpentItemLib.sol"; contract MockOriginator is Originator, TokenReceiverInterface { constructor(LoanManager LM_, address strategist_, uint256 fee_) Originator(LM_, strategist_, fee_, msg.sender) {} @@ -44,19 +48,62 @@ contract MockOriginator is Originator, TokenReceiverInterface { } function execute(Request calldata request) external override returns (Response memory response) { + address issuer = address(this); + if (request.details.length > 0) { + if (request.debt[0].itemType != ItemType.NATIVE) { + Originator.Details memory details = abi.decode(request.details, (Originator.Details)); + issuer = details.issuer == address(0) ? issuer : details.issuer; + _execute(request, details); + } else { + payable(request.receiver).call{value: request.debt[0].amount}(""); + } + } return Response({terms: terms(request.details), issuer: address(this)}); } + + receive() external payable {} +} + +contract MockCustodian is Custodian { + constructor(LoanManager LM_, address seaport) Custodian(LM_, seaport) {} + + function custody( + ReceivedItem[] calldata consideration, + bytes32[] calldata orderHashes, + uint256 contractNonce, + bytes calldata context + ) external virtual override onlyLoanManager returns (bytes4 selector) {} } -contract TestLoanManager is StarPortTest { +contract TestLoanManager is StarPortTest, DeepEq { using Cast for *; + using FixedPointMathLib for uint256; LoanManager.Loan public activeLoan; using {StarPortLib.getId} for LoanManager.Loan; + uint256 public borrowAmount = 100; + MockCustodian mockCustodian = new MockCustodian(LM, address(seaport)); + function setUp() public virtual override { super.setUp(); + + erc20s[0].approve(address(lenderConduit), 100000); + + mockCustodian = new MockCustodian(LM, address(seaport)); + Originator.Details memory defaultLoanDetails = _generateOriginationDetails( + _getERC721Consideration(erc721s[0]), _getERC20SpentItem(erc20s[0], borrowAmount), lender.addr + ); + + LoanManager.Loan memory loan = newLoan( + NewLoanData(address(custodian), new LoanManager.Caveat[](0), abi.encode(defaultLoanDetails)), + Originator(UO), + selectedCollateral + ); + Custodian(custodian).mint(loan); + + loan.toStorage(activeLoan); } function testName() public { @@ -102,8 +149,971 @@ contract TestLoanManager is StarPortTest { // OrderParameters memory op = _buildContractOrder(address(LM), new OfferItem[](0), selectedCollateral); vm.startPrank(seaport); (SpentItem[] memory offer, ReceivedItem[] memory consideration) = - LM.generateOrder(address(this), new SpentItem[](0), maxSpent, abi.encode(O)); + LM.generateOrder(address(this), new SpentItem[](0), maxSpent, abi.encode(Actions.Origination, O)); //TODO:: validate return data matches request // assertEq(keccak256(abi.encode(consideration)), keccak256(abi.encode(maxSpent))); } + + function testCannotSettleUnlessValidCustodian() public { + vm.expectRevert(abi.encodeWithSelector(LoanManager.NotLoanCustodian.selector)); + LM.settle(activeLoan); + } + + function testCannotSettleInvalidLoan() public { + activeLoan.borrower = address(0); + vm.prank(activeLoan.custodian); + vm.expectRevert(abi.encodeWithSelector(LoanManager.InvalidLoan.selector)); + LM.settle(activeLoan); + } + + function testIssued() public { + assert(LM.issued(activeLoan.getId())); + } + + function testInitializedFlagSetProperly() public { + activeLoan.borrower = address(0); + assert(LM.initialized(activeLoan.getId())); + } + + function testTokenURI() public { + assertEq(LM.tokenURI(uint256(keccak256(abi.encode(activeLoan)))), ""); + } + + function testTokenURIInvalidLoan() public { + vm.expectRevert(abi.encodeWithSelector(LoanManager.InvalidLoan.selector)); + LM.tokenURI(uint256(0)); + } + + function testTransferFromFailFromSeaport() public { + vm.startPrank(address(LM.seaport())); + vm.expectRevert(abi.encodeWithSelector(LoanManager.CannotTransferLoans.selector)); + LM.transferFrom(address(this), address(this), uint256(keccak256(abi.encode(activeLoan)))); + vm.expectRevert(abi.encodeWithSelector(LoanManager.CannotTransferLoans.selector)); + LM.safeTransferFrom(address(this), address(this), uint256(keccak256(abi.encode(activeLoan)))); + vm.expectRevert(abi.encodeWithSelector(LoanManager.CannotTransferLoans.selector)); + LM.safeTransferFrom(address(this), address(this), uint256(keccak256(abi.encode(activeLoan))), ""); + vm.stopPrank(); + } + + function testNonDefaultCustodianCustodyCallFails() public { + LoanManager.Obligation memory obligation = LoanManager.Obligation({ + custodian: address(mockCustodian), + borrower: address(0), + debt: new SpentItem[](0), + salt: bytes32(0), + details: "", + approval: "", + caveats: new LoanManager.Caveat[](0), + originator: address(UO) + }); + vm.prank(address(LM.seaport())); + vm.expectRevert(abi.encodeWithSelector(LoanManager.InvalidCustodian.selector)); + LM.ratifyOrder( + new SpentItem[](0), + new ReceivedItem[](0), + abi.encode(Actions.Origination, obligation), + new bytes32[](0), + uint256(0) + ); + } + + function testNonDefaultCustodianCustodyCallSuccess() public { + LoanManager.Obligation memory obligation = LoanManager.Obligation({ + custodian: address(mockCustodian), + borrower: address(0), + debt: new SpentItem[](0), + salt: bytes32(0), + details: "", + approval: "", + caveats: new LoanManager.Caveat[](0), + originator: address(UO) + }); + + vm.mockCall( + address(mockCustodian), + abi.encodeWithSelector( + Custodian.custody.selector, + new ReceivedItem[](0), + new bytes32[](0), + uint256(0), + abi.encode(Actions.Origination, obligation) + ), + abi.encode(bytes4(Custodian.custody.selector)) + ); + vm.prank(address(LM.seaport())); + LM.ratifyOrder( + new SpentItem[](0), + new ReceivedItem[](0), + abi.encode(Actions.Origination, obligation), + new bytes32[](0), + uint256(0) + ); + } + + function testInvalidDebtLength() public { + LoanManager.Obligation memory obligation = LoanManager.Obligation({ + custodian: address(mockCustodian), + borrower: address(0), + debt: new SpentItem[](0), + salt: bytes32(0), + details: "", + approval: "", + caveats: new LoanManager.Caveat[](0), + originator: address(UO) + }); + vm.prank(address(LM.seaport())); + vm.expectRevert(abi.encodeWithSelector(LoanManager.InvalidDebtLength.selector)); + LM.generateOrder( + address(this), new SpentItem[](0), new SpentItem[](0), abi.encode(Actions.Origination, obligation) + ); + + vm.expectRevert(abi.encodeWithSelector(LoanManager.InvalidDebtLength.selector)); + LM.previewOrder( + address(seaport), + address(this), + new SpentItem[](0), + new SpentItem[](0), + abi.encode(Actions.Origination, obligation) + ); + } + + function testInvalidDebtType() public { + MockOriginator MO = new MockOriginator(LM, address(0), 0); + delete debt; + debt.push( + SpentItem({itemType: ItemType.ERC721_WITH_CRITERIA, token: address(erc721s[0]), amount: 100, identifier: 0}) + ); + LoanManager.Obligation memory obligation = LoanManager.Obligation({ + custodian: address(mockCustodian), + borrower: address(0), + debt: debt, + salt: bytes32(0), + details: "", + approval: "", + caveats: new LoanManager.Caveat[](0), + originator: address(MO) + }); + vm.prank(address(LM.seaport())); + vm.expectRevert(abi.encodeWithSelector(LoanManager.InvalidDebtType.selector)); + SpentItem[] memory maxSpent = new SpentItem[](1); + maxSpent[0] = SpentItem({token: address(erc721s[0]), amount: 1, identifier: 1, itemType: ItemType.ERC721}); + LM.generateOrder(address(this), new SpentItem[](0), maxSpent, abi.encode(Actions.Origination, obligation)); + + vm.expectRevert(abi.encodeWithSelector(LoanManager.InvalidDebtType.selector)); + LM.previewOrder( + address(seaport), address(this), new SpentItem[](0), maxSpent, abi.encode(Actions.Origination, obligation) + ); + } + //TODO: make this test meaningful + + function testSeaportMetadata() public view { + LM.getSeaportMetadata(); + } + + function testInvalidMaximumSpentEmpty() public { + LoanManager.Obligation memory obligation = LoanManager.Obligation({ + custodian: address(mockCustodian), + borrower: address(0), + debt: debt, + salt: bytes32(0), + details: "", + approval: "", + caveats: new LoanManager.Caveat[](0), + originator: address(UO) + }); + vm.prank(address(LM.seaport())); + vm.expectRevert(abi.encodeWithSelector(LoanManager.InvalidMaximumSpentEmpty.selector)); + LM.generateOrder( + address(this), new SpentItem[](0), new SpentItem[](0), abi.encode(Actions.Origination, obligation) + ); + vm.expectRevert(abi.encodeWithSelector(LoanManager.InvalidMaximumSpentEmpty.selector)); + LM.previewOrder( + address(seaport), + address(this), + new SpentItem[](0), + new SpentItem[](0), + abi.encode(Actions.Origination, obligation) + ); + } + + function testDefaultFeeRake() public { + assertEq(LM.defaultFeeRake(), 0); + address feeReceiver = address(20); + LM.setFeeData(feeReceiver, 1e17); //10% fees + + Originator.Details memory defaultLoanDetails = _generateOriginationDetails( + _getERC721Consideration(erc721s[0], uint256(2)), _getERC20SpentItem(erc20s[0], borrowAmount), lender.addr + ); + + LoanManager.Loan memory loan = newLoan( + NewLoanData(address(custodian), new LoanManager.Caveat[](0), abi.encode(defaultLoanDetails)), + Originator(UO), + selectedCollateral + ); + assertEq(erc20s[0].balanceOf(feeReceiver), debt[0].amount * 1e17 / 1e18, "fee receiver not paid properly"); + } + + function testOverrideFeeRake() public { + assertEq(LM.defaultFeeRake(), 0); + address feeReceiver = address(20); + LM.setFeeData(feeReceiver, 1e17); //10% fees + LM.setFeeOverride(debt[0].token, 0); //0% fees + + Originator.Details memory defaultLoanDetails = _generateOriginationDetails( + _getERC721Consideration(erc721s[0], uint256(2)), _getERC20SpentItem(erc20s[0], borrowAmount), lender.addr + ); + + LoanManager.Loan memory loan = newLoan( + NewLoanData(address(custodian), new LoanManager.Caveat[](0), abi.encode(defaultLoanDetails)), + Originator(UO), + selectedCollateral + ); + assertEq(erc20s[0].balanceOf(feeReceiver), 0, "fee receiver not paid properly"); + } + + function testCaveatEnforcerInvalidOrigination() public { + Originator originator = new MockOriginator(LM, address(0), 0); + address seaport = address(LM.seaport()); + debt.push(SpentItem({itemType: ItemType.ERC20, token: address(erc20s[0]), amount: 100, identifier: 0})); + + SpentItem[] memory maxSpent = new SpentItem[](1); + maxSpent[0] = SpentItem({token: address(erc721s[0]), amount: 1, identifier: 1, itemType: ItemType.ERC721}); + + TermEnforcer TE = new TermEnforcer(); + + LoanManager.Caveat[] memory caveats = new LoanManager.Caveat[](1); + caveats[0] = LoanManager.Caveat({enforcer: address(TE), terms: ""}); + LoanManager.Obligation memory O = LoanManager.Obligation({ + custodian: address(custodian), + borrower: borrower.addr, + debt: debt, + salt: bytes32(0), + details: "", + approval: "", + caveats: caveats, + originator: address(originator) + }); + + LoanManager.Loan memory mockLoan = LoanManager.Loan({ + start: block.timestamp, + borrower: O.borrower, + collateral: maxSpent, + issuer: O.originator, + custodian: O.custodian, + debt: debt, + originator: O.originator, + terms: LoanManager.Terms({ + hook: address(0), + hookData: new bytes(0), + pricing: address(0), + pricingData: new bytes(0), + handler: address(0), + handlerData: new bytes(0) + }) + }); + vm.mockCall( + address(TE), + abi.encodeWithSelector(TermEnforcer.enforceCaveat.selector, bytes(""), mockLoan), + abi.encode(false) + ); + vm.startPrank(seaport); + vm.expectRevert(abi.encodeWithSelector(LoanManager.InvalidOrigination.selector)); + LM.generateOrder(address(this), new SpentItem[](0), maxSpent, abi.encode(Actions.Origination, O)); + } + + function testGenerateOrderInvalidAction() public { + Originator originator = new MockOriginator(LM, address(0), 0); + address seaport = address(LM.seaport()); + debt.push(SpentItem({itemType: ItemType.ERC20, token: address(erc20s[0]), amount: 100, identifier: 0})); + + SpentItem[] memory maxSpent = new SpentItem[](1); + maxSpent[0] = SpentItem({token: address(erc721s[0]), amount: 1, identifier: 1, itemType: ItemType.ERC721}); + + // + LoanManager.Obligation memory O = LoanManager.Obligation({ + custodian: address(custodian), + borrower: borrower.addr, + debt: debt, + salt: bytes32(0), + details: "", + approval: "", + caveats: new LoanManager.Caveat[](0), + originator: address(originator) + }); + + vm.startPrank(seaport); + vm.expectRevert(abi.encodeWithSelector(LoanManager.InvalidAction.selector)); + LM.generateOrder(address(this), new SpentItem[](0), maxSpent, abi.encode(Actions.Repayment, O)); + } + + function testPreviewOrderInvalidAction() public { + Originator originator = new MockOriginator(LM, address(0), 0); + address seaport = address(LM.seaport()); + + SpentItem[] memory maxSpent = new SpentItem[](1); + maxSpent[0] = SpentItem({token: address(erc721s[0]), amount: 1, identifier: 1, itemType: ItemType.ERC721}); + + LoanManager.Obligation memory O = LoanManager.Obligation({ + custodian: address(custodian), + borrower: borrower.addr, + debt: debt, + salt: bytes32(0), + details: "", + approval: "", + caveats: new LoanManager.Caveat[](0), + originator: address(originator) + }); + + vm.startPrank(seaport); + vm.expectRevert(abi.encodeWithSelector(LoanManager.InvalidAction.selector)); + LM.previewOrder(seaport, address(this), new SpentItem[](0), maxSpent, abi.encode(Actions.Repayment, O)); + } + + function testPreviewOrderOriginationWithNoCaveatsSetNotBorrowerNoFee() public { + Originator originator = new MockOriginator(LM, address(0), 0); + address seaport = address(LM.seaport()); + delete debt; + debt.push(SpentItem({itemType: ItemType.ERC20, token: address(erc20s[0]), amount: 100, identifier: 0})); + + SpentItem[] memory maxSpent = new SpentItem[](1); + maxSpent[0] = SpentItem({token: address(erc721s[0]), amount: 1, identifier: 1, itemType: ItemType.ERC721}); + + LoanManager.Obligation memory O = LoanManager.Obligation({ + custodian: address(custodian), + borrower: borrower.addr, + debt: debt, + salt: bytes32(0), + details: "", + approval: "", + caveats: new LoanManager.Caveat[](0), + originator: address(originator) + }); + + bytes32 caveatHash = + keccak256(LM.encodeWithSaltAndBorrowerCounter(O.borrower, O.salt, keccak256(abi.encode(O.caveats)))); + ReceivedItem[] memory expectedConsider = new ReceivedItem[](maxSpent.length); + for (uint256 i; i < maxSpent.length; i++) { + expectedConsider[i] = ReceivedItem({ + itemType: maxSpent[i].itemType, + token: maxSpent[i].token, + identifier: maxSpent[i].identifier, + amount: maxSpent[i].amount, + recipient: payable(O.custodian) + }); + } + SpentItem[] memory expectedOffer = new SpentItem[](2); + expectedOffer[0] = debt[0]; + expectedOffer[1] = + SpentItem({itemType: ItemType.ERC721, token: address(LM), identifier: uint256(caveatHash), amount: 1}); + (SpentItem[] memory offer, ReceivedItem[] memory consider) = + LM.previewOrder(seaport, address(this), new SpentItem[](0), maxSpent, abi.encode(Actions.Origination, O)); + _deepEq(offer, expectedOffer); + _deepEq(consider, expectedConsider); + } + + function testPreviewOrderOriginationWithNoCaveatsSetNotBorrowerFeeOn() public { + Originator originator = new MockOriginator(LM, address(0), 0); + address seaport = address(LM.seaport()); + LM.setFeeData(address(20), 1e17); //10% fees + delete debt; + debt.push(SpentItem({itemType: ItemType.ERC20, token: address(erc20s[0]), amount: 100, identifier: 0})); + SpentItem[] memory maxSpent = new SpentItem[](1); + maxSpent[0] = SpentItem({token: address(erc721s[0]), amount: 1, identifier: 1, itemType: ItemType.ERC721}); + + LoanManager.Obligation memory O = LoanManager.Obligation({ + custodian: address(custodian), + borrower: borrower.addr, + debt: debt, + salt: bytes32(0), + details: "", + approval: "", + caveats: new LoanManager.Caveat[](0), + originator: address(originator) + }); + + bytes32 caveatHash = + keccak256(LM.encodeWithSaltAndBorrowerCounter(O.borrower, O.salt, keccak256(abi.encode(O.caveats)))); + ReceivedItem[] memory expectedConsider = new ReceivedItem[](maxSpent.length); + for (uint256 i; i < maxSpent.length; i++) { + expectedConsider[i] = ReceivedItem({ + itemType: maxSpent[i].itemType, + token: maxSpent[i].token, + identifier: maxSpent[i].identifier, + amount: maxSpent[i].amount, + recipient: payable(O.custodian) + }); + } + SpentItem[] memory expectedOffer = new SpentItem[](2); + expectedOffer[0] = debt[0]; + expectedOffer[0].amount = debt[0].amount - debt[0].amount.mulDiv(1e17, 1e18); + expectedOffer[1] = + SpentItem({itemType: ItemType.ERC721, token: address(LM), identifier: uint256(caveatHash), amount: 1}); + (SpentItem[] memory offer, ReceivedItem[] memory consider) = + LM.previewOrder(seaport, address(this), new SpentItem[](0), maxSpent, abi.encode(Actions.Origination, O)); + _deepEq(offer, expectedOffer); + _deepEq(consider, expectedConsider); + } + + function testPreviewOrderOriginationWithNoCaveatsSetAsBorrowerNoFee() public { + Originator originator = new MockOriginator(LM, address(0), 0); + address seaport = address(LM.seaport()); + console.log(LM.feeTo()); + delete debt; + debt.push(SpentItem({itemType: ItemType.ERC20, token: address(erc20s[0]), amount: 100, identifier: 0})); + SpentItem[] memory maxSpent = new SpentItem[](1); + maxSpent[0] = SpentItem({token: address(erc721s[0]), amount: 1, identifier: 1, itemType: ItemType.ERC721}); + + LoanManager.Obligation memory O = LoanManager.Obligation({ + custodian: address(custodian), + borrower: borrower.addr, + debt: debt, + salt: bytes32(0), + details: "", + approval: "", + caveats: new LoanManager.Caveat[](0), + originator: address(originator) + }); + + ReceivedItem[] memory expectedConsider = new ReceivedItem[](maxSpent.length); + for (uint256 i; i < maxSpent.length; i++) { + expectedConsider[i] = ReceivedItem({ + itemType: maxSpent[i].itemType, + token: maxSpent[i].token, + identifier: maxSpent[i].identifier, + amount: maxSpent[i].amount, + recipient: payable(O.custodian) + }); + } + SpentItem[] memory expectedOffer = new SpentItem[](0); + (SpentItem[] memory offer, ReceivedItem[] memory consider) = + LM.previewOrder(seaport, borrower.addr, new SpentItem[](0), maxSpent, abi.encode(Actions.Origination, O)); + _deepEq(offer, expectedOffer); + _deepEq(consider, expectedConsider); + } + + function testPreviewOrderOriginationWithNoCaveatsSetAsBorrowerFeeOn() public { + Originator originator = new MockOriginator(LM, address(0), 0); + address seaport = address(LM.seaport()); + LM.setFeeData(address(20), 1e17); //10% fees + delete debt; + debt.push(SpentItem({itemType: ItemType.ERC20, token: address(erc20s[0]), amount: 100, identifier: 0})); + + SpentItem[] memory maxSpent = new SpentItem[](1); + maxSpent[0] = SpentItem({token: address(erc721s[0]), amount: 1, identifier: 1, itemType: ItemType.ERC721}); + + LoanManager.Obligation memory O = LoanManager.Obligation({ + custodian: address(custodian), + borrower: borrower.addr, + debt: debt, + salt: bytes32(0), + details: "", + approval: "", + caveats: new LoanManager.Caveat[](0), + originator: address(originator) + }); + ReceivedItem[] memory expectedConsider = new ReceivedItem[](maxSpent.length); + for (uint256 i; i < maxSpent.length; i++) { + expectedConsider[i] = ReceivedItem({ + itemType: maxSpent[i].itemType, + token: maxSpent[i].token, + identifier: maxSpent[i].identifier, + amount: maxSpent[i].amount, + recipient: payable(O.custodian) + }); + } + SpentItem[] memory expectedOffer = new SpentItem[](1); + expectedOffer[0] = debt[0]; + expectedOffer[0].amount = debt[0].amount - debt[0].amount.mulDiv(1e17, 1e18); + (SpentItem[] memory offer, ReceivedItem[] memory consider) = + LM.previewOrder(seaport, borrower.addr, new SpentItem[](0), maxSpent, abi.encode(Actions.Origination, O)); + _deepEq(offer, expectedOffer); + _deepEq(consider, expectedConsider); + } + + function testPreviewOrderRefinanceAsRefinancerFeeOn() public { + Originator originator = new MockOriginator(LM, address(0), 0); + address seaport = address(LM.seaport()); + LM.setFeeData(address(20), 1e17); //10% fees + + ReceivedItem[] memory expectedConsideration = new ReceivedItem[](1); + for (uint256 i; i < debt.length; i++) { + expectedConsideration[i] = ReceivedItem({ + itemType: debt[i].itemType, + token: debt[i].token, + identifier: debt[i].identifier, + amount: debt[i].amount, + recipient: payable(activeLoan.issuer) + }); + } + (SpentItem[] memory offer, ReceivedItem[] memory originationConsideration) = LM.previewOrder( + seaport, refinancer.addr, new SpentItem[](0), new SpentItem[](0), abi.encode(Actions.Refinance, activeLoan) + ); + _deepEq(originationConsideration, expectedConsideration); + } + + function testPreviewOrderRefinanceAsRefinancerFeeOff() public { + Originator originator = new MockOriginator(LM, address(0), 0); + address seaport = address(LM.seaport()); + + ReceivedItem[] memory expectedConsideration = new ReceivedItem[](1); + for (uint256 i; i < debt.length; i++) { + expectedConsideration[i] = ReceivedItem({ + itemType: debt[i].itemType, + token: debt[i].token, + identifier: debt[i].identifier, + amount: debt[i].amount, + recipient: payable(activeLoan.issuer) + }); + } + (SpentItem[] memory offer, ReceivedItem[] memory originationConsideration) = LM.previewOrder( + seaport, refinancer.addr, new SpentItem[](0), new SpentItem[](0), abi.encode(Actions.Refinance, activeLoan) + ); + _deepEq(originationConsideration, expectedConsideration); + } + + function testRefinanceNoRefinanceConsideration() public { + Originator originator = new MockOriginator(LM, address(0), 0); + address seaport = address(LM.seaport()); + bytes memory newPricingData = + abi.encode(BasePricing.Details({rate: (uint256(1e16) * 100) / (365 * 1 days), carryRate: 0})); + + vm.mockCall( + address(activeLoan.terms.pricing), + abi.encodeWithSelector(Pricing.isValidRefinance.selector, activeLoan, newPricingData, refinancer.addr), + abi.encode(new ReceivedItem[](0), new ReceivedItem[](0), new ReceivedItem[](0)) + ); + vm.expectRevert(abi.encodeWithSelector(LoanManager.InvalidNoRefinanceConsideration.selector)); + LM.previewOrder( + seaport, + refinancer.addr, + new SpentItem[](0), + new SpentItem[](0), + abi.encode(Actions.Refinance, activeLoan, newPricingData) + ); + vm.prank(address(LM.seaport())); + vm.expectRevert(abi.encodeWithSelector(LoanManager.InvalidNoRefinanceConsideration.selector)); + LM.generateOrder( + refinancer.addr, + new SpentItem[](0), + new SpentItem[](0), + abi.encode(Actions.Refinance, activeLoan, newPricingData) + ); + } + + function testExoticDebtWithNoCaveatsNotAsBorrower() public { + Originator originator = new MockOriginator(LM, address(0), 0); + address seaport = address(LM.seaport()); + + SpentItem[] memory exoticDebt = new SpentItem[](2); + exoticDebt[0] = SpentItem({token: address(erc1155s[1]), amount: 1, identifier: 1, itemType: ItemType.ERC1155}); + exoticDebt[1] = SpentItem({token: address(erc721s[2]), amount: 1, identifier: 1, itemType: ItemType.ERC721}); + + SpentItem[] memory maxSpent = new SpentItem[](1); + maxSpent[0] = SpentItem({token: address(erc721s[0]), amount: 1, identifier: 1, itemType: ItemType.ERC721}); + Originator.Details memory OD; + OD.issuer = lender.addr; + OD.conduit = lenderConduit; + vm.prank(lender.addr); + conduitController.updateChannel(lenderConduit, address(originator), true); + + LoanManager.Obligation memory O = LoanManager.Obligation({ + custodian: address(custodian), + borrower: borrower.addr, + debt: exoticDebt, + salt: bytes32(0), + details: abi.encode(OD), + approval: "", + caveats: new LoanManager.Caveat[](0), + originator: address(originator) + }); + + bytes32 caveatHash = + keccak256(LM.encodeWithSaltAndBorrowerCounter(O.borrower, O.salt, keccak256(abi.encode(O.caveats)))); + ReceivedItem[] memory expectedConsider = new ReceivedItem[](maxSpent.length); + for (uint256 i; i < maxSpent.length; i++) { + expectedConsider[i] = ReceivedItem({ + itemType: maxSpent[i].itemType, + token: maxSpent[i].token, + identifier: maxSpent[i].identifier, + amount: maxSpent[i].amount, + recipient: payable(O.custodian) + }); + } + SpentItem[] memory expectedOffer = new SpentItem[](3); + expectedOffer[0] = exoticDebt[0]; + expectedOffer[1] = exoticDebt[1]; + expectedOffer[2] = + SpentItem({itemType: ItemType.ERC721, token: address(LM), identifier: uint256(caveatHash), amount: 1}); + (SpentItem[] memory previewOffer, ReceivedItem[] memory previewConsider) = LM.previewOrder( + address(LM.seaport()), address(this), new SpentItem[](0), maxSpent, abi.encode(Actions.Origination, O) + ); + vm.store(address(seaport), bytes32(uint256(0)), bytes32(uint256(2))); + vm.prank(address(LM.seaport())); + (SpentItem[] memory offer, ReceivedItem[] memory consider) = + LM.generateOrder(address(this), new SpentItem[](0), maxSpent, abi.encode(Actions.Origination, O)); + _deepEq(offer, expectedOffer); + _deepEq(offer, previewOffer); + _deepEq(consider, previewConsider); + _deepEq(consider, expectedConsider); + } + + function testExoticDebtWithNoCaveatsAsBorrower() public { + Originator originator = new MockOriginator(LM, address(0), 0); + address seaport = address(LM.seaport()); + + SpentItem[] memory exoticDebt = new SpentItem[](2); + exoticDebt[0] = SpentItem({token: address(erc1155s[1]), amount: 1, identifier: 1, itemType: ItemType.ERC1155}); + exoticDebt[1] = SpentItem({token: address(erc721s[2]), amount: 1, identifier: 1, itemType: ItemType.ERC721}); + + SpentItem[] memory maxSpent = new SpentItem[](1); + maxSpent[0] = SpentItem({token: address(erc721s[0]), amount: 1, identifier: 1, itemType: ItemType.ERC721}); + Originator.Details memory OD; + OD.issuer = lender.addr; + OD.conduit = lenderConduit; + vm.prank(lender.addr); + conduitController.updateChannel(lenderConduit, address(originator), true); + + LoanManager.Obligation memory O = LoanManager.Obligation({ + custodian: address(custodian), + borrower: borrower.addr, + debt: exoticDebt, + salt: bytes32(0), + details: abi.encode(OD), + approval: "", + caveats: new LoanManager.Caveat[](0), + originator: address(originator) + }); + + (SpentItem[] memory previewOffer, ReceivedItem[] memory previewConsider) = LM.previewOrder( + address(LM.seaport()), borrower.addr, new SpentItem[](0), maxSpent, abi.encode(Actions.Origination, O) + ); + ReceivedItem[] memory expectedConsider = new ReceivedItem[](maxSpent.length); + for (uint256 i; i < maxSpent.length; i++) { + expectedConsider[i] = ReceivedItem({ + itemType: maxSpent[i].itemType, + token: maxSpent[i].token, + identifier: maxSpent[i].identifier, + amount: maxSpent[i].amount, + recipient: payable(O.custodian) + }); + } + vm.prank(address(LM.seaport())); + (SpentItem[] memory offer, ReceivedItem[] memory consider) = + LM.generateOrder(borrower.addr, new SpentItem[](0), maxSpent, abi.encode(Actions.Origination, O)); + _deepEq(offer, new SpentItem[](0)); + _deepEq(previewOffer, offer); + _deepEq(consider, previewConsider); + _deepEq(consider, expectedConsider); + assert(erc721s[2].ownerOf(1) == borrower.addr); + assert(erc1155s[1].balanceOf(borrower.addr, 1) == 1); + } + + function testNativeDebtWithNoCaveatsAsBorrower() public { + Originator originator = new MockOriginator(LM, address(0), 0); + vm.deal(address(originator), 1 ether); + address seaport = address(LM.seaport()); + + SpentItem[] memory exoticDebt = new SpentItem[](1); + exoticDebt[0] = SpentItem({token: address(erc1155s[1]), amount: 100, identifier: 1, itemType: ItemType.NATIVE}); + + SpentItem[] memory maxSpent = new SpentItem[](1); + maxSpent[0] = SpentItem({token: address(erc721s[0]), amount: 1, identifier: 1, itemType: ItemType.ERC721}); + Originator.Details memory OD; + OD.issuer = lender.addr; + OD.conduit = lenderConduit; + vm.prank(lender.addr); + conduitController.updateChannel(lenderConduit, address(originator), true); + + LoanManager.Obligation memory O = LoanManager.Obligation({ + custodian: address(custodian), + borrower: borrower.addr, + debt: exoticDebt, + salt: bytes32(0), + details: abi.encode(OD), + approval: "", + caveats: new LoanManager.Caveat[](0), + originator: address(originator) + }); + + ReceivedItem[] memory expectedConsideration = new ReceivedItem[](1); + for (uint256 i; i < maxSpent.length; i++) { + expectedConsideration[i] = ReceivedItem({ + itemType: maxSpent[i].itemType, + token: maxSpent[i].token, + identifier: maxSpent[i].identifier, + amount: maxSpent[i].amount, + recipient: payable(O.custodian) + }); + } + (SpentItem[] memory previewOffer, ReceivedItem[] memory previewConsider) = LM.previewOrder( + address(LM.seaport()), borrower.addr, new SpentItem[](0), maxSpent, abi.encode(Actions.Origination, O) + ); + + uint256 balanceBefore = borrower.addr.balance; + vm.prank(address(LM.seaport())); + (SpentItem[] memory offer, ReceivedItem[] memory consider) = + LM.generateOrder(borrower.addr, new SpentItem[](0), maxSpent, abi.encode(Actions.Origination, O)); + + _deepEq(offer, new SpentItem[](0)); + _deepEq(previewOffer, offer); + + _deepEq(consider, previewConsider); + _deepEq(consider, expectedConsideration); + + assert(borrower.addr.balance == balanceBefore + exoticDebt[0].amount); + } + + function testNativeDebtWithNoCaveatsNotAsBorrower() public { + Originator originator = new MockOriginator(LM, address(0), 0); + vm.deal(address(originator), 1 ether); + address seaport = address(LM.seaport()); + + SpentItem[] memory exoticDebt = new SpentItem[](1); + exoticDebt[0] = SpentItem({token: address(0), amount: 100, identifier: 1, itemType: ItemType.NATIVE}); + + SpentItem[] memory maxSpent = new SpentItem[](1); + maxSpent[0] = SpentItem({token: address(erc721s[0]), amount: 1, identifier: 1, itemType: ItemType.ERC721}); + Originator.Details memory OD; + OD.issuer = lender.addr; + OD.conduit = lenderConduit; + vm.prank(lender.addr); + conduitController.updateChannel(lenderConduit, address(originator), true); + + LoanManager.Obligation memory O = LoanManager.Obligation({ + custodian: address(custodian), + borrower: borrower.addr, + debt: exoticDebt, + salt: bytes32(0), + details: abi.encode(OD), + approval: "", + caveats: new LoanManager.Caveat[](0), + originator: address(originator) + }); + + (SpentItem[] memory previewOffer, ReceivedItem[] memory previewConsider) = LM.previewOrder( + address(LM.seaport()), address(this), new SpentItem[](0), maxSpent, abi.encode(Actions.Origination, O) + ); + + uint256 balanceOfLM = address(LM).balance; + //enable re entrancy guard + vm.store(address(seaport), bytes32(uint256(0)), bytes32(uint256(2))); + + bytes32 caveatHash = + keccak256(LM.encodeWithSaltAndBorrowerCounter(O.borrower, O.salt, keccak256(abi.encode(O.caveats)))); + ReceivedItem[] memory expectedConsider = new ReceivedItem[](maxSpent.length); + for (uint256 i; i < maxSpent.length; i++) { + expectedConsider[i] = ReceivedItem({ + itemType: maxSpent[i].itemType, + token: maxSpent[i].token, + identifier: maxSpent[i].identifier, + amount: maxSpent[i].amount, + recipient: payable(O.custodian) + }); + } + SpentItem[] memory expectedOffer = new SpentItem[](2); + expectedOffer[0] = exoticDebt[0]; + expectedOffer[1] = + SpentItem({itemType: ItemType.ERC721, token: address(LM), identifier: uint256(caveatHash), amount: 1}); + vm.prank(address(LM.seaport())); + (SpentItem[] memory offer, ReceivedItem[] memory consider) = + LM.generateOrder(address(this), new SpentItem[](0), maxSpent, abi.encode(Actions.Origination, O)); + _deepEq(offer, expectedOffer); + _deepEq(previewOffer, offer); + _deepEq(consider, previewConsider); + _deepEq(consider, expectedConsider); + assert(address(LM).balance == balanceOfLM + exoticDebt[0].amount); + } + + function testNativeDebtWithNoCaveatsNotAsBorrowerFeesOn() public { + Originator originator = new MockOriginator(LM, address(0), 0); + vm.deal(address(originator), 1 ether); + address seaport = address(LM.seaport()); + + LM.setFeeData(address(20), 1e17); //10% fees + SpentItem[] memory exoticDebt = new SpentItem[](1); + exoticDebt[0] = SpentItem({token: address(0), amount: 100, identifier: 1, itemType: ItemType.NATIVE}); + + SpentItem[] memory maxSpent = new SpentItem[](1); + maxSpent[0] = SpentItem({token: address(erc721s[0]), amount: 1, identifier: 1, itemType: ItemType.ERC721}); + Originator.Details memory OD; + OD.issuer = lender.addr; + OD.conduit = lenderConduit; + vm.prank(lender.addr); + conduitController.updateChannel(lenderConduit, address(originator), true); + + LoanManager.Obligation memory O = LoanManager.Obligation({ + custodian: address(custodian), + borrower: borrower.addr, + debt: exoticDebt, + salt: bytes32(0), + details: abi.encode(OD), + approval: "", + caveats: new LoanManager.Caveat[](0), + originator: address(originator) + }); + + bytes memory encodedObligation = abi.encode(Actions.Origination, O); + + (SpentItem[] memory previewOffer, ReceivedItem[] memory previewConsider) = + LM.previewOrder(address(LM.seaport()), address(this), new SpentItem[](0), maxSpent, encodedObligation); + + uint256 balanceOfLM = address(LM).balance; + //enable re entrancy guard + vm.store(address(seaport), bytes32(uint256(0)), bytes32(uint256(2))); + + bytes32 caveatHash = + keccak256(LM.encodeWithSaltAndBorrowerCounter(O.borrower, O.salt, keccak256(abi.encode(O.caveats)))); + + ReceivedItem[] memory expectedConsider = new ReceivedItem[](maxSpent.length); + for (uint256 i; i < maxSpent.length; i++) { + expectedConsider[i] = ReceivedItem({ + itemType: maxSpent[i].itemType, + token: maxSpent[i].token, + identifier: maxSpent[i].identifier, + amount: maxSpent[i].amount, + recipient: payable(O.custodian) + }); + } + SpentItem[] memory expectedOffer = new SpentItem[](2); + expectedOffer[0] = SpentItemLib.copy(exoticDebt[0]); + expectedOffer[0].amount = expectedOffer[0].amount - expectedOffer[0].amount.mulDiv(1e17, 1e18); + expectedOffer[1] = + SpentItem({itemType: ItemType.ERC721, token: address(LM), identifier: uint256(caveatHash), amount: 1}); + vm.prank(address(LM.seaport())); + (SpentItem[] memory offer, ReceivedItem[] memory consider) = + LM.generateOrder(address(this), new SpentItem[](0), maxSpent, encodedObligation); + _deepEq(offer, expectedOffer); + _deepEq(previewOffer, offer); + _deepEq(consider, expectedConsider); + _deepEq(consider, previewConsider); + + assert(address(LM).balance == balanceOfLM + expectedOffer[0].amount); + assert(address(LM.feeTo()).balance == 10); + } + + function testPayableFunctions() public { + vm.deal(seaportAddr, 2 ether); + vm.prank(seaportAddr); + payable(address(LM)).call{value: 1 ether}(abi.encodeWithSignature("helloWorld()")); + vm.prank(seaportAddr); + payable(address(LM)).call{value: 1 ether}(""); + + vm.expectRevert(abi.encodeWithSelector(LoanManager.NotSeaport.selector)); + payable(address(LM)).call{value: 1 ether}(abi.encodeWithSignature("helloWorld()")); + vm.expectRevert(abi.encodeWithSelector(LoanManager.NotSeaport.selector)); + payable(address(LM)).call{value: 1 ether}(""); + } + + function testNonPayableFunctions() public { + vm.expectRevert(); + payable(address(LM)).call{value: 1 ether}(abi.encodeWithSelector(LoanManager.tokenURI.selector, uint256(0))); + vm.expectRevert(); + payable(address(LM)).call{value: 1 ether}( + abi.encodeWithSelector(LoanManager.supportsInterface.selector, bytes4(0)) + ); + vm.expectRevert(); + payable(address(LM)).call{value: 1 ether}(abi.encodeWithSelector(LoanManager.name.selector)); + vm.expectRevert(); + payable(address(LM)).call{value: 1 ether}(abi.encodeWithSelector(LoanManager.symbol.selector)); + vm.expectRevert(); + payable(address(LM)).call{value: 1 ether}( + abi.encodeWithSelector( + LoanManager.ratifyOrder.selector, + new SpentItem[](0), + new ReceivedItem[](0), + new bytes(0), + new bytes32[](0), + uint256(0) + ) + ); + vm.expectRevert(); + payable(address(LM)).call{value: 1 ether}( + abi.encodeWithSelector( + LoanManager.generateOrder.selector, address(0), new SpentItem[](0), new SpentItem[](0), new bytes(0) + ) + ); + vm.expectRevert(); + payable(address(LM)).call{value: 1 ether}(abi.encodeWithSelector(LoanManager.getSeaportMetadata.selector)); + vm.expectRevert(); + payable(address(LM)).call{value: 1 ether}( + abi.encodeWithSelector( + LoanManager.previewOrder.selector, + address(0), + address(0), + new SpentItem[](0), + new SpentItem[](0), + new bytes(0) + ) + ); + vm.expectRevert(); + payable(address(LM)).call{value: 1 ether}( + abi.encodeWithSelector( + LoanManager.onERC1155Received.selector, address(0), address(0), uint256(0), uint256(0), new bytes(0) + ) + ); + } + + function testSafeTransfer1155Receive() public { + erc1155s[0].mint(address(this), 1, 1); + + vm.store(address(LM.seaport()), bytes32(uint256(0)), bytes32(uint256(2))); + erc1155s[0].safeTransferFrom(address(this), address(LM), 1, 1, new bytes(0)); + } + + function testCannotIssueSameLoanTwice() public { + Originator originator = new MockOriginator(LM, address(0), 0); + vm.deal(address(originator), 1 ether); + address seaport = address(LM.seaport()); + + SpentItem[] memory exoticDebt = new SpentItem[](1); + exoticDebt[0] = SpentItem({token: address(0), amount: 100, identifier: 1, itemType: ItemType.NATIVE}); + + SpentItem[] memory maxSpent = new SpentItem[](1); + maxSpent[0] = SpentItem({token: address(erc20s[0]), amount: 20, identifier: 1, itemType: ItemType.ERC20}); + Originator.Details memory OD; + vm.prank(lender.addr); + conduitController.updateChannel(lenderConduit, address(originator), true); + + LoanManager.Obligation memory O = LoanManager.Obligation({ + custodian: address(custodian), + borrower: borrower.addr, + debt: exoticDebt, + salt: bytes32(0), + details: abi.encode(OD), + approval: "", + caveats: new LoanManager.Caveat[](0), + originator: address(originator) + }); + + bytes memory encodedObligation = abi.encode(Actions.Origination, O); + + (SpentItem[] memory previewOffer, ReceivedItem[] memory previewConsider) = + LM.previewOrder(address(LM.seaport()), borrower.addr, new SpentItem[](0), maxSpent, encodedObligation); + + uint256 balanceOfLM = address(LM).balance; + //enable re entrancy guard + vm.store(address(seaport), bytes32(uint256(0)), bytes32(uint256(2))); + + ReceivedItem[] memory expectedConsider = new ReceivedItem[](maxSpent.length); + for (uint256 i; i < maxSpent.length; i++) { + expectedConsider[i] = ReceivedItem({ + itemType: maxSpent[i].itemType, + token: maxSpent[i].token, + identifier: maxSpent[i].identifier, + amount: maxSpent[i].amount, + recipient: payable(O.custodian) + }); + } + + vm.prank(address(LM.seaport())); + (SpentItem[] memory offer, ReceivedItem[] memory consider) = + LM.generateOrder(borrower.addr, new SpentItem[](0), maxSpent, encodedObligation); + _deepEq(offer, new SpentItem[](0)); + _deepEq(previewOffer, offer); + _deepEq(consider, expectedConsider); + _deepEq(consider, previewConsider); + vm.prank(address(LM.seaport())); + vm.expectRevert(LoanManager.LoanExists.selector); + LM.generateOrder(borrower.addr, new SpentItem[](0), maxSpent, encodedObligation); + } }