From ce0a4eb0d0e559a4f2fd85d96513abdc10c4c7b0 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Fri, 22 Mar 2024 10:39:02 -0300 Subject: [PATCH] feat(list-proposal): extend simple loan list offer by list request and rename to proposal --- script/PWN.s.sol | 24 +- src/Deployments.sol | 5 +- .../PWNSimpleLoanFungibleProposal.sol | 2 +- .../proposal/PWNSimpleLoanListProposal.sol | 343 ++++++ .../proposal/offer/PWNSimpleLoanListOffer.sol | 337 ------ test/helper/DeploymentTest.t.sol | 10 +- test/unit/PWNSimpleLoanFungibleProposal.t.sol | 8 +- test/unit/PWNSimpleLoanListOffer.t.sol | 973 ------------------ test/unit/PWNSimpleLoanListProposal.t.sol | 497 +++++++++ test/unit/PWNSimpleLoanSimpleProposal.t.sol | 8 +- 10 files changed, 860 insertions(+), 1347 deletions(-) create mode 100644 src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol delete mode 100644 src/loan/terms/simple/proposal/offer/PWNSimpleLoanListOffer.sol delete mode 100644 test/unit/PWNSimpleLoanListOffer.t.sol create mode 100644 test/unit/PWNSimpleLoanListProposal.t.sol diff --git a/script/PWN.s.sol b/script/PWN.s.sol index 953e2d6..d2dd958 100644 --- a/script/PWN.s.sol +++ b/script/PWN.s.sol @@ -13,7 +13,7 @@ import { IPWNDeployer } from "@pwn/deployer/IPWNDeployer.sol"; import { PWNHub } from "@pwn/hub/PWNHub.sol"; import { PWNHubTags } from "@pwn/hub/PWNHubTags.sol"; import { PWNSimpleLoan } from "@pwn/loan/terms/simple/loan/PWNSimpleLoan.sol"; -import { PWNSimpleLoanListOffer } from "@pwn/loan/terms/simple/proposal/offer/PWNSimpleLoanListOffer.sol"; +import { PWNSimpleLoanListProposal } from "@pwn/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol"; import { PWNSimpleLoanSimpleProposal } from "@pwn/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol"; import { PWNLOAN } from "@pwn/loan/token/PWNLOAN.sol"; import { PWNRevokedNonce } from "@pwn/nonce/PWNRevokedNonce.sol"; @@ -40,11 +40,9 @@ library PWNContractDeployerSalt { // Proposal types bytes32 internal constant SIMPLE_LOAN_SIMPLE_PROPOSAL = keccak256("PWNSimpleLoanSimpleProposal"); + bytes32 internal constant SIMPLE_LOAN_LIST_PROPOSAL = keccak256("PWNSimpleLoanListProposal"); bytes32 internal constant SIMPLE_LOAN_FUNGIBLE_PROPOSAL = keccak256("PWNSimpleLoanFungibleProposal"); - // Offer types - bytes32 internal constant SIMPLE_LOAN_LIST_OFFER = keccak256("PWNSimpleLoanListOffer"); - } @@ -171,22 +169,12 @@ forge script script/PWN.s.sol:Deploy \ ) })); - // - Offers - simpleLoanListOffer = PWNSimpleLoanListOffer(_deploy({ - salt: PWNContractDeployerSalt.SIMPLE_LOAN_LIST_OFFER, - bytecode: abi.encodePacked( - type(PWNSimpleLoanListOffer).creationCode, - abi.encode(address(hub), address(revokedNonce)) - ) - })); - console2.log("PWNConfig - singleton:", configSingleton); console2.log("PWNConfig - proxy:", address(config)); console2.log("PWNHub:", address(hub)); console2.log("PWNLOAN:", address(loanToken)); console2.log("PWNRevokedNonce:", address(revokedNonce)); console2.log("PWNSimpleLoan:", address(simpleLoan)); - console2.log("PWNSimpleLoanListOffer:", address(simpleLoanListOffer)); vm.stopBroadcast(); } @@ -267,14 +255,14 @@ forge script script/PWN.s.sol:Setup \ address[] memory addrs = new address[](4); addrs[0] = address(simpleLoan); addrs[1] = address(simpleLoan); - addrs[2] = address(simpleLoanListOffer); - addrs[3] = address(simpleLoanListOffer); + // addrs[2] = address(simpleLoanListOffer); + // addrs[3] = address(simpleLoanListOffer); bytes32[] memory tags = new bytes32[](4); tags[0] = PWNHubTags.ACTIVE_LOAN; tags[1] = PWNHubTags.NONCE_MANAGER; - tags[2] = PWNHubTags.LOAN_PROPOSAL; - tags[3] = PWNHubTags.NONCE_MANAGER; + // tags[2] = PWNHubTags.LOAN_PROPOSAL; + // tags[3] = PWNHubTags.NONCE_MANAGER; bool success = GnosisSafeLike(protocolSafe).execTransaction({ to: address(hub), diff --git a/src/Deployments.sol b/src/Deployments.sol index 10c86b7..b6c1b0d 100644 --- a/src/Deployments.sol +++ b/src/Deployments.sol @@ -13,7 +13,7 @@ import { IPWNDeployer } from "@pwn/deployer/IPWNDeployer.sol"; import { PWNHub } from "@pwn/hub/PWNHub.sol"; import { PWNHubTags } from "@pwn/hub/PWNHubTags.sol"; import { PWNSimpleLoan } from "@pwn/loan/terms/simple/loan/PWNSimpleLoan.sol"; -import { PWNSimpleLoanListOffer } from "@pwn/loan/terms/simple/proposal/offer/PWNSimpleLoanListOffer.sol"; +import { PWNSimpleLoanListProposal } from "@pwn/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol"; import { PWNSimpleLoanSimpleProposal } from "@pwn/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol"; import { PWNLOAN } from "@pwn/loan/token/PWNLOAN.sol"; import { PWNRevokedNonce } from "@pwn/nonce/PWNRevokedNonce.sol"; @@ -44,7 +44,6 @@ abstract contract Deployments is CommonBase { address protocolTimelock; PWNRevokedNonce revokedNonce; PWNSimpleLoan simpleLoan; - PWNSimpleLoanListOffer simpleLoanListOffer; StateFingerprintComputerRegistry stateFingerprintComputerRegistry; } @@ -68,7 +67,6 @@ abstract contract Deployments is CommonBase { PWNLOAN loanToken; PWNSimpleLoan simpleLoan; PWNRevokedNonce revokedNonce; - PWNSimpleLoanListOffer simpleLoanListOffer; function _loadDeployedAddresses() internal { @@ -96,7 +94,6 @@ abstract contract Deployments is CommonBase { loanToken = deployment.loanToken; simpleLoan = deployment.simpleLoan; revokedNonce = deployment.revokedNonce; - simpleLoanListOffer = deployment.simpleLoanListOffer; stateFingerprintComputerRegistry = deployment.stateFingerprintComputerRegistry; categoryRegistry = deployment.categoryRegistry; } else { diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol index 4213092..676f7a5 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol @@ -81,7 +81,7 @@ contract PWNSimpleLoanFungibleProposal is PWNSimpleLoanProposal { } /** - * @notice Construct defining proposal concrete values + * @notice Construct defining proposal concrete values. * @param collateralAmount Amount of collateral to be used in the loan. */ struct ProposalValues { diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol new file mode 100644 index 0000000..6c7db90 --- /dev/null +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol @@ -0,0 +1,343 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.16; + +import { MultiToken } from "MultiToken/MultiToken.sol"; + +import { MerkleProof } from "openzeppelin-contracts/contracts/utils/cryptography/MerkleProof.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"; + + +/** + * @title PWN Simple Loan List Proposal + * @notice Contract for creating and accepting list loan proposals. + * @dev The proposal can define a list of acceptable collateral ids or the whole collection. + */ +contract PWNSimpleLoanListProposal is PWNSimpleLoanProposal { + + string public constant VERSION = "1.2"; + + /** + * @dev EIP-712 simple proposal struct type hash. + */ + bytes32 public constant PROPOSAL_TYPEHASH = keccak256( + "Proposal(uint8 collateralCategory,address collateralAddress,bytes32 collateralIdsWhitelistMerkleRoot,uint256 collateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address creditAddress,uint256 creditAmount,uint256 availableCreditLimit,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedAcceptor,address proposer,bool isOffer,uint256 refinancingLoanId,uint256 nonceSpace,uint256 nonce,address loanContract)" + ); + + /** + * @notice Construct defining a list proposal. + * @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 collateralIdsWhitelistMerkleRoot Merkle tree root of a set of whitelisted collateral ids. + * @param collateralAmount Amount of tokens used as a collateral, in case of ERC721 should be 0. + * @param checkCollateralStateFingerprint If true, the collateral state fingerprint has to be checked. + * @param collateralStateFingerprint Fingerprint of a collateral state defined by ERC5646. + * @param creditAddress Address of an asset which is lender to a borrower. + * @param creditAmount Amount of tokens which is proposed as a loan to a borrower. + * @param availableCreditLimit Available credit limit for the proposal. It is the maximum amount of tokens which can be borrowed using the proposal. + * @param fixedInterestAmount Fixed interest amount in credit tokens. It is the minimum amount of interest which has to be paid by a borrower. + * @param accruingInterestAPR Accruing interest APR. + * @param duration Loan duration in seconds. + * @param expiration Proposal expiration timestamp in seconds. + * @param allowedAcceptor Address that is allowed to accept proposal. If the address is zero address, anybody can accept the proposal. + * @param proposer Address of a proposal signer. If `isOffer` is true, the proposer is the lender. If `isOffer` is false, the proposer is the borrower. + * @param isOffer If true, the proposal is an offer. If false, the proposal is a request. + * @param refinancingLoanId Id of a loan which is refinanced by this proposal. If the id is 0 and `isOffer` is true, the proposal can refinance any loan. + * @param nonceSpace Nonce space of a proposal nonce. All nonces in the same space can be revoked at once. + * @param nonce Additional value to enable identical proposals in time. Without it, it would be impossible to make again proposal, which was once revoked. + * Can be used to create a group of proposals, where accepting one proposal will make other proposals in the group revoked. + * @param loanContract Address of a loan contract that will create a loan from the proposal. + */ + struct Proposal { + MultiToken.Category collateralCategory; + address collateralAddress; + bytes32 collateralIdsWhitelistMerkleRoot; + uint256 collateralAmount; + bool checkCollateralStateFingerprint; + bytes32 collateralStateFingerprint; + address creditAddress; + uint256 creditAmount; + uint256 availableCreditLimit; + uint256 fixedInterestAmount; + uint40 accruingInterestAPR; + uint32 duration; + uint40 expiration; + address allowedAcceptor; + address proposer; + bool isOffer; + uint256 refinancingLoanId; + uint256 nonceSpace; + uint256 nonce; + address loanContract; + } + + /** + * @notice Construct defining proposal concrete values. + * @param collateralId Selected collateral id to be used as a collateral. + * @param merkleInclusionProof Proof of inclusion, that selected collateral id is whitelisted. + * This proof should create same hash as the merkle tree root given in the proposal. + * Can be empty for a proposal on a whole collection. + */ + struct ProposalValues { + uint256 collateralId; + bytes32[] merkleInclusionProof; + } + + /** + * @dev Emitted when a proposal is made via an on-chain transaction. + */ + event ProposalMade(bytes32 indexed proposalHash, address indexed proposer, Proposal proposal); + + constructor( + address _hub, + address _revokedNonce, + address _stateFingerprintComputerRegistry + ) PWNSimpleLoanProposal( + _hub, _revokedNonce, _stateFingerprintComputerRegistry, "PWNSimpleLoanListProposal", VERSION + ) {} + + /** + * @notice Get an proposal hash according to EIP-712 + * @param proposal Proposal struct to be hashed. + * @return Proposal struct hash. + */ + function getProposalHash(Proposal calldata proposal) public view returns (bytes32) { + return _getProposalHash(PROPOSAL_TYPEHASH, abi.encode(proposal)); + } + + /** + * @notice Make an on-chain proposal. + * @dev Function will mark a proposal hash as proposed. + * @param proposal Proposal struct containing all needed proposal data. + * @return proposalHash Proposal hash. + */ + function makeProposal(Proposal calldata proposal) external returns (bytes32 proposalHash) { + proposalHash = getProposalHash(proposal); + _makeProposal(proposalHash, proposal.proposer); + emit ProposalMade(proposalHash, proposal.proposer, proposal); + } + + /** + * @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 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, + Permit calldata permit, + bytes calldata extra + ) public returns (uint256 loanId) { + // Check if the proposal is refinancing proposal + if (proposal.refinancingLoanId != 0) { + revert InvalidRefinancingLoanId({ refinancingLoanId: proposal.refinancingLoanId }); + } + + // Check permit + _checkPermit(msg.sender, proposal.creditAddress, permit); + + // Accept proposal + (bytes32 proposalHash, PWNSimpleLoan.Terms memory loanTerms) + = _acceptProposal(proposal, proposalValues, signature); + + // Create loan + return PWNSimpleLoan(proposal.loanContract).createLOAN({ + proposalHash: proposalHash, + loanTerms: loanTerms, + permit: permit, + extra: extra + }); + } + + /** + * @notice Accept a refinancing proposal. + * @param loanId Id of a loan to be refinanced. + * @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 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 refinancedLoanId Id of a created refinanced loan. + */ + function acceptRefinanceProposal( + uint256 loanId, + Proposal calldata proposal, + ProposalValues calldata proposalValues, + bytes calldata signature, + Permit calldata permit, + bytes calldata extra + ) public returns (uint256 refinancedLoanId) { + // Check if the proposal is refinancing proposal + if (proposal.refinancingLoanId != loanId) { + if (proposal.refinancingLoanId != 0 || !proposal.isOffer) { + revert InvalidRefinancingLoanId({ refinancingLoanId: proposal.refinancingLoanId }); + } + } + + // Check permit + _checkPermit(msg.sender, proposal.creditAddress, permit); + + // Accept proposal + (bytes32 proposalHash, PWNSimpleLoan.Terms memory loanTerms) + = _acceptProposal(proposal, proposalValues, signature); + + // Refinance loan + return PWNSimpleLoan(proposal.loanContract).refinanceLOAN({ + loanId: loanId, + proposalHash: proposalHash, + loanTerms: loanTerms, + permit: permit, + extra: extra + }); + } + + /** + * @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 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. + */ + function acceptProposal( + Proposal calldata proposal, + ProposalValues calldata proposalValues, + bytes calldata signature, + Permit calldata permit, + bytes calldata extra, + uint256 callersNonceSpace, + uint256 callersNonceToRevoke + ) external returns (uint256 loanId) { + _revokeCallersNonce(msg.sender, callersNonceSpace, callersNonceToRevoke); + return acceptProposal(proposal, proposalValues, signature, permit, extra); + } + + /** + * @notice Accept a refinancing proposal with a callers nonce revocation. + * @dev Function will mark callers nonce as revoked. + * @param loanId Id of a loan to be refinanced. + * @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 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 refinancedLoanId Id of a created refinanced loan. + */ + function acceptRefinanceProposal( + uint256 loanId, + Proposal calldata proposal, + ProposalValues calldata proposalValues, + bytes calldata signature, + Permit calldata permit, + bytes calldata extra, + uint256 callersNonceSpace, + uint256 callersNonceToRevoke + ) external returns (uint256 refinancedLoanId) { + _revokeCallersNonce(msg.sender, callersNonceSpace, callersNonceToRevoke); + return acceptRefinanceProposal(loanId, proposal, proposalValues, signature, permit, extra); + } + + + /*----------------------------------------------------------*| + |* # INTERNALS *| + |*----------------------------------------------------------*/ + + function _acceptProposal( + Proposal calldata proposal, + ProposalValues calldata proposalValues, + bytes calldata signature + ) private returns (bytes32 proposalHash, PWNSimpleLoan.Terms memory loanTerms) { + // Check if the loan contract has a tag + _checkLoanContractTag(proposal.loanContract); + + // Check provided collateral id + if (proposal.collateralIdsWhitelistMerkleRoot != bytes32(0)) { + _checkCollateralId(proposal, proposalValues); + } + + // 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 + }); + } + + // 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)) + }) + ) 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, + duration: proposal.duration, + collateral: MultiToken.Asset({ + category: proposal.collateralCategory, + assetAddress: proposal.collateralAddress, + id: proposalValues.collateralId, + amount: proposal.collateralAmount + }), + credit: MultiToken.ERC20({ + assetAddress: proposal.creditAddress, + amount: proposal.creditAmount + }), + fixedInterestAmount: proposal.fixedInterestAmount, + accruingInterestAPR: proposal.accruingInterestAPR + }); + } + +} diff --git a/src/loan/terms/simple/proposal/offer/PWNSimpleLoanListOffer.sol b/src/loan/terms/simple/proposal/offer/PWNSimpleLoanListOffer.sol deleted file mode 100644 index d33cc39..0000000 --- a/src/loan/terms/simple/proposal/offer/PWNSimpleLoanListOffer.sol +++ /dev/null @@ -1,337 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity 0.8.16; - -import { MultiToken } from "MultiToken/MultiToken.sol"; - -import { MerkleProof } from "openzeppelin-contracts/contracts/utils/cryptography/MerkleProof.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"; - - -/** - * @title PWN Simple Loan List Offer - * @notice Loan terms factory contract creating a simple loan terms from a list offer. - * @dev This offer can be used as a collection offer or define a list of acceptable ids from a collection. - */ -contract PWNSimpleLoanListOffer is PWNSimpleLoanProposal { - - string public constant VERSION = "1.2"; - - /** - * @dev EIP-712 simple offer struct type hash. - */ - bytes32 public constant OFFER_TYPEHASH = keccak256( - "Offer(uint8 collateralCategory,address collateralAddress,bytes32 collateralIdsWhitelistMerkleRoot,uint256 collateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address creditAddress,uint256 creditAmount,uint256 availableCreditLimit,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedBorrower,address lender,uint256 refinancingLoanId,uint256 nonceSpace,uint256 nonce,address loanContract)" - ); - - /** - * @notice Construct defining a list offer. - * @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 collateralIdsWhitelistMerkleRoot Merkle tree root of a set of whitelisted collateral ids. - * @param collateralAmount Amount of tokens used as a collateral, in case of ERC721 should be 0. - * @param checkCollateralStateFingerprint If true, collateral state fingerprint will be checked on loan terms creation. - * @param collateralStateFingerprint Fingerprint of a collateral state. It is used to check if a collateral is in a valid state. - * @param creditAddress Address of an asset which is lender to a borrower. - * @param creditAmount Amount of tokens which is offered as a loan to a borrower. - * @param availableCreditLimit Available credit limit for the offer. It is the maximum amount of tokens which can be borrowed using the offer. - * @param fixedInterestAmount Fixed interest amount in credit tokens. It is the minimum amount of interest which has to be paid by a borrower. - * @param accruingInterestAPR Accruing interest APR. - * @param duration Loan duration in seconds. - * @param expiration Offer expiration timestamp in seconds. - * @param allowedBorrower Address of an allowed borrower. Only this address can accept an offer. If the address is zero address, anybody with a collateral can accept the offer. - * @param lender Address of a lender. This address has to sign an offer to be valid. - * @param refinancingLoanId Id of a loan which is refinanced by this offer. If the id is 0, the offer can refinance any loan. - * @param nonceSpace Nonce space of an offer nonce. All nonces in the same space can be revoked at once. - * @param nonce Additional value to enable identical offers in time. Without it, it would be impossible to make again offer, which was once revoked. - * Can be used to create a group of offers, where accepting one offer will make other offers in the group revoked. - * @param loanContract Address of a loan contract that will create a loan from the offer. - */ - struct Offer { - MultiToken.Category collateralCategory; - address collateralAddress; - bytes32 collateralIdsWhitelistMerkleRoot; - uint256 collateralAmount; - bool checkCollateralStateFingerprint; - bytes32 collateralStateFingerprint; - address creditAddress; - uint256 creditAmount; - uint256 availableCreditLimit; - uint256 fixedInterestAmount; - uint40 accruingInterestAPR; - uint32 duration; - uint40 expiration; - address allowedBorrower; - address lender; - uint256 refinancingLoanId; - uint256 nonceSpace; - uint256 nonce; - address loanContract; - } - - /** - * @notice Construct defining an Offer concrete values - * @param collateralId Selected collateral id to be used as a collateral. - * @param merkleInclusionProof Proof of inclusion, that selected collateral id is whitelisted. - * This proof should create same hash as the merkle tree root given in an Offer. - * Can be empty for collection offers. - */ - struct OfferValues { - uint256 collateralId; - bytes32[] merkleInclusionProof; - } - - /** - * @dev Emitted when a proposal is made via an on-chain transaction. - */ - event OfferMade(bytes32 indexed proposalHash, address indexed proposer, Offer offer); - - constructor( - address _hub, - address _revokedNonce, - address _stateFingerprintComputerRegistry - ) PWNSimpleLoanProposal( - _hub, _revokedNonce, _stateFingerprintComputerRegistry, "PWNSimpleLoanListOffer", VERSION - ) {} - - /** - * @notice Get an offer hash according to EIP-712 - * @param offer Offer struct to be hashed. - * @return Offer struct hash. - */ - function getOfferHash(Offer calldata offer) public view returns (bytes32) { - return _getProposalHash(OFFER_TYPEHASH, abi.encode(offer)); - } - - /** - * @notice Make an on-chain offer. - * @dev Function will mark an offer hash as proposed. - * @param offer Offer struct containing all needed offer data. - * @return proposalHash Offer hash. - */ - function makeOffer(Offer calldata offer) external returns (bytes32 proposalHash) { - proposalHash = getOfferHash(offer); - _makeProposal(proposalHash, offer.lender); - emit OfferMade(proposalHash, offer.lender, offer); - } - - /** - * @notice Accept an offer. - * @param offer Offer struct containing all offer data. - * @param offerValues OfferValues struct specifying all flexible offer values. - * @param signature Lender signature of an offer. - * @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 acceptOffer( - Offer calldata offer, - OfferValues calldata offerValues, - bytes calldata signature, - Permit calldata permit, - bytes calldata extra - ) public returns (uint256 loanId) { - // Check if the offer is refinancing offer - if (offer.refinancingLoanId != 0) { - revert InvalidRefinancingLoanId({ refinancingLoanId: offer.refinancingLoanId }); - } - - // Check permit - _checkPermit(msg.sender, offer.creditAddress, permit); - - // Accept offer - (bytes32 offerHash, PWNSimpleLoan.Terms memory loanTerms) = _acceptOffer(offer, offerValues, signature); - - // Create loan - return PWNSimpleLoan(offer.loanContract).createLOAN({ - proposalHash: offerHash, - loanTerms: loanTerms, - permit: permit, - extra: extra - }); - } - - /** - * @notice Accept a refinancing offer. - * @param loanId Id of a loan to be refinanced. - * @param offer Offer struct containing all offer data. - * @param offerValues OfferValues struct specifying all flexible offer values. - * @param signature Lender signature of an offer. - * @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 refinancedLoanId Id of a created refinanced loan. - */ - function acceptRefinanceOffer( - uint256 loanId, - Offer calldata offer, - OfferValues calldata offerValues, - bytes calldata signature, - Permit calldata permit, - bytes calldata extra - ) public returns (uint256 refinancedLoanId) { - // Check if the offer is refinancing offer - if (offer.refinancingLoanId != 0 && offer.refinancingLoanId != loanId) { - revert InvalidRefinancingLoanId({ refinancingLoanId: offer.refinancingLoanId }); - } - - // Check permit - _checkPermit(msg.sender, offer.creditAddress, permit); - - // Accept offer - (bytes32 offerHash, PWNSimpleLoan.Terms memory loanTerms) = _acceptOffer(offer, offerValues, signature); - - // Refinance loan - return PWNSimpleLoan(offer.loanContract).refinanceLOAN({ - loanId: loanId, - proposalHash: offerHash, - loanTerms: loanTerms, - permit: permit, - extra: extra - }); - } - - /** - * @notice Accept an offer with a callers nonce revocation. - * @dev Function will mark an offer hash and callers nonce as revoked. - * @param offer Offer struct containing all offer data. - * @param offerValues OfferValues struct specifying all flexible offer values. - * @param signature Lender signature of an offer. - * @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. - */ - function acceptOffer( - Offer calldata offer, - OfferValues calldata offerValues, - bytes calldata signature, - Permit calldata permit, - bytes calldata extra, - uint256 callersNonceSpace, - uint256 callersNonceToRevoke - ) external returns (uint256 loanId) { - _revokeCallersNonce(msg.sender, callersNonceSpace, callersNonceToRevoke); - return acceptOffer(offer, offerValues, signature, permit, extra); - } - - /** - * @notice Accept a refinancing offer with a callers nonce revocation. - * @dev Function will mark an offer hash and callers nonce as revoked. - * @param loanId Id of a loan to be refinanced. - * @param offer Offer struct containing all offer data. - * @param offerValues OfferValues struct specifying all flexible offer values. - * @param signature Lender signature of an offer. - * @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 refinancedLoanId Id of a created refinanced loan. - */ - function acceptRefinanceOffer( - uint256 loanId, - Offer calldata offer, - OfferValues calldata offerValues, - bytes calldata signature, - Permit calldata permit, - bytes calldata extra, - uint256 callersNonceSpace, - uint256 callersNonceToRevoke - ) external returns (uint256 refinancedLoanId) { - _revokeCallersNonce(msg.sender, callersNonceSpace, callersNonceToRevoke); - return acceptRefinanceOffer(loanId, offer, offerValues, signature, permit, extra); - } - - - /*----------------------------------------------------------*| - |* # INTERNALS *| - |*----------------------------------------------------------*/ - - function _acceptOffer( - Offer calldata offer, - OfferValues calldata offerValues, - bytes calldata signature - ) private returns (bytes32 offerHash, PWNSimpleLoan.Terms memory loanTerms) { - // Check if the loan contract has a tag - _checkLoanContractTag(offer.loanContract); - - // Check provided collateral id - if (offer.collateralIdsWhitelistMerkleRoot != bytes32(0)) { - _checkCollateralId(offer, offerValues); - } - - // Note: If the `collateralIdsWhitelistMerkleRoot` is empty, then it is a collection offer - // and any collateral id can be used. - - // Check collateral state fingerprint if needed - if (offer.checkCollateralStateFingerprint) { - _checkCollateralState({ - addr: offer.collateralAddress, - id: offerValues.collateralId, - stateFingerprint: offer.collateralStateFingerprint - }); - } - - // Try to accept offer - offerHash = _tryAcceptOffer(offer, signature); - - // Create loan terms object - loanTerms = _createLoanTerms(offer, offerValues); - } - - function _checkCollateralId(Offer calldata offer, OfferValues calldata offerValues) private pure { - // Verify whitelisted collateral id - if ( - !MerkleProof.verify({ - proof: offerValues.merkleInclusionProof, - root: offer.collateralIdsWhitelistMerkleRoot, - leaf: keccak256(abi.encodePacked(offerValues.collateralId)) - }) - ) revert CollateralIdNotWhitelisted({ id: offerValues.collateralId }); - } - - function _tryAcceptOffer(Offer calldata offer, bytes calldata signature) private returns (bytes32 offerHash) { - offerHash = getOfferHash(offer); - _tryAcceptProposal({ - proposalHash: offerHash, - creditAmount: offer.creditAmount, - availableCreditLimit: offer.availableCreditLimit, - apr: offer.accruingInterestAPR, - duration: offer.duration, - expiration: offer.expiration, - nonceSpace: offer.nonceSpace, - nonce: offer.nonce, - allowedAcceptor: offer.allowedBorrower, - acceptor: msg.sender, - signer: offer.lender, - signature: signature - }); - } - - function _createLoanTerms( - Offer calldata offer, - OfferValues calldata offerValues - ) private view returns (PWNSimpleLoan.Terms memory) { - return PWNSimpleLoan.Terms({ - lender: offer.lender, - borrower: msg.sender, - duration: offer.duration, - collateral: MultiToken.Asset({ - category: offer.collateralCategory, - assetAddress: offer.collateralAddress, - id: offerValues.collateralId, - amount: offer.collateralAmount - }), - credit: MultiToken.ERC20({ - assetAddress: offer.creditAddress, - amount: offer.creditAmount - }), - fixedInterestAmount: offer.fixedInterestAmount, - accruingInterestAPR: offer.accruingInterestAPR - }); - } - -} diff --git a/test/helper/DeploymentTest.t.sol b/test/helper/DeploymentTest.t.sol index d2d3170..030e6ad 100644 --- a/test/helper/DeploymentTest.t.sol +++ b/test/helper/DeploymentTest.t.sol @@ -45,20 +45,18 @@ abstract contract DeploymentTest is Deployments, Test { address(hub), address(loanToken), address(config), address(revokedNonce), address(categoryRegistry) ); - simpleLoanListOffer = new PWNSimpleLoanListOffer(address(hub), address(revokedNonce), address(stateFingerprintComputerRegistry)); - // Set hub tags address[] memory addrs = new address[](4); addrs[0] = address(simpleLoan); addrs[1] = address(simpleLoan); - addrs[2] = address(simpleLoanListOffer); - addrs[3] = address(simpleLoanListOffer); + // addrs[2] = address(simpleLoanListOffer); + // addrs[3] = address(simpleLoanListOffer); bytes32[] memory tags = new bytes32[](4); tags[0] = PWNHubTags.ACTIVE_LOAN; tags[1] = PWNHubTags.NONCE_MANAGER; - tags[2] = PWNHubTags.LOAN_PROPOSAL; - tags[3] = PWNHubTags.NONCE_MANAGER; + // tags[2] = PWNHubTags.LOAN_PROPOSAL; + // tags[3] = PWNHubTags.NONCE_MANAGER; vm.prank(protocolSafe); hub.setTags(addrs, tags, true); diff --git a/test/unit/PWNSimpleLoanFungibleProposal.t.sol b/test/unit/PWNSimpleLoanFungibleProposal.t.sol index b3ebc72..dcb4987 100644 --- a/test/unit/PWNSimpleLoanFungibleProposal.t.sol +++ b/test/unit/PWNSimpleLoanFungibleProposal.t.sol @@ -180,7 +180,7 @@ contract PWNSimpleLoanFungibleProposal_RevokeNonce_Test is PWNSimpleLoanFungible contract PWNSimpleLoanFungibleProposal_GetProposalHash_Test is PWNSimpleLoanFungibleProposalTest { - function test_shouldReturnOfferHash() external { + function test_shouldReturnProposalHash() external { assertEq(_proposalHash(proposal), proposalContract.getProposalHash(proposal)); } @@ -201,7 +201,7 @@ contract PWNSimpleLoanFungibleProposal_MakeProposal_Test is PWNSimpleLoanFungibl proposalContract.makeProposal(proposal); } - function test_shouldEmit_OfferMade() external { + function test_shouldEmit_ProposalMade() external { vm.expectEmit(); emit ProposalMade(_proposalHash(proposal), proposal.proposer, proposal); @@ -209,14 +209,14 @@ contract PWNSimpleLoanFungibleProposal_MakeProposal_Test is PWNSimpleLoanFungibl proposalContract.makeProposal(proposal); } - function test_shouldMakeOffer() external { + function test_shouldMakeProposal() external { vm.prank(proposal.proposer); proposalContract.makeProposal(proposal); assertTrue(proposalContract.proposalsMade(_proposalHash(proposal))); } - function test_shouldReturnOfferHash() external { + function test_shouldReturnProposalHash() external { vm.prank(proposal.proposer); assertEq(proposalContract.makeProposal(proposal), _proposalHash(proposal)); } diff --git a/test/unit/PWNSimpleLoanListOffer.t.sol b/test/unit/PWNSimpleLoanListOffer.t.sol deleted file mode 100644 index 3c3a674..0000000 --- a/test/unit/PWNSimpleLoanListOffer.t.sol +++ /dev/null @@ -1,973 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity 0.8.16; - -import "forge-std/Test.sol"; - -import { MultiToken } from "MultiToken/MultiToken.sol"; - -import { PWNHubTags } from "@pwn/hub/PWNHubTags.sol"; -import { PWNSimpleLoanListOffer, PWNSimpleLoan, Permit } - from "@pwn/loan/terms/simple/proposal/offer/PWNSimpleLoanListOffer.sol"; -import "@pwn/PWNErrors.sol"; - - -abstract contract PWNSimpleLoanListOfferTest is Test { - - bytes32 internal constant PROPOSALS_MADE_SLOT = bytes32(uint256(0)); // `proposalsMade` mapping position - bytes32 internal constant CREDIT_USED_SLOT = bytes32(uint256(1)); // `creditUsed` mapping position - - PWNSimpleLoanListOffer offerContract; - address hub = makeAddr("hub"); - address revokedNonce = makeAddr("revokedNonce"); - address stateFingerprintComputerRegistry = makeAddr("stateFingerprintComputerRegistry"); - address activeLoanContract = makeAddr("activeLoanContract"); - PWNSimpleLoanListOffer.Offer offer; - PWNSimpleLoanListOffer.OfferValues offerValues; - address token = makeAddr("token"); - uint256 lenderPK = 73661723; - address lender = vm.addr(lenderPK); - address borrower = makeAddr("borrower"); - address stateFingerprintComputer = makeAddr("stateFingerprintComputer"); - uint256 loanId = 421; - uint256 refinancedLoanId = 123; - Permit permit; - - event OfferMade(bytes32 indexed proposalHash, address indexed proposer, PWNSimpleLoanListOffer.Offer offer); - - function setUp() virtual public { - vm.etch(hub, bytes("data")); - vm.etch(revokedNonce, bytes("data")); - vm.etch(token, bytes("data")); - - offerContract = new PWNSimpleLoanListOffer(hub, revokedNonce, stateFingerprintComputerRegistry); - - offer = PWNSimpleLoanListOffer.Offer({ - collateralCategory: MultiToken.Category.ERC721, - collateralAddress: token, - collateralIdsWhitelistMerkleRoot: bytes32(0), - collateralAmount: 1032, - checkCollateralStateFingerprint: true, - collateralStateFingerprint: keccak256("some state fingerprint"), - creditAddress: token, - creditAmount: 1101001, - availableCreditLimit: 0, - fixedInterestAmount: 1, - accruingInterestAPR: 0, - duration: 1000, - expiration: 60303, - allowedBorrower: address(0), - lender: lender, - refinancingLoanId: 0, - nonceSpace: 1, - nonce: uint256(keccak256("nonce_1")), - loanContract: activeLoanContract - }); - - offerValues = PWNSimpleLoanListOffer.OfferValues({ - collateralId: 32, - merkleInclusionProof: new bytes32[](0) - }); - - vm.mockCall( - revokedNonce, - abi.encodeWithSignature("isNonceUsable(address,uint256,uint256)"), - abi.encode(true) - ); - - vm.mockCall(address(hub), abi.encodeWithSignature("hasTag(address,bytes32)"), abi.encode(false)); - vm.mockCall( - address(hub), - abi.encodeWithSignature("hasTag(address,bytes32)", activeLoanContract, PWNHubTags.ACTIVE_LOAN), - abi.encode(true) - ); - - vm.mockCall( - stateFingerprintComputerRegistry, - abi.encodeWithSignature("getStateFingerprintComputer(address)", offer.collateralAddress), - abi.encode(stateFingerprintComputer) - ); - vm.mockCall( - stateFingerprintComputer, - abi.encodeWithSignature("getStateFingerprint(uint256)"), - abi.encode(offer.collateralStateFingerprint) - ); - - vm.mockCall( - activeLoanContract, abi.encodeWithSelector(PWNSimpleLoan.createLOAN.selector), abi.encode(loanId) - ); - vm.mockCall( - activeLoanContract, abi.encodeWithSelector(PWNSimpleLoan.refinanceLOAN.selector), abi.encode(refinancedLoanId) - ); - } - - - function _offerHash(PWNSimpleLoanListOffer.Offer memory _offer) internal view returns (bytes32) { - return keccak256(abi.encodePacked( - "\x19\x01", - keccak256(abi.encode( - keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), - keccak256("PWNSimpleLoanListOffer"), - keccak256("1.2"), - block.chainid, - address(offerContract) - )), - keccak256(abi.encodePacked( - keccak256("Offer(uint8 collateralCategory,address collateralAddress,bytes32 collateralIdsWhitelistMerkleRoot,uint256 collateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address creditAddress,uint256 creditAmount,uint256 availableCreditLimit,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedBorrower,address lender,uint256 refinancingLoanId,uint256 nonceSpace,uint256 nonce,address loanContract)"), - abi.encode(_offer) - )) - )); - } - - function _signOffer( - uint256 pk, PWNSimpleLoanListOffer.Offer memory _offer - ) internal view returns (bytes memory) { - (uint8 v, bytes32 r, bytes32 s) = vm.sign(pk, _offerHash(_offer)); - return abi.encodePacked(r, s, v); - } - - function _signOfferCompact( - uint256 pk, PWNSimpleLoanListOffer.Offer memory _offer - ) internal view returns (bytes memory) { - (uint8 v, bytes32 r, bytes32 s) = vm.sign(pk, _offerHash(_offer)); - return abi.encodePacked(r, bytes32(uint256(v) - 27) << 255 | s); - } - -} - - -/*----------------------------------------------------------*| -|* # CREDIT USED *| -|*----------------------------------------------------------*/ - -contract PWNSimpleLoanListOffer_CreditUsed_Test is PWNSimpleLoanListOfferTest { - - function testFuzz_shouldReturnUsedCredit(uint256 used) external { - vm.store(address(offerContract), keccak256(abi.encode(_offerHash(offer), CREDIT_USED_SLOT)), bytes32(used)); - - assertEq(offerContract.creditUsed(_offerHash(offer)), used); - } - -} - - -/*----------------------------------------------------------*| -|* # GET OFFER HASH *| -|*----------------------------------------------------------*/ - -contract PWNSimpleLoanListOffer_GetOfferHash_Test is PWNSimpleLoanListOfferTest { - - function test_shouldReturnOfferHash() external { - assertEq(_offerHash(offer), offerContract.getOfferHash(offer)); - } - -} - - -/*----------------------------------------------------------*| -|* # MAKE OFFER *| -|*----------------------------------------------------------*/ - -contract PWNSimpleLoanListOffer_MakeOffer_Test is PWNSimpleLoanListOfferTest { - - function testFuzz_shouldFail_whenCallerIsNotLender(address caller) external { - vm.assume(caller != offer.lender); - - vm.expectRevert(abi.encodeWithSelector(CallerIsNotStatedProposer.selector, lender)); - vm.prank(caller); - offerContract.makeOffer(offer); - } - - function test_shouldEmit_OfferMade() external { - vm.expectEmit(); - emit OfferMade(_offerHash(offer), offer.lender, offer); - - vm.prank(offer.lender); - offerContract.makeOffer(offer); - } - - function test_shouldMakeOffer() external { - vm.prank(offer.lender); - offerContract.makeOffer(offer); - - assertTrue(offerContract.proposalsMade(_offerHash(offer))); - } - - function test_shouldReturnOfferHash() external { - vm.prank(offer.lender); - assertEq(offerContract.makeOffer(offer), _offerHash(offer)); - } - -} - - -/*----------------------------------------------------------*| -|* # REVOKE NONCE *| -|*----------------------------------------------------------*/ - -contract PWNSimpleLoanListOffer_RevokeNonce_Test is PWNSimpleLoanListOfferTest { - - function testFuzz_shouldCallRevokeNonce(address caller, uint256 nonceSpace, uint256 nonce) external { - vm.expectCall( - revokedNonce, - abi.encodeWithSignature("revokeNonce(address,uint256,uint256)", caller, nonceSpace, nonce) - ); - - vm.prank(caller); - offerContract.revokeNonce(nonceSpace, nonce); - } - -} - - -/*----------------------------------------------------------*| -|* # ACCEPT OFFER *| -|*----------------------------------------------------------*/ - -contract PWNSimpleLoanListOffer_AcceptOffer_Test is PWNSimpleLoanListOfferTest { - - function testFuzz_shouldFail_whenRefinancingLoanIdNotZero(uint256 refinancingLoanId) external { - vm.assume(refinancingLoanId != 0); - offer.refinancingLoanId = refinancingLoanId; - - vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, refinancingLoanId)); - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), permit, ""); - } - - function testFuzz_shouldFail_whenLoanContractNotTagged_ACTIVE_LOAN(address loanContract) external { - vm.assume(loanContract != activeLoanContract); - offer.loanContract = loanContract; - - vm.expectRevert(abi.encodeWithSelector(AddressMissingHubTag.selector, loanContract, PWNHubTags.ACTIVE_LOAN)); - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), permit, ""); - } - - function test_shouldAcceptAnyCollateralId_whenMerkleRootIsZero() external { - offerValues.collateralId = 331; - offer.collateralIdsWhitelistMerkleRoot = bytes32(0); - - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), permit, ""); - } - - function test_shouldPass_whenGivenCollateralIdIsWhitelisted() external { - bytes32 id1Hash = keccak256(abi.encodePacked(uint256(331))); - bytes32 id2Hash = keccak256(abi.encodePacked(uint256(133))); - offer.collateralIdsWhitelistMerkleRoot = keccak256(abi.encodePacked(id1Hash, id2Hash)); - - offerValues.collateralId = 331; - offerValues.merkleInclusionProof = new bytes32[](1); - offerValues.merkleInclusionProof[0] = id2Hash; - - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), permit, ""); - } - - function test_shouldFail_whenGivenCollateralIdIsNotWhitelisted() external { - bytes32 id1Hash = keccak256(abi.encodePacked(uint256(331))); - bytes32 id2Hash = keccak256(abi.encodePacked(uint256(133))); - offer.collateralIdsWhitelistMerkleRoot = keccak256(abi.encodePacked(id1Hash, id2Hash)); - - offerValues.collateralId = 333; - offerValues.merkleInclusionProof = new bytes32[](1); - offerValues.merkleInclusionProof[0] = id2Hash; - - vm.expectRevert(abi.encodeWithSelector(CollateralIdNotWhitelisted.selector, offerValues.collateralId)); - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), permit, ""); - } - - function test_shouldNotCallComputerRegistry_whenShouldNotCheckStateFingerprint() external { - offer.checkCollateralStateFingerprint = false; - - vm.expectCall({ - callee: stateFingerprintComputerRegistry, - data: abi.encodeWithSignature("getStateFingerprintComputer(address)", offer.collateralAddress), - count: 0 - }); - - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), permit, ""); - } - - function test_shouldFail_whenComputerRegistryReturnsZeroAddress_whenShouldCheckStateFingerprint() external { - vm.mockCall( - stateFingerprintComputerRegistry, - abi.encodeWithSignature("getStateFingerprintComputer(address)", offer.collateralAddress), - abi.encode(address(0)) - ); - - vm.expectCall( - stateFingerprintComputerRegistry, - abi.encodeWithSignature("getStateFingerprintComputer(address)", offer.collateralAddress) - ); - - vm.expectRevert(abi.encodeWithSelector(MissingStateFingerprintComputer.selector)); - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), permit, ""); - } - - function testFuzz_shouldFail_whenComputerReturnsDifferentStateFingerprint_whenShouldCheckStateFingerprint( - bytes32 stateFingerprint - ) external { - vm.assume(stateFingerprint != offer.collateralStateFingerprint); - - vm.mockCall( - stateFingerprintComputer, - abi.encodeWithSignature("getStateFingerprint(uint256)", offerValues.collateralId), - abi.encode(stateFingerprint) - ); - - vm.expectCall( - stateFingerprintComputer, - abi.encodeWithSignature("getStateFingerprint(uint256)", offerValues.collateralId) - ); - - vm.expectRevert(abi.encodeWithSelector( - InvalidCollateralStateFingerprint.selector, stateFingerprint, offer.collateralStateFingerprint - )); - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), permit, ""); - } - - function test_shouldFail_whenInvalidSignature_whenEOA() external { - vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, offer.lender, _offerHash(offer))); - offerContract.acceptOffer(offer, offerValues, _signOffer(1, offer), permit, ""); - } - - function test_shouldFail_whenInvalidSignature_whenContractAccount() external { - vm.etch(lender, bytes("data")); - - vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, offer.lender, _offerHash(offer))); - offerContract.acceptOffer(offer, offerValues, "", permit, ""); - } - - function test_shouldPass_whenOfferHasBeenMadeOnchain() external { - vm.store( - address(offerContract), - keccak256(abi.encode(_offerHash(offer), PROPOSALS_MADE_SLOT)), - bytes32(uint256(1)) - ); - - offerContract.acceptOffer(offer, offerValues, "", permit, ""); - } - - function test_shouldPass_withValidSignature_whenEOA_whenStandardSignature() external { - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), permit, ""); - } - - function test_shouldPass_withValidSignature_whenEOA_whenCompactEIP2098Signature() external { - offerContract.acceptOffer(offer, offerValues, _signOfferCompact(lenderPK, offer), permit, ""); - } - - function test_shouldPass_whenValidSignature_whenContractAccount() external { - vm.etch(lender, bytes("data")); - - vm.mockCall( - lender, - abi.encodeWithSignature("isValidSignature(bytes32,bytes)"), - abi.encode(bytes4(0x1626ba7e)) - ); - - offerContract.acceptOffer(offer, offerValues, "", permit, ""); - } - - function testFuzz_shouldFail_whenOfferIsExpired(uint256 timestamp) external { - timestamp = bound(timestamp, offer.expiration, type(uint256).max); - vm.warp(timestamp); - - vm.expectRevert(abi.encodeWithSelector(Expired.selector, timestamp, offer.expiration)); - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), permit, ""); - } - - function test_shouldFail_whenOfferNonceNotUsable() external { - vm.mockCall( - revokedNonce, - abi.encodeWithSignature("isNonceUsable(address,uint256,uint256)"), - abi.encode(false) - ); - vm.expectCall( - revokedNonce, - abi.encodeWithSignature("isNonceUsable(address,uint256,uint256)", offer.lender, offer.nonceSpace, offer.nonce) - ); - - vm.expectRevert(abi.encodeWithSelector( - NonceNotUsable.selector, offer.lender, offer.nonceSpace, offer.nonce - )); - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), permit, ""); - } - - function testFuzz_shouldFail_whenCallerIsNotAllowedBorrower(address caller) external { - address allowedBorrower = makeAddr("allowedBorrower"); - vm.assume(caller != allowedBorrower); - offer.allowedBorrower = allowedBorrower; - - vm.expectRevert(abi.encodeWithSelector(CallerNotAllowedAcceptor.selector, caller, offer.allowedBorrower)); - vm.prank(caller); - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), permit, ""); - } - - function testFuzz_shouldFail_whenLessThanMinDuration(uint256 duration) external { - vm.assume(duration < offerContract.MIN_LOAN_DURATION()); - duration = bound(duration, 0, offerContract.MIN_LOAN_DURATION() - 1); - offer.duration = uint32(duration); - - vm.expectRevert(abi.encodeWithSelector(InvalidDuration.selector, duration, offerContract.MIN_LOAN_DURATION())); - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), permit, ""); - } - - function testFuzz_shouldFail_whenAccruingInterestAPROutOfBounds(uint256 interestAPR) external { - uint256 maxInterest = offerContract.MAX_ACCRUING_INTEREST_APR(); - interestAPR = bound(interestAPR, maxInterest + 1, type(uint40).max); - offer.accruingInterestAPR = uint40(interestAPR); - - vm.expectRevert(abi.encodeWithSelector(AccruingInterestAPROutOfBounds.selector, interestAPR, maxInterest)); - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), permit, ""); - } - - function test_shouldRevokeOffer_whenAvailableCreditLimitEqualToZero() external { - offer.availableCreditLimit = 0; - - vm.expectCall( - revokedNonce, - abi.encodeWithSignature( - "revokeNonce(address,uint256,uint256)", offer.lender, offer.nonceSpace, offer.nonce - ) - ); - - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), permit, ""); - } - - function testFuzz_shouldFail_whenUsedCreditExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { - used = bound(used, 1, type(uint256).max - offer.creditAmount); - limit = bound(limit, used, used + offer.creditAmount - 1); - offer.availableCreditLimit = limit; - - vm.store(address(offerContract), keccak256(abi.encode(_offerHash(offer), CREDIT_USED_SLOT)), bytes32(used)); - - vm.expectRevert(abi.encodeWithSelector(AvailableCreditLimitExceeded.selector, used + offer.creditAmount, limit)); - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), permit, ""); - } - - function testFuzz_shouldIncreaseUsedCredit_whenUsedCreditNotExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { - used = bound(used, 1, type(uint256).max - offer.creditAmount); - limit = bound(limit, used + offer.creditAmount, type(uint256).max); - offer.availableCreditLimit = limit; - - vm.store(address(offerContract), keccak256(abi.encode(_offerHash(offer), CREDIT_USED_SLOT)), bytes32(used)); - - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), permit, ""); - - assertEq(offerContract.creditUsed(_offerHash(offer)), used + offer.creditAmount); - } - - function testFuzz_shouldFail_whenPermitOwnerNotCaller(address owner) external { - vm.assume(owner != borrower); - - permit.owner = owner; - permit.asset = offer.creditAddress; - - vm.expectRevert(abi.encodeWithSelector(InvalidPermitOwner.selector, owner, borrower)); - vm.prank(borrower); - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), permit, ""); - } - - function testFuzz_shouldFail_whenPermitAssetNotCreditAsset(address asset) external { - vm.assume(asset != offer.creditAddress && asset != address(0)); - - permit.owner = borrower; - permit.asset = asset; - - vm.expectRevert(abi.encodeWithSelector(InvalidPermitAsset.selector, asset, token)); - vm.prank(borrower); - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), permit, ""); - } - - function test_shouldCallLoanContractWithLoanTerms() external { - permit = Permit({ - asset: token, - owner: borrower, - amount: 100, - deadline: 1000, - v: 27, - r: bytes32(uint256(1)), - s: bytes32(uint256(2)) - }); - bytes memory extra = "lil extra"; - - PWNSimpleLoan.Terms memory loanTerms = PWNSimpleLoan.Terms({ - lender: offer.lender, - borrower: borrower, - duration: offer.duration, - collateral: MultiToken.Asset({ - category: offer.collateralCategory, - assetAddress: offer.collateralAddress, - id: offerValues.collateralId, - amount: offer.collateralAmount - }), - credit: MultiToken.Asset({ - category: MultiToken.Category.ERC20, - assetAddress: offer.creditAddress, - id: 0, - amount: offer.creditAmount - }), - fixedInterestAmount: offer.fixedInterestAmount, - accruingInterestAPR: offer.accruingInterestAPR - }); - - vm.expectCall( - activeLoanContract, - abi.encodeWithSelector( - PWNSimpleLoan.createLOAN.selector, - _offerHash(offer), loanTerms, permit, extra - ) - ); - - vm.prank(borrower); - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), permit, extra); - } - - function test_shouldReturnNewLoanId() external { - assertEq( - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), permit, ""), - loanId - ); - } - -} - - -/*----------------------------------------------------------*| -|* # ACCEPT OFFER AND REVOKE CALLERS NONCE *| -|*----------------------------------------------------------*/ - -contract PWNSimpleLoanListOffer_AcceptOfferAndRevokeCallersNonce_Test is PWNSimpleLoanListOfferTest { - - 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); - offerContract.acceptOffer({ - offer: offer, - offerValues: offerValues, - signature: _signOffer(lenderPK, offer), - permit: permit, - extra: "", - callersNonceSpace: nonceSpace, - callersNonceToRevoke: 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); - offerContract.acceptOffer({ - offer: offer, - offerValues: offerValues, - signature: _signOffer(lenderPK, offer), - permit: permit, - extra: "", - callersNonceSpace: nonceSpace, - callersNonceToRevoke: nonce - }); - } - - // function is calling `acceptOffer`, no need to test it again - function test_shouldCallLoanContract() external { - uint256 newLoanId = offerContract.acceptOffer({ - offer: offer, - offerValues: offerValues, - signature: _signOffer(lenderPK, offer), - permit: permit, - extra: "", - callersNonceSpace: 1, - callersNonceToRevoke: 2 - }); - - assertEq(newLoanId, loanId); - } - -} - - -/*----------------------------------------------------------*| -|* # ACCEPT REFINANCE OFFER *| -|*----------------------------------------------------------*/ - -contract PWNSimpleLoanListOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanListOfferTest { - - function testFuzz_shouldFail_whenRefinancingLoanIdIsNotEqualToLoanId_whenRefinanceingLoanIdNotZero( - uint256 _loanId, uint256 _refinancingLoanId - ) external { - vm.assume(_refinancingLoanId != 0); - vm.assume(_loanId != _refinancingLoanId); - offer.refinancingLoanId = _refinancingLoanId; - - vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, offer.refinancingLoanId)); - offerContract.acceptRefinanceOffer(_loanId, offer, offerValues, _signOffer(lenderPK, offer), permit, ""); - } - - function testFuzz_shouldFail_whenLoanContractNotTagged_ACTIVE_LOAN(address loanContract) external { - vm.assume(loanContract != activeLoanContract); - offer.loanContract = loanContract; - - vm.expectRevert(abi.encodeWithSelector(AddressMissingHubTag.selector, loanContract, PWNHubTags.ACTIVE_LOAN)); - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), permit, ""); - } - - function test_shouldAcceptAnyCollateralId_whenMerkleRootIsZero() external { - offerValues.collateralId = 331; - offer.collateralIdsWhitelistMerkleRoot = bytes32(0); - - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), permit, ""); - } - - function test_shouldPass_whenGivenCollateralIdIsWhitelisted() external { - bytes32 id1Hash = keccak256(abi.encodePacked(uint256(331))); - bytes32 id2Hash = keccak256(abi.encodePacked(uint256(133))); - offer.collateralIdsWhitelistMerkleRoot = keccak256(abi.encodePacked(id1Hash, id2Hash)); - - offerValues.collateralId = 331; - offerValues.merkleInclusionProof = new bytes32[](1); - offerValues.merkleInclusionProof[0] = id2Hash; - - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), permit, ""); - } - - function test_shouldFail_whenGivenCollateralIdIsNotWhitelisted() external { - bytes32 id1Hash = keccak256(abi.encodePacked(uint256(331))); - bytes32 id2Hash = keccak256(abi.encodePacked(uint256(133))); - offer.collateralIdsWhitelistMerkleRoot = keccak256(abi.encodePacked(id1Hash, id2Hash)); - - offerValues.collateralId = 333; - offerValues.merkleInclusionProof = new bytes32[](1); - offerValues.merkleInclusionProof[0] = id2Hash; - - vm.expectRevert(abi.encodeWithSelector(CollateralIdNotWhitelisted.selector, offerValues.collateralId)); - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), permit, ""); - } - - function test_shouldNotCallComputerRegistry_whenShouldNotCheckStateFingerprint() external { - offer.checkCollateralStateFingerprint = false; - - vm.expectCall({ - callee: stateFingerprintComputerRegistry, - data: abi.encodeWithSignature("getStateFingerprintComputer(address)", offer.collateralAddress), - count: 0 - }); - - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), permit, ""); - } - - function test_shouldFail_whenComputerRegistryReturnsZeroAddress_whenShouldCheckStateFingerprint() external { - vm.mockCall( - stateFingerprintComputerRegistry, - abi.encodeWithSignature("getStateFingerprintComputer(address)", offer.collateralAddress), - abi.encode(address(0)) - ); - - vm.expectCall( - stateFingerprintComputerRegistry, - abi.encodeWithSignature("getStateFingerprintComputer(address)", offer.collateralAddress) - ); - - vm.expectRevert(abi.encodeWithSelector(MissingStateFingerprintComputer.selector)); - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), permit, ""); - } - - function testFuzz_shouldFail_whenComputerReturnsDifferentStateFingerprint_whenShouldCheckStateFingerprint( - bytes32 stateFingerprint - ) external { - vm.assume(stateFingerprint != offer.collateralStateFingerprint); - - vm.mockCall( - stateFingerprintComputer, - abi.encodeWithSignature("getStateFingerprint(uint256)", offerValues.collateralId), - abi.encode(stateFingerprint) - ); - - vm.expectCall( - stateFingerprintComputer, - abi.encodeWithSignature("getStateFingerprint(uint256)", offerValues.collateralId) - ); - - vm.expectRevert(abi.encodeWithSelector( - InvalidCollateralStateFingerprint.selector, stateFingerprint, offer.collateralStateFingerprint - )); - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), permit, ""); - } - - function test_shouldFail_whenInvalidSignature_whenEOA() external { - vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, offer.lender, _offerHash(offer))); - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(1, offer), permit, ""); - } - - function test_shouldFail_whenInvalidSignature_whenContractAccount() external { - vm.etch(lender, bytes("data")); - - vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, offer.lender, _offerHash(offer))); - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, "", permit, ""); - } - - function test_shouldPass_whenOfferHasBeenMadeOnchain() external { - vm.store( - address(offerContract), - keccak256(abi.encode(_offerHash(offer), PROPOSALS_MADE_SLOT)), - bytes32(uint256(1)) - ); - - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, "", permit, ""); - } - - function test_shouldPass_withValidSignature_whenEOA_whenStandardSignature() external { - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), permit, ""); - } - - function test_shouldPass_withValidSignature_whenEOA_whenCompactEIP2098Signature() external { - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOfferCompact(lenderPK, offer), permit, ""); - } - - function test_shouldPass_whenValidSignature_whenContractAccount() external { - vm.etch(lender, bytes("data")); - - vm.mockCall( - lender, - abi.encodeWithSignature("isValidSignature(bytes32,bytes)"), - abi.encode(bytes4(0x1626ba7e)) - ); - - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, "", permit, ""); - } - - function testFuzz_shouldFail_whenOfferIsExpired(uint256 timestamp) external { - timestamp = bound(timestamp, offer.expiration, type(uint256).max); - vm.warp(timestamp); - - vm.expectRevert(abi.encodeWithSelector(Expired.selector, timestamp, offer.expiration)); - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), permit, ""); - } - - function test_shouldFail_whenOfferNonceNotUsable() external { - vm.mockCall( - revokedNonce, - abi.encodeWithSignature("isNonceUsable(address,uint256,uint256)"), - abi.encode(false) - ); - vm.expectCall( - revokedNonce, - abi.encodeWithSignature("isNonceUsable(address,uint256,uint256)", offer.lender, offer.nonceSpace, offer.nonce) - ); - - vm.expectRevert(abi.encodeWithSelector( - NonceNotUsable.selector, offer.lender, offer.nonceSpace, offer.nonce - )); - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), permit, ""); - } - - function testFuzz_shouldFail_whenCallerIsNotAllowedBorrower(address caller) external { - address allowedBorrower = makeAddr("allowedBorrower"); - vm.assume(caller != allowedBorrower); - offer.allowedBorrower = allowedBorrower; - - vm.expectRevert(abi.encodeWithSelector(CallerNotAllowedAcceptor.selector, caller, offer.allowedBorrower)); - vm.prank(caller); - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), permit, ""); - } - - function testFuzz_shouldFail_whenLessThanMinDuration(uint256 duration) external { - vm.assume(duration < offerContract.MIN_LOAN_DURATION()); - duration = bound(duration, 0, offerContract.MIN_LOAN_DURATION() - 1); - offer.duration = uint32(duration); - - vm.expectRevert(abi.encodeWithSelector(InvalidDuration.selector, duration, offerContract.MIN_LOAN_DURATION())); - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), permit, ""); - } - - function testFuzz_shouldFail_whenAccruingInterestAPROutOfBounds(uint256 interestAPR) external { - uint256 maxInterest = offerContract.MAX_ACCRUING_INTEREST_APR(); - interestAPR = bound(interestAPR, maxInterest + 1, type(uint40).max); - offer.accruingInterestAPR = uint40(interestAPR); - - vm.expectRevert(abi.encodeWithSelector(AccruingInterestAPROutOfBounds.selector, interestAPR, maxInterest)); - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), permit, ""); - } - - function test_shouldRevokeOffer_whenAvailableCreditLimitEqualToZero() external { - offer.availableCreditLimit = 0; - - vm.expectCall( - revokedNonce, - abi.encodeWithSignature( - "revokeNonce(address,uint256,uint256)", offer.lender, offer.nonceSpace, offer.nonce - ) - ); - - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), permit, ""); - } - - function testFuzz_shouldFail_whenUsedCreditExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { - used = bound(used, 1, type(uint256).max - offer.creditAmount); - limit = bound(limit, used, used + offer.creditAmount - 1); - offer.availableCreditLimit = limit; - - vm.store(address(offerContract), keccak256(abi.encode(_offerHash(offer), CREDIT_USED_SLOT)), bytes32(used)); - - vm.expectRevert(abi.encodeWithSelector(AvailableCreditLimitExceeded.selector, used + offer.creditAmount, limit)); - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), permit, ""); - } - - function testFuzz_shouldIncreaseUsedCredit_whenUsedCreditNotExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { - used = bound(used, 1, type(uint256).max - offer.creditAmount); - limit = bound(limit, used + offer.creditAmount, type(uint256).max); - offer.availableCreditLimit = limit; - - vm.store(address(offerContract), keccak256(abi.encode(_offerHash(offer), CREDIT_USED_SLOT)), bytes32(used)); - - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), permit, ""); - - assertEq(offerContract.creditUsed(_offerHash(offer)), used + offer.creditAmount); - } - - function testFuzz_shouldFail_whenPermitOwnerNotCaller(address owner) external { - vm.assume(owner != borrower); - - permit.owner = owner; - permit.asset = offer.creditAddress; - - vm.expectRevert(abi.encodeWithSelector(InvalidPermitOwner.selector, owner, borrower)); - vm.prank(borrower); - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), permit, ""); - } - - function testFuzz_shouldFail_whenPermitAssetNotCreditAsset(address asset) external { - vm.assume(asset != offer.creditAddress && asset != address(0)); - - permit.owner = borrower; - permit.asset = asset; - - vm.expectRevert(abi.encodeWithSelector(InvalidPermitAsset.selector, asset, token)); - vm.prank(borrower); - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), permit, ""); - } - - function test_shouldCallLoanContract() external { - permit = Permit({ - asset: token, - owner: borrower, - amount: 100, - deadline: 1000, - v: 27, - r: bytes32(uint256(1)), - s: bytes32(uint256(2)) - }); - bytes memory extra = "lil extra"; - - PWNSimpleLoan.Terms memory loanTerms = PWNSimpleLoan.Terms({ - lender: offer.lender, - borrower: borrower, - duration: offer.duration, - collateral: MultiToken.Asset({ - category: offer.collateralCategory, - assetAddress: offer.collateralAddress, - id: offerValues.collateralId, - amount: offer.collateralAmount - }), - credit: MultiToken.Asset({ - category: MultiToken.Category.ERC20, - assetAddress: offer.creditAddress, - id: 0, - amount: offer.creditAmount - }), - fixedInterestAmount: offer.fixedInterestAmount, - accruingInterestAPR: offer.accruingInterestAPR - }); - - vm.expectCall( - activeLoanContract, - abi.encodeWithSelector( - PWNSimpleLoan.refinanceLOAN.selector, - loanId, _offerHash(offer), loanTerms, permit, extra - ) - ); - - vm.prank(borrower); - offerContract.acceptRefinanceOffer( - loanId, offer, offerValues, _signOffer(lenderPK, offer), permit, extra - ); - } - - function test_shouldReturnRefinancedLoanId() external { - assertEq( - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), permit, ""), - refinancedLoanId - ); - } - -} - - -/*----------------------------------------------------------*| -|* # ACCEPT REFINANCE OFFER AND REVOKE CALLERS NONCE *| -|*----------------------------------------------------------*/ - -contract PWNSimpleLoanListOffer_AcceptRefinanceOfferAndRevokeCallersNonce_Test is PWNSimpleLoanListOfferTest { - - 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); - offerContract.acceptRefinanceOffer({ - loanId: loanId, - offer: offer, - offerValues: offerValues, - signature: _signOffer(lenderPK, offer), - permit: permit, - extra: "", - callersNonceSpace: nonceSpace, - callersNonceToRevoke: 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); - offerContract.acceptRefinanceOffer({ - loanId: loanId, - offer: offer, - offerValues: offerValues, - signature: _signOffer(lenderPK, offer), - permit: permit, - extra: "", - callersNonceSpace: nonceSpace, - callersNonceToRevoke: nonce - }); - } - - // function is calling `acceptRefinanceOffer`, no need to test it again - function test_shouldCallLoanContract() external { - uint256 newLoanId = offerContract.acceptRefinanceOffer({ - loanId: loanId, - offer: offer, - offerValues: offerValues, - signature: _signOffer(lenderPK, offer), - permit: permit, - extra: "", - callersNonceSpace: 1, - callersNonceToRevoke: 2 - }); - - assertEq(newLoanId, refinancedLoanId); - } - -} diff --git a/test/unit/PWNSimpleLoanListProposal.t.sol b/test/unit/PWNSimpleLoanListProposal.t.sol new file mode 100644 index 0000000..136a65e --- /dev/null +++ b/test/unit/PWNSimpleLoanListProposal.t.sol @@ -0,0 +1,497 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.16; + +import "forge-std/Test.sol"; + +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 } + from "@pwn/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol"; +import "@pwn/PWNErrors.sol"; + +import { + PWNSimpleLoanProposalTest, + PWNSimpleLoanProposal_AcceptProposal_Test, + PWNSimpleLoanProposal_AcceptProposalAndRevokeCallersNonce_Test, + PWNSimpleLoanProposal_AcceptRefinanceProposal_Test, + PWNSimpleLoanProposal_AcceptRefinanceProposalAndRevokeCallersNonce_Test +} from "@pwn-test/unit/PWNSimpleLoanProposal.t.sol"; + + +abstract contract PWNSimpleLoanListProposalTest is PWNSimpleLoanProposalTest { + + PWNSimpleLoanListProposal proposalContract; + PWNSimpleLoanListProposal.Proposal proposal; + PWNSimpleLoanListProposal.ProposalValues proposalValues; + + event ProposalMade(bytes32 indexed proposalHash, address indexed proposer, PWNSimpleLoanListProposal.Proposal proposal); + + function setUp() virtual public override { + super.setUp(); + + proposalContract = new PWNSimpleLoanListProposal(hub, revokedNonce, stateFingerprintComputerRegistry); + proposalContractAddr = PWNSimpleLoanProposal(proposalContract); + + proposal = PWNSimpleLoanListProposal.Proposal({ + collateralCategory: MultiToken.Category.ERC721, + collateralAddress: token, + collateralIdsWhitelistMerkleRoot: bytes32(0), + collateralAmount: 1032, + checkCollateralStateFingerprint: true, + collateralStateFingerprint: keccak256("some state fingerprint"), + creditAddress: token, + creditAmount: 1101001, + availableCreditLimit: 0, + fixedInterestAmount: 1, + accruingInterestAPR: 0, + duration: 1000, + expiration: 60303, + allowedAcceptor: address(0), + proposer: proposer, + isOffer: true, + refinancingLoanId: 0, + nonceSpace: 1, + nonce: uint256(keccak256("nonce_1")), + loanContract: activeLoanContract + }); + + proposalValues = PWNSimpleLoanListProposal.ProposalValues({ + collateralId: 32, + merkleInclusionProof: new bytes32[](0) + }); + } + + + function _proposalHash(PWNSimpleLoanListProposal.Proposal memory _proposal) internal view returns (bytes32) { + return keccak256(abi.encodePacked( + "\x19\x01", + keccak256(abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256("PWNSimpleLoanListProposal"), + keccak256("1.2"), + block.chainid, + proposalContractAddr + )), + keccak256(abi.encodePacked( + keccak256("Proposal(uint8 collateralCategory,address collateralAddress,bytes32 collateralIdsWhitelistMerkleRoot,uint256 collateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address creditAddress,uint256 creditAmount,uint256 availableCreditLimit,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedAcceptor,address proposer,bool isOffer,uint256 refinancingLoanId,uint256 nonceSpace,uint256 nonce,address loanContract)"), + abi.encode(_proposal) + )) + )); + } + + 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; + } + + function _proposalSignature(Params memory _params) internal view returns (bytes memory signature) { + if (_params.signerPK != 0) { + if (_params.compactSignature) { + signature = _signProposalHashCompact(_params.signerPK, _proposalHash(proposal)); + } else { + signature = _signProposalHash(_params.signerPK, _proposalHash(proposal)); + } + } + } + + + function _callAcceptProposalWith(Params memory _params, Permit memory _permit) internal override returns (uint256) { + _updateProposal(_params); + return proposalContract.acceptProposal(proposal, proposalValues, _proposalSignature(params), _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), _permit, "", nonceSpace, nonce); + } + + function _callAcceptRefinanceProposalWith(uint256 loanId, Params memory _params, Permit memory _permit) internal override returns (uint256) { + _updateProposal(_params); + return proposalContract.acceptRefinanceProposal(loanId, proposal, proposalValues, _proposalSignature(params), _permit, ""); + } + + function _callAcceptRefinanceProposalWith(uint256 loanId, Params memory _params, Permit memory _permit, uint256 nonceSpace, uint256 nonce) internal override returns (uint256) { + _updateProposal(_params); + return proposalContract.acceptRefinanceProposal(loanId, proposal, proposalValues, _proposalSignature(params), _permit, "", nonceSpace, nonce); + } + + function _getProposalHashWith(Params memory _params) internal override returns (bytes32) { + _updateProposal(_params); + return _proposalHash(proposal); + } + +} + + +/*----------------------------------------------------------*| +|* # CREDIT USED *| +|*----------------------------------------------------------*/ + +contract PWNSimpleLoanListProposal_CreditUsed_Test is PWNSimpleLoanListProposalTest { + + function testFuzz_shouldReturnUsedCredit(uint256 used) external { + vm.store(address(proposalContract), keccak256(abi.encode(_proposalHash(proposal), CREDIT_USED_SLOT)), bytes32(used)); + + assertEq(proposalContract.creditUsed(_proposalHash(proposal)), used); + } + +} + + +/*----------------------------------------------------------*| +|* # REVOKE NONCE *| +|*----------------------------------------------------------*/ + +contract PWNSimpleLoanListProposal_RevokeNonce_Test is PWNSimpleLoanListProposalTest { + + function testFuzz_shouldCallRevokeNonce(address caller, uint256 nonceSpace, uint256 nonce) external { + vm.expectCall( + revokedNonce, + abi.encodeWithSignature("revokeNonce(address,uint256,uint256)", caller, nonceSpace, nonce) + ); + + vm.prank(caller); + proposalContract.revokeNonce(nonceSpace, nonce); + } + +} + + +/*----------------------------------------------------------*| +|* # GET PROPOSAL HASH *| +|*----------------------------------------------------------*/ + +contract PWNSimpleLoanListProposal_GetProposalHash_Test is PWNSimpleLoanListProposalTest { + + function test_shouldReturnProposalHash() external { + assertEq(_proposalHash(proposal), proposalContract.getProposalHash(proposal)); + } + +} + + +/*----------------------------------------------------------*| +|* # MAKE PROPOSAL *| +|*----------------------------------------------------------*/ + +contract PWNSimpleLoanListProposal_MakeProposal_Test is PWNSimpleLoanListProposalTest { + + function testFuzz_shouldFail_whenCallerIsNotProposer(address caller) external { + vm.assume(caller != proposal.proposer); + + vm.expectRevert(abi.encodeWithSelector(CallerIsNotStatedProposer.selector, proposal.proposer)); + vm.prank(caller); + proposalContract.makeProposal(proposal); + } + + function test_shouldEmit_ProposalMade() external { + vm.expectEmit(); + emit ProposalMade(_proposalHash(proposal), proposal.proposer, proposal); + + vm.prank(proposal.proposer); + proposalContract.makeProposal(proposal); + } + + function test_shouldMakeProposal() external { + vm.prank(proposal.proposer); + proposalContract.makeProposal(proposal); + + assertTrue(proposalContract.proposalsMade(_proposalHash(proposal))); + } + + function test_shouldReturnProposalHash() external { + vm.prank(proposal.proposer); + assertEq(proposalContract.makeProposal(proposal), _proposalHash(proposal)); + } + +} + + +/*----------------------------------------------------------*| +|* # ACCEPT PROPOSAL *| +|*----------------------------------------------------------*/ + +contract PWNSimpleLoanListProposal_AcceptProposal_Test is PWNSimpleLoanListProposalTest, PWNSimpleLoanProposal_AcceptProposal_Test { + + function setUp() virtual public override(PWNSimpleLoanListProposalTest, PWNSimpleLoanProposalTest) { + super.setUp(); + } + + + function testFuzz_shouldFail_whenRefinancingLoanIdNotZero(uint256 refinancingLoanId) external { + vm.assume(refinancingLoanId != 0); + proposal.refinancingLoanId = refinancingLoanId; + + vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, refinancingLoanId)); + proposalContract.acceptProposal( + proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, "" + ); + } + + function test_shouldAcceptAnyCollateralId_whenMerkleRootIsZero() external { + proposalValues.collateralId = 331; + proposal.collateralIdsWhitelistMerkleRoot = bytes32(0); + + proposalContract.acceptProposal( + proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, "" + ); + } + + 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)); + + proposalValues.collateralId = 331; + proposalValues.merkleInclusionProof = new bytes32[](1); + proposalValues.merkleInclusionProof[0] = id2Hash; + + proposalContract.acceptProposal( + proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, "" + ); + } + + 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)); + + proposalValues.collateralId = 333; + 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)), permit, "" + ); + } + + function testFuzz_shouldCallLoanContractWithLoanTerms(bool isOffer) external { + proposal.isOffer = isOffer; + + 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, + abi.encodeWithSelector( + PWNSimpleLoan.createLOAN.selector, + _proposalHash(proposal), loanTerms, permit, extra + ) + ); + + vm.prank(acceptor); + proposalContract.acceptProposal( + proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, extra + ); + } + +} + + +/*----------------------------------------------------------*| +|* # ACCEPT PROPOSAL AND REVOKE CALLERS NONCE *| +|*----------------------------------------------------------*/ + +contract PWNSimpleLoanListProposal_AcceptProposalAndRevokeCallersNonce_Test is PWNSimpleLoanListProposalTest, PWNSimpleLoanProposal_AcceptProposalAndRevokeCallersNonce_Test { + + function setUp() virtual public override(PWNSimpleLoanListProposalTest, PWNSimpleLoanProposalTest) { + super.setUp(); + } + +} + + +/*----------------------------------------------------------*| +|* # ACCEPT REFINANCE PROPOSAL *| +|*----------------------------------------------------------*/ + +contract PWNSimpleLoanListProposal_AcceptRefinanceProposal_Test is PWNSimpleLoanListProposalTest, PWNSimpleLoanProposal_AcceptRefinanceProposal_Test { + + function setUp() virtual public override(PWNSimpleLoanListProposalTest, PWNSimpleLoanProposalTest) { + super.setUp(); + + proposal.refinancingLoanId = loanId; + } + + + function testFuzz_shouldFail_whenRefinancingLoanIdIsNotEqualToLoanId_whenRefinanceingLoanIdNotZero_whenOffer( + uint256 _loanId, uint256 _refinancingLoanId + ) external { + vm.assume(_refinancingLoanId != 0); + vm.assume(_loanId != _refinancingLoanId); + proposal.refinancingLoanId = _refinancingLoanId; + proposal.isOffer = true; + + vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, _refinancingLoanId)); + proposalContract.acceptRefinanceProposal( + _loanId, proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, extra + ); + } + + function testFuzz_shouldPass_whenRefinancingLoanIdIsNotEqualToLoanId_whenRefinanceingLoanIdZero_whenOffer( + uint256 _loanId + ) external { + vm.assume(_loanId != 0); + proposal.refinancingLoanId = 0; + proposal.isOffer = true; + + proposalContract.acceptRefinanceProposal( + _loanId, proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, extra + ); + } + + function testFuzz_shouldFail_whenRefinancingLoanIdIsNotEqualToLoanId_whenNotOffer( + uint256 _loanId, uint256 _refinancingLoanId + ) external { + vm.assume(_loanId != _refinancingLoanId); + proposal.refinancingLoanId = _refinancingLoanId; + proposal.isOffer = false; + + vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, _refinancingLoanId)); + proposalContract.acceptRefinanceProposal( + _loanId, proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, extra + ); + } + + function test_shouldAcceptAnyCollateralId_whenMerkleRootIsZero() external { + proposalValues.collateralId = 331; + proposal.collateralIdsWhitelistMerkleRoot = bytes32(0); + + proposalContract.acceptRefinanceProposal( + loanId, proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, extra + ); + } + + 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)); + + proposalValues.collateralId = 331; + proposalValues.merkleInclusionProof = new bytes32[](1); + proposalValues.merkleInclusionProof[0] = id2Hash; + + proposalContract.acceptRefinanceProposal( + loanId, proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, extra + ); + } + + 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)); + + proposalValues.collateralId = 333; + proposalValues.merkleInclusionProof = new bytes32[](1); + proposalValues.merkleInclusionProof[0] = id2Hash; + + vm.expectRevert(abi.encodeWithSelector(CollateralIdNotWhitelisted.selector, proposalValues.collateralId)); + proposalContract.acceptRefinanceProposal( + loanId, proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, extra + ); + } + + function testFuzz_shouldCallLoanContractWithLoanTerms(bool isOffer) external { + proposal.isOffer = isOffer; + + 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, + abi.encodeWithSelector( + PWNSimpleLoan.refinanceLOAN.selector, + loanId, _proposalHash(proposal), loanTerms, permit, extra + ) + ); + + vm.prank(acceptor); + proposalContract.acceptRefinanceProposal( + loanId, proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, extra + ); + } + +} + + +/*----------------------------------------------------------*| +|* # ACCEPT REFINANCE PROPOSAL AND REVOKE CALLERS NONCE *| +|*----------------------------------------------------------*/ + +contract PWNSimpleLoanListProposal_AcceptRefinanceProposalAndRevokeCallersNonce_Test is PWNSimpleLoanListProposalTest, PWNSimpleLoanProposal_AcceptRefinanceProposalAndRevokeCallersNonce_Test { + + function setUp() virtual public override(PWNSimpleLoanListProposalTest, PWNSimpleLoanProposalTest) { + super.setUp(); + } + +} diff --git a/test/unit/PWNSimpleLoanSimpleProposal.t.sol b/test/unit/PWNSimpleLoanSimpleProposal.t.sol index 67fbc98..8eb8c79 100644 --- a/test/unit/PWNSimpleLoanSimpleProposal.t.sol +++ b/test/unit/PWNSimpleLoanSimpleProposal.t.sol @@ -168,7 +168,7 @@ contract PWNSimpleLoanSimpleProposal_RevokeNonce_Test is PWNSimpleLoanSimpleProp contract PWNSimpleLoanSimpleProposal_GetProposalHash_Test is PWNSimpleLoanSimpleProposalTest { - function test_shouldReturnOfferHash() external { + function test_shouldReturnProposalHash() external { assertEq(_proposalHash(proposal), proposalContract.getProposalHash(proposal)); } @@ -189,7 +189,7 @@ contract PWNSimpleLoanSimpleProposal_MakeProposal_Test is PWNSimpleLoanSimplePro proposalContract.makeProposal(proposal); } - function test_shouldEmit_OfferMade() external { + function test_shouldEmit_ProposalMade() external { vm.expectEmit(); emit ProposalMade(_proposalHash(proposal), proposal.proposer, proposal); @@ -197,14 +197,14 @@ contract PWNSimpleLoanSimpleProposal_MakeProposal_Test is PWNSimpleLoanSimplePro proposalContract.makeProposal(proposal); } - function test_shouldMakeOffer() external { + function test_shouldMakeProposal() external { vm.prank(proposal.proposer); proposalContract.makeProposal(proposal); assertTrue(proposalContract.proposalsMade(_proposalHash(proposal))); } - function test_shouldReturnOfferHash() external { + function test_shouldReturnProposalHash() external { vm.prank(proposal.proposer); assertEq(proposalContract.makeProposal(proposal), _proposalHash(proposal)); }