diff --git a/.gas-snapshot b/.gas-snapshot index 6e4de034..f7c3efb3 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,44 +1,58 @@ -DiffFuzzTestStarPortLib:testSpentToReceived((uint8,address,uint256,uint256)[]) (runs: 256, μ: 880433, ~: 883031) -DiffFuzzTestStarPortLib:testUnboundSpentToReceived((uint8,address,uint256,uint256)[]) (runs: 256, μ: 232899, ~: 237832) -TestAstariaV1Handler:testGetAuctionStart() (gas: 425169) -TestAstariaV1Handler:testGetAuctionStartNotStarted() (gas: 424764) -TestAstariaV1Handler:testGetCurrentAuctionPrice() (gas: 440046) -TestAstariaV1Handler:testGetCurrentAuctionPriceNoAuction() (gas: 428373) -TestAstariaV1Handler:testGetSettlementDutchAuctionSettlementAbove() (gas: 482578) -TestAstariaV1Handler:testGetSettlementFailedDutchAuction() (gas: 440369) -TestAstariaV1Handler:testGetSettlementLoanNotRecalled() (gas: 430871) -TestAstariaV1Handler:testV1SettlementHandlerExecute() (gas: 411378) -TestAstariaV1Handler:testV1SettlementHandlerValidate() (gas: 411490) -TestAstariaV1Handler:testV1SettlementHandlerValidateInvalidHandler() (gas: 411614) -TestAstariaV1Hook:testCannotRecallTwice() (gas: 567550) -TestAstariaV1Hook:testCannotWithdrawLoanHasNotBeenRefinanced() (gas: 417239) -TestAstariaV1Hook:testCannotWithdrawWithdrawDoesNotExist() (gas: 423656) -TestAstariaV1Hook:testGenerateRecallConsideration() (gas: 466708) -TestAstariaV1Hook:testInvalidRecallInvalidStakeType() (gas: 483833) -TestAstariaV1Hook:testInvalidRecallLoanDoesNotExist() (gas: 513060) -TestAstariaV1Hook:testIsActive() (gas: 424376) -TestAstariaV1Hook:testIsRecalledInsideWindow() (gas: 560673) -TestAstariaV1Hook:testIsRecalledOutsideWindow() (gas: 558574) -TestAstariaV1Hook:testRecallRateActiveRecall() (gas: 546234) -TestAstariaV1Hook:testRecallRateEmptyRecall() (gas: 421919) -TestAstariaV1Loan:testNewLoanERC721CollateralDefaultTermsRecallBase() (gas: 1007784) -TestAstariaV1Loan:testNewLoanERC721CollateralDefaultTermsRecallLender() (gas: 717605) -TestAstariaV1Loan:testNewLoanERC721CollateralDefaultTermsRecallLiquidation() (gas: 742743) -TestBorrowerEnforcer:testBERevertAdditionalTransfers() (gas: 73128) -TestBorrowerEnforcer:testBERevertInvalidLoanTerms() (gas: 78338) -TestBorrowerEnforcer:testBEValidLoanTerms() (gas: 69429) +DiffFuzzTestStarPortLib:testSpentToReceived((uint8,address,uint256,uint256)[]) (runs: 256, μ: 880523, ~: 883225) +DiffFuzzTestStarPortLib:testUnboundSpentToReceived((uint8,address,uint256,uint256)[]) (runs: 256, μ: 232893, ~: 237832) +IntegrationTestCaveats:testOriginateUnapprovedFulfiller() (gas: 341266) +IntegrationTestCaveats:testOriginateWBorrowerApproval() (gas: 296157) +IntegrationTestCaveats:testOriginateWCaveats() (gas: 272197) +IntegrationTestCaveats:testOriginateWCaveatsIncrementedNonce() (gas: 198548) +IntegrationTestCaveats:testOriginateWCaveatsInvalidSalt() (gas: 257806) +IntegrationTestCaveats:testOriginateWCaveatsInvalidSaltManual() (gas: 174782) +IntegrationTestCaveats:testOriginateWLenderApproval() (gas: 296276) +IntegrationTestCaveats:testRefinanceAsLender() (gas: 1056340) +IntegrationTestCaveats:testRefinanceCaveatFailure() (gas: 422075) +IntegrationTestCaveats:testRefinanceUnapprovedFulfiller() (gas: 479978) +IntegrationTestCaveats:testRefinanceWCaveatsInvalidSalt() (gas: 400966) +IntegrationTestCaveats:testRefinanceWLenderApproval() (gas: 423604) +PausableNonReentrantImpl:test() (gas: 2442) +PausableNonReentrantImpl:testReentrancy() (gas: 2735) +TestAstariaV1Handler:testGetAuctionStart() (gas: 427553) +TestAstariaV1Handler:testGetAuctionStartNotStarted() (gas: 427126) +TestAstariaV1Handler:testGetCurrentAuctionPrice() (gas: 442408) +TestAstariaV1Handler:testGetCurrentAuctionPriceNoAuction() (gas: 430757) +TestAstariaV1Handler:testGetSettlementDutchAuctionSettlementAbove() (gas: 484940) +TestAstariaV1Handler:testGetSettlementFailedDutchAuction() (gas: 442731) +TestAstariaV1Handler:testGetSettlementLoanNotRecalled() (gas: 433233) +TestAstariaV1Handler:testV1SettlementHandlerExecute() (gas: 413740) +TestAstariaV1Handler:testV1SettlementHandlerValidate() (gas: 413852) +TestAstariaV1Handler:testV1SettlementHandlerValidateInvalidHandler() (gas: 413976) +TestAstariaV1Hook:testCannotRecallTwice() (gas: 569694) +TestAstariaV1Hook:testCannotWithdrawLoanHasNotBeenRefinanced() (gas: 419557) +TestAstariaV1Hook:testCannotWithdrawWithdrawDoesNotExist() (gas: 425996) +TestAstariaV1Hook:testGenerateRecallConsideration() (gas: 469070) +TestAstariaV1Hook:testInvalidRecallInvalidStakeType() (gas: 486195) +TestAstariaV1Hook:testInvalidRecallLoanDoesNotExist() (gas: 515422) +TestAstariaV1Hook:testIsActive() (gas: 426738) +TestAstariaV1Hook:testIsRecalledInsideWindow() (gas: 562926) +TestAstariaV1Hook:testIsRecalledOutsideWindow() (gas: 560827) +TestAstariaV1Hook:testRecallRateActiveRecall() (gas: 548487) +TestAstariaV1Hook:testRecallRateEmptyRecall() (gas: 424392) +TestAstariaV1Loan:testNewLoanERC721CollateralDefaultTermsRecallBase() (gas: 1016002) +TestAstariaV1Loan:testNewLoanERC721CollateralDefaultTermsRecallLender() (gas: 719784) +TestAstariaV1Loan:testNewLoanERC721CollateralDefaultTermsRecallLiquidation() (gas: 744900) +TestBorrowerEnforcer:testBERevertAdditionalTransfers() (gas: 73195) +TestBorrowerEnforcer:testBERevertInvalidLoanTerms() (gas: 78316) +TestBorrowerEnforcer:testBEValidLoanTerms() (gas: 69518) TestBorrowerEnforcer:testBEValidLoanTermsAnyIssuer() (gas: 69581) -TestCustodian:testCannotLazyMintTwice() (gas: 76698) +TestCustodian:testCannotLazyMintTwice() (gas: 76589) TestCustodian:testCannotMintInvalidLoanInvalidCustodian() (gas: 66861) -TestCustodian:testCannotMintInvalidLoanValidCustodian() (gas: 72501) -TestCustodian:testCustodySelector() (gas: 2625664) +TestCustodian:testCannotMintInvalidLoanValidCustodian() (gas: 72392) +TestCustodian:testCustodySelector() (gas: 2625577) TestCustodian:testDefaultCustodySelectorRevert() (gas: 70083) TestCustodian:testGenerateOrderInvalidHandlerExecution() (gas: 132813) TestCustodian:testGenerateOrderRepay() (gas: 173924) TestCustodian:testGenerateOrderRepayAsRepayApprovedBorrower() (gas: 199549) -TestCustodian:testGenerateOrderRepayERC1155AndERC20AndNative() (gas: 865388) -TestCustodian:testGenerateOrderRepayERC1155AndERC20AndNativeHandlerAuthorized() (gas: 790527) -TestCustodian:testGenerateOrderRepayERC1155WithRevert() (gas: 530139) +TestCustodian:testGenerateOrderRepayERC1155AndERC20AndNative() (gas: 868168) +TestCustodian:testGenerateOrderRepayERC1155AndERC20AndNativeHandlerAuthorized() (gas: 793329) +TestCustodian:testGenerateOrderRepayERC1155WithRevert() (gas: 532529) TestCustodian:testGenerateOrderRepayInvalidHookAddress() (gas: 90307) TestCustodian:testGenerateOrderRepayInvalidHookReturnType() (gas: 84624) TestCustodian:testGenerateOrderRepayNotBorrower() (gas: 96398) @@ -47,95 +61,99 @@ TestCustodian:testGenerateOrderSettlementHandlerAuthorized() (gas: 160486) TestCustodian:testGenerateOrderSettlementNoActiveLoan() (gas: 155707) TestCustodian:testGenerateOrderSettlementUnauthorized() (gas: 94177) TestCustodian:testGetBorrower() (gas: 76322) -TestCustodian:testInvalidAction() (gas: 114421) -TestCustodian:testInvalidActionRepayInActiveLoan() (gas: 117370) -TestCustodian:testInvalidActionSettleActiveLoan() (gas: 117330) +TestCustodian:testInvalidAction() (gas: 114312) +TestCustodian:testInvalidActionRepayInActiveLoan() (gas: 117261) +TestCustodian:testInvalidActionSettleActiveLoan() (gas: 117221) TestCustodian:testName() (gas: 7098) TestCustodian:testNonPayableFunctions() (gas: 225770) TestCustodian:testOnlySeaport() (gas: 17895) TestCustodian:testPayableFunctions() (gas: 41667) -TestCustodian:testPreviewOrderNoActiveLoan() (gas: 98737) -TestCustodian:testPreviewOrderRepay() (gas: 225878) -TestCustodian:testPreviewOrderSettlement() (gas: 182831) -TestCustodian:testPreviewOrderSettlementInvalidFufliller() (gas: 100655) -TestCustodian:testPreviewOrderSettlementInvalidRepayer() (gas: 106785) +TestCustodian:testPreviewOrderNoActiveLoan() (gas: 98628) +TestCustodian:testPreviewOrderRepay() (gas: 225769) +TestCustodian:testPreviewOrderSettlement() (gas: 182722) +TestCustodian:testPreviewOrderSettlementInvalidFufliller() (gas: 100546) +TestCustodian:testPreviewOrderSettlementInvalidRepayer() (gas: 106676) TestCustodian:testRatifyOrder() (gas: 180093) -TestCustodian:testSeaportMetadata() (gas: 8545) +TestCustodian:testSeaportMetadata() (gas: 8567) TestCustodian:testSetRepayApproval() (gas: 37861) TestCustodian:testSupportsInterface() (gas: 9428) TestCustodian:testSymbol() (gas: 7127) TestCustodian:testTokenURI() (gas: 64817) TestCustodian:testTokenURIInvalidLoan() (gas: 13196) -TestLenderEnforcer:testLERevertAdditionalTransfersFromLender() (gas: 73765) -TestLenderEnforcer:testLERevertInvalidLoanTerms() (gas: 78363) +TestLenderEnforcer:testLERevertAdditionalTransfersFromLender() (gas: 73743) +TestLenderEnforcer:testLERevertInvalidLoanTerms() (gas: 78341) TestLenderEnforcer:testLEValidLoanTerms() (gas: 69496) TestLenderEnforcer:testLEValidLoanTermsAnyBorrower() (gas: 69494) -TestLenderEnforcer:testLEValidLoanTermsWithAdditionalTransfers() (gas: 70762) -TestLoanCombinations:testLoan20For721SimpleInterestDutchFixedRepay() (gas: 553060) -TestLoanCombinations:testLoan20for20SimpleInterestDutchFixedRepay() (gas: 540665) -TestLoanCombinations:testLoan721for20SimpleInterestDutchFixedRepay() (gas: 570896) -TestLoanCombinations:testLoanAstariaSettlementRepay() (gas: 560626) -TestLoanCombinations:testLoanSimpleInterestEnglishFixed() (gas: 566100) -TestLoanManager:testActive() (gas: 67135) -TestLoanManager:testAdditionalTransfers() (gas: 293454) -TestLoanManager:testCannotIssueSameLoanTwice() (gas: 331845) -TestLoanManager:testCannotOriginateWhilePaused() (gas: 87814) +TestLenderEnforcer:testLEValidLoanTermsWithAdditionalTransfers() (gas: 70740) +TestLoanCombinations:testLoan20For721SimpleInterestDutchFixedRepay() (gas: 555315) +TestLoanCombinations:testLoan20for20SimpleInterestDutchFixedRepay() (gas: 542898) +TestLoanCombinations:testLoan721for20SimpleInterestDutchFixedRepay() (gas: 573042) +TestLoanCombinations:testLoanAstariaSettlementRepay() (gas: 562881) +TestLoanCombinations:testLoanSimpleInterestEnglishFixed() (gas: 568355) +TestLoanManager:testActive() (gas: 67048) +TestLoanManager:testAdditionalTransfers() (gas: 295721) +TestLoanManager:testCannotIssueSameLoanTwice() (gas: 337073) +TestLoanManager:testCannotOriginateWhilePaused() (gas: 70886) TestLoanManager:testCannotSettleInvalidLoan() (gas: 72745) -TestLoanManager:testCannotSettleUnlessValidCustodian() (gas: 68772) -TestLoanManager:testCaveatEnforcerRevert() (gas: 119189) -TestLoanManager:testDefaultFeeRake() (gas: 352859) -TestLoanManager:testExoticDebtWithNoCaveatsNotAsBorrower() (gas: 343160) -TestLoanManager:testInitializedFlagSetProperly() (gas: 65234) -TestLoanManager:testInvalidAmountCollateral() (gas: 152921) -TestLoanManager:testInvalidAmountCollateral721() (gas: 153064) -TestLoanManager:testInvalidAmountDebt() (gas: 177254) -TestLoanManager:testInvalidIdentifierDebt() (gas: 197248) -TestLoanManager:testInvalidItemType() (gas: 138969) -TestLoanManager:testInvalidTransferLengthCollateral() (gas: 161133) -TestLoanManager:testInvalidTransferLengthDebt() (gas: 165654) -TestLoanManager:testIssued() (gas: 67084) -TestLoanManager:testName() (gas: 7251) -TestLoanManager:testNonDefaultCustodianCustodyCallFails() (gas: 190362) -TestLoanManager:testNonDefaultCustodianCustodyCallSuccess() (gas: 258841) -TestLoanManager:testNonPayableFunctions() (gas: 175599) -TestLoanManager:testOverrideFeeRake() (gas: 346620) -TestLoanManager:testPause() (gas: 34091) +TestLoanManager:testCannotSettleUnlessValidCustodian() (gas: 68816) +TestLoanManager:testCaveatEnforcerRevert() (gas: 124147) +TestLoanManager:testDefaultFeeRake() (gas: 355182) +TestLoanManager:testExoticDebtWithNoCaveatsNotAsBorrower() (gas: 345427) +TestLoanManager:testInitializedFlagSetProperly() (gas: 65344) +TestLoanManager:testInvalidAmountCollateral() (gas: 157882) +TestLoanManager:testInvalidAmountCollateral721() (gas: 158025) +TestLoanManager:testInvalidAmountDebt() (gas: 182237) +TestLoanManager:testInvalidIdentifierDebt() (gas: 202231) +TestLoanManager:testInvalidItemType() (gas: 143930) +TestLoanManager:testInvalidTransferLengthCollateral() (gas: 166116) +TestLoanManager:testInvalidTransferLengthDebt() (gas: 170615) +TestLoanManager:testIssued() (gas: 67216) +TestLoanManager:testName() (gas: 7206) +TestLoanManager:testNonDefaultCustodianCustodyCallFails() (gas: 195345) +TestLoanManager:testNonDefaultCustodianCustodyCallSuccess() (gas: 261108) +TestLoanManager:testNonPayableFunctions() (gas: 180447) +TestLoanManager:testOverrideFeeRake() (gas: 348943) +TestLoanManager:testPause() (gas: 17164) TestLoanManager:testSupportsInterface() (gas: 9181) -TestLoanManager:testSymbol() (gas: 7235) -TestLoanManager:testTokenNoCodeCollateral() (gas: 137741) -TestLoanManager:testTokenNoCodeDebt() (gas: 170885) -TestLoanManager:testTokenURI() (gas: 64945) -TestLoanManager:testTokenURIInvalidLoan() (gas: 13384) -TestLoanManager:testTransferFromFail() (gas: 80088) -TestLoanManager:testUnPause() (gas: 14291) -TestNewLoan:testBuyNowPayLater() (gas: 2830344) -TestNewLoan:testNewLoanERC721CollateralDefaultTerms2() (gas: 392228) +TestLoanManager:testSymbol() (gas: 7257) +TestLoanManager:testTokenNoCodeCollateral() (gas: 142746) +TestLoanManager:testTokenNoCodeDebt() (gas: 175846) +TestLoanManager:testTokenURI() (gas: 64879) +TestLoanManager:testTokenURIInvalidLoan() (gas: 13296) +TestLoanManager:testTransferFromFail() (gas: 80110) +TestLoanManager:testUnpause() (gas: 16177) +TestNewLoan:testBuyNowPayLater() (gas: 2832648) +TestNewLoan:testNewLoanERC721CollateralDefaultTerms2() (gas: 394514) TestNewLoan:testNewLoanERC721CollateralLessDebtThanOffered() (gas: 2348) -TestNewLoan:testNewLoanRefinanceNew() (gas: 185) -TestNewLoan:testNewLoanViaOriginatorBorrowerApprovalAndLenderApproval() (gas: 299558) -TestNewLoan:testNewLoanViaOriginatorLenderApproval() (gas: 354228) +TestNewLoan:testNewLoanRefinanceNew() (gas: 207) +TestNewLoan:testNewLoanViaOriginatorBorrowerApprovalAndLenderApproval() (gas: 301842) +TestNewLoan:testNewLoanViaOriginatorLenderApproval() (gas: 356502) TestNewLoan:testSettleLoan() (gas: 252) +TestPausableNonReentrant:testNotOwner() (gas: 21254) +TestPausableNonReentrant:testPauseAndUnpause() (gas: 22555) +TestPausableNonReentrant:testReentrancy() (gas: 15360) +TestPausableNonReentrant:testUnpauseWhenNotPaused() (gas: 12582) TestRefStarPortLib:testSpentToReceived() (gas: 13315) TestRefStarPortLib:testValidateSalt(address,bytes32) (runs: 256, μ: 33865, ~: 33865) -TestRepayLoan:testRepayLoanApprovedRepayer() (gas: 643968) -TestRepayLoan:testRepayLoanBase() (gas: 580372) -TestRepayLoan:testRepayLoanGenerateOrderNotSeaport() (gas: 400095) -TestRepayLoan:testRepayLoanInSettlement() (gas: 541092) -TestRepayLoan:testRepayLoanInvalidRepayer() (gas: 560501) -TestRepayLoan:testRepayLoanThatDoesNotExist() (gas: 849335) +TestRepayLoan:testRepayLoanApprovedRepayer() (gas: 646307) +TestRepayLoan:testRepayLoanBase() (gas: 582601) +TestRepayLoan:testRepayLoanGenerateOrderNotSeaport() (gas: 402435) +TestRepayLoan:testRepayLoanInSettlement() (gas: 543343) +TestRepayLoan:testRepayLoanInvalidRepayer() (gas: 562730) +TestRepayLoan:testRepayLoanThatDoesNotExist() (gas: 851564) TestStarPortLib:testSpentToReceived() (gas: 13315) TestStarPortLib:testValidateSalt(address,bytes32) (runs: 256, μ: 33865, ~: 33865) TestStrategistOriginator:testEncodeWithAccountCounter() (gas: 12307) TestStrategistOriginator:testGetStrategistData() (gas: 1471010) TestStrategistOriginator:testIncrementCounterAsStrategist() (gas: 18654) TestStrategistOriginator:testIncrementCounterNotAuthorized() (gas: 13445) -TestStrategistOriginator:testInvalidCollateral() (gas: 204438) -TestStrategistOriginator:testInvalidDeadline() (gas: 210167) -TestStrategistOriginator:testInvalidDebt() (gas: 206122) -TestStrategistOriginator:testInvalidDebtAmountAskingMoreThanOffered() (gas: 206570) -TestStrategistOriginator:testInvalidDebtAmountOfferingZero() (gas: 186947) -TestStrategistOriginator:testInvalidDebtAmountRequestingZero() (gas: 206823) -TestStrategistOriginator:testInvalidDebtLength() (gas: 205450) -TestStrategistOriginator:testInvalidOffer() (gas: 396601) -TestStrategistOriginator:testInvalidSigner() (gas: 208683) -TestStrategistOriginator:testSetStrategist() (gas: 17796) \ No newline at end of file +TestStrategistOriginator:testInvalidCollateral() (gas: 204508) +TestStrategistOriginator:testInvalidDeadline() (gas: 210237) +TestStrategistOriginator:testInvalidDebt() (gas: 206192) +TestStrategistOriginator:testInvalidDebtAmountAskingMoreThanOffered() (gas: 206640) +TestStrategistOriginator:testInvalidDebtAmountOfferingZero() (gas: 187017) +TestStrategistOriginator:testInvalidDebtAmountRequestingZero() (gas: 206893) +TestStrategistOriginator:testInvalidDebtLength() (gas: 205520) +TestStrategistOriginator:testInvalidOffer() (gas: 398913) +TestStrategistOriginator:testInvalidSigner() (gas: 208731) +TestStrategistOriginator:testSetStrategist() (gas: 17818) \ No newline at end of file diff --git a/src/LoanManager.sol b/src/LoanManager.sol index 10ed881b..da340f16 100644 --- a/src/LoanManager.sol +++ b/src/LoanManager.sol @@ -42,12 +42,13 @@ 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 {SafeTransferLib} from "solady/src/utils/SafeTransferLib.sol"; +import {PausableNonReentrant} from "starport-core/lib/PausableNonReentrant.sol"; interface LoanSettledCallback { function onLoanSettled(LoanManager.Loan calldata loan) external; } -contract LoanManager is Ownable, ERC721 { +contract LoanManager is ERC721, PausableNonReentrant { using FixedPointMathLib for uint256; using {StarPortLib.toReceivedItems} for SpentItem[]; @@ -69,7 +70,6 @@ contract LoanManager is Ownable, ERC721 { bytes32 public constant INTENT_ORIGINATION_TYPEHASH = keccak256("Origination(bytes32 hash,bytes32 salt,bytes32 caveatHash"); bytes32 public constant VERSION = keccak256("0"); - bool public paused; address public feeTo; uint88 public defaultFeeRake; mapping(address => mapping(bytes32 => bool)) public invalidHashes; @@ -118,8 +118,6 @@ contract LoanManager is Ownable, ERC721 { event Close(uint256 loanId); event Open(uint256 loanId, LoanManager.Loan loan); - event Paused(); - event UnPaused(); error InvalidRefinance(); error InvalidCustodian(); @@ -138,7 +136,6 @@ contract LoanManager is Ownable, ERC721 { error UnauthorizedAdditionalTransferIncluded(); error InvalidCaveatSigner(); error MalformedRefinance(); - error IsPaused(); constructor(ConsiderationInterface seaport_) { address custodian = address(new Custodian(this, seaport_)); @@ -188,34 +185,21 @@ contract LoanManager is Ownable, ERC721 { revert CannotTransferLoans(); } - function pause() external onlyOwner { - paused = true; - emit Paused(); - } - - function unPause() external onlyOwner { - paused = false; - emit UnPaused(); - } - function originate( AdditionalTransfer[] calldata additionalTransfers, CaveatEnforcer.CaveatWithApproval calldata borrowerCaveat, CaveatEnforcer.CaveatWithApproval calldata lenderCaveat, LoanManager.Loan memory loan - ) external payable { - if (paused) { - revert IsPaused(); - } + ) external payable pausableNonReentrant { //cache the addresses address borrower = loan.borrower; address issuer = loan.issuer; address feeRecipient = feeTo; - if (msg.sender != loan.borrower && !(approvals[borrower][msg.sender] == ApprovalType.BORROWER)) { + if (msg.sender != borrower && approvals[borrower][msg.sender] != ApprovalType.BORROWER) { _validateAndEnforceCaveats(borrowerCaveat, borrower, additionalTransfers, loan); } - if (msg.sender != issuer && !(approvals[issuer][msg.sender] == ApprovalType.LENDER)) { + if (msg.sender != issuer && approvals[issuer][msg.sender] != ApprovalType.LENDER) { _validateAndEnforceCaveats(lenderCaveat, issuer, additionalTransfers, loan); } @@ -246,10 +230,7 @@ contract LoanManager is Ownable, ERC721 { CaveatEnforcer.CaveatWithApproval calldata lenderCaveat, LoanManager.Loan memory loan, bytes calldata pricingData - ) external { - if (paused) { - revert IsPaused(); - } + ) external pausableNonReentrant { ( SpentItem[] memory considerationPayment, SpentItem[] memory carryPayment, @@ -260,18 +241,21 @@ contract LoanManager is Ownable, ERC721 { loan = applyRefinanceConsiderationToLoan(loan, considerationPayment, carryPayment, pricingData); _transferSpentItems(considerationPayment, lender, loan.issuer); - _transferSpentItems(carryPayment, lender, loan.originator); + + if (carryPayment.length > 0) { + _transferSpentItems(carryPayment, lender, loan.originator); + } loan.issuer = lender; loan.originator = address(0); loan.start = 0; - if (msg.sender != loan.issuer && !(approvals[loan.issuer][msg.sender] == ApprovalType.LENDER)) { - _validateAndEnforceCaveats(lenderCaveat, loan.issuer, additionalTransfers, loan); + if (msg.sender != lender && approvals[lender][msg.sender] != ApprovalType.LENDER) { + _validateAndEnforceCaveats(lenderCaveat, lender, additionalTransfers, loan); } if (additionalTransfers.length > 0) { - _validateAdditionalTransfers(loan.borrower, loan.issuer, msg.sender, additionalTransfers); + _validateAdditionalTransfers(loan.borrower, lender, msg.sender, additionalTransfers); StarPortLib.transferAdditionalTransfers(additionalTransfers); } @@ -469,6 +453,14 @@ contract LoanManager is Ownable, ERC721 { ); } + function incrementCaveatNonce() external { + ++caveatNonces[msg.sender]; + } + + function invalidateCaveatSalt(bytes32 salt) external { + invalidHashes[msg.sender][salt] = true; + } + /** * @dev the erc721 name of the contract * @return The name of the contract as a string diff --git a/src/lib/PausableNonReentrant.sol b/src/lib/PausableNonReentrant.sol new file mode 100644 index 00000000..dda04d7f --- /dev/null +++ b/src/lib/PausableNonReentrant.sol @@ -0,0 +1,67 @@ +import {Ownable} from "solady/src/auth/Ownable.sol"; + +abstract contract PausableNonReentrant is Ownable { + uint256 private constant _UNLOCKED = 0x1; + uint256 private constant _LOCKED = 0x2; + uint256 private constant _PAUSED = 0x3; + + uint256 private _state = _UNLOCKED; + + event Paused(); + event Unpaused(); + + error IsPaused(); + error IsLocked(); + error NotPaused(); + + modifier pausableNonReentrant() { + assembly { + //If locked or paused, handle revert cases + if gt(sload(_state.slot), _UNLOCKED) { + if gt(sload(_state.slot), _LOCKED) { + //Revert IsPaused + mstore(0, 0x1309a563) + revert(0x1c, 0x04) + } + //Revert IsLocked + mstore(0, 0xcaa30f55) + revert(0x1c, 0x04) + } + sstore(_state.slot, _LOCKED) + } + _; + assembly { + sstore(_state.slot, _UNLOCKED) + } + } + + function pause() external onlyOwner { + assembly { + //If locked, prevent owner from overriding state + if eq(sload(_state.slot), _LOCKED) { + //Revert IsLocked + mstore(0, 0xcaa30f55) + revert(0x1c, 0x04) + } + sstore(_state.slot, _PAUSED) + } + emit Paused(); + } + + function unpause() external onlyOwner { + assembly { + //If not paused, prevent owner from overriding state + if lt(sload(_state.slot), _PAUSED) { + //Revert NotPaused + mstore(0, 0x6cd60201) + revert(0x1c, 0x04) + } + sstore(_state.slot, _UNLOCKED) + } + emit Unpaused(); + } + + function paused() external view returns (bool) { + return _state == _PAUSED; + } +} diff --git a/test/StarPortTest.sol b/test/StarPortTest.sol index e1fbd5d4..029f0551 100644 --- a/test/StarPortTest.sol +++ b/test/StarPortTest.sol @@ -435,6 +435,21 @@ contract StarPortTest is BaseOrderTest { } } + function getBorrowerSignedCaveat( + BorrowerEnforcer.Details memory details, + Account memory signer, + bytes32 salt, + address enforcer + ) public view returns (CaveatEnforcer.CaveatWithApproval memory caveatApproval) { + caveatApproval.caveat = new CaveatEnforcer.Caveat[](1); + caveatApproval.salt = salt; + caveatApproval.caveat[0] = + CaveatEnforcer.Caveat({enforcer: enforcer, deadline: block.timestamp + 1 days, data: abi.encode(details)}); + bytes32 hash = LM.hashCaveatWithSaltAndNonce(signer.addr, salt, caveatApproval.caveat); + + (caveatApproval.v, caveatApproval.r, caveatApproval.s) = vm.sign(signer.key, hash); + } + function getLenderSignedCaveat( LenderEnforcer.Details memory details, Account memory signer, @@ -498,7 +513,7 @@ contract StarPortTest is BaseOrderTest { } function getRefinanceCaveat(LoanManager.Loan memory loan, bytes memory pricingData, address fulfiller) - external + public returns (LoanManager.Loan memory) { (SpentItem[] memory considerationPayment, SpentItem[] memory carryPayment,) = diff --git a/test/integration-testing/TestCaveats.sol b/test/integration-testing/TestCaveats.sol new file mode 100644 index 00000000..00fd9e50 --- /dev/null +++ b/test/integration-testing/TestCaveats.sol @@ -0,0 +1,258 @@ +pragma solidity ^0.8.17; + +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, Actions, AdditionalTransfer} from "starport-core/lib/StarPortLib.sol"; +import "forge-std/console.sol"; + +//Informational Finding: +//If you sign a caveat and submit the caveat as the borrower or lender, then it will not be invalidated +//With the current implementations, I think finding a valid refinance may be difficult + +contract IntegrationTestCaveats is StarPortTest, DeepEq, MockCall { + event LogLoan(LoanManager.Loan loan); + + function testOriginateWCaveats() public { + LoanManager.Loan memory loan = generateDefaultLoanTerms(); + + _setApprovalsForSpentItems(borrower.addr, loan.collateral); + + CaveatEnforcer.CaveatWithApproval memory lenderCaveat = getLenderSignedCaveat({ + details: LenderEnforcer.Details({loan: loan}), + signer: lender, + salt: bytes32(0), + enforcer: address(lenderEnforcer) + }); + + _setApprovalsForSpentItems(lender.addr, loan.debt); + + vm.prank(loan.borrower); + LM.originate(new AdditionalTransfer[](0), _emptyCaveat(), lenderCaveat, loan); + } + + function testOriginateWCaveatsInvalidSalt() public { + LoanManager.Loan memory loan = generateDefaultLoanTerms(); + loan.collateral[0] = _getERC20SpentItem(erc20s[0], 1000); + + CaveatEnforcer.CaveatWithApproval memory borrowerCaveat = getBorrowerSignedCaveat({ + details: BorrowerEnforcer.Details({loan: loan}), + signer: borrower, + salt: bytes32(0), + enforcer: address(borrowerEnforcer) + }); + _setApprovalsForSpentItems(borrower.addr, loan.collateral); + + _setApprovalsForSpentItems(lender.addr, loan.debt); + + vm.startPrank(loan.issuer); + LM.originate(new AdditionalTransfer[](0), borrowerCaveat, _emptyCaveat(), loan); + + vm.expectRevert(StarPortLib.InvalidSalt.selector); + LM.originate(new AdditionalTransfer[](0), borrowerCaveat, _emptyCaveat(), loan); + } + + function testOriginateWCaveatsInvalidSaltManual() public { + LoanManager.Loan memory loan = generateDefaultLoanTerms(); + + CaveatEnforcer.CaveatWithApproval memory borrowerCaveat = getBorrowerSignedCaveat({ + details: BorrowerEnforcer.Details({loan: loan}), + signer: borrower, + salt: bytes32(0), + enforcer: address(borrowerEnforcer) + }); + _setApprovalsForSpentItems(borrower.addr, loan.collateral); + + _setApprovalsForSpentItems(lender.addr, loan.debt); + + vm.prank(borrower.addr); + LM.invalidateCaveatSalt(0); + + vm.expectRevert(StarPortLib.InvalidSalt.selector); + vm.prank(lender.addr); + LM.originate(new AdditionalTransfer[](0), borrowerCaveat, _emptyCaveat(), loan); + } + + function testOriginateWCaveatsIncrementedNonce() public { + LoanManager.Loan memory loan = generateDefaultLoanTerms(); + + CaveatEnforcer.CaveatWithApproval memory borrowerCaveat = getBorrowerSignedCaveat({ + details: BorrowerEnforcer.Details({loan: loan}), + signer: borrower, + salt: bytes32(0), + enforcer: address(borrowerEnforcer) + }); + _setApprovalsForSpentItems(borrower.addr, loan.collateral); + + _setApprovalsForSpentItems(lender.addr, loan.debt); + + vm.prank(borrower.addr); + LM.incrementCaveatNonce(); + + vm.expectRevert(LoanManager.InvalidCaveatSigner.selector); + vm.prank(lender.addr); + LM.originate(new AdditionalTransfer[](0), borrowerCaveat, _emptyCaveat(), loan); + } + + function testOriginateWBorrowerApproval() public { + LoanManager.Loan memory loan = generateDefaultLoanTerms(); + + _setApprovalsForSpentItems(borrower.addr, loan.collateral); + + CaveatEnforcer.CaveatWithApproval memory lenderCaveat = getLenderSignedCaveat({ + details: LenderEnforcer.Details({loan: loan}), + signer: lender, + salt: bytes32(0), + enforcer: address(lenderEnforcer) + }); + + _setApprovalsForSpentItems(lender.addr, loan.debt); + + vm.prank(loan.borrower); + LM.setOriginateApproval(address(0x5), LoanManager.ApprovalType.BORROWER); + vm.prank(address(0x5)); + LM.originate(new AdditionalTransfer[](0), _emptyCaveat(), lenderCaveat, loan); + } + + function testOriginateWLenderApproval() public { + LoanManager.Loan memory loan = generateDefaultLoanTerms(); + + CaveatEnforcer.CaveatWithApproval memory borrowerCaveat = getBorrowerSignedCaveat({ + details: BorrowerEnforcer.Details({loan: loan}), + signer: borrower, + salt: bytes32(0), + enforcer: address(borrowerEnforcer) + }); + _setApprovalsForSpentItems(borrower.addr, loan.collateral); + _setApprovalsForSpentItems(lender.addr, loan.debt); + + vm.prank(lender.addr); + LM.setOriginateApproval(address(0x5), LoanManager.ApprovalType.LENDER); + vm.prank(address(0x5)); + LM.originate(new AdditionalTransfer[](0), borrowerCaveat, _emptyCaveat(), loan); + } + + function testOriginateUnapprovedFulfiller() public { + LoanManager.Loan memory loan = generateDefaultLoanTerms(); + + CaveatEnforcer.CaveatWithApproval memory borrowerCaveat = getBorrowerSignedCaveat({ + details: BorrowerEnforcer.Details({loan: loan}), + signer: borrower, + salt: bytes32(0), + enforcer: address(borrowerEnforcer) + }); + _setApprovalsForSpentItems(borrower.addr, loan.collateral); + + CaveatEnforcer.CaveatWithApproval memory lenderCaveat = getLenderSignedCaveat({ + details: LenderEnforcer.Details({loan: loan}), + signer: lender, + salt: bytes32(0), + enforcer: address(lenderEnforcer) + }); + + _setApprovalsForSpentItems(lender.addr, loan.debt); + + vm.prank(address(0x5)); + LM.originate(new AdditionalTransfer[](0), borrowerCaveat, lenderCaveat, loan); + } + + function testRefinanceWCaveatsInvalidSalt() public { + LoanManager.Loan memory loan = newLoanWithDefaultTerms(); + + LenderEnforcer.Details memory details = LenderEnforcer.Details({ + loan: LM.applyRefinanceConsiderationToLoan(loan, loan.debt, new SpentItem[](0), defaultPricingData) + }); + + details.loan.issuer = lender.addr; + details.loan.originator = address(0); + details.loan.start = 0; + + CaveatEnforcer.CaveatWithApproval memory lenderCaveat = getLenderSignedCaveat({ + details: details, + signer: borrower, + salt: bytes32(msg.sig), + enforcer: address(borrowerEnforcer) + }); + + _setApprovalsForSpentItems(lender.addr, loan.debt); + + vm.warp(block.timestamp + 1); + mockIsValidRefinanceCall(loan.terms.pricing, loan.debt, new SpentItem[](0), new AdditionalTransfer[](0)); + vm.expectRevert(StarPortLib.InvalidSalt.selector); + LM.refinance(lender.addr, lenderCaveat, loan, ""); + } + + function testRefinanceAsLender() public { + LoanManager.Loan memory loan = newLoanWithDefaultTerms(); + + address newLender = address(0x5); + allocateTokensAndApprovals(newLender, type(uint128).max); + _setApprovalsForSpentItems(newLender, loan.debt); + + vm.warp(block.timestamp + 1); + vm.prank(newLender); + mockIsValidRefinanceCall(loan.terms.pricing, loan.debt, new SpentItem[](0), new AdditionalTransfer[](0)); + LM.refinance(newLender, _emptyCaveat(), loan, defaultPricingData); + } + + function testRefinanceWLenderApproval() public { + LoanManager.Loan memory loan = newLoanWithDefaultTerms(); + + _setApprovalsForSpentItems(lender.addr, loan.debt); + + vm.prank(lender.addr); + LM.setOriginateApproval(borrower.addr, LoanManager.ApprovalType.LENDER); + + vm.warp(block.timestamp + 1); + vm.prank(borrower.addr); + mockIsValidRefinanceCall(loan.terms.pricing, loan.debt, new SpentItem[](0), new AdditionalTransfer[](0)); + LM.refinance(lender.addr, _emptyCaveat(), loan, defaultPricingData); + } + + function testRefinanceUnapprovedFulfiller() public { + LoanManager.Loan memory loan = newLoanWithDefaultTerms(); + LenderEnforcer.Details memory details = LenderEnforcer.Details({ + loan: LM.applyRefinanceConsiderationToLoan(loan, loan.debt, new SpentItem[](0), defaultPricingData) + }); + + details.loan.issuer = lender.addr; + details.loan.originator = address(0); + details.loan.start = 0; + + CaveatEnforcer.CaveatWithApproval memory lenderCaveat = getLenderSignedCaveat({ + details: details, + signer: lender, + salt: bytes32(0), + enforcer: address(lenderEnforcer) + }); + + _setApprovalsForSpentItems(lender.addr, loan.debt); + + vm.warp(block.timestamp + 1); + + vm.prank(loan.borrower); + + mockIsValidRefinanceCall(loan.terms.pricing, loan.debt, new SpentItem[](0), new AdditionalTransfer[](0)); + + LM.refinance(lender.addr, lenderCaveat, loan, defaultPricingData); + } + + function testRefinanceCaveatFailure() public { + LoanManager.Loan memory loan = newLoanWithDefaultTerms(); + + CaveatEnforcer.CaveatWithApproval memory lenderCaveat = getLenderSignedCaveat({ + details: LenderEnforcer.Details({loan: loan}), + signer: lender, + salt: bytes32(0), + enforcer: address(lenderEnforcer) + }); + + _setApprovalsForSpentItems(lender.addr, loan.debt); + + vm.prank(loan.borrower); + mockIsValidRefinanceCall(loan.terms.pricing, loan.debt, new SpentItem[](0), new AdditionalTransfer[](0)); + vm.expectRevert(LenderEnforcer.InvalidLoanTerms.selector); + LM.refinance(lender.addr, lenderCaveat, loan, defaultPricingData); + } +} diff --git a/test/unit-testing/TestLoanManager.sol b/test/unit-testing/TestLoanManager.sol index 7f5f8d6f..86f6dbe1 100644 --- a/test/unit-testing/TestLoanManager.sol +++ b/test/unit-testing/TestLoanManager.sol @@ -6,6 +6,7 @@ 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"; +import {PausableNonReentrant} from "starport-core/lib/PausableNonReentrant.sol"; import {Originator} from "starport-core/originators/Originator.sol"; contract MockOriginator is StrategistOriginator, TokenReceiverInterface { @@ -135,18 +136,19 @@ contract TestLoanManager is StarPortTest, DeepEq { } event Paused(); - event UnPaused(); + event Unpaused(); function testPause() public { - vm.expectEmit(); + vm.expectEmit(address(LM)); emit Paused(); LM.pause(); } - function testUnPause() public { - vm.expectEmit(); - emit UnPaused(); - LM.unPause(); + function testUnpause() public { + LM.pause(); + vm.expectEmit(address(LM)); + emit Unpaused(); + LM.unpause(); } function testIssued() public { @@ -201,7 +203,7 @@ contract TestLoanManager is StarPortTest, DeepEq { function testCannotOriginateWhilePaused() public { LM.pause(); LoanManager.Loan memory loan = generateDefaultLoanTerms(); - vm.expectRevert(abi.encodeWithSelector(LoanManager.IsPaused.selector)); + vm.expectRevert(abi.encodeWithSelector(PausableNonReentrant.IsPaused.selector)); LM.originate(new AdditionalTransfer[](0), _emptyCaveat(), _emptyCaveat(), loan); } diff --git a/test/unit-testing/TestPausableNonReentrant.sol b/test/unit-testing/TestPausableNonReentrant.sol new file mode 100644 index 00000000..748a68c4 --- /dev/null +++ b/test/unit-testing/TestPausableNonReentrant.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.17; + +import {PausableNonReentrant} from "starport-core/lib/PausableNonReentrant.sol"; +import {Test} from "forge-std/Test.sol"; +import {Ownable} from "solady/src/auth/Ownable.sol"; + +contract TestPausableNonReentrant is Test { + PausableNonReentrantImpl impl = new PausableNonReentrantImpl(); + + event Paused(); + event Unpaused(); + + //test pause and unpause + function testPauseAndUnpause() public { + impl.test(); + + vm.expectEmit(address(impl)); + emit Paused(); + impl.pause(); + assertTrue(impl.paused()); + + vm.expectRevert(PausableNonReentrant.IsPaused.selector); + impl.test(); + + vm.expectEmit(address(impl)); + emit Unpaused(); + impl.unpause(); + assertFalse(impl.paused()); + + impl.test(); + } + + //test not owner + function testNotOwner() public { + vm.prank(address(0x5)); + vm.expectRevert(Ownable.Unauthorized.selector); + impl.pause(); + + impl.pause(); + + vm.expectRevert(Ownable.Unauthorized.selector); + vm.prank(address(0x5)); + impl.unpause(); + } + + //test reentrancy + function testReentrancy() public { + impl.testReentrancy(); + } + + //test unpause when not paused, prevents owner from overriding lock + function testUnpauseWhenNotPaused() public { + vm.expectRevert(PausableNonReentrant.NotPaused.selector); + impl.unpause(); + } + + //fallback for reentrancy test + fallback() external { + //test reentrancy guard + vm.expectRevert(PausableNonReentrant.IsLocked.selector); + impl.testReentrancy(); + + //test pause during reentrancy, prevents owner from overriding lock + vm.expectRevert(PausableNonReentrant.IsLocked.selector); + impl.pause(); + } +} + +contract PausableNonReentrantImpl is PausableNonReentrant { + constructor() { + _initializeOwner(msg.sender); + } + + function test() public pausableNonReentrant {} + + function testReentrancy() public pausableNonReentrant { + (bool success,) = msg.sender.call(""); + if (!success) { + revert("Call failed"); + } + } +} diff --git a/test/utils/MockCall.sol b/test/utils/MockCall.sol index 11db546b..b15ae2a8 100644 --- a/test/utils/MockCall.sol +++ b/test/utils/MockCall.sol @@ -1,15 +1,30 @@ pragma solidity ^0.8.17; import {ItemType, SpentItem, ReceivedItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; +import {AdditionalTransfer} from "starport-core/lib/StarPortLib.sol"; import {TestBase} from "forge-std/Test.sol"; import {SettlementHook} from "src/hooks/SettlementHook.sol"; import {SettlementHandler} from "src/handlers/SettlementHandler.sol"; +import {Pricing} from "src/pricing/Pricing.sol"; abstract contract MockCall is TestBase { function mockHookCall(address hook, bool status) public { vm.mockCall(hook, abi.encodeWithSelector(SettlementHook.isActive.selector), abi.encode(status)); } + function mockIsValidRefinanceCall( + address pricing, + SpentItem[] memory considerationPayment, + SpentItem[] memory carryPayment, + AdditionalTransfer[] memory additionalTransfers + ) public { + vm.mockCall( + pricing, + abi.encodeWithSelector(Pricing.isValidRefinance.selector), + abi.encode(considerationPayment, carryPayment, additionalTransfers) + ); + } + function mockHandlerCall(address handler, ReceivedItem[] memory receivedItems, address authorized) public { vm.mockCall( handler,