diff --git a/.gas-snapshot b/.gas-snapshot index 2ed265b7..f97179ba 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,85 +1,105 @@ -DiffFuzzTestStarPortLib:testSpentToReceived((uint8,address,uint256,uint256)[]) (runs: 256, μ: 880466, ~: 883301) -DiffFuzzTestStarPortLib:testUnboundSpentToReceived((uint8,address,uint256,uint256)[]) (runs: 256, μ: 232905, ~: 237832) -TestAstariaV1Loan:testNewLoanERC721CollateralDefaultTermsRecallBase() (gas: 1039242) -TestAstariaV1Loan:testNewLoanERC721CollateralDefaultTermsRecallLender() (gas: 691152) -TestAstariaV1Loan:testNewLoanERC721CollateralDefaultTermsRecallLiquidation() (gas: 770721) -TestCustodian:testCannotLazyMintTwice() (gas: 76597) +DiffFuzzTestStarPortLib:testSpentToReceived((uint8,address,uint256,uint256)[]) (runs: 256, μ: 880501, ~: 882340) +DiffFuzzTestStarPortLib:testUnboundSpentToReceived((uint8,address,uint256,uint256)[]) (runs: 256, μ: 232899, ~: 237832) +TestAstariaV1Loan:testNewLoanERC721CollateralDefaultTermsRecallBase() (gas: 1041770) +TestAstariaV1Loan:testNewLoanERC721CollateralDefaultTermsRecallLender() (gas: 693528) +TestAstariaV1Loan:testNewLoanERC721CollateralDefaultTermsRecallLiquidation() (gas: 773097) +TestCustodian:testCannotLazyMintTwice() (gas: 76686) TestCustodian:testCannotMintInvalidLoanInvalidCustodian() (gas: 66883) -TestCustodian:testCannotMintInvalidLoanValidCustodian() (gas: 72422) +TestCustodian:testCannotMintInvalidLoanValidCustodian() (gas: 72511) TestCustodian:testCustodySelector() (gas: 2543980) TestCustodian:testDefaultCustodySelectorRevert() (gas: 70105) TestCustodian:testGenerateOrderInvalidHandlerExecution() (gas: 132855) -TestCustodian:testGenerateOrderRepay() (gas: 164375) -TestCustodian:testGenerateOrderRepayAsRepayApprovedBorrower() (gas: 190022) -TestCustodian:testGenerateOrderRepayERC1155AndERC20AndNative() (gas: 843710) -TestCustodian:testGenerateOrderRepayERC1155AndERC20AndNativeHandlerAuthorized() (gas: 785342) -TestCustodian:testGenerateOrderRepayERC1155WithRevert() (gas: 518146) +TestCustodian:testGenerateOrderRepay() (gas: 164353) +TestCustodian:testGenerateOrderRepayAsRepayApprovedBorrower() (gas: 190000) +TestCustodian:testGenerateOrderRepayERC1155AndERC20AndNative() (gas: 848490) +TestCustodian:testGenerateOrderRepayERC1155AndERC20AndNativeHandlerAuthorized() (gas: 790122) +TestCustodian:testGenerateOrderRepayERC1155WithRevert() (gas: 520526) TestCustodian:testGenerateOrderRepayInvalidHookAddress() (gas: 90249) TestCustodian:testGenerateOrderRepayInvalidHookReturnType() (gas: 84653) TestCustodian:testGenerateOrderRepayNotBorrower() (gas: 96449) -TestCustodian:testGenerateOrderSettlement() (gas: 151381) -TestCustodian:testGenerateOrderSettlementHandlerAuthorized() (gas: 160421) -TestCustodian:testGenerateOrderSettlementNoActiveLoan() (gas: 155620) +TestCustodian:testGenerateOrderSettlement() (gas: 151359) +TestCustodian:testGenerateOrderSettlementHandlerAuthorized() (gas: 160399) +TestCustodian:testGenerateOrderSettlementNoActiveLoan() (gas: 155598) TestCustodian:testGenerateOrderSettlementUnauthorized() (gas: 94219) TestCustodian:testGetBorrower() (gas: 76234) -TestCustodian:testInvalidAction() (gas: 114400) -TestCustodian:testInvalidActionRepayInActiveLoan() (gas: 117327) -TestCustodian:testInvalidActionSettleActiveLoan() (gas: 117287) +TestCustodian:testInvalidAction() (gas: 114489) +TestCustodian:testInvalidActionRepayInActiveLoan() (gas: 117416) +TestCustodian:testInvalidActionSettleActiveLoan() (gas: 117376) TestCustodian:testName() (gas: 7120) TestCustodian:testNonPayableFunctions() (gas: 225770) TestCustodian:testOnlySeaport() (gas: 17917) TestCustodian:testPayableFunctions() (gas: 41689) -TestCustodian:testPreviewOrderNoActiveLoan() (gas: 98687) -TestCustodian:testPreviewOrderRepay() (gas: 208128) -TestCustodian:testPreviewOrderSettlement() (gas: 182676) -TestCustodian:testPreviewOrderSettlementInvalidFufliller() (gas: 100488) -TestCustodian:testPreviewOrderSettlementInvalidRepayer() (gas: 106646) -TestCustodian:testRatifyOrder() (gas: 170218) +TestCustodian:testPreviewOrderNoActiveLoan() (gas: 98776) +TestCustodian:testPreviewOrderRepay() (gas: 208195) +TestCustodian:testPreviewOrderSettlement() (gas: 182743) +TestCustodian:testPreviewOrderSettlementInvalidFufliller() (gas: 100577) +TestCustodian:testPreviewOrderSettlementInvalidRepayer() (gas: 106735) +TestCustodian:testRatifyOrder() (gas: 170196) TestCustodian:testSeaportMetadata() (gas: 8567) TestCustodian:testSetRepayApproval() (gas: 37883) TestCustodian:testSupportsInterface() (gas: 9428) TestCustodian:testSymbol() (gas: 7149) TestCustodian:testTokenURI() (gas: 64839) TestCustodian:testTokenURIInvalidLoan() (gas: 13218) -TestLoanCombinations:testLoan20For721SimpleInterestDutchFixedRepay() (gas: 530961) -TestLoanCombinations:testLoan20for20SimpleInterestDutchFixedRepay() (gas: 499798) -TestLoanCombinations:testLoan721for20SimpleInterestDutchFixedRepay() (gas: 549718) -TestLoanCombinations:testLoanAstariaSettlementRepay() (gas: 539542) -TestLoanCombinations:testLoanSimpleInterestEnglishFixed() (gas: 544202) -TestLoanManager:testAdditionalTransfers() (gas: 293098) -TestLoanManager:testCannotIssueSameLoanTwice() (gas: 331317) +TestLoanCombinations:testLoan20For721SimpleInterestDutchFixedRepay() (gas: 533373) +TestLoanCombinations:testLoan20for20SimpleInterestDutchFixedRepay() (gas: 502230) +TestLoanCombinations:testLoan721for20SimpleInterestDutchFixedRepay() (gas: 552096) +TestLoanCombinations:testLoanAstariaSettlementRepay() (gas: 541954) +TestLoanCombinations:testLoanSimpleInterestEnglishFixed() (gas: 546614) +TestLoanManager:testAdditionalTransfers() (gas: 293327) +TestLoanManager:testCannotIssueSameLoanTwice() (gas: 331775) +TestLoanManager:testCannotOriginateWhilePaused() (gas: 87923) TestLoanManager:testCannotSettleInvalidLoan() (gas: 72594) -TestLoanManager:testCannotSettleUnlessValidCustodian() (gas: 68772) -TestLoanManager:testCaveatEnforcerRevert() (gas: 116691) -TestLoanManager:testDefaultFeeRake() (gas: 350066) -TestLoanManager:testExoticDebtWithNoCaveatsNotAsBorrower() (gas: 342790) -TestLoanManager:testInitializedFlagSetProperly() (gas: 65252) -TestLoanManager:testInvalidAmountCollateral() (gas: 152889) -TestLoanManager:testInvalidAmountCollateral721() (gas: 152987) -TestLoanManager:testInvalidAmountDebt() (gas: 177200) -TestLoanManager:testInvalidItemType() (gas: 138937) -TestLoanManager:testInvalidTransferLengthCollateral() (gas: 161128) -TestLoanManager:testInvalidTransferLengthDebt() (gas: 165661) -TestLoanManager:testIssued() (gas: 67055) +TestLoanManager:testCannotSettleUnlessValidCustodian() (gas: 68750) +TestLoanManager:testCaveatEnforcerRevert() (gas: 119155) +TestLoanManager:testDefaultFeeRake() (gas: 352730) +TestLoanManager:testExoticDebtWithNoCaveatsNotAsBorrower() (gas: 342979) +TestLoanManager:testInitializedFlagSetProperly() (gas: 65262) +TestLoanManager:testInvalidAmountCollateral() (gas: 153078) +TestLoanManager:testInvalidAmountCollateral721() (gas: 153221) +TestLoanManager:testInvalidAmountDebt() (gas: 177389) +TestLoanManager:testInvalidIdentifierDebt() (gas: 197383) +TestLoanManager:testInvalidItemType() (gas: 139126) +TestLoanManager:testInvalidTransferLengthCollateral() (gas: 161295) +TestLoanManager:testInvalidTransferLengthDebt() (gas: 165838) +TestLoanManager:testIssued() (gas: 67144) TestLoanManager:testName() (gas: 7184) -TestLoanManager:testNonDefaultCustodianCustodyCallFails() (gas: 190232) -TestLoanManager:testNonDefaultCustodianCustodyCallSuccess() (gas: 258505) -TestLoanManager:testNonPayableFunctions() (gas: 173085) -TestLoanManager:testOverrideFeeRake() (gas: 343904) +TestLoanManager:testNonDefaultCustodianCustodyCallFails() (gas: 190465) +TestLoanManager:testNonDefaultCustodianCustodyCallSuccess() (gas: 258714) +TestLoanManager:testNonPayableFunctions() (gas: 175555) +TestLoanManager:testOverrideFeeRake() (gas: 346469) +TestLoanManager:testPause() (gas: 34222) TestLoanManager:testSupportsInterface() (gas: 9181) -TestLoanManager:testSymbol() (gas: 7127) -TestLoanManager:testTokenNoCodeCollateral() (gas: 137709) -TestLoanManager:testTokenNoCodeDebt() (gas: 170767) -TestLoanManager:testTokenURI() (gas: 64914) +TestLoanManager:testSymbol() (gas: 7235) +TestLoanManager:testTokenNoCodeCollateral() (gas: 137898) +TestLoanManager:testTokenNoCodeDebt() (gas: 170934) +TestLoanManager:testTokenURI() (gas: 64892) TestLoanManager:testTokenURIInvalidLoan() (gas: 13244) -TestLoanManager:testTransferFromFail() (gas: 80088) -TestNewLoan:testBuyNowPayLater() (gas: 208) -TestNewLoan:testNewLoanERC721CollateralDefaultTerms2() (gas: 389485) +TestLoanManager:testTransferFromFail() (gas: 80176) +TestLoanManager:testUnPause() (gas: 14291) +TestNewLoan:testBuyNowPayLater() (gas: 2831964) +TestNewLoan:testNewLoanERC721CollateralDefaultTerms2() (gas: 392402) TestNewLoan:testNewLoanERC721CollateralLessDebtThanOffered() (gas: 2259) TestNewLoan:testNewLoanRefinanceNew() (gas: 207) +TestNewLoan:testNewLoanViaOriginatorBorrowerApprovalAndLenderApproval() (gas: 299336) +TestNewLoan:testNewLoanViaOriginatorLenderApproval() (gas: 354219) TestNewLoan:testSettleLoan() (gas: 163) TestRefStarPortLib:testSpentToReceived() (gas: 13315) TestRefStarPortLib:testValidateSalt(address,bytes32) (runs: 256, μ: 33865, ~: 33865) TestRepayLoan:testRepayLoan() (gas: 207) TestStarPortLib:testSpentToReceived() (gas: 13315) -TestStarPortLib:testValidateSalt(address,bytes32) (runs: 256, μ: 33865, ~: 33865) \ No newline at end of file +TestStarPortLib:testValidateSalt(address,bytes32) (runs: 256, μ: 33865, ~: 33865) +TestStrategistOriginator:testEncodeWithAccountCounter() (gas: 12307) +TestStrategistOriginator:testGetStrategistData() (gas: 1473217) +TestStrategistOriginator:testIncrementCounterAsStrategist() (gas: 18676) +TestStrategistOriginator:testIncrementCounterNotAuthorized() (gas: 13467) +TestStrategistOriginator:testInvalidCollateral() (gas: 204416) +TestStrategistOriginator:testInvalidDeadline() (gas: 210145) +TestStrategistOriginator:testInvalidDebt() (gas: 206100) +TestStrategistOriginator:testInvalidDebtAmountAskingMoreThanOffered() (gas: 206548) +TestStrategistOriginator:testInvalidDebtAmountOfferingZero() (gas: 186903) +TestStrategistOriginator:testInvalidDebtAmountRequestingZero() (gas: 206801) +TestStrategistOriginator:testInvalidDebtLength() (gas: 205428) +TestStrategistOriginator:testInvalidOffer() (gas: 396592) +TestStrategistOriginator:testInvalidSigner() (gas: 208639) +TestStrategistOriginator:testSetStrategist() (gas: 17818) \ No newline at end of file diff --git a/foundry.toml b/foundry.toml index 4ff40c48..49315808 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,6 +1,7 @@ [profile.default] +#cancun = true src = "src" out = "out" libs = ["lib"] - +#solc = "./bin/solc" # See more config options https://github.com/foundry-rs/foundry/tree/master/config \ No newline at end of file diff --git a/src/BNPLHelper.sol b/src/BNPLHelper.sol new file mode 100644 index 00000000..3cce91d5 --- /dev/null +++ b/src/BNPLHelper.sol @@ -0,0 +1,138 @@ +pragma solidity ^0.8.17; + +import { + OfferItem, + SpentItem, + ConsiderationItem, + AdvancedOrder, + OrderParameters, + CriteriaResolver, + ItemType, + Fulfillment +} from "seaport-types/src/lib/ConsiderationStructs.sol"; +import {ConduitTransfer, ConduitItemType} from "seaport-types/src/conduit/lib/ConduitStructs.sol"; + +import {Seaport} from "seaport/contracts/Seaport.sol"; +import {LoanManager} from "./LoanManager.sol"; +import {CaveatEnforcer} from "./enforcers/CaveatEnforcer.sol"; +import "forge-std/console.sol"; + +interface IVault { + function flashLoan( + IFlashLoanRecipient recipient, + address[] calldata tokens, + uint256[] calldata amounts, + bytes memory userData + ) external; +} + +interface IFlashLoanRecipient { + function receiveFlashLoan( + address[] calldata tokens, + uint256[] calldata amounts, + uint256[] calldata feeAmounts, + bytes memory userData + ) external; +} + +interface IWETH9 { + function withdraw(uint256) external; +} + +interface ERC20 { + function transfer(address, uint256) external returns (bool); +} + +// fulfiller + +contract BNPLHelper is IFlashLoanRecipient { + address private constant vault = address(0xBA12222222228d8Ba445958a75a0704d566BF2C8); + IWETH9 private immutable WETH; + bytes32 private activeUserDataHash; + + constructor(address WETH_) { + WETH = IWETH9(WETH_); + } + + struct Execution { + address lm; + address seaport; + address borrower; + CaveatEnforcer.CaveatWithApproval borrowerCaveat; + CaveatEnforcer.CaveatWithApproval lenderCaveat; + LoanManager.Loan loan; + AdvancedOrder[] orders; + CriteriaResolver[] resolvers; + Fulfillment[] fulfillments; + } + + error SenderNotSelf(); + error DoNotSendETH(); + error InvalidUserDataProvided(); + + function makeFlashLoan( + address[] calldata tokens, + uint256[] calldata amounts, + // uint256[] calldata userProvidedAmounts, + bytes calldata userData + ) external { + // assembly { + // // Compute the hash of userData + // let dataHash := keccak256(userData.offset, calldatasize()) + // + // // Store the hash in the activeUserDataHash state variable + // tstore(activeUserDataHash.slot, dataHash) + // } + activeUserDataHash = keccak256(userData); + + IVault(vault).flashLoan(this, tokens, amounts, userData); + } + + function receiveFlashLoan( + address[] calldata tokens, // are all erc20s + uint256[] calldata amounts, + uint256[] calldata feeAmounts, + bytes calldata userData + ) external override { + require(msg.sender == vault); + + if (activeUserDataHash != keccak256(userData)) { + revert InvalidUserDataProvided(); + } + delete activeUserDataHash; + + Execution memory execution = abi.decode(userData, (Execution)); + //approve seaport + for (uint256 i = 0; i < tokens.length;) { + ERC20(tokens[i]).transfer(execution.borrower, amounts[i]); + unchecked { + ++i; + } + } + Seaport(payable(execution.seaport)).matchAdvancedOrders( + execution.orders, execution.resolvers, execution.fulfillments, execution.borrower + ); + + ConduitTransfer[] memory transfers = new ConduitTransfer[](tokens.length); + for (uint256 i = 0; i < tokens.length;) { + transfers[i] = ConduitTransfer({ + itemType: ConduitItemType.ERC20, + identifier: 0, + token: tokens[0], + from: execution.borrower, + to: vault, + amount: amounts[0] + feeAmounts[0] + }); + unchecked { + ++i; + } + } + LoanManager(execution.lm).originate(transfers, execution.borrowerCaveat, execution.lenderCaveat, execution.loan); + } + + receive() external payable { + if (msg.sender != address(WETH)) { + revert DoNotSendETH(); + } + } +} diff --git a/src/ConduitHelper.sol b/src/ConduitHelper.sol index 312d2b45..e6f40901 100644 --- a/src/ConduitHelper.sol +++ b/src/ConduitHelper.sol @@ -18,7 +18,7 @@ * * Chainworks Labs */ -pragma solidity =0.8.17; +pragma solidity ^0.8.17; import {ItemType, OfferItem, Schema, SpentItem, ReceivedItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; diff --git a/src/Custodian.sol b/src/Custodian.sol index 5f2da6e0..08edd17c 100644 --- a/src/Custodian.sol +++ b/src/Custodian.sol @@ -18,7 +18,7 @@ * * Chainworks Labs */ -pragma solidity =0.8.17; +pragma solidity ^0.8.17; import {ERC721} from "solady/src/tokens/ERC721.sol"; import {ERC20} from "solady/src/tokens/ERC20.sol"; diff --git a/src/LoanManager.sol b/src/LoanManager.sol index ab800512..89f5c913 100644 --- a/src/LoanManager.sol +++ b/src/LoanManager.sol @@ -18,7 +18,7 @@ * * Chainworks Labs */ -pragma solidity =0.8.17; +pragma solidity ^0.8.17; import {ERC721} from "solady/src/tokens/ERC721.sol"; import {ERC20} from "solady/src/tokens/ERC20.sol"; @@ -57,7 +57,6 @@ contract LoanManager is Ownable, ERC721 { 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; @@ -70,20 +69,28 @@ 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; - uint96 public defaultFeeRake; + uint88 public defaultFeeRake; mapping(address => mapping(bytes32 => bool)) public invalidHashes; - mapping(address => mapping(address => bool)) public approvals; - mapping(address => uint256) public caveatNonces; - //contract to token //fee rake - mapping(address => Fee) public feeOverride; + // mapping(address => mapping(address => bool)) public approvals; + enum ApprovalType { + NOTHING, + BORROWER, + LENDER + } enum FieldFlags { INITIALIZED, ACTIVE, INACTIVE } + mapping(address => mapping(address => ApprovalType)) public approvals; + mapping(address => uint256) public caveatNonces; + //contract to token //fee rake + mapping(address => Fee) public feeOverride; + struct Terms { address hook; //the address of the hookmodule bytes hookData; //bytes encoded hook data @@ -104,54 +111,34 @@ contract LoanManager is Ownable, ERC721 { Terms terms; //the actionable terms of the loan } - struct Caveat { - address enforcer; - bytes terms; - } - - struct Obligation { - address custodian; - SpentItem[] debt; - address originator; - address borrower; - bytes32 salt; - Caveat[] caveats; - bytes details; - bytes approval; - } - struct Fee { bool enabled; - uint96 amount; + uint88 amount; } event Close(uint256 loanId); event Open(uint256 loanId, LoanManager.Loan loan); - event SeaportCompatibleContractDeployed(); + event Paused(); + event UnPaused(); - error CannotTransferLoans(); - error ConduitTransferError(); - error InvalidAction(); - error InvalidConduit(); error InvalidRefinance(); error InvalidCustodian(); error InvalidLoan(); error InvalidItemAmount(); + error InvalidItemIdentifier(); //must be zero for ERC20's error InvalidItemTokenNoCode(); + error InvalidItemType(); error InvalidTransferLength(); - // error InvalidNoRefinanceConsideration(); + error CannotTransferLoans(); + error ConduitTransferError(); error LoanExists(); error NotLoanCustodian(); - error NotPayingFees(); error NotSeaport(); - error NotEnteredViaSeaport(); - error NativeAssetsNotSupported(); - error HashAlreadyInvalidated(); - error InvalidItemType(); error UnauthorizedAdditionalTransferIncluded(); error InvalidCaveatSigner(); error MalformedRefinance(); + error IsPaused(); constructor(ConsiderationInterface seaport_) { address custodian = address(new Custodian(this, seaport_)); @@ -165,32 +152,75 @@ contract LoanManager is Ownable, ERC721 { DEFAULT_CUSTODIAN_CODE_HASH = defaultCustodianCodeHash; _DOMAIN_SEPARATOR = keccak256(abi.encode(EIP_DOMAIN, VERSION, block.chainid, address(this))); _initializeOwner(msg.sender); - emit SeaportCompatibleContractDeployed(); + } + // + // function setApproval(address who, bool approved) external { + // assembly { + // // Compute the storage slot of approvals[msg.sender] + // let slot := keccak256(add(mul(caller(), 0x1000000000000000000000000), mload(approvals.slot)), 0x20) + // + // // Compute the storage slot of approvals[msg.sender][who] + // slot := keccak256(add(who, slot), 0x20) + // + // // Update the value at the computed storage slot + // sstore(slot, approved) + // } + // } + + // function setApprovalTransient(address who) external { + // assembly { + // // Compute the storage slot of approvals[msg.sender] + // let slot := keccak256(add(mul(caller(), 0x1000000000000000000000000), mload(approvals.slot)), 0x20) + // + // // Compute the storage slot of approvals[msg.sender][who] + // slot := keccak256(add(who, slot), 0x20) + // + // // Update the value at the computed storage slot + // tstore(slot, 1) + // } + // } + + function setOriginateApproval(address who, ApprovalType approvalType) external { + approvals[msg.sender][who] = approvalType; } function transferFrom(address from, address to, uint256 tokenId) public payable override { revert CannotTransferLoans(); } + function pause() external onlyOwner { + paused = true; + emit Paused(); + } + + function unPause() external onlyOwner { + paused = false; + emit UnPaused(); + } + function originate( ConduitTransfer[] calldata additionalTransfers, CaveatEnforcer.CaveatWithApproval calldata borrowerCaveat, CaveatEnforcer.CaveatWithApproval calldata lenderCaveat, LoanManager.Loan memory loan ) external payable { + if (paused) { + revert IsPaused(); + } //cache the addresses address borrower = loan.borrower; address issuer = loan.issuer; address feeRecipient = feeTo; - if (msg.sender != loan.borrower) { + if (msg.sender != loan.borrower && !(approvals[borrower][msg.sender] == ApprovalType.BORROWER)) { _validateAndEnforceCaveats(borrowerCaveat, borrower, additionalTransfers, loan); } - if (msg.sender != issuer && !approvals[issuer][msg.sender]) { + if (msg.sender != issuer && !(approvals[issuer][msg.sender] == ApprovalType.LENDER)) { _validateAndEnforceCaveats(lenderCaveat, issuer, additionalTransfers, loan); } _transferSpentItems(loan.collateral, borrower, loan.custodian); + _callCustody(loan); if (feeRecipient == address(0)) { _transferSpentItems(loan.debt, issuer, borrower); @@ -217,6 +247,9 @@ contract LoanManager is Ownable, ERC721 { LoanManager.Loan memory loan, bytes calldata pricingData ) external { + if (paused) { + revert IsPaused(); + } ( SpentItem[] memory considerationPayment, SpentItem[] memory carryPayment, @@ -233,7 +266,7 @@ contract LoanManager is Ownable, ERC721 { loan.originator = address(0); loan.start = 0; - if (msg.sender != loan.issuer && !approvals[loan.issuer][msg.sender]) { + if (msg.sender != loan.issuer && !(approvals[loan.issuer][msg.sender] == ApprovalType.LENDER)) { _validateAndEnforceCaveats(lenderCaveat, loan.issuer, additionalTransfers, loan); } @@ -410,7 +443,9 @@ contract LoanManager is Ownable, ERC721 { } if (amount > 0) { if (itemType == ItemType.ERC20) { - // erc20 transfer + if (identifier > 0) { + revert InvalidItemIdentifier(); + } SafeTransferLib.safeTransferFrom(token, from, to, amount); } else if (itemType == ItemType.ERC721) { // erc721 transfer @@ -567,7 +602,7 @@ contract LoanManager is Ownable, ERC721 { * @param feeTo_ The feeToAddress * @param defaultFeeRake_ the default fee rake in WAD denomination(1e17 = 10%) */ - function setFeeData(address feeTo_, uint96 defaultFeeRake_) external onlyOwner { + function setFeeData(address feeTo_, uint88 defaultFeeRake_) external onlyOwner { feeTo = feeTo_; defaultFeeRake = defaultFeeRake_; } @@ -578,7 +613,7 @@ contract LoanManager is Ownable, ERC721 { * @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 { + function setFeeOverride(address token, uint88 overrideValue) external onlyOwner { feeOverride[token].enabled = true; feeOverride[token].amount = overrideValue; } diff --git a/src/custodians/AAVEPoolCustodian.sol b/src/custodians/AAVEPoolCustodian.sol index d75285f3..177a81c3 100644 --- a/src/custodians/AAVEPoolCustodian.sol +++ b/src/custodians/AAVEPoolCustodian.sol @@ -1,4 +1,4 @@ -//pragma solidity =0.8.17; +//pragma solidity ^0.8.17;; // //import {ERC20} from "solady/src/tokens/ERC20.sol"; //import "../Custodian.sol"; diff --git a/src/enforcers/BorrowerEnforcer.sol b/src/enforcers/BorrowerEnforcer.sol index eab66f95..ae504e39 100644 --- a/src/enforcers/BorrowerEnforcer.sol +++ b/src/enforcers/BorrowerEnforcer.sol @@ -1,8 +1,9 @@ -pragma solidity =0.8.17; +pragma solidity ^0.8.17; import {CaveatEnforcer} from "starport-core/enforcers/CaveatEnforcer.sol"; import {ConduitTransfer} from "seaport-types/src/conduit/lib/ConduitStructs.sol"; import {LoanManager} from "starport-core/LoanManager.sol"; +import {ConsiderationInterface} from "seaport-types/src/interfaces/ConsiderationInterface.sol"; contract BorrowerEnforcer is CaveatEnforcer { error BorrowerOnlyEnforcer(); diff --git a/src/enforcers/BorrowerEnforcerBNPL.sol b/src/enforcers/BorrowerEnforcerBNPL.sol new file mode 100644 index 00000000..b79ecdb2 --- /dev/null +++ b/src/enforcers/BorrowerEnforcerBNPL.sol @@ -0,0 +1,64 @@ +pragma solidity ^0.8.17; + +import {CaveatEnforcer} from "starport-core/enforcers/CaveatEnforcer.sol"; +import {ConduitTransfer} from "seaport-types/src/conduit/lib/ConduitStructs.sol"; +import {LoanManager} from "starport-core/LoanManager.sol"; +import {ConsiderationInterface} from "seaport-types/src/interfaces/ConsiderationInterface.sol"; + +contract BorrowerEnforcerBNPL is CaveatEnforcer { + error BorrowerOnlyEnforcer(); + error InvalidLoanTerms(); + error InvalidAdditionalTransfer(); + + error OrderInvalid(); + + struct Details { + LoanManager.Loan loan; + address seaport; + bytes32 offerHash; + ConduitTransfer additionalTransfer; + } + + function validate( + ConduitTransfer[] calldata additionalTransfers, + LoanManager.Loan calldata loan, + bytes calldata caveatData + ) public view virtual override { + bytes32 loanHash = keccak256(abi.encode(loan)); + + Details memory details = abi.decode(caveatData, (Details)); + if (details.loan.borrower != loan.borrower) { + revert BorrowerOnlyEnforcer(); + } + details.loan.issuer = loan.issuer; + + if (loanHash != keccak256(abi.encode(details.loan))) { + revert InvalidLoanTerms(); + } + + if (additionalTransfers.length > 0) { + if (details.offerHash != bytes32(0)) { + (bool isValidated, bool isCancelled, uint256 numerator, uint256 denominator) = + ConsiderationInterface(details.seaport).getOrderStatus(details.offerHash); + + if (isCancelled || !isValidated) { + revert OrderInvalid(); + } + + if (additionalTransfers.length > 1) { + revert InvalidAdditionalTransfer(); + } + if ( + additionalTransfers[0].itemType != details.additionalTransfer.itemType + || additionalTransfers[0].identifier != details.additionalTransfer.identifier + || additionalTransfers[0].amount > details.additionalTransfer.amount + || additionalTransfers[0].token != details.additionalTransfer.token + ) { + revert InvalidAdditionalTransfer(); + } + } else { + revert InvalidAdditionalTransfer(); + } + } + } +} diff --git a/src/enforcers/CaveatEnforcer.sol b/src/enforcers/CaveatEnforcer.sol index ffce25fc..1766ffa1 100644 --- a/src/enforcers/CaveatEnforcer.sol +++ b/src/enforcers/CaveatEnforcer.sol @@ -1,5 +1,4 @@ -pragma solidity =0.8.17; -// import {ReceivedItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; +pragma solidity ^0.8.17; import {ConduitTransfer} from "seaport-types/src/conduit/lib/ConduitStructs.sol"; import {LoanManager} from "starport-core/LoanManager.sol"; diff --git a/src/enforcers/LenderEnforcer.sol b/src/enforcers/LenderEnforcer.sol index c1b79710..0b6ed7fc 100644 --- a/src/enforcers/LenderEnforcer.sol +++ b/src/enforcers/LenderEnforcer.sol @@ -1,4 +1,4 @@ -pragma solidity =0.8.17; +pragma solidity ^0.8.17; import {CaveatEnforcer} from "starport-core/enforcers/CaveatEnforcer.sol"; import {ConduitTransfer} from "seaport-types/src/conduit/lib/ConduitStructs.sol"; @@ -18,8 +18,6 @@ contract LenderEnforcer is CaveatEnforcer { LoanManager.Loan calldata loan, bytes calldata caveatData ) public view virtual override { - // bytes32 loanHash = keccak256(abi.encode(loan)); - Details memory details = abi.decode(caveatData, (Details)); if (details.loan.issuer != loan.issuer) revert LenderOnlyEnforcer(); details.loan.borrower = loan.borrower; diff --git a/src/handlers/AstariaV1SettlementHandler.sol b/src/handlers/AstariaV1SettlementHandler.sol index 196227e5..7a9d2500 100644 --- a/src/handlers/AstariaV1SettlementHandler.sol +++ b/src/handlers/AstariaV1SettlementHandler.sol @@ -1,4 +1,4 @@ -pragma solidity =0.8.17; +pragma solidity ^0.8.17; import {LoanManager, SpentItem, ReceivedItem, SettlementHandler} from "starport-core/handlers/SettlementHandler.sol"; import {BaseHook} from "starport-core/hooks/BaseHook.sol"; diff --git a/src/handlers/DutchAuctionHandler.sol b/src/handlers/DutchAuctionHandler.sol index 2504980c..bbc106f1 100644 --- a/src/handlers/DutchAuctionHandler.sol +++ b/src/handlers/DutchAuctionHandler.sol @@ -1,4 +1,4 @@ -pragma solidity =0.8.17; +pragma solidity ^0.8.17; import { ItemType, diff --git a/src/handlers/EnglishAuctionHandler.sol b/src/handlers/EnglishAuctionHandler.sol index f76b165c..27bfad48 100644 --- a/src/handlers/EnglishAuctionHandler.sol +++ b/src/handlers/EnglishAuctionHandler.sol @@ -1,4 +1,4 @@ -pragma solidity =0.8.17; +pragma solidity ^0.8.17; import {LoanManager} from "starport-core/LoanManager.sol"; import {ConduitTransfer, ConduitItemType} from "seaport-types/src/conduit/lib/ConduitStructs.sol"; diff --git a/src/handlers/FixedTermDutchAuctionHandler.sol b/src/handlers/FixedTermDutchAuctionHandler.sol index aae335d8..707fc5a9 100644 --- a/src/handlers/FixedTermDutchAuctionHandler.sol +++ b/src/handlers/FixedTermDutchAuctionHandler.sol @@ -1,4 +1,4 @@ -pragma solidity =0.8.17; +pragma solidity ^0.8.17; import {LoanManager, SpentItem, ReceivedItem, SettlementHandler} from "starport-core/handlers/SettlementHandler.sol"; import {BaseHook} from "starport-core/hooks/BaseHook.sol"; diff --git a/src/handlers/SettlementHandler.sol b/src/handlers/SettlementHandler.sol index 870f88b7..19000518 100644 --- a/src/handlers/SettlementHandler.sol +++ b/src/handlers/SettlementHandler.sol @@ -18,7 +18,7 @@ * * Chainworks Labs */ -pragma solidity =0.8.17; +pragma solidity ^0.8.17; import {LoanManager} from "starport-core/LoanManager.sol"; diff --git a/src/hh_helpers/MockERC20.sol b/src/hh_helpers/MockERC20.sol index 8f311695..2d34bdcf 100644 --- a/src/hh_helpers/MockERC20.sol +++ b/src/hh_helpers/MockERC20.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.17; +pragma solidity ^0.8.17; import {TestERC20} from "seaport/contracts/test/TestERC20.sol"; diff --git a/src/hh_helpers/MockERC721.sol b/src/hh_helpers/MockERC721.sol index 523e52fa..2e6e99e7 100644 --- a/src/hh_helpers/MockERC721.sol +++ b/src/hh_helpers/MockERC721.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.17; +pragma solidity ^0.8.17; import {TestERC721} from "seaport/contracts/test/TestERC721.sol"; diff --git a/src/hooks/AstariaV1SettlementHook.sol b/src/hooks/AstariaV1SettlementHook.sol index 8a61be97..3e592581 100644 --- a/src/hooks/AstariaV1SettlementHook.sol +++ b/src/hooks/AstariaV1SettlementHook.sol @@ -1,4 +1,4 @@ -pragma solidity =0.8.17; +pragma solidity ^0.8.17; import {LoanManager} from "starport-core/LoanManager.sol"; import {BaseRecall} from "starport-core/hooks/BaseRecall.sol"; diff --git a/src/hooks/BaseHook.sol b/src/hooks/BaseHook.sol index b6865cb5..48bd7d2c 100644 --- a/src/hooks/BaseHook.sol +++ b/src/hooks/BaseHook.sol @@ -18,7 +18,7 @@ * * Chainworks Labs */ -pragma solidity =0.8.17; +pragma solidity ^0.8.17; import {SettlementHook} from "starport-core/hooks/SettlementHook.sol"; import {LoanManager} from "starport-core/LoanManager.sol"; diff --git a/src/hooks/BaseRecall.sol b/src/hooks/BaseRecall.sol index a28f8e76..0a778af2 100644 --- a/src/hooks/BaseRecall.sol +++ b/src/hooks/BaseRecall.sol @@ -18,7 +18,7 @@ * * Chainworks Labs */ -pragma solidity =0.8.17; +pragma solidity ^0.8.17; import "forge-std/console2.sol"; diff --git a/src/hooks/FixedTermHook.sol b/src/hooks/FixedTermHook.sol index d2e0887b..c85039a4 100644 --- a/src/hooks/FixedTermHook.sol +++ b/src/hooks/FixedTermHook.sol @@ -1,4 +1,4 @@ -pragma solidity =0.8.17; +pragma solidity ^0.8.17; import {LoanManager} from "starport-core/LoanManager.sol"; import {SettlementHook} from "starport-core/hooks/SettlementHook.sol"; diff --git a/src/hooks/SettlementHook.sol b/src/hooks/SettlementHook.sol index 34657779..8747bbaa 100644 --- a/src/hooks/SettlementHook.sol +++ b/src/hooks/SettlementHook.sol @@ -18,7 +18,7 @@ * * Chainworks Labs */ -pragma solidity =0.8.17; +pragma solidity ^0.8.17; import {LoanManager} from "starport-core/LoanManager.sol"; diff --git a/src/interfaces/Lender.sol b/src/interfaces/Lender.sol index 2dd8c68f..15220422 100644 --- a/src/interfaces/Lender.sol +++ b/src/interfaces/Lender.sol @@ -1,4 +1,4 @@ -pragma solidity =0.8.17; +pragma solidity ^0.8.17; import "starport-core/LoanManager.sol"; diff --git a/src/interfaces/TokenReceiverInterface.sol b/src/interfaces/TokenReceiverInterface.sol index 7b18c01f..1b7cc9fd 100644 --- a/src/interfaces/TokenReceiverInterface.sol +++ b/src/interfaces/TokenReceiverInterface.sol @@ -1,4 +1,4 @@ -pragma solidity =0.8.17; +pragma solidity ^0.8.17; interface TokenReceiverInterface { function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data) diff --git a/src/originators/Originator.sol b/src/originators/Originator.sol index 77ec41c6..efb1e955 100644 --- a/src/originators/Originator.sol +++ b/src/originators/Originator.sol @@ -18,63 +18,24 @@ * * Chainworks Labs */ -pragma solidity =0.8.17; +pragma solidity ^0.8.17; import {LoanManager} from "starport-core/LoanManager.sol"; import {SpentItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; import {ConduitTransfer, ConduitItemType} from "seaport-types/src/conduit/lib/ConduitStructs.sol"; import {Ownable} from "solady/src/auth/Ownable.sol"; +import {CaveatEnforcer} from "starport-core/enforcers/CaveatEnforcer.sol"; abstract contract Originator is Ownable { - struct Response { - LoanManager.Terms terms; - address issuer; - } - struct Request { - address custodian; - address receiver; + address borrower; + CaveatEnforcer.CaveatWithApproval borrowerCaveat; SpentItem[] collateral; SpentItem[] debt; bytes details; bytes approval; } - event Origination(uint256 indexed loanId, address indexed issuer, bytes nlrDetails); - - function execute(Request calldata params) external virtual returns (Response memory response); - - function _packageTransfers(SpentItem[] memory loan, address borrower, address issuer) - internal - pure - returns (ConduitTransfer[] memory transfers) - { - uint256 i = 0; - transfers = new ConduitTransfer[](loan.length); - for (; i < loan.length;) { - ConduitItemType itemType; - SpentItem memory debt = loan[i]; - - assembly { - itemType := mload(debt) - switch itemType - case 1 {} - case 2 {} - case 3 {} - default { revert(0, 0) } //TODO: Update with error selector - InvalidContext(ContextErrors.INVALID_LOAN) - } - transfers[i] = ConduitTransfer({ - itemType: itemType, - from: issuer, - token: loan[i].token, - identifier: loan[i].identifier, - amount: loan[i].amount, - to: borrower - }); - unchecked { - ++i; - } - } - } + function originate(Request calldata params) external virtual; } diff --git a/src/originators/StrategistOriginator.sol b/src/originators/StrategistOriginator.sol index f484e929..77fbd483 100644 --- a/src/originators/StrategistOriginator.sol +++ b/src/originators/StrategistOriginator.sol @@ -18,7 +18,7 @@ * * Chainworks Labs */ -pragma solidity =0.8.17; +pragma solidity ^0.8.17; import {LoanManager} from "starport-core/LoanManager.sol"; @@ -31,6 +31,7 @@ import {ECDSA} from "solady/src/utils/ECDSA.sol"; import {SignatureCheckerLib} from "solady/src/utils/SignatureCheckerLib.sol"; import {Ownable} from "solady/src/auth/Ownable.sol"; import {Originator} from "starport-core/originators/Originator.sol"; +import {CaveatEnforcer} from "starport-core/enforcers/CaveatEnforcer.sol"; // Validator abstract contract that lays out the necessary structure and functions for the validator contract StrategistOriginator is Ownable, Originator { @@ -40,7 +41,6 @@ contract StrategistOriginator is Ownable, Originator { struct Details { address custodian; - address conduit; address issuer; uint256 deadline; Offer offer; @@ -53,17 +53,10 @@ contract StrategistOriginator is Ownable, Originator { SpentItem[] debt; } - event CounterUpdated(); + event CounterUpdated(uint256); event HashInvalidated(bytes32 hash); - modifier onlyLoanManager() { - if (msg.sender != address(LM)) { - revert NotLoanManager(); - } - _; - } - error NotLoanManager(); error NotAuthorized(); error InvalidDebt(); @@ -110,14 +103,6 @@ contract StrategistOriginator is Ownable, Originator { emit StrategistTransferred(newStrategist); } - function _buildResponse(Request calldata params, Details memory details) - internal - virtual - returns (Response memory response) - { - response = Response({terms: details.offer.terms, issuer: details.issuer}); - } - // Encode the data with the account's nonce for generating a signature function encodeWithAccountCounter(bytes32 contextHash) public view virtual returns (bytes memory) { bytes32 hash = keccak256(abi.encode(ORIGINATOR_DETAILS_TYPEHASH, _counter, contextHash)); @@ -135,11 +120,11 @@ contract StrategistOriginator is Ownable, Originator { } function incrementCounter() external { - if (msg.sender != strategist || msg.sender != owner()) { + if (msg.sender != strategist && msg.sender != owner()) { revert NotAuthorized(); } _counter += uint256(blockhash(block.number - 1) << 0x80); - emit CounterUpdated(); + emit CounterUpdated(_counter); } // Function to generate the domain separator for signatures @@ -147,17 +132,23 @@ contract StrategistOriginator is Ownable, Originator { return _DOMAIN_SEPARATOR; } - function execute(Request calldata params) - external - virtual - override - onlyLoanManager - returns (Response memory response) - { + function originate(Request calldata params) external virtual override { Details memory details = abi.decode(params.details, (Details)); _validateOffer(params, details); - _execute(params, details); - response = _buildResponse(params, details); + + LoanManager.Loan memory loan = LoanManager.Loan({ + start: uint256(0), // are set in the loan manager + originator: address(0), // are set in the loan manager + custodian: details.custodian, + issuer: details.issuer, + borrower: params.borrower, + collateral: params.collateral, + debt: params.debt, + terms: details.offer.terms + }); + + CaveatEnforcer.CaveatWithApproval memory le; + LM.originate(new ConduitTransfer[](0), params.borrowerCaveat, le, loan); } function _validateAsk(Request calldata request, Details memory details) internal virtual { @@ -167,7 +158,7 @@ contract StrategistOriginator is Ownable, Originator { //loop through collateral and check if the collateral is the same - for (uint256 i = 0; i < request.collateral.length;) { + for (uint256 i = 0; i < request.debt.length;) { if ( request.debt[i].itemType != details.offer.debt[i].itemType || request.debt[i].token != details.offer.debt[i].token @@ -191,9 +182,6 @@ contract StrategistOriginator is Ownable, Originator { function _validateOffer(Request calldata request, Details memory details) internal virtual { bytes32 contextHash = keccak256(request.details); _validateSignature(keccak256(encodeWithAccountCounter(keccak256(request.details))), request.approval); - if (request.custodian != details.custodian) { - revert InvalidCustodian(); - } if (request.debt.length != details.offer.debt.length) { revert InvalidDebtLength(); } @@ -212,15 +200,6 @@ contract StrategistOriginator is Ownable, Originator { } } - function _execute(Request calldata request, Details memory details) internal virtual { - if ( - ConduitInterface(details.conduit).execute(_packageTransfers(request.debt, request.receiver, details.issuer)) - != ConduitInterface.execute.selector - ) { - revert ConduitTransferError(); - } - } - function _validateSignature(bytes32 hash, bytes memory signature) internal view virtual { if (!SignatureCheckerLib.isValidSignatureNow(strategist, hash, signature)) { revert InvalidSigner(); diff --git a/src/pricing/AstariaV1Pricing.sol b/src/pricing/AstariaV1Pricing.sol index ea220a53..879f6f55 100644 --- a/src/pricing/AstariaV1Pricing.sol +++ b/src/pricing/AstariaV1Pricing.sol @@ -1,4 +1,4 @@ -pragma solidity =0.8.17; +pragma solidity ^0.8.17; import {LoanManager} from "starport-core/LoanManager.sol"; import {CompoundInterestPricing} from "starport-core/pricing/CompoundInterestPricing.sol"; diff --git a/src/pricing/BasePricing.sol b/src/pricing/BasePricing.sol index 7b37409f..17b79767 100644 --- a/src/pricing/BasePricing.sol +++ b/src/pricing/BasePricing.sol @@ -18,7 +18,7 @@ * * Chainworks Labs */ -pragma solidity =0.8.17; +pragma solidity ^0.8.17; import {LoanManager} from "starport-core/LoanManager.sol"; import {Pricing} from "starport-core/pricing/Pricing.sol"; diff --git a/src/pricing/BaseRecallPricing.sol b/src/pricing/BaseRecallPricing.sol index 67bed392..f4cd3366 100644 --- a/src/pricing/BaseRecallPricing.sol +++ b/src/pricing/BaseRecallPricing.sol @@ -18,7 +18,7 @@ * * Chainworks Labs */ -pragma solidity =0.8.17; +pragma solidity ^0.8.17; import {LoanManager} from "starport-core/LoanManager.sol"; import {BasePricing} from "starport-core/pricing/BasePricing.sol"; diff --git a/src/pricing/CompoundInterestPricing.sol b/src/pricing/CompoundInterestPricing.sol index bc4ebef8..ac88ec4f 100644 --- a/src/pricing/CompoundInterestPricing.sol +++ b/src/pricing/CompoundInterestPricing.sol @@ -1,4 +1,4 @@ -pragma solidity =0.8.17; +pragma solidity ^0.8.17; import {LoanManager} from "starport-core/LoanManager.sol"; import {BasePricing} from "starport-core/pricing/BasePricing.sol"; diff --git a/src/pricing/Pricing.sol b/src/pricing/Pricing.sol index 4e986313..c4d911fb 100644 --- a/src/pricing/Pricing.sol +++ b/src/pricing/Pricing.sol @@ -18,7 +18,7 @@ * * Chainworks Labs */ -pragma solidity =0.8.17; +pragma solidity ^0.8.17; import {LoanManager} from "starport-core/LoanManager.sol"; import {SpentItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; diff --git a/src/pricing/SimpleInterestPricing.sol b/src/pricing/SimpleInterestPricing.sol index 3b4f59eb..7b2202ab 100644 --- a/src/pricing/SimpleInterestPricing.sol +++ b/src/pricing/SimpleInterestPricing.sol @@ -18,7 +18,7 @@ * * Chainworks Labs */ -pragma solidity =0.8.17; +pragma solidity ^0.8.17; import {ReceivedItem, BasePricing} from "starport-core/pricing/BasePricing.sol"; import {FixedPointMathLib} from "solady/src/utils/FixedPointMathLib.sol"; diff --git a/src/scripts/Deploy.sol b/src/scripts/Deploy.sol index b9b5afdd..18beff8c 100644 --- a/src/scripts/Deploy.sol +++ b/src/scripts/Deploy.sol @@ -1,4 +1,4 @@ -//pragma solidity =0.8.17; +//pragma solidity ^0.8.17;; // //import "forge-std/Script.sol"; //import "../LoanManager.sol"; diff --git a/test/AstariaV1Test.sol b/test/AstariaV1Test.sol index 5bebc7e5..396921e4 100644 --- a/test/AstariaV1Test.sol +++ b/test/AstariaV1Test.sol @@ -1,4 +1,4 @@ -pragma solidity =0.8.17; +pragma solidity ^0.8.17; import "forge-std/console2.sol"; diff --git a/test/StarPortTest.sol b/test/StarPortTest.sol index b969415f..4192a471 100644 --- a/test/StarPortTest.sol +++ b/test/StarPortTest.sol @@ -1,4 +1,4 @@ -pragma solidity =0.8.17; +pragma solidity ^0.8.17; import "forge-std/Test.sol"; import {Vm} from "forge-std/Vm.sol"; @@ -66,7 +66,7 @@ import {Actions} from "starport-core/lib/StarPortLib.sol"; import {CaveatEnforcer} from "starport-core/enforcers/CaveatEnforcer.sol"; import {BorrowerEnforcer} from "starport-core/enforcers/BorrowerEnforcer.sol"; -import {LenderEnforcer} from "starport-core/enforcers/LenderEnforcer.sol"; +import {BorrowerEnforcerBNPL} from "starport-core/enforcers/BorrowerEnforcerBNPL.sol"; import {LenderEnforcer} from "starport-core/enforcers/LenderEnforcer.sol"; @@ -155,6 +155,8 @@ contract StarPortTest is BaseOrderTest { StrategistOriginator SO; BorrowerEnforcer borrowerEnforcer; + BorrowerEnforcerBNPL borrowerEnforcerBNPL; + LenderEnforcer lenderEnforcer; bytes32 conduitKeyRefinancer; @@ -221,8 +223,10 @@ contract StarPortTest is BaseOrderTest { erc721s[2].mint(lender.addr, 1); } borrowerEnforcer = new BorrowerEnforcer(); + borrowerEnforcerBNPL = new BorrowerEnforcerBNPL(); lenderEnforcer = new LenderEnforcer(); vm.label(address(borrowerEnforcer), "BorrowerEnforcer"); + vm.label(address(borrowerEnforcerBNPL), "BorrowerEnforcerBNPL"); vm.label(address(lenderEnforcer), "LenderEnforcer"); conduitKeyOne = bytes32(uint256(uint160(address(lender.addr))) << 96); @@ -275,12 +279,6 @@ contract StarPortTest is BaseOrderTest { // ConsiderationItem[] collateral20; SpentItem[] activeDebt; - struct NewLoanData { - address custodian; - LoanManager.Caveat[] caveats; - bytes details; - } - function _setApprovalsForSpentItems(address approver, SpentItem[] memory items) internal { vm.startPrank(approver); uint256 i = 0; @@ -300,6 +298,16 @@ contract StarPortTest is BaseOrderTest { vm.stopPrank(); } + function _emptyCaveat() internal returns (CaveatEnforcer.CaveatWithApproval memory) { + return CaveatEnforcer.CaveatWithApproval({ + v: 0, + r: bytes32(0), + s: bytes32(0), + salt: bytes32(0), + caveat: new CaveatEnforcer.Caveat[](0) + }); + } + // loan.borrower and signer.addr could be mismatched function _generateSignedCaveatBorrower(LoanManager.Loan memory loan, Account memory signer, bytes32 salt) public @@ -526,42 +534,6 @@ contract StarPortTest is BaseOrderTest { } } - function buyNowPayLater( - AdvancedOrder memory thingToBuy, - NewLoanData memory loanData, - StrategistOriginator originator, - ConsiderationItem[] storage collateral - ) internal { - // (uint8 v, bytes32 r, bytes32 s) = - // vm.sign(strategist.key, keccak256(originator.encodeWithAccountCounter(keccak256(loanData.details)))); - - // LoanManager.Loan memory loan = LoanManager.Loan({ - // custodian: address(loanData.custodian), - // issuer: address(0), - // borrower: borrower.addr, - // originator: address(0), - // terms: abi.decode(loanData.details, (StrategistOriginator.Details)).offer.terms, - // debt: debt, - // collateral: ConsiderationItemLib.toSpentItemArray(collateral), - // start: uint256(0) - // }); - - // _buyNowPLNLR( - // thingToBuy, - // LoanManager.Obligation({ - // custodian: address(loanData.custodian), - // borrower: borrower.addr, - // debt: debt, - // details: loanData.details, - // salt: bytes32(0), - // approval: abi.encodePacked(r, s, v), - // caveats: loanData.caveats, - // originator: address(originator) - // }), - // collateral // for building contract offer - // ); - } - function _buildContractOrder(address offerer, OfferItem[] memory offer, ConsiderationItem[] memory consider) internal view @@ -726,246 +698,6 @@ contract StarPortTest is BaseOrderTest { vm.stopPrank(); } - function _buyNowPLNLR( - AdvancedOrder memory x, - // LoanManager.Loan memory loanAsk, - LoanManager.Obligation memory nlr, - ConsiderationItem[] memory collateral // collateral (nft) and weth (purchase price is incoming weth plus debt) - ) internal returns (LoanManager.Loan memory loan) { - // //use murky to create a tree that is good - - // bytes32 caveatHash = bytes32(uint256(0)); - // // keccak256(LM.encodeWithSaltAndBorrowerCounter(nlr.borrower, nlr.salt, keccak256(abi.encode(nlr.caveats)))); - // OfferItem[] memory offer = new OfferItem[](nlr.debt.length + 1); - - // for (uint256 i; i < debt.length;) { - // offer[i] = OfferItem({ - // itemType: debt[i].itemType, - // token: debt[i].token, - // identifierOrCriteria: debt[i].identifier, - // startAmount: debt[i].amount, - // endAmount: debt[i].amount - // }); - // unchecked { - // ++i; - // } - // } - - // offer[nlr.debt.length] = OfferItem({ - // itemType: ItemType.ERC721, - // token: address(LM), - // identifierOrCriteria: uint256(caveatHash), - // startAmount: 1, - // endAmount: 1 - // }); - - // OfferItem[] memory zOffer = new OfferItem[](1); - // zOffer[0] = OfferItem({ - // itemType: nlr.debt[0].itemType, - // token: nlr.debt[0].token, - // identifierOrCriteria: nlr.debt[0].identifier, - // startAmount: x.parameters.consideration[0].startAmount - nlr.debt[0].amount, - // endAmount: x.parameters.consideration[0].startAmount - nlr.debt[0].amount - // }); - // ConsiderationItem[] memory zConsider = new ConsiderationItem[](1); - // zConsider[0] = ConsiderationItem({ - // itemType: ItemType.ERC721, - // token: address(LM), - // identifierOrCriteria: uint256(caveatHash), - // startAmount: 1, - // endAmount: 1, - // recipient: payable(address(nlr.borrower)) - // }); - // OrderParameters memory zOP = OrderParameters({ - // offerer: address(nlr.borrower), - // zone: address(0), - // offer: zOffer, - // consideration: zConsider, - // orderType: OrderType.FULL_OPEN, - // startTime: block.timestamp, - // endTime: block.timestamp + 100, - // zoneHash: bytes32(0), - // salt: 0, - // conduitKey: bytes32(0), - // totalOriginalConsiderationItems: 1 - // }); - // AdvancedOrder memory z = - // AdvancedOrder({parameters: zOP, numerator: 1, denominator: 1, signature: "", extraData: ""}); - - // AdvancedOrder[] memory orders = new AdvancedOrder[](3); - // orders[0] = x; - // orders[1] = AdvancedOrder({ - // parameters: _buildContractOrder(address(LM), offer, collateral), - // numerator: 1, - // denominator: 1, - // signature: "", - // extraData: abi.encode(Actions.Origination, nlr) - // }); - // orders[2] = z; - - // // x is offering erc721 1 to satisfy y consideration - // Fulfillment[] memory fill = new Fulfillment[](4); - // fill[0] = Fulfillment({ - // offerComponents: new FulfillmentComponent[](1), - // considerationComponents: new FulfillmentComponent[](1) - // }); - - // fill[0].offerComponents[0] = FulfillmentComponent({orderIndex: 1, itemIndex: 0}); - // fill[0].considerationComponents[0] = FulfillmentComponent({orderIndex: 0, itemIndex: 0}); - // fill[1] = Fulfillment({ - // offerComponents: new FulfillmentComponent[](1), - // considerationComponents: new FulfillmentComponent[](1) - // }); - - // fill[1].offerComponents[0] = FulfillmentComponent({orderIndex: 2, itemIndex: 0}); - - // fill[1].considerationComponents[0] = FulfillmentComponent({orderIndex: 0, itemIndex: 0}); - - // fill[2] = Fulfillment({ - // offerComponents: new FulfillmentComponent[](1), - // considerationComponents: new FulfillmentComponent[](1) - // }); - - // fill[2].offerComponents[0] = FulfillmentComponent({orderIndex: 0, itemIndex: 0}); - - // fill[2].considerationComponents[0] = FulfillmentComponent({orderIndex: 1, itemIndex: 0}); - - // fill[3] = Fulfillment({ - // offerComponents: new FulfillmentComponent[](1), - // considerationComponents: new FulfillmentComponent[](1) - // }); - - // fill[3].offerComponents[0] = FulfillmentComponent({orderIndex: 1, itemIndex: 1}); - - // fill[3].considerationComponents[0] = FulfillmentComponent({orderIndex: 2, itemIndex: 0}); - - // uint256 balanceBefore = erc20s[0].balanceOf(seller.addr); - // vm.recordLogs(); - // vm.startPrank(borrower.addr); - - // consideration.matchAdvancedOrders(orders, new CriteriaResolver[](0), fill, address(borrower.addr)); - - // Vm.Log[] memory logs = vm.getRecordedLogs(); - - // // console.logBytes32(logs[logs.length - 4].topics[0]); - // for (uint256 i = 0; i < logs.length; i++) { - // if (logs[i].topics[0] == bytes32(0x57cb72d73c48fadf55428537f6c9efbe080ae111339b0c5af42d9027ed20ba17)) { - // (, loan) = abi.decode(logs[i].data, (uint256, LoanManager.Loan)); - // break; - // } - // } - - // assertEq(erc721s[1].ownerOf(1), address(nlr.custodian)); - // assertEq(erc20s[0].balanceOf(seller.addr), balanceBefore + x.parameters.consideration[0].startAmount); - // vm.stopPrank(); - } - - function _executeNLR(LoanManager.Obligation memory nlr, ConsiderationItem[] memory collateral) - 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 = bytes32(uint256(0)); - // // keccak256(LM.encodeWithSaltAndBorrowerCounter(nlr.borrower, nlr.salt, keccak256(abi.encode(nlr.caveats)))); - // OfferItem[] memory offer = new OfferItem[](nlr.debt.length + 1); - - // for (uint256 i; i < debt.length;) { - // offer[i] = OfferItem({ - // itemType: debt[i].itemType, - // token: debt[i].token, - // identifierOrCriteria: debt[i].identifier, - // startAmount: debt[i].amount, - // endAmount: debt[i].amount - // }); - // unchecked { - // ++i; - // } - // } - - // offer[nlr.debt.length] = OfferItem({ - // itemType: ItemType.ERC721, - // token: address(LM), - // identifierOrCriteria: uint256(caveatHash), - // startAmount: 1, - // endAmount: 1 - // }); - - // OrderParameters memory op = - // _buildContractOrder(address(LM), nlr.caveats.length == 0 ? new OfferItem[](0) : offer, collateral); - - // 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)) { - // balanceBefore = borrower.addr.balance; - // } else { - // balanceBefore = ERC20(debt[0].token).balanceOf(borrower.addr); - // } - // 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, - // criteriaResolvers: new CriteriaResolver[](0), - // fulfillerConduitKey: bytes32(0), - // recipient: address(borrower.addr) - // }); - // } else { - // consideration.fulfillAdvancedOrder({ - // advancedOrder: x, - // criteriaResolvers: new CriteriaResolver[](0), - // fulfillerConduitKey: bytes32(0), - // recipient: address(borrower.addr) - // }); - // } - // Vm.Log[] memory logs = vm.getRecordedLogs(); - // uint256 loanId; - - // // console.logBytes32(logs[logs.length - 4].topics[0]); - // bytes32 lienOpenTopic = bytes32(0x57cb72d73c48fadf55428537f6c9efbe080ae111339b0c5af42d9027ed20ba17); - // for (uint256 i = 0; i < logs.length; i++) { - // if (logs[i].topics[0] == lienOpenTopic) { - // (loanId, loan) = abi.decode(logs[i].data, (uint256, LoanManager.Loan)); - // break; - // } - // } - - // uint256 balanceAfter; - // if (debt[0].token == address(0)) { - // balanceAfter = borrower.addr.balance; - // } else { - // balanceAfter = ERC20(debt[0].token).balanceOf(borrower.addr); - // } - - // 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(); - } - function _repayLoan(address borrower, uint256 amount, LoanManager.Loan memory loan) internal { vm.startPrank(borrower); erc20s[0].approve(address(consideration), amount); @@ -1047,40 +779,6 @@ contract StarPortTest is BaseOrderTest { loan.custodian = incomingCustodian; } - function _createLoanWithCaveat( - address lender, - LoanManager.Terms memory terms, - ConsiderationItem memory collateralItem, - SpentItem memory debtItem, - LoanManager.Caveat[] memory caveats - ) internal returns (LoanManager.Loan memory loan) { - // selectedCollateral.push(collateralItem); - // debt.push(debtItem); - - // StrategistOriginator.Details memory loanDetails = StrategistOriginator.Details({ - // conduit: address(lenderConduit), - // custodian: address(custodian), - // issuer: lender, - // deadline: block.timestamp + 100, - // offer: StrategistOriginator.Offer({ - // salt: bytes32(0), - // terms: terms, - // collateral: ConsiderationItemLib.toSpentItemArray(selectedCollateral), - // debt: debt - // }) - // }); - - // loan = newLoan( - // NewLoanData({ - // custodian: address(custodian), - // caveats: caveats, // TODO check - // details: abi.encode(loanDetails) - // }), - // StrategistOriginator(SO), - // selectedCollateral - // ); - } - function _getERC20SpentItem(TestERC20 token, uint256 amount) internal pure returns (SpentItem memory) { return SpentItem({ itemType: ItemType.ERC20, diff --git a/test/integration-testing/TestAstariaV1Loan.sol b/test/integration-testing/TestAstariaV1Loan.sol index e90d2164..4d571f9a 100644 --- a/test/integration-testing/TestAstariaV1Loan.sol +++ b/test/integration-testing/TestAstariaV1Loan.sol @@ -1,3 +1,5 @@ +pragma solidity ^0.8.17; + import "starport-test/AstariaV1Test.sol"; import {BaseRecall} from "starport-core/hooks/BaseRecall.sol"; diff --git a/test/integration-testing/TestLoanCombinations.t.sol b/test/integration-testing/TestLoanCombinations.t.sol index d955d558..d416b4c8 100644 --- a/test/integration-testing/TestLoanCombinations.t.sol +++ b/test/integration-testing/TestLoanCombinations.t.sol @@ -1,3 +1,5 @@ +pragma solidity ^0.8.17; + import "starport-test/StarPortTest.sol"; import {FixedPointMathLib} from "solady/src/utils/FixedPointMathLib.sol"; import {LibString} from "solady/src/utils/LibString.sol"; diff --git a/test/integration-testing/TestNewLoan.sol b/test/integration-testing/TestNewLoan.sol index 7cd81cfe..dfe8649d 100644 --- a/test/integration-testing/TestNewLoan.sol +++ b/test/integration-testing/TestNewLoan.sol @@ -1,6 +1,56 @@ +pragma solidity ^0.8.17; + import "starport-test/StarPortTest.sol"; import {AstariaV1Pricing} from "starport-core/pricing/AstariaV1Pricing.sol"; import {Actions} from "starport-core/lib/StarPortLib.sol"; +import {BNPLHelper, IFlashLoanRecipient} from "starport-core/BNPLHelper.sol"; +import {Originator} from "starport-core/originators/Originator.sol"; + +contract FlashLoan { + function flashLoan( + IFlashLoanRecipient recipient, + address[] memory tokens, + uint256[] memory amounts, + bytes memory userData + ) external { + uint256[] memory feeAmounts = new uint256[](tokens.length); + uint256[] memory preLoanBalances = new uint256[](tokens.length); + + // Used to ensure `tokens` is sorted in ascending order, which ensures token uniqueness. + ERC20 previousToken = ERC20(address(0)); + + for (uint256 i = 0; i < tokens.length; ++i) { + ERC20 token = ERC20(tokens[i]); + uint256 amount = amounts[i]; + + // previousToken = token; + + preLoanBalances[i] = token.balanceOf(address(this)); + feeAmounts[i] = uint256(0); + + token.transfer(address(recipient), amount); + } + + recipient.receiveFlashLoan(tokens, amounts, feeAmounts, userData); + + for (uint256 i = 0; i < tokens.length; ++i) { + ERC20 token = ERC20(tokens[i]); + uint256 preLoanBalance = preLoanBalances[i]; + + // Checking for loan repayment first (without accounting for fees) makes for simpler debugging, and results + // in more accurate revert reasons if the flash loan protocol fee percentage is zero. + uint256 postLoanBalance = token.balanceOf(address(this)); + require(postLoanBalance >= preLoanBalance, "loan was not paid back"); + + // No need for checked arithmetic since we know the loan was fully repaid. + uint256 receivedFeeAmount = postLoanBalance - preLoanBalance; + require(receivedFeeAmount >= feeAmounts[i], "no fees paid"); + + // _payFeeAmount(token, receivedFeeAmount); + // emit FlashLoan(recipient, token, amounts[i], receivedFeeAmount); + } + } +} contract TestNewLoan is StarPortTest { function testNewLoanERC721CollateralDefaultTerms2() public returns (LoanManager.Loan memory) { @@ -114,101 +164,234 @@ contract TestNewLoan is StarPortTest { } function testBuyNowPayLater() public { - // ConsiderationItem[] memory want = new ConsiderationItem[](1); - // want[0] = ConsiderationItem({ - // token: address(erc20s[0]), - // startAmount: 150, - // endAmount: 150, - // identifierOrCriteria: 0, - // itemType: ItemType.ERC20, - // recipient: payable(seller.addr) - // }); - + ConsiderationItem[] memory want = new ConsiderationItem[](1); + want[0] = ConsiderationItem({ + token: address(erc20s[0]), + startAmount: 100, + endAmount: 100, + identifierOrCriteria: 0, + itemType: ItemType.ERC20, + recipient: payable(seller.addr) + }); // //order 1, which lets is the seller, they have something that we can borrower againt (ERC721) // //order 2 which is the - // OfferItem[] memory sellingNFT = new OfferItem[](1); - // sellingNFT[0] = OfferItem({ - // identifierOrCriteria: 1, - // token: address(erc721s[1]), - // startAmount: 1, - // endAmount: 1, - // itemType: ItemType.ERC721 - // }); - // OrderParameters memory thingToSell = OrderParameters({ - // offerer: seller.addr, - // zone: address(0), - // offer: sellingNFT, - // consideration: want, - // orderType: OrderType.FULL_OPEN, - // startTime: block.timestamp, - // endTime: block.timestamp + 150, - // zoneHash: bytes32(0), - // salt: 0, - // conduitKey: bytes32(0), - // totalOriginalConsiderationItems: 1 - // }); - // bytes32 sellingHash = consideration.getOrderHash(OrderParametersLib.toOrderComponents(thingToSell, 0)); - // (bytes32 r, bytes32 s, uint8 v) = getSignatureComponents(consideration, seller.key, sellingHash); - - // AdvancedOrder memory advThingToSell = AdvancedOrder({ - // parameters: thingToSell, - // numerator: 1, - // denominator: 1, - // signature: abi.encodePacked(r, s, v), - // extraData: "" - // }); - - // 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( - // ConsiderationItem({ - // token: address(erc721s[1]), - // startAmount: 1, - // endAmount: 1, - // identifierOrCriteria: 1, - // itemType: ItemType.ERC721, - // recipient: payable(address(custody)) - // }) - // ); - - // debt.push(SpentItem({itemType: ItemType.ERC20, token: address(erc20s[0]), amount: 100, identifier: 0})); - // StrategistOriginator.Details memory loanDetails = StrategistOriginator.Details({ - // conduit: address(lenderConduit), - // custodian: address(custody), - // issuer: lender.addr, - // deadline: block.timestamp + 100, - // offer: StrategistOriginator.Offer({ - // salt: bytes32(0), - // terms: terms, - // collateral: ConsiderationItemLib.toSpentItemArray(selectedCollateral), - // debt: debt - // }) - // }); + OfferItem[] memory sellingNFT = new OfferItem[](1); + sellingNFT[0] = OfferItem({ + identifierOrCriteria: 1, + token: address(erc721s[1]), + startAmount: 1, + endAmount: 1, + itemType: ItemType.ERC721 + }); + OrderParameters memory thingToSell = OrderParameters({ + offerer: seller.addr, + zone: address(0), + offer: sellingNFT, + consideration: want, + orderType: OrderType.FULL_OPEN, + startTime: block.timestamp, + endTime: block.timestamp + 150, + zoneHash: bytes32(0), + salt: 0, + conduitKey: bytes32(0), + totalOriginalConsiderationItems: 1 + }); + bytes32 r; + bytes32 s; + uint8 v; + (r, s, v) = getSignatureComponents( + consideration, seller.key, consideration.getOrderHash(OrderParametersLib.toOrderComponents(thingToSell, 0)) + ); + + LoanManager.Loan memory loan = generateDefaultLoanTerms(); + + loan.collateral[0].token = sellingNFT[0].token; + loan.collateral[0].identifier = sellingNFT[0].identifierOrCriteria; + loan.collateral[0].amount = 1; + + loan.debt[0].identifier = 0; + loan.debt[0].amount = 100; + + BNPLHelper helper = new BNPLHelper(address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2)); + + AdvancedOrder[] memory orders = new AdvancedOrder[](2); + orders[0] = AdvancedOrder({ + parameters: thingToSell, + numerator: 1, + denominator: 1, + signature: abi.encodePacked(r, s, v), + extraData: "" + }); + OrderParameters memory buyerOrderParams = + createMirrorOrderParameters(thingToSell, payable(borrower.addr), address(0), bytes32(0)); + bytes32 buyingHash = consideration.getOrderHash(OrderParametersLib.toOrderComponents(buyerOrderParams, 0)); //0 is for the current nonce + (r, s, v) = getSignatureComponents(consideration, borrower.key, buyingHash); + orders[1] = AdvancedOrder({ + parameters: buyerOrderParams, + numerator: 1, + denominator: 1, + signature: abi.encodePacked(r, s, v), + extraData: "" + }); + { + _setApprovalsForSpentItems(loan.issuer, loan.debt); + _setApprovalsForSpentItems(loan.borrower, loan.debt); + _setApprovalsForSpentItems(loan.borrower, loan.collateral); + } + + address balancerVault = address(0xBA12222222228d8Ba445958a75a0704d566BF2C8); + vm.etch(balancerVault, address(new FlashLoan()).code); + deal(address(erc20s[0]), balancerVault, type(uint128).max); + { + LoanManager.Loan memory loan2 = loan; + bytes32 buyingHash2 = buyingHash; + Fulfillment[] memory fill = new Fulfillment[](2); + fill[0] = Fulfillment({ + offerComponents: new FulfillmentComponent[](1), + considerationComponents: new FulfillmentComponent[](1) + }); + + fill[0].offerComponents[0] = FulfillmentComponent({orderIndex: 0, itemIndex: 0}); + fill[0].considerationComponents[0] = FulfillmentComponent({orderIndex: 1, itemIndex: 0}); + fill[1] = Fulfillment({ + offerComponents: new FulfillmentComponent[](1), + considerationComponents: new FulfillmentComponent[](1) + }); + + fill[1].offerComponents[0] = FulfillmentComponent({orderIndex: 1, itemIndex: 0}); + + fill[1].considerationComponents[0] = FulfillmentComponent({orderIndex: 0, itemIndex: 0}); + + address[] memory tokens = new address[](1); + tokens[0] = address(erc20s[0]); + uint256[] memory amounts = new uint256[](1); + amounts[0] = 100; + + helper.makeFlashLoan( + tokens, + amounts, + abi.encode( + BNPLHelper.Execution({ + lm: address(LM), + seaport: address(seaport), + borrower: borrower.addr, + borrowerCaveat: signCaveatForAccount( + CaveatEnforcer.Caveat({ + enforcer: address(borrowerEnforcerBNPL), + deadline: block.timestamp + 1 days, + data: abi.encode( + BorrowerEnforcerBNPL.Details({ + loan: loan2, + offerHash: buyingHash2, + additionalTransfer: ConduitTransfer({ + itemType: ConduitItemType.ERC20, + identifier: 0, + token: address(erc20s[0]), + amount: 100, + from: borrower.addr, + to: address(0) + }), + seaport: address(consideration) + }) + ) + }), + bytes32(uint256(1)), + borrower + ), + lenderCaveat: _generateSignedCaveatLender(loan2, lender, bytes32(uint256(1))), + loan: loan2, + orders: orders, + resolvers: new CriteriaResolver[](0), + fulfillments: fill + }) + ) + ); + } + } - // TermEnforcer TE = new TermEnforcer(); + function testNewLoanViaOriginatorLenderApproval() public { + LoanManager.Loan memory loan = generateDefaultLoanTerms(); + + StrategistOriginator.Details memory newLoanDetails = StrategistOriginator.Details({ + custodian: LM.defaultCustodian(), + issuer: lender.addr, + deadline: block.timestamp + 100, + offer: StrategistOriginator.Offer({ + terms: loan.terms, + salt: bytes32(0), + collateral: loan.collateral, + debt: loan.debt + }) + }); - // TermEnforcer.Details memory TEDetails = - // TermEnforcer.Details({pricing: address(pricing), hook: address(hook), handler: address(handler)}); + _setApprovalsForSpentItems(loan.borrower, loan.collateral); + _setApprovalsForSpentItems(loan.issuer, loan.debt); + + (uint8 v, bytes32 r, bytes32 s) = + vm.sign(strategist.key, keccak256(SO.encodeWithAccountCounter(keccak256(abi.encode(newLoanDetails))))); + + uint256 borrowerBalanceBefore = erc20s[0].balanceOf(borrower.addr); + uint256 lenderBalanceBefore = erc20s[0].balanceOf(lender.addr); + vm.prank(lender.addr); + LM.setOriginateApproval(address(SO), LoanManager.ApprovalType.LENDER); + vm.prank(borrower.addr); + SO.originate( + Originator.Request({ + borrower: borrower.addr, + borrowerCaveat: _generateSignedCaveatBorrower(loan, borrower, bytes32(uint256(5))), + collateral: loan.collateral, + debt: loan.debt, + details: abi.encode(newLoanDetails), + approval: abi.encodePacked(r, s, v) + }) + ); + assert(erc20s[0].balanceOf(borrower.addr) == borrowerBalanceBefore + loan.debt[0].amount); + assert(erc20s[0].balanceOf(lender.addr) == lenderBalanceBefore - loan.debt[0].amount); + assert(erc721s[0].ownerOf(loan.collateral[0].identifier) == address(LM.defaultCustodian())); + } - // LoanManager.Caveat[] memory caveats = new LoanManager.Caveat[](1); - // caveats[0] = LoanManager.Caveat({enforcer: address(TE), terms: abi.encode(TEDetails)}); + function testNewLoanViaOriginatorBorrowerApprovalAndLenderApproval() public { + LoanManager.Loan memory loan = generateDefaultLoanTerms(); + + StrategistOriginator.Details memory newLoanDetails = StrategistOriginator.Details({ + custodian: LM.defaultCustodian(), + issuer: lender.addr, + deadline: block.timestamp + 100, + offer: StrategistOriginator.Offer({ + terms: loan.terms, + salt: bytes32(0), + collateral: loan.collateral, + debt: loan.debt + }) + }); - // buyNowPayLater( - // advThingToSell, - // NewLoanData(address(custody), caveats, abi.encode(loanDetails)), - // StrategistOriginator(SO), - // selectedCollateral - // ); + _setApprovalsForSpentItems(loan.borrower, loan.collateral); + _setApprovalsForSpentItems(loan.issuer, loan.debt); + + (uint8 v, bytes32 r, bytes32 s) = + vm.sign(strategist.key, keccak256(SO.encodeWithAccountCounter(keccak256(abi.encode(newLoanDetails))))); + + uint256 borrowerBalanceBefore = erc20s[0].balanceOf(borrower.addr); + uint256 lenderBalanceBefore = erc20s[0].balanceOf(lender.addr); + vm.prank(borrower.addr); + LM.setOriginateApproval(address(SO), LoanManager.ApprovalType.BORROWER); + vm.prank(lender.addr); + LM.setOriginateApproval(address(SO), LoanManager.ApprovalType.LENDER); + vm.prank(borrower.addr); + SO.originate( + Originator.Request({ + borrower: borrower.addr, + borrowerCaveat: _emptyCaveat(), + collateral: loan.collateral, + debt: loan.debt, + details: abi.encode(newLoanDetails), + approval: abi.encodePacked(r, s, v) + }) + ); + assert(erc20s[0].balanceOf(borrower.addr) == borrowerBalanceBefore + loan.debt[0].amount); + assert(erc20s[0].balanceOf(lender.addr) == lenderBalanceBefore - loan.debt[0].amount); + assert(erc721s[0].ownerOf(loan.collateral[0].identifier) == address(LM.defaultCustodian())); } function testSettleLoan() public { diff --git a/test/integration-testing/TestRepayLoan.sol b/test/integration-testing/TestRepayLoan.sol index a4438468..74f809bb 100644 --- a/test/integration-testing/TestRepayLoan.sol +++ b/test/integration-testing/TestRepayLoan.sol @@ -1,3 +1,5 @@ +pragma solidity ^0.8.17; + import "starport-test/StarPortTest.sol"; contract TestRepayLoan is StarPortTest { diff --git a/test/unit-testing/EnforcerTest.t.sol b/test/unit-testing/EnforcerTest.t.sol index 747bb039..c62f97e2 100644 --- a/test/unit-testing/EnforcerTest.t.sol +++ b/test/unit-testing/EnforcerTest.t.sol @@ -1,3 +1,5 @@ +pragma solidity ^0.8.17; + import "starport-test/StarPortTest.sol"; import {FixedPointMathLib} from "solady/src/utils/FixedPointMathLib.sol"; import {LibString} from "solady/src/utils/LibString.sol"; diff --git a/test/unit-testing/TestCustodian.sol b/test/unit-testing/TestCustodian.sol index 9bff8c5d..1e87a46f 100644 --- a/test/unit-testing/TestCustodian.sol +++ b/test/unit-testing/TestCustodian.sol @@ -1,3 +1,5 @@ +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"; diff --git a/test/unit-testing/TestLoanManager.sol b/test/unit-testing/TestLoanManager.sol index a838bd1d..0ee80df1 100644 --- a/test/unit-testing/TestLoanManager.sol +++ b/test/unit-testing/TestLoanManager.sol @@ -1,3 +1,5 @@ +pragma solidity ^0.8.17; + import "starport-test/StarPortTest.sol"; import {StarPortLib} from "starport-core/lib/StarPortLib.sol"; import {DeepEq} from "starport-test/utils/DeepEq.sol"; @@ -50,19 +52,23 @@ contract MockOriginator is StrategistOriginator, 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) { - StrategistOriginator.Details memory details = - abi.decode(request.details, (StrategistOriginator.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)}); + function originate(Request calldata params) external virtual override { + StrategistOriginator.Details memory details = abi.decode(params.details, (StrategistOriginator.Details)); + _validateOffer(params, details); + + LoanManager.Loan memory loan = LoanManager.Loan({ + start: uint256(0), // are set in the loan manager + originator: address(0), // are set in the loan manager + custodian: details.custodian, + issuer: details.issuer, + borrower: params.borrower, + collateral: params.collateral, + debt: params.debt, + terms: details.offer.terms + }); + + CaveatEnforcer.CaveatWithApproval memory le; + LM.originate(new ConduitTransfer[](0), params.borrowerCaveat, le, loan); } receive() external payable {} @@ -128,6 +134,21 @@ contract TestLoanManager is StarPortTest, DeepEq { LM.settle(activeLoan); } + event Paused(); + event UnPaused(); + + function testPause() public { + vm.expectEmit(); + emit Paused(); + LM.pause(); + } + + function testUnPause() public { + vm.expectEmit(); + emit UnPaused(); + LM.unPause(); + } + function testIssued() public { assert(LM.issued(activeLoan.getId())); } @@ -173,6 +194,13 @@ contract TestLoanManager is StarPortTest, DeepEq { vm.stopPrank(); } + function testCannotOriginateWhilePaused() public { + LM.pause(); + LoanManager.Loan memory loan = generateDefaultLoanTerms(); + vm.expectRevert(abi.encodeWithSelector(LoanManager.IsPaused.selector)); + LM.originate(new ConduitTransfer[](0), _emptyCaveat(), _emptyCaveat(), loan); + } + function testNonDefaultCustodianCustodyCallSuccess() public { CaveatEnforcer.CaveatWithApproval memory borrowerCaveat; @@ -237,6 +265,27 @@ contract TestLoanManager is StarPortTest, DeepEq { vm.stopPrank(); } + function testInvalidIdentifierDebt() public { + CaveatEnforcer.CaveatWithApproval memory borrowerCaveat; + + LoanManager.Loan memory loan = generateDefaultLoanTerms(); + loan.collateral[0].identifier = uint256(2); + loan.debt[0].identifier = uint256(2); + + CaveatEnforcer.CaveatWithApproval memory lenderCaveat = getLenderSignedCaveat({ + details: LenderEnforcer.Details({loan: loan}), + signer: lender, + salt: bytes32(0), + enforcer: address(lenderEnforcer) + }); + _setApprovalsForSpentItems(loan.borrower, loan.collateral); + _setApprovalsForSpentItems(loan.issuer, loan.debt); + vm.startPrank(loan.borrower); + vm.expectRevert(abi.encodeWithSelector(LoanManager.InvalidItemIdentifier.selector)); + LM.originate(new ConduitTransfer[](0), borrowerCaveat, lenderCaveat, loan); + vm.stopPrank(); + } + function testInvalidAmountCollateral() public { CaveatEnforcer.CaveatWithApproval memory borrowerCaveat; diff --git a/test/unit-testing/TestStrategistOriginator.sol b/test/unit-testing/TestStrategistOriginator.sol new file mode 100644 index 00000000..94868bb0 --- /dev/null +++ b/test/unit-testing/TestStrategistOriginator.sol @@ -0,0 +1,464 @@ +pragma solidity ^0.8.17; + +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"; +import {Originator} from "starport-core/originators/Originator.sol"; +import {CaveatEnforcer} from "starport-core/enforcers/CaveatEnforcer.sol"; + +contract TestStrategistOriginator is StarPortTest, DeepEq { + using Cast for *; + using FixedPointMathLib for uint256; + + event StrategistTransferred(address newStrategist); + event CounterUpdated(uint256); + event HashInvalidated(bytes32 hash); + + using {StarPortLib.getId} for LoanManager.Loan; + + uint256 public borrowAmount = 100; + + function setUp() public virtual override { + super.setUp(); + } + + function testSetStrategist() public { + vm.expectEmit(); + emit StrategistTransferred(address(this)); + SO.setStrategist(address(this)); + } + + function testEncodeWithAccountCounter() public { + bytes32 contextHash = keccak256(abi.encodePacked(string("test"))); + bytes32 hash = keccak256(abi.encode(SO.ORIGINATOR_DETAILS_TYPEHASH(), SO.getCounter(), contextHash)); + + assert( + keccak256(SO.encodeWithAccountCounter(contextHash)) + == keccak256(abi.encodePacked(bytes1(0x19), bytes1(0x01), SO.domainSeparator(), hash)) + ); + } + + function testGetStrategistData() public { + StrategistOriginator SOO = new StrategistOriginator(LM, strategist.addr, 1e17, address(this)); + (address activeStrategist, uint256 strategistFee) = SOO.getStrategistData(); + assert(activeStrategist == strategist.addr); + assert(strategistFee == 1e17); + } + + function testIncrementCounterAsStrategist() public { + uint256 newCounter = SO.getCounter() + uint256(blockhash(block.number - 1) << 0x80); + vm.expectEmit(); + emit CounterUpdated(newCounter); + vm.prank(strategist.addr); + SO.incrementCounter(); + } + + function testIncrementCounterNotAuthorized() public { + vm.expectRevert(abi.encodeWithSelector(StrategistOriginator.NotAuthorized.selector)); + vm.prank(address(1)); + SO.incrementCounter(); + } + + function testInvalidDeadline() public { + LoanManager.Loan memory loan = generateDefaultLoanTerms(); + + StrategistOriginator.Details memory newLoanDetails = StrategistOriginator.Details({ + custodian: LM.defaultCustodian(), + issuer: lender.addr, + deadline: block.timestamp + 100, + offer: StrategistOriginator.Offer({ + terms: loan.terms, + salt: bytes32(0), + collateral: loan.collateral, + debt: loan.debt + }) + }); + + _setApprovalsForSpentItems(loan.borrower, loan.collateral); + _setApprovalsForSpentItems(loan.issuer, loan.debt); + + (uint8 v, bytes32 r, bytes32 s) = + vm.sign(strategist.key, keccak256(SO.encodeWithAccountCounter(keccak256(abi.encode(newLoanDetails))))); + + uint256 borrowerBalanceBefore = erc20s[0].balanceOf(borrower.addr); + uint256 lenderBalanceBefore = erc20s[0].balanceOf(lender.addr); + + CaveatEnforcer.CaveatWithApproval memory be = _generateSignedCaveatBorrower(loan, borrower, bytes32(uint256(5))); + vm.prank(lender.addr); + LM.setOriginateApproval(address(SO), LoanManager.ApprovalType.LENDER); + skip(200); + vm.expectRevert(abi.encodeWithSelector(StrategistOriginator.InvalidDeadline.selector)); + vm.prank(borrower.addr); + SO.originate( + Originator.Request({ + borrower: borrower.addr, + borrowerCaveat: be, + collateral: loan.collateral, + debt: loan.debt, + details: abi.encode(newLoanDetails), + approval: abi.encodePacked(r, s, v) + }) + ); + } + + function testInvalidCollateral() public { + LoanManager.Loan memory loan = generateDefaultLoanTerms(); + + StrategistOriginator.Details memory newLoanDetails = StrategistOriginator.Details({ + custodian: LM.defaultCustodian(), + issuer: lender.addr, + deadline: block.timestamp + 100, + offer: StrategistOriginator.Offer({ + terms: loan.terms, + salt: bytes32(0), + collateral: loan.collateral, + debt: loan.debt + }) + }); + + _setApprovalsForSpentItems(loan.borrower, loan.collateral); + _setApprovalsForSpentItems(loan.issuer, loan.debt); + + bytes memory encodedLoanDetails = abi.encode(newLoanDetails); + (uint8 v, bytes32 r, bytes32 s) = + vm.sign(strategist.key, keccak256(SO.encodeWithAccountCounter(keccak256(encodedLoanDetails)))); + + loan.collateral[0].identifier = uint256(7); + uint256 borrowerBalanceBefore = erc20s[0].balanceOf(borrower.addr); + uint256 lenderBalanceBefore = erc20s[0].balanceOf(lender.addr); + + CaveatEnforcer.CaveatWithApproval memory be = _generateSignedCaveatBorrower(loan, borrower, bytes32(uint256(5))); + vm.prank(lender.addr); + LM.setOriginateApproval(address(SO), LoanManager.ApprovalType.LENDER); + vm.expectRevert(abi.encodeWithSelector(StrategistOriginator.InvalidCollateral.selector)); + vm.prank(borrower.addr); + SO.originate( + Originator.Request({ + borrower: borrower.addr, + borrowerCaveat: be, + collateral: loan.collateral, + debt: loan.debt, + details: encodedLoanDetails, + approval: abi.encodePacked(r, s, v) + }) + ); + } + + function testInvalidDebt() public { + LoanManager.Loan memory loan = generateDefaultLoanTerms(); + + StrategistOriginator.Details memory newLoanDetails = StrategistOriginator.Details({ + custodian: LM.defaultCustodian(), + issuer: lender.addr, + deadline: block.timestamp + 100, + offer: StrategistOriginator.Offer({ + terms: loan.terms, + salt: bytes32(0), + collateral: loan.collateral, + debt: loan.debt + }) + }); + + _setApprovalsForSpentItems(loan.borrower, loan.collateral); + _setApprovalsForSpentItems(loan.issuer, loan.debt); + + bytes memory encodedLoanDetails = abi.encode(newLoanDetails); + (uint8 v, bytes32 r, bytes32 s) = + vm.sign(strategist.key, keccak256(SO.encodeWithAccountCounter(keccak256(encodedLoanDetails)))); + + loan.debt[0].identifier = uint256(7); + uint256 borrowerBalanceBefore = erc20s[0].balanceOf(borrower.addr); + uint256 lenderBalanceBefore = erc20s[0].balanceOf(lender.addr); + + CaveatEnforcer.CaveatWithApproval memory be = _generateSignedCaveatBorrower(loan, borrower, bytes32(uint256(5))); + vm.prank(lender.addr); + LM.setOriginateApproval(address(SO), LoanManager.ApprovalType.LENDER); + vm.expectRevert(abi.encodeWithSelector(StrategistOriginator.InvalidDebt.selector)); + vm.prank(borrower.addr); + SO.originate( + Originator.Request({ + borrower: borrower.addr, + borrowerCaveat: be, + collateral: loan.collateral, + debt: loan.debt, + details: encodedLoanDetails, + approval: abi.encodePacked(r, s, v) + }) + ); + } + + function testInvalidDebtAmountRequestingZero() public { + LoanManager.Loan memory loan = generateDefaultLoanTerms(); + + StrategistOriginator.Details memory newLoanDetails = StrategistOriginator.Details({ + custodian: LM.defaultCustodian(), + issuer: lender.addr, + deadline: block.timestamp + 100, + offer: StrategistOriginator.Offer({ + terms: loan.terms, + salt: bytes32(0), + collateral: loan.collateral, + debt: loan.debt + }) + }); + + _setApprovalsForSpentItems(loan.borrower, loan.collateral); + _setApprovalsForSpentItems(loan.issuer, loan.debt); + + bytes memory encodedLoanDetails = abi.encode(newLoanDetails); + (uint8 v, bytes32 r, bytes32 s) = + vm.sign(strategist.key, keccak256(SO.encodeWithAccountCounter(keccak256(encodedLoanDetails)))); + + loan.debt[0].amount = 0; + uint256 borrowerBalanceBefore = erc20s[0].balanceOf(borrower.addr); + uint256 lenderBalanceBefore = erc20s[0].balanceOf(lender.addr); + + CaveatEnforcer.CaveatWithApproval memory be = _generateSignedCaveatBorrower(loan, borrower, bytes32(uint256(5))); + vm.prank(lender.addr); + LM.setOriginateApproval(address(SO), LoanManager.ApprovalType.LENDER); + vm.expectRevert(abi.encodeWithSelector(StrategistOriginator.InvalidDebtAmount.selector)); + vm.prank(borrower.addr); + SO.originate( + Originator.Request({ + borrower: borrower.addr, + borrowerCaveat: be, + collateral: loan.collateral, + debt: loan.debt, + details: encodedLoanDetails, + approval: abi.encodePacked(r, s, v) + }) + ); + } + + function testInvalidDebtAmountOfferingZero() public { + LoanManager.Loan memory loan = generateDefaultLoanTerms(); + loan.debt[0].amount = 0; + + StrategistOriginator.Details memory newLoanDetails = StrategistOriginator.Details({ + custodian: LM.defaultCustodian(), + issuer: lender.addr, + deadline: block.timestamp + 100, + offer: StrategistOriginator.Offer({ + terms: loan.terms, + salt: bytes32(0), + collateral: loan.collateral, + debt: loan.debt + }) + }); + + _setApprovalsForSpentItems(loan.borrower, loan.collateral); + _setApprovalsForSpentItems(loan.issuer, loan.debt); + + bytes memory encodedLoanDetails = abi.encode(newLoanDetails); + (uint8 v, bytes32 r, bytes32 s) = + vm.sign(strategist.key, keccak256(SO.encodeWithAccountCounter(keccak256(encodedLoanDetails)))); + + uint256 borrowerBalanceBefore = erc20s[0].balanceOf(borrower.addr); + uint256 lenderBalanceBefore = erc20s[0].balanceOf(lender.addr); + + CaveatEnforcer.CaveatWithApproval memory be = _generateSignedCaveatBorrower(loan, borrower, bytes32(uint256(5))); + vm.prank(lender.addr); + LM.setOriginateApproval(address(SO), LoanManager.ApprovalType.LENDER); + vm.expectRevert(abi.encodeWithSelector(StrategistOriginator.InvalidDebtAmount.selector)); + vm.prank(borrower.addr); + SO.originate( + Originator.Request({ + borrower: borrower.addr, + borrowerCaveat: be, + collateral: loan.collateral, + debt: loan.debt, + details: encodedLoanDetails, + approval: abi.encodePacked(r, s, v) + }) + ); + } + + function testInvalidDebtAmountAskingMoreThanOffered() public { + LoanManager.Loan memory loan = generateDefaultLoanTerms(); + + StrategistOriginator.Details memory newLoanDetails = StrategistOriginator.Details({ + custodian: LM.defaultCustodian(), + issuer: lender.addr, + deadline: block.timestamp + 100, + offer: StrategistOriginator.Offer({ + terms: loan.terms, + salt: bytes32(0), + collateral: loan.collateral, + debt: loan.debt + }) + }); + + _setApprovalsForSpentItems(loan.borrower, loan.collateral); + _setApprovalsForSpentItems(loan.issuer, loan.debt); + + bytes memory encodedLoanDetails = abi.encode(newLoanDetails); + (uint8 v, bytes32 r, bytes32 s) = + vm.sign(strategist.key, keccak256(SO.encodeWithAccountCounter(keccak256(encodedLoanDetails)))); + + uint256 borrowerBalanceBefore = erc20s[0].balanceOf(borrower.addr); + uint256 lenderBalanceBefore = erc20s[0].balanceOf(lender.addr); + loan.debt[0].amount = 2e18; + CaveatEnforcer.CaveatWithApproval memory be = _generateSignedCaveatBorrower(loan, borrower, bytes32(uint256(5))); + vm.prank(lender.addr); + LM.setOriginateApproval(address(SO), LoanManager.ApprovalType.LENDER); + vm.expectRevert(abi.encodeWithSelector(StrategistOriginator.InvalidDebtAmount.selector)); + vm.prank(borrower.addr); + SO.originate( + Originator.Request({ + borrower: borrower.addr, + borrowerCaveat: be, + collateral: loan.collateral, + debt: loan.debt, + details: encodedLoanDetails, + approval: abi.encodePacked(r, s, v) + }) + ); + } + + function testInvalidDebtLength() public { + LoanManager.Loan memory loan = generateDefaultLoanTerms(); + + StrategistOriginator.Details memory newLoanDetails = StrategistOriginator.Details({ + custodian: LM.defaultCustodian(), + issuer: lender.addr, + deadline: block.timestamp + 100, + offer: StrategistOriginator.Offer({ + terms: loan.terms, + salt: bytes32(0), + collateral: loan.collateral, + debt: loan.debt + }) + }); + + _setApprovalsForSpentItems(loan.borrower, loan.collateral); + _setApprovalsForSpentItems(loan.issuer, loan.debt); + + bytes memory encodedLoanDetails = abi.encode(newLoanDetails); + (uint8 v, bytes32 r, bytes32 s) = + vm.sign(strategist.key, keccak256(SO.encodeWithAccountCounter(keccak256(encodedLoanDetails)))); + + uint256 borrowerBalanceBefore = erc20s[0].balanceOf(borrower.addr); + uint256 lenderBalanceBefore = erc20s[0].balanceOf(lender.addr); + SpentItem[] memory newDebt = new SpentItem[](2); + newDebt[0] = loan.debt[0]; + loan.debt = newDebt; + CaveatEnforcer.CaveatWithApproval memory be = _generateSignedCaveatBorrower(loan, borrower, bytes32(uint256(5))); + vm.prank(lender.addr); + LM.setOriginateApproval(address(SO), LoanManager.ApprovalType.LENDER); + vm.expectRevert(abi.encodeWithSelector(StrategistOriginator.InvalidDebtLength.selector)); + vm.prank(borrower.addr); + SO.originate( + Originator.Request({ + borrower: borrower.addr, + borrowerCaveat: be, + collateral: loan.collateral, + debt: loan.debt, + details: encodedLoanDetails, + approval: abi.encodePacked(r, s, v) + }) + ); + } + + function testInvalidSigner() public { + LoanManager.Loan memory loan = generateDefaultLoanTerms(); + + StrategistOriginator.Details memory newLoanDetails = StrategistOriginator.Details({ + custodian: LM.defaultCustodian(), + issuer: lender.addr, + deadline: block.timestamp + 100, + offer: StrategistOriginator.Offer({ + terms: loan.terms, + salt: bytes32(0), + collateral: loan.collateral, + debt: loan.debt + }) + }); + + _setApprovalsForSpentItems(loan.borrower, loan.collateral); + _setApprovalsForSpentItems(loan.issuer, loan.debt); + + bytes memory encodedLoanDetails = abi.encode(newLoanDetails); + (uint8 v, bytes32 r, bytes32 s) = + vm.sign(strategist.key, keccak256(SO.encodeWithAccountCounter(keccak256(encodedLoanDetails)))); + + loan.debt[0].amount = 2e18; + + uint256 borrowerBalanceBefore = erc20s[0].balanceOf(borrower.addr); + uint256 lenderBalanceBefore = erc20s[0].balanceOf(lender.addr); + SpentItem[] memory newDebt = new SpentItem[](2); + CaveatEnforcer.CaveatWithApproval memory be = _generateSignedCaveatBorrower(loan, borrower, bytes32(uint256(5))); + vm.prank(lender.addr); + LM.setOriginateApproval(address(SO), LoanManager.ApprovalType.LENDER); + vm.expectRevert(abi.encodeWithSelector(StrategistOriginator.InvalidSigner.selector)); + vm.prank(borrower.addr); + SO.originate( + Originator.Request({ + borrower: borrower.addr, + borrowerCaveat: be, + collateral: loan.collateral, + debt: loan.debt, + details: abi.encode(newLoanDetails), + approval: abi.encodePacked(r, s, v) + }) + ); + } + + function testInvalidOffer() public { + LoanManager.Loan memory loan = generateDefaultLoanTerms(); + + StrategistOriginator.Details memory newLoanDetails = StrategistOriginator.Details({ + custodian: LM.defaultCustodian(), + issuer: lender.addr, + deadline: block.timestamp + 100, + offer: StrategistOriginator.Offer({ + terms: loan.terms, + salt: bytes32(uint256(1)), + collateral: loan.collateral, + debt: loan.debt + }) + }); + + _setApprovalsForSpentItems(loan.borrower, loan.collateral); + _setApprovalsForSpentItems(loan.issuer, loan.debt); + + bytes memory encodedLoanDetails = abi.encode(newLoanDetails); + (uint8 v, bytes32 r, bytes32 s) = + vm.sign(strategist.key, keccak256(SO.encodeWithAccountCounter(keccak256(encodedLoanDetails)))); + + uint256 borrowerBalanceBefore = erc20s[0].balanceOf(borrower.addr); + uint256 lenderBalanceBefore = erc20s[0].balanceOf(lender.addr); + SpentItem[] memory newDebt = new SpentItem[](2); + CaveatEnforcer.CaveatWithApproval memory be = _generateSignedCaveatBorrower(loan, borrower, bytes32(uint256(5))); + vm.prank(lender.addr); + LM.setOriginateApproval(address(SO), LoanManager.ApprovalType.LENDER); + + vm.expectEmit(); + emit HashInvalidated(keccak256(encodedLoanDetails)); + vm.prank(borrower.addr); + SO.originate( + Originator.Request({ + borrower: borrower.addr, + borrowerCaveat: be, + collateral: loan.collateral, + debt: loan.debt, + details: encodedLoanDetails, + approval: abi.encodePacked(r, s, v) + }) + ); + vm.expectRevert(abi.encodeWithSelector(StrategistOriginator.InvalidOffer.selector)); + vm.prank(borrower.addr); + SO.originate( + Originator.Request({ + borrower: borrower.addr, + borrowerCaveat: be, + collateral: loan.collateral, + debt: loan.debt, + details: encodedLoanDetails, + approval: abi.encodePacked(r, s, v) + }) + ); + } +} diff --git a/test/utils/Bound.sol b/test/utils/Bound.sol index 3f5149b7..ce3c888b 100644 --- a/test/utils/Bound.sol +++ b/test/utils/Bound.sol @@ -1,4 +1,4 @@ -pragma solidity =0.8.17; +pragma solidity ^0.8.17; import {ItemType, SpentItem, ReceivedItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; import {Cast} from "starport-test/utils/Cast.sol"; diff --git a/test/utils/Cast.sol b/test/utils/Cast.sol index e1b24f12..2bca518d 100644 --- a/test/utils/Cast.sol +++ b/test/utils/Cast.sol @@ -1,4 +1,4 @@ -pragma solidity =0.8.17; +pragma solidity ^0.8.17; import {ItemType, SpentItem, ReceivedItem, ConsiderationItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; import "starport-test/utils/FuzzStructs.sol" as Fuzz; diff --git a/test/utils/FuzzStructs.sol b/test/utils/FuzzStructs.sol index 3b98758d..25c1c8b0 100644 --- a/test/utils/FuzzStructs.sol +++ b/test/utils/FuzzStructs.sol @@ -1,4 +1,4 @@ -pragma solidity =0.8.17; +pragma solidity ^0.8.17; struct ReceivedItem { uint8 itemType; diff --git a/test/utils/MockCall.sol b/test/utils/MockCall.sol index 06ebe266..11db546b 100644 --- a/test/utils/MockCall.sol +++ b/test/utils/MockCall.sol @@ -1,3 +1,5 @@ +pragma solidity ^0.8.17; + import {ItemType, SpentItem, ReceivedItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; import {TestBase} from "forge-std/Test.sol"; import {SettlementHook} from "src/hooks/SettlementHook.sol";