diff --git a/src/PWNErrors.sol b/src/PWNErrors.sol index eb2c026..58a9391 100644 --- a/src/PWNErrors.sol +++ b/src/PWNErrors.sol @@ -60,6 +60,7 @@ error AuctionDurationNotInFullMinutes(uint256 current); error InvalidCreditAmountRange(uint256 minCreditAmount, uint256 maxCreditAmount); error InvalidCreditAmount(uint256 auctionCreditAmount, uint256 intendedCreditAmount, uint256 slippage); error AuctionNotInProgress(uint256 currentTimestamp, uint256 auctionStart); +error CallerNotLoanContract(address caller, address loanContract); // Input data error InvalidInputData(); diff --git a/src/loan/terms/simple/loan/PWNSimpleLoan.sol b/src/loan/terms/simple/loan/PWNSimpleLoan.sol index ece6578..5ac8d8e 100644 --- a/src/loan/terms/simple/loan/PWNSimpleLoan.sol +++ b/src/loan/terms/simple/loan/PWNSimpleLoan.sol @@ -34,6 +34,9 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { |* # VARIABLES & CONSTANTS DEFINITIONS *| |*----------------------------------------------------------*/ + uint32 public constant MIN_LOAN_DURATION = 10 minutes; + uint40 public constant MAX_ACCRUING_INTEREST_APR = 1e11; // 1,000,000% APR + uint256 public constant APR_INTEREST_DENOMINATOR = 1e4; uint256 public constant DAILY_INTEREST_DENOMINATOR = 1e10; @@ -82,6 +85,32 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { uint40 accruingInterestAPR; } + /** + * @notice Loan proposal specification during loan creation. + * @param proposalContract Address of a loan proposal contract. + * @param proposalData Encoded proposal data that is passed to the loan proposal contract. + * @param signature Signature of the proposal. + */ + struct ProposalSpec { + address proposalContract; + bytes proposalData; + bytes signature; + } + + /** + * @notice Caller specification during loan creation. + * @param refinancingLoanId Id of a loan to be refinanced. 0 if creating a new loan. + * @param revokeNonce Flag if the callers nonce should be revoked. + * @param nonce Callers nonce to be revoked. Nonce is revoked from the current nonce space. + * @param permit Callers permit data for a loans credit asset. + */ + struct CallerSpec { + uint256 refinancingLoanId; + bool revokeNonce; + uint256 nonce; + Permit permit; + } + /** * @notice Struct defining a simple loan. * @param status 0 == none/dead || 2 == running/accepted offer/accepted request || 3 == paid back || 4 == expired. @@ -142,6 +171,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { */ mapping (bytes32 => bool) public extensionProposalsMade; + /*----------------------------------------------------------*| |* # EVENTS & ERRORS DEFINITIONS *| |*----------------------------------------------------------*/ @@ -203,47 +233,131 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { /** * @notice Create a new loan. * @dev The function assumes a prior token approval to a contract address or signed permits. - * @param proposalHash Hash of a loan offer / request that is signed by a lender / borrower. - * @param loanTerms Loan terms struct. - * @param permit Callers credit permit data. + * @param proposalSpec Proposal specification struct. + * @param callerSpec Caller specification struct. * @param extra Auxiliary data that are emitted in the loan creation event. They are not used in the contract logic. * @return loanId Id of the created LOAN token. */ function createLOAN( - bytes32 proposalHash, - Terms calldata loanTerms, - Permit calldata permit, + ProposalSpec calldata proposalSpec, + CallerSpec calldata callerSpec, bytes calldata extra ) external returns (uint256 loanId) { - // Check that caller is loan proposal contract - if (!hub.hasTag(msg.sender, PWNHubTags.LOAN_PROPOSAL)) { - revert CallerMissingHubTag(PWNHubTags.LOAN_PROPOSAL); + // Check provided proposal contract + if (!hub.hasTag(proposalSpec.proposalContract, PWNHubTags.LOAN_PROPOSAL)) { + revert AddressMissingHubTag({ addr: proposalSpec.proposalContract, tag: PWNHubTags.LOAN_PROPOSAL }); + } + + // Revoke nonce if needed + if (callerSpec.revokeNonce) { + revokedNonce.revokeNonce(msg.sender, callerSpec.nonce); + } + + // If refinancing a loan, check that the loan can be repaid + if (callerSpec.refinancingLoanId != 0) { + LOAN storage loan = LOANs[callerSpec.refinancingLoanId]; + _checkLoanCanBeRepaid(loan.status, loan.defaultTimestamp); + } + + // Accept proposal and get loan terms + (bytes32 proposalHash, Terms memory loanTerms) = PWNSimpleLoanProposal(proposalSpec.proposalContract) + .acceptProposal({ + acceptor: msg.sender, + refinancingLoanId: callerSpec.refinancingLoanId, + proposalData: proposalSpec.proposalData, + signature: proposalSpec.signature + }); + + // Check minimum loan duration + if (loanTerms.duration < MIN_LOAN_DURATION) { + revert InvalidDuration({ current: loanTerms.duration, limit: MIN_LOAN_DURATION }); + } + // Check maximum accruing interest APR + if (loanTerms.accruingInterestAPR > MAX_ACCRUING_INTEREST_APR) { + revert AccruingInterestAPROutOfBounds({ current: loanTerms.accruingInterestAPR, limit: MAX_ACCRUING_INTEREST_APR }); } - // Check loan terms - _checkLoanTerms(loanTerms); + if (callerSpec.refinancingLoanId == 0) { + // Check loan credit and collateral validity + _checkValidAsset(loanTerms.credit); + _checkValidAsset(loanTerms.collateral); + } else { + // Check refinance loan terms + _checkRefinanceLoanTerms(callerSpec.refinancingLoanId, loanTerms); + } // Create a new loan loanId = _createLoan({ proposalHash: proposalHash, - proposalContract: msg.sender, + proposalContract: proposalSpec.proposalContract, loanTerms: loanTerms, extra: extra }); - // Transfer collateral to Vault and credit to borrower - _settleNewLoan(loanTerms, permit); + // Execute permit for the caller + _checkPermit(msg.sender, loanTerms.credit.assetAddress, callerSpec.permit); + _tryPermit(callerSpec.permit); + + if (callerSpec.refinancingLoanId == 0) { + // Transfer collateral to Vault and credit to borrower + _settleNewLoan(loanTerms); + } else { + // Refinance the original loan + _refinanceOriginalLoan(callerSpec.refinancingLoanId, loanTerms); + + emit LOANRefinanced({ loanId: callerSpec.refinancingLoanId, refinancedLoanId: loanId }); + } } /** - * @notice Check loan terms validity. - * @dev The function will revert if the loan terms are not valid. - * @param loanTerms Loan terms struct. + * @notice Check that permit data have correct owner and asset. + * @param caller Caller address. + * @param creditAddress Address of a credit to be used. + * @param permit Permit to be checked. */ - function _checkLoanTerms(Terms calldata loanTerms) private view { - // Check loan credit and collateral validity - _checkValidAsset(loanTerms.credit); - _checkValidAsset(loanTerms.collateral); + function _checkPermit(address caller, address creditAddress, Permit calldata permit) private pure { + if (permit.asset != address(0)) { + if (permit.owner != caller) { + revert InvalidPermitOwner({ current: permit.owner, expected: caller}); + } + if (permit.asset != creditAddress) { + revert InvalidPermitAsset({ current: permit.asset, expected: creditAddress }); + } + } + } + + /** + * @notice Check if the loan terms are valid for refinancing. + * @dev The function will revert if the loan terms are not valid for refinancing. + * @param loanId Original loan id. + * @param loanTerms Refinancing loan terms struct. + */ + function _checkRefinanceLoanTerms(uint256 loanId, Terms memory loanTerms) private view { + LOAN storage loan = LOANs[loanId]; + + // Check that the credit asset is the same as in the original loan + // Note: Address check is enough because the asset has always ERC20 category and zero id. + // Amount can be different, but nonzero. + if ( + loan.creditAddress != loanTerms.credit.assetAddress || + loanTerms.credit.amount == 0 + ) revert RefinanceCreditMismatch(); + + // Check that the collateral is identical to the original one + if ( + loan.collateral.category != loanTerms.collateral.category || + loan.collateral.assetAddress != loanTerms.collateral.assetAddress || + loan.collateral.id != loanTerms.collateral.id || + loan.collateral.amount != loanTerms.collateral.amount + ) revert RefinanceCollateralMismatch(); + + // Check that the borrower is the same as in the original loan + if (loan.borrower != loanTerms.borrower) { + revert RefinanceBorrowerMismatch({ + currentBorrower: loan.borrower, + newBorrower: loanTerms.borrower + }); + } } /** @@ -256,7 +370,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { function _createLoan( bytes32 proposalHash, address proposalContract, - Terms calldata loanTerms, + Terms memory loanTerms, bytes calldata extra ) private returns (uint256 loanId) { // Mint LOAN token for lender @@ -290,15 +404,8 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { * @notice Transfer collateral to Vault and credit to borrower. * @dev The function assumes a prior token approval to a contract address or signed permits. * @param loanTerms Loan terms struct. - * @param permit Callers credit permit data. */ - function _settleNewLoan( - Terms calldata loanTerms, - Permit calldata permit - ) private { - // Execute permit for the caller - _tryPermit(permit); - + function _settleNewLoan(Terms memory loanTerms) private { // Transfer collateral to Vault _pull(loanTerms.collateral, loanTerms.borrower); @@ -322,91 +429,6 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { _pushFrom(creditHelper, loanTerms.lender, loanTerms.borrower); } - - /*----------------------------------------------------------*| - |* # REFINANCE LOAN *| - |*----------------------------------------------------------*/ - - /** - * @notice Refinance a loan by repaying the original loan and creating a new one. - * @dev If the new lender is the same as the current LOAN owner, - * the function will transfer only the surplus to the borrower, if any. - * If the new loan amount is not enough to cover the original loan, the borrower needs to contribute. - * The function assumes a prior token approval to a contract address or signed permits. - * @param proposalHash Hash of a loan offer / request that is signed by a lender / borrower. Used to uniquely identify a loan offer / request. - * @param loanTerms Loan terms struct. - * @param permit Callers credit permit data. - * @param extra Auxiliary data that are emitted in the loan creation event. They are not used in the contract logic. - * @return refinancedLoanId Id of the refinanced LOAN token. - */ - function refinanceLOAN( - uint256 loanId, - bytes32 proposalHash, - Terms calldata loanTerms, - Permit calldata permit, - bytes calldata extra - ) external returns (uint256 refinancedLoanId) { - // Check that caller is loan proposal contract - if (!hub.hasTag(msg.sender, PWNHubTags.LOAN_PROPOSAL)) { - revert CallerMissingHubTag(PWNHubTags.LOAN_PROPOSAL); - } - - LOAN storage loan = LOANs[loanId]; - - // Check that the original loan can be repaid, revert if not - _checkLoanCanBeRepaid(loan.status, loan.defaultTimestamp); - - // Check refinance loan terms - _checkRefinanceLoanTerms(loanId, loanTerms); - - // Create a new loan - refinancedLoanId = _createLoan({ - proposalHash: proposalHash, - proposalContract: msg.sender, - loanTerms: loanTerms, - extra: extra - }); - - // Refinance the original loan - _refinanceOriginalLoan(loanId, loanTerms, permit); - - emit LOANRefinanced({ loanId: loanId, refinancedLoanId: refinancedLoanId }); - } - - /** - * @notice Check if the loan terms are valid for refinancing. - * @dev The function will revert if the loan terms are not valid for refinancing. - * @param loanId Original loan id. - * @param loanTerms Refinancing loan terms struct. - */ - function _checkRefinanceLoanTerms(uint256 loanId, Terms calldata loanTerms) private view { - LOAN storage loan = LOANs[loanId]; - - // Check that the credit asset is the same as in the original loan - // Note: Address check is enough because the asset has always ERC20 category and zero id. - // Amount can be different, but nonzero. - if ( - loan.creditAddress != loanTerms.credit.assetAddress || - loanTerms.credit.amount == 0 - ) revert RefinanceCreditMismatch(); - - // Check that the collateral is identical to the original one - if ( - loan.collateral.category != loanTerms.collateral.category || - loan.collateral.assetAddress != loanTerms.collateral.assetAddress || - loan.collateral.id != loanTerms.collateral.id || - loan.collateral.amount != loanTerms.collateral.amount - ) revert RefinanceCollateralMismatch(); - - // Check that the borrower is the same as in the original loan - if (loan.borrower != loanTerms.borrower) { - revert RefinanceBorrowerMismatch({ - currentBorrower: loan.borrower, - newBorrower: loanTerms.borrower - }); - } - } - /** * @notice Repay the original loan and transfer the surplus to the borrower if any. * @dev If the new lender is the same as the current LOAN owner, @@ -415,12 +437,10 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { * The function assumes a prior token approval to a contract address or signed permits. * @param loanId Id of a loan that is being refinanced. * @param loanTerms Loan terms struct. - * @param permit Callers credit permit data. */ function _refinanceOriginalLoan( uint256 loanId, - Terms calldata loanTerms, - Permit calldata permit + Terms memory loanTerms ) private { uint256 repaymentAmount = _loanRepaymentAmount(loanId); @@ -432,8 +452,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { repayLoanDirectly: repayLoanDirectly, loanOwner: loanOwner, repaymentAmount: repaymentAmount, - loanTerms: loanTerms, - permit: permit + loanTerms: loanTerms }); } @@ -446,20 +465,15 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { * @param loanOwner Address of the current LOAN owner. * @param repaymentAmount Amount of the original loan to be repaid. * @param loanTerms Loan terms struct. - * @param permit Callers credit permit data. */ function _settleLoanRefinance( bool repayLoanDirectly, address loanOwner, uint256 repaymentAmount, - Terms calldata loanTerms, - Permit calldata permit + Terms memory loanTerms ) private { MultiToken.Asset memory creditHelper = loanTerms.credit; - // Execute permit for the caller - _tryPermit(permit); - // Collect fees (uint256 feeAmount, uint256 newLoanAmount) = PWNFeeCalculator.calculateFeeAmount(config.fee(), loanTerms.credit.amount); @@ -530,15 +544,15 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { (bool repayLoanDirectly, address loanOwner) = _deleteOrUpdateRepaidLoan(loanId); - _settleLoanRepayment({ - repayLoanDirectly: repayLoanDirectly, - loanOwner: loanOwner, - repayingAddress: msg.sender, - borrower: borrower, - repaymentCredit: repaymentCredit, - collateral: collateral, - permit: permit - }); + // Execute permit for the caller + _checkPermit(msg.sender, repaymentCredit.assetAddress, permit); + _tryPermit(permit); + + // Transfer credit to the original lender or to the Vault + _transferLoanRepayment(repayLoanDirectly, repaymentCredit, msg.sender, loanOwner); + + // Transfer collateral back to borrower + _push(collateral, borrower); } /** @@ -592,36 +606,6 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { } } - /** - * @notice Settle the loan repayment. - * @dev The function assumes a prior token approval to a contract address or a signed permit. - * @param repayLoanDirectly If the loan can be repaid directly to the current LOAN owner. - * @param loanOwner Address of the current LOAN owner. - * @param repayingAddress Address of the account repaying the loan. - * @param borrower Address of the borrower associated with the loan. - * @param repaymentCredit Credit asset to be repaid. - * @param collateral Collateral to be transferred back to the borrower. - * @param permit Callers credit permit data. - */ - function _settleLoanRepayment( - bool repayLoanDirectly, - address loanOwner, - address repayingAddress, - address borrower, - MultiToken.Asset memory repaymentCredit, - MultiToken.Asset memory collateral, - Permit calldata permit - ) private { - // Execute permit for the caller - _tryPermit(permit); - - // Transfer credit to the original lender or to the Vault - _transferLoanRepayment(repayLoanDirectly, repaymentCredit, repayingAddress, loanOwner); - - // Transfer collateral back to borrower - _push(collateral, borrower); - } - /** * @notice Transfer the repaid credit to the original lender or to the Vault. * @param repayLoanDirectly If the loan can be repaid directly to the current LOAN owner. @@ -869,6 +853,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { _checkValidAsset(compensation); // Transfer compensation to the loan owner + _checkPermit(msg.sender, extension.compensationAddress, permit); _tryPermit(permit); _pushFrom(compensation, loan.borrower, loanOwner); } diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol index e1cbb51..e1b2ae9 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol @@ -7,7 +7,6 @@ import { Math } from "openzeppelin-contracts/contracts/utils/math/Math.sol"; import { PWNSimpleLoan } from "@pwn/loan/terms/simple/loan/PWNSimpleLoan.sol"; import { PWNSimpleLoanProposal } from "@pwn/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol"; -import { Permit } from "@pwn/loan/vault/Permit.sol"; import "@pwn/PWNErrors.sol"; @@ -121,6 +120,29 @@ contract PWNSimpleLoanDutchAuctionProposal is PWNSimpleLoanProposal { emit ProposalMade(proposalHash, proposal.proposer, proposal); } + /** + * @notice Encode proposal data. + * @param proposal Proposal struct to be encoded. + * @param proposalValues ProposalValues struct to be encoded. + * @return Encoded proposal data. + */ + function encodeProposalData( + Proposal memory proposal, + ProposalValues memory proposalValues + ) external pure returns (bytes memory) { + return abi.encode(proposal, proposalValues); + } + + /** + * @notice Decode proposal data. + * @param proposalData Encoded proposal data. + * @return Decoded proposal struct. + * @return Decoded proposal values struct. + */ + function decodeProposalData(bytes memory proposalData) public pure returns (Proposal memory, ProposalValues memory) { + return abi.decode(proposalData, (Proposal, ProposalValues)); + } + /** * @notice Get credit amount for an auction in a specific timestamp. * @dev Auction runs one minute longer than `auctionDuration` to have `maxCreditAmount` value in the last minute. @@ -128,7 +150,7 @@ contract PWNSimpleLoanDutchAuctionProposal is PWNSimpleLoanProposal { * @param timestamp Timestamp to calculate auction credit amount for. * @return Credit amount in the auction for provided timestamp. */ - function getCreditAmount(Proposal calldata proposal, uint256 timestamp) public pure returns (uint256) { + function getCreditAmount(Proposal memory proposal, uint256 timestamp) public pure returns (uint256) { // Check proposal if (proposal.auctionDuration < 1 minutes) { revert InvalidAuctionDuration({ @@ -181,92 +203,19 @@ contract PWNSimpleLoanDutchAuctionProposal is PWNSimpleLoanProposal { } /** - * @notice Accept a proposal with a callers nonce revocation. - * @dev Function will mark callers nonce as revoked. - * @param proposal Proposal struct containing all proposal data. - * @param proposalValues Proposal values struct containing concrete proposal values. - * @param signature Proposal signature signed by a proposer. - * @param permit Callers permit data. - * @param refinancingLoanId Id of a loan to be refinanced. 0 if creating a new loan. - * @param extra Auxiliary data that are emitted in the loan creation event. They are not used in the contract logic. - * @param callersNonceSpace Nonce space of a callers nonce. - * @param callersNonceToRevoke Nonce to revoke. - * @return loanId Id of a created loan. + * @inheritdoc PWNSimpleLoanProposal */ function acceptProposal( - Proposal calldata proposal, - ProposalValues calldata proposalValues, - bytes calldata signature, + address acceptor, uint256 refinancingLoanId, - Permit calldata permit, - bytes calldata extra, - uint256 callersNonceSpace, - uint256 callersNonceToRevoke - ) external returns (uint256 loanId) { - _revokeCallersNonce(msg.sender, callersNonceSpace, callersNonceToRevoke); - return acceptProposal(proposal, proposalValues, signature, refinancingLoanId, permit, extra); - } - - /** - * @notice Accept a proposal. - * @param proposal Proposal struct containing all proposal data. - * @param proposalValues Proposal values struct containing concrete proposal values. - * @param signature Proposal signature signed by a proposer. - * @param refinancingLoanId Id of a loan to be refinanced. 0 if creating a new loan. - * @param permit Callers permit data. - * @param extra Auxiliary data that are emitted in the loan creation event. They are not used in the contract logic. - * @return loanId Id of a created loan. - */ - function acceptProposal( - Proposal calldata proposal, - ProposalValues calldata proposalValues, - bytes calldata signature, - uint256 refinancingLoanId, - Permit calldata permit, - bytes calldata extra - ) public returns (uint256 loanId) { - // Check refinancing id - _checkRefinancingLoanId(refinancingLoanId, proposal.refinancingLoanId, proposal.isOffer); - - // Check permit - _checkPermit(msg.sender, proposal.creditAddress, permit); - - // Accept proposal - (bytes32 proposalHash, PWNSimpleLoan.Terms memory loanTerms) - = _acceptProposal(proposal, proposalValues, signature); - - if (refinancingLoanId == 0) { - // Create loan - return PWNSimpleLoan(proposal.loanContract).createLOAN({ - proposalHash: proposalHash, - loanTerms: loanTerms, - permit: permit, - extra: extra - }); - } else { - // Refinance loan - return PWNSimpleLoan(proposal.loanContract).refinanceLOAN({ - loanId: refinancingLoanId, - proposalHash: proposalHash, - loanTerms: loanTerms, - permit: permit, - extra: extra - }); - } - } - - - /*----------------------------------------------------------*| - |* # INTERNALS *| - |*----------------------------------------------------------*/ - - function _acceptProposal( - Proposal calldata proposal, - ProposalValues calldata proposalValues, + bytes calldata proposalData, bytes calldata signature - ) private returns (bytes32 proposalHash, PWNSimpleLoan.Terms memory loanTerms) { - // Check if the loan contract has a tag - _checkLoanContractTag(proposal.loanContract); + ) override external returns (bytes32 proposalHash, PWNSimpleLoan.Terms memory loanTerms) { + // Decode proposal data + (Proposal memory proposal, ProposalValues memory proposalValues) = decodeProposalData(proposalData); + + // Make proposal hash + proposalHash = _getProposalHash(PROPOSAL_TYPEHASH, abi.encode(proposal)); // Calculate current credit amount uint256 creditAmount = getCreditAmount(proposal, block.timestamp); @@ -296,51 +245,34 @@ contract PWNSimpleLoanDutchAuctionProposal is PWNSimpleLoanProposal { } } - // Check collateral state fingerprint if needed - if (proposal.checkCollateralStateFingerprint) { - _checkCollateralState({ - addr: proposal.collateralAddress, - id: proposal.collateralId, - stateFingerprint: proposal.collateralStateFingerprint - }); - } - // Try to accept proposal - proposalHash = _tryAcceptProposal(proposal, creditAmount, signature); + _acceptProposal( + acceptor, + refinancingLoanId, + proposalHash, + signature, + ProposalBase({ + collateralAddress: proposal.collateralAddress, + collateralId: proposal.collateralId, + checkCollateralStateFingerprint: proposal.checkCollateralStateFingerprint, + collateralStateFingerprint: proposal.collateralStateFingerprint, + creditAmount: creditAmount, + availableCreditLimit: proposal.availableCreditLimit, + expiration: proposal.auctionStart + proposal.auctionDuration + 1 minutes, + allowedAcceptor: proposal.allowedAcceptor, + proposer: proposal.proposer, + isOffer: proposal.isOffer, + refinancingLoanId: proposal.refinancingLoanId, + nonceSpace: proposal.nonceSpace, + nonce: proposal.nonce, + loanContract: proposal.loanContract + }) + ); // Create loan terms object - loanTerms = _createLoanTerms(proposal, creditAmount); - } - - function _tryAcceptProposal( - Proposal calldata proposal, - uint256 creditAmount, - bytes calldata signature - ) private returns (bytes32 proposalHash) { - proposalHash = getProposalHash(proposal); - _tryAcceptProposal({ - proposalHash: proposalHash, - creditAmount: creditAmount, - availableCreditLimit: proposal.availableCreditLimit, - apr: proposal.accruingInterestAPR, - duration: proposal.duration, - expiration: proposal.auctionStart + proposal.auctionDuration + 1 minutes, - nonceSpace: proposal.nonceSpace, - nonce: proposal.nonce, - allowedAcceptor: proposal.allowedAcceptor, - acceptor: msg.sender, - signer: proposal.proposer, - signature: signature - }); - } - - function _createLoanTerms( - Proposal calldata proposal, - uint256 creditAmount - ) private view returns (PWNSimpleLoan.Terms memory) { - return PWNSimpleLoan.Terms({ - lender: proposal.isOffer ? proposal.proposer : msg.sender, - borrower: proposal.isOffer ? msg.sender : proposal.proposer, + loanTerms = PWNSimpleLoan.Terms({ + lender: proposal.isOffer ? proposal.proposer : acceptor, + borrower: proposal.isOffer ? acceptor : proposal.proposer, duration: proposal.duration, collateral: MultiToken.Asset({ category: proposal.collateralCategory, diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol index 898a36e..d36776a 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol @@ -7,7 +7,6 @@ import { Math } from "openzeppelin-contracts/contracts/utils/math/Math.sol"; import { PWNSimpleLoan } from "@pwn/loan/terms/simple/loan/PWNSimpleLoan.sol"; import { PWNSimpleLoanProposal } from "@pwn/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol"; -import { Permit } from "@pwn/loan/vault/Permit.sol"; import "@pwn/PWNErrors.sol"; @@ -35,7 +34,7 @@ contract PWNSimpleLoanFungibleProposal is PWNSimpleLoanProposal { /** * @notice Construct defining a fungible proposal. - * @param collateralCategory Category of an asset used as a collateral (0 == ERC20, 2 == ERC1155). + * @param collateralCategory Category of an asset used as a collateral (0 == ERC20, 1 == ERC721, 2 == ERC1155). * @param collateralAddress Address of an asset used as a collateral. * @param collateralId Token id of an asset used as a collateral, in case of ERC20 should be 0. * @param minCollateralAmount Minimal amount of tokens used as a collateral. @@ -120,6 +119,29 @@ contract PWNSimpleLoanFungibleProposal is PWNSimpleLoanProposal { emit ProposalMade(proposalHash, proposal.proposer, proposal); } + /** + * @notice Encode proposal data. + * @param proposal Proposal struct to be encoded. + * @param proposalValues ProposalValues struct to be encoded. + * @return Encoded proposal data. + */ + function encodeProposalData( + Proposal memory proposal, + ProposalValues memory proposalValues + ) external pure returns (bytes memory) { + return abi.encode(proposal, proposalValues); + } + + /** + * @notice Decode proposal data. + * @param proposalData Encoded proposal data. + * @return Decoded proposal struct. + * @return Decoded proposal values struct. + */ + function decodeProposalData(bytes memory proposalData) public pure returns (Proposal memory, ProposalValues memory) { + return abi.decode(proposalData, (Proposal, ProposalValues)); + } + /** * @notice Compute credit amount from collateral amount and credit per collateral unit. * @param collateralAmount Amount of collateral. @@ -131,92 +153,19 @@ contract PWNSimpleLoanFungibleProposal is PWNSimpleLoanProposal { } /** - * @notice Accept a proposal with a callers nonce revocation. - * @dev Function will mark callers nonce as revoked. - * @param proposal Proposal struct containing all proposal data. - * @param proposalValues ProposalValues struct specifying all flexible proposal values. - * @param signature Proposal signature signed by a proposer. - * @param refinancingLoanId Id of a loan to be refinanced. 0 if creating a new loan. - * @param permit Callers permit data. - * @param extra Auxiliary data that are emitted in the loan creation event. They are not used in the contract logic. - * @param callersNonceSpace Nonce space of a callers nonce. - * @param callersNonceToRevoke Nonce to revoke. - * @return loanId Id of a created loan. + * @inheritdoc PWNSimpleLoanProposal */ function acceptProposal( - Proposal calldata proposal, - ProposalValues calldata proposalValues, - bytes calldata signature, + address acceptor, uint256 refinancingLoanId, - Permit calldata permit, - bytes calldata extra, - uint256 callersNonceSpace, - uint256 callersNonceToRevoke - ) external returns (uint256 loanId) { - _revokeCallersNonce(msg.sender, callersNonceSpace, callersNonceToRevoke); - return acceptProposal(proposal, proposalValues, signature, refinancingLoanId, permit, extra); - } - - /** - * @notice Accept a proposal. - * @param proposal Proposal struct containing all proposal data. - * @param proposalValues ProposalValues struct specifying all flexible proposal values. - * @param signature Proposal signature signed by a proposer. - * @param refinancingLoanId Id of a loan to be refinanced. 0 if creating a new loan. - * @param permit Callers permit data. - * @param extra Auxiliary data that are emitted in the loan creation event. They are not used in the contract logic. - * @return loanId Id of a created loan. - */ - function acceptProposal( - Proposal calldata proposal, - ProposalValues calldata proposalValues, - bytes calldata signature, - uint256 refinancingLoanId, - Permit calldata permit, - bytes calldata extra - ) public returns (uint256 loanId) { - // Check refinancing id - _checkRefinancingLoanId(refinancingLoanId, proposal.refinancingLoanId, proposal.isOffer); - - // Check permit - _checkPermit(msg.sender, proposal.creditAddress, permit); - - // Accept proposal - (bytes32 proposalHash, PWNSimpleLoan.Terms memory loanTerms) - = _acceptProposal(proposal, proposalValues, signature); - - if (refinancingLoanId == 0) { - // Create loan - return PWNSimpleLoan(proposal.loanContract).createLOAN({ - proposalHash: proposalHash, - loanTerms: loanTerms, - permit: permit, - extra: extra - }); - } else { - // Refinance loan - return PWNSimpleLoan(proposal.loanContract).refinanceLOAN({ - loanId: refinancingLoanId, - proposalHash: proposalHash, - loanTerms: loanTerms, - permit: permit, - extra: extra - }); - } - } - - - /*----------------------------------------------------------*| - |* # INTERNALS *| - |*----------------------------------------------------------*/ - - function _acceptProposal( - Proposal calldata proposal, - ProposalValues calldata proposalValues, + bytes calldata proposalData, bytes calldata signature - ) private returns (bytes32 proposalHash, PWNSimpleLoan.Terms memory loanTerms) { - // Check if the loan contract has a tag - _checkLoanContractTag(proposal.loanContract); + ) override external returns (bytes32 proposalHash, PWNSimpleLoan.Terms memory loanTerms) { + // Decode proposal data + (Proposal memory proposal, ProposalValues memory proposalValues) = decodeProposalData(proposalData); + + // Make proposal hash + proposalHash = _getProposalHash(PROPOSAL_TYPEHASH, abi.encode(proposal)); // Check min collateral amount if (proposal.minCollateralAmount == 0) { @@ -229,61 +178,43 @@ contract PWNSimpleLoanFungibleProposal is PWNSimpleLoanProposal { }); } - // Check collateral state fingerprint if needed - if (proposal.checkCollateralStateFingerprint) { - _checkCollateralState({ - addr: proposal.collateralAddress, - id: proposal.collateralId, - stateFingerprint: proposal.collateralStateFingerprint - }); - } - // Calculate credit amount uint256 creditAmount = getCreditAmount(proposalValues.collateralAmount, proposal.creditPerCollateralUnit); // Try to accept proposal - proposalHash = _tryAcceptProposal(proposal, creditAmount, signature); + _acceptProposal( + acceptor, + refinancingLoanId, + proposalHash, + signature, + ProposalBase({ + collateralAddress: proposal.collateralAddress, + collateralId: proposal.collateralId, + checkCollateralStateFingerprint: proposal.checkCollateralStateFingerprint, + collateralStateFingerprint: proposal.collateralStateFingerprint, + creditAmount: creditAmount, + availableCreditLimit: proposal.availableCreditLimit, + expiration: proposal.expiration, + allowedAcceptor: proposal.allowedAcceptor, + proposer: proposal.proposer, + isOffer: proposal.isOffer, + refinancingLoanId: proposal.refinancingLoanId, + nonceSpace: proposal.nonceSpace, + nonce: proposal.nonce, + loanContract: proposal.loanContract + }) + ); // Create loan terms object - loanTerms = _createLoanTerms(proposal, proposalValues.collateralAmount, creditAmount); - } - - function _tryAcceptProposal( - Proposal calldata proposal, - uint256 creditAmount, - bytes calldata signature - ) private returns (bytes32 proposalHash) { - proposalHash = getProposalHash(proposal); - _tryAcceptProposal({ - proposalHash: proposalHash, - creditAmount: creditAmount, - availableCreditLimit: proposal.availableCreditLimit, - apr: proposal.accruingInterestAPR, - duration: proposal.duration, - expiration: proposal.expiration, - nonceSpace: proposal.nonceSpace, - nonce: proposal.nonce, - allowedAcceptor: proposal.allowedAcceptor, - acceptor: msg.sender, - signer: proposal.proposer, - signature: signature - }); - } - - function _createLoanTerms( - Proposal calldata proposal, - uint256 collateralAmount, - uint256 creditAmount - ) private view returns (PWNSimpleLoan.Terms memory) { - return PWNSimpleLoan.Terms({ - lender: proposal.isOffer ? proposal.proposer : msg.sender, - borrower: proposal.isOffer ? msg.sender : proposal.proposer, + loanTerms = PWNSimpleLoan.Terms({ + lender: proposal.isOffer ? proposal.proposer : acceptor, + borrower: proposal.isOffer ? acceptor : proposal.proposer, duration: proposal.duration, collateral: MultiToken.Asset({ category: proposal.collateralCategory, assetAddress: proposal.collateralAddress, id: proposal.collateralId, - amount: collateralAmount + amount: proposalValues.collateralAmount }), credit: MultiToken.ERC20({ assetAddress: proposal.creditAddress, diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol index 488f757..80e776f 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol @@ -7,7 +7,6 @@ import { MerkleProof } from "openzeppelin-contracts/contracts/utils/cryptography import { PWNSimpleLoan } from "@pwn/loan/terms/simple/loan/PWNSimpleLoan.sol"; import { PWNSimpleLoanProposal } from "@pwn/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol"; -import { Permit } from "@pwn/loan/vault/Permit.sol"; import "@pwn/PWNErrors.sol"; @@ -119,153 +118,85 @@ contract PWNSimpleLoanListProposal is PWNSimpleLoanProposal { } /** - * @notice Accept a proposal with a callers nonce revocation. - * @dev Function will mark callers nonce as revoked. - * @param proposal Proposal struct containing all proposal data. - * @param proposalValues ProposalValues struct specifying all flexible proposal values. - * @param signature Proposal signature signed by a proposer. - * @param refinancingLoanId Id of a loan to be refinanced. 0 if creating a new loan. - * @param permit Callers permit data. - * @param extra Auxiliary data that are emitted in the loan creation event. They are not used in the contract logic. - * @param callersNonceSpace Nonce space of a callers nonce. - * @param callersNonceToRevoke Nonce to revoke. - * @return loanId Id of a created loan. + * @notice Encode proposal data. + * @param proposal Proposal struct to be encoded. + * @param proposalValues ProposalValues struct to be encoded. + * @return Encoded proposal data. */ - function acceptProposal( - Proposal calldata proposal, - ProposalValues calldata proposalValues, - bytes calldata signature, - uint256 refinancingLoanId, - Permit calldata permit, - bytes calldata extra, - uint256 callersNonceSpace, - uint256 callersNonceToRevoke - ) external returns (uint256 loanId) { - _revokeCallersNonce(msg.sender, callersNonceSpace, callersNonceToRevoke); - return acceptProposal(proposal, proposalValues, signature, refinancingLoanId, permit, extra); + function encodeProposalData( + Proposal memory proposal, + ProposalValues memory proposalValues + ) external pure returns (bytes memory) { + return abi.encode(proposal, proposalValues); } /** - * @notice Accept a proposal. - * @param proposal Proposal struct containing all proposal data. - * @param proposalValues ProposalValues struct specifying all flexible proposal values. - * @param signature Proposal signature signed by a proposer. - * @param refinancingLoanId Id of a loan to be refinanced. 0 if creating a new loan. - * @param permit Callers permit data. - * @param extra Auxiliary data that are emitted in the loan creation event. They are not used in the contract logic. - * @return loanId Id of a created loan. + * @notice Decode proposal data. + * @param proposalData Encoded proposal data. + * @return Decoded proposal struct. + * @return Decoded proposal values struct. */ - function acceptProposal( - Proposal calldata proposal, - ProposalValues calldata proposalValues, - bytes calldata signature, - uint256 refinancingLoanId, - Permit calldata permit, - bytes calldata extra - ) public returns (uint256 loanId) { - // Check refinancing id - _checkRefinancingLoanId(refinancingLoanId, proposal.refinancingLoanId, proposal.isOffer); - - // Check permit - _checkPermit(msg.sender, proposal.creditAddress, permit); - - // Accept proposal - (bytes32 proposalHash, PWNSimpleLoan.Terms memory loanTerms) - = _acceptProposal(proposal, proposalValues, signature); - - if (refinancingLoanId == 0) { - // Create loan - return PWNSimpleLoan(proposal.loanContract).createLOAN({ - proposalHash: proposalHash, - loanTerms: loanTerms, - permit: permit, - extra: extra - }); - } else { - // Refinance loan - return PWNSimpleLoan(proposal.loanContract).refinanceLOAN({ - loanId: refinancingLoanId, - proposalHash: proposalHash, - loanTerms: loanTerms, - permit: permit, - extra: extra - }); - } + function decodeProposalData(bytes memory proposalData) public pure returns (Proposal memory, ProposalValues memory) { + return abi.decode(proposalData, (Proposal, ProposalValues)); } - - /*----------------------------------------------------------*| - |* # INTERNALS *| - |*----------------------------------------------------------*/ - - function _acceptProposal( - Proposal calldata proposal, - ProposalValues calldata proposalValues, + /** + * @inheritdoc PWNSimpleLoanProposal + */ + function acceptProposal( + address acceptor, + uint256 refinancingLoanId, + bytes calldata proposalData, bytes calldata signature - ) private returns (bytes32 proposalHash, PWNSimpleLoan.Terms memory loanTerms) { - // Check if the loan contract has a tag - _checkLoanContractTag(proposal.loanContract); + ) override external returns (bytes32 proposalHash, PWNSimpleLoan.Terms memory loanTerms) { + // Decode proposal data + (Proposal memory proposal, ProposalValues memory proposalValues) = decodeProposalData(proposalData); + + // Make proposal hash + proposalHash = _getProposalHash(PROPOSAL_TYPEHASH, abi.encode(proposal)); // Check provided collateral id if (proposal.collateralIdsWhitelistMerkleRoot != bytes32(0)) { - _checkCollateralId(proposal, proposalValues); + // Verify whitelisted collateral id + if ( + !MerkleProof.verify({ + proof: proposalValues.merkleInclusionProof, + root: proposal.collateralIdsWhitelistMerkleRoot, + leaf: keccak256(abi.encodePacked(proposalValues.collateralId)) + }) + ) revert CollateralIdNotWhitelisted({ id: proposalValues.collateralId }); } - // Note: If the `collateralIdsWhitelistMerkleRoot` is empty, then it is a collection proposal - // and any collateral id can be used. - - // Check collateral state fingerprint if needed - if (proposal.checkCollateralStateFingerprint) { - _checkCollateralState({ - addr: proposal.collateralAddress, - id: proposalValues.collateralId, - stateFingerprint: proposal.collateralStateFingerprint - }); - } + // Note: If the `collateralIdsWhitelistMerkleRoot` is empty, any collateral id can be used. // Try to accept proposal - proposalHash = _tryAcceptProposal(proposal, signature); - - // Create loan terms object - loanTerms = _createLoanTerms(proposal, proposalValues); - } - - function _checkCollateralId(Proposal calldata proposal, ProposalValues calldata proposalValues) private pure { - // Verify whitelisted collateral id - if ( - !MerkleProof.verify({ - proof: proposalValues.merkleInclusionProof, - root: proposal.collateralIdsWhitelistMerkleRoot, - leaf: keccak256(abi.encodePacked(proposalValues.collateralId)) + _acceptProposal( + acceptor, + refinancingLoanId, + proposalHash, + signature, + ProposalBase({ + collateralAddress: proposal.collateralAddress, + collateralId: proposalValues.collateralId, + checkCollateralStateFingerprint: proposal.checkCollateralStateFingerprint, + collateralStateFingerprint: proposal.collateralStateFingerprint, + creditAmount: proposal.creditAmount, + availableCreditLimit: proposal.availableCreditLimit, + expiration: proposal.expiration, + allowedAcceptor: proposal.allowedAcceptor, + proposer: proposal.proposer, + isOffer: proposal.isOffer, + refinancingLoanId: proposal.refinancingLoanId, + nonceSpace: proposal.nonceSpace, + nonce: proposal.nonce, + loanContract: proposal.loanContract }) - ) revert CollateralIdNotWhitelisted({ id: proposalValues.collateralId }); - } + ); - function _tryAcceptProposal(Proposal calldata proposal, bytes calldata signature) private returns (bytes32 proposalHash) { - proposalHash = getProposalHash(proposal); - _tryAcceptProposal({ - proposalHash: proposalHash, - creditAmount: proposal.creditAmount, - availableCreditLimit: proposal.availableCreditLimit, - apr: proposal.accruingInterestAPR, - duration: proposal.duration, - expiration: proposal.expiration, - nonceSpace: proposal.nonceSpace, - nonce: proposal.nonce, - allowedAcceptor: proposal.allowedAcceptor, - acceptor: msg.sender, - signer: proposal.proposer, - signature: signature - }); - } - - function _createLoanTerms( - Proposal calldata proposal, - ProposalValues calldata proposalValues - ) private view returns (PWNSimpleLoan.Terms memory) { - return PWNSimpleLoan.Terms({ - lender: proposal.isOffer ? proposal.proposer : msg.sender, - borrower: proposal.isOffer ? msg.sender : proposal.proposer, + // Create loan terms object + loanTerms = PWNSimpleLoan.Terms({ + lender: proposal.isOffer ? proposal.proposer : acceptor, + borrower: proposal.isOffer ? acceptor : proposal.proposer, duration: proposal.duration, collateral: MultiToken.Asset({ category: proposal.collateralCategory, diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol index 0ab4bf2..1de93c5 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol @@ -5,7 +5,7 @@ import { PWNConfig, IERC5646 } from "@pwn/config/PWNConfig.sol"; import { PWNHub } from "@pwn/hub/PWNHub.sol"; import { PWNHubTags } from "@pwn/hub/PWNHubTags.sol"; import { PWNSignatureChecker } from "@pwn/loan/lib/PWNSignatureChecker.sol"; -import { Permit } from "@pwn/loan/vault/Permit.sol"; +import { PWNSimpleLoan } from "@pwn/loan/terms/simple/loan/PWNSimpleLoan.sol"; import { PWNRevokedNonce } from "@pwn/nonce/PWNRevokedNonce.sol"; import "@pwn/PWNErrors.sol"; @@ -15,15 +15,29 @@ import "@pwn/PWNErrors.sol"; */ abstract contract PWNSimpleLoanProposal { - uint32 public constant MIN_LOAN_DURATION = 10 minutes; - uint40 public constant MAX_ACCRUING_INTEREST_APR = 1e11; // 1,000,000% APR - bytes32 public immutable DOMAIN_SEPARATOR; PWNHub public immutable hub; PWNRevokedNonce public immutable revokedNonce; PWNConfig public immutable config; + struct ProposalBase { + address collateralAddress; + uint256 collateralId; + bool checkCollateralStateFingerprint; + bytes32 collateralStateFingerprint; + uint256 creditAmount; + uint256 availableCreditLimit; + uint40 expiration; + address allowedAcceptor; + address proposer; + bool isOffer; + uint256 refinancingLoanId; + uint256 nonceSpace; + uint256 nonce; + address loanContract; + } + /** * @dev Mapping of proposals made via on-chain transactions. * Could be used by contract wallets instead of EIP-1271. @@ -58,6 +72,10 @@ abstract contract PWNSimpleLoanProposal { } + /*----------------------------------------------------------*| + |* # EXTERNALS *| + |*----------------------------------------------------------*/ + /** * @notice Helper function for revoking a proposal nonce on behalf of a caller. * @param nonceSpace Nonce space of a proposal nonce to be revoked. @@ -67,202 +85,146 @@ abstract contract PWNSimpleLoanProposal { revokedNonce.revokeNonce(msg.sender, nonceSpace, nonce); } + /** + * @notice Accept a proposal and create new loan terms. + * @dev Function can be called only by a loan contract with appropriate PWN Hub tag. + * @param acceptor Address of a proposal acceptor. + * @param refinancingLoanId Id of a loan to be refinanced. 0 if creating a new loan. + * @param proposalData Encoded proposal data with signature. + * @return proposalHash Proposal hash. + * @return loanTerms Loan terms. + */ + function acceptProposal( + address acceptor, + uint256 refinancingLoanId, + bytes calldata proposalData, + bytes calldata signature + ) virtual external returns (bytes32 proposalHash, PWNSimpleLoan.Terms memory loanTerms); + /*----------------------------------------------------------*| |* # INTERNALS *| |*----------------------------------------------------------*/ /** - * @notice Try to accept a proposal. + * @notice Get a proposal hash according to EIP-712. + * @param encodedProposal Encoded proposal struct. + * @return Struct hash. + */ + function _getProposalHash( + bytes32 proposalTypehash, + bytes memory encodedProposal + ) internal view returns (bytes32) { + return keccak256(abi.encodePacked( + hex"1901", DOMAIN_SEPARATOR, keccak256(abi.encodePacked( + proposalTypehash, encodedProposal + )) + )); + } + + /** + * @notice Make an on-chain proposal. + * @dev Function will mark a proposal hash as proposed. + * @param proposalHash Proposal hash. + * @param proposer Address of a proposal proposer. + */ + function _makeProposal(bytes32 proposalHash, address proposer) internal { + if (msg.sender != proposer) { + revert CallerIsNotStatedProposer(proposer); + } + + proposalsMade[proposalHash] = true; + } + + /** + * @notice Try to accept proposal base. + * @param acceptor Address of a proposal acceptor. + * @param refinancingLoanId Refinancing loan ID. * @param proposalHash Proposal hash. - * @param creditAmount Amount of credit to be used. - * @param availableCreditLimit Available credit limit. - * @param apr Accruing interest APR. - * @param duration Loan duration. - * @param expiration Proposal expiration. - * @param nonceSpace Nonce space of a proposal nonce. - * @param nonce Proposal nonce. - * @param allowedAcceptor Allowed acceptor address. - * @param acceptor Acctual acceptor address. - * @param signer Signer address. * @param signature Signature of a proposal. + * @param proposal Proposal base struct. */ - function _tryAcceptProposal( - bytes32 proposalHash, - uint256 creditAmount, - uint256 availableCreditLimit, - uint40 apr, - uint32 duration, - uint40 expiration, - uint256 nonceSpace, - uint256 nonce, - address allowedAcceptor, + function _acceptProposal( address acceptor, - address signer, - bytes memory signature + uint256 refinancingLoanId, + bytes32 proposalHash, + bytes memory signature, + ProposalBase memory proposal ) internal { + // Check loan contract + if (msg.sender != proposal.loanContract) { + revert CallerNotLoanContract({ caller: msg.sender, loanContract: proposal.loanContract }); + } + if (!hub.hasTag(proposal.loanContract, PWNHubTags.ACTIVE_LOAN)) { + revert AddressMissingHubTag({ addr: proposal.loanContract, tag: PWNHubTags.ACTIVE_LOAN }); + } + // Check proposal has been made via on-chain tx, EIP-1271 or signed off-chain if (!proposalsMade[proposalHash]) { - if (!PWNSignatureChecker.isValidSignatureNow(signer, proposalHash, signature)) { - revert InvalidSignature({ signer: signer, digest: proposalHash }); + if (!PWNSignatureChecker.isValidSignatureNow(proposal.proposer, proposalHash, signature)) { + revert InvalidSignature({ signer: proposal.proposer, digest: proposalHash }); + } + } + + // Check refinancing proposal + if (refinancingLoanId == 0) { + if (proposal.refinancingLoanId != 0) { + revert InvalidRefinancingLoanId({ refinancingLoanId: proposal.refinancingLoanId }); + } + } else { + if (refinancingLoanId != proposal.refinancingLoanId) { + if (proposal.refinancingLoanId != 0 || !proposal.isOffer) { + revert InvalidRefinancingLoanId({ refinancingLoanId: proposal.refinancingLoanId }); + } } } // Check proposal is not expired - if (block.timestamp >= expiration) { - revert Expired({ current: block.timestamp, expiration: expiration }); + if (block.timestamp >= proposal.expiration) { + revert Expired({ current: block.timestamp, expiration: proposal.expiration }); } // Check proposal is not revoked - if (!revokedNonce.isNonceUsable(signer, nonceSpace, nonce)) { - revert NonceNotUsable({ addr: signer, nonceSpace: nonceSpace, nonce: nonce }); + if (!revokedNonce.isNonceUsable(proposal.proposer, proposal.nonceSpace, proposal.nonce)) { + revert NonceNotUsable({ addr: proposal.proposer, nonceSpace: proposal.nonceSpace, nonce: proposal.nonce }); } // Check propsal is accepted by an allowed address - if (allowedAcceptor != address(0) && acceptor != allowedAcceptor) { - revert CallerNotAllowedAcceptor({ current: acceptor, allowed: allowedAcceptor }); - } - - // Check minimum loan duration - if (duration < MIN_LOAN_DURATION) { - revert InvalidDuration({ current: duration, limit: MIN_LOAN_DURATION }); + if (proposal.allowedAcceptor != address(0) && acceptor != proposal.allowedAcceptor) { + revert CallerNotAllowedAcceptor({ current: acceptor, allowed: proposal.allowedAcceptor }); } - // Check maximum accruing interest APR - if (apr > MAX_ACCRUING_INTEREST_APR) { - revert AccruingInterestAPROutOfBounds({ current: apr, limit: MAX_ACCRUING_INTEREST_APR }); - } - - if (availableCreditLimit == 0) { + if (proposal.availableCreditLimit == 0) { // Revoke nonce if credit limit is 0, proposal can be accepted only once - revokedNonce.revokeNonce(signer, nonceSpace, nonce); - } else if (creditUsed[proposalHash] + creditAmount <= availableCreditLimit) { + revokedNonce.revokeNonce(proposal.proposer, proposal.nonceSpace, proposal.nonce); + } else if (creditUsed[proposalHash] + proposal.creditAmount <= proposal.availableCreditLimit) { // Increase used credit if credit limit is not exceeded - creditUsed[proposalHash] += creditAmount; + creditUsed[proposalHash] += proposal.creditAmount; } else { // Revert if credit limit is exceeded revert AvailableCreditLimitExceeded({ - used: creditUsed[proposalHash] + creditAmount, - limit: availableCreditLimit + used: creditUsed[proposalHash] + proposal.creditAmount, + limit: proposal.availableCreditLimit }); } - } - /** - * @notice Check if a collateral state fingerprint is valid. - * @param addr Address of a collateral contract. - * @param id Collateral ID. - * @param stateFingerprint Proposed state fingerprint. - */ - function _checkCollateralState(address addr, uint256 id, bytes32 stateFingerprint) internal view { - IERC5646 computer = config.getStateFingerprintComputer(addr); - if (address(computer) == address(0)) { - // Asset is not implementing ERC5646 and no computer is registered - revert MissingStateFingerprintComputer(); - } - - bytes32 currentFingerprint = computer.getStateFingerprint(id); - if (stateFingerprint != currentFingerprint) { - // Fingerprint mismatch - revert InvalidCollateralStateFingerprint({ - current: currentFingerprint, - proposed: stateFingerprint - }); - } - } - - /** - * @notice Check if a loan contract has an active loan tag. - * @param loanContract Loan contract address. - */ - function _checkLoanContractTag(address loanContract) internal view { - if (!hub.hasTag(loanContract, PWNHubTags.ACTIVE_LOAN)) { - revert AddressMissingHubTag({ addr: loanContract, tag: PWNHubTags.ACTIVE_LOAN }); - } - } - - /** - * @notice Check that permit data have correct owner and asset. - * @param caller Caller address. - * @param creditAddress Address of a credit to be used. - * @param permit Permit to be checked. - */ - function _checkPermit(address caller, address creditAddress, Permit calldata permit) internal pure { - if (permit.asset != address(0)) { - if (permit.owner != caller) { - revert InvalidPermitOwner({ current: permit.owner, expected: caller}); - } - if (creditAddress != permit.asset) { - revert InvalidPermitAsset({ current: permit.asset, expected: creditAddress }); + // Check collateral state fingerprint if needed + if (proposal.checkCollateralStateFingerprint) { + IERC5646 computer = config.getStateFingerprintComputer(proposal.collateralAddress); + if (address(computer) == address(0)) { + // Asset is not implementing ERC5646 and no computer is registered + revert MissingStateFingerprintComputer(); } - } - } - /** - * @notice Check if refinancing loan ID is valid. - * @param refinancingLoanId Refinancing loan ID. - * @param proposalRefinancingLoanId Proposal refinancing loan ID. - * @param isOffer True if proposal is an offer, false if it is a request. - */ - function _checkRefinancingLoanId( - uint256 refinancingLoanId, - uint256 proposalRefinancingLoanId, - bool isOffer - ) internal pure { - if (refinancingLoanId == 0) { - if (proposalRefinancingLoanId != 0) { - revert InvalidRefinancingLoanId({ refinancingLoanId: proposalRefinancingLoanId }); + bytes32 currentFingerprint = computer.getStateFingerprint(proposal.collateralId); + if (proposal.collateralStateFingerprint != currentFingerprint) { + // Fingerprint mismatch + revert InvalidCollateralStateFingerprint({ + current: currentFingerprint, + proposed: proposal.collateralStateFingerprint + }); } - } else { - if (refinancingLoanId != proposalRefinancingLoanId) { - if (proposalRefinancingLoanId != 0 || !isOffer) { - revert InvalidRefinancingLoanId({ refinancingLoanId: proposalRefinancingLoanId }); - } - } - } - } - - /** - * @notice Make an on-chain proposal. - * @dev Function will mark a proposal hash as proposed. - * @param proposalHash Proposal hash. - * @param proposer Address of a proposal proposer. - */ - function _makeProposal(bytes32 proposalHash, address proposer) internal { - if (msg.sender != proposer) { - revert CallerIsNotStatedProposer(proposer); - } - - proposalsMade[proposalHash] = true; - } - - /** - * @notice Get a proposal hash according to EIP-712. - * @param encodedProposal Encoded proposal struct. - * @return Struct hash. - */ - function _getProposalHash( - bytes32 proposalTypehash, - bytes memory encodedProposal - ) internal view returns (bytes32) { - return keccak256(abi.encodePacked( - hex"1901", DOMAIN_SEPARATOR, keccak256(abi.encodePacked( - proposalTypehash, encodedProposal - )) - )); - } - - /** - * @notice Revoke a nonce of a caller. - * @param caller Caller address. - * @param nonceSpace Nonce space of a nonce to be revoked. - * @param nonce Nonce to be revoked. - */ - function _revokeCallersNonce(address caller, uint256 nonceSpace, uint256 nonce) internal { - if (!revokedNonce.isNonceUsable(caller, nonceSpace, nonce)) { - revert NonceNotUsable({ addr: caller, nonceSpace: nonceSpace, nonce: nonce }); } - revokedNonce.revokeNonce(caller, nonceSpace, nonce); } } diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol index 723af8a..01de7eb 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol @@ -5,7 +5,6 @@ import { MultiToken } from "MultiToken/MultiToken.sol"; import { PWNSimpleLoan } from "@pwn/loan/terms/simple/loan/PWNSimpleLoan.sol"; import { PWNSimpleLoanProposal } from "@pwn/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol"; -import { Permit } from "@pwn/loan/vault/Permit.sol"; import "@pwn/PWNErrors.sol"; @@ -103,127 +102,67 @@ contract PWNSimpleLoanSimpleProposal is PWNSimpleLoanProposal { emit ProposalMade(proposalHash, proposal.proposer, proposal); } + /** + * @notice Encode proposal data. + * @param proposal Proposal struct to be encoded. + * @return Encoded proposal data. + */ + function encodeProposalData(Proposal memory proposal) external pure returns (bytes memory) { + return abi.encode(proposal); + } /** - * @notice Accept a proposal with a callers nonce revocation. - * @dev Function will mark callers nonce as revoked. - * @param proposal Proposal struct containing all proposal data. - * @param signature Proposal signature signed by a proposer. - * @param refinancingLoanId Id of a loan to be refinanced. 0 if creating a new loan. - * @param permit Callers permit data. - * @param extra Auxiliary data that are emitted in the loan creation event. They are not used in the contract logic. - * @param callersNonceSpace Nonce space of a callers nonce. - * @param callersNonceToRevoke Nonce to revoke. - * @return loanId Id of a created loan. + * @notice Decode proposal data. + * @param proposalData Encoded proposal data. + * @return Decoded proposal struct. */ - function acceptProposal( - Proposal calldata proposal, - bytes calldata signature, - uint256 refinancingLoanId, - Permit calldata permit, - bytes calldata extra, - uint256 callersNonceSpace, - uint256 callersNonceToRevoke - ) external returns (uint256 loanId) { - _revokeCallersNonce(msg.sender, callersNonceSpace, callersNonceToRevoke); - return acceptProposal(proposal, signature, refinancingLoanId, permit, extra); + function decodeProposalData(bytes memory proposalData) public pure returns (Proposal memory) { + return abi.decode(proposalData, (Proposal)); } /** - * @notice Accept a proposal. - * @param proposal Proposal struct containing all proposal data. - * @param signature Proposal signature signed by a proposer. - * @param refinancingLoanId Id of a loan to be refinanced. 0 if creating a new loan. - * @param permit Callers permit data. - * @param extra Auxiliary data that are emitted in the loan creation event. They are not used in the contract logic. - * @return loanId Id of a created loan. + * @inheritdoc PWNSimpleLoanProposal */ function acceptProposal( - Proposal calldata proposal, - bytes calldata signature, + address acceptor, uint256 refinancingLoanId, - Permit calldata permit, - bytes calldata extra - ) public returns (uint256 loanId) { - // Check refinancing id - _checkRefinancingLoanId(refinancingLoanId, proposal.refinancingLoanId, proposal.isOffer); - - // Check permit - _checkPermit(msg.sender, proposal.creditAddress, permit); - - // Accept proposal - (bytes32 proposalHash, PWNSimpleLoan.Terms memory loanTerms) = _acceptProposal(proposal, signature); - - if (refinancingLoanId == 0) { - // Create loan - return PWNSimpleLoan(proposal.loanContract).createLOAN({ - proposalHash: proposalHash, - loanTerms: loanTerms, - permit: permit, - extra: extra - }); - } else { - // Refinance loan - return PWNSimpleLoan(proposal.loanContract).refinanceLOAN({ - loanId: refinancingLoanId, - proposalHash: proposalHash, - loanTerms: loanTerms, - permit: permit, - extra: extra - }); - } - } - - - /*----------------------------------------------------------*| - |* # INTERNALS *| - |*----------------------------------------------------------*/ - - function _acceptProposal( - Proposal calldata proposal, + bytes calldata proposalData, bytes calldata signature - ) private returns (bytes32 proposalHash, PWNSimpleLoan.Terms memory loanTerms) { - // Check if the loan contract has a tag - _checkLoanContractTag(proposal.loanContract); - - // Check collateral state fingerprint if needed - if (proposal.checkCollateralStateFingerprint) { - _checkCollateralState({ - addr: proposal.collateralAddress, - id: proposal.collateralId, - stateFingerprint: proposal.collateralStateFingerprint - }); - } + ) override external returns (bytes32 proposalHash, PWNSimpleLoan.Terms memory loanTerms) { + // Decode proposal data + Proposal memory proposal = decodeProposalData(proposalData); + + // Make proposal hash + proposalHash = _getProposalHash(PROPOSAL_TYPEHASH, abi.encode(proposal)); // Try to accept proposal - proposalHash = _tryAcceptProposal(proposal, signature); + _acceptProposal( + acceptor, + refinancingLoanId, + proposalHash, + signature, + ProposalBase({ + collateralAddress: proposal.collateralAddress, + collateralId: proposal.collateralId, + checkCollateralStateFingerprint: proposal.checkCollateralStateFingerprint, + collateralStateFingerprint: proposal.collateralStateFingerprint, + creditAmount: proposal.creditAmount, + availableCreditLimit: proposal.availableCreditLimit, + expiration: proposal.expiration, + allowedAcceptor: proposal.allowedAcceptor, + proposer: proposal.proposer, + isOffer: proposal.isOffer, + refinancingLoanId: proposal.refinancingLoanId, + nonceSpace: proposal.nonceSpace, + nonce: proposal.nonce, + loanContract: proposal.loanContract + }) + ); // Create loan terms object - loanTerms = _createLoanTerms(proposal); - } - - function _tryAcceptProposal(Proposal calldata proposal, bytes calldata signature) private returns (bytes32 proposalHash) { - proposalHash = getProposalHash(proposal); - _tryAcceptProposal({ - proposalHash: proposalHash, - creditAmount: proposal.creditAmount, - availableCreditLimit: proposal.availableCreditLimit, - apr: proposal.accruingInterestAPR, - duration: proposal.duration, - expiration: proposal.expiration, - nonceSpace: proposal.nonceSpace, - nonce: proposal.nonce, - allowedAcceptor: proposal.allowedAcceptor, - acceptor: msg.sender, - signer: proposal.proposer, - signature: signature - }); - } - - function _createLoanTerms(Proposal calldata proposal) private view returns (PWNSimpleLoan.Terms memory) { - return PWNSimpleLoan.Terms({ - lender: proposal.isOffer ? proposal.proposer : msg.sender, - borrower: proposal.isOffer ? msg.sender : proposal.proposer, + loanTerms = PWNSimpleLoan.Terms({ + lender: proposal.isOffer ? proposal.proposer : acceptor, + borrower: proposal.isOffer ? acceptor : proposal.proposer, duration: proposal.duration, collateral: MultiToken.Asset({ category: proposal.collateralCategory, diff --git a/test/unit/PWNSimpleLoan.t.sol b/test/unit/PWNSimpleLoan.t.sol index c030c41..f627e09 100644 --- a/test/unit/PWNSimpleLoan.t.sol +++ b/test/unit/PWNSimpleLoan.t.sol @@ -24,6 +24,8 @@ abstract contract PWNSimpleLoanTest is Test { address feeCollector = makeAddr("feeCollector"); address alice = makeAddr("alice"); address proposalContract = makeAddr("proposalContract"); + bytes proposalData = bytes("proposalData"); + bytes signature = bytes("signature"); uint256 loanId = 42; address lender = makeAddr("lender"); address borrower = makeAddr("borrower"); @@ -31,6 +33,8 @@ abstract contract PWNSimpleLoanTest is Test { PWNSimpleLoan.LOAN simpleLoan; PWNSimpleLoan.LOAN nonExistingLoan; PWNSimpleLoan.Terms simpleLoanTerms; + PWNSimpleLoan.ProposalSpec proposalSpec; + PWNSimpleLoan.CallerSpec callerSpec; PWNSimpleLoan.ExtensionProposal extension; T20 fungibleAsset; T721 nonFungibleAsset; @@ -96,6 +100,12 @@ abstract contract PWNSimpleLoanTest is Test { accruingInterestAPR: 0 }); + proposalSpec = PWNSimpleLoan.ProposalSpec({ + proposalContract: proposalContract, + proposalData: proposalData, + signature: signature + }); + nonExistingLoan = PWNSimpleLoan.LOAN({ status: 0, creditAddress: address(0), @@ -142,6 +152,7 @@ abstract contract PWNSimpleLoanTest is Test { abi.encode(true) ); + _mockLoanTerms(simpleLoanTerms); _mockLOANMint(loanId); _mockLOANTokenOwner(loanId, lender); @@ -210,6 +221,14 @@ abstract contract PWNSimpleLoanTest is Test { _storeLOANWord(loanSlot + 7, abi.encodePacked(_simpleLoan.collateral.amount)); } + function _mockLoanTerms(PWNSimpleLoan.Terms memory _terms) internal { + vm.mockCall( + proposalContract, + abi.encodeWithSignature("acceptProposal(address,uint256,bytes,bytes)"), + abi.encode(proposalHash, _terms) + ); + } + function _mockLOANMint(uint256 _loanId) internal { vm.mockCall(loanToken, abi.encodeWithSignature("mint(address)"), abi.encode(_loanId)); } @@ -267,15 +286,93 @@ abstract contract PWNSimpleLoanTest is Test { contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { - function testFuzz_shouldFail_whenCallerNotTagged_LOAN_PROPOSAL(address caller) external { - vm.assume(caller != proposalContract); + function testFuzz_shouldFail_whenProposalContractNotTagged_LOAN_PROPOSAL(address _proposalContract) external { + vm.assume(_proposalContract != proposalContract); + + proposalSpec.proposalContract = _proposalContract; + + vm.expectRevert(abi.encodeWithSelector(AddressMissingHubTag.selector, _proposalContract, PWNHubTags.LOAN_PROPOSAL)); + loan.createLOAN({ + proposalSpec: proposalSpec, + callerSpec: callerSpec, + extra: "" + }); + } + + function testFuzz_shouldRevokeCallersNonce_whenFlagIsTrue(address caller, uint256 nonce) external { + callerSpec.revokeNonce = true; + callerSpec.nonce = nonce; + + vm.expectCall( + revokedNonce, + abi.encodeWithSignature("revokeNonce(address,uint256)", caller, nonce) + ); + + vm.prank(caller); + loan.createLOAN({ + proposalSpec: proposalSpec, + callerSpec: callerSpec, + extra: "" + }); + } + + function testFuzz_shouldNotRevokeCallersNonce_whenFlagIsTrue(address caller, uint256 nonce) external { + callerSpec.revokeNonce = false; + callerSpec.nonce = nonce; + + vm.expectCall({ + callee: revokedNonce, + data: abi.encodeWithSignature("revokeNonce(address,uint256)", caller, nonce), + count: 0 + }); - vm.expectRevert(abi.encodeWithSelector(CallerMissingHubTag.selector, PWNHubTags.LOAN_PROPOSAL)); vm.prank(caller); loan.createLOAN({ - proposalHash: proposalHash, - loanTerms: simpleLoanTerms, - permit: permit, + proposalSpec: proposalSpec, + callerSpec: callerSpec, + extra: "" + }); + } + + function testFuzz_shouldCallProposalContract(address caller) external { + vm.expectCall( + proposalContract, + abi.encodeWithSignature("acceptProposal(address,uint256,bytes,bytes)", caller, 0, proposalData, signature) + ); + + vm.prank(caller); + loan.createLOAN({ + proposalSpec: proposalSpec, + callerSpec: callerSpec, + extra: "" + }); + } + + function testFuzz_shouldFail_whenLoanTermsDurationLessThanMin(uint256 duration) external { + uint256 minDuration = loan.MIN_LOAN_DURATION(); + vm.assume(duration < minDuration); + duration = bound(duration, 0, minDuration - 1); + simpleLoanTerms.duration = uint32(duration); + _mockLoanTerms(simpleLoanTerms); + + vm.expectRevert(abi.encodeWithSelector(InvalidDuration.selector, duration, minDuration)); + loan.createLOAN({ + proposalSpec: proposalSpec, + callerSpec: callerSpec, + extra: "" + }); + } + + function testFuzz_shouldFail_whenLoanTermsAccruingInterestAPROutOfBounds(uint256 interestAPR) external { + uint256 maxInterest = loan.MAX_ACCRUING_INTEREST_APR(); + interestAPR = bound(interestAPR, maxInterest + 1, type(uint40).max); + simpleLoanTerms.accruingInterestAPR = uint40(interestAPR); + _mockLoanTerms(simpleLoanTerms); + + vm.expectRevert(abi.encodeWithSelector(AccruingInterestAPROutOfBounds.selector, interestAPR, maxInterest)); + loan.createLOAN({ + proposalSpec: proposalSpec, + callerSpec: callerSpec, extra: "" }); } @@ -296,11 +393,9 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { simpleLoanTerms.credit.amount ) ); - vm.prank(proposalContract); loan.createLOAN({ - proposalHash: proposalHash, - loanTerms: simpleLoanTerms, - permit: permit, + proposalSpec: proposalSpec, + callerSpec: callerSpec, extra: "" }); } @@ -321,11 +416,9 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { simpleLoanTerms.collateral.amount ) ); - vm.prank(proposalContract); loan.createLOAN({ - proposalHash: proposalHash, - loanTerms: simpleLoanTerms, - permit: permit, + proposalSpec: proposalSpec, + callerSpec: callerSpec, extra: "" }); } @@ -333,11 +426,9 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { function test_shouldMintLOANToken() external { vm.expectCall(address(loanToken), abi.encodeWithSignature("mint(address)", lender)); - vm.prank(proposalContract); loan.createLOAN({ - proposalHash: proposalHash, - loanTerms: simpleLoanTerms, - permit: permit, + proposalSpec: proposalSpec, + callerSpec: callerSpec, extra: "" }); } @@ -345,12 +436,11 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { function testFuzz_shouldStoreLoanData(uint40 accruingInterestAPR) external { accruingInterestAPR = uint40(bound(accruingInterestAPR, 0, 1e11)); simpleLoanTerms.accruingInterestAPR = accruingInterestAPR; + _mockLoanTerms(simpleLoanTerms); - vm.prank(proposalContract); loan.createLOAN({ - proposalHash: proposalHash, - loanTerms: simpleLoanTerms, - permit: permit, + proposalSpec: proposalSpec, + callerSpec: callerSpec, extra: "" }); @@ -358,6 +448,38 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { _assertLOANEq(loanId, simpleLoan); } + function testFuzz_shouldFail_whenInvalidPermitData_permitOwner(address permitOwner) external { + vm.assume(permitOwner != borrower && permitOwner != address(0)); + permit.asset = simpleLoan.creditAddress; + permit.owner = permitOwner; + + callerSpec.permit = permit; + + vm.expectRevert(abi.encodeWithSelector(InvalidPermitOwner.selector, permitOwner, borrower)); + vm.prank(borrower); + loan.createLOAN({ + proposalSpec: proposalSpec, + callerSpec: callerSpec, + extra: "" + }); + } + + function testFuzz_shouldFail_whenInvalidPermitData_permitAsset(address permitAsset) external { + vm.assume(permitAsset != simpleLoan.creditAddress && permitAsset != address(0)); + permit.asset = permitAsset; + permit.owner = borrower; + + callerSpec.permit = permit; + + vm.expectRevert(abi.encodeWithSelector(InvalidPermitAsset.selector, permitAsset, simpleLoan.creditAddress)); + vm.prank(borrower); + loan.createLOAN({ + proposalSpec: proposalSpec, + callerSpec: callerSpec, + extra: "" + }); + } + function test_shouldCallPermit_whenProvided() external { permit.asset = simpleLoan.creditAddress; permit.owner = borrower; @@ -367,6 +489,8 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { permit.r = bytes32(uint256(2)); permit.s = bytes32(uint256(3)); + callerSpec.permit = permit; + vm.expectCall( permit.asset, abi.encodeWithSignature( @@ -375,11 +499,10 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { ) ); - vm.prank(proposalContract); + vm.prank(borrower); loan.createLOAN({ - proposalHash: proposalHash, - loanTerms: simpleLoanTerms, - permit: permit, + proposalSpec: proposalSpec, + callerSpec: callerSpec, extra: "" }); } @@ -389,17 +512,18 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { simpleLoanTerms.collateral.assetAddress = address(fungibleAsset); simpleLoanTerms.collateral.id = 0; simpleLoanTerms.collateral.amount = 100; + _mockLoanTerms(simpleLoanTerms); vm.expectCall( simpleLoanTerms.collateral.assetAddress, - abi.encodeWithSignature("transferFrom(address,address,uint256)", borrower, address(loan), simpleLoanTerms.collateral.amount) + abi.encodeWithSignature( + "transferFrom(address,address,uint256)", borrower, address(loan), simpleLoanTerms.collateral.amount + ) ); - vm.prank(proposalContract); loan.createLOAN({ - proposalHash: proposalHash, - loanTerms: simpleLoanTerms, - permit: permit, + proposalSpec: proposalSpec, + callerSpec: callerSpec, extra: "" }); } @@ -413,6 +537,7 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { simpleLoanTerms.credit.amount = loanAmount; fungibleAsset.mint(lender, loanAmount); + _mockLoanTerms(simpleLoanTerms); vm.mockCall(config, abi.encodeWithSignature("fee()"), abi.encode(fee)); uint256 feeAmount = Math.mulDiv(loanAmount, fee, 1e4); @@ -430,11 +555,9 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { abi.encodeWithSignature("transferFrom(address,address,uint256)", lender, borrower, newAmount) ); - vm.prank(proposalContract); loan.createLOAN({ - proposalHash: proposalHash, - loanTerms: simpleLoanTerms, - permit: permit, + proposalSpec: proposalSpec, + callerSpec: callerSpec, extra: "" }); } @@ -443,25 +566,23 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { vm.expectEmit(); emit LOANCreated(loanId, simpleLoanTerms, proposalHash, proposalContract, "lil extra"); - vm.prank(proposalContract); loan.createLOAN({ - proposalHash: proposalHash, - loanTerms: simpleLoanTerms, - permit: permit, + proposalSpec: proposalSpec, + callerSpec: callerSpec, extra: "lil extra" }); } - function test_shouldReturnCreatedLoanId() external { - vm.prank(proposalContract); + function testFuzz_shouldReturnNewLoanId(uint256 _loanId) external { + _mockLOANMint(_loanId); + uint256 createdLoanId = loan.createLOAN({ - proposalHash: proposalHash, - loanTerms: simpleLoanTerms, - permit: permit, + proposalSpec: proposalSpec, + callerSpec: callerSpec, extra: "" }); - assertEq(createdLoanId, loanId); + assertEq(createdLoanId, _loanId); } } @@ -471,11 +592,12 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { |* # REFINANCE LOAN *| |*----------------------------------------------------------*/ +/// @dev This contract tests only different behaviour of `createLOAN` with refinancingLoanId >0. contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { PWNSimpleLoan.LOAN refinancedLoan; PWNSimpleLoan.Terms refinancedLoanTerms; - uint256 ferinancedLoanId = 44; + uint256 refinancingLoanId = 44; address newLender = makeAddr("newLender"); function setUp() override public { @@ -508,54 +630,36 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { accruingInterestAPR: 0 }); - _mockLOAN(loanId, simpleLoan); - _mockLOANMint(ferinancedLoanId); + _mockLoanTerms(refinancedLoanTerms); + _mockLOAN(refinancingLoanId, simpleLoan); + _mockLOANTokenOwner(refinancingLoanId, lender); + callerSpec.refinancingLoanId = refinancingLoanId; vm.prank(newLender); fungibleAsset.approve(address(loan), type(uint256).max); } - function testFuzz_shouldFail_whenCallerNotTagged_LOAN_PROPOSAL(address caller) external { - vm.assume(caller != proposalContract); - - vm.expectRevert(abi.encodeWithSelector(CallerMissingHubTag.selector, PWNHubTags.LOAN_PROPOSAL)); - vm.prank(caller); - loan.refinanceLOAN({ - loanId: loanId, - proposalHash: proposalHash, - loanTerms: refinancedLoanTerms, - permit: permit, - extra: "" - }); - } - function test_shouldFail_whenLoanDoesNotExist() external { simpleLoan.status = 0; - _mockLOAN(loanId, simpleLoan); + _mockLOAN(refinancingLoanId, simpleLoan); vm.expectRevert(abi.encodeWithSelector(NonExistingLoan.selector)); - vm.prank(proposalContract); - loan.refinanceLOAN({ - loanId: loanId, - proposalHash: proposalHash, - loanTerms: refinancedLoanTerms, - permit: permit, + loan.createLOAN({ + proposalSpec: proposalSpec, + callerSpec: callerSpec, extra: "" }); } function test_shouldFail_whenLoanIsNotRunning() external { simpleLoan.status = 3; - _mockLOAN(loanId, simpleLoan); + _mockLOAN(refinancingLoanId, simpleLoan); vm.expectRevert(abi.encodeWithSelector(InvalidLoanStatus.selector, 3)); - vm.prank(proposalContract); - loan.refinanceLOAN({ - loanId: loanId, - proposalHash: proposalHash, - loanTerms: refinancedLoanTerms, - permit: permit, + loan.createLOAN({ + proposalSpec: proposalSpec, + callerSpec: callerSpec, extra: "" }); } @@ -564,12 +668,9 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { vm.warp(simpleLoan.defaultTimestamp); vm.expectRevert(abi.encodeWithSelector(LoanDefaulted.selector, simpleLoan.defaultTimestamp)); - vm.prank(proposalContract); - loan.refinanceLOAN({ - loanId: loanId, - proposalHash: proposalHash, - loanTerms: refinancedLoanTerms, - permit: permit, + loan.createLOAN({ + proposalSpec: proposalSpec, + callerSpec: callerSpec, extra: "" }); } @@ -577,28 +678,24 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { function testFuzz_shouldFail_whenCreditAssetMismatch(address _assetAddress) external { vm.assume(_assetAddress != simpleLoan.creditAddress); refinancedLoanTerms.credit.assetAddress = _assetAddress; + _mockLoanTerms(refinancedLoanTerms); vm.expectRevert(abi.encodeWithSelector(RefinanceCreditMismatch.selector)); - vm.prank(proposalContract); - loan.refinanceLOAN({ - loanId: loanId, - proposalHash: proposalHash, - loanTerms: refinancedLoanTerms, - permit: permit, + loan.createLOAN({ + proposalSpec: proposalSpec, + callerSpec: callerSpec, extra: "" }); } function test_shouldFail_whenCreditAssetAmountZero() external { refinancedLoanTerms.credit.amount = 0; + _mockLoanTerms(refinancedLoanTerms); vm.expectRevert(abi.encodeWithSelector(RefinanceCreditMismatch.selector)); - vm.prank(proposalContract); - loan.refinanceLOAN({ - loanId: loanId, - proposalHash: proposalHash, - loanTerms: refinancedLoanTerms, - permit: permit, + loan.createLOAN({ + proposalSpec: proposalSpec, + callerSpec: callerSpec, extra: "" }); } @@ -607,14 +704,12 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { _category = _category % 4; vm.assume(_category != uint8(simpleLoan.collateral.category)); refinancedLoanTerms.collateral.category = MultiToken.Category(_category); + _mockLoanTerms(refinancedLoanTerms); vm.expectRevert(abi.encodeWithSelector(RefinanceCollateralMismatch.selector)); - vm.prank(proposalContract); - loan.refinanceLOAN({ - loanId: loanId, - proposalHash: proposalHash, - loanTerms: refinancedLoanTerms, - permit: permit, + loan.createLOAN({ + proposalSpec: proposalSpec, + callerSpec: callerSpec, extra: "" }); } @@ -622,14 +717,12 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { function testFuzz_shouldFail_whenCollateralAddressMismatch(address _assetAddress) external { vm.assume(_assetAddress != simpleLoan.collateral.assetAddress); refinancedLoanTerms.collateral.assetAddress = _assetAddress; + _mockLoanTerms(refinancedLoanTerms); vm.expectRevert(abi.encodeWithSelector(RefinanceCollateralMismatch.selector)); - vm.prank(proposalContract); - loan.refinanceLOAN({ - loanId: loanId, - proposalHash: proposalHash, - loanTerms: refinancedLoanTerms, - permit: permit, + loan.createLOAN({ + proposalSpec: proposalSpec, + callerSpec: callerSpec, extra: "" }); } @@ -637,14 +730,12 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { function testFuzz_shouldFail_whenCollateralIdMismatch(uint256 _id) external { vm.assume(_id != simpleLoan.collateral.id); refinancedLoanTerms.collateral.id = _id; + _mockLoanTerms(refinancedLoanTerms); vm.expectRevert(abi.encodeWithSelector(RefinanceCollateralMismatch.selector)); - vm.prank(proposalContract); - loan.refinanceLOAN({ - loanId: loanId, - proposalHash: proposalHash, - loanTerms: refinancedLoanTerms, - permit: permit, + loan.createLOAN({ + proposalSpec: proposalSpec, + callerSpec: callerSpec, extra: "" }); } @@ -652,14 +743,12 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { function testFuzz_shouldFail_whenCollateralAmountMismatch(uint256 _amount) external { vm.assume(_amount != simpleLoan.collateral.amount); refinancedLoanTerms.collateral.amount = _amount; + _mockLoanTerms(refinancedLoanTerms); vm.expectRevert(abi.encodeWithSelector(RefinanceCollateralMismatch.selector)); - vm.prank(proposalContract); - loan.refinanceLOAN({ - loanId: loanId, - proposalHash: proposalHash, - loanTerms: refinancedLoanTerms, - permit: permit, + loan.createLOAN({ + proposalSpec: proposalSpec, + callerSpec: callerSpec, extra: "" }); } @@ -667,149 +756,55 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { function testFuzz_shouldFail_whenBorrowerMismatch(address _borrower) external { vm.assume(_borrower != simpleLoan.borrower); refinancedLoanTerms.borrower = _borrower; + _mockLoanTerms(refinancedLoanTerms); vm.expectRevert(abi.encodeWithSelector(RefinanceBorrowerMismatch.selector, simpleLoan.borrower, _borrower)); - vm.prank(proposalContract); - loan.refinanceLOAN({ - loanId: loanId, - proposalHash: proposalHash, - loanTerms: refinancedLoanTerms, - permit: permit, - extra: "" - }); - } - - function test_shouldMintLOANToken() external { - vm.expectCall(address(loanToken), abi.encodeWithSignature("mint(address)")); - - vm.prank(proposalContract); - loan.refinanceLOAN({ - loanId: loanId, - proposalHash: proposalHash, - loanTerms: refinancedLoanTerms, - permit: permit, - extra: "" - }); - } - - function test_shouldStoreRefinancedLoanData() external { - vm.prank(proposalContract); - loan.refinanceLOAN({ - loanId: loanId, - proposalHash: proposalHash, - loanTerms: refinancedLoanTerms, - permit: permit, - extra: "" - }); - - _assertLOANEq(ferinancedLoanId, refinancedLoan); - } - - function test_shouldEmit_LOANCreated() external { - vm.expectEmit(); - emit LOANCreated(ferinancedLoanId, refinancedLoanTerms, proposalHash, proposalContract, "lil extra"); - - vm.prank(proposalContract); - loan.refinanceLOAN({ - loanId: loanId, - proposalHash: proposalHash, - loanTerms: refinancedLoanTerms, - permit: permit, - extra: "lil extra" - }); - } - - function test_shouldReturnNewLoanId() external { - vm.prank(proposalContract); - uint256 newLoanId = loan.refinanceLOAN({ - loanId: loanId, - proposalHash: proposalHash, - loanTerms: refinancedLoanTerms, - permit: permit, + loan.createLOAN({ + proposalSpec: proposalSpec, + callerSpec: callerSpec, extra: "" }); - - assertEq(newLoanId, ferinancedLoanId); } function test_shouldEmit_LOANPaidBack() external { vm.expectEmit(); - emit LOANPaidBack(loanId); + emit LOANPaidBack(refinancingLoanId); - vm.prank(proposalContract); - loan.refinanceLOAN({ - loanId: loanId, - proposalHash: proposalHash, - loanTerms: refinancedLoanTerms, - permit: permit, + loan.createLOAN({ + proposalSpec: proposalSpec, + callerSpec: callerSpec, extra: "" }); } function test_shouldEmit_LOANRefinanced() external { vm.expectEmit(); - emit LOANRefinanced(loanId, ferinancedLoanId); + emit LOANRefinanced(refinancingLoanId, loanId); - vm.prank(proposalContract); - loan.refinanceLOAN({ - loanId: loanId, - proposalHash: proposalHash, - loanTerms: refinancedLoanTerms, - permit: permit, + loan.createLOAN({ + proposalSpec: proposalSpec, + callerSpec: callerSpec, extra: "" }); } function test_shouldDeleteOldLoanData_whenLOANOwnerIsOriginalLender() external { - vm.prank(proposalContract); - loan.refinanceLOAN({ - loanId: loanId, - proposalHash: proposalHash, - loanTerms: refinancedLoanTerms, - permit: permit, + loan.createLOAN({ + proposalSpec: proposalSpec, + callerSpec: callerSpec, extra: "" }); - _assertLOANEq(loanId, nonExistingLoan); + _assertLOANEq(refinancingLoanId, nonExistingLoan); } function test_shouldEmit_LOANClaimed_whenLOANOwnerIsOriginalLender() external { vm.expectEmit(); - emit LOANClaimed(loanId, false); + emit LOANClaimed(refinancingLoanId, false); - vm.prank(proposalContract); - loan.refinanceLOAN({ - loanId: loanId, - proposalHash: proposalHash, - loanTerms: refinancedLoanTerms, - permit: permit, - extra: "" - }); - } - - function test_shouldCallPermit_whenProvided() external { - permit.asset = simpleLoan.creditAddress; - permit.owner = borrower; - permit.amount = 321; - permit.deadline = 2; - permit.v = 3; - permit.r = bytes32(uint256(4)); - permit.s = bytes32(uint256(5)); - - vm.expectCall( - permit.asset, - abi.encodeWithSignature( - "permit(address,address,uint256,uint256,uint8,bytes32,bytes32)", - permit.owner, address(loan), permit.amount, permit.deadline, permit.v, permit.r, permit.s - ) - ); - - vm.prank(proposalContract); - loan.refinanceLOAN({ - loanId: loanId, - proposalHash: proposalHash, - loanTerms: refinancedLoanTerms, - permit: permit, + loan.createLOAN({ + proposalSpec: proposalSpec, + callerSpec: callerSpec, extra: "" }); } @@ -817,7 +812,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { function testFuzz_shouldUpdateLoanData_whenLOANOwnerIsNotOriginalLender( uint256 _days, uint256 principal, uint256 fixedInterest, uint256 dailyInterest ) external { - _mockLOANTokenOwner(loanId, makeAddr("notOriginalLender")); + _mockLOANTokenOwner(refinancingLoanId, makeAddr("notOriginalLender")); _days = bound(_days, 0, loanDurationInDays - 1); principal = bound(principal, 1, 1e40); @@ -827,19 +822,16 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { simpleLoan.principalAmount = principal; simpleLoan.fixedInterestAmount = fixedInterest; simpleLoan.accruingInterestDailyRate = uint40(dailyInterest); - _mockLOAN(loanId, simpleLoan); + _mockLOAN(refinancingLoanId, simpleLoan); vm.warp(simpleLoan.startTimestamp + _days * 1 days); - uint256 loanRepaymentAmount = loan.loanRepaymentAmount(loanId); + uint256 loanRepaymentAmount = loan.loanRepaymentAmount(refinancingLoanId); fungibleAsset.mint(borrower, loanRepaymentAmount); - vm.prank(proposalContract); - loan.refinanceLOAN({ - loanId: loanId, - proposalHash: proposalHash, - loanTerms: refinancedLoanTerms, - permit: permit, + loan.createLOAN({ + proposalSpec: proposalSpec, + callerSpec: callerSpec, extra: "" }); @@ -847,13 +839,13 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { simpleLoan.status = 3; // move loan to repaid state simpleLoan.fixedInterestAmount = loanRepaymentAmount - principal; // stored accrued interest at the time of repayment simpleLoan.accruingInterestDailyRate = 0; // stop accruing interest - _assertLOANEq(loanId, simpleLoan); + _assertLOANEq(refinancingLoanId, simpleLoan); } function testFuzz_shouldTransferOriginalLoanRepaymentDirectly_andTransferSurplusToBorrower_whenLOANOwnerIsOriginalLender_whenRefinanceLoanMoreThanOrEqualToOriginalLoan( uint256 refinanceAmount, uint256 fee ) external { - uint256 loanRepaymentAmount = loan.loanRepaymentAmount(loanId); + uint256 loanRepaymentAmount = loan.loanRepaymentAmount(refinancingLoanId); fee = bound(fee, 0, 9999); // 0 - 99.99% uint256 minRefinanceAmount = Math.mulDiv(loanRepaymentAmount, 1e4, 1e4 - fee); refinanceAmount = bound( @@ -866,7 +858,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { refinancedLoanTerms.credit.amount = refinanceAmount; refinancedLoanTerms.lender = newLender; - _mockLOANTokenOwner(loanId, simpleLoan.originalLender); + _mockLoanTerms(refinancedLoanTerms); + _mockLOANTokenOwner(refinancingLoanId, simpleLoan.originalLender); vm.mockCall(config, abi.encodeWithSignature("fee()"), abi.encode(fee)); fungibleAsset.mint(newLender, refinanceAmount); @@ -895,12 +888,9 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { count: borrowerSurplus > 0 ? 1 : 0 }); - vm.prank(proposalContract); - loan.refinanceLOAN({ - loanId: loanId, - proposalHash: proposalHash, - loanTerms: refinancedLoanTerms, - permit: permit, + loan.createLOAN({ + proposalSpec: proposalSpec, + callerSpec: callerSpec, extra: "" }); } @@ -908,7 +898,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { function testFuzz_shouldTransferOriginalLoanRepaymentToVault_andTransferSurplusToBorrower_whenLOANOwnerIsNotOriginalLender_whenRefinanceLoanMoreThanOrEqualToOriginalLoan( uint256 refinanceAmount, uint256 fee ) external { - uint256 loanRepaymentAmount = loan.loanRepaymentAmount(loanId); + uint256 loanRepaymentAmount = loan.loanRepaymentAmount(refinancingLoanId); fee = bound(fee, 0, 9999); // 0 - 99.99% uint256 minRefinanceAmount = Math.mulDiv(loanRepaymentAmount, 1e4, 1e4 - fee); refinanceAmount = bound( @@ -921,7 +911,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { refinancedLoanTerms.credit.amount = refinanceAmount; refinancedLoanTerms.lender = newLender; - _mockLOANTokenOwner(loanId, makeAddr("notOriginalLender")); + _mockLoanTerms(refinancedLoanTerms); + _mockLOANTokenOwner(refinancingLoanId, makeAddr("notOriginalLender")); vm.mockCall(config, abi.encodeWithSignature("fee()"), abi.encode(fee)); fungibleAsset.mint(newLender, refinanceAmount); @@ -950,12 +941,9 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { count: borrowerSurplus > 0 ? 1 : 0 }); - vm.prank(proposalContract); - loan.refinanceLOAN({ - loanId: loanId, - proposalHash: proposalHash, - loanTerms: refinancedLoanTerms, - permit: permit, + loan.createLOAN({ + proposalSpec: proposalSpec, + callerSpec: callerSpec, extra: "" }); } @@ -963,7 +951,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { function testFuzz_shouldNotTransferOriginalLoanRepayment_andTransferSurplusToBorrower_whenLOANOwnerIsNewLender_whenRefinanceLoanMoreThanOrEqualOriginalLoan( uint256 refinanceAmount, uint256 fee ) external { - uint256 loanRepaymentAmount = loan.loanRepaymentAmount(loanId); + uint256 loanRepaymentAmount = loan.loanRepaymentAmount(refinancingLoanId); fee = bound(fee, 0, 9999); // 0 - 99.99% uint256 minRefinanceAmount = Math.mulDiv(loanRepaymentAmount, 1e4, 1e4 - fee); refinanceAmount = bound( @@ -976,7 +964,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { refinancedLoanTerms.credit.amount = refinanceAmount; refinancedLoanTerms.lender = newLender; - _mockLOANTokenOwner(loanId, newLender); + _mockLoanTerms(refinancedLoanTerms); + _mockLOANTokenOwner(refinancingLoanId, newLender); vm.mockCall(config, abi.encodeWithSignature("fee()"), abi.encode(fee)); fungibleAsset.mint(newLender, refinanceAmount); @@ -1006,12 +995,9 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { count: borrowerSurplus > 0 ? 1 : 0 }); - vm.prank(proposalContract); - loan.refinanceLOAN({ - loanId: loanId, - proposalHash: proposalHash, - loanTerms: refinancedLoanTerms, - permit: permit, + loan.createLOAN({ + proposalSpec: proposalSpec, + callerSpec: callerSpec, extra: "" }); } @@ -1019,7 +1005,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { function testFuzz_shouldTransferOriginalLoanRepaymentDirectly_andContributeFromBorrower_whenLOANOwnerIsOriginalLender_whenRefinanceLoanLessThanOriginalLoan( uint256 refinanceAmount, uint256 fee ) external { - uint256 loanRepaymentAmount = loan.loanRepaymentAmount(loanId); + uint256 loanRepaymentAmount = loan.loanRepaymentAmount(refinancingLoanId); fee = bound(fee, 0, 9999); // 0 - 99.99% uint256 minRefinanceAmount = Math.mulDiv(loanRepaymentAmount, 1e4, 1e4 - fee); refinanceAmount = bound(refinanceAmount, 1, minRefinanceAmount - 1); @@ -1028,7 +1014,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { refinancedLoanTerms.credit.amount = refinanceAmount; refinancedLoanTerms.lender = newLender; - _mockLOANTokenOwner(loanId, simpleLoan.originalLender); + _mockLoanTerms(refinancedLoanTerms); + _mockLOANTokenOwner(refinancingLoanId, simpleLoan.originalLender); vm.mockCall(config, abi.encodeWithSignature("fee()"), abi.encode(fee)); fungibleAsset.mint(newLender, refinanceAmount); @@ -1057,12 +1044,9 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { count: borrowerContribution > 0 ? 1 : 0 }); - vm.prank(proposalContract); - loan.refinanceLOAN({ - loanId: loanId, - proposalHash: proposalHash, - loanTerms: refinancedLoanTerms, - permit: permit, + loan.createLOAN({ + proposalSpec: proposalSpec, + callerSpec: callerSpec, extra: "" }); } @@ -1070,7 +1054,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { function testFuzz_shouldTransferOriginalLoanRepaymentToVault_andContributeFromBorrower_whenLOANOwnerIsNotOriginalLender_whenRefinanceLoanLessThanOriginalLoan( uint256 refinanceAmount, uint256 fee ) external { - uint256 loanRepaymentAmount = loan.loanRepaymentAmount(loanId); + uint256 loanRepaymentAmount = loan.loanRepaymentAmount(refinancingLoanId); fee = bound(fee, 0, 9999); // 0 - 99.99% uint256 minRefinanceAmount = Math.mulDiv(loanRepaymentAmount, 1e4, 1e4 - fee); refinanceAmount = bound(refinanceAmount, 1, minRefinanceAmount - 1); @@ -1079,7 +1063,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { refinancedLoanTerms.credit.amount = refinanceAmount; refinancedLoanTerms.lender = newLender; - _mockLOANTokenOwner(loanId, makeAddr("notOriginalLender")); + _mockLoanTerms(refinancedLoanTerms); + _mockLOANTokenOwner(refinancingLoanId, makeAddr("notOriginalLender")); vm.mockCall(config, abi.encodeWithSignature("fee()"), abi.encode(fee)); fungibleAsset.mint(newLender, refinanceAmount); @@ -1108,12 +1093,9 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { count: borrowerContribution > 0 ? 1 : 0 }); - vm.prank(proposalContract); - loan.refinanceLOAN({ - loanId: loanId, - proposalHash: proposalHash, - loanTerms: refinancedLoanTerms, - permit: permit, + loan.createLOAN({ + proposalSpec: proposalSpec, + callerSpec: callerSpec, extra: "" }); } @@ -1121,7 +1103,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { function testFuzz_shouldNotTransferOriginalLoanRepayment_andContributeFromBorrower_whenLOANOwnerIsNewLender_whenRefinanceLoanLessThanOriginalLoan( uint256 refinanceAmount, uint256 fee ) external { - uint256 loanRepaymentAmount = loan.loanRepaymentAmount(loanId); + uint256 loanRepaymentAmount = loan.loanRepaymentAmount(refinancingLoanId); fee = bound(fee, 0, 9999); // 0 - 99.99% uint256 minRefinanceAmount = Math.mulDiv(loanRepaymentAmount, 1e4, 1e4 - fee); refinanceAmount = bound(refinanceAmount, 1, minRefinanceAmount - 1); @@ -1130,7 +1112,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { refinancedLoanTerms.credit.amount = refinanceAmount; refinancedLoanTerms.lender = newLender; - _mockLOANTokenOwner(loanId, newLender); + _mockLoanTerms(refinancedLoanTerms); + _mockLOANTokenOwner(refinancingLoanId, newLender); vm.mockCall(config, abi.encodeWithSignature("fee()"), abi.encode(fee)); fungibleAsset.mint(newLender, refinanceAmount); @@ -1160,12 +1143,9 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { count: borrowerContribution > 0 ? 1 : 0 }); - vm.prank(proposalContract); - loan.refinanceLOAN({ - loanId: loanId, - proposalHash: proposalHash, - loanTerms: refinancedLoanTerms, - permit: permit, + loan.createLOAN({ + proposalSpec: proposalSpec, + callerSpec: callerSpec, extra: "" }); } @@ -1181,18 +1161,19 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { simpleLoan.principalAmount = principal; simpleLoan.fixedInterestAmount = fixedInterest; simpleLoan.accruingInterestDailyRate = uint40(dailyInterest); - _mockLOAN(loanId, simpleLoan); + _mockLOAN(refinancingLoanId, simpleLoan); vm.warp(simpleLoan.startTimestamp + _days * 1 days); - uint256 loanRepaymentAmount = loan.loanRepaymentAmount(loanId); + uint256 loanRepaymentAmount = loan.loanRepaymentAmount(refinancingLoanId); refinanceAmount = bound( refinanceAmount, 1, type(uint256).max - loanRepaymentAmount - fungibleAsset.totalSupply() ); refinancedLoanTerms.credit.amount = refinanceAmount; refinancedLoanTerms.lender = newLender; - _mockLOANTokenOwner(loanId, lender); + _mockLoanTerms(refinancedLoanTerms); + _mockLOANTokenOwner(refinancingLoanId, lender); fungibleAsset.mint(newLender, refinanceAmount); if (loanRepaymentAmount > refinanceAmount) { @@ -1201,12 +1182,9 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { uint256 originalBalance = fungibleAsset.balanceOf(lender); - vm.prank(proposalContract); - loan.refinanceLOAN({ - loanId: loanId, - proposalHash: proposalHash, - loanTerms: refinancedLoanTerms, - permit: permit, + loan.createLOAN({ + proposalSpec: proposalSpec, + callerSpec: callerSpec, extra: "" }); @@ -1224,11 +1202,11 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { simpleLoan.principalAmount = principal; simpleLoan.fixedInterestAmount = fixedInterest; simpleLoan.accruingInterestDailyRate = uint40(dailyInterest); - _mockLOAN(loanId, simpleLoan); + _mockLOAN(refinancingLoanId, simpleLoan); vm.warp(simpleLoan.startTimestamp + _days * 1 days); - uint256 loanRepaymentAmount = loan.loanRepaymentAmount(loanId); + uint256 loanRepaymentAmount = loan.loanRepaymentAmount(refinancingLoanId); fee = bound(fee, 1, 9999); // 0 - 99.99% refinanceAmount = bound( refinanceAmount, 1, type(uint256).max - loanRepaymentAmount - fungibleAsset.totalSupply() @@ -1237,7 +1215,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { refinancedLoanTerms.credit.amount = refinanceAmount; refinancedLoanTerms.lender = newLender; - _mockLOANTokenOwner(loanId, lender); + _mockLoanTerms(refinancedLoanTerms); + _mockLOANTokenOwner(refinancingLoanId, lender); vm.mockCall(config, abi.encodeWithSignature("fee()"), abi.encode(fee)); fungibleAsset.mint(newLender, refinanceAmount); @@ -1247,12 +1226,9 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { uint256 originalBalance = fungibleAsset.balanceOf(feeCollector); - vm.prank(proposalContract); - loan.refinanceLOAN({ - loanId: loanId, - proposalHash: proposalHash, - loanTerms: refinancedLoanTerms, - permit: permit, + loan.createLOAN({ + proposalSpec: proposalSpec, + callerSpec: callerSpec, extra: "" }); @@ -1260,7 +1236,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { } function testFuzz_shouldTransferSurplusToBorrower(uint256 refinanceAmount) external { - uint256 loanRepaymentAmount = loan.loanRepaymentAmount(loanId); + uint256 loanRepaymentAmount = loan.loanRepaymentAmount(refinancingLoanId); refinanceAmount = bound( refinanceAmount, loanRepaymentAmount + 1, type(uint256).max - loanRepaymentAmount - fungibleAsset.totalSupply() ); @@ -1268,17 +1244,15 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { refinancedLoanTerms.credit.amount = refinanceAmount; refinancedLoanTerms.lender = newLender; - _mockLOANTokenOwner(loanId, lender); + _mockLoanTerms(refinancedLoanTerms); + _mockLOANTokenOwner(refinancingLoanId, lender); fungibleAsset.mint(newLender, refinanceAmount); uint256 originalBalance = fungibleAsset.balanceOf(borrower); - vm.prank(proposalContract); - loan.refinanceLOAN({ - loanId: loanId, - proposalHash: proposalHash, - loanTerms: refinancedLoanTerms, - permit: permit, + loan.createLOAN({ + proposalSpec: proposalSpec, + callerSpec: callerSpec, extra: "" }); @@ -1286,23 +1260,21 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { } function testFuzz_shouldContributeFromBorrower(uint256 refinanceAmount) external { - uint256 loanRepaymentAmount = loan.loanRepaymentAmount(loanId); + uint256 loanRepaymentAmount = loan.loanRepaymentAmount(refinancingLoanId); refinanceAmount = bound(refinanceAmount, 1, loanRepaymentAmount - 1); uint256 contribution = loanRepaymentAmount - refinanceAmount; refinancedLoanTerms.credit.amount = refinanceAmount; refinancedLoanTerms.lender = newLender; - _mockLOANTokenOwner(loanId, lender); + _mockLoanTerms(refinancedLoanTerms); + _mockLOANTokenOwner(refinancingLoanId, lender); fungibleAsset.mint(newLender, refinanceAmount); uint256 originalBalance = fungibleAsset.balanceOf(borrower); - vm.prank(proposalContract); - loan.refinanceLOAN({ - loanId: loanId, - proposalHash: proposalHash, - loanTerms: refinancedLoanTerms, - permit: permit, + loan.createLOAN({ + proposalSpec: proposalSpec, + callerSpec: callerSpec, extra: "" }); @@ -1354,6 +1326,30 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { loan.repayLOAN(loanId, permit); } + function testFuzz_shouldFail_whenInvalidPermitData_permitOwner(address permitOwner) external { + vm.assume(permitOwner != borrower && permitOwner != address(0)); + permit.asset = simpleLoan.creditAddress; + permit.owner = permitOwner; + + callerSpec.permit = permit; + + vm.expectRevert(abi.encodeWithSelector(InvalidPermitOwner.selector, permitOwner, borrower)); + vm.prank(borrower); + loan.repayLOAN(loanId, permit); + } + + function testFuzz_shouldFail_whenInvalidPermitData_permitAsset(address permitAsset) external { + vm.assume(permitAsset != simpleLoan.creditAddress && permitAsset != address(0)); + permit.asset = permitAsset; + permit.owner = borrower; + + callerSpec.permit = permit; + + vm.expectRevert(abi.encodeWithSelector(InvalidPermitAsset.selector, permitAsset, simpleLoan.creditAddress)); + vm.prank(borrower); + loan.repayLOAN(loanId, permit); + } + function test_shouldCallPermit_whenProvided() external { permit.asset = simpleLoan.creditAddress; permit.owner = borrower; @@ -1963,11 +1959,39 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { loan.extendLOAN(extension, "", permit); } + function testFuzz_shouldFail_whenInvalidPermitData_permitOwner(address permitOwner) external { + _mockExtensionProposalMade(extension); + + vm.assume(permitOwner != lender && permitOwner != address(0)); + permit.asset = extension.compensationAddress; + permit.owner = permitOwner; + + callerSpec.permit = permit; + + vm.expectRevert(abi.encodeWithSelector(InvalidPermitOwner.selector, permitOwner, lender)); + vm.prank(lender); + loan.extendLOAN(extension, "", permit); + } + + function testFuzz_shouldFail_whenInvalidPermitData_permitAsset(address permitAsset) external { + _mockExtensionProposalMade(extension); + + vm.assume(permitAsset != extension.compensationAddress && permitAsset != address(0)); + permit.asset = permitAsset; + permit.owner = lender; + + callerSpec.permit = permit; + + vm.expectRevert(abi.encodeWithSelector(InvalidPermitAsset.selector, permitAsset, extension.compensationAddress)); + vm.prank(lender); + loan.extendLOAN(extension, "", permit); + } + function test_shouldCallPermit_whenProvided() external { _mockExtensionProposalMade(extension); permit.asset = extension.compensationAddress; - permit.owner = borrower; + permit.owner = lender; permit.amount = 321; permit.deadline = 2; permit.v = 3; diff --git a/test/unit/PWNSimpleLoanDutchAuctionProposal.t.sol b/test/unit/PWNSimpleLoanDutchAuctionProposal.t.sol index 8e937c5..6951f9e 100644 --- a/test/unit/PWNSimpleLoanDutchAuctionProposal.t.sol +++ b/test/unit/PWNSimpleLoanDutchAuctionProposal.t.sol @@ -8,14 +8,13 @@ import { MultiToken } from "MultiToken/MultiToken.sol"; import { Math } from "openzeppelin-contracts/contracts/utils/math/Math.sol"; import { PWNHubTags } from "@pwn/hub/PWNHubTags.sol"; -import { PWNSimpleLoanDutchAuctionProposal, PWNSimpleLoanProposal, PWNSimpleLoan, Permit } +import { PWNSimpleLoanDutchAuctionProposal, PWNSimpleLoanProposal, PWNSimpleLoan } from "@pwn/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol"; import "@pwn/PWNErrors.sol"; import { PWNSimpleLoanProposalTest, - PWNSimpleLoanProposal_AcceptProposal_Test, - PWNSimpleLoanProposal_AcceptProposalAndRevokeCallersNonce_Test + PWNSimpleLoanProposal_AcceptProposal_Test } from "@pwn-test/unit/PWNSimpleLoanProposal.t.sol"; @@ -47,8 +46,8 @@ abstract contract PWNSimpleLoanDutchAuctionProposalTest is PWNSimpleLoanProposal fixedInterestAmount: 1, accruingInterestAPR: 0, duration: 1000, - auctionStart: 1, - auctionDuration: 1 minutes, + auctionStart: uint40(block.timestamp), + auctionDuration: 100 minutes, allowedAcceptor: address(0), proposer: proposer, isOffer: true, @@ -83,26 +82,29 @@ abstract contract PWNSimpleLoanDutchAuctionProposalTest is PWNSimpleLoanProposal } function _updateProposal(Params memory _params) internal { - proposal.checkCollateralStateFingerprint = _params.checkCollateralStateFingerprint; - proposal.collateralStateFingerprint = _params.collateralStateFingerprint; - if (proposal.isOffer) { - proposal.minCreditAmount = _params.creditAmount; - proposal.maxCreditAmount = proposal.minCreditAmount + 1000; + if (_params.base.isOffer) { + proposal.minCreditAmount = _params.base.creditAmount; + proposal.maxCreditAmount = proposal.minCreditAmount * 10; proposalValues.intendedCreditAmount = proposal.minCreditAmount; } else { - proposal.maxCreditAmount = _params.creditAmount; - proposal.minCreditAmount = proposal.maxCreditAmount - 1000; + proposal.maxCreditAmount = _params.base.creditAmount; + proposal.minCreditAmount = proposal.maxCreditAmount / 10; proposalValues.intendedCreditAmount = proposal.maxCreditAmount; } - proposal.availableCreditLimit = _params.availableCreditLimit; - proposal.duration = _params.duration; - proposal.accruingInterestAPR = _params.accruingInterestAPR; - proposal.auctionDuration = _params.expiration - proposal.auctionStart - 1 minutes; - proposal.allowedAcceptor = _params.allowedAcceptor; - proposal.proposer = _params.proposer; - proposal.loanContract = _params.loanContract; - proposal.nonceSpace = _params.nonceSpace; - proposal.nonce = _params.nonce; + + proposal.collateralAddress = _params.base.collateralAddress; + proposal.collateralId = _params.base.collateralId; + proposal.checkCollateralStateFingerprint = _params.base.checkCollateralStateFingerprint; + proposal.collateralStateFingerprint = _params.base.collateralStateFingerprint; + proposal.availableCreditLimit = _params.base.availableCreditLimit; + proposal.auctionDuration = _params.base.expiration - proposal.auctionStart - 1 minutes; + proposal.allowedAcceptor = _params.base.allowedAcceptor; + proposal.proposer = _params.base.proposer; + proposal.isOffer = _params.base.isOffer; + proposal.refinancingLoanId = _params.base.refinancingLoanId; + proposal.nonceSpace = _params.base.nonceSpace; + proposal.nonce = _params.base.nonce; + proposal.loanContract = _params.base.loanContract; } function _proposalSignature(Params memory _params) internal view returns (bytes memory signature) { @@ -116,14 +118,14 @@ abstract contract PWNSimpleLoanDutchAuctionProposalTest is PWNSimpleLoanProposal } - function _callAcceptProposalWith(Params memory _params, Permit memory _permit) internal override returns (uint256) { + function _callAcceptProposalWith(Params memory _params) internal override returns (bytes32, PWNSimpleLoan.Terms memory) { _updateProposal(_params); - return proposalContract.acceptProposal(proposal, proposalValues, _proposalSignature(params), 0, _permit, ""); - } - - function _callAcceptProposalWith(Params memory _params, Permit memory _permit, uint256 nonceSpace, uint256 nonce) internal override returns (uint256) { - _updateProposal(_params); - return proposalContract.acceptProposal(proposal, proposalValues, _proposalSignature(params), 0, _permit, "", nonceSpace, nonce); + return proposalContract.acceptProposal({ + acceptor: _params.acceptor, + refinancingLoanId: _params.refinancingLoanId, + proposalData: abi.encode(proposal, proposalValues), + signature: _proposalSignature(_params) + }); } function _getProposalHashWith(Params memory _params) internal override returns (bytes32) { @@ -218,6 +220,64 @@ contract PWNSimpleLoanDutchAuctionProposal_MakeProposal_Test is PWNSimpleLoanDut } +/*----------------------------------------------------------*| +|* # ENCODE PROPOSAL DATA *| +|*----------------------------------------------------------*/ + +contract PWNSimpleLoanDutchAuctionProposal_EncodeProposalData_Test is PWNSimpleLoanDutchAuctionProposalTest { + + function test_shouldReturnEncodedProposalData() external { + assertEq( + proposalContract.encodeProposalData(proposal, proposalValues), + abi.encode(proposal, proposalValues) + ); + } + +} + + +/*----------------------------------------------------------*| +|* # DECODE PROPOSAL DATA *| +|*----------------------------------------------------------*/ + +contract PWNSimpleLoanDutchAuctionProposal_DecodeProposalData_Test is PWNSimpleLoanDutchAuctionProposalTest { + + function test_shouldReturnDecodedProposalData() external { + ( + PWNSimpleLoanDutchAuctionProposal.Proposal memory _proposal, + PWNSimpleLoanDutchAuctionProposal.ProposalValues memory _proposalValues + ) = proposalContract.decodeProposalData(abi.encode(proposal, proposalValues)); + + assertEq(uint8(_proposal.collateralCategory), uint8(proposal.collateralCategory)); + assertEq(_proposal.collateralAddress, proposal.collateralAddress); + assertEq(_proposal.collateralId, proposal.collateralId); + assertEq(_proposal.collateralAmount, proposal.collateralAmount); + assertEq(_proposal.checkCollateralStateFingerprint, proposal.checkCollateralStateFingerprint); + assertEq(_proposal.collateralStateFingerprint, proposal.collateralStateFingerprint); + assertEq(_proposal.creditAddress, proposal.creditAddress); + assertEq(_proposal.minCreditAmount, proposal.minCreditAmount); + assertEq(_proposal.maxCreditAmount, proposal.maxCreditAmount); + assertEq(_proposal.availableCreditLimit, proposal.availableCreditLimit); + assertEq(_proposal.fixedInterestAmount, proposal.fixedInterestAmount); + assertEq(_proposal.accruingInterestAPR, proposal.accruingInterestAPR); + assertEq(_proposal.duration, proposal.duration); + assertEq(_proposal.auctionStart, proposal.auctionStart); + assertEq(_proposal.auctionDuration, proposal.auctionDuration); + assertEq(_proposal.allowedAcceptor, proposal.allowedAcceptor); + assertEq(_proposal.proposer, proposal.proposer); + assertEq(_proposal.isOffer, proposal.isOffer); + assertEq(_proposal.refinancingLoanId, proposal.refinancingLoanId); + assertEq(_proposal.nonceSpace, proposal.nonceSpace); + assertEq(_proposal.nonce, proposal.nonce); + assertEq(_proposal.loanContract, proposal.loanContract); + + assertEq(_proposalValues.intendedCreditAmount, proposalValues.intendedCreditAmount); + assertEq(_proposalValues.slippage, proposalValues.slippage); + } + +} + + /*----------------------------------------------------------*| |* # GET CREDIT AMOUNT *| |*----------------------------------------------------------*/ @@ -328,19 +388,6 @@ contract PWNSimpleLoanDutchAuctionProposal_GetCreditAmount_Test is PWNSimpleLoan } -/*----------------------------------------------------------*| -|* # ACCEPT PROPOSAL AND REVOKE CALLERS NONCE *| -|*----------------------------------------------------------*/ - -contract PWNSimpleLoanDutchAuctionProposal_AcceptProposalAndRevokeCallersNonce_Test is PWNSimpleLoanDutchAuctionProposalTest, PWNSimpleLoanProposal_AcceptProposalAndRevokeCallersNonce_Test { - - function setUp() virtual public override(PWNSimpleLoanDutchAuctionProposalTest, PWNSimpleLoanProposalTest) { - super.setUp(); - } - -} - - /*----------------------------------------------------------*| |* # ACCEPT PROPOSAL *| |*----------------------------------------------------------*/ @@ -352,55 +399,6 @@ contract PWNSimpleLoanDutchAuctionProposal_AcceptProposal_Test is PWNSimpleLoanD } - function testFuzz_shouldFail_whenProposedRefinancingLoanIdNotZero_whenRefinancingLoanIdZero(uint256 proposedRefinancingLoanId) external { - vm.assume(proposedRefinancingLoanId != 0); - proposal.refinancingLoanId = proposedRefinancingLoanId; - - vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, proposedRefinancingLoanId)); - proposalContract.acceptProposal( - proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), 0, permit, "" - ); - } - - function testFuzz_shouldFail_whenRefinancingLoanIdsIsNotEqual_whenProposedRefinanceingLoanIdNotZero_whenRefinancingLoanIdNotZero_whenOffer( - uint256 refinancingLoanId, uint256 proposedRefinancingLoanId - ) external { - vm.assume(proposedRefinancingLoanId != 0); - vm.assume(refinancingLoanId != proposedRefinancingLoanId); - proposal.refinancingLoanId = proposedRefinancingLoanId; - proposal.isOffer = true; - - vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, proposedRefinancingLoanId)); - proposalContract.acceptProposal( - proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), refinancingLoanId, permit, extra - ); - } - - function testFuzz_shouldPass_whenRefinancingLoanIdsNotEqual_whenProposedRefinanceingLoanIdZero_whenRefinancingLoanIdNotZero_whenOffer( - uint256 refinancingLoanId - ) external { - vm.assume(refinancingLoanId != 0); - proposal.refinancingLoanId = 0; - proposal.isOffer = true; - - proposalContract.acceptProposal( - proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), refinancingLoanId, permit, extra - ); - } - - function testFuzz_shouldFail_whenRefinancingLoanIdsNotEqual_whenRefinancingLoanIdNotZero_whenRequest( - uint256 refinancingLoanId, uint256 proposedRefinancingLoanId - ) external { - vm.assume(refinancingLoanId != proposedRefinancingLoanId); - proposal.refinancingLoanId = proposedRefinancingLoanId; - proposal.isOffer = false; - - vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, proposedRefinancingLoanId)); - proposalContract.acceptProposal( - proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), refinancingLoanId, permit, extra - ); - } - function testFuzz_shouldFail_whenCurrentAuctionCreditAmountNotInIntendedCreditAmountRange_whenOffer( uint256 intendedCreditAmount ) external { @@ -426,9 +424,13 @@ contract PWNSimpleLoanDutchAuctionProposal_AcceptProposal_Test is PWNSimpleLoanD vm.expectRevert(abi.encodeWithSelector( InvalidCreditAmount.selector, auctionCreditAmount, proposalValues.intendedCreditAmount, proposalValues.slippage )); - proposalContract.acceptProposal( - proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), 0, permit, "" - ); + vm.prank(activeLoanContract); + proposalContract.acceptProposal({ + acceptor: acceptor, + refinancingLoanId: 0, + proposalData: abi.encode(proposal, proposalValues), + signature: _signProposalHash(proposerPK, _proposalHash(proposal)) + }); } function testFuzz_shouldFail_whenCurrentAuctionCreditAmountNotInIntendedCreditAmountRange_whenRequest( @@ -456,9 +458,13 @@ contract PWNSimpleLoanDutchAuctionProposal_AcceptProposal_Test is PWNSimpleLoanD vm.expectRevert(abi.encodeWithSelector( InvalidCreditAmount.selector, auctionCreditAmount, proposalValues.intendedCreditAmount, proposalValues.slippage )); - proposalContract.acceptProposal( - proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), 0, permit, "" - ); + vm.prank(activeLoanContract); + proposalContract.acceptProposal({ + acceptor: acceptor, + refinancingLoanId: 0, + proposalData: abi.encode(proposal, proposalValues), + signature: _signProposalHash(proposerPK, _proposalHash(proposal)) + }); } function testFuzz_shouldCallLoanContractWithLoanTerms( @@ -466,15 +472,13 @@ contract PWNSimpleLoanDutchAuctionProposal_AcceptProposal_Test is PWNSimpleLoanD uint256 maxCreditAmount, uint40 auctionDuration, uint256 timeInAuction, - bool isOffer, - uint256 refinancingLoanId + bool isOffer ) external { vm.assume(minCreditAmount < maxCreditAmount); auctionDuration = uint40(bound(auctionDuration, 1, type(uint40).max / 1 minutes - 1)) * 1 minutes; timeInAuction = bound(timeInAuction, 0, auctionDuration - 1); proposal.isOffer = isOffer; - proposal.refinancingLoanId = refinancingLoanId; proposal.minCreditAmount = minCreditAmount; proposal.maxCreditAmount = maxCreditAmount; proposal.auctionStart = 1; @@ -482,55 +486,32 @@ contract PWNSimpleLoanDutchAuctionProposal_AcceptProposal_Test is PWNSimpleLoanD vm.warp(proposal.auctionStart + timeInAuction); - proposalValues.intendedCreditAmount = proposalContract.getCreditAmount(proposal, block.timestamp); + uint256 creditAmount = proposalContract.getCreditAmount(proposal, block.timestamp); + proposalValues.intendedCreditAmount = creditAmount; proposalValues.slippage = 0; - permit = Permit({ - asset: token, - owner: acceptor, - amount: 100, - deadline: 1000, - v: 27, - r: bytes32(uint256(1)), - s: bytes32(uint256(2)) - }); - extra = "lil extra"; - - PWNSimpleLoan.Terms memory loanTerms = PWNSimpleLoan.Terms({ - lender: isOffer ? proposal.proposer : acceptor, - borrower: isOffer ? acceptor : proposal.proposer, - duration: proposal.duration, - collateral: MultiToken.Asset({ - category: proposal.collateralCategory, - assetAddress: proposal.collateralAddress, - id: proposal.collateralId, - amount: proposal.collateralAmount - }), - credit: MultiToken.Asset({ - category: MultiToken.Category.ERC20, - assetAddress: proposal.creditAddress, - id: 0, - amount: proposalValues.intendedCreditAmount - }), - fixedInterestAmount: proposal.fixedInterestAmount, - accruingInterestAPR: proposal.accruingInterestAPR + vm.prank(activeLoanContract); + (bytes32 proposalHash, PWNSimpleLoan.Terms memory terms) = proposalContract.acceptProposal({ + acceptor: acceptor, + refinancingLoanId: 0, + proposalData: abi.encode(proposal, proposalValues), + signature: _signProposalHash(proposerPK, _proposalHash(proposal)) }); - vm.expectCall( - activeLoanContract, - refinancingLoanId == 0 - ? abi.encodeWithSelector( - PWNSimpleLoan.createLOAN.selector, _proposalHash(proposal), loanTerms, permit, extra - ) - : abi.encodeWithSelector( - PWNSimpleLoan.refinanceLOAN.selector, refinancingLoanId, _proposalHash(proposal), loanTerms, permit, extra - ) - ); - - vm.prank(acceptor); - proposalContract.acceptProposal( - proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), refinancingLoanId, permit, extra - ); + assertEq(proposalHash, _proposalHash(proposal)); + assertEq(terms.lender, isOffer ? proposal.proposer : acceptor); + assertEq(terms.borrower, isOffer ? acceptor : proposal.proposer); + assertEq(terms.duration, proposal.duration); + assertEq(uint8(terms.collateral.category), uint8(proposal.collateralCategory)); + assertEq(terms.collateral.assetAddress, proposal.collateralAddress); + assertEq(terms.collateral.id, proposal.collateralId); + assertEq(terms.collateral.amount, proposal.collateralAmount); + assertEq(uint8(terms.credit.category), uint8(MultiToken.Category.ERC20)); + assertEq(terms.credit.assetAddress, proposal.creditAddress); + assertEq(terms.credit.id, 0); + assertEq(terms.credit.amount, creditAmount); + assertEq(terms.fixedInterestAmount, proposal.fixedInterestAmount); + assertEq(terms.accruingInterestAPR, proposal.accruingInterestAPR); } } diff --git a/test/unit/PWNSimpleLoanFungibleProposal.t.sol b/test/unit/PWNSimpleLoanFungibleProposal.t.sol index f95f163..497a391 100644 --- a/test/unit/PWNSimpleLoanFungibleProposal.t.sol +++ b/test/unit/PWNSimpleLoanFungibleProposal.t.sol @@ -8,14 +8,13 @@ import { MultiToken } from "MultiToken/MultiToken.sol"; import { Math } from "openzeppelin-contracts/contracts/utils/math/Math.sol"; import { PWNHubTags } from "@pwn/hub/PWNHubTags.sol"; -import { PWNSimpleLoanFungibleProposal, PWNSimpleLoanProposal, PWNSimpleLoan, Permit } +import { PWNSimpleLoanFungibleProposal, PWNSimpleLoanProposal, PWNSimpleLoan } from "@pwn/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol"; import "@pwn/PWNErrors.sol"; import { PWNSimpleLoanProposalTest, - PWNSimpleLoanProposal_AcceptProposal_Test, - PWNSimpleLoanProposal_AcceptProposalAndRevokeCallersNonce_Test + PWNSimpleLoanProposal_AcceptProposal_Test } from "@pwn-test/unit/PWNSimpleLoanProposal.t.sol"; @@ -80,19 +79,21 @@ abstract contract PWNSimpleLoanFungibleProposalTest is PWNSimpleLoanProposalTest } function _updateProposal(Params memory _params) internal { - proposalValues.collateralAmount = _params.creditAmount; - - proposal.checkCollateralStateFingerprint = _params.checkCollateralStateFingerprint; - proposal.collateralStateFingerprint = _params.collateralStateFingerprint; - proposal.availableCreditLimit = _params.availableCreditLimit; - proposal.duration = _params.duration; - proposal.accruingInterestAPR = _params.accruingInterestAPR; - proposal.expiration = _params.expiration; - proposal.allowedAcceptor = _params.allowedAcceptor; - proposal.proposer = _params.proposer; - proposal.loanContract = _params.loanContract; - proposal.nonceSpace = _params.nonceSpace; - proposal.nonce = _params.nonce; + proposal.collateralAddress = _params.base.collateralAddress; + proposal.collateralId = _params.base.collateralId; + proposal.checkCollateralStateFingerprint = _params.base.checkCollateralStateFingerprint; + proposal.collateralStateFingerprint = _params.base.collateralStateFingerprint; + proposal.availableCreditLimit = _params.base.availableCreditLimit; + proposal.expiration = _params.base.expiration; + proposal.allowedAcceptor = _params.base.allowedAcceptor; + proposal.proposer = _params.base.proposer; + proposal.isOffer = _params.base.isOffer; + proposal.refinancingLoanId = _params.base.refinancingLoanId; + proposal.nonceSpace = _params.base.nonceSpace; + proposal.nonce = _params.base.nonce; + proposal.loanContract = _params.base.loanContract; + + proposalValues.collateralAmount = _params.base.creditAmount; } function _proposalSignature(Params memory _params) internal view returns (bytes memory signature) { @@ -106,14 +107,14 @@ abstract contract PWNSimpleLoanFungibleProposalTest is PWNSimpleLoanProposalTest } - function _callAcceptProposalWith(Params memory _params, Permit memory _permit) internal override returns (uint256) { + function _callAcceptProposalWith(Params memory _params) internal override returns (bytes32, PWNSimpleLoan.Terms memory) { _updateProposal(_params); - return proposalContract.acceptProposal(proposal, proposalValues, _proposalSignature(params), 0, _permit, ""); - } - - function _callAcceptProposalWith(Params memory _params, Permit memory _permit, uint256 nonceSpace, uint256 nonce) internal override returns (uint256) { - _updateProposal(_params); - return proposalContract.acceptProposal(proposal, proposalValues, _proposalSignature(params), 0, _permit, "", nonceSpace, nonce); + return proposalContract.acceptProposal({ + acceptor: _params.acceptor, + refinancingLoanId: _params.refinancingLoanId, + proposalData: abi.encode(proposal, proposalValues), + signature: _proposalSignature(_params) + }); } function _getProposalHashWith(Params memory _params) internal override returns (bytes32) { @@ -209,20 +210,15 @@ contract PWNSimpleLoanFungibleProposal_MakeProposal_Test is PWNSimpleLoanFungibl /*----------------------------------------------------------*| -|* # GET CREDIT AMOUNT *| +|* # ENCODE PROPOSAL DATA *| |*----------------------------------------------------------*/ -contract PWNSimpleLoanFungibleProposal_GetCreditAmount_Test is PWNSimpleLoanFungibleProposalTest { - - function testFuzz_shouldReturnCreditAmount(uint256 collateralAmount, uint256 creditPerCollateralUnit) external { - collateralAmount = bound(collateralAmount, 0, 1e70); - creditPerCollateralUnit = bound( - creditPerCollateralUnit, 1, collateralAmount == 0 ? type(uint256).max : type(uint256).max / collateralAmount - ); +contract PWNSimpleLoanFungibleProposal_EncodeProposalData_Test is PWNSimpleLoanFungibleProposalTest { + function test_shouldReturnEncodedProposalData() external { assertEq( - proposalContract.getCreditAmount(collateralAmount, creditPerCollateralUnit), - Math.mulDiv(collateralAmount, creditPerCollateralUnit, proposalContract.CREDIT_PER_COLLATERAL_UNIT_DENOMINATOR()) + proposalContract.encodeProposalData(proposal, proposalValues), + abi.encode(proposal, proposalValues) ); } @@ -230,85 +226,87 @@ contract PWNSimpleLoanFungibleProposal_GetCreditAmount_Test is PWNSimpleLoanFung /*----------------------------------------------------------*| -|* # ACCEPT PROPOSAL AND REVOKE CALLERS NONCE *| +|* # DECODE PROPOSAL DATA *| |*----------------------------------------------------------*/ -contract PWNSimpleLoanFungibleProposal_AcceptProposalAndRevokeCallersNonce_Test is PWNSimpleLoanFungibleProposalTest, PWNSimpleLoanProposal_AcceptProposalAndRevokeCallersNonce_Test { - - function setUp() virtual public override(PWNSimpleLoanFungibleProposalTest, PWNSimpleLoanProposalTest) { - super.setUp(); +contract PWNSimpleLoanFungibleProposal_DecodeProposalData_Test is PWNSimpleLoanFungibleProposalTest { + + function test_shouldReturnDecodedProposalData() external { + ( + PWNSimpleLoanFungibleProposal.Proposal memory _proposal, + PWNSimpleLoanFungibleProposal.ProposalValues memory _proposalValues + ) = proposalContract.decodeProposalData(abi.encode(proposal, proposalValues)); + + assertEq(uint8(_proposal.collateralCategory), uint8(proposal.collateralCategory)); + assertEq(_proposal.collateralAddress, proposal.collateralAddress); + assertEq(_proposal.collateralId, proposal.collateralId); + assertEq(_proposal.minCollateralAmount, proposal.minCollateralAmount); + assertEq(_proposal.checkCollateralStateFingerprint, proposal.checkCollateralStateFingerprint); + assertEq(_proposal.collateralStateFingerprint, proposal.collateralStateFingerprint); + assertEq(_proposal.creditAddress, proposal.creditAddress); + assertEq(_proposal.creditPerCollateralUnit, proposal.creditPerCollateralUnit); + assertEq(_proposal.availableCreditLimit, proposal.availableCreditLimit); + assertEq(_proposal.fixedInterestAmount, proposal.fixedInterestAmount); + assertEq(_proposal.accruingInterestAPR, proposal.accruingInterestAPR); + assertEq(_proposal.duration, proposal.duration); + assertEq(_proposal.expiration, proposal.expiration); + assertEq(_proposal.allowedAcceptor, proposal.allowedAcceptor); + assertEq(_proposal.proposer, proposal.proposer); + assertEq(_proposal.isOffer, proposal.isOffer); + assertEq(_proposal.refinancingLoanId, proposal.refinancingLoanId); + assertEq(_proposal.nonceSpace, proposal.nonceSpace); + assertEq(_proposal.nonce, proposal.nonce); + assertEq(_proposal.loanContract, proposal.loanContract); + + assertEq(_proposalValues.collateralAmount, proposalValues.collateralAmount); } } /*----------------------------------------------------------*| -|* # ACCEPT PROPOSAL *| +|* # GET CREDIT AMOUNT *| |*----------------------------------------------------------*/ -contract PWNSimpleLoanFungibleProposal_AcceptProposal_Test is PWNSimpleLoanFungibleProposalTest, PWNSimpleLoanProposal_AcceptProposal_Test { - - function setUp() virtual public override(PWNSimpleLoanFungibleProposalTest, PWNSimpleLoanProposalTest) { - super.setUp(); - } - - - function testFuzz_shouldFail_whenProposedRefinancingLoanIdNotZero_whenRefinancingLoanIdZero(uint256 proposedRefinancingLoanId) external { - vm.assume(proposedRefinancingLoanId != 0); - proposal.refinancingLoanId = proposedRefinancingLoanId; +contract PWNSimpleLoanFungibleProposal_GetCreditAmount_Test is PWNSimpleLoanFungibleProposalTest { - vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, proposedRefinancingLoanId)); - proposalContract.acceptProposal( - proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), 0, permit, "" + function testFuzz_shouldReturnCreditAmount(uint256 collateralAmount, uint256 creditPerCollateralUnit) external { + collateralAmount = bound(collateralAmount, 0, 1e70); + creditPerCollateralUnit = bound( + creditPerCollateralUnit, 1, collateralAmount == 0 ? type(uint256).max : type(uint256).max / collateralAmount ); - } - function testFuzz_shouldFail_whenRefinancingLoanIdsIsNotEqual_whenProposedRefinanceingLoanIdNotZero_whenRefinancingLoanIdNotZero_whenOffer( - uint256 refinancingLoanId, uint256 proposedRefinancingLoanId - ) external { - vm.assume(proposedRefinancingLoanId != 0); - vm.assume(refinancingLoanId != proposedRefinancingLoanId); - proposal.refinancingLoanId = proposedRefinancingLoanId; - proposal.isOffer = true; - - vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, proposedRefinancingLoanId)); - proposalContract.acceptProposal( - proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), refinancingLoanId, permit, extra + assertEq( + proposalContract.getCreditAmount(collateralAmount, creditPerCollateralUnit), + Math.mulDiv(collateralAmount, creditPerCollateralUnit, proposalContract.CREDIT_PER_COLLATERAL_UNIT_DENOMINATOR()) ); } - function testFuzz_shouldPass_whenRefinancingLoanIdsNotEqual_whenProposedRefinanceingLoanIdZero_whenRefinancingLoanIdNotZero_whenOffer( - uint256 refinancingLoanId - ) external { - vm.assume(refinancingLoanId != 0); - proposal.refinancingLoanId = 0; - proposal.isOffer = true; +} - proposalContract.acceptProposal( - proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), refinancingLoanId, permit, extra - ); - } - function testFuzz_shouldFail_whenRefinancingLoanIdsNotEqual_whenRefinancingLoanIdNotZero_whenRequest( - uint256 refinancingLoanId, uint256 proposedRefinancingLoanId - ) external { - vm.assume(refinancingLoanId != proposedRefinancingLoanId); - proposal.refinancingLoanId = proposedRefinancingLoanId; - proposal.isOffer = false; +/*----------------------------------------------------------*| +|* # ACCEPT PROPOSAL *| +|*----------------------------------------------------------*/ - vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, proposedRefinancingLoanId)); - proposalContract.acceptProposal( - proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), refinancingLoanId, permit, extra - ); +contract PWNSimpleLoanFungibleProposal_AcceptProposal_Test is PWNSimpleLoanFungibleProposalTest, PWNSimpleLoanProposal_AcceptProposal_Test { + + function setUp() virtual public override(PWNSimpleLoanFungibleProposalTest, PWNSimpleLoanProposalTest) { + super.setUp(); } + function test_shouldFail_whenZeroMinCollateralAmount() external { proposal.minCollateralAmount = 0; vm.expectRevert(abi.encodeWithSelector(MinCollateralAmountNotSet.selector)); - proposalContract.acceptProposal( - proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), 0, permit, "" - ); + vm.prank(activeLoanContract); + proposalContract.acceptProposal({ + acceptor: acceptor, + refinancingLoanId: 0, + proposalData: abi.encode(proposal, proposalValues), + signature: _signProposalHash(proposerPK, _proposalHash(proposal)) + }); } function testFuzz_shouldFail_whenCollateralAmountLessThanMinCollateralAmount( @@ -320,65 +318,44 @@ contract PWNSimpleLoanFungibleProposal_AcceptProposal_Test is PWNSimpleLoanFungi vm.expectRevert(abi.encodeWithSelector( InsufficientCollateralAmount.selector, proposalValues.collateralAmount, proposal.minCollateralAmount )); - proposalContract.acceptProposal( - proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), 0, permit, "" - ); + vm.prank(activeLoanContract); + proposalContract.acceptProposal({ + acceptor: acceptor, + refinancingLoanId: 0, + proposalData: abi.encode(proposal, proposalValues), + signature: _signProposalHash(proposerPK, _proposalHash(proposal)) + }); } function testFuzz_shouldCallLoanContractWithLoanTerms( - uint256 collateralAmount, uint256 creditPerCollateralUnit, bool isOffer, uint256 refinancingLoanId + uint256 collateralAmount, uint256 creditPerCollateralUnit, bool isOffer ) external { proposalValues.collateralAmount = bound(collateralAmount, proposal.minCollateralAmount, 1e40); proposal.creditPerCollateralUnit = bound(creditPerCollateralUnit, 1, type(uint256).max / proposalValues.collateralAmount); proposal.isOffer = isOffer; - proposal.refinancingLoanId = refinancingLoanId; - - permit = Permit({ - asset: token, - owner: acceptor, - amount: 100, - deadline: 1000, - v: 27, - r: bytes32(uint256(1)), - s: bytes32(uint256(2)) - }); - extra = "lil extra"; - - PWNSimpleLoan.Terms memory loanTerms = PWNSimpleLoan.Terms({ - lender: isOffer ? proposal.proposer : acceptor, - borrower: isOffer ? acceptor : proposal.proposer, - duration: proposal.duration, - collateral: MultiToken.Asset({ - category: proposal.collateralCategory, - assetAddress: proposal.collateralAddress, - id: proposal.collateralId, - amount: proposalValues.collateralAmount - }), - credit: MultiToken.Asset({ - category: MultiToken.Category.ERC20, - assetAddress: proposal.creditAddress, - id: 0, - amount: proposalContract.getCreditAmount(proposalValues.collateralAmount, proposal.creditPerCollateralUnit) - }), - fixedInterestAmount: proposal.fixedInterestAmount, - accruingInterestAPR: proposal.accruingInterestAPR - }); - vm.expectCall( - activeLoanContract, - refinancingLoanId == 0 - ? abi.encodeWithSelector( - PWNSimpleLoan.createLOAN.selector, _proposalHash(proposal), loanTerms, permit, extra - ) - : abi.encodeWithSelector( - PWNSimpleLoan.refinanceLOAN.selector, refinancingLoanId, _proposalHash(proposal), loanTerms, permit, extra - ) - ); + vm.prank(activeLoanContract); + (bytes32 proposalHash, PWNSimpleLoan.Terms memory terms) = proposalContract.acceptProposal({ + acceptor: acceptor, + refinancingLoanId: 0, + proposalData: abi.encode(proposal, proposalValues), + signature: _signProposalHash(proposerPK, _proposalHash(proposal)) + }); - vm.prank(acceptor); - proposalContract.acceptProposal( - proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), refinancingLoanId, permit, extra - ); + assertEq(proposalHash, _proposalHash(proposal)); + assertEq(terms.lender, isOffer ? proposal.proposer : acceptor); + assertEq(terms.borrower, isOffer ? acceptor : proposal.proposer); + assertEq(terms.duration, proposal.duration); + assertEq(uint8(terms.collateral.category), uint8(proposal.collateralCategory)); + assertEq(terms.collateral.assetAddress, proposal.collateralAddress); + assertEq(terms.collateral.id, proposal.collateralId); + assertEq(terms.collateral.amount, proposalValues.collateralAmount); + assertEq(uint8(terms.credit.category), uint8(MultiToken.Category.ERC20)); + assertEq(terms.credit.assetAddress, proposal.creditAddress); + assertEq(terms.credit.id, 0); + assertEq(terms.credit.amount, proposalContract.getCreditAmount(proposalValues.collateralAmount, proposal.creditPerCollateralUnit)); + assertEq(terms.fixedInterestAmount, proposal.fixedInterestAmount); + assertEq(terms.accruingInterestAPR, proposal.accruingInterestAPR); } } diff --git a/test/unit/PWNSimpleLoanListProposal.t.sol b/test/unit/PWNSimpleLoanListProposal.t.sol index ea4bd0d..8ae1dc9 100644 --- a/test/unit/PWNSimpleLoanListProposal.t.sol +++ b/test/unit/PWNSimpleLoanListProposal.t.sol @@ -8,14 +8,13 @@ import { MultiToken } from "MultiToken/MultiToken.sol"; import { Math } from "openzeppelin-contracts/contracts/utils/math/Math.sol"; import { PWNHubTags } from "@pwn/hub/PWNHubTags.sol"; -import { PWNSimpleLoanListProposal, PWNSimpleLoanProposal, PWNSimpleLoan, Permit } +import { PWNSimpleLoanListProposal, PWNSimpleLoanProposal, PWNSimpleLoan } from "@pwn/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol"; import "@pwn/PWNErrors.sol"; import { PWNSimpleLoanProposalTest, - PWNSimpleLoanProposal_AcceptProposal_Test, - PWNSimpleLoanProposal_AcceptProposalAndRevokeCallersNonce_Test + PWNSimpleLoanProposal_AcceptProposal_Test } from "@pwn-test/unit/PWNSimpleLoanProposal.t.sol"; @@ -81,18 +80,21 @@ abstract contract PWNSimpleLoanListProposalTest is PWNSimpleLoanProposalTest { } function _updateProposal(Params memory _params) internal { - proposal.checkCollateralStateFingerprint = _params.checkCollateralStateFingerprint; - proposal.collateralStateFingerprint = _params.collateralStateFingerprint; - proposal.creditAmount = _params.creditAmount; - proposal.availableCreditLimit = _params.availableCreditLimit; - proposal.duration = _params.duration; - proposal.accruingInterestAPR = _params.accruingInterestAPR; - proposal.expiration = _params.expiration; - proposal.allowedAcceptor = _params.allowedAcceptor; - proposal.proposer = _params.proposer; - proposal.loanContract = _params.loanContract; - proposal.nonceSpace = _params.nonceSpace; - proposal.nonce = _params.nonce; + proposal.collateralAddress = _params.base.collateralAddress; + proposal.checkCollateralStateFingerprint = _params.base.checkCollateralStateFingerprint; + proposal.collateralStateFingerprint = _params.base.collateralStateFingerprint; + proposal.creditAmount = _params.base.creditAmount; + proposal.availableCreditLimit = _params.base.availableCreditLimit; + proposal.expiration = _params.base.expiration; + proposal.allowedAcceptor = _params.base.allowedAcceptor; + proposal.proposer = _params.base.proposer; + proposal.isOffer = _params.base.isOffer; + proposal.refinancingLoanId = _params.base.refinancingLoanId; + proposal.nonceSpace = _params.base.nonceSpace; + proposal.nonce = _params.base.nonce; + proposal.loanContract = _params.base.loanContract; + + proposalValues.collateralId = _params.base.collateralId; } function _proposalSignature(Params memory _params) internal view returns (bytes memory signature) { @@ -106,14 +108,14 @@ abstract contract PWNSimpleLoanListProposalTest is PWNSimpleLoanProposalTest { } - function _callAcceptProposalWith(Params memory _params, Permit memory _permit) internal override returns (uint256) { + function _callAcceptProposalWith(Params memory _params) internal override returns (bytes32, PWNSimpleLoan.Terms memory) { _updateProposal(_params); - return proposalContract.acceptProposal(proposal, proposalValues, _proposalSignature(params), 0, _permit, ""); - } - - function _callAcceptProposalWith(Params memory _params, Permit memory _permit, uint256 nonceSpace, uint256 nonce) internal override returns (uint256) { - _updateProposal(_params); - return proposalContract.acceptProposal(proposal, proposalValues, _proposalSignature(params), 0, _permit, "", nonceSpace, nonce); + return proposalContract.acceptProposal({ + acceptor: _params.acceptor, + refinancingLoanId: _params.refinancingLoanId, + proposalData: abi.encode(proposal, proposalValues), + signature: _proposalSignature(_params) + }); } function _getProposalHashWith(Params memory _params) internal override returns (bytes32) { @@ -209,166 +211,162 @@ contract PWNSimpleLoanListProposal_MakeProposal_Test is PWNSimpleLoanListProposa /*----------------------------------------------------------*| -|* # ACCEPT PROPOSAL AND REVOKE CALLERS NONCE *| +|* # ENCODE PROPOSAL DATA *| |*----------------------------------------------------------*/ -contract PWNSimpleLoanListProposal_AcceptProposalAndRevokeCallersNonce_Test is PWNSimpleLoanListProposalTest, PWNSimpleLoanProposal_AcceptProposalAndRevokeCallersNonce_Test { +contract PWNSimpleLoanListProposal_EncodeProposalData_Test is PWNSimpleLoanListProposalTest { - function setUp() virtual public override(PWNSimpleLoanListProposalTest, PWNSimpleLoanProposalTest) { - super.setUp(); + function test_shouldReturnEncodedProposalData() external { + assertEq( + proposalContract.encodeProposalData(proposal, proposalValues), + abi.encode(proposal, proposalValues) + ); } } /*----------------------------------------------------------*| -|* # ACCEPT PROPOSAL *| +|* # DECODE PROPOSAL DATA *| |*----------------------------------------------------------*/ -contract PWNSimpleLoanListProposal_AcceptProposal_Test is PWNSimpleLoanListProposalTest, PWNSimpleLoanProposal_AcceptProposal_Test { - - function setUp() virtual public override(PWNSimpleLoanListProposalTest, PWNSimpleLoanProposalTest) { - super.setUp(); +contract PWNSimpleLoanListProposal_DecodeProposalData_Test is PWNSimpleLoanListProposalTest { + + function test_shouldReturnDecodedProposalData() external { + ( + PWNSimpleLoanListProposal.Proposal memory _proposal, + PWNSimpleLoanListProposal.ProposalValues memory _proposalValues + ) = proposalContract.decodeProposalData(abi.encode(proposal, proposalValues)); + + assertEq(uint8(_proposal.collateralCategory), uint8(proposal.collateralCategory)); + assertEq(_proposal.collateralAddress, proposal.collateralAddress); + assertEq(_proposal.collateralIdsWhitelistMerkleRoot, proposal.collateralIdsWhitelistMerkleRoot); + assertEq(_proposal.collateralAmount, proposal.collateralAmount); + assertEq(_proposal.checkCollateralStateFingerprint, proposal.checkCollateralStateFingerprint); + assertEq(_proposal.collateralStateFingerprint, proposal.collateralStateFingerprint); + assertEq(_proposal.creditAddress, proposal.creditAddress); + assertEq(_proposal.creditAmount, proposal.creditAmount); + assertEq(_proposal.availableCreditLimit, proposal.availableCreditLimit); + assertEq(_proposal.fixedInterestAmount, proposal.fixedInterestAmount); + assertEq(_proposal.accruingInterestAPR, proposal.accruingInterestAPR); + assertEq(_proposal.duration, proposal.duration); + assertEq(_proposal.expiration, proposal.expiration); + assertEq(_proposal.allowedAcceptor, proposal.allowedAcceptor); + assertEq(_proposal.proposer, proposal.proposer); + assertEq(_proposal.isOffer, proposal.isOffer); + assertEq(_proposal.refinancingLoanId, proposal.refinancingLoanId); + assertEq(_proposal.nonceSpace, proposal.nonceSpace); + assertEq(_proposal.nonce, proposal.nonce); + assertEq(_proposal.loanContract, proposal.loanContract); + + assertEq(_proposalValues.collateralId, proposalValues.collateralId); + assertEq(_proposalValues.merkleInclusionProof.length, proposalValues.merkleInclusionProof.length); + for (uint256 i; i < _proposalValues.merkleInclusionProof.length; ++i) { + assertEq(_proposalValues.merkleInclusionProof[i], proposalValues.merkleInclusionProof[i]); + } } +} - function testFuzz_shouldFail_whenProposedRefinancingLoanIdNotZero_whenRefinancingLoanIdZero(uint256 proposedRefinancingLoanId) external { - vm.assume(proposedRefinancingLoanId != 0); - proposal.refinancingLoanId = proposedRefinancingLoanId; - - vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, proposedRefinancingLoanId)); - proposalContract.acceptProposal( - proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), 0, permit, "" - ); - } - function testFuzz_shouldFail_whenRefinancingLoanIdsIsNotEqual_whenProposedRefinanceingLoanIdNotZero_whenRefinancingLoanIdNotZero_whenOffer( - uint256 refinancingLoanId, uint256 proposedRefinancingLoanId - ) external { - vm.assume(proposedRefinancingLoanId != 0); - vm.assume(refinancingLoanId != proposedRefinancingLoanId); - proposal.refinancingLoanId = proposedRefinancingLoanId; - proposal.isOffer = true; - - vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, proposedRefinancingLoanId)); - proposalContract.acceptProposal( - proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), refinancingLoanId, permit, extra - ); - } +/*----------------------------------------------------------*| +|* # ACCEPT PROPOSAL *| +|*----------------------------------------------------------*/ - function testFuzz_shouldPass_whenRefinancingLoanIdsNotEqual_whenProposedRefinanceingLoanIdZero_whenRefinancingLoanIdNotZero_whenOffer( - uint256 refinancingLoanId - ) external { - vm.assume(refinancingLoanId != 0); - proposal.refinancingLoanId = 0; - proposal.isOffer = true; +contract PWNSimpleLoanListProposal_AcceptProposal_Test is PWNSimpleLoanListProposalTest, PWNSimpleLoanProposal_AcceptProposal_Test { - proposalContract.acceptProposal( - proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), refinancingLoanId, permit, extra - ); + function setUp() virtual public override(PWNSimpleLoanListProposalTest, PWNSimpleLoanProposalTest) { + super.setUp(); } - function testFuzz_shouldFail_whenRefinancingLoanIdsNotEqual_whenRefinancingLoanIdNotZero_whenRequest( - uint256 refinancingLoanId, uint256 proposedRefinancingLoanId - ) external { - vm.assume(refinancingLoanId != proposedRefinancingLoanId); - proposal.refinancingLoanId = proposedRefinancingLoanId; - proposal.isOffer = false; - vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, proposedRefinancingLoanId)); - proposalContract.acceptProposal( - proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), refinancingLoanId, permit, extra - ); - } - - function test_shouldAcceptAnyCollateralId_whenMerkleRootIsZero() external { - proposalValues.collateralId = 331; + function testFuzz_shouldAcceptAnyCollateralId_whenMerkleRootIsZero(uint256 collId) external { + proposalValues.collateralId = collId; proposal.collateralIdsWhitelistMerkleRoot = bytes32(0); - proposalContract.acceptProposal( - proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), 0, permit, "" - ); + vm.prank(activeLoanContract); + proposalContract.acceptProposal({ + acceptor: acceptor, + refinancingLoanId: 0, + proposalData: abi.encode(proposal, proposalValues), + signature: _signProposalHash(proposerPK, _proposalHash(proposal)) + }); } - function test_shouldPass_whenGivenCollateralIdIsWhitelisted() external { - bytes32 id1Hash = keccak256(abi.encodePacked(uint256(331))); - bytes32 id2Hash = keccak256(abi.encodePacked(uint256(133))); - proposal.collateralIdsWhitelistMerkleRoot = keccak256(abi.encodePacked(id1Hash, id2Hash)); + function testFuzz_shouldPass_whenGivenCollateralIdIsWhitelisted(uint256 collId1, uint256 collId2) external { + bytes32 id1Hash = keccak256(abi.encodePacked(collId1)); + bytes32 id2Hash = keccak256(abi.encodePacked(collId2)); + proposal.collateralIdsWhitelistMerkleRoot = keccak256( + uint256(id1Hash) < uint256(id2Hash) + ? abi.encodePacked(id1Hash, id2Hash) + : abi.encodePacked(id2Hash, id1Hash) + ); - proposalValues.collateralId = 331; + proposalValues.collateralId = collId1; proposalValues.merkleInclusionProof = new bytes32[](1); proposalValues.merkleInclusionProof[0] = id2Hash; - proposalContract.acceptProposal( - proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), 0, permit, "" - ); + vm.prank(activeLoanContract); + proposalContract.acceptProposal({ + acceptor: acceptor, + refinancingLoanId: 0, + proposalData: abi.encode(proposal, proposalValues), + signature: _signProposalHash(proposerPK, _proposalHash(proposal)) + }); } - function test_shouldFail_whenGivenCollateralIdIsNotWhitelisted() external { - bytes32 id1Hash = keccak256(abi.encodePacked(uint256(331))); - bytes32 id2Hash = keccak256(abi.encodePacked(uint256(133))); - proposal.collateralIdsWhitelistMerkleRoot = keccak256(abi.encodePacked(id1Hash, id2Hash)); + function testFuzz_shouldFail_whenGivenCollateralIdIsNotWhitelisted( + uint256 collId1, uint256 collId2, uint256 collId3 + ) external { + vm.assume(collId1 < collId2); + vm.assume(collId3 != collId1 && collId3 != collId2); + bytes32 id1Hash = keccak256(abi.encodePacked(collId1)); + bytes32 id2Hash = keccak256(abi.encodePacked(collId2)); + proposal.collateralIdsWhitelistMerkleRoot = keccak256( + uint256(id1Hash) < uint256(id2Hash) + ? abi.encodePacked(id1Hash, id2Hash) + : abi.encodePacked(id2Hash, id1Hash) + ); - proposalValues.collateralId = 333; + proposalValues.collateralId = collId3; proposalValues.merkleInclusionProof = new bytes32[](1); proposalValues.merkleInclusionProof[0] = id2Hash; vm.expectRevert(abi.encodeWithSelector(CollateralIdNotWhitelisted.selector, proposalValues.collateralId)); - proposalContract.acceptProposal( - proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), 0, permit, "" - ); + vm.prank(activeLoanContract); + proposalContract.acceptProposal({ + acceptor: acceptor, + refinancingLoanId: 0, + proposalData: abi.encode(proposal, proposalValues), + signature: _signProposalHash(proposerPK, _proposalHash(proposal)) + }); } - function testFuzz_shouldCallLoanContractWithLoanTerms(bool isOffer, uint256 refinancingLoanId) external { + function testFuzz_shouldCallLoanContractWithLoanTerms(bool isOffer) external { proposal.isOffer = isOffer; - proposal.refinancingLoanId = refinancingLoanId; - - permit = Permit({ - asset: token, - owner: acceptor, - amount: 100, - deadline: 1000, - v: 27, - r: bytes32(uint256(1)), - s: bytes32(uint256(2)) - }); - extra = "lil extra"; - - PWNSimpleLoan.Terms memory loanTerms = PWNSimpleLoan.Terms({ - lender: isOffer ? proposal.proposer : acceptor, - borrower: isOffer ? acceptor : proposal.proposer, - duration: proposal.duration, - collateral: MultiToken.Asset({ - category: proposal.collateralCategory, - assetAddress: proposal.collateralAddress, - id: proposalValues.collateralId, - amount: proposal.collateralAmount - }), - credit: MultiToken.Asset({ - category: MultiToken.Category.ERC20, - assetAddress: proposal.creditAddress, - id: 0, - amount: proposal.creditAmount - }), - fixedInterestAmount: proposal.fixedInterestAmount, - accruingInterestAPR: proposal.accruingInterestAPR - }); - vm.expectCall( - activeLoanContract, - refinancingLoanId == 0 - ? abi.encodeWithSelector( - PWNSimpleLoan.createLOAN.selector, _proposalHash(proposal), loanTerms, permit, extra - ) - : abi.encodeWithSelector( - PWNSimpleLoan.refinanceLOAN.selector, refinancingLoanId, _proposalHash(proposal), loanTerms, permit, extra - ) - ); + vm.prank(activeLoanContract); + (bytes32 proposalHash, PWNSimpleLoan.Terms memory terms) = proposalContract.acceptProposal({ + acceptor: acceptor, + refinancingLoanId: 0, + proposalData: abi.encode(proposal, proposalValues), + signature: _signProposalHash(proposerPK, _proposalHash(proposal)) + }); - vm.prank(acceptor); - proposalContract.acceptProposal( - proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), refinancingLoanId, permit, extra - ); + assertEq(proposalHash, _proposalHash(proposal)); + assertEq(terms.lender, isOffer ? proposal.proposer : acceptor); + assertEq(terms.borrower, isOffer ? acceptor : proposal.proposer); + assertEq(terms.duration, proposal.duration); + assertEq(uint8(terms.collateral.category), uint8(proposal.collateralCategory)); + assertEq(terms.collateral.assetAddress, proposal.collateralAddress); + assertEq(terms.collateral.id, proposalValues.collateralId); + assertEq(terms.collateral.amount, proposal.collateralAmount); + assertEq(uint8(terms.credit.category), uint8(MultiToken.Category.ERC20)); + assertEq(terms.credit.assetAddress, proposal.creditAddress); + assertEq(terms.credit.id, 0); + assertEq(terms.credit.amount, proposal.creditAmount); + assertEq(terms.fixedInterestAmount, proposal.fixedInterestAmount); + assertEq(terms.accruingInterestAPR, proposal.accruingInterestAPR); } } diff --git a/test/unit/PWNSimpleLoanProposal.t.sol b/test/unit/PWNSimpleLoanProposal.t.sol index 9d48ea2..19af484 100644 --- a/test/unit/PWNSimpleLoanProposal.t.sol +++ b/test/unit/PWNSimpleLoanProposal.t.sol @@ -8,7 +8,7 @@ import { MultiToken } from "MultiToken/MultiToken.sol"; import { Math } from "openzeppelin-contracts/contracts/utils/math/Math.sol"; import { PWNHubTags } from "@pwn/hub/PWNHubTags.sol"; -import { PWNSimpleLoan, Permit } from "@pwn/loan/terms/simple/loan/PWNSimpleLoan.sol"; +import { PWNSimpleLoan } from "@pwn/loan/terms/simple/loan/PWNSimpleLoan.sol"; import { PWNSimpleLoanProposal } from "@pwn/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol"; import "@pwn/PWNErrors.sol"; @@ -31,27 +31,16 @@ abstract contract PWNSimpleLoanProposalTest is Test { uint256 public loanId = 421; Params public params; - Permit public permit; bytes public extra; PWNSimpleLoanProposal public proposalContractAddr; // Need to set in the inheriting contract struct Params { - bool checkCollateralStateFingerprint; - bytes32 collateralStateFingerprint; - uint256 creditAmount; - uint256 availableCreditLimit; - uint32 duration; - uint40 accruingInterestAPR; - uint40 expiration; - address allowedAcceptor; - address proposer; - address loanContract; - uint256 nonceSpace; - uint256 nonce; + PWNSimpleLoanProposal.ProposalBase base; + address acceptor; + uint256 refinancingLoanId; uint256 signerPK; bool compactSignature; - // cannot add anymore fields b/c of stack too deep error } function setUp() virtual public { @@ -59,13 +48,14 @@ abstract contract PWNSimpleLoanProposalTest is Test { vm.etch(revokedNonce, bytes("data")); vm.etch(token, bytes("data")); - params.creditAmount = 1e10; - params.checkCollateralStateFingerprint = true; - params.collateralStateFingerprint = keccak256("some state fingerprint"); - params.duration = 1 hours; - params.expiration = uint40(block.timestamp + 20 minutes); - params.proposer = proposer; - params.loanContract = activeLoanContract; + params.base.creditAmount = 1e10; + params.base.checkCollateralStateFingerprint = true; + params.base.collateralStateFingerprint = keccak256("some state fingerprint"); + params.base.expiration = uint40(block.timestamp + 20 minutes); + params.base.proposer = proposer; + params.base.loanContract = activeLoanContract; + params.acceptor = acceptor; + params.refinancingLoanId = 0; params.signerPK = proposerPK; params.compactSignature = false; @@ -90,14 +80,7 @@ abstract contract PWNSimpleLoanProposalTest is Test { vm.mockCall( stateFingerprintComputer, abi.encodeWithSignature("getStateFingerprint(uint256)"), - abi.encode(params.collateralStateFingerprint) - ); - - vm.mockCall( - activeLoanContract, abi.encodeWithSelector(PWNSimpleLoan.createLOAN.selector), abi.encode(loanId) - ); - vm.mockCall( - activeLoanContract, abi.encodeWithSelector(PWNSimpleLoan.refinanceLOAN.selector), abi.encode(loanId) + abi.encode(params.base.collateralStateFingerprint) ); } @@ -111,8 +94,8 @@ abstract contract PWNSimpleLoanProposalTest is Test { return abi.encodePacked(r, bytes32(uint256(v) - 27) << 255 | s); } - function _callAcceptProposalWith() internal returns (uint256) { - return _callAcceptProposalWith(params, permit); + function _callAcceptProposalWith() internal returns (bytes32, PWNSimpleLoan.Terms memory) { + return _callAcceptProposalWith(params); } function _getProposalHashWith() internal returns (bytes32) { @@ -120,100 +103,33 @@ abstract contract PWNSimpleLoanProposalTest is Test { } // Virtual functions to be implemented in inheriting contract - function _callAcceptProposalWith(Params memory _params, Permit memory _permit) internal virtual returns (uint256); - function _callAcceptProposalWith(Params memory _params, Permit memory _permit, uint256 nonceSpace, uint256 nonce) internal virtual returns (uint256); + function _callAcceptProposalWith(Params memory _params) internal virtual returns (bytes32, PWNSimpleLoan.Terms memory); function _getProposalHashWith(Params memory _params) internal virtual returns (bytes32); } -/*----------------------------------------------------------*| -|* # ACCEPT PROPOSAL AND REVOKE CALLERS NONCE *| -|*----------------------------------------------------------*/ - -abstract contract PWNSimpleLoanProposal_AcceptProposalAndRevokeCallersNonce_Test is PWNSimpleLoanProposalTest { - - function testFuzz_shouldFail_whenNonceIsNotUsable(address caller, uint256 nonceSpace, uint256 nonce) external { - vm.mockCall( - revokedNonce, - abi.encodeWithSignature("isNonceUsable(address,uint256,uint256)", caller, nonceSpace, nonce), - abi.encode(false) - ); - - vm.expectRevert(abi.encodeWithSelector(NonceNotUsable.selector, caller, nonceSpace, nonce)); - vm.prank(caller); - _callAcceptProposalWith(params, permit, nonceSpace, nonce); - } - - function testFuzz_shouldRevokeCallersNonce(address caller, uint256 nonceSpace, uint256 nonce) external { - vm.expectCall( - revokedNonce, - abi.encodeWithSignature("isNonceUsable(address,uint256,uint256)", caller, nonceSpace, nonce) - ); - - vm.prank(caller); - _callAcceptProposalWith(params, permit, nonceSpace, nonce); - } - - // function is calling `acceptProposal`, no need to test it again - function test_shouldCallLoanContract() external { - assertEq(_callAcceptProposalWith(params, permit, 1, 2), loanId); - } - -} - - /*----------------------------------------------------------*| |* # ACCEPT PROPOSAL *| |*----------------------------------------------------------*/ abstract contract PWNSimpleLoanProposal_AcceptProposal_Test is PWNSimpleLoanProposalTest { - function testFuzz_shouldFail_whenLoanContractNotTagged_ACTIVE_LOAN(address loanContract) external { - vm.assume(loanContract != activeLoanContract); - params.loanContract = loanContract; - - vm.expectRevert(abi.encodeWithSelector(AddressMissingHubTag.selector, loanContract, PWNHubTags.ACTIVE_LOAN)); - _callAcceptProposalWith(); - } - - function test_shouldNotCallComputerRegistry_whenShouldNotCheckStateFingerprint() external { - params.checkCollateralStateFingerprint = false; - - vm.expectCall({ - callee: config, - data: abi.encodeWithSignature("getStateFingerprintComputer(address)"), - count: 0 - }); - - _callAcceptProposalWith(); - } + function testFuzz_shouldFail_whenCallerIsNotProposedLoanContract(address caller) external { + vm.assume(caller != activeLoanContract); + params.base.loanContract = activeLoanContract; - function test_shouldFail_whenComputerRegistryReturnsZeroAddress_whenShouldCheckStateFingerprint() external { - vm.mockCall( - config, - abi.encodeWithSignature("getStateFingerprintComputer(address)", token), // test expects `token` being used as collateral asset - abi.encode(address(0)) - ); - - vm.expectRevert(abi.encodeWithSelector(MissingStateFingerprintComputer.selector)); + vm.expectRevert(abi.encodeWithSelector(CallerNotLoanContract.selector, caller, activeLoanContract)); + vm.prank(caller); _callAcceptProposalWith(); } - function testFuzz_shouldFail_whenComputerReturnsDifferentStateFingerprint_whenShouldCheckStateFingerprint( - bytes32 stateFingerprint - ) external { - vm.assume(stateFingerprint != params.collateralStateFingerprint); + function testFuzz_shouldFail_whenCallerNotTagged_ACTIVE_LOAN(address caller) external { + vm.assume(caller != activeLoanContract); + params.base.loanContract = caller; - vm.mockCall( - stateFingerprintComputer, - abi.encodeWithSignature("getStateFingerprint(uint256)"), - abi.encode(stateFingerprint) - ); - - vm.expectRevert(abi.encodeWithSelector( - InvalidCollateralStateFingerprint.selector, stateFingerprint, params.collateralStateFingerprint - )); + vm.expectRevert(abi.encodeWithSelector(AddressMissingHubTag.selector, caller, PWNHubTags.ACTIVE_LOAN)); + vm.prank(caller); _callAcceptProposalWith(); } @@ -221,6 +137,7 @@ abstract contract PWNSimpleLoanProposal_AcceptProposal_Test is PWNSimpleLoanProp params.signerPK = 1; vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, proposer, _getProposalHashWith(params))); + vm.prank(activeLoanContract); _callAcceptProposalWith(); } @@ -229,6 +146,7 @@ abstract contract PWNSimpleLoanProposal_AcceptProposal_Test is PWNSimpleLoanProp params.signerPK = 0; vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, proposer, _getProposalHashWith(params))); + vm.prank(activeLoanContract); _callAcceptProposalWith(); } @@ -240,16 +158,21 @@ abstract contract PWNSimpleLoanProposal_AcceptProposal_Test is PWNSimpleLoanProp ); params.signerPK = 0; + vm.prank(activeLoanContract); _callAcceptProposalWith(); } function test_shouldPass_withValidSignature_whenEOA_whenStandardSignature() external { params.compactSignature = false; + + vm.prank(activeLoanContract); _callAcceptProposalWith(); } function test_shouldPass_withValidSignature_whenEOA_whenCompactEIP2098Signature() external { params.compactSignature = true; + + vm.prank(activeLoanContract); _callAcceptProposalWith(); } @@ -263,20 +186,71 @@ abstract contract PWNSimpleLoanProposal_AcceptProposal_Test is PWNSimpleLoanProp abi.encode(bytes4(0x1626ba7e)) ); + vm.prank(activeLoanContract); + _callAcceptProposalWith(); + } + + function testFuzz_shouldFail_whenProposedRefinancingLoanIdNotZero_whenRefinancingLoanIdZero(uint256 proposedRefinancingLoanId) external { + vm.assume(proposedRefinancingLoanId != 0); + params.base.refinancingLoanId = proposedRefinancingLoanId; + params.refinancingLoanId = 0; + + vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, proposedRefinancingLoanId)); + vm.prank(activeLoanContract); + _callAcceptProposalWith(); + } + + function testFuzz_shouldFail_whenRefinancingLoanIdsIsNotEqual_whenProposedRefinanceingLoanIdNotZero_whenRefinancingLoanIdNotZero_whenOffer( + uint256 refinancingLoanId, uint256 proposedRefinancingLoanId + ) external { + vm.assume(proposedRefinancingLoanId != 0); + vm.assume(refinancingLoanId != proposedRefinancingLoanId); + params.base.refinancingLoanId = proposedRefinancingLoanId; + params.base.isOffer = true; + params.refinancingLoanId = refinancingLoanId; + + vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, proposedRefinancingLoanId)); + vm.prank(activeLoanContract); + _callAcceptProposalWith(); + } + + function testFuzz_shouldPass_whenRefinancingLoanIdsNotEqual_whenProposedRefinanceingLoanIdZero_whenRefinancingLoanIdNotZero_whenOffer( + uint256 refinancingLoanId + ) external { + vm.assume(refinancingLoanId != 0); + params.base.refinancingLoanId = 0; + params.base.isOffer = true; + params.refinancingLoanId = refinancingLoanId; + + vm.prank(activeLoanContract); + _callAcceptProposalWith(); + } + + function testFuzz_shouldFail_whenRefinancingLoanIdsNotEqual_whenRefinancingLoanIdNotZero_whenRequest( + uint256 refinancingLoanId, uint256 proposedRefinancingLoanId + ) external { + vm.assume(refinancingLoanId != proposedRefinancingLoanId); + params.base.refinancingLoanId = proposedRefinancingLoanId; + params.base.isOffer = false; + params.refinancingLoanId = refinancingLoanId; + + vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, proposedRefinancingLoanId)); + vm.prank(activeLoanContract); _callAcceptProposalWith(); } function testFuzz_shouldFail_whenProposalExpired(uint256 timestamp) external { - timestamp = bound(timestamp, params.expiration, type(uint256).max); + timestamp = bound(timestamp, params.base.expiration, type(uint256).max); vm.warp(timestamp); - vm.expectRevert(abi.encodeWithSelector(Expired.selector, timestamp, params.expiration)); + vm.expectRevert(abi.encodeWithSelector(Expired.selector, timestamp, params.base.expiration)); + vm.prank(activeLoanContract); _callAcceptProposalWith(); } function testFuzz_shouldFail_whenOfferNonceNotUsable(uint256 nonceSpace, uint256 nonce) external { - params.nonceSpace = nonceSpace; - params.nonce = nonce; + params.base.nonceSpace = nonceSpace; + params.base.nonce = nonce; vm.mockCall( revokedNonce, @@ -288,59 +262,41 @@ abstract contract PWNSimpleLoanProposal_AcceptProposal_Test is PWNSimpleLoanProp abi.encodeWithSignature("isNonceUsable(address,uint256,uint256)", proposer, nonceSpace, nonce) ); - vm.expectRevert(abi.encodeWithSelector( - NonceNotUsable.selector, proposer, nonceSpace, nonce - )); + vm.expectRevert(abi.encodeWithSelector(NonceNotUsable.selector, proposer, nonceSpace, nonce)); + vm.prank(activeLoanContract); _callAcceptProposalWith(); } function testFuzz_shouldFail_whenCallerIsNotAllowedAcceptor(address caller) external { address allowedAcceptor = makeAddr("allowedAcceptor"); vm.assume(caller != allowedAcceptor); - params.allowedAcceptor = allowedAcceptor; + params.base.allowedAcceptor = allowedAcceptor; + params.acceptor = caller; vm.expectRevert(abi.encodeWithSelector(CallerNotAllowedAcceptor.selector, caller, allowedAcceptor)); - vm.prank(caller); - _callAcceptProposalWith(); - } - - function testFuzz_shouldFail_whenLessThanMinDuration(uint256 duration) external { - uint256 minDuration = proposalContractAddr.MIN_LOAN_DURATION(); - vm.assume(duration < minDuration); - duration = bound(duration, 0, minDuration - 1); - params.duration = uint32(duration); - - vm.expectRevert(abi.encodeWithSelector(InvalidDuration.selector, duration, minDuration)); - _callAcceptProposalWith(); - } - - function testFuzz_shouldFail_whenAccruingInterestAPROutOfBounds(uint256 interestAPR) external { - uint256 maxInterest = proposalContractAddr.MAX_ACCRUING_INTEREST_APR(); - interestAPR = bound(interestAPR, maxInterest + 1, type(uint40).max); - params.accruingInterestAPR = uint40(interestAPR); - - vm.expectRevert(abi.encodeWithSelector(AccruingInterestAPROutOfBounds.selector, interestAPR, maxInterest)); + vm.prank(activeLoanContract); _callAcceptProposalWith(); } function test_shouldRevokeOffer_whenAvailableCreditLimitEqualToZero(uint256 nonceSpace, uint256 nonce) external { - params.availableCreditLimit = 0; - params.nonceSpace = nonceSpace; - params.nonce = nonce; + params.base.availableCreditLimit = 0; + params.base.nonceSpace = nonceSpace; + params.base.nonce = nonce; vm.expectCall( revokedNonce, abi.encodeWithSignature("revokeNonce(address,uint256,uint256)", proposer, nonceSpace, nonce) ); + vm.prank(activeLoanContract); _callAcceptProposalWith(); } function testFuzz_shouldFail_whenUsedCreditExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { - used = bound(used, 1, type(uint256).max - params.creditAmount); - limit = bound(limit, used, used + params.creditAmount - 1); + used = bound(used, 1, type(uint256).max - params.base.creditAmount); + limit = bound(limit, used, used + params.base.creditAmount - 1); - params.availableCreditLimit = limit; + params.base.availableCreditLimit = limit; vm.store( address(proposalContractAddr), @@ -348,15 +304,18 @@ abstract contract PWNSimpleLoanProposal_AcceptProposal_Test is PWNSimpleLoanProp bytes32(used) ); - vm.expectRevert(abi.encodeWithSelector(AvailableCreditLimitExceeded.selector, used + params.creditAmount, limit)); + vm.expectRevert(abi.encodeWithSelector( + AvailableCreditLimitExceeded.selector, used + params.base.creditAmount, limit + )); + vm.prank(activeLoanContract); _callAcceptProposalWith(); } function testFuzz_shouldIncreaseUsedCredit_whenUsedCreditNotExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { - used = bound(used, 1, type(uint256).max - params.creditAmount); - limit = bound(limit, used + params.creditAmount, type(uint256).max); + used = bound(used, 1, type(uint256).max - params.base.creditAmount); + limit = bound(limit, used + params.base.creditAmount, type(uint256).max); - params.availableCreditLimit = limit; + params.base.availableCreditLimit = limit; bytes32 proposalHash = _getProposalHashWith(params); @@ -366,35 +325,55 @@ abstract contract PWNSimpleLoanProposal_AcceptProposal_Test is PWNSimpleLoanProp bytes32(used) ); + vm.prank(activeLoanContract); _callAcceptProposalWith(); - assertEq(proposalContractAddr.creditUsed(proposalHash), used + params.creditAmount); + assertEq(proposalContractAddr.creditUsed(proposalHash), used + params.base.creditAmount); } - function testFuzz_shouldFail_whenPermitOwnerNotCaller(address owner, address caller) external { - vm.assume(owner != caller && owner != address(0) && caller != address(0)); + function test_shouldNotCallComputerRegistry_whenShouldNotCheckStateFingerprint() external { + params.base.checkCollateralStateFingerprint = false; - permit.owner = owner; - permit.asset = token; // test expects `token` being used as credit asset + vm.expectCall({ + callee: config, + data: abi.encodeWithSignature("getStateFingerprintComputer(address)"), + count: 0 + }); - vm.expectRevert(abi.encodeWithSelector(InvalidPermitOwner.selector, owner, caller)); - vm.prank(caller); + vm.prank(activeLoanContract); _callAcceptProposalWith(); } - function testFuzz_shouldFail_whenPermitAssetNotCreditAsset(address asset, address caller) external { - vm.assume(asset != token && asset != address(0) && caller != address(0)); + function test_shouldFail_whenComputerRegistryReturnsZeroAddress_whenShouldCheckStateFingerprint() external { + params.base.collateralAddress = token; - permit.owner = caller; - permit.asset = asset; // test expects `token` being used as credit asset + vm.mockCall( + config, + abi.encodeWithSignature("getStateFingerprintComputer(address)", token), + abi.encode(address(0)) + ); - vm.expectRevert(abi.encodeWithSelector(InvalidPermitAsset.selector, asset, token)); - vm.prank(caller); + vm.expectRevert(abi.encodeWithSelector(MissingStateFingerprintComputer.selector)); + vm.prank(activeLoanContract); _callAcceptProposalWith(); } - function test_shouldReturnNewLoanId() external { - assertEq(_callAcceptProposalWith(), loanId); + function testFuzz_shouldFail_whenComputerReturnsDifferentStateFingerprint_whenShouldCheckStateFingerprint( + bytes32 stateFingerprint + ) external { + vm.assume(stateFingerprint != params.base.collateralStateFingerprint); + + vm.mockCall( + stateFingerprintComputer, + abi.encodeWithSignature("getStateFingerprint(uint256)"), + abi.encode(stateFingerprint) + ); + + vm.expectRevert(abi.encodeWithSelector( + InvalidCollateralStateFingerprint.selector, stateFingerprint, params.base.collateralStateFingerprint + )); + vm.prank(activeLoanContract); + _callAcceptProposalWith(); } } diff --git a/test/unit/PWNSimpleLoanSimpleProposal.t.sol b/test/unit/PWNSimpleLoanSimpleProposal.t.sol index ab6005a..a41e462 100644 --- a/test/unit/PWNSimpleLoanSimpleProposal.t.sol +++ b/test/unit/PWNSimpleLoanSimpleProposal.t.sol @@ -6,14 +6,13 @@ import "forge-std/Test.sol"; import { MultiToken } from "MultiToken/MultiToken.sol"; import { PWNHubTags } from "@pwn/hub/PWNHubTags.sol"; -import { PWNSimpleLoanSimpleProposal, PWNSimpleLoanProposal, PWNSimpleLoan, Permit } +import { PWNSimpleLoanSimpleProposal, PWNSimpleLoanProposal, PWNSimpleLoan } from "@pwn/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol"; import "@pwn/PWNErrors.sol"; import { PWNSimpleLoanProposalTest, - PWNSimpleLoanProposal_AcceptProposal_Test, - PWNSimpleLoanProposal_AcceptProposalAndRevokeCallersNonce_Test + PWNSimpleLoanProposal_AcceptProposal_Test } from "@pwn-test/unit/PWNSimpleLoanProposal.t.sol"; @@ -73,18 +72,20 @@ abstract contract PWNSimpleLoanSimpleProposalTest is PWNSimpleLoanProposalTest { } function _updateProposal(Params memory _params) internal { - proposal.checkCollateralStateFingerprint = _params.checkCollateralStateFingerprint; - proposal.collateralStateFingerprint = _params.collateralStateFingerprint; - proposal.creditAmount = _params.creditAmount; - proposal.availableCreditLimit = _params.availableCreditLimit; - proposal.duration = _params.duration; - proposal.accruingInterestAPR = _params.accruingInterestAPR; - proposal.expiration = _params.expiration; - proposal.allowedAcceptor = _params.allowedAcceptor; - proposal.proposer = _params.proposer; - proposal.loanContract = _params.loanContract; - proposal.nonceSpace = _params.nonceSpace; - proposal.nonce = _params.nonce; + proposal.collateralAddress = _params.base.collateralAddress; + proposal.collateralId = _params.base.collateralId; + proposal.checkCollateralStateFingerprint = _params.base.checkCollateralStateFingerprint; + proposal.collateralStateFingerprint = _params.base.collateralStateFingerprint; + proposal.creditAmount = _params.base.creditAmount; + proposal.availableCreditLimit = _params.base.availableCreditLimit; + proposal.expiration = _params.base.expiration; + proposal.allowedAcceptor = _params.base.allowedAcceptor; + proposal.proposer = _params.base.proposer; + proposal.isOffer = _params.base.isOffer; + proposal.refinancingLoanId = _params.base.refinancingLoanId; + proposal.nonceSpace = _params.base.nonceSpace; + proposal.nonce = _params.base.nonce; + proposal.loanContract = _params.base.loanContract; } function _proposalSignature(Params memory _params) internal view returns (bytes memory signature) { @@ -98,14 +99,14 @@ abstract contract PWNSimpleLoanSimpleProposalTest is PWNSimpleLoanProposalTest { } - function _callAcceptProposalWith(Params memory _params, Permit memory _permit) internal override returns (uint256) { + function _callAcceptProposalWith(Params memory _params) internal override returns (bytes32, PWNSimpleLoan.Terms memory) { _updateProposal(_params); - return proposalContract.acceptProposal(proposal, _proposalSignature(params), 0, _permit, ""); - } - - function _callAcceptProposalWith(Params memory _params, Permit memory _permit, uint256 nonceSpace, uint256 nonce) internal override returns (uint256) { - _updateProposal(_params); - return proposalContract.acceptProposal(proposal, _proposalSignature(params), 0, _permit, "", nonceSpace, nonce); + return proposalContract.acceptProposal({ + acceptor: _params.acceptor, + refinancingLoanId: _params.refinancingLoanId, + proposalData: abi.encode(proposal), + signature: _proposalSignature(_params) + }); } function _getProposalHashWith(Params memory _params) internal override returns (bytes32) { @@ -201,128 +202,88 @@ contract PWNSimpleLoanSimpleProposal_MakeProposal_Test is PWNSimpleLoanSimplePro /*----------------------------------------------------------*| -|* # ACCEPT PROPOSAL AND REVOKE CALLERS NONCE *| +|* # ENCODE PROPOSAL DATA *| |*----------------------------------------------------------*/ -contract PWNSimpleLoanSimpleProposal_AcceptProposalAndRevokeCallersNonce_Test is PWNSimpleLoanSimpleProposalTest, PWNSimpleLoanProposal_AcceptProposalAndRevokeCallersNonce_Test { +contract PWNSimpleLoanSimpleProposal_EncodeProposalData_Test is PWNSimpleLoanSimpleProposalTest { - function setUp() virtual public override(PWNSimpleLoanSimpleProposalTest, PWNSimpleLoanProposalTest) { - super.setUp(); + function test_shouldReturnEncodedProposalData() external { + assertEq(proposalContract.encodeProposalData(proposal), abi.encode(proposal)); } } /*----------------------------------------------------------*| -|* # ACCEPT PROPOSAL *| +|* # DECODE PROPOSAL DATA *| |*----------------------------------------------------------*/ -contract PWNSimpleLoanSimpleProposal_AcceptProposal_Test is PWNSimpleLoanSimpleProposalTest, PWNSimpleLoanProposal_AcceptProposal_Test { - - function setUp() virtual public override(PWNSimpleLoanSimpleProposalTest, PWNSimpleLoanProposalTest) { - super.setUp(); +contract PWNSimpleLoanSimpleProposal_DecodeProposalData_Test is PWNSimpleLoanSimpleProposalTest { + + function test_shouldReturnDecodedProposalData() external { + PWNSimpleLoanSimpleProposal.Proposal memory _proposal = proposalContract.decodeProposalData(abi.encode(proposal)); + + assertEq(uint8(_proposal.collateralCategory), uint8(proposal.collateralCategory)); + assertEq(_proposal.collateralAddress, proposal.collateralAddress); + assertEq(_proposal.collateralId, proposal.collateralId); + assertEq(_proposal.collateralAmount, proposal.collateralAmount); + assertEq(_proposal.checkCollateralStateFingerprint, proposal.checkCollateralStateFingerprint); + assertEq(_proposal.collateralStateFingerprint, proposal.collateralStateFingerprint); + assertEq(_proposal.creditAddress, proposal.creditAddress); + assertEq(_proposal.creditAmount, proposal.creditAmount); + assertEq(_proposal.availableCreditLimit, proposal.availableCreditLimit); + assertEq(_proposal.fixedInterestAmount, proposal.fixedInterestAmount); + assertEq(_proposal.accruingInterestAPR, proposal.accruingInterestAPR); + assertEq(_proposal.duration, proposal.duration); + assertEq(_proposal.expiration, proposal.expiration); + assertEq(_proposal.allowedAcceptor, proposal.allowedAcceptor); + assertEq(_proposal.proposer, proposal.proposer); + assertEq(_proposal.isOffer, proposal.isOffer); + assertEq(_proposal.refinancingLoanId, proposal.refinancingLoanId); + assertEq(_proposal.nonceSpace, proposal.nonceSpace); + assertEq(_proposal.nonce, proposal.nonce); + assertEq(_proposal.loanContract, proposal.loanContract); } +} - function testFuzz_shouldFail_whenProposedRefinancingLoanIdNotZero_whenRefinancingLoanIdZero(uint256 proposedRefinancingLoanId) external { - vm.assume(proposedRefinancingLoanId != 0); - proposal.refinancingLoanId = proposedRefinancingLoanId; - - vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, proposedRefinancingLoanId)); - proposalContract.acceptProposal( - proposal, _signProposalHash(proposerPK, _proposalHash(proposal)), 0, permit, "" - ); - } - function testFuzz_shouldFail_whenRefinancingLoanIdsIsNotEqual_whenProposedRefinanceingLoanIdNotZero_whenRefinancingLoanIdNotZero_whenOffer( - uint256 refinancingLoanId, uint256 proposedRefinancingLoanId - ) external { - vm.assume(proposedRefinancingLoanId != 0); - vm.assume(refinancingLoanId != proposedRefinancingLoanId); - proposal.refinancingLoanId = proposedRefinancingLoanId; - proposal.isOffer = true; - - vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, proposedRefinancingLoanId)); - proposalContract.acceptProposal( - proposal, _signProposalHash(proposerPK, _proposalHash(proposal)), refinancingLoanId, permit, extra - ); - } +/*----------------------------------------------------------*| +|* # ACCEPT PROPOSAL *| +|*----------------------------------------------------------*/ - function testFuzz_shouldPass_whenRefinancingLoanIdsNotEqual_whenProposedRefinanceingLoanIdZero_whenRefinancingLoanIdNotZero_whenOffer( - uint256 refinancingLoanId - ) external { - vm.assume(refinancingLoanId != 0); - proposal.refinancingLoanId = 0; - proposal.isOffer = true; +contract PWNSimpleLoanSimpleProposal_AcceptProposal_Test is PWNSimpleLoanSimpleProposalTest, PWNSimpleLoanProposal_AcceptProposal_Test { - proposalContract.acceptProposal( - proposal, _signProposalHash(proposerPK, _proposalHash(proposal)), refinancingLoanId, permit, extra - ); + function setUp() virtual public override(PWNSimpleLoanSimpleProposalTest, PWNSimpleLoanProposalTest) { + super.setUp(); } - function testFuzz_shouldFail_whenRefinancingLoanIdsNotEqual_whenRefinancingLoanIdNotZero_whenRequest( - uint256 refinancingLoanId, uint256 proposedRefinancingLoanId - ) external { - vm.assume(refinancingLoanId != proposedRefinancingLoanId); - proposal.refinancingLoanId = proposedRefinancingLoanId; - proposal.isOffer = false; - vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, proposedRefinancingLoanId)); - proposalContract.acceptProposal( - proposal, _signProposalHash(proposerPK, _proposalHash(proposal)), refinancingLoanId, permit, extra - ); - } - - function testFuzz_shouldCallLoanContractWithLoanTerms(bool isOffer, uint256 refinancingLoanId) external { + function testFuzz_shouldReturnProposalHashAndLoanTerms(bool isOffer) external { proposal.isOffer = isOffer; - proposal.refinancingLoanId = refinancingLoanId; - - permit = Permit({ - asset: token, - owner: acceptor, - amount: 100, - deadline: 1000, - v: 27, - r: bytes32(uint256(1)), - s: bytes32(uint256(2)) - }); - extra = "lil extra"; - - PWNSimpleLoan.Terms memory loanTerms = PWNSimpleLoan.Terms({ - lender: isOffer ? proposal.proposer : acceptor, - borrower: isOffer ? acceptor : proposal.proposer, - duration: proposal.duration, - collateral: MultiToken.Asset({ - category: proposal.collateralCategory, - assetAddress: proposal.collateralAddress, - id: proposal.collateralId, - amount: proposal.collateralAmount - }), - credit: MultiToken.Asset({ - category: MultiToken.Category.ERC20, - assetAddress: proposal.creditAddress, - id: 0, - amount: proposal.creditAmount - }), - fixedInterestAmount: proposal.fixedInterestAmount, - accruingInterestAPR: proposal.accruingInterestAPR - }); - vm.expectCall( - activeLoanContract, - refinancingLoanId == 0 - ? abi.encodeWithSelector( - PWNSimpleLoan.createLOAN.selector, _proposalHash(proposal), loanTerms, permit, extra - ) - : abi.encodeWithSelector( - PWNSimpleLoan.refinanceLOAN.selector, refinancingLoanId, _proposalHash(proposal), loanTerms, permit, extra - ) - ); + vm.prank(activeLoanContract); + (bytes32 proposalHash, PWNSimpleLoan.Terms memory terms) = proposalContract.acceptProposal({ + acceptor: acceptor, + refinancingLoanId: 0, + proposalData: abi.encode(proposal), + signature: _signProposalHash(proposerPK, _proposalHash(proposal)) + }); - vm.prank(acceptor); - proposalContract.acceptProposal( - proposal, _signProposalHash(proposerPK, _proposalHash(proposal)), refinancingLoanId, permit, extra - ); + assertEq(proposalHash, _proposalHash(proposal)); + assertEq(terms.lender, isOffer ? proposal.proposer : acceptor); + assertEq(terms.borrower, isOffer ? acceptor : proposal.proposer); + assertEq(terms.duration, proposal.duration); + assertEq(uint8(terms.collateral.category), uint8(proposal.collateralCategory)); + assertEq(terms.collateral.assetAddress, proposal.collateralAddress); + assertEq(terms.collateral.id, proposal.collateralId); + assertEq(terms.collateral.amount, proposal.collateralAmount); + assertEq(uint8(terms.credit.category), uint8(MultiToken.Category.ERC20)); + assertEq(terms.credit.assetAddress, proposal.creditAddress); + assertEq(terms.credit.id, 0); + assertEq(terms.credit.amount, proposal.creditAmount); + assertEq(terms.fixedInterestAmount, proposal.fixedInterestAmount); + assertEq(terms.accruingInterestAPR, proposal.accruingInterestAPR); } }