From 68647524c9435da4cf37ce6d82f363bb89eb2633 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Tue, 13 Feb 2024 11:01:23 -0100 Subject: [PATCH 001/129] feat(offer): rename `borrower` address field in offer struct that is allowed to accept an offer to `allowedBorrower` --- .../simple/factory/offer/PWNSimpleLoanListOffer.sol | 12 ++++++------ .../factory/offer/PWNSimpleLoanSimpleOffer.sol | 12 ++++++------ test/integration/BaseIntegrationTest.t.sol | 2 +- .../contracts/PWNSimpleLoanIntegration.t.sol | 4 ++-- .../PWNSimpleLoanSimpleOfferIntegration.t.sol | 2 +- test/integration/use-cases/UseCases.t.sol | 2 +- test/unit/PWNSimpleLoanListOffer.t.sol | 10 +++++----- test/unit/PWNSimpleLoanSimpleOffer.t.sol | 10 +++++----- 8 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol b/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol index adef5b9..217106a 100644 --- a/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol +++ b/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol @@ -28,7 +28,7 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanOffer { * @dev EIP-712 simple offer struct type hash. */ bytes32 constant internal OFFER_TYPEHASH = keccak256( - "Offer(uint8 collateralCategory,address collateralAddress,bytes32 collateralIdsWhitelistMerkleRoot,uint256 collateralAmount,address loanAssetAddress,uint256 loanAmount,uint256 loanYield,uint32 duration,uint40 expiration,address borrower,address lender,bool isPersistent,uint256 nonce)" + "Offer(uint8 collateralCategory,address collateralAddress,bytes32 collateralIdsWhitelistMerkleRoot,uint256 collateralAmount,address loanAssetAddress,uint256 loanAmount,uint256 loanYield,uint32 duration,uint40 expiration,address allowedBorrower,address lender,bool isPersistent,uint256 nonce)" ); bytes32 immutable internal DOMAIN_SEPARATOR; @@ -44,7 +44,7 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanOffer { * @param loanYield Amount of tokens which acts as a lenders loan interest. Borrower has to pay back a borrowed amount + yield. * @param duration Loan duration in seconds. * @param expiration Offer expiration timestamp in seconds. - * @param borrower Address of a borrower. Only this address can accept an offer. If the address is zero address, anybody with a collateral can accept the offer. + * @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 isPersistent If true, offer will not be revoked on acceptance. Persistent offer can be revoked manually. * @param nonce Additional value to enable identical offers in time. Without it, it would be impossible to make again offer, which was once revoked. @@ -60,7 +60,7 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanOffer { uint256 loanYield; uint32 duration; uint40 expiration; - address borrower; + address allowedBorrower; address lender; bool isPersistent; uint256 nonce; @@ -139,9 +139,9 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanOffer { if (revokedOfferNonce.isNonceRevoked(lender, offer.nonce) == true) revert NonceAlreadyRevoked(); - if (offer.borrower != address(0)) - if (borrower != offer.borrower) - revert CallerIsNotStatedBorrower(offer.borrower); + if (offer.allowedBorrower != address(0)) + if (borrower != offer.allowedBorrower) + revert CallerIsNotStatedBorrower(offer.allowedBorrower); if (offer.duration < MIN_LOAN_DURATION) revert InvalidDuration(); diff --git a/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol b/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol index 98628c4..cbdc258 100644 --- a/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol +++ b/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol @@ -25,7 +25,7 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanOffer { * @dev EIP-712 simple offer struct type hash. */ bytes32 constant internal OFFER_TYPEHASH = keccak256( - "Offer(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,address loanAssetAddress,uint256 loanAmount,uint256 loanYield,uint32 duration,uint40 expiration,address borrower,address lender,bool isPersistent,uint256 nonce)" + "Offer(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,address loanAssetAddress,uint256 loanAmount,uint256 loanYield,uint32 duration,uint40 expiration,address allowedBorrower,address lender,bool isPersistent,uint256 nonce)" ); bytes32 immutable internal DOMAIN_SEPARATOR; @@ -41,7 +41,7 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanOffer { * @param loanYield Amount of tokens which acts as a lenders loan interest. Borrower has to pay back a borrowed amount + yield. * @param duration Loan duration in seconds. * @param expiration Offer expiration timestamp in seconds. - * @param borrower Address of a borrower. Only this address can accept an offer. If the address is zero address, anybody with a collateral can accept the offer. + * @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 isPersistent If true, offer will not be revoked on acceptance. Persistent offer can be revoked manually. * @param nonce Additional value to enable identical offers in time. Without it, it would be impossible to make again offer, which was once revoked. @@ -57,7 +57,7 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanOffer { uint256 loanYield; uint32 duration; uint40 expiration; - address borrower; + address allowedBorrower; address lender; bool isPersistent; uint256 nonce; @@ -124,9 +124,9 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanOffer { if (revokedOfferNonce.isNonceRevoked(lender, offer.nonce) == true) revert NonceAlreadyRevoked(); - if (offer.borrower != address(0)) - if (borrower != offer.borrower) - revert CallerIsNotStatedBorrower(offer.borrower); + if (offer.allowedBorrower != address(0)) + if (borrower != offer.allowedBorrower) + revert CallerIsNotStatedBorrower(offer.allowedBorrower); if (offer.duration < MIN_LOAN_DURATION) revert InvalidDuration(); diff --git a/test/integration/BaseIntegrationTest.t.sol b/test/integration/BaseIntegrationTest.t.sol index 8ad1cd3..685aa75 100644 --- a/test/integration/BaseIntegrationTest.t.sol +++ b/test/integration/BaseIntegrationTest.t.sol @@ -45,7 +45,7 @@ abstract contract BaseIntegrationTest is DeploymentTest { loanYield: 10e18, duration: 3600, expiration: 0, - borrower: borrower, + allowedBorrower: borrower, lender: lender, isPersistent: false, nonce: nonce diff --git a/test/integration/contracts/PWNSimpleLoanIntegration.t.sol b/test/integration/contracts/PWNSimpleLoanIntegration.t.sol index 79e633c..cb1fed4 100644 --- a/test/integration/contracts/PWNSimpleLoanIntegration.t.sol +++ b/test/integration/contracts/PWNSimpleLoanIntegration.t.sol @@ -23,7 +23,7 @@ contract PWNSimpleLoanIntegrationTest is BaseIntegrationTest { loanYield: 10e18, duration: 3600, expiration: 0, - borrower: borrower, + allowedBorrower: borrower, lender: lender, isPersistent: false, nonce: nonce @@ -89,7 +89,7 @@ contract PWNSimpleLoanIntegrationTest is BaseIntegrationTest { loanYield: 10e18, duration: 3600, expiration: 0, - borrower: borrower, + allowedBorrower: borrower, lender: lender, isPersistent: false, nonce: nonce diff --git a/test/integration/contracts/PWNSimpleLoanSimpleOfferIntegration.t.sol b/test/integration/contracts/PWNSimpleLoanSimpleOfferIntegration.t.sol index b6b121b..65d784e 100644 --- a/test/integration/contracts/PWNSimpleLoanSimpleOfferIntegration.t.sol +++ b/test/integration/contracts/PWNSimpleLoanSimpleOfferIntegration.t.sol @@ -28,7 +28,7 @@ contract PWNSimpleLoanSimpleOfferIntegrationTest is BaseIntegrationTest { loanYield: 10e18, duration: 3600, expiration: 0, - borrower: borrower, + allowedBorrower: borrower, lender: lender, isPersistent: false, nonce: nonce diff --git a/test/integration/use-cases/UseCases.t.sol b/test/integration/use-cases/UseCases.t.sol index 1e3b290..b1fca7c 100644 --- a/test/integration/use-cases/UseCases.t.sol +++ b/test/integration/use-cases/UseCases.t.sol @@ -52,7 +52,7 @@ abstract contract UseCasesTest is DeploymentTest { loanYield: 0, duration: 3600, expiration: 0, - borrower: address(0), + allowedBorrower: address(0), lender: lender, isPersistent: false, nonce: 0 diff --git a/test/unit/PWNSimpleLoanListOffer.t.sol b/test/unit/PWNSimpleLoanListOffer.t.sol index c5c0f89..f79bcbc 100644 --- a/test/unit/PWNSimpleLoanListOffer.t.sol +++ b/test/unit/PWNSimpleLoanListOffer.t.sol @@ -44,7 +44,7 @@ abstract contract PWNSimpleLoanListOfferTest is Test { loanYield: 1, duration: 1000, expiration: 0, - borrower: address(0), + allowedBorrower: address(0), lender: lender, isPersistent: false, nonce: uint256(keccak256("nonce_1")) @@ -74,7 +74,7 @@ abstract contract PWNSimpleLoanListOfferTest is Test { address(offerContract) )), keccak256(abi.encodePacked( - keccak256("Offer(uint8 collateralCategory,address collateralAddress,bytes32 collateralIdsWhitelistMerkleRoot,uint256 collateralAmount,address loanAssetAddress,uint256 loanAmount,uint256 loanYield,uint32 duration,uint40 expiration,address borrower,address lender,bool isPersistent,uint256 nonce)"), + keccak256("Offer(uint8 collateralCategory,address collateralAddress,bytes32 collateralIdsWhitelistMerkleRoot,uint256 collateralAmount,address loanAssetAddress,uint256 loanAmount,uint256 loanYield,uint32 duration,uint40 expiration,address allowedBorrower,address lender,bool isPersistent,uint256 nonce)"), abi.encode(_offer) )) )); @@ -256,11 +256,11 @@ contract PWNSimpleLoanListOffer_CreateLOANTerms_Test is PWNSimpleLoanListOfferTe offerContract.createLOANTerms(borrower, abi.encode(offer, offerValues), signature); } - function test_shouldFail_whenCallerIsNotBorrower_whenSetBorrower() external { - offer.borrower = address(0x50303); + function test_shouldFail_whenCallerIsNotAllowedBorrower() external { + offer.allowedBorrower = address(0x50303); signature = _signOfferCompact(lenderPK, offer); - vm.expectRevert(abi.encodeWithSelector(CallerIsNotStatedBorrower.selector, offer.borrower)); + vm.expectRevert(abi.encodeWithSelector(CallerIsNotStatedBorrower.selector, offer.allowedBorrower)); vm.prank(activeLoanContract); offerContract.createLOANTerms(borrower, abi.encode(offer, offerValues), signature); } diff --git a/test/unit/PWNSimpleLoanSimpleOffer.t.sol b/test/unit/PWNSimpleLoanSimpleOffer.t.sol index a3c2976..54eb0bd 100644 --- a/test/unit/PWNSimpleLoanSimpleOffer.t.sol +++ b/test/unit/PWNSimpleLoanSimpleOffer.t.sol @@ -43,7 +43,7 @@ abstract contract PWNSimpleLoanSimpleOfferTest is Test { loanYield: 1, duration: 1000, expiration: 0, - borrower: address(0), + allowedBorrower: address(0), lender: lender, isPersistent: false, nonce: uint256(keccak256("nonce_1")) @@ -68,7 +68,7 @@ abstract contract PWNSimpleLoanSimpleOfferTest is Test { address(offerContract) )), keccak256(abi.encodePacked( - keccak256("Offer(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,address loanAssetAddress,uint256 loanAmount,uint256 loanYield,uint32 duration,uint40 expiration,address borrower,address lender,bool isPersistent,uint256 nonce)"), + keccak256("Offer(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,address loanAssetAddress,uint256 loanAmount,uint256 loanYield,uint32 duration,uint40 expiration,address allowedBorrower,address lender,bool isPersistent,uint256 nonce)"), abi.encode(_offer) )) )); @@ -250,11 +250,11 @@ contract PWNSimpleLoanSimpleOffer_CreateLOANTerms_Test is PWNSimpleLoanSimpleOff offerContract.createLOANTerms(borrower, abi.encode(offer), signature); } - function test_shouldFail_whenCallerIsNotBorrower_whenSetBorrower() external { - offer.borrower = address(0x50303); + function test_shouldFail_whenCallerIsNotAllowedBorrower() external { + offer.allowedBorrower = address(0x50303); signature = _signOfferCompact(lenderPK, offer); - vm.expectRevert(abi.encodeWithSelector(CallerIsNotStatedBorrower.selector, offer.borrower)); + vm.expectRevert(abi.encodeWithSelector(CallerIsNotStatedBorrower.selector, offer.allowedBorrower)); vm.prank(activeLoanContract); offerContract.createLOANTerms(borrower, abi.encode(offer), signature); } From 6b306caa05a7a73ca1b92e4d439d5d4cb658120e Mon Sep 17 00:00:00 2001 From: ashhanai Date: Tue, 13 Feb 2024 11:02:56 -0100 Subject: [PATCH 002/129] feat(request): rename `lender` address field in request struct that is allowed to accept a request to `allowedLender` --- .../factory/request/PWNSimpleLoanSimpleRequest.sol | 12 ++++++------ .../contracts/PWNSimpleLoanIntegration.t.sol | 2 +- .../PWNSimpleLoanSimpleRequestIntegration.t.sol | 2 +- test/unit/PWNSimpleLoanSimpleRequest.t.sol | 10 +++++----- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol b/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol index 760777d..896b51b 100644 --- a/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol +++ b/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol @@ -25,7 +25,7 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanRequest { * @dev EIP-712 simple request struct type hash. */ bytes32 constant internal REQUEST_TYPEHASH = keccak256( - "Request(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,address loanAssetAddress,uint256 loanAmount,uint256 loanYield,uint32 duration,uint40 expiration,address borrower,address lender,uint256 nonce)" + "Request(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,address loanAssetAddress,uint256 loanAmount,uint256 loanYield,uint32 duration,uint40 expiration,address allowedLender,address borrower,uint256 nonce)" ); bytes32 immutable internal DOMAIN_SEPARATOR; @@ -41,8 +41,8 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanRequest { * @param loanYield Amount of tokens which acts as a lenders loan interest. Borrower has to pay back a borrowed amount + yield. * @param duration Loan duration in seconds. * @param expiration Request expiration timestamp in seconds. + * @param allowedLender Address of an allowed lender. Only this address can accept a request. If the address is zero address, anybody with a loan asset can accept the request. * @param borrower Address of a borrower. This address has to sign a request to be valid. - * @param lender Address of a lender. Only this address can accept a request. If the address is zero address, anybody with a loan asset can accept the request. * @param nonce Additional value to enable identical requests in time. Without it, it would be impossible to make again request, which was once revoked. * Can be used to create a group of requests, where accepting one request will make other requests in the group revoked. */ @@ -56,8 +56,8 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanRequest { uint256 loanYield; uint32 duration; uint40 expiration; + address allowedLender; address borrower; - address lender; uint256 nonce; } @@ -122,9 +122,9 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanRequest { if (revokedRequestNonce.isNonceRevoked(borrower, request.nonce) == true) revert NonceAlreadyRevoked(); - if (request.lender != address(0)) - if (lender != request.lender) - revert CallerIsNotStatedLender(request.lender); + if (request.allowedLender != address(0)) + if (lender != request.allowedLender) + revert CallerIsNotStatedLender(request.allowedLender); if (request.duration < MIN_LOAN_DURATION) revert InvalidDuration(); diff --git a/test/integration/contracts/PWNSimpleLoanIntegration.t.sol b/test/integration/contracts/PWNSimpleLoanIntegration.t.sol index cb1fed4..dba3e1c 100644 --- a/test/integration/contracts/PWNSimpleLoanIntegration.t.sol +++ b/test/integration/contracts/PWNSimpleLoanIntegration.t.sol @@ -157,8 +157,8 @@ contract PWNSimpleLoanIntegrationTest is BaseIntegrationTest { loanYield: 10e18, duration: 3600, expiration: 0, + allowedLender: lender, borrower: borrower, - lender: lender, nonce: nonce }); diff --git a/test/integration/contracts/PWNSimpleLoanSimpleRequestIntegration.t.sol b/test/integration/contracts/PWNSimpleLoanSimpleRequestIntegration.t.sol index 9b62536..7fd0a7c 100644 --- a/test/integration/contracts/PWNSimpleLoanSimpleRequestIntegration.t.sol +++ b/test/integration/contracts/PWNSimpleLoanSimpleRequestIntegration.t.sol @@ -28,8 +28,8 @@ contract PWNSimpleLoanSimpleRequestIntegrationTest is BaseIntegrationTest { loanYield: 10e18, duration: 3600, expiration: 0, + allowedLender: lender, borrower: borrower, - lender: lender, nonce: nonce }); bytes memory signature1 = _sign(borrowerPK, simpleLoanSimpleRequest.getRequestHash(request)); diff --git a/test/unit/PWNSimpleLoanSimpleRequest.t.sol b/test/unit/PWNSimpleLoanSimpleRequest.t.sol index 3404874..4d81840 100644 --- a/test/unit/PWNSimpleLoanSimpleRequest.t.sol +++ b/test/unit/PWNSimpleLoanSimpleRequest.t.sol @@ -43,8 +43,8 @@ abstract contract PWNSimpleLoanSimpleRequestTest is Test { loanYield: 1, duration: 1000, expiration: 0, + allowedLender: address(0), borrower: borrower, - lender: address(0), nonce: uint256(keccak256("nonce_1")) }); @@ -67,7 +67,7 @@ abstract contract PWNSimpleLoanSimpleRequestTest is Test { address(requestContract) )), keccak256(abi.encodePacked( - keccak256("Request(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,address loanAssetAddress,uint256 loanAmount,uint256 loanYield,uint32 duration,uint40 expiration,address borrower,address lender,uint256 nonce)"), + keccak256("Request(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,address loanAssetAddress,uint256 loanAmount,uint256 loanYield,uint32 duration,uint40 expiration,address allowedLender,address borrower,uint256 nonce)"), abi.encode(_request) )) )); @@ -249,11 +249,11 @@ contract PWNSimpleLoanSimpleRequest_CreateLOANTerms_Test is PWNSimpleLoanSimpleR requestContract.createLOANTerms(lender, abi.encode(request), signature); } - function test_shouldFail_whenCallerIsNotLender_whenSetLender() external { - request.lender = address(0x50303); + function test_shouldFail_whenCallerIsNotAllowedLender() external { + request.allowedLender = address(0x50303); signature = _signRequestCompact(borrowerPK, request); - vm.expectRevert(abi.encodeWithSelector(CallerIsNotStatedLender.selector, request.lender)); + vm.expectRevert(abi.encodeWithSelector(CallerIsNotStatedLender.selector, request.allowedLender)); vm.prank(activeLoanContract); requestContract.createLOANTerms(lender, abi.encode(request), signature); } From 323761c6dc44de10435c6a18f16df6b2ea2cf751 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Tue, 13 Feb 2024 11:25:43 -0100 Subject: [PATCH 003/129] feat(versioning): increase contracts version to 1.2 --- src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol | 2 +- .../terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol | 2 +- .../terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol | 2 +- src/loan/terms/simple/loan/PWNSimpleLoan.sol | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol b/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol index 217106a..b6a0ebf 100644 --- a/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol +++ b/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol @@ -18,7 +18,7 @@ import "@pwn/PWNErrors.sol"; */ contract PWNSimpleLoanListOffer is PWNSimpleLoanOffer { - string internal constant VERSION = "1.1"; + string internal constant VERSION = "1.2"; /*----------------------------------------------------------*| |* # VARIABLES & CONSTANTS DEFINITIONS *| diff --git a/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol b/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol index cbdc258..6afbf34 100644 --- a/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol +++ b/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol @@ -15,7 +15,7 @@ import "@pwn/PWNErrors.sol"; */ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanOffer { - string internal constant VERSION = "1.1"; + string internal constant VERSION = "1.2"; /*----------------------------------------------------------*| |* # VARIABLES & CONSTANTS DEFINITIONS *| diff --git a/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol b/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol index 896b51b..3b76e7b 100644 --- a/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol +++ b/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol @@ -15,7 +15,7 @@ import "@pwn/PWNErrors.sol"; */ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanRequest { - string internal constant VERSION = "1.1"; + string internal constant VERSION = "1.2"; /*----------------------------------------------------------*| |* # VARIABLES & CONSTANTS DEFINITIONS *| diff --git a/src/loan/terms/simple/loan/PWNSimpleLoan.sol b/src/loan/terms/simple/loan/PWNSimpleLoan.sol index 4196ea9..aae14ab 100644 --- a/src/loan/terms/simple/loan/PWNSimpleLoan.sol +++ b/src/loan/terms/simple/loan/PWNSimpleLoan.sol @@ -22,7 +22,7 @@ import "@pwn/PWNErrors.sol"; */ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { - string internal constant VERSION = "1.1"; + string internal constant VERSION = "1.2"; uint256 public constant MAX_EXPIRATION_EXTENSION = 2_592_000; // 30 days /*----------------------------------------------------------*| From 1906dfea633718914f5ce4276c04e6c14a0ffa2a Mon Sep 17 00:00:00 2001 From: ashhanai Date: Tue, 13 Feb 2024 11:26:41 -0100 Subject: [PATCH 004/129] feat(versioning): use contract version in domain separator --- src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol | 2 +- .../terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol | 2 +- .../terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol | 2 +- test/unit/PWNSimpleLoanListOffer.t.sol | 2 +- test/unit/PWNSimpleLoanSimpleOffer.t.sol | 2 +- test/unit/PWNSimpleLoanSimpleRequest.t.sol | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol b/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol index b6a0ebf..aab6e41 100644 --- a/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol +++ b/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol @@ -87,7 +87,7 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanOffer { DOMAIN_SEPARATOR = keccak256(abi.encode( keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), keccak256("PWNSimpleLoanListOffer"), - keccak256("1"), + keccak256(abi.encodePacked(VERSION)), block.chainid, address(this) )); diff --git a/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol b/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol index 6afbf34..c15894e 100644 --- a/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol +++ b/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol @@ -72,7 +72,7 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanOffer { DOMAIN_SEPARATOR = keccak256(abi.encode( keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), keccak256("PWNSimpleLoanSimpleOffer"), - keccak256("1"), + keccak256(abi.encodePacked(VERSION)), block.chainid, address(this) )); diff --git a/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol b/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol index 3b76e7b..6279b9f 100644 --- a/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol +++ b/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol @@ -70,7 +70,7 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanRequest { DOMAIN_SEPARATOR = keccak256(abi.encode( keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), keccak256("PWNSimpleLoanSimpleRequest"), - keccak256("1"), + keccak256(abi.encodePacked(VERSION)), block.chainid, address(this) )); diff --git a/test/unit/PWNSimpleLoanListOffer.t.sol b/test/unit/PWNSimpleLoanListOffer.t.sol index f79bcbc..790cab3 100644 --- a/test/unit/PWNSimpleLoanListOffer.t.sol +++ b/test/unit/PWNSimpleLoanListOffer.t.sol @@ -69,7 +69,7 @@ abstract contract PWNSimpleLoanListOfferTest is Test { keccak256(abi.encode( keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), keccak256("PWNSimpleLoanListOffer"), - keccak256("1"), + keccak256("1.2"), block.chainid, address(offerContract) )), diff --git a/test/unit/PWNSimpleLoanSimpleOffer.t.sol b/test/unit/PWNSimpleLoanSimpleOffer.t.sol index 54eb0bd..17e98ce 100644 --- a/test/unit/PWNSimpleLoanSimpleOffer.t.sol +++ b/test/unit/PWNSimpleLoanSimpleOffer.t.sol @@ -63,7 +63,7 @@ abstract contract PWNSimpleLoanSimpleOfferTest is Test { keccak256(abi.encode( keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), keccak256("PWNSimpleLoanSimpleOffer"), - keccak256("1"), + keccak256("1.2"), block.chainid, address(offerContract) )), diff --git a/test/unit/PWNSimpleLoanSimpleRequest.t.sol b/test/unit/PWNSimpleLoanSimpleRequest.t.sol index 4d81840..23b9911 100644 --- a/test/unit/PWNSimpleLoanSimpleRequest.t.sol +++ b/test/unit/PWNSimpleLoanSimpleRequest.t.sol @@ -62,7 +62,7 @@ abstract contract PWNSimpleLoanSimpleRequestTest is Test { keccak256(abi.encode( keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), keccak256("PWNSimpleLoanSimpleRequest"), - keccak256("1"), + keccak256("1.2"), block.chainid, address(requestContract) )), From 0788ca4be56268290c15c4a0ab3388fdb446e536 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Tue, 13 Feb 2024 11:36:16 -0100 Subject: [PATCH 005/129] feat: change version, typehash, and domain separator constants visibility to public --- .../terms/simple/factory/offer/PWNSimpleLoanListOffer.sol | 6 +++--- .../terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol | 6 +++--- .../simple/factory/request/PWNSimpleLoanSimpleRequest.sol | 6 +++--- src/loan/terms/simple/loan/PWNSimpleLoan.sol | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol b/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol index aab6e41..593b5c6 100644 --- a/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol +++ b/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol @@ -18,7 +18,7 @@ import "@pwn/PWNErrors.sol"; */ contract PWNSimpleLoanListOffer is PWNSimpleLoanOffer { - string internal constant VERSION = "1.2"; + string public constant VERSION = "1.2"; /*----------------------------------------------------------*| |* # VARIABLES & CONSTANTS DEFINITIONS *| @@ -27,11 +27,11 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanOffer { /** * @dev EIP-712 simple offer struct type hash. */ - bytes32 constant internal OFFER_TYPEHASH = keccak256( + bytes32 public constant OFFER_TYPEHASH = keccak256( "Offer(uint8 collateralCategory,address collateralAddress,bytes32 collateralIdsWhitelistMerkleRoot,uint256 collateralAmount,address loanAssetAddress,uint256 loanAmount,uint256 loanYield,uint32 duration,uint40 expiration,address allowedBorrower,address lender,bool isPersistent,uint256 nonce)" ); - bytes32 immutable internal DOMAIN_SEPARATOR; + bytes32 public immutable DOMAIN_SEPARATOR; /** * @notice Construct defining a list offer. diff --git a/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol b/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol index c15894e..41f3b9a 100644 --- a/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol +++ b/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol @@ -15,7 +15,7 @@ import "@pwn/PWNErrors.sol"; */ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanOffer { - string internal constant VERSION = "1.2"; + string public constant VERSION = "1.2"; /*----------------------------------------------------------*| |* # VARIABLES & CONSTANTS DEFINITIONS *| @@ -24,11 +24,11 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanOffer { /** * @dev EIP-712 simple offer struct type hash. */ - bytes32 constant internal OFFER_TYPEHASH = keccak256( + bytes32 public constant OFFER_TYPEHASH = keccak256( "Offer(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,address loanAssetAddress,uint256 loanAmount,uint256 loanYield,uint32 duration,uint40 expiration,address allowedBorrower,address lender,bool isPersistent,uint256 nonce)" ); - bytes32 immutable internal DOMAIN_SEPARATOR; + bytes32 public immutable DOMAIN_SEPARATOR; /** * @notice Construct defining a simple offer. diff --git a/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol b/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol index 6279b9f..628fc52 100644 --- a/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol +++ b/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol @@ -15,7 +15,7 @@ import "@pwn/PWNErrors.sol"; */ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanRequest { - string internal constant VERSION = "1.2"; + string public constant VERSION = "1.2"; /*----------------------------------------------------------*| |* # VARIABLES & CONSTANTS DEFINITIONS *| @@ -24,11 +24,11 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanRequest { /** * @dev EIP-712 simple request struct type hash. */ - bytes32 constant internal REQUEST_TYPEHASH = keccak256( + bytes32 public constant REQUEST_TYPEHASH = keccak256( "Request(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,address loanAssetAddress,uint256 loanAmount,uint256 loanYield,uint32 duration,uint40 expiration,address allowedLender,address borrower,uint256 nonce)" ); - bytes32 immutable internal DOMAIN_SEPARATOR; + bytes32 public immutable DOMAIN_SEPARATOR; /** * @notice Construct defining a simple request. diff --git a/src/loan/terms/simple/loan/PWNSimpleLoan.sol b/src/loan/terms/simple/loan/PWNSimpleLoan.sol index aae14ab..73397b1 100644 --- a/src/loan/terms/simple/loan/PWNSimpleLoan.sol +++ b/src/loan/terms/simple/loan/PWNSimpleLoan.sol @@ -22,7 +22,7 @@ import "@pwn/PWNErrors.sol"; */ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { - string internal constant VERSION = "1.2"; + string public constant VERSION = "1.2"; uint256 public constant MAX_EXPIRATION_EXTENSION = 2_592_000; // 30 days /*----------------------------------------------------------*| From 0a892898475f700a30213db1a56b0c81e34cfe3d Mon Sep 17 00:00:00 2001 From: ashhanai Date: Tue, 13 Feb 2024 18:27:10 -0100 Subject: [PATCH 006/129] feat(repayment): transfer repaid loan asset directly to original lender if he is LOAN token owner --- src/loan/terms/simple/loan/PWNSimpleLoan.sol | 33 +++++- .../contracts/PWNProtocolIntegrity.t.sol | 58 ++++++++-- .../contracts/PWNSimpleLoanIntegration.t.sol | 23 ++-- test/unit/PWNSimpleLoan.t.sol | 103 +++++++++++++++--- 4 files changed, 179 insertions(+), 38 deletions(-) diff --git a/src/loan/terms/simple/loan/PWNSimpleLoan.sol b/src/loan/terms/simple/loan/PWNSimpleLoan.sol index 73397b1..01c595e 100644 --- a/src/loan/terms/simple/loan/PWNSimpleLoan.sol +++ b/src/loan/terms/simple/loan/PWNSimpleLoan.sol @@ -41,6 +41,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { * @param loanAssetAddress Address of an asset used as a loan credit. * @param loanRepayAmount Amount of a loan asset to be paid back. * @param collateral Asset used as a loan collateral. For a definition see { MultiToken dependency lib }. + * @param originalLender Address of a lender that funded the loan. */ struct LOAN { uint8 status; @@ -49,6 +50,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { address loanAssetAddress; uint256 loanRepayAmount; MultiToken.Asset collateral; + address originalLender; } /** @@ -144,6 +146,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { loan.loanAssetAddress = loanTerms.asset.assetAddress; loan.loanRepayAmount = loanTerms.loanRepayAmount; loan.collateral = loanTerms.collateral; + loan.originalLender = loanTerms.lender; emit LOANCreated(loanId, loanTerms, factoryDataHash, loanTermsFactoryContract); @@ -204,24 +207,42 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { if (loan.expiration <= block.timestamp) revert LoanDefaulted(loan.expiration); - // Move loan to repaid state - loan.status = 3; - - // Transfer repaid amount of loan asset to Vault MultiToken.Asset memory repayLoanAsset = MultiToken.Asset({ category: MultiToken.Category.ERC20, assetAddress: loan.loanAssetAddress, id: 0, amount: loan.loanRepayAmount }); + MultiToken.Asset memory collateral = loan.collateral; + address borrower = loan.borrower; + address originalLender = loan.originalLender; _permit(repayLoanAsset, msg.sender, loanAssetPermit); - _pull(repayLoanAsset, msg.sender); + + // Note: Assuming that it is safe to transfer the loan asset to the original lender because + // the lender was able to sign an offer or make a contract call, thus can handle incoming transfers. + bool immediateClaim = originalLender == loanToken.ownerOf(loanId); + if (immediateClaim) { + // Delete loan data & burn LOAN token before calling safe transfer + _deleteLoan(loanId); + + // Transfer the repaid loan asset to the original lender + _pushFrom(repayLoanAsset, msg.sender, originalLender); + } else { + // Move loan to repaid state and wait for the lender to claim the repaid loan asset + loan.status = 3; + + // Transfer the repaid loan asset to the Vault + _pull(repayLoanAsset, msg.sender); + } // Transfer collateral back to borrower - _push(loan.collateral, loan.borrower); + _push(collateral, borrower); emit LOANPaidBack(loanId); + + if (immediateClaim) + emit LOANClaimed(loanId, false); } diff --git a/test/integration/contracts/PWNProtocolIntegrity.t.sol b/test/integration/contracts/PWNProtocolIntegrity.t.sol index eab23eb..8067b04 100644 --- a/test/integration/contracts/PWNProtocolIntegrity.t.sol +++ b/test/integration/contracts/PWNProtocolIntegrity.t.sol @@ -11,7 +11,7 @@ import "@pwn-test/integration/BaseIntegrationTest.t.sol"; contract PWNProtocolIntegrityTest is BaseIntegrationTest { - function test_shouldFailCreatingLOANOnNotActiveLoanContract() external { + function test_shouldFailToCreateLOAN_whenLoanContractNotActive() external { // Remove ACTIVE_LOAN tag vm.prank(protocolSafe); hub.setTag(address(simpleLoan), PWNHubTags.ACTIVE_LOAN, false); @@ -22,7 +22,7 @@ contract PWNProtocolIntegrityTest is BaseIntegrationTest { ); } - function test_shouldRepayLOANWithNotActiveLoanContract() external { + function test_shouldRepayLOAN_whenLoanContractNotActive_whenOriginalLenderIsLOANOwner() external { // Create LOAN uint256 loanId = _createERC1155Loan(); @@ -30,25 +30,63 @@ contract PWNProtocolIntegrityTest is BaseIntegrationTest { vm.prank(protocolSafe); hub.setTag(address(simpleLoan), PWNHubTags.ACTIVE_LOAN, false); - // Repay loan + // Repay loan directly to original lender + _repayLoan(loanId); + + // Assert final state + vm.expectRevert("ERC721: invalid token ID"); + loanToken.ownerOf(loanId); + + assertEq(loanAsset.balanceOf(lender), 110e18); + assertEq(loanAsset.balanceOf(borrower), 0); + assertEq(loanAsset.balanceOf(address(simpleLoan)), 0); + + assertEq(t1155.balanceOf(lender, 42), 0); + assertEq(t1155.balanceOf(borrower, 42), 10e18); + assertEq(t1155.balanceOf(address(simpleLoan), 42), 0); + } + + function test_shouldRepayLOAN_whenLoanContractNotActive_whenOriginalLenderIsNotLOANOwner() external { + address lender2 = makeAddr("lender2"); + + // Create LOAN + uint256 loanId = _createERC1155Loan(); + + // Transfer loan to another lender + vm.prank(lender); + loanToken.transferFrom(lender, lender2, loanId); + + // Remove ACTIVE_LOAN tag + vm.prank(protocolSafe); + hub.setTag(address(simpleLoan), PWNHubTags.ACTIVE_LOAN, false); + + // Repay loan directly to original lender _repayLoan(loanId); // Assert final state - assertEq(loanToken.ownerOf(loanId), lender); + assertEq(loanToken.ownerOf(loanId), lender2); assertEq(loanAsset.balanceOf(lender), 0); + assertEq(loanAsset.balanceOf(lender2), 0); assertEq(loanAsset.balanceOf(borrower), 0); assertEq(loanAsset.balanceOf(address(simpleLoan)), 110e18); assertEq(t1155.balanceOf(lender, 42), 0); + assertEq(t1155.balanceOf(lender2, 42), 0); assertEq(t1155.balanceOf(borrower, 42), 10e18); assertEq(t1155.balanceOf(address(simpleLoan), 42), 0); } - function test_shouldClaimRepaidLOANWithNotActiveLoanContract() external { + function test_shouldClaimRepaidLOAN_whenLoanContractNotActive() external { + address lender2 = makeAddr("lender2"); + // Create LOAN uint256 loanId = _createERC1155Loan(); + // Transfer loan to another lender + vm.prank(lender); + loanToken.transferFrom(lender, lender2, loanId); + // Repay loan _repayLoan(loanId); @@ -57,23 +95,25 @@ contract PWNProtocolIntegrityTest is BaseIntegrationTest { hub.setTag(address(simpleLoan), PWNHubTags.ACTIVE_LOAN, false); // Claim loan - vm.prank(lender); + vm.prank(lender2); simpleLoan.claimLOAN(loanId); // Assert final state vm.expectRevert("ERC721: invalid token ID"); loanToken.ownerOf(loanId); - assertEq(loanAsset.balanceOf(lender), 110e18); + assertEq(loanAsset.balanceOf(lender), 0); + assertEq(loanAsset.balanceOf(lender2), 110e18); assertEq(loanAsset.balanceOf(borrower), 0); assertEq(loanAsset.balanceOf(address(simpleLoan)), 0); assertEq(t1155.balanceOf(lender, 42), 0); + assertEq(t1155.balanceOf(lender2, 42), 0); assertEq(t1155.balanceOf(borrower, 42), 10e18); assertEq(t1155.balanceOf(address(simpleLoan), 42), 0); } - function test_shouldFail_whenCallerIsNotActiveLoan() external { + function test_shouldFailToCreateLOANTerms_whenCallerIsNotActiveLoan() external { // Remove ACTIVE_LOAN tag vm.prank(protocolSafe); hub.setTag(address(simpleLoan), PWNHubTags.ACTIVE_LOAN, false); @@ -85,7 +125,7 @@ contract PWNProtocolIntegrityTest is BaseIntegrationTest { simpleLoanSimpleOffer.createLOANTerms(borrower, "", ""); // Offer data are not important in this test } - function test_shouldFail_whenPassingInvalidTermsFactoryContract() external { + function test_shouldFailToCreateLOAN_whenPassingInvalidTermsFactoryContract() external { // Remove SIMPLE_LOAN_TERMS_FACTORY tag vm.prank(protocolSafe); hub.setTag(address(simpleLoanSimpleOffer), PWNHubTags.SIMPLE_LOAN_TERMS_FACTORY, false); diff --git a/test/integration/contracts/PWNSimpleLoanIntegration.t.sol b/test/integration/contracts/PWNSimpleLoanIntegration.t.sol index dba3e1c..6a3e1a1 100644 --- a/test/integration/contracts/PWNSimpleLoanIntegration.t.sol +++ b/test/integration/contracts/PWNSimpleLoanIntegration.t.sol @@ -272,7 +272,7 @@ contract PWNSimpleLoanIntegrationTest is BaseIntegrationTest { // Repay LOAN - function test_shouldRepayLoan_whenNotExpired() external { + function test_shouldRepayLoan_whenNotExpired_whenOriginalLenderIsLOANOwner() external { // Create LOAN uint256 loanId = _createERC1155Loan(); @@ -280,11 +280,12 @@ contract PWNSimpleLoanIntegrationTest is BaseIntegrationTest { _repayLoan(loanId); // Assert final state - assertEq(loanToken.ownerOf(loanId), lender); + vm.expectRevert("ERC721: invalid token ID"); + loanToken.ownerOf(loanId); - assertEq(loanAsset.balanceOf(lender), 0); + assertEq(loanAsset.balanceOf(lender), 110e18); assertEq(loanAsset.balanceOf(borrower), 0); - assertEq(loanAsset.balanceOf(address(simpleLoan)), 110e18); + assertEq(loanAsset.balanceOf(address(simpleLoan)), 0); assertEq(t1155.balanceOf(lender, 42), 0); assertEq(t1155.balanceOf(borrower, 42), 10e18); @@ -309,26 +310,34 @@ contract PWNSimpleLoanIntegrationTest is BaseIntegrationTest { // Claim LOAN - function test_shouldClaimRepaidLOAN() external { + function test_shouldClaimRepaidLOAN_whenOriginalLenderIsNotLOANOwner() external { + address lender2 = makeAddr("lender2"); + // Create LOAN uint256 loanId = _createERC1155Loan(); + // Transfer loan to another lender + vm.prank(lender); + loanToken.transferFrom(lender, lender2, loanId); + // Repay loan _repayLoan(loanId); // Claim loan - vm.prank(lender); + vm.prank(lender2); simpleLoan.claimLOAN(loanId); // Assert final state vm.expectRevert("ERC721: invalid token ID"); loanToken.ownerOf(loanId); - assertEq(loanAsset.balanceOf(lender), 110e18); + assertEq(loanAsset.balanceOf(lender), 0); + assertEq(loanAsset.balanceOf(lender2), 110e18); assertEq(loanAsset.balanceOf(borrower), 0); assertEq(loanAsset.balanceOf(address(simpleLoan)), 0); assertEq(t1155.balanceOf(lender, 42), 0); + assertEq(t1155.balanceOf(lender2, 42), 0); assertEq(t1155.balanceOf(borrower, 42), 10e18); assertEq(t1155.balanceOf(address(simpleLoan), 42), 0); } diff --git a/test/unit/PWNSimpleLoan.t.sol b/test/unit/PWNSimpleLoan.t.sol index 60d2f19..ece46e0 100644 --- a/test/unit/PWNSimpleLoan.t.sol +++ b/test/unit/PWNSimpleLoan.t.sol @@ -88,7 +88,8 @@ abstract contract PWNSimpleLoanTest is Test { expiration: uint40(block.timestamp + 40039), loanAssetAddress: address(fungibleAsset), loanRepayAmount: 6731, - collateral: MultiToken.Asset(MultiToken.Category.ERC721, address(nonFungibleAsset), 2, 0) + collateral: MultiToken.Asset(MultiToken.Category.ERC721, address(nonFungibleAsset), 2, 0), + originalLender: lender }); simpleLoanTerms = PWNLOANTerms.Simple({ @@ -106,7 +107,8 @@ abstract contract PWNSimpleLoanTest is Test { expiration: 0, loanAssetAddress: address(0), loanRepayAmount: 0, - collateral: MultiToken.Asset(MultiToken.Category.ERC20, address(0), 0, 0) + collateral: MultiToken.Asset(MultiToken.Category.ERC20, address(0), 0, 0), + originalLender: address(0) }); loanFactoryDataHash = keccak256("factoryData"); @@ -129,6 +131,7 @@ abstract contract PWNSimpleLoanTest is Test { assertEq(_simpleLoan1.collateral.assetAddress, _simpleLoan2.collateral.assetAddress); assertEq(_simpleLoan1.collateral.id, _simpleLoan2.collateral.id); assertEq(_simpleLoan1.collateral.amount, _simpleLoan2.collateral.amount); + assertEq(_simpleLoan1.originalLender, _simpleLoan2.originalLender); } function _assertLOANEq(uint256 _loanId, PWNSimpleLoan.LOAN memory _simpleLoan) internal { @@ -148,6 +151,8 @@ abstract contract PWNSimpleLoanTest is Test { _assertLOANWord(loanSlot + 4, abi.encodePacked(_simpleLoan.collateral.id)); // Collateral amount _assertLOANWord(loanSlot + 5, abi.encodePacked(_simpleLoan.collateral.amount)); + // Original lender + _assertLOANWord(loanSlot + 6, abi.encodePacked(uint96(0), _simpleLoan.originalLender)); } function _mockLOAN(uint256 _loanId, PWNSimpleLoan.LOAN memory _simpleLoan) internal { @@ -167,6 +172,8 @@ abstract contract PWNSimpleLoanTest is Test { _storeLOANWord(loanSlot + 4, abi.encodePacked(_simpleLoan.collateral.id)); // Collateral amount _storeLOANWord(loanSlot + 5, abi.encodePacked(_simpleLoan.collateral.amount)); + // Original lender + _storeLOANWord(loanSlot + 6, abi.encodePacked(uint96(0), _simpleLoan.originalLender)); } @@ -398,14 +405,27 @@ contract PWNSimpleLoan_CreateLoan_Test is PWNSimpleLoanTest { contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { + address notOriginalLender = makeAddr("notOriginalLender"); + function setUp() override public { super.setUp(); + vm.mockCall( + loanToken, abi.encodeWithSignature("ownerOf(uint256)", loanId), abi.encode(lender) + ); + // Move collateral to vault vm.prank(borrower); nonFungibleAsset.transferFrom(borrower, address(loan), 2); } + function _LOANTokenNotOwnedByOriginalLender() internal { + vm.mockCall( + loanToken, abi.encodeWithSignature("ownerOf(uint256)", loanId), abi.encode(notOriginalLender) + ); + } + + function test_shouldFail_whenLoanDoesNotExist() external { simpleLoan.status = 0; _mockLOAN(loanId, simpleLoan); @@ -431,34 +451,73 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { loan.repayLOAN(loanId, loanAssetPermit); } - function test_shouldMoveLoanToRepaidState() external { + function test_shouldCallPermit_whenProvided() external { + _mockLOAN(loanId, simpleLoan); + loanAssetPermit = abi.encodePacked(uint256(1), uint256(2), uint256(3), uint8(4)); + + vm.expectCall( + simpleLoan.loanAssetAddress, + abi.encodeWithSignature( + "permit(address,address,uint256,uint256,uint8,bytes32,bytes32)", + borrower, address(loan), simpleLoan.loanRepayAmount, 1, uint8(4), uint256(2), uint256(3) + ) + ); + + vm.prank(borrower); + loan.repayLOAN(loanId, loanAssetPermit); + } + + function test_shouldDeleteLoanData_whenLOANOwnerIsOriginalLender() external { _mockLOAN(loanId, simpleLoan); loan.repayLOAN(loanId, loanAssetPermit); - bytes32 loanSlot = keccak256(abi.encode( - loanId, - LOANS_SLOT - )); - // Parse status value from first storage slot - bytes32 statusValue = vm.load(address(loan), loanSlot) & bytes32(uint256(0xff)); - assertTrue(statusValue == bytes32(uint256(3))); + _assertLOANEq(loanId, nonExistingLoan); } - function test_shouldTransferRepaidAmountToVault() external { + function test_shouldBurnLOANToken_whenLOANOwnerIsOriginalLender() external { + _mockLOAN(loanId, simpleLoan); + + vm.expectCall(loanToken, abi.encodeWithSignature("burn(uint256)", loanId)); + + loan.repayLOAN(loanId, loanAssetPermit); + } + + function test_shouldTransferRepaidAmountToLender_whenLOANOwnerIsOriginalLender() external { _mockLOAN(loanId, simpleLoan); - loanAssetPermit = abi.encodePacked(uint256(1), uint256(2), uint256(3), uint8(4)); vm.expectCall( simpleLoan.loanAssetAddress, abi.encodeWithSignature( - "permit(address,address,uint256,uint256,uint8,bytes32,bytes32)", - borrower, address(loan), simpleLoan.loanRepayAmount, 1, uint8(4), uint256(2), uint256(3) + "transferFrom(address,address,uint256)", borrower, lender, simpleLoan.loanRepayAmount ) ); + + vm.prank(borrower); + loan.repayLOAN(loanId, loanAssetPermit); + } + + function test_shouldMoveLoanToRepaidState_whenLOANOwnerIsNotOriginalLender() external { + _mockLOAN(loanId, simpleLoan); + _LOANTokenNotOwnedByOriginalLender(); + + loan.repayLOAN(loanId, loanAssetPermit); + + bytes32 loanSlot = keccak256(abi.encode(loanId, LOANS_SLOT)); + // Parse status value from first storage slot + bytes32 statusValue = vm.load(address(loan), loanSlot) & bytes32(uint256(0xff)); + assertTrue(statusValue == bytes32(uint256(3))); + } + + function test_shouldTransferRepaidAmountToVault_whenLOANOwnerIsNotOriginalLender() external { + _mockLOAN(loanId, simpleLoan); + _LOANTokenNotOwnedByOriginalLender(); + vm.expectCall( simpleLoan.loanAssetAddress, - abi.encodeWithSignature("transferFrom(address,address,uint256)", borrower, address(loan), simpleLoan.loanRepayAmount) + abi.encodeWithSignature( + "transferFrom(address,address,uint256)", borrower, address(loan), simpleLoan.loanRepayAmount + ) ); vm.prank(borrower); @@ -470,7 +529,10 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { vm.expectCall( simpleLoan.collateral.assetAddress, - abi.encodeWithSignature("safeTransferFrom(address,address,uint256,bytes)", address(loan), simpleLoan.borrower, simpleLoan.collateral.id) + abi.encodeWithSignature( + "safeTransferFrom(address,address,uint256,bytes)", + address(loan), simpleLoan.borrower, simpleLoan.collateral.id + ) ); loan.repayLOAN(loanId, loanAssetPermit); @@ -485,6 +547,15 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { loan.repayLOAN(loanId, loanAssetPermit); } + function test_shouldEmitEvent_LOANClaimed_whenLOANOwnerIsOriginalLender() external { + _mockLOAN(loanId, simpleLoan); + + vm.expectEmit(true, true, true, true); + emit LOANClaimed(loanId, false); + + loan.repayLOAN(loanId, loanAssetPermit); + } + } From cbb695fb689f8b4f210be63840f05341e295c122 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Tue, 13 Feb 2024 18:42:42 -0100 Subject: [PATCH 007/129] test(deployed): remove deployed protocol tests for all testnets but sepolia --- test/integration/deployed/DeployedProtocol.t.sol | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/integration/deployed/DeployedProtocol.t.sol b/test/integration/deployed/DeployedProtocol.t.sol index 095efdc..bfec2c2 100644 --- a/test/integration/deployed/DeployedProtocol.t.sol +++ b/test/integration/deployed/DeployedProtocol.t.sol @@ -107,9 +107,5 @@ contract DeployedProtocolTest is DeploymentTest { function test_deployedProtocol_bsc() external { _test_deployedProtocol("bsc"); } function test_deployedProtocol_sepolia() external { _test_deployedProtocol("sepolia"); } - function test_deployedProtocol_goerli() external { _test_deployedProtocol("goerli"); } - function test_deployedProtocol_base_goerli() external { _test_deployedProtocol("base_goerli"); } - function test_deployedProtocol_cronos_testnet() external { _test_deployedProtocol("cronos_testnet"); } - function test_deployedProtocol_mantle_testnet() external { _test_deployedProtocol("mantle_testnet"); } } From 59f90b1621f99d8d73da100c527a89404f8119a2 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Tue, 13 Feb 2024 18:52:49 -0100 Subject: [PATCH 008/129] feat(config): initialize config with zero values on deployment, forcing use as an implementation for a proxy --- src/config/PWNConfig.sol | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/config/PWNConfig.sol b/src/config/PWNConfig.sol index de0f30e..836b703 100644 --- a/src/config/PWNConfig.sol +++ b/src/config/PWNConfig.sol @@ -14,7 +14,7 @@ import "@pwn/PWNErrors.sol"; */ contract PWNConfig is Ownable2Step, Initializable { - string internal constant VERSION = "1.0"; + string internal constant VERSION = "1.2"; /*----------------------------------------------------------*| |* # VARIABLES & CONSTANTS DEFINITIONS *| @@ -65,7 +65,9 @@ contract PWNConfig is Ownable2Step, Initializable { |*----------------------------------------------------------*/ constructor() Ownable2Step() { - + // PWNConfig is used as a proxy. Use initializer to setup initial properties. + // Initialized the implementation contract with zero values. + initialize(address(0), 0, address(0)); } function initialize(address _owner, uint16 _fee, address _feeCollector) initializer external { From c8ef8586b17e26d6e7e8001d8bef1b56260277b6 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Wed, 14 Feb 2024 10:25:27 -0100 Subject: [PATCH 009/129] feat(config): implement default loan metadata uri --- src/PWNErrors.sol | 1 + src/config/PWNConfig.sol | 49 +++++++++--- test/unit/PWNConfig.t.sol | 161 ++++++++++++++++++++++++++++++++++---- 3 files changed, 187 insertions(+), 24 deletions(-) diff --git a/src/PWNErrors.sol b/src/PWNErrors.sol index e22eb12..f93adda 100644 --- a/src/PWNErrors.sol +++ b/src/PWNErrors.sol @@ -49,3 +49,4 @@ error InvalidInputData(); // Config error InvalidFeeValue(); error InvalidFeeCollector(); +error ZeroLoanContract(); diff --git a/src/config/PWNConfig.sol b/src/config/PWNConfig.sol index 836b703..025e3cd 100644 --- a/src/config/PWNConfig.sol +++ b/src/config/PWNConfig.sol @@ -36,8 +36,9 @@ contract PWNConfig is Ownable2Step, Initializable { /** * @notice Mapping of a loan contract address to LOAN token metadata uri. * @dev LOAN token minted by a loan contract will return metadata uri stored in this mapping. + * If there is no metadata uri for a loan contract, default metadata uri will be used stored under address(0). */ - mapping (address => string) public loanMetadataUri; + mapping (address => string) private _loanMetadataUri; /*----------------------------------------------------------*| @@ -57,7 +58,12 @@ contract PWNConfig is Ownable2Step, Initializable { /** * @dev Emitted when new LOAN token metadata uri is set. */ - event LoanMetadataUriUpdated(address indexed loanContract, string newUri); + event LOANMetadataUriUpdated(address indexed loanContract, string newUri); + + /** + * @dev Emitted when new default LOAN token metadata uri is set. + */ + event DefaultLOANMetadataUriUpdated(string newUri); /*----------------------------------------------------------*| @@ -66,11 +72,11 @@ contract PWNConfig is Ownable2Step, Initializable { constructor() Ownable2Step() { // PWNConfig is used as a proxy. Use initializer to setup initial properties. - // Initialized the implementation contract with zero values. - initialize(address(0), 0, address(0)); + _disableInitializers(); + _transferOwnership(address(0)); } - function initialize(address _owner, uint16 _fee, address _feeCollector) initializer external { + function initialize(address _owner, uint16 _fee, address _feeCollector) external initializer { require(_owner != address(0), "Owner is zero address"); _transferOwnership(_owner); @@ -123,7 +129,7 @@ contract PWNConfig is Ownable2Step, Initializable { /*----------------------------------------------------------*| - |* # LOAN METADATA MANAGEMENT *| + |* # LOAN METADATA *| |*----------------------------------------------------------*/ /** @@ -131,9 +137,34 @@ contract PWNConfig is Ownable2Step, Initializable { * @param loanContract Address of a loan contract. * @param metadataUri New value of LOAN token metadata uri for given `loanContract`. */ - function setLoanMetadataUri(address loanContract, string memory metadataUri) external onlyOwner { - loanMetadataUri[loanContract] = metadataUri; - emit LoanMetadataUriUpdated(loanContract, metadataUri); + function setLOANMetadataUri(address loanContract, string memory metadataUri) external onlyOwner { + if (loanContract == address(0)) + // address(0) is used as a default metadata uri. Use `setDefaultLOANMetadataUri` to set default metadata uri. + revert ZeroLoanContract(); + + _loanMetadataUri[loanContract] = metadataUri; + emit LOANMetadataUriUpdated(loanContract, metadataUri); + } + + /** + * @notice Set a default LOAN token metadata uri. + * @param metadataUri New value of default LOAN token metadata uri. + */ + function setDefaultLOANMetadataUri(string memory metadataUri) external onlyOwner { + _loanMetadataUri[address(0)] = metadataUri; + emit DefaultLOANMetadataUriUpdated(metadataUri); + } + + /** + * @notice Return a LOAN token metadata uri base on a loan contract that minted the token. + * @param loanContract Address of a loan contract. + * @return uri Metadata uri for given loan contract. + */ + function loanMetadataUri(address loanContract) external view returns (string memory uri) { + uri = _loanMetadataUri[loanContract]; + // If there is no metadata uri for a loan contract, use default metadata uri. + if (bytes(uri).length == 0) + uri = _loanMetadataUri[address(0)]; } } diff --git a/test/unit/PWNConfig.t.sol b/test/unit/PWNConfig.t.sol index 13c66dc..198bf1c 100644 --- a/test/unit/PWNConfig.t.sol +++ b/test/unit/PWNConfig.t.sol @@ -10,6 +10,7 @@ abstract contract PWNConfigTest is Test { bytes32 internal constant OWNER_SLOT = bytes32(uint256(0)); // `_owner` property position bytes32 internal constant PENDING_OWNER_SLOT = bytes32(uint256(1)); // `_pendingOwner` property position + bytes32 internal constant INITIALIZED_SLOT = bytes32(uint256(1)); // `_initialized` property position bytes32 internal constant FEE_SLOT = bytes32(uint256(1)); // `fee` property position bytes32 internal constant FEE_COLLECTOR_SLOT = bytes32(uint256(2)); // `feeCollector` property position bytes32 internal constant LOAN_METADATA_URI_SLOT = bytes32(uint256(3)); // `loanMetadataUri` mapping position @@ -20,12 +21,42 @@ abstract contract PWNConfigTest is Test { event FeeUpdated(uint16 oldFee, uint16 newFee); event FeeCollectorUpdated(address oldFeeCollector, address newFeeCollector); - event LoanMetadataUriUpdated(address indexed loanContract, string newUri); + event LOANMetadataUriUpdated(address indexed loanContract, string newUri); + event DefaultLOANMetadataUriUpdated(string newUri); function setUp() virtual public { config = new PWNConfig(); } + function _initialize() internal { + // initialize owner to `owner`, fee to 0 and feeCollector to `feeCollector` + vm.store(address(config), OWNER_SLOT, bytes32(uint256(uint160(owner)))); + vm.store(address(config), FEE_COLLECTOR_SLOT, bytes32(uint256(uint160(feeCollector)))); + } + +} + + +/*----------------------------------------------------------*| +|* # CONSTRUCTOR *| +|*----------------------------------------------------------*/ + +contract PWNConfig_Constructor_Test is PWNConfigTest { + + function test_shouldInitializeWithZeroValues() external { + bytes32 ownerValue = vm.load(address(config), OWNER_SLOT); + assertEq(address(uint160(uint256(ownerValue))), address(0)); + + bytes32 initializedSlotValue = vm.load(address(config), INITIALIZED_SLOT); + assertEq(uint16(uint256(initializedSlotValue << 88 >> 248)), 255); // disable initializers + + bytes32 feeSlotValue = vm.load(address(config), FEE_SLOT); + assertEq(uint16(uint256(feeSlotValue << 64 >> 240)), 0); + + bytes32 feeCollectorValue = vm.load(address(config), FEE_COLLECTOR_SLOT); + assertEq(address(uint160(uint256(feeCollectorValue))), address(0)); + } + } @@ -37,14 +68,21 @@ contract PWNConfig_Initialize_Test is PWNConfigTest { uint16 fee = 32; - function test_shouldSetOwner() external { + function setUp() override public { + super.setUp(); + + // mock that contract is not initialized + vm.store(address(config), INITIALIZED_SLOT, bytes32(0)); + } + + function test_shouldSetValues() external { config.initialize(owner, fee, feeCollector); bytes32 ownerValue = vm.load(address(config), OWNER_SLOT); assertEq(address(uint160(uint256(ownerValue))), owner); bytes32 feeSlotValue = vm.load(address(config), FEE_SLOT); - assertEq(uint16(uint256(feeSlotValue >> 176)), fee); + assertEq(uint16(uint256(feeSlotValue << 64 >> 240)), fee); bytes32 feeCollectorValue = vm.load(address(config), FEE_COLLECTOR_SLOT); assertEq(address(uint160(uint256(feeCollectorValue))), feeCollector); @@ -79,7 +117,7 @@ contract PWNConfig_SetFee_Test is PWNConfigTest { function setUp() override public { super.setUp(); - config.initialize(owner, 0, feeCollector); + _initialize(); } @@ -125,11 +163,10 @@ contract PWNConfig_SetFeeCollector_Test is PWNConfigTest { address newFeeCollector = address(0xfee); - function setUp() override public { super.setUp(); - config.initialize(owner, 0, feeCollector); + _initialize(); } @@ -167,7 +204,7 @@ contract PWNConfig_SetFeeCollector_Test is PWNConfigTest { |* # SET LOAN METADATA URI *| |*----------------------------------------------------------*/ -contract PWNConfig_SetLoanMetadataUri_Test is PWNConfigTest { +contract PWNConfig_SetLOANMetadataUri_Test is PWNConfigTest { string tokenUri = "test.token.uri"; address loanContract = address(0x63); @@ -175,18 +212,27 @@ contract PWNConfig_SetLoanMetadataUri_Test is PWNConfigTest { function setUp() override public { super.setUp(); - config.initialize(owner, 0, feeCollector); + _initialize(); } function test_shouldFail_whenCallerIsNotOwner() external { vm.expectRevert("Ownable: caller is not the owner"); - config.setLoanMetadataUri(loanContract, tokenUri); + config.setLOANMetadataUri(loanContract, tokenUri); + } + + function test_shouldFail_whenZeroLoanContract() external { + vm.expectRevert(abi.encodeWithSelector(ZeroLoanContract.selector)); + vm.prank(owner); + config.setLOANMetadataUri(address(0), tokenUri); } - function test_shouldSetLoanMetadataUriToLoanContract() external { + function testFuzz_shouldStoreLoanMetadataUriToLoanContract(address _loanContract) external { + vm.assume(_loanContract != address(0)); + loanContract = _loanContract; + vm.prank(owner); - config.setLoanMetadataUri(loanContract, tokenUri); + config.setLOANMetadataUri(loanContract, tokenUri); bytes32 tokenUriValue = vm.load( address(config), @@ -201,12 +247,97 @@ contract PWNConfig_SetLoanMetadataUri_Test is PWNConfigTest { assertEq(keccak256(abi.encodePacked(tokenUriValue >> 8)), keccak256(abi.encodePacked(_tokenUri >> 8))); } - function test_shouldEmitEvent_LoanMetadataUriUpdated() external { - vm.expectEmit(true, true, false, false); - emit LoanMetadataUriUpdated(loanContract, tokenUri); + function test_shouldEmitEvent_LOANMetadataUriUpdated() external { + vm.expectEmit(true, true, true, true); + emit LOANMetadataUriUpdated(loanContract, tokenUri); vm.prank(owner); - config.setLoanMetadataUri(loanContract, tokenUri); + config.setLOANMetadataUri(loanContract, tokenUri); + } + +} + + +/*----------------------------------------------------------*| +|* # SET DEFAULT LOAN METADATA URI *| +|*----------------------------------------------------------*/ + +contract PWNConfig_SetDefaultLOANMetadataUri_Test is PWNConfigTest { + + string tokenUri = "test.token.uri"; + + function setUp() override public { + super.setUp(); + + _initialize(); + } + + + function test_shouldFail_whenCallerIsNotOwner() external { + vm.expectRevert("Ownable: caller is not the owner"); + config.setDefaultLOANMetadataUri(tokenUri); + } + + function test_shouldStoreDefaultLoanMetadataUri() external { + vm.prank(owner); + config.setDefaultLOANMetadataUri(tokenUri); + + bytes32 tokenUriValue = vm.load( + address(config), + keccak256(abi.encode(address(0), LOAN_METADATA_URI_SLOT)) + ); + bytes memory memoryTokenUri = bytes(tokenUri); + bytes32 _tokenUri; + assembly { + _tokenUri := mload(add(memoryTokenUri, 0x20)) + } + // Remove string length + assertEq(keccak256(abi.encodePacked(tokenUriValue >> 8)), keccak256(abi.encodePacked(_tokenUri >> 8))); + } + + function test_shouldEmitEvent_DefaultLOANMetadataUriUpdated() external { + vm.expectEmit(true, true, true, true); + emit DefaultLOANMetadataUriUpdated(tokenUri); + + vm.prank(owner); + config.setDefaultLOANMetadataUri(tokenUri); + } + +} + + +/*----------------------------------------------------------*| +|* # LOAN METADATA URI *| +|*----------------------------------------------------------*/ + +contract PWNConfig_LoanMetadataUri_Test is PWNConfigTest { + + function setUp() override public { + super.setUp(); + + _initialize(); + } + + + function testFuzz_shouldReturnDefaultLoanMetadataUri_whenNoStoreValueForLoanContract(address loanContract) external { + string memory defaultUri = "default.token.uri"; + + vm.prank(owner); + config.setDefaultLOANMetadataUri(defaultUri); + + string memory uri = config.loanMetadataUri(loanContract); + assertEq(uri, defaultUri); + } + + function testFuzz_shouldReturnLoanMetadataUri_whenStoredValueForLoanContract(address loanContract) external { + vm.assume(loanContract != address(0)); + string memory tokenUri = "test.token.uri"; + + vm.prank(owner); + config.setLOANMetadataUri(loanContract, tokenUri); + + string memory uri = config.loanMetadataUri(loanContract); + assertEq(uri, tokenUri); } } From 75394c746684474ec533036bb6524de266b3558e Mon Sep 17 00:00:00 2001 From: ashhanai Date: Wed, 14 Feb 2024 13:27:36 -0100 Subject: [PATCH 010/129] feat(config): remove dependency of initializer values on config address --- script/PWN.s.sol | 43 ++++++++++++------------------------------- 1 file changed, 12 insertions(+), 31 deletions(-) diff --git a/script/PWN.s.sol b/script/PWN.s.sol index 1ee6855..73d89e1 100644 --- a/script/PWN.s.sol +++ b/script/PWN.s.sol @@ -91,7 +91,7 @@ forge script script/PWN.s.sol:Deploy \ --rpc-url $RPC_URL \ --private-key $PRIVATE_KEY \ --with-gas-price $(cast --to-wei 15 gwei) \ ---verify --etherscan-api-key $BSCSCAN_API_KEY \ +--verify --etherscan-api-key $ETHERSCAN_API_KEY \ --broadcast */ /// @dev Expecting to have deployer, deployerSafe, protocolSafe, daoSafe & feeCollector addresses set in the `deployments.json` @@ -104,6 +104,8 @@ forge script script/PWN.s.sol:Deploy \ require(daoSafe != address(0), "DAO safe not set"); require(feeCollector != address(0), "Fee collector not set"); + uint256 initialConfigHelper = vmSafe.envUint("INITIAL_CONFIG_HELPER"); + vm.startBroadcast(); // Deploy protocol @@ -117,13 +119,17 @@ forge script script/PWN.s.sol:Deploy \ salt: PWNContractDeployerSalt.CONFIG_PROXY, bytecode: abi.encodePacked( type(TransparentUpgradeableProxy).creationCode, - abi.encode( - configSingleton, - protocolSafe, - abi.encodeWithSignature("initialize(address,uint16,address)", daoSafe, 0, feeCollector) - ) + abi.encode(configSingleton, vm.addr(initialConfigHelper), "") ) })); + config.initialize(daoSafe, 0, feeCollector); + + vm.stopBroadcast(); + + vm.broadcast(initialConfigHelper); + TransparentUpgradeableProxy(payable(address(config))).changeAdmin(protocolSafe); + + vm.startBroadcast(); // - Hub hub = PWNHub(_deployAndTransferOwnership({ @@ -229,37 +235,12 @@ forge script script/PWN.s.sol:Setup \ vm.startBroadcast(); - _initializeConfigImpl(); _acceptOwnership(protocolSafe, address(hub)); _setTags(); vm.stopBroadcast(); } -/* -forge script script/PWN.s.sol:Setup \ ---sig "initializeConfigImpl()" \ ---rpc-url $RPC_URL \ ---private-key $PRIVATE_KEY \ ---with-gas-price $(cast --to-wei 15 gwei) \ ---broadcast -*/ - /// @dev Expecting to have configSingleton address set in the `deployments.json` - function initializeConfigImpl() external { - _loadDeployedAddresses(); - - vm.startBroadcast(); - _initializeConfigImpl(); - vm.stopBroadcast(); - } - - function _initializeConfigImpl() internal { - address deadAddr = 0x000000000000000000000000000000000000dEaD; - configSingleton.initialize(deadAddr, 0, deadAddr); - - console2.log("Config impl initialized"); - } - /* forge script script/PWN.s.sol:Setup \ --sig "acceptOwnership(address,address)" $SAFE $CONTRACT \ From d39ea0367212fdda0294a44bf3cb0d539add4b4c Mon Sep 17 00:00:00 2001 From: ashhanai Date: Thu, 15 Feb 2024 11:42:10 -0100 Subject: [PATCH 011/129] build(forge): increase forge version to v1.7.6 --- lib/forge-std | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/forge-std b/lib/forge-std index eb980e1..ae570fe 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit eb980e1d4f0e8173ec27da77297ae411840c8ccb +Subproject commit ae570fec082bfe1c1f45b0acca4a2b4f84d345ce From 47803c0d57ca714fc8d2ed5cebdf61a737160c92 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Thu, 15 Feb 2024 11:46:34 -0100 Subject: [PATCH 012/129] build(multitoken): increase multitoken version to v2.3.0-beta.2 --- lib/MultiToken | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/MultiToken b/lib/MultiToken index a2971be..9f5d642 160000 --- a/lib/MultiToken +++ b/lib/MultiToken @@ -1 +1 @@ -Subproject commit a2971be1ee44a4cd49c2d9f5a88830cce9560a6b +Subproject commit 9f5d642f682e13d67c0266b4250f8e54f484be8b From 8a32748a5b1d9cafed54e603e091c0e14d080dae Mon Sep 17 00:00:00 2001 From: ashhanai Date: Thu, 15 Feb 2024 11:47:01 -0100 Subject: [PATCH 013/129] build(open-zeppelin): increase open-zeppelin version to v4.9.5 --- lib/openzeppelin-contracts | 2 +- script/PWN.s.sol | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts index 4e5b119..bd325d5 160000 --- a/lib/openzeppelin-contracts +++ b/lib/openzeppelin-contracts @@ -1 +1 @@ -Subproject commit 4e5b11919e91b18b6683b6f49a1b4fdede579969 +Subproject commit bd325d56b4c62c9c5c1aff048c37c6bb18ac0290 diff --git a/script/PWN.s.sol b/script/PWN.s.sol index 73d89e1..741efd3 100644 --- a/script/PWN.s.sol +++ b/script/PWN.s.sol @@ -3,7 +3,8 @@ pragma solidity 0.8.16; import "forge-std/Script.sol"; -import "openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import { TransparentUpgradeableProxy, ITransparentUpgradeableProxy } +from "openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; import { GnosisSafeLike, GnosisSafeUtils } from "./lib/GnosisSafeUtils.sol"; @@ -127,7 +128,7 @@ forge script script/PWN.s.sol:Deploy \ vm.stopBroadcast(); vm.broadcast(initialConfigHelper); - TransparentUpgradeableProxy(payable(address(config))).changeAdmin(protocolSafe); + ITransparentUpgradeableProxy(address(config)).changeAdmin(protocolSafe); vm.startBroadcast(); From a735e3572129dced1b47bd42e4429739c93c84de Mon Sep 17 00:00:00 2001 From: ashhanai Date: Fri, 16 Feb 2024 16:39:36 -0100 Subject: [PATCH 014/129] feat(category-registry): check passed asset category in category registry --- deployments.json | 39 +++++++++++++------- src/Deployments.sol | 6 +++ src/loan/terms/simple/loan/PWNSimpleLoan.sol | 21 ++++++----- test/helper/DeploymentTest.t.sol | 9 ++++- test/unit/PWNSimpleLoan.t.sol | 32 ++++++++++------ 5 files changed, 73 insertions(+), 34 deletions(-) diff --git a/deployments.json b/deployments.json index b3017aa..740a639 100644 --- a/deployments.json +++ b/deployments.json @@ -19,7 +19,8 @@ "simpleLoan": "0x57c88D78f6D08b5c88b4A3b7BbB0C1AA34c3280A", "simpleLoanSimpleOffer": "0x5E551f09b8d1353075A1FF3B484Ee688aCAc02F6", "simpleLoanListOffer": "0xDA027058708961Be3676daEB68Fde1758B210065", - "simpleLoanSimpleRequest": "0x9Cb87eC6448299aBc326F32d60E191Ef32Ab225D" + "simpleLoanSimpleRequest": "0x9Cb87eC6448299aBc326F32d60E191Ef32Ab225D", + "categoryRegistry": "0x0000000000000000000000000000000000000000" }, "5": { "dao": "0x0000000000000000000000000000000000000000", @@ -39,7 +40,8 @@ "simpleLoan": "0x57c88D78f6D08b5c88b4A3b7BbB0C1AA34c3280A", "simpleLoanSimpleOffer": "0x5E551f09b8d1353075A1FF3B484Ee688aCAc02F6", "simpleLoanListOffer": "0xDA027058708961Be3676daEB68Fde1758B210065", - "simpleLoanSimpleRequest": "0x9Cb87eC6448299aBc326F32d60E191Ef32Ab225D" + "simpleLoanSimpleRequest": "0x9Cb87eC6448299aBc326F32d60E191Ef32Ab225D", + "categoryRegistry": "0x0000000000000000000000000000000000000000" }, "10": { "dao": "0x0000000000000000000000000000000000000000", @@ -59,7 +61,8 @@ "simpleLoan": "0x4188C513fd94B0458715287570c832d9560bc08a", "simpleLoanSimpleOffer": "0x5E551f09b8d1353075A1FF3B484Ee688aCAc02F6", "simpleLoanListOffer": "0xDA027058708961Be3676daEB68Fde1758B210065", - "simpleLoanSimpleRequest": "0x9Cb87eC6448299aBc326F32d60E191Ef32Ab225D" + "simpleLoanSimpleRequest": "0x9Cb87eC6448299aBc326F32d60E191Ef32Ab225D", + "categoryRegistry": "0x0000000000000000000000000000000000000000" }, "25": { "dao": "0x0000000000000000000000000000000000000000", @@ -79,7 +82,8 @@ "simpleLoan": "0x4188C513fd94B0458715287570c832d9560bc08a", "simpleLoanSimpleOffer": "0x5E551f09b8d1353075A1FF3B484Ee688aCAc02F6", "simpleLoanListOffer": "0xDA027058708961Be3676daEB68Fde1758B210065", - "simpleLoanSimpleRequest": "0x9Cb87eC6448299aBc326F32d60E191Ef32Ab225D" + "simpleLoanSimpleRequest": "0x9Cb87eC6448299aBc326F32d60E191Ef32Ab225D", + "categoryRegistry": "0x0000000000000000000000000000000000000000" }, "56": { "dao": "0x0000000000000000000000000000000000000000", @@ -99,7 +103,8 @@ "simpleLoan": "0x57c88D78f6D08b5c88b4A3b7BbB0C1AA34c3280A", "simpleLoanSimpleOffer": "0x5E551f09b8d1353075A1FF3B484Ee688aCAc02F6", "simpleLoanListOffer": "0xDA027058708961Be3676daEB68Fde1758B210065", - "simpleLoanSimpleRequest": "0x9Cb87eC6448299aBc326F32d60E191Ef32Ab225D" + "simpleLoanSimpleRequest": "0x9Cb87eC6448299aBc326F32d60E191Ef32Ab225D", + "categoryRegistry": "0x0000000000000000000000000000000000000000" }, "137": { "dao": "0x0000000000000000000000000000000000000000", @@ -119,7 +124,8 @@ "simpleLoan": "0x57c88D78f6D08b5c88b4A3b7BbB0C1AA34c3280A", "simpleLoanSimpleOffer": "0x5E551f09b8d1353075A1FF3B484Ee688aCAc02F6", "simpleLoanListOffer": "0xDA027058708961Be3676daEB68Fde1758B210065", - "simpleLoanSimpleRequest": "0x9Cb87eC6448299aBc326F32d60E191Ef32Ab225D" + "simpleLoanSimpleRequest": "0x9Cb87eC6448299aBc326F32d60E191Ef32Ab225D", + "categoryRegistry": "0x0000000000000000000000000000000000000000" }, "338": { "dao": "0x0000000000000000000000000000000000000000", @@ -139,7 +145,8 @@ "simpleLoan": "0x4188C513fd94B0458715287570c832d9560bc08a", "simpleLoanSimpleOffer": "0x5E551f09b8d1353075A1FF3B484Ee688aCAc02F6", "simpleLoanListOffer": "0xDA027058708961Be3676daEB68Fde1758B210065", - "simpleLoanSimpleRequest": "0x9Cb87eC6448299aBc326F32d60E191Ef32Ab225D" + "simpleLoanSimpleRequest": "0x9Cb87eC6448299aBc326F32d60E191Ef32Ab225D", + "categoryRegistry": "0x0000000000000000000000000000000000000000" }, "5000": { "dao": "0x0000000000000000000000000000000000000000", @@ -159,7 +166,8 @@ "simpleLoan": "0x4188C513fd94B0458715287570c832d9560bc08a", "simpleLoanSimpleOffer": "0x5E551f09b8d1353075A1FF3B484Ee688aCAc02F6", "simpleLoanListOffer": "0xDA027058708961Be3676daEB68Fde1758B210065", - "simpleLoanSimpleRequest": "0x9Cb87eC6448299aBc326F32d60E191Ef32Ab225D" + "simpleLoanSimpleRequest": "0x9Cb87eC6448299aBc326F32d60E191Ef32Ab225D", + "categoryRegistry": "0x0000000000000000000000000000000000000000" }, "5001": { "dao": "0x0000000000000000000000000000000000000000", @@ -179,7 +187,8 @@ "simpleLoan": "0x4188C513fd94B0458715287570c832d9560bc08a", "simpleLoanSimpleOffer": "0x5E551f09b8d1353075A1FF3B484Ee688aCAc02F6", "simpleLoanListOffer": "0xDA027058708961Be3676daEB68Fde1758B210065", - "simpleLoanSimpleRequest": "0x9Cb87eC6448299aBc326F32d60E191Ef32Ab225D" + "simpleLoanSimpleRequest": "0x9Cb87eC6448299aBc326F32d60E191Ef32Ab225D", + "categoryRegistry": "0x0000000000000000000000000000000000000000" }, "8453": { "dao": "0x0000000000000000000000000000000000000000", @@ -199,7 +208,8 @@ "simpleLoan": "0x4188C513fd94B0458715287570c832d9560bc08a", "simpleLoanSimpleOffer": "0x5E551f09b8d1353075A1FF3B484Ee688aCAc02F6", "simpleLoanListOffer": "0xDA027058708961Be3676daEB68Fde1758B210065", - "simpleLoanSimpleRequest": "0x9Cb87eC6448299aBc326F32d60E191Ef32Ab225D" + "simpleLoanSimpleRequest": "0x9Cb87eC6448299aBc326F32d60E191Ef32Ab225D", + "categoryRegistry": "0x0000000000000000000000000000000000000000" }, "42161": { "dao": "0x0000000000000000000000000000000000000000", @@ -219,7 +229,8 @@ "simpleLoan": "0x57c88D78f6D08b5c88b4A3b7BbB0C1AA34c3280A", "simpleLoanSimpleOffer": "0x5E551f09b8d1353075A1FF3B484Ee688aCAc02F6", "simpleLoanListOffer": "0xDA027058708961Be3676daEB68Fde1758B210065", - "simpleLoanSimpleRequest": "0x9Cb87eC6448299aBc326F32d60E191Ef32Ab225D" + "simpleLoanSimpleRequest": "0x9Cb87eC6448299aBc326F32d60E191Ef32Ab225D", + "categoryRegistry": "0x0000000000000000000000000000000000000000" }, "84531": { "dao": "0x0000000000000000000000000000000000000000", @@ -239,7 +250,8 @@ "simpleLoan": "0x4188C513fd94B0458715287570c832d9560bc08a", "simpleLoanSimpleOffer": "0x5E551f09b8d1353075A1FF3B484Ee688aCAc02F6", "simpleLoanListOffer": "0xDA027058708961Be3676daEB68Fde1758B210065", - "simpleLoanSimpleRequest": "0x9Cb87eC6448299aBc326F32d60E191Ef32Ab225D" + "simpleLoanSimpleRequest": "0x9Cb87eC6448299aBc326F32d60E191Ef32Ab225D", + "categoryRegistry": "0x0000000000000000000000000000000000000000" }, "11155111": { "dao": "0x0000000000000000000000000000000000000000", @@ -259,7 +271,8 @@ "simpleLoan": "0x4188C513fd94B0458715287570c832d9560bc08a", "simpleLoanSimpleOffer": "0x5E551f09b8d1353075A1FF3B484Ee688aCAc02F6", "simpleLoanListOffer": "0xDA027058708961Be3676daEB68Fde1758B210065", - "simpleLoanSimpleRequest": "0x9Cb87eC6448299aBc326F32d60E191Ef32Ab225D" + "simpleLoanSimpleRequest": "0x9Cb87eC6448299aBc326F32d60E191Ef32Ab225D", + "categoryRegistry": "0x0000000000000000000000000000000000000000" } } } diff --git a/src/Deployments.sol b/src/Deployments.sol index 3c86e84..a3e51e5 100644 --- a/src/Deployments.sol +++ b/src/Deployments.sol @@ -4,6 +4,8 @@ pragma solidity 0.8.16; import "forge-std/StdJson.sol"; import "forge-std/Base.sol"; +import "MultiToken/interfaces/IMultiTokenCategoryRegistry.sol"; + import "openzeppelin-contracts/contracts/utils/Strings.sol"; import "@pwn/config/PWNConfig.sol"; @@ -27,6 +29,7 @@ abstract contract Deployments is CommonBase { // Properties need to be in alphabetical order struct Deployment { + IMultiTokenCategoryRegistry categoryRegistry; PWNConfig config; PWNConfig configSingleton; address dao; @@ -57,6 +60,8 @@ abstract contract Deployments is CommonBase { address daoSafe; address feeCollector; + IMultiTokenCategoryRegistry categoryRegistry; + IPWNDeployer deployer; PWNHub hub; PWNConfig configSingleton; @@ -99,6 +104,7 @@ abstract contract Deployments is CommonBase { simpleLoanSimpleOffer = deployment.simpleLoanSimpleOffer; simpleLoanListOffer = deployment.simpleLoanListOffer; simpleLoanSimpleRequest = deployment.simpleLoanSimpleRequest; + categoryRegistry = deployment.categoryRegistry; } else { _protocolNotDeployedOnSelectedChain(); } diff --git a/src/loan/terms/simple/loan/PWNSimpleLoan.sol b/src/loan/terms/simple/loan/PWNSimpleLoan.sol index 01c595e..0187b5e 100644 --- a/src/loan/terms/simple/loan/PWNSimpleLoan.sol +++ b/src/loan/terms/simple/loan/PWNSimpleLoan.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; -import "MultiToken/MultiToken.sol"; +import { MultiToken, IMultiTokenCategoryRegistry } from "MultiToken/MultiToken.sol"; import "@pwn/config/PWNConfig.sol"; import "@pwn/hub/PWNHub.sol"; @@ -29,9 +29,11 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { |* # VARIABLES & CONSTANTS DEFINITIONS *| |*----------------------------------------------------------*/ - PWNHub immutable internal hub; - PWNLOAN immutable internal loanToken; - PWNConfig immutable internal config; + PWNHub internal immutable hub; + PWNLOAN internal immutable loanToken; + PWNConfig internal immutable config; + + IMultiTokenCategoryRegistry public immutable categoryRegistry; /** * @notice Struct defining a simple loan. @@ -88,10 +90,11 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { |* # CONSTRUCTOR *| |*----------------------------------------------------------*/ - constructor(address _hub, address _loanToken, address _config) { + constructor(address _hub, address _loanToken, address _config, address _categoryRegistry) { hub = PWNHub(_hub); loanToken = PWNLOAN(_loanToken); config = PWNConfig(_config); + categoryRegistry = IMultiTokenCategoryRegistry(_categoryRegistry); } @@ -117,22 +120,22 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { bytes calldata collateralPermit ) external returns (uint256 loanId) { // Check that loan terms factory contract is tagged in PWNHub - if (hub.hasTag(loanTermsFactoryContract, PWNHubTags.SIMPLE_LOAN_TERMS_FACTORY) == false) + if (!hub.hasTag(loanTermsFactoryContract, PWNHubTags.SIMPLE_LOAN_TERMS_FACTORY)) revert CallerMissingHubTag(PWNHubTags.SIMPLE_LOAN_TERMS_FACTORY); // Build PWNLOANTerms.Simple by loan factory - (PWNLOANTerms.Simple memory loanTerms, bytes32 factoryDataHash) = PWNSimpleLoanTermsFactory(loanTermsFactoryContract).createLOANTerms({ + (loanTerms, factoryDataHash) = PWNSimpleLoanTermsFactory(loanTermsFactoryContract).createLOANTerms({ caller: msg.sender, factoryData: loanTermsFactoryData, signature: signature }); // Check loan asset validity - if (MultiToken.isValid(loanTerms.asset) == false) + if (!MultiToken.isValid(loanTerms.asset, categoryRegistry)) revert InvalidLoanAsset(); // Check collateral validity - if (MultiToken.isValid(loanTerms.collateral) == false) + if (!MultiToken.isValid(loanTerms.collateral, categoryRegistry)) revert InvalidCollateralAsset(); // Mint LOAN token for lender diff --git a/test/helper/DeploymentTest.t.sol b/test/helper/DeploymentTest.t.sol index 3d483f9..072b23f 100644 --- a/test/helper/DeploymentTest.t.sol +++ b/test/helper/DeploymentTest.t.sol @@ -3,6 +3,9 @@ pragma solidity 0.8.16; import "forge-std/Test.sol"; +import "MultiToken/MultiTokenCategoryRegistry.sol"; +import "MultiToken/interfaces/IMultiTokenCategoryRegistry.sol"; + import "openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; import "@pwn/Deployments.sol"; @@ -19,6 +22,10 @@ abstract contract DeploymentTest is Deployments, Test { daoSafe = makeAddr("daoSafe"); feeCollector = makeAddr("feeCollector"); + // Deploy category registry + vm.prank(protocolSafe); + categoryRegistry = IMultiTokenCategoryRegistry(new MultiTokenCategoryRegistry()); + // Deploy protocol configSingleton = new PWNConfig(); TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy( @@ -32,7 +39,7 @@ abstract contract DeploymentTest is Deployments, Test { hub = new PWNHub(); loanToken = new PWNLOAN(address(hub)); - simpleLoan = new PWNSimpleLoan(address(hub), address(loanToken), address(config)); + simpleLoan = new PWNSimpleLoan(address(hub), address(loanToken), address(config), address(categoryRegistry)); revokedOfferNonce = new PWNRevokedNonce(address(hub), PWNHubTags.LOAN_OFFER); simpleLoanSimpleOffer = new PWNSimpleLoanSimpleOffer(address(hub), address(revokedOfferNonce)); diff --git a/test/unit/PWNSimpleLoan.t.sol b/test/unit/PWNSimpleLoan.t.sol index ece46e0..fd5e00f 100644 --- a/test/unit/PWNSimpleLoan.t.sol +++ b/test/unit/PWNSimpleLoan.t.sol @@ -24,6 +24,7 @@ abstract contract PWNSimpleLoanTest is Test { address hub = makeAddr("hub"); address loanToken = makeAddr("loanToken"); address config = makeAddr("config"); + address categoryRegistry = makeAddr("categoryRegistry"); address feeCollector = makeAddr("feeCollector"); address alice = makeAddr("alice"); address loanFactory = makeAddr("loanFactory"); @@ -55,7 +56,7 @@ abstract contract PWNSimpleLoanTest is Test { } function setUp() virtual public { - loan = new PWNSimpleLoan(hub, loanToken, config); + loan = new PWNSimpleLoan(hub, loanToken, config, categoryRegistry); fungibleAsset = new T20(); nonFungibleAsset = new T721(); @@ -88,7 +89,7 @@ abstract contract PWNSimpleLoanTest is Test { expiration: uint40(block.timestamp + 40039), loanAssetAddress: address(fungibleAsset), loanRepayAmount: 6731, - collateral: MultiToken.Asset(MultiToken.Category.ERC721, address(nonFungibleAsset), 2, 0), + collateral: MultiToken.ERC721(address(nonFungibleAsset), 2), originalLender: lender }); @@ -96,8 +97,8 @@ abstract contract PWNSimpleLoanTest is Test { lender: lender, borrower: borrower, expiration: uint40(block.timestamp + 40039), - collateral: MultiToken.Asset(MultiToken.Category.ERC721, address(nonFungibleAsset), 2, 0), - asset: MultiToken.Asset(MultiToken.Category.ERC20, address(fungibleAsset), 0, 100), + collateral: MultiToken.ERC721(address(nonFungibleAsset), 2), + asset: MultiToken.ERC20(address(fungibleAsset), 100), loanRepayAmount: 6731 }); @@ -107,7 +108,7 @@ abstract contract PWNSimpleLoanTest is Test { expiration: 0, loanAssetAddress: address(0), loanRepayAmount: 0, - collateral: MultiToken.Asset(MultiToken.Category.ERC20, address(0), 0, 0), + collateral: MultiToken.Asset(MultiToken.Category(0), address(0), 0, 0), originalLender: address(0) }); @@ -118,6 +119,11 @@ abstract contract PWNSimpleLoanTest is Test { abi.encodeWithSignature("permit(address,address,uint256,uint256,uint8,bytes32,bytes32)"), abi.encode() ); + vm.mockCall( + categoryRegistry, + abi.encodeWithSignature("registeredCategoryValue(address)"), + abi.encode(type(uint8).max) + ); } @@ -262,8 +268,11 @@ contract PWNSimpleLoan_CreateLoan_Test is PWNSimpleLoanTest { } function test_shouldFailWhenLoanAssetIsInvalid() external { - simpleLoanTerms.asset.assetAddress = address(nonFungibleAsset); - simpleLoanTerms.asset.amount = 100; + vm.mockCall( + categoryRegistry, + abi.encodeWithSignature("registeredCategoryValue(address)", simpleLoanTerms.asset.assetAddress), + abi.encode(1) + ); vm.mockCall( loanFactory, @@ -276,10 +285,11 @@ contract PWNSimpleLoan_CreateLoan_Test is PWNSimpleLoanTest { } function test_shouldFailWhenCollateralAssetIsInvalid() external { - simpleLoanTerms.collateral.category = MultiToken.Category.ERC721; - simpleLoanTerms.collateral.assetAddress = address(nonFungibleAsset); - simpleLoanTerms.collateral.id = 123; - simpleLoanTerms.collateral.amount = 100; // Invalid, ERC721 has to have amount = 0 + vm.mockCall( + categoryRegistry, + abi.encodeWithSignature("registeredCategoryValue(address)", simpleLoanTerms.collateral.assetAddress), + abi.encode(0) + ); vm.mockCall( loanFactory, From 2df2ece4b99871485a455c176862c810b7a67531 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Wed, 21 Feb 2024 14:50:42 +0000 Subject: [PATCH 015/129] refactor(fee-calculator): use OZ Math.mulDiv --- src/loan/lib/PWNFeeCalculator.sol | 11 ++++------- test/unit/PWNFeeCalculator.t.sol | 2 +- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/loan/lib/PWNFeeCalculator.sol b/src/loan/lib/PWNFeeCalculator.sol index 90b8586..83d963c 100644 --- a/src/loan/lib/PWNFeeCalculator.sol +++ b/src/loan/lib/PWNFeeCalculator.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; +import { Math } from "openzeppelin-contracts/contracts/utils/math/Math.sol"; + /** * @title PWN Fee Calculator @@ -8,7 +10,7 @@ pragma solidity 0.8.16; */ library PWNFeeCalculator { - string internal constant VERSION = "1.0"; + string internal constant VERSION = "1.1"; /** * @notice Compute fee amount. @@ -21,12 +23,7 @@ library PWNFeeCalculator { if (fee == 0) return (0, loanAmount); - unchecked { - if ((loanAmount * fee) / fee == loanAmount) - feeAmount = loanAmount * uint256(fee) / 1e4; - else - feeAmount = loanAmount / 1e4 * uint256(fee); - } + feeAmount = Math.mulDiv(loanAmount, fee, 1e4); newLoanAmount = loanAmount - feeAmount; } diff --git a/test/unit/PWNFeeCalculator.t.sol b/test/unit/PWNFeeCalculator.t.sol index 2ce0528..2986a32 100644 --- a/test/unit/PWNFeeCalculator.t.sol +++ b/test/unit/PWNFeeCalculator.t.sol @@ -37,7 +37,7 @@ contract PWNFeeCalculator_CalculateFeeAmount_Test is Test { } function testFuzz_feeAndNewLoanAmountAreEqToOriginalLoanAmount(uint16 fee, uint256 loanAmount) external { - vm.assume(fee < 10001); + fee = fee % 10001; (uint256 feeAmount, uint256 newLoanAmount) = PWNFeeCalculator.calculateFeeAmount(fee, loanAmount); assertEq(loanAmount, feeAmount + newLoanAmount); From 9adbe53188ed69875af2e41c3a6deb6b6483d19f Mon Sep 17 00:00:00 2001 From: ashhanai Date: Wed, 21 Feb 2024 15:22:56 +0000 Subject: [PATCH 016/129] feat(refinance): enable borrower to refinance a running loan --- src/PWNErrors.sol | 1 + src/loan/terms/simple/loan/PWNSimpleLoan.sol | 490 +++++++++--- test/unit/PWNSimpleLoan.t.sol | 765 +++++++++++++++++-- 3 files changed, 1093 insertions(+), 163 deletions(-) diff --git a/src/PWNErrors.sol b/src/PWNErrors.sol index f93adda..1867ee1 100644 --- a/src/PWNErrors.sol +++ b/src/PWNErrors.sol @@ -11,6 +11,7 @@ error InvalidLoanStatus(uint256); error NonExistingLoan(); error CallerNotLOANTokenHolder(); error InvalidExtendedExpirationDate(); +error BorrowerMismatch(address currentBorrower, address newBorrower); // Invalid asset error InvalidLoanAsset(); diff --git a/src/loan/terms/simple/loan/PWNSimpleLoan.sol b/src/loan/terms/simple/loan/PWNSimpleLoan.sol index 0187b5e..05b339e 100644 --- a/src/loan/terms/simple/loan/PWNSimpleLoan.sol +++ b/src/loan/terms/simple/loan/PWNSimpleLoan.sol @@ -3,6 +3,8 @@ pragma solidity 0.8.16; import { MultiToken, IMultiTokenCategoryRegistry } from "MultiToken/MultiToken.sol"; +import { Math } from "openzeppelin-contracts/contracts/utils/math/Math.sol"; + import "@pwn/config/PWNConfig.sol"; import "@pwn/hub/PWNHub.sol"; import "@pwn/hub/PWNHubTags.sol"; @@ -119,6 +121,39 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { bytes calldata loanAssetPermit, bytes calldata collateralPermit ) external returns (uint256 loanId) { + // Create loan terms or revert if factory contract is not tagged in PWN Hub + (PWNLOANTerms.Simple memory loanTerms, bytes32 factoryDataHash) + = _createLoanTerms(loanTermsFactoryContract, loanTermsFactoryData, signature); + + // Check loan asset validity + if (!MultiToken.isValid(loanTerms.asset, categoryRegistry)) + revert InvalidLoanAsset(); + + // Check collateral validity + if (!MultiToken.isValid(loanTerms.collateral, categoryRegistry)) + revert InvalidCollateralAsset(); + + // Create a new loan + loanId = _createLoan(loanTerms, factoryDataHash, loanTermsFactoryContract); + + // Transfer collateral to Vault and loan asset to borrower + _settleNewLoan(loanTerms, loanAssetPermit, collateralPermit); + } + + /** + * @notice Create a loan terms by a loan terms factory contract. + * @dev The function will revert if the loan terms factory contract is not tagged in PWN Hub. + * @param loanTermsFactoryContract Address of a loan terms factory contract. Need to have `SIMPLE_LOAN_TERMS_FACTORY` tag in PWN Hub. + * @param loanTermsFactoryData Encoded data for a loan terms factory. + * @param signature Signed loan factory data. Could be empty if an offer / request has been made via on-chain transaction. + * @return loanTerms Loan terms struct. + * @return factoryDataHash Hash of the factory data. + */ + function _createLoanTerms( + address loanTermsFactoryContract, + bytes calldata loanTermsFactoryData, + bytes calldata signature + ) private returns (PWNLOANTerms.Simple memory loanTerms, bytes32 factoryDataHash) { // Check that loan terms factory contract is tagged in PWNHub if (!hub.hasTag(loanTermsFactoryContract, PWNHubTags.SIMPLE_LOAN_TERMS_FACTORY)) revert CallerMissingHubTag(PWNHubTags.SIMPLE_LOAN_TERMS_FACTORY); @@ -129,15 +164,20 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { factoryData: loanTermsFactoryData, signature: signature }); + } - // Check loan asset validity - if (!MultiToken.isValid(loanTerms.asset, categoryRegistry)) - revert InvalidLoanAsset(); - - // Check collateral validity - if (!MultiToken.isValid(loanTerms.collateral, categoryRegistry)) - revert InvalidCollateralAsset(); - + /** + * @notice Store a new loan in the contract state, mints new LOAN token, and emit a `LOANCreated` event. + * @param loanTerms Loan terms struct. + * @param factoryDataHash Hash of the factory data. + * @param loanTermsFactoryContract Address of a loan terms factory contract. + * @return loanId Id of a newly minted LOAN token. + */ + function _createLoan( + PWNLOANTerms.Simple memory loanTerms, + bytes32 factoryDataHash, + address loanTermsFactoryContract + ) private returns (uint256 loanId) { // Mint LOAN token for lender loanId = loanToken.mint(loanTerms.lender); @@ -151,28 +191,43 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { loan.collateral = loanTerms.collateral; loan.originalLender = loanTerms.lender; - emit LOANCreated(loanId, loanTerms, factoryDataHash, loanTermsFactoryContract); + emit LOANCreated({ + loanId: loanId, + terms: loanTerms, + factoryDataHash: factoryDataHash, + factoryAddress: loanTermsFactoryContract + }); + } + /** + * @notice Transfer collateral to Vault and loan asset to borrower. + * @dev The function assumes a prior token approval to a contract address or signed permits. + * @param loanTerms Loan terms struct. + * @param loanAssetPermit Permit data for a loan asset signed by a lender. + * @param collateralPermit Permit data for a collateral signed by a borrower. + */ + function _settleNewLoan( + PWNLOANTerms.Simple memory loanTerms, + bytes calldata loanAssetPermit, + bytes calldata collateralPermit + ) private { // Transfer collateral to Vault _permit(loanTerms.collateral, loanTerms.borrower, collateralPermit); _pull(loanTerms.collateral, loanTerms.borrower); - // Permit spending if permit data provided + // Permit loan asset spending if permit provided _permit(loanTerms.asset, loanTerms.lender, loanAssetPermit); - uint16 fee = config.fee(); - if (fee > 0) { - // Compute fee size - (uint256 feeAmount, uint256 newLoanAmount) = PWNFeeCalculator.calculateFeeAmount(fee, loanTerms.asset.amount); + // Collect fee if any and update loan asset amount + (uint256 feeAmount, uint256 newLoanAmount) + = PWNFeeCalculator.calculateFeeAmount(config.fee(), loanTerms.asset.amount); + if (feeAmount > 0) { + // Transfer fee amount to fee collector + loanTerms.asset.amount = feeAmount; + _pushFrom(loanTerms.asset, loanTerms.lender, config.feeCollector()); - if (feeAmount > 0) { - // Transfer fee amount to fee collector - loanTerms.asset.amount = feeAmount; - _pushFrom(loanTerms.asset, loanTerms.lender, config.feeCollector()); - - // Set new loan amount value - loanTerms.asset.amount = newLoanAmount; - } + // Set new loan amount value + loanTerms.asset.amount = newLoanAmount; } // Transfer loan asset to borrower @@ -180,6 +235,201 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { } + /*----------------------------------------------------------*| + |* # 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 loanId Id of a loan that is being refinanced. + * @param loanTermsFactoryContract Address of a loan terms factory contract. Need to have `SIMPLE_LOAN_TERMS_FACTORY` tag in PWN Hub. + * @param loanTermsFactoryData Encoded data for a loan terms factory. + * @param signature Signed loan factory data. Could be empty if an offer / request has been made via on-chain transaction. + * @param lenderLoanAssetPermit Permit data for a loan asset signed by a lender. + * @param borrowerLoanAssetPermit Permit data for a loan asset signed by a borrower. + * @return refinancedLoanId Id of the refinanced LOAN token. + */ + function refinanceLOAN( + uint256 loanId, + address loanTermsFactoryContract, + bytes calldata loanTermsFactoryData, + bytes calldata signature, + bytes calldata lenderLoanAssetPermit, + bytes calldata borrowerLoanAssetPermit + ) external returns (uint256 refinancedLoanId) { + LOAN storage loan = LOANs[loanId]; + + // Check that the original loan can be repaid, revert if not + _checkLoanCanBeRepaid(loan.status, loan.expiration); + + // Create loan terms or revert if factory contract is not tagged in PWN Hub + (PWNLOANTerms.Simple memory loanTerms, bytes32 factoryDataHash) + = _createLoanTerms(loanTermsFactoryContract, loanTermsFactoryData, signature); + + // Check loan terms validity, revert if not + _checkRefinanceLoanTerms(loan, loanTerms); + + // Create a new loan + refinancedLoanId = _createLoan(loanTerms, factoryDataHash, loanTermsFactoryContract); + + // Refinance the original loan + _refinanceOriginalLoan( + loanId, + loan.loanRepayAmount, + loanTerms, + lenderLoanAssetPermit, + borrowerLoanAssetPermit + ); + } + + /** + * @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 loan Original loan struct. + * @param loanTerms Refinancing loan terms struct. + */ + function _checkRefinanceLoanTerms(LOAN storage loan, PWNLOANTerms.Simple memory loanTerms) private { + // Check that the loan 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.loanAssetAddress != loanTerms.asset.assetAddress || + loanTerms.asset.amount == 0 + ) revert InvalidLoanAsset(); + + // 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 InvalidCollateralAsset(); + + // Check that the borrower is the same as in the original loan + if (loan.borrower != loanTerms.borrower) { + revert BorrowerMismatch({ + 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, + * 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 loanId Id of a loan that is being refinanced. + * @param loanRepayAmount Amount of the original loan to be repaid. + * @param loanTerms Loan terms struct. + * @param lenderLoanAssetPermit Permit data for a loan asset signed by a lender. + * @param borrowerLoanAssetPermit Permit data for a loan asset signed by a borrower. + */ + function _refinanceOriginalLoan( + uint256 loanId, + uint256 loanRepayAmount, + PWNLOANTerms.Simple memory loanTerms, + bytes calldata lenderLoanAssetPermit, + bytes calldata borrowerLoanAssetPermit + ) private { + // Delete or update the original loan + (bool repayLoanDirectly, address loanOwner) = _deleteOrUpdateRepaidLoan(loanId); + + // Repay the original loan and transfer the surplus to the borrower if any + _settleLoanRefinance({ + repayLoanDirectly: repayLoanDirectly, + loanOwner: loanOwner, + loanRepayAmount: loanRepayAmount, + loanTerms: loanTerms, + lenderPermit: lenderLoanAssetPermit, + borrowerPermit: borrowerLoanAssetPermit + }); + } + + /** + * @notice Settle the refinanced loan. 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 repayLoanDirectly If the loan can be repaid directly to the current LOAN owner. + * @param loanOwner Address of the current LOAN owner. + * @param loanRepayAmount Amount of the original loan to be repaid. + * @param loanTerms Loan terms struct. + * @param lenderPermit Permit data for a loan asset signed by a lender. + * @param borrowerPermit Permit data for a loan asset signed by a borrower. + */ + function _settleLoanRefinance( + bool repayLoanDirectly, + address loanOwner, + uint256 loanRepayAmount, + PWNLOANTerms.Simple memory loanTerms, + bytes calldata lenderPermit, + bytes calldata borrowerPermit + ) private { + MultiToken.Asset memory loanAssetHelper = MultiToken.ERC20(loanTerms.asset.assetAddress, 0); + + // Compute fee size + (uint256 feeAmount, uint256 newLoanAmount) + = PWNFeeCalculator.calculateFeeAmount(config.fee(), loanTerms.asset.amount); + + // Set new loan amount value + loanTerms.asset.amount = newLoanAmount; + + // Note: At this point `loanTerms` struct has loan asset amount deducted by the fee amount. + + // Permit lenders loan asset spending if permit provided + loanAssetHelper.amount = loanTerms.asset.amount + feeAmount; // Permit the whole loan amount + fee + loanAssetHelper.amount -= loanTerms.lender == loanOwner // Permit only the surplus transfer + fee + ? Math.min(loanRepayAmount, loanTerms.asset.amount) : 0; + if (loanAssetHelper.amount > 0) + _permit(loanAssetHelper, loanTerms.lender, lenderPermit); + + // Collect fees + if (feeAmount > 0) { + loanAssetHelper.amount = feeAmount; + _pushFrom(loanAssetHelper, loanTerms.lender, config.feeCollector()); + } + + // If the new lender is the LOAN token owner, don't execute the transfer at all, + // it would make transfer from the same address to the same address + if (loanTerms.lender != loanOwner) { + loanAssetHelper.amount = Math.min(loanRepayAmount, loanTerms.asset.amount); + _transferLoanRepayment({ + repayLoanDirectly: repayLoanDirectly, + asset: loanAssetHelper, + repayingAddress: loanTerms.lender, + currentLoanOwner: loanOwner + }); + } + + if (loanTerms.asset.amount >= loanRepayAmount) { + // New loan covers the whole original loan, transfer surplus to the borrower if any + uint256 surplus = loanTerms.asset.amount - loanRepayAmount; + if (surplus > 0) { + loanAssetHelper.amount = surplus; + _pushFrom(loanAssetHelper, loanTerms.lender, loanTerms.borrower); + } + } else { + // Permit borrowers loan asset spending if permit provided + loanAssetHelper.amount = loanRepayAmount - loanTerms.asset.amount; + _permit(loanAssetHelper, loanTerms.borrower, borrowerPermit); + + // New loan covers only part of the original loan, borrower needs to contribute + _transferLoanRepayment({ + repayLoanDirectly: repayLoanDirectly || loanTerms.lender == loanOwner, + asset: loanAssetHelper, + repayingAddress: loanTerms.borrower, + currentLoanOwner: loanOwner + }); + } + } + + /*----------------------------------------------------------*| |* # REPAY LOAN *| |*----------------------------------------------------------*/ @@ -196,56 +446,115 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { uint256 loanId, bytes calldata loanAssetPermit ) external { - LOAN storage loan = LOANs[loanId]; - uint8 status = loan.status; + LOAN memory loan = LOANs[loanId]; - // Check that loan is not from a different loan contract - if (status == 0) - revert NonExistingLoan(); - // Check that loan is running - else if (status != 2) - revert InvalidLoanStatus(status); + _checkLoanCanBeRepaid(loan.status, loan.expiration); - // Check that loan is not expired - if (loan.expiration <= block.timestamp) - revert LoanDefaulted(loan.expiration); - - MultiToken.Asset memory repayLoanAsset = MultiToken.Asset({ - category: MultiToken.Category.ERC20, - assetAddress: loan.loanAssetAddress, - id: 0, - amount: loan.loanRepayAmount + (bool repayLoanDirectly, address loanOwner) = _deleteOrUpdateRepaidLoan(loanId); + + _settleLoanRepayment({ + repayLoanDirectly: repayLoanDirectly, + loanOwner: loanOwner, + repayingAddress: msg.sender, + borrower: loan.borrower, + repayLoanAsset: MultiToken.ERC20(loan.loanAssetAddress, loan.loanRepayAmount), + collateral: loan.collateral, + loanAssetPermit: loanAssetPermit }); - MultiToken.Asset memory collateral = loan.collateral; - address borrower = loan.borrower; - address originalLender = loan.originalLender; + } - _permit(repayLoanAsset, msg.sender, loanAssetPermit); + /** + * @notice Check if the loan can be repaid. + * @dev The function will revert if the loan cannot be repaid. + * @param status Loan status. + * @param expiration Loan expiration date. + */ + function _checkLoanCanBeRepaid(uint8 status, uint40 expiration) private view { + // Check that loan exists and is not from a different loan contract + if (status == 0) revert NonExistingLoan(); + // Check that loan is running + if (status != 2) revert InvalidLoanStatus(status); + // Check that loan is not expired + if (expiration <= block.timestamp) revert LoanDefaulted(expiration); + } - // Note: Assuming that it is safe to transfer the loan asset to the original lender because - // the lender was able to sign an offer or make a contract call, thus can handle incoming transfers. - bool immediateClaim = originalLender == loanToken.ownerOf(loanId); - if (immediateClaim) { + /** + * @notice Delete or update the original loan. + * @dev If the loan can be repaid directly to the current LOAN owner, + * the function will delete the loan and burn the LOAN token. + * If the loan cannot be repaid directly to the current LOAN owner, + * the function will move the loan to repaid state and wait for the lender to claim the repaid loan asset. + * @param loanId Id of a loan that is being repaid. + * @return repayLoanDirectly If the loan can be repaid directly to the current LOAN owner. + * @return loanOwner Address of the current LOAN owner. + */ + function _deleteOrUpdateRepaidLoan(uint256 loanId) private returns (bool repayLoanDirectly, address loanOwner) { + emit LOANPaidBack({ loanId: loanId }); + + // Note: Assuming that it is safe to transfer the loan asset to the original lender + // if the lender still owns the LOAN token because the lender was able to sign an offer + // or make a contract call, thus can handle incoming transfers. + loanOwner = loanToken.ownerOf(loanId); + repayLoanDirectly = LOANs[loanId].originalLender == loanOwner; + if (repayLoanDirectly) { // Delete loan data & burn LOAN token before calling safe transfer _deleteLoan(loanId); - // Transfer the repaid loan asset to the original lender - _pushFrom(repayLoanAsset, msg.sender, originalLender); + emit LOANClaimed({ loanId: loanId, defaulted: false }); } else { // Move loan to repaid state and wait for the lender to claim the repaid loan asset - loan.status = 3; - - // Transfer the repaid loan asset to the Vault - _pull(repayLoanAsset, msg.sender); + LOANs[loanId].status = 3; } + } + + /** + * @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 repayLoanAsset Loan asset to be repaid. + * @param collateral Collateral to be transferred back to the borrower. + * @param loanAssetPermit Permit data for a loan asset signed by a borrower. + */ + function _settleLoanRepayment( + bool repayLoanDirectly, + address loanOwner, + address repayingAddress, + address borrower, + MultiToken.Asset memory repayLoanAsset, + MultiToken.Asset memory collateral, + bytes calldata loanAssetPermit + ) private { + // Transfer loan asset to the original lender or to the Vault + _permit(repayLoanAsset, repayingAddress, loanAssetPermit); + _transferLoanRepayment(repayLoanDirectly, repayLoanAsset, repayingAddress, loanOwner); // Transfer collateral back to borrower _push(collateral, borrower); + } - emit LOANPaidBack(loanId); - - if (immediateClaim) - emit LOANClaimed(loanId, false); + /** + * @notice Transfer the repaid loan asset to the original lender or to the Vault. + * @param repayLoanDirectly If the loan can be repaid directly to the current LOAN owner. + * @param asset Asset to be repaid. + * @param repayingAddress Address of the account repaying the loan. + * @param currentLoanOwner Address of the current LOAN owner. + */ + function _transferLoanRepayment( + bool repayLoanDirectly, + MultiToken.Asset memory asset, + address repayingAddress, + address currentLoanOwner + ) private { + if (repayLoanDirectly) { + // Transfer the repaid loan asset to the LOAN token owner + _pushFrom(asset, repayingAddress, currentLoanOwner); + } else { + // Transfer the repaid loan asset to the Vault + _pull(asset, repayingAddress); + } } @@ -266,44 +575,46 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { if (loanToken.ownerOf(loanId) != msg.sender) revert CallerNotLOANTokenHolder(); - if (loan.status == 0) { + // Loan is not existing or from a different loan contract + if (loan.status == 0) revert NonExistingLoan(); - } // Loan has been paid back - else if (loan.status == 3) { - MultiToken.Asset memory loanAsset = MultiToken.Asset({ - category: MultiToken.Category.ERC20, - assetAddress: loan.loanAssetAddress, - id: 0, - amount: loan.loanRepayAmount - }); - - // Delete loan data & burn LOAN token before calling safe transfer - _deleteLoan(loanId); + else if (loan.status == 3) + _settleLoanClaim({ loanId: loanId, loanOwner: msg.sender, defaulted: false }); + // Loan is running but expired + else if (loan.status == 2 && loan.expiration <= block.timestamp) + _settleLoanClaim({ loanId: loanId, loanOwner: msg.sender, defaulted: true }); + // Loan is in wrong state + else + revert InvalidLoanStatus(loan.status); + } - // Transfer repaid loan to lender - _push(loanAsset, msg.sender); + /** + * @notice Settle the loan claim. + * @param loanId Id of a loan that is being claimed. + * @param loanOwner Address of the LOAN token holder. + * @param defaulted If the loan is defaulted. + */ + function _settleLoanClaim(uint256 loanId, address loanOwner, bool defaulted) private { + LOAN storage loan = LOANs[loanId]; - emit LOANClaimed(loanId, false); - } - // Loan is running but expired - else if (loan.status == 2 && loan.expiration <= block.timestamp) { - MultiToken.Asset memory collateral = loan.collateral; + MultiToken.Asset memory asset = defaulted + ? loan.collateral + : MultiToken.ERC20(loan.loanAssetAddress, loan.loanRepayAmount); - // Delete loan data & burn LOAN token before calling safe transfer - _deleteLoan(loanId); + // Delete loan data & burn LOAN token before calling safe transfer + _deleteLoan(loanId); - // Transfer collateral to lender - _push(collateral, msg.sender); + emit LOANClaimed({ loanId: loanId, defaulted: defaulted }); - emit LOANClaimed(loanId, true); - } - // Loan is in wrong state or from a different loan contract - else { - revert InvalidLoanStatus(loan.status); - } + // Transfer asset to current LOAN token owner + _push(asset, loanOwner); } + /** + * @notice Delete loan data and burn LOAN token. + * @param loanId Id of a loan that is being deleted. + */ function _deleteLoan(uint256 loanId) private { loanToken.burn(loanId); delete LOANs[loanId]; @@ -341,7 +652,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { // Extend expiration date loan.expiration = extendedExpirationDate; - emit LOANExpirationDateExtended(loanId, extendedExpirationDate); + emit LOANExpirationDateExtended({ loanId: loanId, extendedExpirationDate: extendedExpirationDate }); } @@ -359,6 +670,11 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { loan.status = _getLOANStatus(loanId); } + /** + * @notice Return a LOAN status associated with a loan id. + * @param loanId Id of a loan in question. + * @return status LOAN status. + */ function _getLOANStatus(uint256 loanId) private view returns (uint8) { LOAN storage loan = LOANs[loanId]; return (loan.status == 2 && loan.expiration <= block.timestamp) ? 4 : loan.status; diff --git a/test/unit/PWNSimpleLoan.t.sol b/test/unit/PWNSimpleLoan.t.sol index fd5e00f..fe6ed36 100644 --- a/test/unit/PWNSimpleLoan.t.sol +++ b/test/unit/PWNSimpleLoan.t.sol @@ -5,6 +5,8 @@ import "forge-std/Test.sol"; import "MultiToken/MultiToken.sol"; +import { Math } from "openzeppelin-contracts/contracts/utils/math/Math.sol"; + import "@pwn/hub/PWNHubTags.sol"; import "@pwn/loan/terms/simple/loan/PWNSimpleLoan.sol"; import "@pwn/loan/terms/PWNLOANTerms.sol"; @@ -48,14 +50,12 @@ abstract contract PWNSimpleLoanTest is Test { event LOANClaimed(uint256 indexed loanId, bool indexed defaulted); event LOANExpirationDateExtended(uint256 indexed loanId, uint40 extendedExpirationDate); - constructor() { + function setUp() virtual public { vm.etch(hub, bytes("data")); vm.etch(loanToken, bytes("data")); vm.etch(loanFactory, bytes("data")); vm.etch(config, bytes("data")); - } - function setUp() virtual public { loan = new PWNSimpleLoan(hub, loanToken, config, categoryRegistry); fungibleAsset = new T20(); nonFungibleAsset = new T721(); @@ -119,11 +119,26 @@ abstract contract PWNSimpleLoanTest is Test { abi.encodeWithSignature("permit(address,address,uint256,uint256,uint8,bytes32,bytes32)"), abi.encode() ); + vm.mockCall( categoryRegistry, abi.encodeWithSignature("registeredCategoryValue(address)"), abi.encode(type(uint8).max) ); + + vm.mockCall(config, abi.encodeWithSignature("fee()"), abi.encode(0)); + vm.mockCall(config, abi.encodeWithSignature("feeCollector()"), abi.encode(feeCollector)); + + vm.mockCall(hub, abi.encodeWithSignature("hasTag(address,bytes32)"), abi.encode(false)); + vm.mockCall( + hub, + abi.encodeWithSignature("hasTag(address,bytes32)", loanFactory, PWNHubTags.SIMPLE_LOAN_TERMS_FACTORY), + abi.encode(true) + ); + + _mockLoanTerms(simpleLoanTerms, loanFactoryDataHash); + _mockLOANMint(loanId); + _mockLOANTokenOwner(loanId, lender); } @@ -161,6 +176,7 @@ abstract contract PWNSimpleLoanTest is Test { _assertLOANWord(loanSlot + 6, abi.encodePacked(uint96(0), _simpleLoan.originalLender)); } + function _mockLOAN(uint256 _loanId, PWNSimpleLoan.LOAN memory _simpleLoan) internal { uint256 loanSlot = uint256(keccak256(abi.encode( _loanId, @@ -182,6 +198,22 @@ abstract contract PWNSimpleLoanTest is Test { _storeLOANWord(loanSlot + 6, abi.encodePacked(uint96(0), _simpleLoan.originalLender)); } + function _mockLoanTerms(PWNLOANTerms.Simple memory _loanTerms, bytes32 _loanFactoryDataHash) internal { + vm.mockCall( + loanFactory, + abi.encodeWithSignature("createLOANTerms(address,bytes,bytes)"), + abi.encode(_loanTerms, _loanFactoryDataHash) + ); + } + + function _mockLOANMint(uint256 _loanId) internal { + vm.mockCall(loanToken, abi.encodeWithSignature("mint(address)"), abi.encode(_loanId)); + } + + function _mockLOANTokenOwner(uint256 _loanId, address _owner) internal { + vm.mockCall(loanToken, abi.encodeWithSignature("ownerOf(uint256)", _loanId), abi.encode(_owner)); + } + function _assertLOANWord(uint256 wordSlot, bytes memory word) private { assertEq( @@ -209,45 +241,6 @@ abstract contract PWNSimpleLoanTest is Test { contract PWNSimpleLoan_CreateLoan_Test is PWNSimpleLoanTest { - function setUp() override public { - super.setUp(); - - vm.mockCall( - config, - abi.encodeWithSignature("fee()"), - abi.encode(0) - ); - vm.mockCall( - config, - abi.encodeWithSignature("feeCollector()"), - abi.encode(feeCollector) - ); - - vm.mockCall( - hub, - abi.encodeWithSignature("hasTag(address,bytes32)"), - abi.encode(false) - ); - vm.mockCall( - hub, - abi.encodeWithSignature("hasTag(address,bytes32)", loanFactory, PWNHubTags.SIMPLE_LOAN_TERMS_FACTORY), - abi.encode(true) - ); - - vm.mockCall( - loanFactory, - abi.encodeWithSignature("createLOANTerms(address,bytes,bytes)"), - abi.encode(simpleLoanTerms, loanFactoryDataHash) - ); - - vm.mockCall( - loanToken, - abi.encodeWithSignature("mint(address)"), - abi.encode(loanId) - ); - } - - function test_shouldFail_whenLoanFactoryContractIsNotTaggerInPWNHub() external { address notLoanFactory = address(0); @@ -274,11 +267,7 @@ contract PWNSimpleLoan_CreateLoan_Test is PWNSimpleLoanTest { abi.encode(1) ); - vm.mockCall( - loanFactory, - abi.encodeWithSignature("createLOANTerms(address,bytes,bytes)"), - abi.encode(simpleLoanTerms, loanFactoryDataHash) - ); + _mockLoanTerms(simpleLoanTerms, loanFactoryDataHash); vm.expectRevert(InvalidLoanAsset.selector); loan.createLOAN(loanFactory, loanFactoryData, signature, loanAssetPermit, collateralPermit); @@ -291,11 +280,7 @@ contract PWNSimpleLoan_CreateLoan_Test is PWNSimpleLoanTest { abi.encode(0) ); - vm.mockCall( - loanFactory, - abi.encodeWithSignature("createLOANTerms(address,bytes,bytes)"), - abi.encode(simpleLoanTerms, loanFactoryDataHash) - ); + _mockLoanTerms(simpleLoanTerms, loanFactoryDataHash); vm.expectRevert(InvalidCollateralAsset.selector); loan.createLOAN(loanFactory, loanFactoryData, signature, loanAssetPermit, collateralPermit); @@ -321,13 +306,7 @@ contract PWNSimpleLoan_CreateLoan_Test is PWNSimpleLoanTest { simpleLoanTerms.collateral.assetAddress = address(fungibleAsset); simpleLoanTerms.collateral.id = 0; simpleLoanTerms.collateral.amount = 100; - - vm.mockCall( - loanFactory, - abi.encodeWithSignature("createLOANTerms(address,bytes,bytes)"), - abi.encode(simpleLoanTerms, loanFactoryDataHash) - ); - + _mockLoanTerms(simpleLoanTerms, loanFactoryDataHash); collateralPermit = abi.encodePacked(uint256(1), uint256(2), uint256(3), uint8(4)); vm.expectCall( @@ -364,11 +343,7 @@ contract PWNSimpleLoan_CreateLoan_Test is PWNSimpleLoanTest { } function test_shouldTransferLoanAsset_fromLender_toBorrowerAndFeeCollector_whenNonZeroFee() external { - vm.mockCall( - config, - abi.encodeWithSignature("fee()"), - abi.encode(1000) - ); + vm.mockCall(config, abi.encodeWithSignature("fee()"), abi.encode(1000)); loanAssetPermit = abi.encodePacked(uint256(1), uint256(2), uint256(3), uint8(4)); @@ -410,29 +385,667 @@ contract PWNSimpleLoan_CreateLoan_Test is PWNSimpleLoanTest { /*----------------------------------------------------------*| -|* # REPAY LOAN *| +|* # REFINANCE LOAN *| |*----------------------------------------------------------*/ -contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { +contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { - address notOriginalLender = makeAddr("notOriginalLender"); + PWNSimpleLoan.LOAN refinancedLoan; + PWNLOANTerms.Simple refinancedLoanTerms; + uint256 ferinancedLoanId = 44; + address newLender = makeAddr("newLender"); function setUp() override public { super.setUp(); - vm.mockCall( - loanToken, abi.encodeWithSignature("ownerOf(uint256)", loanId), abi.encode(lender) - ); - // Move collateral to vault vm.prank(borrower); nonFungibleAsset.transferFrom(borrower, address(loan), 2); + + refinancedLoan = PWNSimpleLoan.LOAN({ + status: 2, + borrower: borrower, + expiration: uint40(block.timestamp + 40039), + loanAssetAddress: address(fungibleAsset), + loanRepayAmount: 6731, + collateral: MultiToken.ERC721(address(nonFungibleAsset), 2), + originalLender: lender + }); + + refinancedLoanTerms = PWNLOANTerms.Simple({ + lender: lender, + borrower: borrower, + expiration: uint40(block.timestamp + 40039), + collateral: MultiToken.ERC721(address(nonFungibleAsset), 2), + asset: MultiToken.ERC20(address(fungibleAsset), 100), + loanRepayAmount: 6731 + }); + + loanAssetPermit = abi.encodePacked(uint256(1), uint256(2), uint256(3), uint8(4)); + + _mockLOAN(loanId, simpleLoan); + _mockLOANMint(ferinancedLoanId); + _mockLoanTerms(refinancedLoanTerms, loanFactoryDataHash); + + vm.prank(newLender); + fungibleAsset.approve(address(loan), type(uint256).max); } - function _LOANTokenNotOwnedByOriginalLender() internal { - vm.mockCall( - loanToken, abi.encodeWithSignature("ownerOf(uint256)", loanId), abi.encode(notOriginalLender) + + function test_shouldFail_whenLoanDoesNotExist() external { + simpleLoan.status = 0; + _mockLOAN(loanId, simpleLoan); + + vm.expectRevert(abi.encodeWithSelector(NonExistingLoan.selector)); + loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); + } + + function test_shouldFail_whenLoanIsNotRunning() external { + simpleLoan.status = 3; + _mockLOAN(loanId, simpleLoan); + + vm.expectRevert(abi.encodeWithSelector(InvalidLoanStatus.selector, 3)); + loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); + } + + function test_shouldFail_whenLoanIsExpired() external { + vm.warp(simpleLoan.expiration + 10000); + + vm.expectRevert(abi.encodeWithSelector(LoanDefaulted.selector, simpleLoan.expiration)); + loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); + } + + function test_shouldFail_whenLoanFactoryContractIsNotTaggerInPWNHub() external { + address notLoanFactory = makeAddr("notLoanFactory"); + + vm.expectRevert(abi.encodeWithSelector(CallerMissingHubTag.selector, PWNHubTags.SIMPLE_LOAN_TERMS_FACTORY)); + loan.refinanceLOAN(loanId, notLoanFactory, loanFactoryData, signature, "", ""); + } + + function test_shouldGetLOANTermsStructFromGivenFactoryContract() external { + loanFactoryData = abi.encode(1, 2, "data"); + signature = abi.encode("other data", "whaat?", uint256(312312)); + + vm.expectCall( + address(loanFactory), + abi.encodeWithSignature("createLOANTerms(address,bytes,bytes)", address(this), loanFactoryData, signature) ); + + loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); + } + + function testFuzz_shouldFail_whenLoanAssetMismatch(address _assetAddress) external { + vm.assume(_assetAddress != simpleLoan.loanAssetAddress); + + refinancedLoanTerms.asset.assetAddress = _assetAddress; + _mockLoanTerms(refinancedLoanTerms, loanFactoryDataHash); + + vm.expectRevert(abi.encodeWithSelector(InvalidLoanAsset.selector)); + loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); + } + + function test_shouldFail_whenLoanAssetAmountZero() external { + refinancedLoanTerms.asset.amount = 0; + _mockLoanTerms(refinancedLoanTerms, loanFactoryDataHash); + + vm.expectRevert(abi.encodeWithSelector(InvalidLoanAsset.selector)); + loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); + } + + function testFuzz_shouldFail_whenCollateralCategoryMismatch(uint8 _category) external { + _category = _category % 4; + vm.assume(_category != uint8(simpleLoan.collateral.category)); + + refinancedLoanTerms.collateral.category = MultiToken.Category(_category); + _mockLoanTerms(refinancedLoanTerms, loanFactoryDataHash); + + vm.expectRevert(abi.encodeWithSelector(InvalidCollateralAsset.selector)); + loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); + } + + function testFuzz_shouldFail_whenCollateralAddressMismatch(address _assetAddress) external { + vm.assume(_assetAddress != simpleLoan.collateral.assetAddress); + + refinancedLoanTerms.collateral.assetAddress = _assetAddress; + _mockLoanTerms(refinancedLoanTerms, loanFactoryDataHash); + + vm.expectRevert(abi.encodeWithSelector(InvalidCollateralAsset.selector)); + loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); + } + + function testFuzz_shouldFail_whenCollateralIdMismatch(uint256 _id) external { + vm.assume(_id != simpleLoan.collateral.id); + + refinancedLoanTerms.collateral.id = _id; + _mockLoanTerms(refinancedLoanTerms, loanFactoryDataHash); + + vm.expectRevert(abi.encodeWithSelector(InvalidCollateralAsset.selector)); + loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); + } + + function testFuzz_shouldFail_whenCollateralAmountMismatch(uint256 _amount) external { + vm.assume(_amount != simpleLoan.collateral.amount); + + refinancedLoanTerms.collateral.amount = _amount; + _mockLoanTerms(refinancedLoanTerms, loanFactoryDataHash); + + vm.expectRevert(abi.encodeWithSelector(InvalidCollateralAsset.selector)); + loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); + } + + function testFuzz_shouldFail_whenBorrowerMismatch(address _borrower) external { + vm.assume(_borrower != simpleLoan.borrower); + + refinancedLoanTerms.borrower = _borrower; + _mockLoanTerms(refinancedLoanTerms, loanFactoryDataHash); + + vm.expectRevert(abi.encodeWithSelector(BorrowerMismatch.selector, simpleLoan.borrower, _borrower)); + loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); + } + + function test_shouldMintLOANToken() external { + vm.expectCall(address(loanToken), abi.encodeWithSignature("mint(address)")); + + loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); + } + + function test_shouldStoreRefinancedLoanData() external { + loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); + + _assertLOANEq(ferinancedLoanId, refinancedLoan); + } + + function test_shouldEmit_LOANCreated() external { + vm.expectEmit(); + emit LOANCreated(ferinancedLoanId, refinancedLoanTerms, loanFactoryDataHash, loanFactory); + + loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); + } + + function test_shouldReturnNewLoanId() external { + uint256 newLoanId = loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); + + assertEq(newLoanId, ferinancedLoanId); + } + + function test_shouldEmit_LOANPaidBack() external { + vm.expectEmit(); + emit LOANPaidBack(loanId); + + loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); + } + + function test_shouldDeleteOldLoanData_whenLOANOwnerIsOriginalLender() external { + loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); + + _assertLOANEq(loanId, nonExistingLoan); + } + + function test_shouldEmit_LOANClaimed_whenLOANOwnerIsOriginalLender() external { + vm.expectEmit(); + emit LOANClaimed(loanId, false); + + loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); + } + + function test_shouldMoveLoanToRepaidState_whenLOANOwnerIsNotOriginalLender() external { + _mockLOANTokenOwner(loanId, makeAddr("notOriginalLender")); + + loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); + + bytes32 loanSlot = keccak256(abi.encode(loanId, LOANS_SLOT)); + // Parse status value from first storage slot + bytes32 statusValue = vm.load(address(loan), loanSlot) & bytes32(uint256(0xff)); + assertTrue(statusValue == bytes32(uint256(3))); + } + + function testFuzz_shouldTransferOriginalLoanRepaymentDirectly_andTransferSurplusToBorrower_whenLOANOwnerIsOriginalLender_whenRefinanceLoanMoreThanOrEqualToOriginalLoan( + uint256 refinanceAmount, uint256 fee + ) external { + fee = bound(fee, 0, 9999); // 0 - 99.99% + uint256 minRefinanceAmount = Math.mulDiv(simpleLoan.loanRepayAmount, 1e4, 1e4 - fee); + refinanceAmount = bound( + refinanceAmount, + minRefinanceAmount, + type(uint256).max - minRefinanceAmount - fungibleAsset.totalSupply() + ); + uint256 feeAmount = Math.mulDiv(refinanceAmount, fee, 1e4); + uint256 borrowerSurplus = refinanceAmount - feeAmount - simpleLoan.loanRepayAmount; + + refinancedLoanTerms.asset.amount = refinanceAmount; + refinancedLoanTerms.lender = newLender; + _mockLoanTerms(refinancedLoanTerms, loanFactoryDataHash); + _mockLOANTokenOwner(loanId, simpleLoan.originalLender); + vm.mockCall(config, abi.encodeWithSignature("fee()"), abi.encode(fee)); + + fungibleAsset.mint(newLender, refinanceAmount); + + vm.expectCall( // lender permit + refinancedLoanTerms.asset.assetAddress, + abi.encodeWithSignature( + "permit(address,address,uint256,uint256,uint8,bytes32,bytes32)", + newLender, address(loan), refinanceAmount, 1, uint8(4), uint256(2), uint256(3) + ) + ); + // no borrower permit + vm.expectCall({ // fee transfer + callee: refinancedLoanTerms.asset.assetAddress, + data: abi.encodeWithSignature( + "transferFrom(address,address,uint256)", + newLender, feeCollector, feeAmount + ), + count: feeAmount > 0 ? 1 : 0 + }); + vm.expectCall( // lender repayment + refinancedLoanTerms.asset.assetAddress, + abi.encodeWithSignature( + "transferFrom(address,address,uint256)", + newLender, simpleLoan.originalLender, simpleLoan.loanRepayAmount + ) + ); + vm.expectCall({ // borrower surplus + callee: refinancedLoanTerms.asset.assetAddress, + data: abi.encodeWithSignature( + "transferFrom(address,address,uint256)", + newLender, borrower, borrowerSurplus + ), + count: borrowerSurplus > 0 ? 1 : 0 + }); + + loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, loanAssetPermit, ""); + } + + function testFuzz_shouldTransferOriginalLoanRepaymentToVault_andTransferSurplusToBorrower_whenLOANOwnerIsNotOriginalLender_whenRefinanceLoanMoreThanOrEqualToOriginalLoan( + uint256 refinanceAmount, uint256 fee + ) external { + fee = bound(fee, 0, 9999); // 0 - 99.99% + uint256 minRefinanceAmount = Math.mulDiv(simpleLoan.loanRepayAmount, 1e4, 1e4 - fee); + refinanceAmount = bound( + refinanceAmount, + minRefinanceAmount, + type(uint256).max - minRefinanceAmount - fungibleAsset.totalSupply() + ); + uint256 feeAmount = Math.mulDiv(refinanceAmount, fee, 1e4); + uint256 borrowerSurplus = refinanceAmount - feeAmount - simpleLoan.loanRepayAmount; + + refinancedLoanTerms.asset.amount = refinanceAmount; + refinancedLoanTerms.lender = newLender; + _mockLoanTerms(refinancedLoanTerms, loanFactoryDataHash); + _mockLOANTokenOwner(loanId, makeAddr("notOriginalLender")); + vm.mockCall(config, abi.encodeWithSignature("fee()"), abi.encode(fee)); + + fungibleAsset.mint(newLender, refinanceAmount); + + vm.expectCall( // lender permit + refinancedLoanTerms.asset.assetAddress, + abi.encodeWithSignature( + "permit(address,address,uint256,uint256,uint8,bytes32,bytes32)", + newLender, address(loan), refinanceAmount, 1, uint8(4), uint256(2), uint256(3) + ) + ); + // no borrower permit + vm.expectCall({ // fee transfer + callee: refinancedLoanTerms.asset.assetAddress, + data: abi.encodeWithSignature( + "transferFrom(address,address,uint256)", + newLender, feeCollector, feeAmount + ), + count: feeAmount > 0 ? 1 : 0 + }); + vm.expectCall( // lender repayment + refinancedLoanTerms.asset.assetAddress, + abi.encodeWithSignature( + "transferFrom(address,address,uint256)", + newLender, address(loan), simpleLoan.loanRepayAmount + ) + ); + vm.expectCall({ // borrower surplus + callee: refinancedLoanTerms.asset.assetAddress, + data: abi.encodeWithSignature( + "transferFrom(address,address,uint256)", + newLender, borrower, borrowerSurplus + ), + count: borrowerSurplus > 0 ? 1 : 0 + }); + + loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, loanAssetPermit, ""); + } + + function testFuzz_shouldNotTransferOriginalLoanRepayment_andTransferSurplusToBorrower_whenLOANOwnerIsNewLender_whenRefinanceLoanMoreThanOrEqualOriginalLoan( + uint256 refinanceAmount, uint256 fee + ) external { + fee = bound(fee, 0, 9999); // 0 - 99.99% + uint256 minRefinanceAmount = Math.mulDiv(simpleLoan.loanRepayAmount, 1e4, 1e4 - fee); + refinanceAmount = bound( + refinanceAmount, + minRefinanceAmount, + type(uint256).max - minRefinanceAmount - fungibleAsset.totalSupply() + ); + uint256 feeAmount = Math.mulDiv(refinanceAmount, fee, 1e4); + uint256 borrowerSurplus = refinanceAmount - feeAmount - simpleLoan.loanRepayAmount; + + refinancedLoanTerms.asset.amount = refinanceAmount; + refinancedLoanTerms.lender = newLender; + _mockLoanTerms(refinancedLoanTerms, loanFactoryDataHash); + _mockLOANTokenOwner(loanId, newLender); + vm.mockCall(config, abi.encodeWithSignature("fee()"), abi.encode(fee)); + + fungibleAsset.mint(newLender, refinanceAmount); + + vm.expectCall({ // lender permit + callee: refinancedLoanTerms.asset.assetAddress, + data: abi.encodeWithSignature( + "permit(address,address,uint256,uint256,uint8,bytes32,bytes32)", + newLender, address(loan), borrowerSurplus + feeAmount, 1, uint8(4), uint256(2), uint256(3) + ), + count: borrowerSurplus + feeAmount > 0 ? 1 : 0 + }); + // no borrower permit + vm.expectCall({ // fee transfer + callee: refinancedLoanTerms.asset.assetAddress, + data: abi.encodeWithSignature( + "transferFrom(address,address,uint256)", + newLender, feeCollector, feeAmount + ), + count: feeAmount > 0 ? 1 : 0 + }); + vm.expectCall({ // lender repayment + callee: refinancedLoanTerms.asset.assetAddress, + data: abi.encodeWithSignature( + "transferFrom(address,address,uint256)", + newLender, newLender, simpleLoan.loanRepayAmount + ), + count: 0 + }); + vm.expectCall({ // borrower surplus + callee: refinancedLoanTerms.asset.assetAddress, + data: abi.encodeWithSignature( + "transferFrom(address,address,uint256)", + newLender, borrower, borrowerSurplus + ), + count: borrowerSurplus > 0 ? 1 : 0 + }); + + loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, loanAssetPermit, ""); + } + + function testFuzz_shouldTransferOriginalLoanRepaymentDirectly_andContributeFromBorrower_whenLOANOwnerIsOriginalLender_whenRefinanceLoanLessThanOriginalLoan( + uint256 refinanceAmount, uint256 fee + ) external { + fee = bound(fee, 0, 9999); // 0 - 99.99% + uint256 minRefinanceAmount = Math.mulDiv(simpleLoan.loanRepayAmount, 1e4, 1e4 - fee); + refinanceAmount = bound(refinanceAmount, 1, minRefinanceAmount - 1); + uint256 feeAmount = Math.mulDiv(refinanceAmount, fee, 1e4); + uint256 borrowerContribution = simpleLoan.loanRepayAmount - (refinanceAmount - feeAmount); + + refinancedLoanTerms.asset.amount = refinanceAmount; + refinancedLoanTerms.lender = newLender; + _mockLoanTerms(refinancedLoanTerms, loanFactoryDataHash); + _mockLOANTokenOwner(loanId, simpleLoan.originalLender); + vm.mockCall(config, abi.encodeWithSignature("fee()"), abi.encode(fee)); + + fungibleAsset.mint(newLender, refinanceAmount); + + vm.expectCall( // lender permit + refinancedLoanTerms.asset.assetAddress, + abi.encodeWithSignature( + "permit(address,address,uint256,uint256,uint8,bytes32,bytes32)", + newLender, address(loan), refinanceAmount, 1, uint8(4), uint256(2), uint256(3) + ) + ); + vm.expectCall({ // borrower permit + callee: refinancedLoanTerms.asset.assetAddress, + data: abi.encodeWithSignature( + "permit(address,address,uint256,uint256,uint8,bytes32,bytes32)", + borrower, address(loan), borrowerContribution, 1, uint8(4), uint256(2), uint256(3) + ), + count: borrowerContribution > 0 ? 1 : 0 + }); + vm.expectCall({ // fee transfer + callee: refinancedLoanTerms.asset.assetAddress, + data: abi.encodeWithSignature( + "transferFrom(address,address,uint256)", + newLender, feeCollector, feeAmount + ), + count: feeAmount > 0 ? 1 : 0 + }); + vm.expectCall( // lender repayment + refinancedLoanTerms.asset.assetAddress, + abi.encodeWithSignature( + "transferFrom(address,address,uint256)", + newLender, simpleLoan.originalLender, refinanceAmount - feeAmount + ) + ); + vm.expectCall({ // borrower contribution + callee: refinancedLoanTerms.asset.assetAddress, + data: abi.encodeWithSignature( + "transferFrom(address,address,uint256)", + borrower, simpleLoan.originalLender, borrowerContribution + ), + count: borrowerContribution > 0 ? 1 : 0 + }); + + loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, loanAssetPermit, loanAssetPermit); + } + + function testFuzz_shouldTransferOriginalLoanRepaymentToVault_andContributeFromBorrower_whenLOANOwnerIsNotOriginalLender_whenRefinanceLoanLessThanOriginalLoan( + uint256 refinanceAmount, uint256 fee + ) external { + fee = bound(fee, 0, 9999); // 0 - 99.99% + uint256 minRefinanceAmount = Math.mulDiv(simpleLoan.loanRepayAmount, 1e4, 1e4 - fee); + refinanceAmount = bound(refinanceAmount, 1, minRefinanceAmount - 1); + uint256 feeAmount = Math.mulDiv(refinanceAmount, fee, 1e4); + uint256 borrowerContribution = simpleLoan.loanRepayAmount - (refinanceAmount - feeAmount); + + refinancedLoanTerms.asset.amount = refinanceAmount; + refinancedLoanTerms.lender = newLender; + _mockLoanTerms(refinancedLoanTerms, loanFactoryDataHash); + _mockLOANTokenOwner(loanId, makeAddr("notOriginalLender")); + vm.mockCall(config, abi.encodeWithSignature("fee()"), abi.encode(fee)); + + fungibleAsset.mint(newLender, refinanceAmount); + + vm.expectCall( // lender permit + refinancedLoanTerms.asset.assetAddress, + abi.encodeWithSignature( + "permit(address,address,uint256,uint256,uint8,bytes32,bytes32)", + newLender, address(loan), refinanceAmount, 1, uint8(4), uint256(2), uint256(3) + ) + ); + vm.expectCall({ // borrower permit + callee: refinancedLoanTerms.asset.assetAddress, + data: abi.encodeWithSignature( + "permit(address,address,uint256,uint256,uint8,bytes32,bytes32)", + borrower, address(loan), borrowerContribution, 1, uint8(4), uint256(2), uint256(3) + ), + count: borrowerContribution > 0 ? 1 : 0 + }); + vm.expectCall({ // fee transfer + callee: refinancedLoanTerms.asset.assetAddress, + data: abi.encodeWithSignature( + "transferFrom(address,address,uint256)", + newLender, feeCollector, feeAmount + ), + count: feeAmount > 0 ? 1 : 0 + }); + vm.expectCall( // lender repayment + refinancedLoanTerms.asset.assetAddress, + abi.encodeWithSignature( + "transferFrom(address,address,uint256)", + newLender, address(loan), refinanceAmount - feeAmount + ) + ); + vm.expectCall({ // borrower contribution + callee: refinancedLoanTerms.asset.assetAddress, + data: abi.encodeWithSignature( + "transferFrom(address,address,uint256)", + borrower, address(loan), borrowerContribution + ), + count: borrowerContribution > 0 ? 1 : 0 + }); + + loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, loanAssetPermit, loanAssetPermit); + } + + function testFuzz_shouldNotTransferOriginalLoanRepayment_andContributeFromBorrower_whenLOANOwnerIsNewLender_whenRefinanceLoanLessThanOriginalLoan( + uint256 refinanceAmount, uint256 fee + ) external { + fee = bound(fee, 0, 9999); // 0 - 99.99% + uint256 minRefinanceAmount = Math.mulDiv(simpleLoan.loanRepayAmount, 1e4, 1e4 - fee); + refinanceAmount = bound(refinanceAmount, 1, minRefinanceAmount - 1); + uint256 feeAmount = Math.mulDiv(refinanceAmount, fee, 1e4); + uint256 borrowerContribution = simpleLoan.loanRepayAmount - (refinanceAmount - feeAmount); + + refinancedLoanTerms.asset.amount = refinanceAmount; + refinancedLoanTerms.lender = newLender; + _mockLoanTerms(refinancedLoanTerms, loanFactoryDataHash); + _mockLOANTokenOwner(loanId, newLender); + vm.mockCall(config, abi.encodeWithSignature("fee()"), abi.encode(fee)); + + fungibleAsset.mint(newLender, refinanceAmount); + + vm.expectCall({ // lender permit + callee: refinancedLoanTerms.asset.assetAddress, + data: abi.encodeWithSignature( + "permit(address,address,uint256,uint256,uint8,bytes32,bytes32)", + newLender, address(loan), feeAmount, 1, uint8(4), uint256(2), uint256(3) + ), + count: feeAmount > 0 ? 1 : 0 + }); + vm.expectCall({ // borrower permit + callee: refinancedLoanTerms.asset.assetAddress, + data: abi.encodeWithSignature( + "permit(address,address,uint256,uint256,uint8,bytes32,bytes32)", + borrower, address(loan), borrowerContribution, 1, uint8(4), uint256(2), uint256(3) + ), + count: borrowerContribution > 0 ? 1 : 0 + }); + vm.expectCall({ // fee transfer + callee: refinancedLoanTerms.asset.assetAddress, + data: abi.encodeWithSignature( + "transferFrom(address,address,uint256)", + newLender, feeCollector, feeAmount + ), + count: feeAmount > 0 ? 1 : 0 + }); + vm.expectCall({ // lender repayment + callee: refinancedLoanTerms.asset.assetAddress, + data: abi.encodeWithSignature( + "transferFrom(address,address,uint256)", + newLender, newLender, refinanceAmount - feeAmount + ), + count: 0 + }); + vm.expectCall({ // borrower contribution + callee: refinancedLoanTerms.asset.assetAddress, + data: abi.encodeWithSignature( + "transferFrom(address,address,uint256)", + borrower, newLender, borrowerContribution + ), + count: borrowerContribution > 0 ? 1 : 0 + }); + + loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, loanAssetPermit, loanAssetPermit); + } + + function testFuzz_shouldRepayOriginalLoan(uint256 refinanceAmount) external { + refinanceAmount = bound( + refinanceAmount, + 1, + type(uint256).max - simpleLoan.loanRepayAmount - fungibleAsset.totalSupply() + ); + + refinancedLoanTerms.asset.amount = refinanceAmount; + refinancedLoanTerms.lender = newLender; + _mockLoanTerms(refinancedLoanTerms, loanFactoryDataHash); + _mockLOANTokenOwner(loanId, lender); + + fungibleAsset.mint(newLender, refinanceAmount); + uint256 originalBalance = fungibleAsset.balanceOf(lender); + + loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); + + assertEq(fungibleAsset.balanceOf(lender), originalBalance + simpleLoan.loanRepayAmount); + } + + function testFuzz_shouldCollectProtocolFee(uint256 refinanceAmount, uint256 fee) external { + fee = bound(fee, 1, 9999); // 0 - 99.99% + refinanceAmount = bound( + refinanceAmount, + 1, + type(uint256).max - simpleLoan.loanRepayAmount - fungibleAsset.totalSupply() + ); + uint256 feeAmount = Math.mulDiv(refinanceAmount, fee, 1e4); + + refinancedLoanTerms.asset.amount = refinanceAmount; + refinancedLoanTerms.lender = newLender; + _mockLoanTerms(refinancedLoanTerms, loanFactoryDataHash); + _mockLOANTokenOwner(loanId, lender); + vm.mockCall(config, abi.encodeWithSignature("fee()"), abi.encode(fee)); + + fungibleAsset.mint(newLender, refinanceAmount); + uint256 originalBalance = fungibleAsset.balanceOf(feeCollector); + + loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); + + assertEq(fungibleAsset.balanceOf(feeCollector), originalBalance + feeAmount); + } + + function testFuzz_shouldTransferSurplusToBorrower(uint256 refinanceAmount) external { + refinanceAmount = bound( + refinanceAmount, + simpleLoan.loanRepayAmount + 1, + type(uint256).max - simpleLoan.loanRepayAmount - fungibleAsset.totalSupply() + ); + uint256 surplus = refinanceAmount - simpleLoan.loanRepayAmount; + + refinancedLoanTerms.asset.amount = refinanceAmount; + refinancedLoanTerms.lender = newLender; + _mockLoanTerms(refinancedLoanTerms, loanFactoryDataHash); + _mockLOANTokenOwner(loanId, lender); + + fungibleAsset.mint(newLender, refinanceAmount); + uint256 originalBalance = fungibleAsset.balanceOf(borrower); + + loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); + + assertEq(fungibleAsset.balanceOf(borrower), originalBalance + surplus); + } + + function testFuzz_shouldContributeFromBorrower(uint256 refinanceAmount) external { + refinanceAmount = bound(refinanceAmount, 1, simpleLoan.loanRepayAmount - 1); + uint256 contribution = simpleLoan.loanRepayAmount - refinanceAmount; + + refinancedLoanTerms.asset.amount = refinanceAmount; + refinancedLoanTerms.lender = newLender; + _mockLoanTerms(refinancedLoanTerms, loanFactoryDataHash); + _mockLOANTokenOwner(loanId, lender); + + fungibleAsset.mint(newLender, refinanceAmount); + uint256 originalBalance = fungibleAsset.balanceOf(borrower); + + loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); + + assertEq(fungibleAsset.balanceOf(borrower), originalBalance - contribution); + } + +} + + +/*----------------------------------------------------------*| +|* # REPAY LOAN *| +|*----------------------------------------------------------*/ + +contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { + + address notOriginalLender = makeAddr("notOriginalLender"); + + function setUp() override public { + super.setUp(); + + // Move collateral to vault + vm.prank(borrower); + nonFungibleAsset.transferFrom(borrower, address(loan), 2); } @@ -509,7 +1122,7 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { function test_shouldMoveLoanToRepaidState_whenLOANOwnerIsNotOriginalLender() external { _mockLOAN(loanId, simpleLoan); - _LOANTokenNotOwnedByOriginalLender(); + _mockLOANTokenOwner(loanId, notOriginalLender); loan.repayLOAN(loanId, loanAssetPermit); @@ -521,7 +1134,7 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { function test_shouldTransferRepaidAmountToVault_whenLOANOwnerIsNotOriginalLender() external { _mockLOAN(loanId, simpleLoan); - _LOANTokenNotOwnedByOriginalLender(); + _mockLOANTokenOwner(loanId, notOriginalLender); vm.expectCall( simpleLoan.loanAssetAddress, From 3f8d2adc69fdcd73abb98ea299b2440d7be211e6 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Wed, 21 Feb 2024 15:28:17 +0000 Subject: [PATCH 017/129] style(simple-loan): code styling --- src/loan/terms/simple/loan/PWNSimpleLoan.sol | 25 ++++++++++---------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/loan/terms/simple/loan/PWNSimpleLoan.sol b/src/loan/terms/simple/loan/PWNSimpleLoan.sol index 05b339e..105b4b2 100644 --- a/src/loan/terms/simple/loan/PWNSimpleLoan.sol +++ b/src/loan/terms/simple/loan/PWNSimpleLoan.sol @@ -5,15 +5,16 @@ import { MultiToken, IMultiTokenCategoryRegistry } from "MultiToken/MultiToken.s import { Math } from "openzeppelin-contracts/contracts/utils/math/Math.sol"; -import "@pwn/config/PWNConfig.sol"; -import "@pwn/hub/PWNHub.sol"; -import "@pwn/hub/PWNHubTags.sol"; -import "@pwn/loan/lib/PWNFeeCalculator.sol"; -import "@pwn/loan/terms/PWNLOANTerms.sol"; -import "@pwn/loan/terms/simple/factory/PWNSimpleLoanTermsFactory.sol"; -import "@pwn/loan/token/IERC5646.sol"; -import "@pwn/loan/token/PWNLOAN.sol"; -import "@pwn/loan/PWNVault.sol"; +import { PWNConfig } from "@pwn/config/PWNConfig.sol"; +import { PWNHub } from "@pwn/hub/PWNHub.sol"; +import { PWNHubTags } from "@pwn/hub/PWNHubTags.sol"; +import { PWNFeeCalculator } from "@pwn/loan/lib/PWNFeeCalculator.sol"; +import { PWNLOANTerms } from "@pwn/loan/terms/PWNLOANTerms.sol"; +import { PWNSimpleLoanTermsFactory } from "@pwn/loan/terms/simple/factory/PWNSimpleLoanTermsFactory.sol"; +import { IERC5646 } from "@pwn/loan/token/IERC5646.sol"; +import { IPWNLoanMetadataProvider } from "@pwn/loan/token/IPWNLoanMetadataProvider.sol"; +import { PWNLOAN } from "@pwn/loan/token/PWNLOAN.sol"; +import { PWNVault } from "@pwn/loan/PWNVault.sol"; import "@pwn/PWNErrors.sol"; @@ -292,7 +293,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { * @param loan Original loan struct. * @param loanTerms Refinancing loan terms struct. */ - function _checkRefinanceLoanTerms(LOAN storage loan, PWNLOANTerms.Simple memory loanTerms) private { + function _checkRefinanceLoanTerms(LOAN storage loan, PWNLOANTerms.Simple memory loanTerms) private view { // Check that the loan 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. @@ -686,7 +687,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { |*----------------------------------------------------------*/ /** - * @notice See { IPWNLoanMetadataProvider.sol }. + * @inheritdoc IPWNLoanMetadataProvider */ function loanMetadataUri() override external view returns (string memory) { return config.loanMetadataUri(address(this)); @@ -698,7 +699,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { |*----------------------------------------------------------*/ /** - * @dev See {IERC5646-getStateFingerprint}. + * @inheritdoc IERC5646 */ function getStateFingerprint(uint256 tokenId) external view virtual override returns (bytes32) { LOAN storage loan = LOANs[tokenId]; From 86790e78477505db4bfbdd67252dacef2488d1da Mon Sep 17 00:00:00 2001 From: ashhanai Date: Wed, 21 Feb 2024 18:39:03 +0000 Subject: [PATCH 018/129] refactor(loan-default-timestamp): rename loan expiration to default timestamp --- src/loan/terms/PWNLOANTerms.sol | 4 +- .../factory/offer/PWNSimpleLoanListOffer.sol | 4 +- .../offer/PWNSimpleLoanSimpleOffer.sol | 4 +- .../request/PWNSimpleLoanSimpleRequest.sol | 4 +- src/loan/terms/simple/loan/PWNSimpleLoan.sol | 30 ++++----- test/unit/PWNSimpleLoan.t.sol | 66 +++++++++---------- test/unit/PWNSimpleLoanListOffer.t.sol | 2 +- test/unit/PWNSimpleLoanSimpleOffer.t.sol | 2 +- test/unit/PWNSimpleLoanSimpleRequest.t.sol | 2 +- 9 files changed, 59 insertions(+), 59 deletions(-) diff --git a/src/loan/terms/PWNLOANTerms.sol b/src/loan/terms/PWNLOANTerms.sol index d0d6914..3e90a77 100644 --- a/src/loan/terms/PWNLOANTerms.sol +++ b/src/loan/terms/PWNLOANTerms.sol @@ -11,7 +11,7 @@ library PWNLOANTerms { * @dev This struct is created by loan factories and never stored. * @param lender Address of a lender. * @param borrower Address of a borrower. - * @param expiration Unix timestamp (in seconds) setting up a default date. + * @param defaultTimestamp Unix timestamp (in seconds) setting up a default date. * @param collateral Asset used as a loan collateral. For a definition see { MultiToken dependency lib }. * @param asset Asset used as a loan credit. For a definition see { MultiToken dependency lib }. * @param loanRepayAmount Amount of a loan asset to be paid back. @@ -19,7 +19,7 @@ library PWNLOANTerms { struct Simple { address lender; address borrower; - uint40 expiration; + uint40 defaultTimestamp; MultiToken.Asset collateral; MultiToken.Asset asset; uint256 loanRepayAmount; diff --git a/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol b/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol index 593b5c6..996aa3e 100644 --- a/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol +++ b/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol @@ -172,11 +172,11 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanOffer { amount: offer.loanAmount }); - // Create loan object + // Create loan terms object loanTerms = PWNLOANTerms.Simple({ lender: lender, borrower: borrower, - expiration: uint40(block.timestamp) + offer.duration, + defaultTimestamp: uint40(block.timestamp) + offer.duration, collateral: collateral, asset: loanAsset, loanRepayAmount: offer.loanAmount + offer.loanYield diff --git a/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol b/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol index 41f3b9a..fc9c7be 100644 --- a/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol +++ b/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol @@ -145,11 +145,11 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanOffer { amount: offer.loanAmount }); - // Create loan object + // Create loan terms object loanTerms = PWNLOANTerms.Simple({ lender: lender, borrower: borrower, - expiration: uint40(block.timestamp) + offer.duration, + defaultTimestamp: uint40(block.timestamp) + offer.duration, collateral: collateral, asset: loanAsset, loanRepayAmount: offer.loanAmount + offer.loanYield diff --git a/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol b/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol index 628fc52..34f7751 100644 --- a/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol +++ b/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol @@ -143,11 +143,11 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanRequest { amount: request.loanAmount }); - // Create loan object + // Create loan terms object loanTerms = PWNLOANTerms.Simple({ lender: lender, borrower: borrower, - expiration: uint40(block.timestamp) + request.duration, + defaultTimestamp: uint40(block.timestamp) + request.duration, collateral: collateral, asset: loanAsset, loanRepayAmount: request.loanAmount + request.loanYield diff --git a/src/loan/terms/simple/loan/PWNSimpleLoan.sol b/src/loan/terms/simple/loan/PWNSimpleLoan.sol index 105b4b2..d314a9a 100644 --- a/src/loan/terms/simple/loan/PWNSimpleLoan.sol +++ b/src/loan/terms/simple/loan/PWNSimpleLoan.sol @@ -42,7 +42,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { * @notice Struct defining a simple loan. * @param status 0 == none/dead || 2 == running/accepted offer/accepted request || 3 == paid back || 4 == expired. * @param borrower Address of a borrower. - * @param expiration Unix timestamp (in seconds) setting up a default date. + * @param defaultTimestamp Unix timestamp (in seconds) setting up a default date. * @param loanAssetAddress Address of an asset used as a loan credit. * @param loanRepayAmount Amount of a loan asset to be paid back. * @param collateral Asset used as a loan collateral. For a definition see { MultiToken dependency lib }. @@ -51,7 +51,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { struct LOAN { uint8 status; address borrower; - uint40 expiration; + uint40 defaultTimestamp; address loanAssetAddress; uint256 loanRepayAmount; MultiToken.Asset collateral; @@ -186,7 +186,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { LOAN storage loan = LOANs[loanId]; loan.status = 2; loan.borrower = loanTerms.borrower; - loan.expiration = loanTerms.expiration; + loan.defaultTimestamp = loanTerms.defaultTimestamp; loan.loanAssetAddress = loanTerms.asset.assetAddress; loan.loanRepayAmount = loanTerms.loanRepayAmount; loan.collateral = loanTerms.collateral; @@ -265,7 +265,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { LOAN storage loan = LOANs[loanId]; // Check that the original loan can be repaid, revert if not - _checkLoanCanBeRepaid(loan.status, loan.expiration); + _checkLoanCanBeRepaid(loan.status, loan.defaultTimestamp); // Create loan terms or revert if factory contract is not tagged in PWN Hub (PWNLOANTerms.Simple memory loanTerms, bytes32 factoryDataHash) @@ -449,7 +449,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { ) external { LOAN memory loan = LOANs[loanId]; - _checkLoanCanBeRepaid(loan.status, loan.expiration); + _checkLoanCanBeRepaid(loan.status, loan.defaultTimestamp); (bool repayLoanDirectly, address loanOwner) = _deleteOrUpdateRepaidLoan(loanId); @@ -468,15 +468,15 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { * @notice Check if the loan can be repaid. * @dev The function will revert if the loan cannot be repaid. * @param status Loan status. - * @param expiration Loan expiration date. + * @param defaultTimestamp Loan default timestamp. */ - function _checkLoanCanBeRepaid(uint8 status, uint40 expiration) private view { + function _checkLoanCanBeRepaid(uint8 status, uint40 defaultTimestamp) private view { // Check that loan exists and is not from a different loan contract if (status == 0) revert NonExistingLoan(); // Check that loan is running if (status != 2) revert InvalidLoanStatus(status); - // Check that loan is not expired - if (expiration <= block.timestamp) revert LoanDefaulted(expiration); + // Check that loan is not defaulted + if (defaultTimestamp <= block.timestamp) revert LoanDefaulted(defaultTimestamp); } /** @@ -583,7 +583,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { else if (loan.status == 3) _settleLoanClaim({ loanId: loanId, loanOwner: msg.sender, defaulted: false }); // Loan is running but expired - else if (loan.status == 2 && loan.expiration <= block.timestamp) + else if (loan.status == 2 && loan.defaultTimestamp <= block.timestamp) _settleLoanClaim({ loanId: loanId, loanOwner: msg.sender, defaulted: true }); // Loan is in wrong state else @@ -647,11 +647,11 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { revert InvalidExtendedExpirationDate(); if (extendedExpirationDate <= uint40(block.timestamp)) // have to extend expiration futher in time revert InvalidExtendedExpirationDate(); - if (extendedExpirationDate <= loan.expiration) // have to be later than current expiration date + if (extendedExpirationDate <= loan.defaultTimestamp) // have to be later than current expiration date revert InvalidExtendedExpirationDate(); // Extend expiration date - loan.expiration = extendedExpirationDate; + loan.defaultTimestamp = extendedExpirationDate; emit LOANExpirationDateExtended({ loanId: loanId, extendedExpirationDate: extendedExpirationDate }); } @@ -678,7 +678,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { */ function _getLOANStatus(uint256 loanId) private view returns (uint8) { LOAN storage loan = LOANs[loanId]; - return (loan.status == 2 && loan.expiration <= block.timestamp) ? 4 : loan.status; + return (loan.status == 2 && loan.defaultTimestamp <= block.timestamp) ? 4 : loan.status; } @@ -708,12 +708,12 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { return bytes32(0); // The only mutable state properties are: - // - status, expiration + // - status, defaultTimestamp // Status is updated for expired loans based on block.timestamp. // Others don't have to be part of the state fingerprint as it does not act as a token identification. return keccak256(abi.encode( _getLOANStatus(tokenId), - loan.expiration + loan.defaultTimestamp )); } diff --git a/test/unit/PWNSimpleLoan.t.sol b/test/unit/PWNSimpleLoan.t.sol index fe6ed36..cfbec55 100644 --- a/test/unit/PWNSimpleLoan.t.sol +++ b/test/unit/PWNSimpleLoan.t.sol @@ -86,7 +86,7 @@ abstract contract PWNSimpleLoanTest is Test { simpleLoan = PWNSimpleLoan.LOAN({ status: 2, borrower: borrower, - expiration: uint40(block.timestamp + 40039), + defaultTimestamp: uint40(block.timestamp + 40039), loanAssetAddress: address(fungibleAsset), loanRepayAmount: 6731, collateral: MultiToken.ERC721(address(nonFungibleAsset), 2), @@ -96,7 +96,7 @@ abstract contract PWNSimpleLoanTest is Test { simpleLoanTerms = PWNLOANTerms.Simple({ lender: lender, borrower: borrower, - expiration: uint40(block.timestamp + 40039), + defaultTimestamp: uint40(block.timestamp + 40039), collateral: MultiToken.ERC721(address(nonFungibleAsset), 2), asset: MultiToken.ERC20(address(fungibleAsset), 100), loanRepayAmount: 6731 @@ -105,7 +105,7 @@ abstract contract PWNSimpleLoanTest is Test { nonExistingLoan = PWNSimpleLoan.LOAN({ status: 0, borrower: address(0), - expiration: 0, + defaultTimestamp: 0, loanAssetAddress: address(0), loanRepayAmount: 0, collateral: MultiToken.Asset(MultiToken.Category(0), address(0), 0, 0), @@ -145,7 +145,7 @@ abstract contract PWNSimpleLoanTest is Test { function _assertLOANEq(PWNSimpleLoan.LOAN memory _simpleLoan1, PWNSimpleLoan.LOAN memory _simpleLoan2) internal { assertEq(_simpleLoan1.status, _simpleLoan2.status); assertEq(_simpleLoan1.borrower, _simpleLoan2.borrower); - assertEq(_simpleLoan1.expiration, _simpleLoan2.expiration); + assertEq(_simpleLoan1.defaultTimestamp, _simpleLoan2.defaultTimestamp); assertEq(_simpleLoan1.loanAssetAddress, _simpleLoan2.loanAssetAddress); assertEq(_simpleLoan1.loanRepayAmount, _simpleLoan2.loanRepayAmount); assertEq(uint8(_simpleLoan1.collateral.category), uint8(_simpleLoan2.collateral.category)); @@ -160,8 +160,8 @@ abstract contract PWNSimpleLoanTest is Test { _loanId, LOANS_SLOT ))); - // Status, borrower address & expiration in one storage slot - _assertLOANWord(loanSlot + 0, abi.encodePacked(uint48(0), _simpleLoan.expiration, _simpleLoan.borrower, _simpleLoan.status)); + // Status, borrower address & defaultTimestamp in one storage slot + _assertLOANWord(loanSlot + 0, abi.encodePacked(uint48(0), _simpleLoan.defaultTimestamp, _simpleLoan.borrower, _simpleLoan.status)); // Loan asset address _assertLOANWord(loanSlot + 1, abi.encodePacked(uint96(0), _simpleLoan.loanAssetAddress)); // Loan repay amount @@ -182,8 +182,8 @@ abstract contract PWNSimpleLoanTest is Test { _loanId, LOANS_SLOT ))); - // Status, borrower address & expiration in one storage slot - _storeLOANWord(loanSlot + 0, abi.encodePacked(uint48(0), _simpleLoan.expiration, _simpleLoan.borrower, _simpleLoan.status)); + // Status, borrower address & defaultTimestamp in one storage slot + _storeLOANWord(loanSlot + 0, abi.encodePacked(uint48(0), _simpleLoan.defaultTimestamp, _simpleLoan.borrower, _simpleLoan.status)); // Loan asset address _storeLOANWord(loanSlot + 1, abi.encodePacked(uint96(0), _simpleLoan.loanAssetAddress)); // Loan repay amount @@ -405,7 +405,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { refinancedLoan = PWNSimpleLoan.LOAN({ status: 2, borrower: borrower, - expiration: uint40(block.timestamp + 40039), + defaultTimestamp: uint40(block.timestamp + 40039), loanAssetAddress: address(fungibleAsset), loanRepayAmount: 6731, collateral: MultiToken.ERC721(address(nonFungibleAsset), 2), @@ -415,7 +415,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { refinancedLoanTerms = PWNLOANTerms.Simple({ lender: lender, borrower: borrower, - expiration: uint40(block.timestamp + 40039), + defaultTimestamp: uint40(block.timestamp + 40039), collateral: MultiToken.ERC721(address(nonFungibleAsset), 2), asset: MultiToken.ERC20(address(fungibleAsset), 100), loanRepayAmount: 6731 @@ -448,10 +448,10 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); } - function test_shouldFail_whenLoanIsExpired() external { - vm.warp(simpleLoan.expiration + 10000); + function test_shouldFail_whenLoanIsDefaulted() external { + vm.warp(simpleLoan.defaultTimestamp + 10000); - vm.expectRevert(abi.encodeWithSelector(LoanDefaulted.selector, simpleLoan.expiration)); + vm.expectRevert(abi.encodeWithSelector(LoanDefaulted.selector, simpleLoan.defaultTimestamp)); loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); } @@ -1065,12 +1065,12 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { loan.repayLOAN(loanId, loanAssetPermit); } - function test_shouldFail_whenLoanIsExpired() external { + function test_shouldFail_whenLoanIsDefaulted() external { _mockLOAN(loanId, simpleLoan); - vm.warp(simpleLoan.expiration + 10000); + vm.warp(simpleLoan.defaultTimestamp + 10000); - vm.expectRevert(abi.encodeWithSelector(LoanDefaulted.selector, simpleLoan.expiration)); + vm.expectRevert(abi.encodeWithSelector(LoanDefaulted.selector, simpleLoan.defaultTimestamp)); loan.repayLOAN(loanId, loanAssetPermit); } @@ -1235,11 +1235,11 @@ contract PWNSimpleLoan_ClaimLOAN_Test is PWNSimpleLoanTest { loan.claimLOAN(loanId); } - function test_shouldPass_whenLoanIsExpired() external { + function test_shouldPass_whenLoanIsDefaulted() external { simpleLoan.status = 2; _mockLOAN(loanId, simpleLoan); - vm.warp(simpleLoan.expiration + 10000); + vm.warp(simpleLoan.defaultTimestamp + 10000); vm.prank(lender); loan.claimLOAN(loanId); @@ -1280,11 +1280,11 @@ contract PWNSimpleLoan_ClaimLOAN_Test is PWNSimpleLoanTest { loan.claimLOAN(loanId); } - function test_shouldTransferCollateralToLender_whenLoanIsExpired() external { + function test_shouldTransferCollateralToLender_whenLoanIsDefaulted() external { simpleLoan.status = 2; _mockLOAN(loanId, simpleLoan); - vm.warp(simpleLoan.expiration + 10000); + vm.warp(simpleLoan.defaultTimestamp + 10000); vm.expectCall( simpleLoan.collateral.assetAddress, @@ -1312,7 +1312,7 @@ contract PWNSimpleLoan_ClaimLOAN_Test is PWNSimpleLoanTest { simpleLoan.status = 2; _mockLOAN(loanId, simpleLoan); - vm.warp(simpleLoan.expiration + 10000); + vm.warp(simpleLoan.defaultTimestamp + 10000); vm.expectEmit(true, true, false, false); emit LOANClaimed(loanId, true); @@ -1347,7 +1347,7 @@ contract PWNSimpleLoan_ExtendExpirationDate_Test is PWNSimpleLoanTest { vm.expectRevert(abi.encodeWithSelector(CallerNotLOANTokenHolder.selector)); vm.prank(borrower); - loan.extendLOANExpirationDate(loanId, simpleLoan.expiration + 1); + loan.extendLOANExpirationDate(loanId, simpleLoan.defaultTimestamp + 1); } function test_shouldFail_whenExtendedExpirationDateIsSmallerThanCurrentExpirationDate() external { @@ -1355,17 +1355,17 @@ contract PWNSimpleLoan_ExtendExpirationDate_Test is PWNSimpleLoanTest { vm.expectRevert(abi.encodeWithSelector(InvalidExtendedExpirationDate.selector)); vm.prank(lender); - loan.extendLOANExpirationDate(loanId, simpleLoan.expiration - 1); + loan.extendLOANExpirationDate(loanId, simpleLoan.defaultTimestamp - 1); } function test_shouldFail_whenExtendedExpirationDateIsSmallerThanCurrentDate() external { _mockLOAN(loanId, simpleLoan); - vm.warp(simpleLoan.expiration + 1000); + vm.warp(simpleLoan.defaultTimestamp + 1000); vm.expectRevert(abi.encodeWithSelector(InvalidExtendedExpirationDate.selector)); vm.prank(lender); - loan.extendLOANExpirationDate(loanId, simpleLoan.expiration + 500); + loan.extendLOANExpirationDate(loanId, simpleLoan.defaultTimestamp + 500); } function test_shouldFail_whenExtendedExpirationDateIsBiggerThanMaxExpirationExtension() external { @@ -1379,7 +1379,7 @@ contract PWNSimpleLoan_ExtendExpirationDate_Test is PWNSimpleLoanTest { function test_shouldStoreExtendedExpirationDate() external { _mockLOAN(loanId, simpleLoan); - uint40 newExpiration = uint40(simpleLoan.expiration + 10000); + uint40 newExpiration = uint40(simpleLoan.defaultTimestamp + 10000); vm.prank(lender); loan.extendLOANExpirationDate(loanId, newExpiration); @@ -1393,7 +1393,7 @@ contract PWNSimpleLoan_ExtendExpirationDate_Test is PWNSimpleLoanTest { function test_shouldEmitEvent_LOANExpirationDateExtended() external { _mockLOAN(loanId, simpleLoan); - uint40 newExpiration = uint40(simpleLoan.expiration + 10000); + uint40 newExpiration = uint40(simpleLoan.defaultTimestamp + 10000); vm.expectEmit(true, true, true, true); emit LOANExpirationDateExtended(loanId, newExpiration); @@ -1420,7 +1420,7 @@ contract PWNSimpleLoan_GetLOAN_Test is PWNSimpleLoanTest { function test_shouldReturnExpiredStatus_whenLOANExpired() external { _mockLOAN(loanId, simpleLoan); - vm.warp(simpleLoan.expiration + 10000); + vm.warp(simpleLoan.defaultTimestamp + 10000); simpleLoan.status = 4; _assertLOANEq(loan.getLOAN(loanId), simpleLoan); @@ -1487,14 +1487,14 @@ contract PWNSimpleLoan_GetStateFingerprint_Test is PWNSimpleLoanTest { function test_shouldReturnCorrectStateFingerprint() external { _mockLOAN(loanId, simpleLoan); - vm.warp(simpleLoan.expiration - 10000); - assertEq(loan.getStateFingerprint(loanId), keccak256(abi.encode(2, simpleLoan.expiration))); + vm.warp(simpleLoan.defaultTimestamp - 10000); + assertEq(loan.getStateFingerprint(loanId), keccak256(abi.encode(2, simpleLoan.defaultTimestamp))); - vm.warp(simpleLoan.expiration + 10000); - assertEq(loan.getStateFingerprint(loanId), keccak256(abi.encode(4, simpleLoan.expiration))); + vm.warp(simpleLoan.defaultTimestamp + 10000); + assertEq(loan.getStateFingerprint(loanId), keccak256(abi.encode(4, simpleLoan.defaultTimestamp))); simpleLoan.status = 3; - simpleLoan.expiration = 60039; + simpleLoan.defaultTimestamp = 60039; _mockLOAN(loanId, simpleLoan); assertEq(loan.getStateFingerprint(loanId), keccak256(abi.encode(3, 60039))); } diff --git a/test/unit/PWNSimpleLoanListOffer.t.sol b/test/unit/PWNSimpleLoanListOffer.t.sol index 790cab3..39136b7 100644 --- a/test/unit/PWNSimpleLoanListOffer.t.sol +++ b/test/unit/PWNSimpleLoanListOffer.t.sol @@ -351,7 +351,7 @@ contract PWNSimpleLoanListOffer_CreateLOANTerms_Test is PWNSimpleLoanListOfferTe assertTrue(loanTerms.lender == offer.lender); assertTrue(loanTerms.borrower == borrower); - assertTrue(loanTerms.expiration == currentTimestamp + offer.duration); + assertTrue(loanTerms.defaultTimestamp == currentTimestamp + offer.duration); assertTrue(loanTerms.collateral.category == offer.collateralCategory); assertTrue(loanTerms.collateral.assetAddress == offer.collateralAddress); assertTrue(loanTerms.collateral.id == offerValues.collateralId); diff --git a/test/unit/PWNSimpleLoanSimpleOffer.t.sol b/test/unit/PWNSimpleLoanSimpleOffer.t.sol index 17e98ce..e6e5d8b 100644 --- a/test/unit/PWNSimpleLoanSimpleOffer.t.sol +++ b/test/unit/PWNSimpleLoanSimpleOffer.t.sol @@ -307,7 +307,7 @@ contract PWNSimpleLoanSimpleOffer_CreateLOANTerms_Test is PWNSimpleLoanSimpleOff assertTrue(loanTerms.lender == offer.lender); assertTrue(loanTerms.borrower == borrower); - assertTrue(loanTerms.expiration == currentTimestamp + offer.duration); + assertTrue(loanTerms.defaultTimestamp == currentTimestamp + offer.duration); assertTrue(loanTerms.collateral.category == offer.collateralCategory); assertTrue(loanTerms.collateral.assetAddress == offer.collateralAddress); assertTrue(loanTerms.collateral.id == offer.collateralId); diff --git a/test/unit/PWNSimpleLoanSimpleRequest.t.sol b/test/unit/PWNSimpleLoanSimpleRequest.t.sol index 23b9911..61f3824 100644 --- a/test/unit/PWNSimpleLoanSimpleRequest.t.sol +++ b/test/unit/PWNSimpleLoanSimpleRequest.t.sol @@ -291,7 +291,7 @@ contract PWNSimpleLoanSimpleRequest_CreateLOANTerms_Test is PWNSimpleLoanSimpleR assertTrue(loanTerms.lender == lender); assertTrue(loanTerms.borrower == request.borrower); - assertTrue(loanTerms.expiration == currentTimestamp + request.duration); + assertTrue(loanTerms.defaultTimestamp == currentTimestamp + request.duration); assertTrue(loanTerms.collateral.category == request.collateralCategory); assertTrue(loanTerms.collateral.assetAddress == request.collateralAddress); assertTrue(loanTerms.collateral.id == request.collateralId); From 0a6e7715a6d4034f3d6fd5fdca80f722d4c82fdd Mon Sep 17 00:00:00 2001 From: ashhanai Date: Wed, 21 Feb 2024 18:41:24 +0000 Subject: [PATCH 019/129] feat(refinancing-loan-id): add refinancing loan id into loan request and extend simple loan terms by canCreate and canRefinance flags --- src/PWNErrors.sol | 3 ++ src/loan/terms/PWNLOANTerms.sol | 6 +++ .../factory/offer/PWNSimpleLoanListOffer.sol | 5 ++- .../offer/PWNSimpleLoanSimpleOffer.sol | 5 ++- .../request/PWNSimpleLoanSimpleRequest.sol | 9 +++- src/loan/terms/simple/loan/PWNSimpleLoan.sol | 42 ++++++++++++++----- .../contracts/PWNSimpleLoanIntegration.t.sol | 1 + ...WNSimpleLoanSimpleRequestIntegration.t.sol | 1 + test/unit/PWNSimpleLoan.t.sol | 36 +++++++++++++++- test/unit/PWNSimpleLoanListOffer.t.sol | 2 + test/unit/PWNSimpleLoanSimpleOffer.t.sol | 2 + test/unit/PWNSimpleLoanSimpleRequest.t.sol | 5 ++- 12 files changed, 100 insertions(+), 17 deletions(-) diff --git a/src/PWNErrors.sol b/src/PWNErrors.sol index 1867ee1..8ba8c5b 100644 --- a/src/PWNErrors.sol +++ b/src/PWNErrors.sol @@ -43,6 +43,9 @@ error RequestExpired(); // Request & Offer error InvalidDuration(); +error InvalidCreateTerms(); +error InvalidRefinanceTerms(); +error InvalidRefinancingLoanId(uint256 refinancingLoanId); // Input data error InvalidInputData(); diff --git a/src/loan/terms/PWNLOANTerms.sol b/src/loan/terms/PWNLOANTerms.sol index 3e90a77..7b9256e 100644 --- a/src/loan/terms/PWNLOANTerms.sol +++ b/src/loan/terms/PWNLOANTerms.sol @@ -15,6 +15,9 @@ library PWNLOANTerms { * @param collateral Asset used as a loan collateral. For a definition see { MultiToken dependency lib }. * @param asset Asset used as a loan credit. For a definition see { MultiToken dependency lib }. * @param loanRepayAmount Amount of a loan asset to be paid back. + * @param canCreate If true, the terms can be used to create a new loan. + * @param canRefinance If true, the terms can be used to refinance a running loan. + * @param refinancingLoanId Id of a loan which is refinanced by this terms. If the id is 0, any loan can be refinanced. */ struct Simple { address lender; @@ -23,6 +26,9 @@ library PWNLOANTerms { MultiToken.Asset collateral; MultiToken.Asset asset; uint256 loanRepayAmount; + bool canCreate; + bool canRefinance; + uint256 refinancingLoanId; } } diff --git a/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol b/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol index 996aa3e..9d3c419 100644 --- a/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol +++ b/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol @@ -179,7 +179,10 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanOffer { defaultTimestamp: uint40(block.timestamp) + offer.duration, collateral: collateral, asset: loanAsset, - loanRepayAmount: offer.loanAmount + offer.loanYield + loanRepayAmount: offer.loanAmount + offer.loanYield, + canCreate: true, + canRefinance: true, + refinancingLoanId: 0 }); // Revoke offer if not persistent diff --git a/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol b/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol index fc9c7be..70593dd 100644 --- a/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol +++ b/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol @@ -152,7 +152,10 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanOffer { defaultTimestamp: uint40(block.timestamp) + offer.duration, collateral: collateral, asset: loanAsset, - loanRepayAmount: offer.loanAmount + offer.loanYield + loanRepayAmount: offer.loanAmount + offer.loanYield, + canCreate: true, + canRefinance: true, + refinancingLoanId: 0 }); // Revoke offer if not persistent diff --git a/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol b/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol index 34f7751..deada0c 100644 --- a/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol +++ b/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol @@ -25,7 +25,7 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanRequest { * @dev EIP-712 simple request struct type hash. */ bytes32 public constant REQUEST_TYPEHASH = keccak256( - "Request(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,address loanAssetAddress,uint256 loanAmount,uint256 loanYield,uint32 duration,uint40 expiration,address allowedLender,address borrower,uint256 nonce)" + "Request(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,address loanAssetAddress,uint256 loanAmount,uint256 loanYield,uint32 duration,uint40 expiration,address allowedLender,address borrower,uint256 refinancingLoanId,uint256 nonce)" ); bytes32 public immutable DOMAIN_SEPARATOR; @@ -43,6 +43,7 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanRequest { * @param expiration Request expiration timestamp in seconds. * @param allowedLender Address of an allowed lender. Only this address can accept a request. If the address is zero address, anybody with a loan asset can accept the request. * @param borrower Address of a borrower. This address has to sign a request to be valid. + * @param refinancingLoanId Id of a loan which is refinanced by this request. If the id is 0, the request is not a refinancing request. * @param nonce Additional value to enable identical requests in time. Without it, it would be impossible to make again request, which was once revoked. * Can be used to create a group of requests, where accepting one request will make other requests in the group revoked. */ @@ -58,6 +59,7 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanRequest { uint40 expiration; address allowedLender; address borrower; + uint256 refinancingLoanId; uint256 nonce; } @@ -150,7 +152,10 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanRequest { defaultTimestamp: uint40(block.timestamp) + request.duration, collateral: collateral, asset: loanAsset, - loanRepayAmount: request.loanAmount + request.loanYield + loanRepayAmount: request.loanAmount + request.loanYield, + canCreate: request.refinancingLoanId == 0, + canRefinance: request.refinancingLoanId != 0, + refinancingLoanId: request.refinancingLoanId }); revokedRequestNonce.revokeNonce(borrower, request.nonce); diff --git a/src/loan/terms/simple/loan/PWNSimpleLoan.sol b/src/loan/terms/simple/loan/PWNSimpleLoan.sol index d314a9a..c4f90ff 100644 --- a/src/loan/terms/simple/loan/PWNSimpleLoan.sol +++ b/src/loan/terms/simple/loan/PWNSimpleLoan.sol @@ -126,13 +126,8 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { (PWNLOANTerms.Simple memory loanTerms, bytes32 factoryDataHash) = _createLoanTerms(loanTermsFactoryContract, loanTermsFactoryData, signature); - // Check loan asset validity - if (!MultiToken.isValid(loanTerms.asset, categoryRegistry)) - revert InvalidLoanAsset(); - - // Check collateral validity - if (!MultiToken.isValid(loanTerms.collateral, categoryRegistry)) - revert InvalidCollateralAsset(); + // Check loan terms validity, revert if not + _checkNewLoanTerms(loanTerms); // Create a new loan loanId = _createLoan(loanTerms, factoryDataHash, loanTermsFactoryContract); @@ -167,6 +162,25 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { }); } + /** + * @notice Check if the loan terms are valid for creating a new loan. + * @dev The function will revert if the loan terms are not valid for creating a new loan. + * @param loanTerms New loan terms struct. + */ + function _checkNewLoanTerms(PWNLOANTerms.Simple memory loanTerms) private view { + // Check loan asset validity + if (!MultiToken.isValid(loanTerms.asset, categoryRegistry)) + revert InvalidLoanAsset(); + + // Check collateral validity + if (!MultiToken.isValid(loanTerms.collateral, categoryRegistry)) + revert InvalidCollateralAsset(); + + // Check that the terms can create a new loan + if (!loanTerms.canCreate) + revert InvalidCreateTerms(); + } + /** * @notice Store a new loan in the contract state, mints new LOAN token, and emit a `LOANCreated` event. * @param loanTerms Loan terms struct. @@ -272,7 +286,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { = _createLoanTerms(loanTermsFactoryContract, loanTermsFactoryData, signature); // Check loan terms validity, revert if not - _checkRefinanceLoanTerms(loan, loanTerms); + _checkRefinanceLoanTerms(loanId, loanTerms); // Create a new loan refinancedLoanId = _createLoan(loanTerms, factoryDataHash, loanTermsFactoryContract); @@ -290,10 +304,12 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { /** * @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 loan Original loan struct. + * @param loanId Original loan id. * @param loanTerms Refinancing loan terms struct. */ - function _checkRefinanceLoanTerms(LOAN storage loan, PWNLOANTerms.Simple memory loanTerms) private view { + function _checkRefinanceLoanTerms(uint256 loanId, PWNLOANTerms.Simple memory loanTerms) private view { + LOAN storage loan = LOANs[loanId]; + // Check that the loan 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. @@ -317,6 +333,12 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { newBorrower: loanTerms.borrower }); } + + // Check that the terms can refinance a loan + if (!loanTerms.canRefinance) + revert InvalidRefinanceTerms(); + if (loanTerms.refinancingLoanId != 0 && loanTerms.refinancingLoanId != loanId) + revert InvalidRefinancingLoanId({ refinancingLoanId: loanTerms.refinancingLoanId }); } /** diff --git a/test/integration/contracts/PWNSimpleLoanIntegration.t.sol b/test/integration/contracts/PWNSimpleLoanIntegration.t.sol index 6a3e1a1..5985958 100644 --- a/test/integration/contracts/PWNSimpleLoanIntegration.t.sol +++ b/test/integration/contracts/PWNSimpleLoanIntegration.t.sol @@ -159,6 +159,7 @@ contract PWNSimpleLoanIntegrationTest is BaseIntegrationTest { expiration: 0, allowedLender: lender, borrower: borrower, + refinancingLoanId: 0, nonce: nonce }); diff --git a/test/integration/contracts/PWNSimpleLoanSimpleRequestIntegration.t.sol b/test/integration/contracts/PWNSimpleLoanSimpleRequestIntegration.t.sol index 7fd0a7c..92a7736 100644 --- a/test/integration/contracts/PWNSimpleLoanSimpleRequestIntegration.t.sol +++ b/test/integration/contracts/PWNSimpleLoanSimpleRequestIntegration.t.sol @@ -30,6 +30,7 @@ contract PWNSimpleLoanSimpleRequestIntegrationTest is BaseIntegrationTest { expiration: 0, allowedLender: lender, borrower: borrower, + refinancingLoanId: 0, nonce: nonce }); bytes memory signature1 = _sign(borrowerPK, simpleLoanSimpleRequest.getRequestHash(request)); diff --git a/test/unit/PWNSimpleLoan.t.sol b/test/unit/PWNSimpleLoan.t.sol index cfbec55..736d5b6 100644 --- a/test/unit/PWNSimpleLoan.t.sol +++ b/test/unit/PWNSimpleLoan.t.sol @@ -99,7 +99,10 @@ abstract contract PWNSimpleLoanTest is Test { defaultTimestamp: uint40(block.timestamp + 40039), collateral: MultiToken.ERC721(address(nonFungibleAsset), 2), asset: MultiToken.ERC20(address(fungibleAsset), 100), - loanRepayAmount: 6731 + loanRepayAmount: 6731, + canCreate: true, + canRefinance: true, + refinancingLoanId: 0 }); nonExistingLoan = PWNSimpleLoan.LOAN({ @@ -286,6 +289,14 @@ contract PWNSimpleLoan_CreateLoan_Test is PWNSimpleLoanTest { loan.createLOAN(loanFactory, loanFactoryData, signature, loanAssetPermit, collateralPermit); } + function test_shouldFail_whenInvalidCreateTerms() external { + simpleLoanTerms.canCreate = false; + _mockLoanTerms(simpleLoanTerms, loanFactoryDataHash); + + vm.expectRevert(abi.encodeWithSelector(InvalidCreateTerms.selector)); + loan.createLOAN(loanFactory, loanFactoryData, signature, loanAssetPermit, collateralPermit); + } + function test_shouldMintLOANToken() external { vm.expectCall( address(loanToken), @@ -418,7 +429,10 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { defaultTimestamp: uint40(block.timestamp + 40039), collateral: MultiToken.ERC721(address(nonFungibleAsset), 2), asset: MultiToken.ERC20(address(fungibleAsset), 100), - loanRepayAmount: 6731 + loanRepayAmount: 6731, + canCreate: false, + canRefinance: true, + refinancingLoanId: 0 }); loanAssetPermit = abi.encodePacked(uint256(1), uint256(2), uint256(3), uint8(4)); @@ -543,6 +557,24 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); } + function test_shouldFail_whenInvalidRefinanceTerms() external { + refinancedLoanTerms.canRefinance = false; + _mockLoanTerms(refinancedLoanTerms, loanFactoryDataHash); + + vm.expectRevert(abi.encodeWithSelector(InvalidRefinanceTerms.selector)); + loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); + } + + function testFuzz_shouldFail_whenInvalidRefinancingLoanId(uint256 _loanId) external { + vm.assume(_loanId != loanId && _loanId != 0); + + refinancedLoanTerms.refinancingLoanId = _loanId; + _mockLoanTerms(refinancedLoanTerms, loanFactoryDataHash); + + vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, _loanId)); + loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); + } + function test_shouldMintLOANToken() external { vm.expectCall(address(loanToken), abi.encodeWithSignature("mint(address)")); diff --git a/test/unit/PWNSimpleLoanListOffer.t.sol b/test/unit/PWNSimpleLoanListOffer.t.sol index 39136b7..b467ed9 100644 --- a/test/unit/PWNSimpleLoanListOffer.t.sol +++ b/test/unit/PWNSimpleLoanListOffer.t.sol @@ -361,6 +361,8 @@ contract PWNSimpleLoanListOffer_CreateLOANTerms_Test is PWNSimpleLoanListOfferTe assertTrue(loanTerms.asset.id == 0); assertTrue(loanTerms.asset.amount == offer.loanAmount); assertTrue(loanTerms.loanRepayAmount == offer.loanAmount + offer.loanYield); + assertTrue(loanTerms.canRefinance == true); + assertTrue(loanTerms.refinancingLoanId == 0); assertTrue(offerHash == _offerHash(offer)); } diff --git a/test/unit/PWNSimpleLoanSimpleOffer.t.sol b/test/unit/PWNSimpleLoanSimpleOffer.t.sol index e6e5d8b..af5d4d4 100644 --- a/test/unit/PWNSimpleLoanSimpleOffer.t.sol +++ b/test/unit/PWNSimpleLoanSimpleOffer.t.sol @@ -317,6 +317,8 @@ contract PWNSimpleLoanSimpleOffer_CreateLOANTerms_Test is PWNSimpleLoanSimpleOff assertTrue(loanTerms.asset.id == 0); assertTrue(loanTerms.asset.amount == offer.loanAmount); assertTrue(loanTerms.loanRepayAmount == offer.loanAmount + offer.loanYield); + assertTrue(loanTerms.canRefinance == true); + assertTrue(loanTerms.refinancingLoanId == 0); assertTrue(offerHash == _offerHash(offer)); } diff --git a/test/unit/PWNSimpleLoanSimpleRequest.t.sol b/test/unit/PWNSimpleLoanSimpleRequest.t.sol index 61f3824..53f0f87 100644 --- a/test/unit/PWNSimpleLoanSimpleRequest.t.sol +++ b/test/unit/PWNSimpleLoanSimpleRequest.t.sol @@ -45,6 +45,7 @@ abstract contract PWNSimpleLoanSimpleRequestTest is Test { expiration: 0, allowedLender: address(0), borrower: borrower, + refinancingLoanId: 0, nonce: uint256(keccak256("nonce_1")) }); @@ -67,7 +68,7 @@ abstract contract PWNSimpleLoanSimpleRequestTest is Test { address(requestContract) )), keccak256(abi.encodePacked( - keccak256("Request(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,address loanAssetAddress,uint256 loanAmount,uint256 loanYield,uint32 duration,uint40 expiration,address allowedLender,address borrower,uint256 nonce)"), + keccak256("Request(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,address loanAssetAddress,uint256 loanAmount,uint256 loanYield,uint32 duration,uint40 expiration,address allowedLender,address borrower,uint256 refinancingLoanId,uint256 nonce)"), abi.encode(_request) )) )); @@ -301,6 +302,8 @@ contract PWNSimpleLoanSimpleRequest_CreateLOANTerms_Test is PWNSimpleLoanSimpleR assertTrue(loanTerms.asset.id == 0); assertTrue(loanTerms.asset.amount == request.loanAmount); assertTrue(loanTerms.loanRepayAmount == request.loanAmount + request.loanYield); + assertTrue(loanTerms.canRefinance == false); + assertTrue(loanTerms.refinancingLoanId == request.refinancingLoanId); assertTrue(requestHash == _requestHash(request)); } From d48539cd21f3d618490d573658cf72d00ccc4cc0 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Thu, 22 Feb 2024 11:20:47 +0000 Subject: [PATCH 020/129] feat(is-valid-asset): expose MultiToken isValid function on loan contract --- src/loan/terms/simple/loan/PWNSimpleLoan.sol | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/loan/terms/simple/loan/PWNSimpleLoan.sol b/src/loan/terms/simple/loan/PWNSimpleLoan.sol index c4f90ff..e6f3800 100644 --- a/src/loan/terms/simple/loan/PWNSimpleLoan.sol +++ b/src/loan/terms/simple/loan/PWNSimpleLoan.sol @@ -169,11 +169,11 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { */ function _checkNewLoanTerms(PWNLOANTerms.Simple memory loanTerms) private view { // Check loan asset validity - if (!MultiToken.isValid(loanTerms.asset, categoryRegistry)) + if (!isValidAsset(loanTerms.asset)) revert InvalidLoanAsset(); // Check collateral validity - if (!MultiToken.isValid(loanTerms.collateral, categoryRegistry)) + if (!isValidAsset(loanTerms.collateral)) revert InvalidCollateralAsset(); // Check that the terms can create a new loan @@ -704,6 +704,21 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { } + /*----------------------------------------------------------*| + |* # MultiToken *| + |*----------------------------------------------------------*/ + + /** + * @notice Check if the asset is valid with the MultiToken dependency lib and the category registry. + * @dev See MultiToken.isValid for more details. + * @param asset Asset to be checked. + * @return True if the asset is valid. + */ + function isValidAsset(MultiToken.Asset memory asset) public view returns (bool) { + return MultiToken.isValid(asset, categoryRegistry); + } + + /*----------------------------------------------------------*| |* # IPWNLoanMetadataProvider *| |*----------------------------------------------------------*/ From 7c623a4e9a4777e6362030fa748ab114557c9234 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Mon, 26 Feb 2024 12:37:29 +0100 Subject: [PATCH 021/129] feat(daily-interest-rate): update fixed loan interest to interest accrued daily --- src/PWNErrors.sol | 1 + src/loan/terms/PWNLOANTerms.sol | 6 +- .../factory/PWNSimpleLoanTermsFactory.sol | 1 + .../factory/offer/PWNSimpleLoanListOffer.sol | 20 +- .../offer/PWNSimpleLoanSimpleOffer.sol | 20 +- .../request/PWNSimpleLoanSimpleRequest.sol | 20 +- src/loan/terms/simple/loan/PWNSimpleLoan.sol | 152 +++-- test/unit/PWNSimpleLoan.t.sol | 628 ++++++++++++------ test/unit/PWNSimpleLoanListOffer.t.sol | 36 +- test/unit/PWNSimpleLoanSimpleOffer.t.sol | 36 +- test/unit/PWNSimpleLoanSimpleRequest.t.sol | 30 +- 11 files changed, 652 insertions(+), 298 deletions(-) diff --git a/src/PWNErrors.sol b/src/PWNErrors.sol index 8ba8c5b..489f740 100644 --- a/src/PWNErrors.sol +++ b/src/PWNErrors.sol @@ -46,6 +46,7 @@ error InvalidDuration(); error InvalidCreateTerms(); error InvalidRefinanceTerms(); error InvalidRefinancingLoanId(uint256 refinancingLoanId); +error AccruingInterestAPROutOfBounds(uint40 providedAPR, uint40 maxAPR); // Input data error InvalidInputData(); diff --git a/src/loan/terms/PWNLOANTerms.sol b/src/loan/terms/PWNLOANTerms.sol index 7b9256e..ed77c08 100644 --- a/src/loan/terms/PWNLOANTerms.sol +++ b/src/loan/terms/PWNLOANTerms.sol @@ -14,7 +14,8 @@ library PWNLOANTerms { * @param defaultTimestamp Unix timestamp (in seconds) setting up a default date. * @param collateral Asset used as a loan collateral. For a definition see { MultiToken dependency lib }. * @param asset Asset used as a loan credit. For a definition see { MultiToken dependency lib }. - * @param loanRepayAmount Amount of a loan asset to be paid back. + * @param fixedInterestAmount Fixed interest amount in loan asset tokens. It is the minimum amount of interest which has to be paid by a borrower. + * @param accruingInterestAPR Accruing interest APR. * @param canCreate If true, the terms can be used to create a new loan. * @param canRefinance If true, the terms can be used to refinance a running loan. * @param refinancingLoanId Id of a loan which is refinanced by this terms. If the id is 0, any loan can be refinanced. @@ -25,7 +26,8 @@ library PWNLOANTerms { uint40 defaultTimestamp; MultiToken.Asset collateral; MultiToken.Asset asset; - uint256 loanRepayAmount; + uint256 fixedInterestAmount; + uint40 accruingInterestAPR; bool canCreate; bool canRefinance; uint256 refinancingLoanId; diff --git a/src/loan/terms/simple/factory/PWNSimpleLoanTermsFactory.sol b/src/loan/terms/simple/factory/PWNSimpleLoanTermsFactory.sol index 73ea5ad..dc2adc8 100644 --- a/src/loan/terms/simple/factory/PWNSimpleLoanTermsFactory.sol +++ b/src/loan/terms/simple/factory/PWNSimpleLoanTermsFactory.sol @@ -11,6 +11,7 @@ import "@pwn/loan/terms/PWNLOANTerms.sol"; abstract contract PWNSimpleLoanTermsFactory { uint32 public constant MIN_LOAN_DURATION = 600; // 10 min + uint40 public constant MAX_ACCRUING_INTEREST_APR = 1e11; // 1,000,000% APR /** * @notice Build a simple loan terms from given data. diff --git a/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol b/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol index 9d3c419..fe20b5f 100644 --- a/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol +++ b/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol @@ -28,7 +28,7 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanOffer { * @dev EIP-712 simple offer struct type hash. */ bytes32 public constant OFFER_TYPEHASH = keccak256( - "Offer(uint8 collateralCategory,address collateralAddress,bytes32 collateralIdsWhitelistMerkleRoot,uint256 collateralAmount,address loanAssetAddress,uint256 loanAmount,uint256 loanYield,uint32 duration,uint40 expiration,address allowedBorrower,address lender,bool isPersistent,uint256 nonce)" + "Offer(uint8 collateralCategory,address collateralAddress,bytes32 collateralIdsWhitelistMerkleRoot,uint256 collateralAmount,address loanAssetAddress,uint256 loanAmount,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedBorrower,address lender,bool isPersistent,uint256 nonce)" ); bytes32 public immutable DOMAIN_SEPARATOR; @@ -41,7 +41,8 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanOffer { * @param collateralAmount Amount of tokens used as a collateral, in case of ERC721 should be 0. * @param loanAssetAddress Address of an asset which is lender to a borrower. * @param loanAmount Amount of tokens which is offered as a loan to a borrower. - * @param loanYield Amount of tokens which acts as a lenders loan interest. Borrower has to pay back a borrowed amount + yield. + * @param fixedInterestAmount Fixed interest amount in loan asset 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. @@ -57,7 +58,8 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanOffer { uint256 collateralAmount; address loanAssetAddress; uint256 loanAmount; - uint256 loanYield; + uint256 fixedInterestAmount; + uint40 accruingInterestAPR; uint32 duration; uint40 expiration; address allowedBorrower; @@ -113,7 +115,7 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanOffer { |*----------------------------------------------------------*/ /** - * @notice See { IPWNSimpleLoanFactory.sol }. + * @inheritdoc PWNSimpleLoanTermsFactory */ function createLOANTerms( address caller, @@ -146,6 +148,13 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanOffer { if (offer.duration < MIN_LOAN_DURATION) revert InvalidDuration(); + // Check APR + if (offer.accruingInterestAPR > MAX_ACCRUING_INTEREST_APR) + revert AccruingInterestAPROutOfBounds({ + providedAPR: offer.accruingInterestAPR, + maxAPR: MAX_ACCRUING_INTEREST_APR + }); + // Collateral id list if (offer.collateralIdsWhitelistMerkleRoot != bytes32(0)) { // Verify whitelisted collateral id @@ -179,7 +188,8 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanOffer { defaultTimestamp: uint40(block.timestamp) + offer.duration, collateral: collateral, asset: loanAsset, - loanRepayAmount: offer.loanAmount + offer.loanYield, + fixedInterestAmount: offer.fixedInterestAmount, + accruingInterestAPR: offer.accruingInterestAPR, canCreate: true, canRefinance: true, refinancingLoanId: 0 diff --git a/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol b/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol index 70593dd..7a040c1 100644 --- a/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol +++ b/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol @@ -25,7 +25,7 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanOffer { * @dev EIP-712 simple offer struct type hash. */ bytes32 public constant OFFER_TYPEHASH = keccak256( - "Offer(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,address loanAssetAddress,uint256 loanAmount,uint256 loanYield,uint32 duration,uint40 expiration,address allowedBorrower,address lender,bool isPersistent,uint256 nonce)" + "Offer(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,address loanAssetAddress,uint256 loanAmount,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedBorrower,address lender,bool isPersistent,uint256 nonce)" ); bytes32 public immutable DOMAIN_SEPARATOR; @@ -38,7 +38,8 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanOffer { * @param collateralAmount Amount of tokens used as a collateral, in case of ERC721 should be 0. * @param loanAssetAddress Address of an asset which is lender to a borrower. * @param loanAmount Amount of tokens which is offered as a loan to a borrower. - * @param loanYield Amount of tokens which acts as a lenders loan interest. Borrower has to pay back a borrowed amount + yield. + * @param fixedInterestAmount Fixed interest amount in loan asset 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. @@ -54,7 +55,8 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanOffer { uint256 collateralAmount; address loanAssetAddress; uint256 loanAmount; - uint256 loanYield; + uint256 fixedInterestAmount; + uint40 accruingInterestAPR; uint32 duration; uint40 expiration; address allowedBorrower; @@ -98,7 +100,7 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanOffer { |*----------------------------------------------------------*/ /** - * @notice See { IPWNSimpleLoanFactory.sol }. + * @inheritdoc PWNSimpleLoanTermsFactory */ function createLOANTerms( address caller, @@ -131,6 +133,13 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanOffer { if (offer.duration < MIN_LOAN_DURATION) revert InvalidDuration(); + // Check APR + if (offer.accruingInterestAPR > MAX_ACCRUING_INTEREST_APR) + revert AccruingInterestAPROutOfBounds({ + providedAPR: offer.accruingInterestAPR, + maxAPR: MAX_ACCRUING_INTEREST_APR + }); + // Prepare collateral and loan asset MultiToken.Asset memory collateral = MultiToken.Asset({ category: offer.collateralCategory, @@ -152,7 +161,8 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanOffer { defaultTimestamp: uint40(block.timestamp) + offer.duration, collateral: collateral, asset: loanAsset, - loanRepayAmount: offer.loanAmount + offer.loanYield, + fixedInterestAmount: offer.fixedInterestAmount, + accruingInterestAPR: offer.accruingInterestAPR, canCreate: true, canRefinance: true, refinancingLoanId: 0 diff --git a/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol b/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol index deada0c..127f6aa 100644 --- a/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol +++ b/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol @@ -25,7 +25,7 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanRequest { * @dev EIP-712 simple request struct type hash. */ bytes32 public constant REQUEST_TYPEHASH = keccak256( - "Request(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,address loanAssetAddress,uint256 loanAmount,uint256 loanYield,uint32 duration,uint40 expiration,address allowedLender,address borrower,uint256 refinancingLoanId,uint256 nonce)" + "Request(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,address loanAssetAddress,uint256 loanAmount,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedLender,address borrower,uint256 refinancingLoanId,uint256 nonce)" ); bytes32 public immutable DOMAIN_SEPARATOR; @@ -38,7 +38,8 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanRequest { * @param collateralAmount Amount of tokens used as a collateral, in case of ERC721 should be 0. * @param loanAssetAddress Address of an asset which is lender to a borrower. * @param loanAmount Amount of tokens which is requested as a loan to a borrower. - * @param loanYield Amount of tokens which acts as a lenders loan interest. Borrower has to pay back a borrowed amount + yield. + * @param fixedInterestAmount Fixed interest amount in loan asset 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 Request expiration timestamp in seconds. * @param allowedLender Address of an allowed lender. Only this address can accept a request. If the address is zero address, anybody with a loan asset can accept the request. @@ -54,7 +55,8 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanRequest { uint256 collateralAmount; address loanAssetAddress; uint256 loanAmount; - uint256 loanYield; + uint256 fixedInterestAmount; + uint40 accruingInterestAPR; uint32 duration; uint40 expiration; address allowedLender; @@ -98,7 +100,7 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanRequest { |*----------------------------------------------------------*/ /** - * @notice See { IPWNSimpleLoanFactory.sol }. + * @inheritdoc PWNSimpleLoanTermsFactory */ function createLOANTerms( address caller, @@ -131,6 +133,13 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanRequest { if (request.duration < MIN_LOAN_DURATION) revert InvalidDuration(); + // Check APR + if (request.accruingInterestAPR > MAX_ACCRUING_INTEREST_APR) + revert AccruingInterestAPROutOfBounds({ + providedAPR: request.accruingInterestAPR, + maxAPR: MAX_ACCRUING_INTEREST_APR + }); + // Prepare collateral and loan asset MultiToken.Asset memory collateral = MultiToken.Asset({ category: request.collateralCategory, @@ -152,7 +161,8 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanRequest { defaultTimestamp: uint40(block.timestamp) + request.duration, collateral: collateral, asset: loanAsset, - loanRepayAmount: request.loanAmount + request.loanYield, + fixedInterestAmount: request.fixedInterestAmount, + accruingInterestAPR: request.accruingInterestAPR, canCreate: request.refinancingLoanId == 0, canRefinance: request.refinancingLoanId != 0, refinancingLoanId: request.refinancingLoanId diff --git a/src/loan/terms/simple/loan/PWNSimpleLoan.sol b/src/loan/terms/simple/loan/PWNSimpleLoan.sol index e6f3800..51adfc5 100644 --- a/src/loan/terms/simple/loan/PWNSimpleLoan.sol +++ b/src/loan/terms/simple/loan/PWNSimpleLoan.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.16; import { MultiToken, IMultiTokenCategoryRegistry } from "MultiToken/MultiToken.sol"; import { Math } from "openzeppelin-contracts/contracts/utils/math/Math.sol"; +import { SafeCast } from "openzeppelin-contracts/contracts/utils/math/SafeCast.sol"; import { PWNConfig } from "@pwn/config/PWNConfig.sol"; import { PWNHub } from "@pwn/hub/PWNHub.sol"; @@ -26,6 +27,13 @@ import "@pwn/PWNErrors.sol"; contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { string public constant VERSION = "1.2"; + + uint256 public constant APR_INTEREST_DENOMINATOR = 1e4; + uint256 public constant DAILY_INTEREST_DENOMINATOR = 1e10; + + uint256 public constant APR_TO_DAILY_INTEREST_NUMERATOR = 274; + uint256 public constant APR_TO_DAILY_INTEREST_DENOMINATOR = 1e5; + uint256 public constant MAX_EXPIRATION_EXTENSION = 2_592_000; // 30 days /*----------------------------------------------------------*| @@ -41,21 +49,29 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { /** * @notice Struct defining a simple loan. * @param status 0 == none/dead || 2 == running/accepted offer/accepted request || 3 == paid back || 4 == expired. - * @param borrower Address of a borrower. - * @param defaultTimestamp Unix timestamp (in seconds) setting up a default date. * @param loanAssetAddress Address of an asset used as a loan credit. - * @param loanRepayAmount Amount of a loan asset to be paid back. - * @param collateral Asset used as a loan collateral. For a definition see { MultiToken dependency lib }. + * @param startTimestamp Unix timestamp (in seconds) of a start date. + * @param defaultTimestamp Unix timestamp (in seconds) of a default date. + * @param borrower Address of a borrower. * @param originalLender Address of a lender that funded the loan. + * @param accruingInterestDailyRate Accruing daily interest rate. + * @param fixedInterestAmount Fixed interest amount in loan asset tokens. + * It is the minimum amount of interest which has to be paid by a borrower. + * This property is reused to store the final interest amount if the loan is repaid and waiting to be claimed. + * @param principalAmount Principal amount in loan asset tokens. + * @param collateral Asset used as a loan collateral. For a definition see { MultiToken dependency lib }. */ struct LOAN { uint8 status; - address borrower; - uint40 defaultTimestamp; address loanAssetAddress; - uint256 loanRepayAmount; - MultiToken.Asset collateral; + uint40 startTimestamp; + uint40 defaultTimestamp; + address borrower; address originalLender; + uint40 accruingInterestDailyRate; + uint256 fixedInterestAmount; + uint256 principalAmount; + MultiToken.Asset collateral; } /** @@ -199,12 +215,17 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { // Store loan data under loan id LOAN storage loan = LOANs[loanId]; loan.status = 2; - loan.borrower = loanTerms.borrower; - loan.defaultTimestamp = loanTerms.defaultTimestamp; loan.loanAssetAddress = loanTerms.asset.assetAddress; - loan.loanRepayAmount = loanTerms.loanRepayAmount; - loan.collateral = loanTerms.collateral; + loan.startTimestamp = uint40(block.timestamp); + loan.defaultTimestamp = loanTerms.defaultTimestamp; + loan.borrower = loanTerms.borrower; loan.originalLender = loanTerms.lender; + loan.accruingInterestDailyRate = SafeCast.toUint40(Math.mulDiv( + loanTerms.accruingInterestAPR, APR_TO_DAILY_INTEREST_NUMERATOR, APR_TO_DAILY_INTEREST_DENOMINATOR + )); + loan.fixedInterestAmount = loanTerms.fixedInterestAmount; + loan.principalAmount = loanTerms.asset.amount; + loan.collateral = loanTerms.collateral; emit LOANCreated({ loanId: loanId, @@ -294,7 +315,6 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { // Refinance the original loan _refinanceOriginalLoan( loanId, - loan.loanRepayAmount, loanTerms, lenderLoanAssetPermit, borrowerLoanAssetPermit @@ -348,18 +368,18 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { * 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 loanId Id of a loan that is being refinanced. - * @param loanRepayAmount Amount of the original loan to be repaid. * @param loanTerms Loan terms struct. * @param lenderLoanAssetPermit Permit data for a loan asset signed by a lender. * @param borrowerLoanAssetPermit Permit data for a loan asset signed by a borrower. */ function _refinanceOriginalLoan( uint256 loanId, - uint256 loanRepayAmount, PWNLOANTerms.Simple memory loanTerms, bytes calldata lenderLoanAssetPermit, bytes calldata borrowerLoanAssetPermit ) private { + uint256 repaymentAmount = _loanRepaymentAmount(loanId); + // Delete or update the original loan (bool repayLoanDirectly, address loanOwner) = _deleteOrUpdateRepaidLoan(loanId); @@ -367,7 +387,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { _settleLoanRefinance({ repayLoanDirectly: repayLoanDirectly, loanOwner: loanOwner, - loanRepayAmount: loanRepayAmount, + repaymentAmount: repaymentAmount, loanTerms: loanTerms, lenderPermit: lenderLoanAssetPermit, borrowerPermit: borrowerLoanAssetPermit @@ -381,7 +401,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { * The function assumes a prior token approval to a contract address or signed permits. * @param repayLoanDirectly If the loan can be repaid directly to the current LOAN owner. * @param loanOwner Address of the current LOAN owner. - * @param loanRepayAmount Amount of the original loan to be repaid. + * @param repaymentAmount Amount of the original loan to be repaid. * @param loanTerms Loan terms struct. * @param lenderPermit Permit data for a loan asset signed by a lender. * @param borrowerPermit Permit data for a loan asset signed by a borrower. @@ -389,7 +409,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { function _settleLoanRefinance( bool repayLoanDirectly, address loanOwner, - uint256 loanRepayAmount, + uint256 repaymentAmount, PWNLOANTerms.Simple memory loanTerms, bytes calldata lenderPermit, bytes calldata borrowerPermit @@ -408,7 +428,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { // Permit lenders loan asset spending if permit provided loanAssetHelper.amount = loanTerms.asset.amount + feeAmount; // Permit the whole loan amount + fee loanAssetHelper.amount -= loanTerms.lender == loanOwner // Permit only the surplus transfer + fee - ? Math.min(loanRepayAmount, loanTerms.asset.amount) : 0; + ? Math.min(repaymentAmount, loanTerms.asset.amount) : 0; if (loanAssetHelper.amount > 0) _permit(loanAssetHelper, loanTerms.lender, lenderPermit); @@ -421,7 +441,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { // If the new lender is the LOAN token owner, don't execute the transfer at all, // it would make transfer from the same address to the same address if (loanTerms.lender != loanOwner) { - loanAssetHelper.amount = Math.min(loanRepayAmount, loanTerms.asset.amount); + loanAssetHelper.amount = Math.min(repaymentAmount, loanTerms.asset.amount); _transferLoanRepayment({ repayLoanDirectly: repayLoanDirectly, asset: loanAssetHelper, @@ -430,16 +450,16 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { }); } - if (loanTerms.asset.amount >= loanRepayAmount) { + if (loanTerms.asset.amount >= repaymentAmount) { // New loan covers the whole original loan, transfer surplus to the borrower if any - uint256 surplus = loanTerms.asset.amount - loanRepayAmount; + uint256 surplus = loanTerms.asset.amount - repaymentAmount; if (surplus > 0) { loanAssetHelper.amount = surplus; _pushFrom(loanAssetHelper, loanTerms.lender, loanTerms.borrower); } } else { // Permit borrowers loan asset spending if permit provided - loanAssetHelper.amount = loanRepayAmount - loanTerms.asset.amount; + loanAssetHelper.amount = repaymentAmount - loanTerms.asset.amount; _permit(loanAssetHelper, loanTerms.borrower, borrowerPermit); // New loan covers only part of the original loan, borrower needs to contribute @@ -469,23 +489,69 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { uint256 loanId, bytes calldata loanAssetPermit ) external { - LOAN memory loan = LOANs[loanId]; + LOAN storage loan = LOANs[loanId]; _checkLoanCanBeRepaid(loan.status, loan.defaultTimestamp); + address borrower = loan.borrower; + MultiToken.Asset memory collateral = loan.collateral; + MultiToken.Asset memory repaymentLoanAsset = MultiToken.ERC20(loan.loanAssetAddress, _loanRepaymentAmount(loanId)); + (bool repayLoanDirectly, address loanOwner) = _deleteOrUpdateRepaidLoan(loanId); _settleLoanRepayment({ repayLoanDirectly: repayLoanDirectly, loanOwner: loanOwner, repayingAddress: msg.sender, - borrower: loan.borrower, - repayLoanAsset: MultiToken.ERC20(loan.loanAssetAddress, loan.loanRepayAmount), - collateral: loan.collateral, + borrower: borrower, + repaymentLoanAsset: repaymentLoanAsset, + collateral: collateral, loanAssetPermit: loanAssetPermit }); } + /** + * @notice Calculate the loan repayment amount with fixed and accrued interest. + * @param loanId Id of a loan. + * @return Repayment amount. + */ + function loanRepaymentAmount(uint256 loanId) public view returns (uint256) { + LOAN storage loan = LOANs[loanId]; + + // Check non-existent + if (loan.status == 0) return 0; + + return _loanRepaymentAmount(loanId); + } + + /** + * @notice Internal function to calculate the loan repayment amount with fixed and accrued interest. + * @param loanId Id of a loan. + * @return Repayment amount. + */ + function _loanRepaymentAmount(uint256 loanId) private view returns (uint256) { + LOAN storage loan = LOANs[loanId]; + + // Return loan principal with accrued interest + return loan.principalAmount + _loanAccruedInterest(loan); + } + + /** + * @notice Calculate the loan accrued interest. + * @param loan Loan data struct. + * @return Accrued interest amount. + */ + function _loanAccruedInterest(LOAN storage loan) private view returns (uint256) { + if (loan.accruingInterestDailyRate == 0) + return loan.fixedInterestAmount; + + uint256 accruingDays = (block.timestamp - loan.startTimestamp) / 1 days; + uint256 accruedInterest = Math.mulDiv( + loan.principalAmount, loan.accruingInterestDailyRate * accruingDays, DAILY_INTEREST_DENOMINATOR + ); + return loan.fixedInterestAmount + accruedInterest; + } + /** * @notice Check if the loan can be repaid. * @dev The function will revert if the loan cannot be repaid. @@ -512,13 +578,15 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { * @return loanOwner Address of the current LOAN owner. */ function _deleteOrUpdateRepaidLoan(uint256 loanId) private returns (bool repayLoanDirectly, address loanOwner) { + LOAN storage loan = LOANs[loanId]; + emit LOANPaidBack({ loanId: loanId }); // Note: Assuming that it is safe to transfer the loan asset to the original lender // if the lender still owns the LOAN token because the lender was able to sign an offer // or make a contract call, thus can handle incoming transfers. loanOwner = loanToken.ownerOf(loanId); - repayLoanDirectly = LOANs[loanId].originalLender == loanOwner; + repayLoanDirectly = loan.originalLender == loanOwner; if (repayLoanDirectly) { // Delete loan data & burn LOAN token before calling safe transfer _deleteLoan(loanId); @@ -526,7 +594,12 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { emit LOANClaimed({ loanId: loanId, defaulted: false }); } else { // Move loan to repaid state and wait for the lender to claim the repaid loan asset - LOANs[loanId].status = 3; + loan.status = 3; + // Update accrued interest amount + loan.fixedInterestAmount = _loanAccruedInterest(loan); + // Note: Reusing `fixedInterestAmount` to store accrued interest at the time of repayment + // to have the value at the time of claim and stop accruing new interest. + loan.accruingInterestDailyRate = 0; } } @@ -537,7 +610,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { * @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 repayLoanAsset Loan asset to be repaid. + * @param repaymentLoanAsset Loan asset to be repaid. * @param collateral Collateral to be transferred back to the borrower. * @param loanAssetPermit Permit data for a loan asset signed by a borrower. */ @@ -546,13 +619,13 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { address loanOwner, address repayingAddress, address borrower, - MultiToken.Asset memory repayLoanAsset, + MultiToken.Asset memory repaymentLoanAsset, MultiToken.Asset memory collateral, bytes calldata loanAssetPermit ) private { // Transfer loan asset to the original lender or to the Vault - _permit(repayLoanAsset, repayingAddress, loanAssetPermit); - _transferLoanRepayment(repayLoanDirectly, repayLoanAsset, repayingAddress, loanOwner); + _permit(repaymentLoanAsset, repayingAddress, loanAssetPermit); + _transferLoanRepayment(repayLoanDirectly, repaymentLoanAsset, repayingAddress, loanOwner); // Transfer collateral back to borrower _push(collateral, borrower); @@ -621,9 +694,10 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { function _settleLoanClaim(uint256 loanId, address loanOwner, bool defaulted) private { LOAN storage loan = LOANs[loanId]; + // Store in memory before deleting the loan MultiToken.Asset memory asset = defaulted ? loan.collateral - : MultiToken.ERC20(loan.loanAssetAddress, loan.loanRepayAmount); + : MultiToken.ERC20(loan.loanAssetAddress, _loanRepaymentAmount(loanId)); // Delete loan data & burn LOAN token before calling safe transfer _deleteLoan(loanId); @@ -745,12 +819,16 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { return bytes32(0); // The only mutable state properties are: - // - status, defaultTimestamp - // Status is updated for expired loans based on block.timestamp. + // - status: updated for expired loans based on block.timestamp + // - defaultTimestamp: updated when the loan expiration date is extended + // - fixedInterestAmount: updated when the loan is repaid and waiting to be claimed + // - accruingInterestDailyRate: updated when the loan is repaid and waiting to be claimed // Others don't have to be part of the state fingerprint as it does not act as a token identification. return keccak256(abi.encode( _getLOANStatus(tokenId), - loan.defaultTimestamp + loan.defaultTimestamp, + loan.fixedInterestAmount, + loan.accruingInterestDailyRate )); } diff --git a/test/unit/PWNSimpleLoan.t.sol b/test/unit/PWNSimpleLoan.t.sol index 736d5b6..5526724 100644 --- a/test/unit/PWNSimpleLoan.t.sol +++ b/test/unit/PWNSimpleLoan.t.sol @@ -33,6 +33,7 @@ abstract contract PWNSimpleLoanTest is Test { uint256 loanId = 42; address lender = makeAddr("lender"); address borrower = makeAddr("borrower"); + uint256 loanDurationInDays = 101; PWNSimpleLoan.LOAN simpleLoan; PWNSimpleLoan.LOAN nonExistingLoan; PWNLOANTerms.Simple simpleLoanTerms; @@ -85,21 +86,25 @@ abstract contract PWNSimpleLoanTest is Test { simpleLoan = PWNSimpleLoan.LOAN({ status: 2, - borrower: borrower, - defaultTimestamp: uint40(block.timestamp + 40039), loanAssetAddress: address(fungibleAsset), - loanRepayAmount: 6731, - collateral: MultiToken.ERC721(address(nonFungibleAsset), 2), - originalLender: lender + startTimestamp: uint40(block.timestamp), + defaultTimestamp: uint40(block.timestamp + loanDurationInDays * 1 days), + borrower: borrower, + originalLender: lender, + accruingInterestDailyRate: 0, + fixedInterestAmount: 6631, + principalAmount: 100, + collateral: MultiToken.ERC721(address(nonFungibleAsset), 2) }); simpleLoanTerms = PWNLOANTerms.Simple({ lender: lender, borrower: borrower, - defaultTimestamp: uint40(block.timestamp + 40039), + defaultTimestamp: uint40(block.timestamp + loanDurationInDays * 1 days), collateral: MultiToken.ERC721(address(nonFungibleAsset), 2), asset: MultiToken.ERC20(address(fungibleAsset), 100), - loanRepayAmount: 6731, + fixedInterestAmount: 6631, + accruingInterestAPR: 0, canCreate: true, canRefinance: true, refinancingLoanId: 0 @@ -107,12 +112,15 @@ abstract contract PWNSimpleLoanTest is Test { nonExistingLoan = PWNSimpleLoan.LOAN({ status: 0, - borrower: address(0), - defaultTimestamp: 0, loanAssetAddress: address(0), - loanRepayAmount: 0, - collateral: MultiToken.Asset(MultiToken.Category(0), address(0), 0, 0), - originalLender: address(0) + startTimestamp: 0, + defaultTimestamp: 0, + borrower: address(0), + originalLender: address(0), + accruingInterestDailyRate: 0, + fixedInterestAmount: 0, + principalAmount: 0, + collateral: MultiToken.Asset(MultiToken.Category(0), address(0), 0, 0) }); loanFactoryDataHash = keccak256("factoryData"); @@ -147,58 +155,61 @@ abstract contract PWNSimpleLoanTest is Test { function _assertLOANEq(PWNSimpleLoan.LOAN memory _simpleLoan1, PWNSimpleLoan.LOAN memory _simpleLoan2) internal { assertEq(_simpleLoan1.status, _simpleLoan2.status); - assertEq(_simpleLoan1.borrower, _simpleLoan2.borrower); - assertEq(_simpleLoan1.defaultTimestamp, _simpleLoan2.defaultTimestamp); assertEq(_simpleLoan1.loanAssetAddress, _simpleLoan2.loanAssetAddress); - assertEq(_simpleLoan1.loanRepayAmount, _simpleLoan2.loanRepayAmount); + assertEq(_simpleLoan1.startTimestamp, _simpleLoan2.startTimestamp); + assertEq(_simpleLoan1.defaultTimestamp, _simpleLoan2.defaultTimestamp); + assertEq(_simpleLoan1.borrower, _simpleLoan2.borrower); + assertEq(_simpleLoan1.originalLender, _simpleLoan2.originalLender); + assertEq(_simpleLoan1.accruingInterestDailyRate, _simpleLoan2.accruingInterestDailyRate); + assertEq(_simpleLoan1.fixedInterestAmount, _simpleLoan2.fixedInterestAmount); + assertEq(_simpleLoan1.principalAmount, _simpleLoan2.principalAmount); assertEq(uint8(_simpleLoan1.collateral.category), uint8(_simpleLoan2.collateral.category)); assertEq(_simpleLoan1.collateral.assetAddress, _simpleLoan2.collateral.assetAddress); assertEq(_simpleLoan1.collateral.id, _simpleLoan2.collateral.id); assertEq(_simpleLoan1.collateral.amount, _simpleLoan2.collateral.amount); - assertEq(_simpleLoan1.originalLender, _simpleLoan2.originalLender); } function _assertLOANEq(uint256 _loanId, PWNSimpleLoan.LOAN memory _simpleLoan) internal { - uint256 loanSlot = uint256(keccak256(abi.encode( - _loanId, - LOANS_SLOT - ))); - // Status, borrower address & defaultTimestamp in one storage slot - _assertLOANWord(loanSlot + 0, abi.encodePacked(uint48(0), _simpleLoan.defaultTimestamp, _simpleLoan.borrower, _simpleLoan.status)); - // Loan asset address - _assertLOANWord(loanSlot + 1, abi.encodePacked(uint96(0), _simpleLoan.loanAssetAddress)); - // Loan repay amount - _assertLOANWord(loanSlot + 2, abi.encodePacked(_simpleLoan.loanRepayAmount)); - // Collateral category & collateral asset address in one storage slot - _assertLOANWord(loanSlot + 3, abi.encodePacked(uint88(0), _simpleLoan.collateral.assetAddress, _simpleLoan.collateral.category)); + uint256 loanSlot = uint256(keccak256(abi.encode(_loanId, LOANS_SLOT))); + + // Status, loan asset address, start timestamp, default timestamp + _assertLOANWord(loanSlot + 0, abi.encodePacked(uint8(0), _simpleLoan.defaultTimestamp, _simpleLoan.startTimestamp, _simpleLoan.loanAssetAddress, _simpleLoan.status)); + // Borrower address + _assertLOANWord(loanSlot + 1, abi.encodePacked(uint96(0), _simpleLoan.borrower)); + // Original lender, accruing interest daily rate + _assertLOANWord(loanSlot + 2, abi.encodePacked(uint56(0), _simpleLoan.accruingInterestDailyRate, _simpleLoan.originalLender)); + // Fixed interest amount + _assertLOANWord(loanSlot + 3, abi.encodePacked(_simpleLoan.fixedInterestAmount)); + // Principal amount + _assertLOANWord(loanSlot + 4, abi.encodePacked(_simpleLoan.principalAmount)); + // Collateral category, collateral asset address + _assertLOANWord(loanSlot + 5, abi.encodePacked(uint88(0), _simpleLoan.collateral.assetAddress, _simpleLoan.collateral.category)); // Collateral id - _assertLOANWord(loanSlot + 4, abi.encodePacked(_simpleLoan.collateral.id)); + _assertLOANWord(loanSlot + 6, abi.encodePacked(_simpleLoan.collateral.id)); // Collateral amount - _assertLOANWord(loanSlot + 5, abi.encodePacked(_simpleLoan.collateral.amount)); - // Original lender - _assertLOANWord(loanSlot + 6, abi.encodePacked(uint96(0), _simpleLoan.originalLender)); + _assertLOANWord(loanSlot + 7, abi.encodePacked(_simpleLoan.collateral.amount)); } function _mockLOAN(uint256 _loanId, PWNSimpleLoan.LOAN memory _simpleLoan) internal { - uint256 loanSlot = uint256(keccak256(abi.encode( - _loanId, - LOANS_SLOT - ))); - // Status, borrower address & defaultTimestamp in one storage slot - _storeLOANWord(loanSlot + 0, abi.encodePacked(uint48(0), _simpleLoan.defaultTimestamp, _simpleLoan.borrower, _simpleLoan.status)); - // Loan asset address - _storeLOANWord(loanSlot + 1, abi.encodePacked(uint96(0), _simpleLoan.loanAssetAddress)); - // Loan repay amount - _storeLOANWord(loanSlot + 2, abi.encodePacked(_simpleLoan.loanRepayAmount)); - // Collateral category & collateral asset address in one storage slot - _storeLOANWord(loanSlot + 3, abi.encodePacked(uint88(0), _simpleLoan.collateral.assetAddress, _simpleLoan.collateral.category)); + uint256 loanSlot = uint256(keccak256(abi.encode(_loanId, LOANS_SLOT))); + + // Status, loan asset address, start timestamp, default timestamp + _storeLOANWord(loanSlot + 0, abi.encodePacked(uint8(0), _simpleLoan.defaultTimestamp, _simpleLoan.startTimestamp, _simpleLoan.loanAssetAddress, _simpleLoan.status)); + // Borrower address + _storeLOANWord(loanSlot + 1, abi.encodePacked(uint96(0), _simpleLoan.borrower)); + // Original lender, accruing interest daily rate + _storeLOANWord(loanSlot + 2, abi.encodePacked(uint56(0), _simpleLoan.accruingInterestDailyRate, _simpleLoan.originalLender)); + // Fixed interest amount + _storeLOANWord(loanSlot + 3, abi.encodePacked(_simpleLoan.fixedInterestAmount)); + // Principal amount + _storeLOANWord(loanSlot + 4, abi.encodePacked(_simpleLoan.principalAmount)); + // Collateral category, collateral asset address + _storeLOANWord(loanSlot + 5, abi.encodePacked(uint88(0), _simpleLoan.collateral.assetAddress, _simpleLoan.collateral.category)); // Collateral id - _storeLOANWord(loanSlot + 4, abi.encodePacked(_simpleLoan.collateral.id)); + _storeLOANWord(loanSlot + 6, abi.encodePacked(_simpleLoan.collateral.id)); // Collateral amount - _storeLOANWord(loanSlot + 5, abi.encodePacked(_simpleLoan.collateral.amount)); - // Original lender - _storeLOANWord(loanSlot + 6, abi.encodePacked(uint96(0), _simpleLoan.originalLender)); + _storeLOANWord(loanSlot + 7, abi.encodePacked(_simpleLoan.collateral.amount)); } function _mockLoanTerms(PWNLOANTerms.Simple memory _loanTerms, bytes32 _loanFactoryDataHash) internal { @@ -244,14 +255,14 @@ abstract contract PWNSimpleLoanTest is Test { contract PWNSimpleLoan_CreateLoan_Test is PWNSimpleLoanTest { - function test_shouldFail_whenLoanFactoryContractIsNotTaggerInPWNHub() external { - address notLoanFactory = address(0); + function testFuzz_shouldFail_whenLoanFactoryContractIsNotTaggerInPWNHub(address notLoanFactory) external { + vm.assume(notLoanFactory != loanFactory); vm.expectRevert(abi.encodeWithSelector(CallerMissingHubTag.selector, PWNHubTags.SIMPLE_LOAN_TERMS_FACTORY)); loan.createLOAN(notLoanFactory, loanFactoryData, signature, loanAssetPermit, collateralPermit); } - function test_shouldGetLOANTermsStructFromGivenFactoryContract() external { + function test_shouldCall_createLOANTerms_onProvidedFactoryContract() external { loanFactoryData = abi.encode(1, 2, "data"); signature = abi.encode("other data", "whaat?", uint256(312312)); @@ -270,8 +281,6 @@ contract PWNSimpleLoan_CreateLoan_Test is PWNSimpleLoanTest { abi.encode(1) ); - _mockLoanTerms(simpleLoanTerms, loanFactoryDataHash); - vm.expectRevert(InvalidLoanAsset.selector); loan.createLOAN(loanFactory, loanFactoryData, signature, loanAssetPermit, collateralPermit); } @@ -283,8 +292,6 @@ contract PWNSimpleLoan_CreateLoan_Test is PWNSimpleLoanTest { abi.encode(0) ); - _mockLoanTerms(simpleLoanTerms, loanFactoryDataHash); - vm.expectRevert(InvalidCollateralAsset.selector); loan.createLOAN(loanFactory, loanFactoryData, signature, loanAssetPermit, collateralPermit); } @@ -306,9 +313,15 @@ contract PWNSimpleLoan_CreateLoan_Test is PWNSimpleLoanTest { loan.createLOAN(loanFactory, loanFactoryData, signature, loanAssetPermit, collateralPermit); } - function test_shouldStoreLoanData() external { + function testFuzz_shouldStoreLoanData(uint40 accruingInterestAPR) external { + accruingInterestAPR = uint40(bound(accruingInterestAPR, 0, 1e11)); + + simpleLoanTerms.accruingInterestAPR = accruingInterestAPR; + _mockLoanTerms(simpleLoanTerms, loanFactoryDataHash); + loan.createLOAN(loanFactory, loanFactoryData, signature, loanAssetPermit, collateralPermit); + simpleLoan.accruingInterestDailyRate = uint40(uint256(accruingInterestAPR) * 274 / 1e5); _assertLOANEq(loanId, simpleLoan); } @@ -335,52 +348,46 @@ contract PWNSimpleLoan_CreateLoan_Test is PWNSimpleLoanTest { loan.createLOAN(loanFactory, loanFactoryData, signature, loanAssetPermit, collateralPermit); } - function test_shouldTransferLoanAsset_fromLender_toBorrower_whenZeroFees() external { - loanAssetPermit = abi.encodePacked(uint256(1), uint256(2), uint256(3), uint8(4)); - - vm.expectCall( - simpleLoanTerms.asset.assetAddress, - abi.encodeWithSignature( - "permit(address,address,uint256,uint256,uint8,bytes32,bytes32)", - lender, address(loan), simpleLoanTerms.asset.amount, 1, uint8(4), uint256(2), uint256(3) - ) - ); - vm.expectCall( - simpleLoanTerms.asset.assetAddress, - abi.encodeWithSignature("transferFrom(address,address,uint256)", lender, borrower, simpleLoanTerms.asset.amount) - ); - - loan.createLOAN(loanFactory, loanFactoryData, signature, loanAssetPermit, collateralPermit); - } + function testFuzz_shouldTransferLoanAsset_fromLender_toBorrowerAndFeeCollector( + uint256 fee, uint256 loanAmount + ) external { + fee = bound(fee, 0, 9999); + loanAmount = bound(loanAmount, 1, 1e40); - function test_shouldTransferLoanAsset_fromLender_toBorrowerAndFeeCollector_whenNonZeroFee() external { - vm.mockCall(config, abi.encodeWithSignature("fee()"), abi.encode(1000)); + simpleLoanTerms.asset.amount = loanAmount; + _mockLoanTerms(simpleLoanTerms, loanFactoryDataHash); + fungibleAsset.mint(lender, loanAmount); + vm.mockCall(config, abi.encodeWithSignature("fee()"), abi.encode(fee)); loanAssetPermit = abi.encodePacked(uint256(1), uint256(2), uint256(3), uint8(4)); + uint256 feeAmount = Math.mulDiv(loanAmount, fee, 1e4); + uint256 newAmount = loanAmount - feeAmount; + vm.expectCall( simpleLoanTerms.asset.assetAddress, abi.encodeWithSignature( "permit(address,address,uint256,uint256,uint8,bytes32,bytes32)", - lender, address(loan), simpleLoanTerms.asset.amount, 1, uint8(4), uint256(2), uint256(3) + lender, address(loan), loanAmount, 1, uint8(4), uint256(2), uint256(3) ) ); // Fee transfer - vm.expectCall( - simpleLoanTerms.asset.assetAddress, - abi.encodeWithSignature("transferFrom(address,address,uint256)", lender, feeCollector, 10) - ); + vm.expectCall({ + callee: simpleLoanTerms.asset.assetAddress, + data: abi.encodeWithSignature("transferFrom(address,address,uint256)", lender, feeCollector, feeAmount), + count: feeAmount > 0 ? 1 : 0 + }); // Updated amount transfer vm.expectCall( simpleLoanTerms.asset.assetAddress, - abi.encodeWithSignature("transferFrom(address,address,uint256)", lender, borrower, 90) + abi.encodeWithSignature("transferFrom(address,address,uint256)", lender, borrower, newAmount) ); loan.createLOAN(loanFactory, loanFactoryData, signature, loanAssetPermit, collateralPermit); } function test_shouldEmitEvent_LOANCreated() external { - vm.expectEmit(true, true, true, true); + vm.expectEmit(); emit LOANCreated(loanId, simpleLoanTerms, loanFactoryDataHash, loanFactory); loan.createLOAN(loanFactory, loanFactoryData, signature, loanAssetPermit, collateralPermit); @@ -415,12 +422,15 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { refinancedLoan = PWNSimpleLoan.LOAN({ status: 2, - borrower: borrower, - defaultTimestamp: uint40(block.timestamp + 40039), loanAssetAddress: address(fungibleAsset), - loanRepayAmount: 6731, - collateral: MultiToken.ERC721(address(nonFungibleAsset), 2), - originalLender: lender + startTimestamp: uint40(block.timestamp), + defaultTimestamp: uint40(block.timestamp + 40039), + borrower: borrower, + originalLender: lender, + accruingInterestDailyRate: 0, + fixedInterestAmount: 6631, + principalAmount: 100, + collateral: MultiToken.ERC721(address(nonFungibleAsset), 2) }); refinancedLoanTerms = PWNLOANTerms.Simple({ @@ -429,7 +439,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { defaultTimestamp: uint40(block.timestamp + 40039), collateral: MultiToken.ERC721(address(nonFungibleAsset), 2), asset: MultiToken.ERC20(address(fungibleAsset), 100), - loanRepayAmount: 6731, + fixedInterestAmount: 6631, + accruingInterestAPR: 0, canCreate: false, canRefinance: true, refinancingLoanId: 0 @@ -463,7 +474,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { } function test_shouldFail_whenLoanIsDefaulted() external { - vm.warp(simpleLoan.defaultTimestamp + 10000); + vm.warp(simpleLoan.defaultTimestamp); vm.expectRevert(abi.encodeWithSelector(LoanDefaulted.selector, simpleLoan.defaultTimestamp)); loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); @@ -620,29 +631,48 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); } - function test_shouldMoveLoanToRepaidState_whenLOANOwnerIsNotOriginalLender() external { + function testFuzz_shouldUpdateLoanData_whenLOANOwnerIsNotOriginalLender( + uint256 _days, uint256 principal, uint256 fixedInterest, uint256 dailyInterest + ) external { _mockLOANTokenOwner(loanId, makeAddr("notOriginalLender")); + _days = bound(_days, 0, loanDurationInDays - 1); + principal = bound(principal, 1, 1e40); + fixedInterest = bound(fixedInterest, 0, 1e40); + dailyInterest = bound(dailyInterest, 1, 274e8); + + simpleLoan.principalAmount = principal; + simpleLoan.fixedInterestAmount = fixedInterest; + simpleLoan.accruingInterestDailyRate = uint40(dailyInterest); + _mockLOAN(loanId, simpleLoan); + + vm.warp(simpleLoan.startTimestamp + _days * 1 days); + + uint256 loanRepaymentAmount = loan.loanRepaymentAmount(loanId); + fungibleAsset.mint(borrower, loanRepaymentAmount); + loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); - bytes32 loanSlot = keccak256(abi.encode(loanId, LOANS_SLOT)); - // Parse status value from first storage slot - bytes32 statusValue = vm.load(address(loan), loanSlot) & bytes32(uint256(0xff)); - assertTrue(statusValue == bytes32(uint256(3))); + // Update loan and compare + 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); } function testFuzz_shouldTransferOriginalLoanRepaymentDirectly_andTransferSurplusToBorrower_whenLOANOwnerIsOriginalLender_whenRefinanceLoanMoreThanOrEqualToOriginalLoan( uint256 refinanceAmount, uint256 fee ) external { + uint256 loanRepaymentAmount = loan.loanRepaymentAmount(loanId); fee = bound(fee, 0, 9999); // 0 - 99.99% - uint256 minRefinanceAmount = Math.mulDiv(simpleLoan.loanRepayAmount, 1e4, 1e4 - fee); + uint256 minRefinanceAmount = Math.mulDiv(loanRepaymentAmount, 1e4, 1e4 - fee); refinanceAmount = bound( refinanceAmount, minRefinanceAmount, type(uint256).max - minRefinanceAmount - fungibleAsset.totalSupply() ); uint256 feeAmount = Math.mulDiv(refinanceAmount, fee, 1e4); - uint256 borrowerSurplus = refinanceAmount - feeAmount - simpleLoan.loanRepayAmount; + uint256 borrowerSurplus = refinanceAmount - feeAmount - loanRepaymentAmount; refinancedLoanTerms.asset.amount = refinanceAmount; refinancedLoanTerms.lender = newLender; @@ -672,7 +702,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { refinancedLoanTerms.asset.assetAddress, abi.encodeWithSignature( "transferFrom(address,address,uint256)", - newLender, simpleLoan.originalLender, simpleLoan.loanRepayAmount + newLender, simpleLoan.originalLender, loanRepaymentAmount ) ); vm.expectCall({ // borrower surplus @@ -690,15 +720,16 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { function testFuzz_shouldTransferOriginalLoanRepaymentToVault_andTransferSurplusToBorrower_whenLOANOwnerIsNotOriginalLender_whenRefinanceLoanMoreThanOrEqualToOriginalLoan( uint256 refinanceAmount, uint256 fee ) external { + uint256 loanRepaymentAmount = loan.loanRepaymentAmount(loanId); fee = bound(fee, 0, 9999); // 0 - 99.99% - uint256 minRefinanceAmount = Math.mulDiv(simpleLoan.loanRepayAmount, 1e4, 1e4 - fee); + uint256 minRefinanceAmount = Math.mulDiv(loanRepaymentAmount, 1e4, 1e4 - fee); refinanceAmount = bound( refinanceAmount, minRefinanceAmount, type(uint256).max - minRefinanceAmount - fungibleAsset.totalSupply() ); uint256 feeAmount = Math.mulDiv(refinanceAmount, fee, 1e4); - uint256 borrowerSurplus = refinanceAmount - feeAmount - simpleLoan.loanRepayAmount; + uint256 borrowerSurplus = refinanceAmount - feeAmount - loanRepaymentAmount; refinancedLoanTerms.asset.amount = refinanceAmount; refinancedLoanTerms.lender = newLender; @@ -728,7 +759,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { refinancedLoanTerms.asset.assetAddress, abi.encodeWithSignature( "transferFrom(address,address,uint256)", - newLender, address(loan), simpleLoan.loanRepayAmount + newLender, address(loan), loanRepaymentAmount ) ); vm.expectCall({ // borrower surplus @@ -746,15 +777,16 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { function testFuzz_shouldNotTransferOriginalLoanRepayment_andTransferSurplusToBorrower_whenLOANOwnerIsNewLender_whenRefinanceLoanMoreThanOrEqualOriginalLoan( uint256 refinanceAmount, uint256 fee ) external { + uint256 loanRepaymentAmount = loan.loanRepaymentAmount(loanId); fee = bound(fee, 0, 9999); // 0 - 99.99% - uint256 minRefinanceAmount = Math.mulDiv(simpleLoan.loanRepayAmount, 1e4, 1e4 - fee); + uint256 minRefinanceAmount = Math.mulDiv(loanRepaymentAmount, 1e4, 1e4 - fee); refinanceAmount = bound( refinanceAmount, minRefinanceAmount, type(uint256).max - minRefinanceAmount - fungibleAsset.totalSupply() ); uint256 feeAmount = Math.mulDiv(refinanceAmount, fee, 1e4); - uint256 borrowerSurplus = refinanceAmount - feeAmount - simpleLoan.loanRepayAmount; + uint256 borrowerSurplus = refinanceAmount - feeAmount - loanRepaymentAmount; refinancedLoanTerms.asset.amount = refinanceAmount; refinancedLoanTerms.lender = newLender; @@ -785,7 +817,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { callee: refinancedLoanTerms.asset.assetAddress, data: abi.encodeWithSignature( "transferFrom(address,address,uint256)", - newLender, newLender, simpleLoan.loanRepayAmount + newLender, newLender, loanRepaymentAmount ), count: 0 }); @@ -804,11 +836,12 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { function testFuzz_shouldTransferOriginalLoanRepaymentDirectly_andContributeFromBorrower_whenLOANOwnerIsOriginalLender_whenRefinanceLoanLessThanOriginalLoan( uint256 refinanceAmount, uint256 fee ) external { + uint256 loanRepaymentAmount = loan.loanRepaymentAmount(loanId); fee = bound(fee, 0, 9999); // 0 - 99.99% - uint256 minRefinanceAmount = Math.mulDiv(simpleLoan.loanRepayAmount, 1e4, 1e4 - fee); + uint256 minRefinanceAmount = Math.mulDiv(loanRepaymentAmount, 1e4, 1e4 - fee); refinanceAmount = bound(refinanceAmount, 1, minRefinanceAmount - 1); uint256 feeAmount = Math.mulDiv(refinanceAmount, fee, 1e4); - uint256 borrowerContribution = simpleLoan.loanRepayAmount - (refinanceAmount - feeAmount); + uint256 borrowerContribution = loanRepaymentAmount - (refinanceAmount - feeAmount); refinancedLoanTerms.asset.amount = refinanceAmount; refinancedLoanTerms.lender = newLender; @@ -863,11 +896,12 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { function testFuzz_shouldTransferOriginalLoanRepaymentToVault_andContributeFromBorrower_whenLOANOwnerIsNotOriginalLender_whenRefinanceLoanLessThanOriginalLoan( uint256 refinanceAmount, uint256 fee ) external { + uint256 loanRepaymentAmount = loan.loanRepaymentAmount(loanId); fee = bound(fee, 0, 9999); // 0 - 99.99% - uint256 minRefinanceAmount = Math.mulDiv(simpleLoan.loanRepayAmount, 1e4, 1e4 - fee); + uint256 minRefinanceAmount = Math.mulDiv(loanRepaymentAmount, 1e4, 1e4 - fee); refinanceAmount = bound(refinanceAmount, 1, minRefinanceAmount - 1); uint256 feeAmount = Math.mulDiv(refinanceAmount, fee, 1e4); - uint256 borrowerContribution = simpleLoan.loanRepayAmount - (refinanceAmount - feeAmount); + uint256 borrowerContribution = loanRepaymentAmount - (refinanceAmount - feeAmount); refinancedLoanTerms.asset.amount = refinanceAmount; refinancedLoanTerms.lender = newLender; @@ -922,11 +956,12 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { function testFuzz_shouldNotTransferOriginalLoanRepayment_andContributeFromBorrower_whenLOANOwnerIsNewLender_whenRefinanceLoanLessThanOriginalLoan( uint256 refinanceAmount, uint256 fee ) external { + uint256 loanRepaymentAmount = loan.loanRepaymentAmount(loanId); fee = bound(fee, 0, 9999); // 0 - 99.99% - uint256 minRefinanceAmount = Math.mulDiv(simpleLoan.loanRepayAmount, 1e4, 1e4 - fee); + uint256 minRefinanceAmount = Math.mulDiv(loanRepaymentAmount, 1e4, 1e4 - fee); refinanceAmount = bound(refinanceAmount, 1, minRefinanceAmount - 1); uint256 feeAmount = Math.mulDiv(refinanceAmount, fee, 1e4); - uint256 borrowerContribution = simpleLoan.loanRepayAmount - (refinanceAmount - feeAmount); + uint256 borrowerContribution = loanRepaymentAmount - (refinanceAmount - feeAmount); refinancedLoanTerms.asset.amount = refinanceAmount; refinancedLoanTerms.lender = newLender; @@ -980,11 +1015,24 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, loanAssetPermit, loanAssetPermit); } - function testFuzz_shouldRepayOriginalLoan(uint256 refinanceAmount) external { + function testFuzz_shouldRepayOriginalLoan( + uint256 _days, uint256 principal, uint256 fixedInterest, uint256 dailyInterest, uint256 refinanceAmount + ) external { + _days = bound(_days, 0, loanDurationInDays - 1); + principal = bound(principal, 1, 1e40); + fixedInterest = bound(fixedInterest, 0, 1e40); + dailyInterest = bound(dailyInterest, 1, 274e8); + + simpleLoan.principalAmount = principal; + simpleLoan.fixedInterestAmount = fixedInterest; + simpleLoan.accruingInterestDailyRate = uint40(dailyInterest); + _mockLOAN(loanId, simpleLoan); + + vm.warp(simpleLoan.startTimestamp + _days * 1 days); + + uint256 loanRepaymentAmount = loan.loanRepaymentAmount(loanId); refinanceAmount = bound( - refinanceAmount, - 1, - type(uint256).max - simpleLoan.loanRepayAmount - fungibleAsset.totalSupply() + refinanceAmount, 1, type(uint256).max - loanRepaymentAmount - fungibleAsset.totalSupply() ); refinancedLoanTerms.asset.amount = refinanceAmount; @@ -993,19 +1041,36 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { _mockLOANTokenOwner(loanId, lender); fungibleAsset.mint(newLender, refinanceAmount); + if (loanRepaymentAmount > refinanceAmount) { + fungibleAsset.mint(borrower, loanRepaymentAmount - refinanceAmount); + } + uint256 originalBalance = fungibleAsset.balanceOf(lender); loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); - assertEq(fungibleAsset.balanceOf(lender), originalBalance + simpleLoan.loanRepayAmount); + assertEq(fungibleAsset.balanceOf(lender), originalBalance + loanRepaymentAmount); } - function testFuzz_shouldCollectProtocolFee(uint256 refinanceAmount, uint256 fee) external { + function testFuzz_shouldCollectProtocolFee( + uint256 _days, uint256 principal, uint256 fixedInterest, uint256 dailyInterest, uint256 refinanceAmount, uint256 fee + ) external { + _days = bound(_days, 0, loanDurationInDays - 1); + principal = bound(principal, 1, 1e40); + fixedInterest = bound(fixedInterest, 0, 1e40); + dailyInterest = bound(dailyInterest, 1, 274e8); + + simpleLoan.principalAmount = principal; + simpleLoan.fixedInterestAmount = fixedInterest; + simpleLoan.accruingInterestDailyRate = uint40(dailyInterest); + _mockLOAN(loanId, simpleLoan); + + vm.warp(simpleLoan.startTimestamp + _days * 1 days); + + uint256 loanRepaymentAmount = loan.loanRepaymentAmount(loanId); fee = bound(fee, 1, 9999); // 0 - 99.99% refinanceAmount = bound( - refinanceAmount, - 1, - type(uint256).max - simpleLoan.loanRepayAmount - fungibleAsset.totalSupply() + refinanceAmount, 1, type(uint256).max - loanRepaymentAmount - fungibleAsset.totalSupply() ); uint256 feeAmount = Math.mulDiv(refinanceAmount, fee, 1e4); @@ -1016,6 +1081,10 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { vm.mockCall(config, abi.encodeWithSignature("fee()"), abi.encode(fee)); fungibleAsset.mint(newLender, refinanceAmount); + if (loanRepaymentAmount > refinanceAmount - feeAmount) { + fungibleAsset.mint(borrower, loanRepaymentAmount - (refinanceAmount - feeAmount)); + } + uint256 originalBalance = fungibleAsset.balanceOf(feeCollector); loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); @@ -1024,12 +1093,11 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { } function testFuzz_shouldTransferSurplusToBorrower(uint256 refinanceAmount) external { + uint256 loanRepaymentAmount = loan.loanRepaymentAmount(loanId); refinanceAmount = bound( - refinanceAmount, - simpleLoan.loanRepayAmount + 1, - type(uint256).max - simpleLoan.loanRepayAmount - fungibleAsset.totalSupply() + refinanceAmount, loanRepaymentAmount + 1, type(uint256).max - loanRepaymentAmount - fungibleAsset.totalSupply() ); - uint256 surplus = refinanceAmount - simpleLoan.loanRepayAmount; + uint256 surplus = refinanceAmount - loanRepaymentAmount; refinancedLoanTerms.asset.amount = refinanceAmount; refinancedLoanTerms.lender = newLender; @@ -1045,8 +1113,9 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { } function testFuzz_shouldContributeFromBorrower(uint256 refinanceAmount) external { - refinanceAmount = bound(refinanceAmount, 1, simpleLoan.loanRepayAmount - 1); - uint256 contribution = simpleLoan.loanRepayAmount - refinanceAmount; + uint256 loanRepaymentAmount = loan.loanRepaymentAmount(loanId); + refinanceAmount = bound(refinanceAmount, 1, loanRepaymentAmount - 1); + uint256 contribution = loanRepaymentAmount - refinanceAmount; refinancedLoanTerms.asset.amount = refinanceAmount; refinancedLoanTerms.lender = newLender; @@ -1075,6 +1144,8 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { function setUp() override public { super.setUp(); + _mockLOAN(loanId, simpleLoan); + // Move collateral to vault vm.prank(borrower); nonFungibleAsset.transferFrom(borrower, address(loan), 2); @@ -1098,23 +1169,37 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { } function test_shouldFail_whenLoanIsDefaulted() external { - _mockLOAN(loanId, simpleLoan); - - vm.warp(simpleLoan.defaultTimestamp + 10000); + vm.warp(simpleLoan.defaultTimestamp); vm.expectRevert(abi.encodeWithSelector(LoanDefaulted.selector, simpleLoan.defaultTimestamp)); loan.repayLOAN(loanId, loanAssetPermit); } - function test_shouldCallPermit_whenProvided() external { + function testFuzz_shouldCallPermit_whenProvided( + uint256 _days, uint256 _principal, uint256 _fixedInterest, uint256 _dailyInterest + ) external { + _days = bound(_days, 0, loanDurationInDays - 1); + _principal = bound(_principal, 1, 1e40); + _fixedInterest = bound(_fixedInterest, 0, 1e40); + _dailyInterest = bound(_dailyInterest, 1, 274e8); + + simpleLoan.principalAmount = _principal; + simpleLoan.fixedInterestAmount = _fixedInterest; + simpleLoan.accruingInterestDailyRate = uint40(_dailyInterest); _mockLOAN(loanId, simpleLoan); + + vm.warp(simpleLoan.startTimestamp + _days * 1 days); + + uint256 loanRepaymentAmount = loan.loanRepaymentAmount(loanId); + fungibleAsset.mint(borrower, loanRepaymentAmount); + loanAssetPermit = abi.encodePacked(uint256(1), uint256(2), uint256(3), uint8(4)); vm.expectCall( simpleLoan.loanAssetAddress, abi.encodeWithSignature( "permit(address,address,uint256,uint256,uint8,bytes32,bytes32)", - borrower, address(loan), simpleLoan.loanRepayAmount, 1, uint8(4), uint256(2), uint256(3) + borrower, address(loan), loanRepaymentAmount, 1, uint8(4), uint256(2), uint256(3) ) ); @@ -1123,28 +1208,39 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { } function test_shouldDeleteLoanData_whenLOANOwnerIsOriginalLender() external { - _mockLOAN(loanId, simpleLoan); - loan.repayLOAN(loanId, loanAssetPermit); _assertLOANEq(loanId, nonExistingLoan); } function test_shouldBurnLOANToken_whenLOANOwnerIsOriginalLender() external { - _mockLOAN(loanId, simpleLoan); - vm.expectCall(loanToken, abi.encodeWithSignature("burn(uint256)", loanId)); loan.repayLOAN(loanId, loanAssetPermit); } - function test_shouldTransferRepaidAmountToLender_whenLOANOwnerIsOriginalLender() external { + function testFuzz_shouldTransferRepaidAmountToLender_whenLOANOwnerIsOriginalLender( + uint256 _days, uint256 _principal, uint256 _fixedInterest, uint256 _dailyInterest + ) external { + _days = bound(_days, 0, loanDurationInDays - 1); + _principal = bound(_principal, 1, 1e40); + _fixedInterest = bound(_fixedInterest, 0, 1e40); + _dailyInterest = bound(_dailyInterest, 1, 274e8); + + simpleLoan.principalAmount = _principal; + simpleLoan.fixedInterestAmount = _fixedInterest; + simpleLoan.accruingInterestDailyRate = uint40(_dailyInterest); _mockLOAN(loanId, simpleLoan); + vm.warp(simpleLoan.startTimestamp + _days * 1 days); + + uint256 loanRepaymentAmount = loan.loanRepaymentAmount(loanId); + fungibleAsset.mint(borrower, loanRepaymentAmount); + vm.expectCall( simpleLoan.loanAssetAddress, abi.encodeWithSignature( - "transferFrom(address,address,uint256)", borrower, lender, simpleLoan.loanRepayAmount + "transferFrom(address,address,uint256)", borrower, lender, loanRepaymentAmount ) ); @@ -1152,26 +1248,60 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { loan.repayLOAN(loanId, loanAssetPermit); } - function test_shouldMoveLoanToRepaidState_whenLOANOwnerIsNotOriginalLender() external { - _mockLOAN(loanId, simpleLoan); + function testFuzz_shouldUpdateLoanData_whenLOANOwnerIsNotOriginalLender( + uint256 _days, uint256 _principal, uint256 _fixedInterest, uint256 _dailyInterest + ) external { _mockLOANTokenOwner(loanId, notOriginalLender); + _days = bound(_days, 0, loanDurationInDays - 1); + _principal = bound(_principal, 1, 1e40); + _fixedInterest = bound(_fixedInterest, 0, 1e40); + _dailyInterest = bound(_dailyInterest, 1, 274e8); + + simpleLoan.principalAmount = _principal; + simpleLoan.fixedInterestAmount = _fixedInterest; + simpleLoan.accruingInterestDailyRate = uint40(_dailyInterest); + _mockLOAN(loanId, simpleLoan); + + vm.warp(simpleLoan.startTimestamp + _days * 1 days); + + uint256 loanRepaymentAmount = loan.loanRepaymentAmount(loanId); + fungibleAsset.mint(borrower, loanRepaymentAmount); + + vm.prank(borrower); loan.repayLOAN(loanId, loanAssetPermit); - bytes32 loanSlot = keccak256(abi.encode(loanId, LOANS_SLOT)); - // Parse status value from first storage slot - bytes32 statusValue = vm.load(address(loan), loanSlot) & bytes32(uint256(0xff)); - assertTrue(statusValue == bytes32(uint256(3))); + // Update loan and compare + 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); } - function test_shouldTransferRepaidAmountToVault_whenLOANOwnerIsNotOriginalLender() external { - _mockLOAN(loanId, simpleLoan); + function testFuzz_shouldTransferRepaidAmountToVault_whenLOANOwnerIsNotOriginalLender( + uint256 _days, uint256 _principal, uint256 _fixedInterest, uint256 _dailyInterest + ) external { _mockLOANTokenOwner(loanId, notOriginalLender); + _days = bound(_days, 0, loanDurationInDays - 1); + _principal = bound(_principal, 1, 1e40); + _fixedInterest = bound(_fixedInterest, 0, 1e40); + _dailyInterest = bound(_dailyInterest, 1, 274e8); + + simpleLoan.principalAmount = _principal; + simpleLoan.fixedInterestAmount = _fixedInterest; + simpleLoan.accruingInterestDailyRate = uint40(_dailyInterest); + _mockLOAN(loanId, simpleLoan); + + vm.warp(simpleLoan.startTimestamp + _days * 1 days); + + uint256 loanRepaymentAmount = loan.loanRepaymentAmount(loanId); + fungibleAsset.mint(borrower, loanRepaymentAmount); + vm.expectCall( simpleLoan.loanAssetAddress, abi.encodeWithSignature( - "transferFrom(address,address,uint256)", borrower, address(loan), simpleLoan.loanRepayAmount + "transferFrom(address,address,uint256)", borrower, address(loan), loanRepaymentAmount ) ); @@ -1180,8 +1310,6 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { } function test_shouldTransferCollateralToBorrower() external { - _mockLOAN(loanId, simpleLoan); - vm.expectCall( simpleLoan.collateral.assetAddress, abi.encodeWithSignature( @@ -1194,18 +1322,14 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { } function test_shouldEmitEvent_LOANPaidBack() external { - _mockLOAN(loanId, simpleLoan); - - vm.expectEmit(true, false, false, false); + vm.expectEmit(); emit LOANPaidBack(loanId); loan.repayLOAN(loanId, loanAssetPermit); } function test_shouldEmitEvent_LOANClaimed_whenLOANOwnerIsOriginalLender() external { - _mockLOAN(loanId, simpleLoan); - - vm.expectEmit(true, true, true, true); + vm.expectEmit(); emit LOANClaimed(loanId, false); loan.repayLOAN(loanId, loanAssetPermit); @@ -1214,6 +1338,58 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { } +/*----------------------------------------------------------*| +|* # LOAN REPAYMENT AMOUNT *| +|*----------------------------------------------------------*/ + +contract PWNSimpleLoan_LoanRepaymentAmount_Test is PWNSimpleLoanTest { + + function test_shouldReturnZero_whenLoanDoesNotExist() external { + assertEq(loan.loanRepaymentAmount(loanId), 0); + } + + function testFuzz_shouldReturnFixedInterest_whenZeroAccruedInterest( + uint256 _days, uint256 _principal, uint256 _fixedInterest + ) external { + _days = bound(_days, 0, 2 * loanDurationInDays); // should return non zero value even after loan expiration + _principal = bound(_principal, 1, 1e40); + _fixedInterest = bound(_fixedInterest, 0, 1e40); + + simpleLoan.defaultTimestamp = simpleLoan.startTimestamp + 101 * 1 days; + simpleLoan.principalAmount = _principal; + simpleLoan.fixedInterestAmount = _fixedInterest; + simpleLoan.accruingInterestDailyRate = 0; + _mockLOAN(loanId, simpleLoan); + + vm.warp(simpleLoan.startTimestamp + _days + 1 days); // should not have an effect + + assertEq(loan.loanRepaymentAmount(loanId), _principal + _fixedInterest); + } + + function test_shouldReturnAccruedInterest_whenNonZeroAccruedInterest( + uint256 _days, uint256 _principal, uint256 _fixedInterest, uint256 _dailyInterest + ) external { + _days = bound(_days, 0, 2 * loanDurationInDays); // should return non zero value even after loan expiration + _principal = bound(_principal, 1, 1e40); + _fixedInterest = bound(_fixedInterest, 0, 1e40); + _dailyInterest = bound(_dailyInterest, 1, 274e8); + + simpleLoan.defaultTimestamp = simpleLoan.startTimestamp + 101 * 1 days; + simpleLoan.principalAmount = _principal; + simpleLoan.fixedInterestAmount = _fixedInterest; + simpleLoan.accruingInterestDailyRate = uint40(_dailyInterest); + _mockLOAN(loanId, simpleLoan); + + vm.warp(simpleLoan.startTimestamp + _days * 1 days + 1); + + uint256 expectedInterest = _fixedInterest + _principal * _dailyInterest * _days / 1e10; + uint256 expectedLoanRepaymentAmount = _principal + expectedInterest; + assertEq(loan.loanRepaymentAmount(loanId), expectedLoanRepaymentAmount); + } + +} + + /*----------------------------------------------------------*| |* # CLAIM LOAN *| |*----------------------------------------------------------*/ @@ -1223,13 +1399,8 @@ contract PWNSimpleLoan_ClaimLOAN_Test is PWNSimpleLoanTest { function setUp() override public { super.setUp(); - vm.mockCall( - loanToken, - abi.encodeWithSignature("ownerOf(uint256)", loanId), - abi.encode(lender) - ); - simpleLoan.status = 3; + _mockLOAN(loanId, simpleLoan); // Move collateral to vault vm.prank(borrower); @@ -1237,15 +1408,18 @@ contract PWNSimpleLoan_ClaimLOAN_Test is PWNSimpleLoanTest { } - function test_shouldFail_whenCallerIsNotLOANTokenHolder() external { - _mockLOAN(loanId, simpleLoan); + function testFuzz_shouldFail_whenCallerIsNotLOANTokenHolder(address caller) external { + vm.assume(caller != lender); vm.expectRevert(abi.encodeWithSelector(CallerNotLOANTokenHolder.selector)); - vm.prank(borrower); + vm.prank(caller); loan.claimLOAN(loanId); } function test_shouldFail_whenLoanDoesNotExist() external { + simpleLoan.status = 0; + _mockLOAN(loanId, simpleLoan); + vm.expectRevert(abi.encodeWithSelector(NonExistingLoan.selector)); vm.prank(lender); loan.claimLOAN(loanId); @@ -1261,8 +1435,6 @@ contract PWNSimpleLoan_ClaimLOAN_Test is PWNSimpleLoanTest { } function test_shouldPass_whenLoanIsRepaid() external { - _mockLOAN(loanId, simpleLoan); - vm.prank(lender); loan.claimLOAN(loanId); } @@ -1271,15 +1443,13 @@ contract PWNSimpleLoan_ClaimLOAN_Test is PWNSimpleLoanTest { simpleLoan.status = 2; _mockLOAN(loanId, simpleLoan); - vm.warp(simpleLoan.defaultTimestamp + 10000); + vm.warp(simpleLoan.defaultTimestamp); vm.prank(lender); loan.claimLOAN(loanId); } function test_shouldDeleteLoanData() external { - _mockLOAN(loanId, simpleLoan); - vm.prank(lender); loan.claimLOAN(loanId); @@ -1287,8 +1457,6 @@ contract PWNSimpleLoan_ClaimLOAN_Test is PWNSimpleLoanTest { } function test_shouldBurnLOANToken() external { - _mockLOAN(loanId, simpleLoan); - vm.expectCall( loanToken, abi.encodeWithSignature("burn(uint256)", loanId) @@ -1298,14 +1466,24 @@ contract PWNSimpleLoan_ClaimLOAN_Test is PWNSimpleLoanTest { loan.claimLOAN(loanId); } - function test_shouldTransferRepaidAmountToLender_whenLoanIsRepaid() external { - simpleLoan.loanRepayAmount = 110; + function testFuzz_shouldTransferRepaidAmountToLender_whenLoanIsRepaid( + uint256 _principal, uint256 _fixedInterest + ) external { + _principal = bound(_principal, 1, 1e40); + _fixedInterest = bound(_fixedInterest, 0, 1e40); - _mockLOAN(loanId, simpleLoan); + // Note: loan repayment into Vault will reuse `fixedInterestAmount` and store total interest + // at the time of repayment and set `accruingInterestDailyRate` to zero. + simpleLoan.principalAmount = _principal; + simpleLoan.fixedInterestAmount = _fixedInterest; + simpleLoan.accruingInterestDailyRate = 0; + uint256 loanRepaymentAmount = loan.loanRepaymentAmount(loanId); + + fungibleAsset.mint(address(loan), loanRepaymentAmount); vm.expectCall( simpleLoan.loanAssetAddress, - abi.encodeWithSignature("transfer(address,uint256)", lender, simpleLoan.loanRepayAmount) + abi.encodeWithSignature("transfer(address,uint256)", lender, loanRepaymentAmount) ); vm.prank(lender); @@ -1316,7 +1494,7 @@ contract PWNSimpleLoan_ClaimLOAN_Test is PWNSimpleLoanTest { simpleLoan.status = 2; _mockLOAN(loanId, simpleLoan); - vm.warp(simpleLoan.defaultTimestamp + 10000); + vm.warp(simpleLoan.defaultTimestamp); vm.expectCall( simpleLoan.collateral.assetAddress, @@ -1331,9 +1509,7 @@ contract PWNSimpleLoan_ClaimLOAN_Test is PWNSimpleLoanTest { } function test_shouldEmitEvent_LOANClaimed_whenRepaid() external { - _mockLOAN(loanId, simpleLoan); - - vm.expectEmit(true, true, false, false); + vm.expectEmit(); emit LOANClaimed(loanId, false); vm.prank(lender); @@ -1344,9 +1520,9 @@ contract PWNSimpleLoan_ClaimLOAN_Test is PWNSimpleLoanTest { simpleLoan.status = 2; _mockLOAN(loanId, simpleLoan); - vm.warp(simpleLoan.defaultTimestamp + 10000); + vm.warp(simpleLoan.defaultTimestamp); - vm.expectEmit(true, true, false, false); + vm.expectEmit(); emit LOANClaimed(loanId, true); vm.prank(lender); @@ -1365,69 +1541,71 @@ contract PWNSimpleLoan_ExtendExpirationDate_Test is PWNSimpleLoanTest { function setUp() override public { super.setUp(); - // vm.warp(block.timestamp - 30039); // orig: block.timestamp + 40039 - vm.mockCall( - loanToken, - abi.encodeWithSignature("ownerOf(uint256)", loanId), - abi.encode(lender) - ); + _mockLOAN(loanId, simpleLoan); + + // Set current timestamp to 5 days before loan default + vm.warp(simpleLoan.defaultTimestamp - 5 days); } - function test_shouldFail_whenCallerIsNotLOANTokenHolder() external { - _mockLOAN(loanId, simpleLoan); + function testFuzz_shouldFail_whenCallerIsNotLOANTokenHolder(address caller) external { + vm.assume(caller != lender); vm.expectRevert(abi.encodeWithSelector(CallerNotLOANTokenHolder.selector)); - vm.prank(borrower); + vm.prank(caller); loan.extendLOANExpirationDate(loanId, simpleLoan.defaultTimestamp + 1); } - function test_shouldFail_whenExtendedExpirationDateIsSmallerThanCurrentExpirationDate() external { - _mockLOAN(loanId, simpleLoan); + function testFuzz_shouldFail_whenExtendedExpirationDateIsSmallerThanOrEqualToCurrentExpirationDate( + uint40 newExpiration + ) external { + newExpiration = uint40(bound(newExpiration, block.timestamp + 1, simpleLoan.defaultTimestamp)); vm.expectRevert(abi.encodeWithSelector(InvalidExtendedExpirationDate.selector)); vm.prank(lender); - loan.extendLOANExpirationDate(loanId, simpleLoan.defaultTimestamp - 1); + loan.extendLOANExpirationDate(loanId, newExpiration); } - function test_shouldFail_whenExtendedExpirationDateIsSmallerThanCurrentDate() external { - _mockLOAN(loanId, simpleLoan); - - vm.warp(simpleLoan.defaultTimestamp + 1000); + function testFuzz_shouldFail_whenExtendedExpirationDateIsSmallerThanOrEqualToCurrentDate(uint40 newExpiration) external { + newExpiration = uint40(bound(newExpiration, 0, block.timestamp)); vm.expectRevert(abi.encodeWithSelector(InvalidExtendedExpirationDate.selector)); vm.prank(lender); - loan.extendLOANExpirationDate(loanId, simpleLoan.defaultTimestamp + 500); + loan.extendLOANExpirationDate(loanId, newExpiration); } - function test_shouldFail_whenExtendedExpirationDateIsBiggerThanMaxExpirationExtension() external { - _mockLOAN(loanId, simpleLoan); + function testFuzz_shouldFail_whenExtendedExpirationDateIsBiggerThanMaxExpirationExtension( + uint40 newExpiration + ) external { + newExpiration = uint40(bound( + newExpiration, block.timestamp + MAX_EXPIRATION_EXTENSION + 1, type(uint40).max + )); vm.expectRevert(abi.encodeWithSelector(InvalidExtendedExpirationDate.selector)); vm.prank(lender); - loan.extendLOANExpirationDate(loanId, uint40(block.timestamp + MAX_EXPIRATION_EXTENSION + 1)); + loan.extendLOANExpirationDate(loanId, newExpiration); } - function test_shouldStoreExtendedExpirationDate() external { - _mockLOAN(loanId, simpleLoan); - - uint40 newExpiration = uint40(simpleLoan.defaultTimestamp + 10000); + function testFuzz_shouldStoreExtendedExpirationDate(uint40 newExpiration) external { + newExpiration = uint40(bound( + newExpiration, simpleLoan.defaultTimestamp + 1, block.timestamp + MAX_EXPIRATION_EXTENSION + )); vm.prank(lender); loan.extendLOANExpirationDate(loanId, newExpiration); bytes32 loanFirstSlot = keccak256(abi.encode(loanId, LOANS_SLOT)); bytes32 firstSlotValue = vm.load(address(loan), loanFirstSlot); - bytes32 expirationDateValue = firstSlotValue >> 168; + bytes32 expirationDateValue = firstSlotValue << 8 >> 216; assertEq(uint256(expirationDateValue), newExpiration); } - function test_shouldEmitEvent_LOANExpirationDateExtended() external { - _mockLOAN(loanId, simpleLoan); - - uint40 newExpiration = uint40(simpleLoan.defaultTimestamp + 10000); + function testFuzz_shouldEmitEvent_LOANExpirationDateExtended(uint40 newExpiration) external { + newExpiration = uint40(bound( + newExpiration, simpleLoan.defaultTimestamp + 1, block.timestamp + MAX_EXPIRATION_EXTENSION + )); - vm.expectEmit(true, true, true, true); + vm.expectEmit(); emit LOANExpirationDateExtended(loanId, newExpiration); vm.prank(lender); @@ -1449,10 +1627,10 @@ contract PWNSimpleLoan_GetLOAN_Test is PWNSimpleLoanTest { _assertLOANEq(loan.getLOAN(loanId), simpleLoan); } - function test_shouldReturnExpiredStatus_whenLOANExpired() external { + function test_shouldReturnDefaultedStatus_whenLOANDefaulted() external { _mockLOAN(loanId, simpleLoan); - vm.warp(simpleLoan.defaultTimestamp + 10000); + vm.warp(simpleLoan.defaultTimestamp); simpleLoan.status = 4; _assertLOANEq(loan.getLOAN(loanId), simpleLoan); @@ -1516,19 +1694,33 @@ contract PWNSimpleLoan_GetStateFingerprint_Test is PWNSimpleLoanTest { assertEq(fingerprint, bytes32(0)); } - function test_shouldReturnCorrectStateFingerprint() external { + function test_shouldUpdateStateFingerprint_whenLoanDefaulted() external { _mockLOAN(loanId, simpleLoan); - vm.warp(simpleLoan.defaultTimestamp - 10000); - assertEq(loan.getStateFingerprint(loanId), keccak256(abi.encode(2, simpleLoan.defaultTimestamp))); + vm.warp(simpleLoan.defaultTimestamp - 1); + assertEq( + loan.getStateFingerprint(loanId), + keccak256(abi.encode(2, simpleLoan.defaultTimestamp, simpleLoan.fixedInterestAmount, simpleLoan.accruingInterestDailyRate)) + ); - vm.warp(simpleLoan.defaultTimestamp + 10000); - assertEq(loan.getStateFingerprint(loanId), keccak256(abi.encode(4, simpleLoan.defaultTimestamp))); + vm.warp(simpleLoan.defaultTimestamp); + assertEq( + loan.getStateFingerprint(loanId), + keccak256(abi.encode(4, simpleLoan.defaultTimestamp, simpleLoan.fixedInterestAmount, simpleLoan.accruingInterestDailyRate)) + ); + } - simpleLoan.status = 3; - simpleLoan.defaultTimestamp = 60039; + function testFuzz_shouldReturnCorrectStateFingerprint( + uint256 fixedInterestAmount, uint40 accruingInterestDailyRate + ) external { + simpleLoan.fixedInterestAmount = fixedInterestAmount; + simpleLoan.accruingInterestDailyRate = accruingInterestDailyRate; _mockLOAN(loanId, simpleLoan); - assertEq(loan.getStateFingerprint(loanId), keccak256(abi.encode(3, 60039))); + + assertEq( + loan.getStateFingerprint(loanId), + keccak256(abi.encode(2, simpleLoan.defaultTimestamp, simpleLoan.fixedInterestAmount, simpleLoan.accruingInterestDailyRate)) + ); } } diff --git a/test/unit/PWNSimpleLoanListOffer.t.sol b/test/unit/PWNSimpleLoanListOffer.t.sol index b467ed9..db12f85 100644 --- a/test/unit/PWNSimpleLoanListOffer.t.sol +++ b/test/unit/PWNSimpleLoanListOffer.t.sol @@ -41,7 +41,8 @@ abstract contract PWNSimpleLoanListOfferTest is Test { collateralAmount: 1032, loanAssetAddress: token, loanAmount: 1101001, - loanYield: 1, + fixedInterestAmount: 1, + accruingInterestAPR: 0, duration: 1000, expiration: 0, allowedBorrower: address(0), @@ -74,7 +75,7 @@ abstract contract PWNSimpleLoanListOfferTest is Test { address(offerContract) )), keccak256(abi.encodePacked( - keccak256("Offer(uint8 collateralCategory,address collateralAddress,bytes32 collateralIdsWhitelistMerkleRoot,uint256 collateralAmount,address loanAssetAddress,uint256 loanAmount,uint256 loanYield,uint32 duration,uint40 expiration,address allowedBorrower,address lender,bool isPersistent,uint256 nonce)"), + keccak256("Offer(uint8 collateralCategory,address collateralAddress,bytes32 collateralIdsWhitelistMerkleRoot,uint256 collateralAmount,address loanAssetAddress,uint256 loanAmount,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedBorrower,address lender,bool isPersistent,uint256 nonce)"), abi.encode(_offer) )) )); @@ -276,6 +277,18 @@ contract PWNSimpleLoanListOffer_CreateLOANTerms_Test is PWNSimpleLoanListOfferTe offerContract.createLOANTerms(borrower, abi.encode(offer, offerValues), signature); } + function testFuzz_shouldFail_whenAccruingInterestAPROutOfBounds(uint40 interestAPR) external { + uint40 maxInterest = offerContract.MAX_ACCRUING_INTEREST_APR(); + interestAPR = uint40(bound(interestAPR, maxInterest + 1, type(uint40).max)); + + offer.accruingInterestAPR = interestAPR; + signature = _signOfferCompact(lenderPK, offer); + + vm.expectRevert(abi.encodeWithSelector(AccruingInterestAPROutOfBounds.selector, interestAPR, maxInterest)); + vm.prank(activeLoanContract); + offerContract.createLOANTerms(borrower, abi.encode(offer, offerValues), signature); + } + function test_shouldRevokeOffer_whenIsNotPersistent() external { offer.isPersistent = false; signature = _signOfferCompact(lenderPK, offer); @@ -289,15 +302,15 @@ contract PWNSimpleLoanListOffer_CreateLOANTerms_Test is PWNSimpleLoanListOfferTe offerContract.createLOANTerms(borrower, abi.encode(offer, offerValues), signature); } - // This test should fail because `revokeNonce` is not called for persistent offer - function testFail_shouldNotRevokeOffer_whenIsPersistent() external { + function test_shouldNotRevokeOffer_whenIsPersistent() external { offer.isPersistent = true; signature = _signOfferCompact(lenderPK, offer); - vm.expectCall( - revokedOfferNonce, - abi.encodeWithSignature("revokeNonce(address,uint256)", offer.lender, offer.nonce) - ); + vm.expectCall({ + callee: revokedOfferNonce, + data: abi.encodeWithSignature("revokeNonce(address,uint256)", offer.lender, offer.nonce), + count: 0 + }); vm.prank(activeLoanContract); offerContract.createLOANTerms(borrower, abi.encode(offer, offerValues), signature); @@ -347,7 +360,8 @@ contract PWNSimpleLoanListOffer_CreateLOANTerms_Test is PWNSimpleLoanListOfferTe signature = _signOfferCompact(lenderPK, offer); vm.prank(activeLoanContract); - (PWNLOANTerms.Simple memory loanTerms, bytes32 offerHash) = offerContract.createLOANTerms(borrower, abi.encode(offer, offerValues), signature); + (PWNLOANTerms.Simple memory loanTerms, bytes32 offerHash) + = offerContract.createLOANTerms(borrower, abi.encode(offer, offerValues), signature); assertTrue(loanTerms.lender == offer.lender); assertTrue(loanTerms.borrower == borrower); @@ -360,7 +374,9 @@ contract PWNSimpleLoanListOffer_CreateLOANTerms_Test is PWNSimpleLoanListOfferTe assertTrue(loanTerms.asset.assetAddress == offer.loanAssetAddress); assertTrue(loanTerms.asset.id == 0); assertTrue(loanTerms.asset.amount == offer.loanAmount); - assertTrue(loanTerms.loanRepayAmount == offer.loanAmount + offer.loanYield); + assertTrue(loanTerms.fixedInterestAmount == offer.fixedInterestAmount); + assertTrue(loanTerms.accruingInterestAPR == offer.accruingInterestAPR); + assertTrue(loanTerms.canCreate == true); assertTrue(loanTerms.canRefinance == true); assertTrue(loanTerms.refinancingLoanId == 0); diff --git a/test/unit/PWNSimpleLoanSimpleOffer.t.sol b/test/unit/PWNSimpleLoanSimpleOffer.t.sol index af5d4d4..1df3bdd 100644 --- a/test/unit/PWNSimpleLoanSimpleOffer.t.sol +++ b/test/unit/PWNSimpleLoanSimpleOffer.t.sol @@ -40,7 +40,8 @@ abstract contract PWNSimpleLoanSimpleOfferTest is Test { collateralAmount: 1032, loanAssetAddress: token, loanAmount: 1101001, - loanYield: 1, + fixedInterestAmount: 1, + accruingInterestAPR: 0, duration: 1000, expiration: 0, allowedBorrower: address(0), @@ -68,7 +69,7 @@ abstract contract PWNSimpleLoanSimpleOfferTest is Test { address(offerContract) )), keccak256(abi.encodePacked( - keccak256("Offer(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,address loanAssetAddress,uint256 loanAmount,uint256 loanYield,uint32 duration,uint40 expiration,address allowedBorrower,address lender,bool isPersistent,uint256 nonce)"), + keccak256("Offer(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,address loanAssetAddress,uint256 loanAmount,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedBorrower,address lender,bool isPersistent,uint256 nonce)"), abi.encode(_offer) )) )); @@ -270,6 +271,18 @@ contract PWNSimpleLoanSimpleOffer_CreateLOANTerms_Test is PWNSimpleLoanSimpleOff offerContract.createLOANTerms(borrower, abi.encode(offer), signature); } + function testFuzz_shouldFail_whenAccruingInterestAPROutOfBounds(uint40 interestAPR) external { + uint40 maxInterest = offerContract.MAX_ACCRUING_INTEREST_APR(); + interestAPR = uint40(bound(interestAPR, maxInterest + 1, type(uint40).max)); + + offer.accruingInterestAPR = interestAPR; + signature = _signOfferCompact(lenderPK, offer); + + vm.expectRevert(abi.encodeWithSelector(AccruingInterestAPROutOfBounds.selector, interestAPR, maxInterest)); + vm.prank(activeLoanContract); + offerContract.createLOANTerms(borrower, abi.encode(offer), signature); + } + function test_shouldRevokeOffer_whenIsNotPersistent() external { offer.isPersistent = false; signature = _signOfferCompact(lenderPK, offer); @@ -283,15 +296,15 @@ contract PWNSimpleLoanSimpleOffer_CreateLOANTerms_Test is PWNSimpleLoanSimpleOff offerContract.createLOANTerms(borrower, abi.encode(offer), signature); } - // This test should fail because `revokeNonce` is not called for persistent offer - function testFail_shouldNotRevokeOffer_whenIsPersistent() external { + function test_shouldNotRevokeOffer_whenIsPersistent() external { offer.isPersistent = true; signature = _signOfferCompact(lenderPK, offer); - vm.expectCall( - revokedOfferNonce, - abi.encodeWithSignature("revokeNonce(address,uint256)", offer.lender, offer.nonce) - ); + vm.expectCall({ + callee: revokedOfferNonce, + data: abi.encodeWithSignature("revokeNonce(address,uint256)", offer.lender, offer.nonce), + count: 0 + }); vm.prank(activeLoanContract); offerContract.createLOANTerms(borrower, abi.encode(offer), signature); @@ -303,7 +316,8 @@ contract PWNSimpleLoanSimpleOffer_CreateLOANTerms_Test is PWNSimpleLoanSimpleOff signature = _signOfferCompact(lenderPK, offer); vm.prank(activeLoanContract); - (PWNLOANTerms.Simple memory loanTerms, bytes32 offerHash) = offerContract.createLOANTerms(borrower, abi.encode(offer), signature); + (PWNLOANTerms.Simple memory loanTerms, bytes32 offerHash) + = offerContract.createLOANTerms(borrower, abi.encode(offer), signature); assertTrue(loanTerms.lender == offer.lender); assertTrue(loanTerms.borrower == borrower); @@ -316,7 +330,9 @@ contract PWNSimpleLoanSimpleOffer_CreateLOANTerms_Test is PWNSimpleLoanSimpleOff assertTrue(loanTerms.asset.assetAddress == offer.loanAssetAddress); assertTrue(loanTerms.asset.id == 0); assertTrue(loanTerms.asset.amount == offer.loanAmount); - assertTrue(loanTerms.loanRepayAmount == offer.loanAmount + offer.loanYield); + assertTrue(loanTerms.fixedInterestAmount == offer.fixedInterestAmount); + assertTrue(loanTerms.accruingInterestAPR == offer.accruingInterestAPR); + assertTrue(loanTerms.canCreate == true); assertTrue(loanTerms.canRefinance == true); assertTrue(loanTerms.refinancingLoanId == 0); diff --git a/test/unit/PWNSimpleLoanSimpleRequest.t.sol b/test/unit/PWNSimpleLoanSimpleRequest.t.sol index 53f0f87..20d7b59 100644 --- a/test/unit/PWNSimpleLoanSimpleRequest.t.sol +++ b/test/unit/PWNSimpleLoanSimpleRequest.t.sol @@ -40,7 +40,8 @@ abstract contract PWNSimpleLoanSimpleRequestTest is Test { collateralAmount: 1032, loanAssetAddress: token, loanAmount: 1101001, - loanYield: 1, + fixedInterestAmount: 1, + accruingInterestAPR: 0, duration: 1000, expiration: 0, allowedLender: address(0), @@ -68,7 +69,7 @@ abstract contract PWNSimpleLoanSimpleRequestTest is Test { address(requestContract) )), keccak256(abi.encodePacked( - keccak256("Request(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,address loanAssetAddress,uint256 loanAmount,uint256 loanYield,uint32 duration,uint40 expiration,address allowedLender,address borrower,uint256 refinancingLoanId,uint256 nonce)"), + keccak256("Request(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,address loanAssetAddress,uint256 loanAmount,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedLender,address borrower,uint256 refinancingLoanId,uint256 nonce)"), abi.encode(_request) )) )); @@ -270,6 +271,18 @@ contract PWNSimpleLoanSimpleRequest_CreateLOANTerms_Test is PWNSimpleLoanSimpleR requestContract.createLOANTerms(lender, abi.encode(request), signature); } + function testFuzz_shouldFail_whenAccruingInterestAPROutOfBounds(uint40 interestAPR) external { + uint40 maxInterest = requestContract.MAX_ACCRUING_INTEREST_APR(); + interestAPR = uint40(bound(interestAPR, maxInterest + 1, type(uint40).max)); + + request.accruingInterestAPR = interestAPR; + signature = _signRequestCompact(borrowerPK, request); + + vm.expectRevert(abi.encodeWithSelector(AccruingInterestAPROutOfBounds.selector, interestAPR, maxInterest)); + vm.prank(activeLoanContract); + requestContract.createLOANTerms(lender, abi.encode(request), signature); + } + function test_shouldRevokeRequest() external { signature = _signRequestCompact(borrowerPK, request); @@ -282,13 +295,16 @@ contract PWNSimpleLoanSimpleRequest_CreateLOANTerms_Test is PWNSimpleLoanSimpleR requestContract.createLOANTerms(lender, abi.encode(request), signature); } - function test_shouldReturnCorrectValues() external { + function testFuzz_shouldReturnCorrectValues(uint256 _refinancingLoanId) external { + request.refinancingLoanId = _refinancingLoanId; + uint256 currentTimestamp = 40303; vm.warp(currentTimestamp); signature = _signRequestCompact(borrowerPK, request); vm.prank(activeLoanContract); - (PWNLOANTerms.Simple memory loanTerms, bytes32 requestHash) = requestContract.createLOANTerms(lender, abi.encode(request), signature); + (PWNLOANTerms.Simple memory loanTerms, bytes32 requestHash) + = requestContract.createLOANTerms(lender, abi.encode(request), signature); assertTrue(loanTerms.lender == lender); assertTrue(loanTerms.borrower == request.borrower); @@ -301,8 +317,10 @@ contract PWNSimpleLoanSimpleRequest_CreateLOANTerms_Test is PWNSimpleLoanSimpleR assertTrue(loanTerms.asset.assetAddress == request.loanAssetAddress); assertTrue(loanTerms.asset.id == 0); assertTrue(loanTerms.asset.amount == request.loanAmount); - assertTrue(loanTerms.loanRepayAmount == request.loanAmount + request.loanYield); - assertTrue(loanTerms.canRefinance == false); + assertTrue(loanTerms.fixedInterestAmount == request.fixedInterestAmount); + assertTrue(loanTerms.accruingInterestAPR == request.accruingInterestAPR); + assertTrue(loanTerms.canCreate == (request.refinancingLoanId == 0)); + assertTrue(loanTerms.canRefinance == (request.refinancingLoanId != 0)); assertTrue(loanTerms.refinancingLoanId == request.refinancingLoanId); assertTrue(requestHash == _requestHash(request)); From 44073ae6c82a940fee906fd037df9639de0b63ea Mon Sep 17 00:00:00 2001 From: ashhanai Date: Mon, 26 Feb 2024 13:41:42 +0100 Subject: [PATCH 022/129] feat(refinance): emit refinancing loan event --- src/loan/terms/simple/loan/PWNSimpleLoan.sol | 7 +++++++ test/unit/PWNSimpleLoan.t.sol | 8 ++++++++ 2 files changed, 15 insertions(+) diff --git a/src/loan/terms/simple/loan/PWNSimpleLoan.sol b/src/loan/terms/simple/loan/PWNSimpleLoan.sol index 51adfc5..289a001 100644 --- a/src/loan/terms/simple/loan/PWNSimpleLoan.sol +++ b/src/loan/terms/simple/loan/PWNSimpleLoan.sol @@ -99,6 +99,11 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { */ event LOANClaimed(uint256 indexed loanId, bool indexed defaulted); + /** + * @dev Emitted when a loan is refinanced. + */ + event LOANRefinanced(uint256 indexed loanId, uint256 indexed refinancedLoanId); + /** * @dev Emitted when a LOAN token holder extends loan expiration date. */ @@ -319,6 +324,8 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { lenderLoanAssetPermit, borrowerLoanAssetPermit ); + + emit LOANRefinanced({ loanId: loanId, refinancedLoanId: refinancedLoanId }); } /** diff --git a/test/unit/PWNSimpleLoan.t.sol b/test/unit/PWNSimpleLoan.t.sol index 5526724..e0a2558 100644 --- a/test/unit/PWNSimpleLoan.t.sol +++ b/test/unit/PWNSimpleLoan.t.sol @@ -49,6 +49,7 @@ abstract contract PWNSimpleLoanTest is Test { event LOANCreated(uint256 indexed loanId, PWNLOANTerms.Simple terms, bytes32 indexed factoryDataHash, address indexed factoryAddress); event LOANPaidBack(uint256 indexed loanId); event LOANClaimed(uint256 indexed loanId, bool indexed defaulted); + event LOANRefinanced(uint256 indexed loanId, uint256 indexed refinancedLoanId); event LOANExpirationDateExtended(uint256 indexed loanId, uint40 extendedExpirationDate); function setUp() virtual public { @@ -618,6 +619,13 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); } + function test_shouldEmit_LOANRefinanced() external { + vm.expectEmit(); + emit LOANRefinanced(loanId, ferinancedLoanId); + + loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); + } + function test_shouldDeleteOldLoanData_whenLOANOwnerIsOriginalLender() external { loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); From 858f5dde6863f47b69db63796abb63d4237f6182 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Mon, 26 Feb 2024 14:29:29 +0100 Subject: [PATCH 023/129] refactor(get-loan): return loan properties separately with some additional computed properties --- src/loan/terms/simple/loan/PWNSimpleLoan.sol | 43 +++++- test/unit/PWNSimpleLoan.t.sol | 149 ++++++++++++++++++- 2 files changed, 181 insertions(+), 11 deletions(-) diff --git a/src/loan/terms/simple/loan/PWNSimpleLoan.sol b/src/loan/terms/simple/loan/PWNSimpleLoan.sol index 289a001..00516a7 100644 --- a/src/loan/terms/simple/loan/PWNSimpleLoan.sol +++ b/src/loan/terms/simple/loan/PWNSimpleLoan.sol @@ -767,11 +767,44 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { /** * @notice Return a LOAN data struct associated with a loan id. * @param loanId Id of a loan in question. - * @return loan LOAN data struct or empty struct if the LOAN doesn't exist. - */ - function getLOAN(uint256 loanId) external view returns (LOAN memory loan) { - loan = LOANs[loanId]; - loan.status = _getLOANStatus(loanId); + * @return status LOAN status. + * @return startTimestamp Unix timestamp (in seconds) of a loan creation date. + * @return defaultTimestamp Unix timestamp (in seconds) of a loan expiration date. + * @return borrower Address of a loan borrower. + * @return originalLender Address of a loan original lender. + * @return loanOwner Address of a LOAN token holder. + * @return accruingInterestDailyRate Daily interest rate in basis points. + * @return fixedInterestAmount Fixed interest amount in loan asset tokens. + * @return loanAsset Asset used as a loan credit. For a definition see { MultiToken dependency lib }. + * @return collateral Asset used as a loan collateral. For a definition see { MultiToken dependency lib }. + * @return repaymentAmount Loan repayment amount in loan asset tokens. + */ + function getLOAN(uint256 loanId) external view returns ( + uint8 status, + uint40 startTimestamp, + uint40 defaultTimestamp, + address borrower, + address originalLender, + address loanOwner, + uint40 accruingInterestDailyRate, + uint256 fixedInterestAmount, + MultiToken.Asset memory loanAsset, + MultiToken.Asset memory collateral, + uint256 repaymentAmount + ) { + LOAN storage loan = LOANs[loanId]; + + status = _getLOANStatus(loanId); + startTimestamp = loan.startTimestamp; + defaultTimestamp = loan.defaultTimestamp; + borrower = loan.borrower; + originalLender = loan.originalLender; + loanOwner = loan.status != 0 ? loanToken.ownerOf(loanId) : address(0); + accruingInterestDailyRate = loan.accruingInterestDailyRate; + fixedInterestAmount = loan.fixedInterestAmount; + loanAsset = MultiToken.ERC20(loan.loanAssetAddress, loan.principalAmount); + collateral = loan.collateral; + repaymentAmount = loanRepaymentAmount(loanId); } /** diff --git a/test/unit/PWNSimpleLoan.t.sol b/test/unit/PWNSimpleLoan.t.sol index e0a2558..c283d00 100644 --- a/test/unit/PWNSimpleLoan.t.sol +++ b/test/unit/PWNSimpleLoan.t.sol @@ -1629,23 +1629,160 @@ contract PWNSimpleLoan_ExtendExpirationDate_Test is PWNSimpleLoanTest { contract PWNSimpleLoan_GetLOAN_Test is PWNSimpleLoanTest { - function test_shouldReturnLOANData() external { + function testFuzz_shouldReturnStaticLOANData( + uint40 _startTimestamp, + uint40 _defaultTimestamp, + address _borrower, + address _originalLender, + uint40 _accruingInterestDailyRate, + uint256 _fixedInterestAmount, + address _loanAssetAddress, + uint256 _principalAmount, + uint8 _collateralCategory, + address _collateralAssetAddress, + uint256 _collateralId, + uint256 _collateralAmount + ) external { + _startTimestamp = uint40(bound(_startTimestamp, 0, type(uint40).max - 1)); + _defaultTimestamp = uint40(bound(_defaultTimestamp, _startTimestamp + 1, type(uint40).max)); + _accruingInterestDailyRate = uint40(bound(_accruingInterestDailyRate, 0, 274e8)); + _fixedInterestAmount = bound(_fixedInterestAmount, 0, type(uint256).max - _principalAmount); + + simpleLoan.startTimestamp = _startTimestamp; + simpleLoan.defaultTimestamp = _defaultTimestamp; + simpleLoan.borrower = _borrower; + simpleLoan.originalLender = _originalLender; + simpleLoan.accruingInterestDailyRate = _accruingInterestDailyRate; + simpleLoan.fixedInterestAmount = _fixedInterestAmount; + simpleLoan.loanAssetAddress = _loanAssetAddress; + simpleLoan.principalAmount = _principalAmount; + simpleLoan.collateral.category = MultiToken.Category(_collateralCategory % 4); + simpleLoan.collateral.assetAddress = _collateralAssetAddress; + simpleLoan.collateral.id = _collateralId; + simpleLoan.collateral.amount = _collateralAmount; _mockLOAN(loanId, simpleLoan); - _assertLOANEq(loan.getLOAN(loanId), simpleLoan); + vm.warp(_startTimestamp); + + // test every property separately to avoid stack too deep error + { + (,uint40 startTimestamp,,,,,,,,,) = loan.getLOAN(loanId); + assertEq(startTimestamp, _startTimestamp); + } + { + (,,uint40 defaultTimestamp,,,,,,,,) = loan.getLOAN(loanId); + assertEq(defaultTimestamp, _defaultTimestamp); + } + { + (,,,address borrower,,,,,,,) = loan.getLOAN(loanId); + assertEq(borrower, _borrower); + } + { + (,,,,address originalLender,,,,,,) = loan.getLOAN(loanId); + assertEq(originalLender, _originalLender); + } + { + (,,,,,,uint40 accruingInterestDailyRate,,,,) = loan.getLOAN(loanId); + assertEq(accruingInterestDailyRate, _accruingInterestDailyRate); + } + { + (,,,,,,,uint256 fixedInterestAmount,,,) = loan.getLOAN(loanId); + assertEq(fixedInterestAmount, _fixedInterestAmount); + } + { + (,,,,,,,,MultiToken.Asset memory loanAsset,,) = loan.getLOAN(loanId); + assertEq(loanAsset.assetAddress, _loanAssetAddress); + assertEq(loanAsset.amount, _principalAmount); + } + { + (,,,,,,,,,MultiToken.Asset memory collateral,) = loan.getLOAN(loanId); + assertEq(collateral.assetAddress, _collateralAssetAddress); + assertEq(uint8(collateral.category), _collateralCategory % 4); + assertEq(collateral.id, _collateralId); + assertEq(collateral.amount, _collateralAmount); + } } - function test_shouldReturnDefaultedStatus_whenLOANDefaulted() external { + function test_shouldReturnCorrectStatus() external { _mockLOAN(loanId, simpleLoan); + (uint8 status,,,,,,,,,,) = loan.getLOAN(loanId); + assertEq(status, 2); + vm.warp(simpleLoan.defaultTimestamp); - simpleLoan.status = 4; - _assertLOANEq(loan.getLOAN(loanId), simpleLoan); + (status,,,,,,,,,,) = loan.getLOAN(loanId); + assertEq(status, 4); + + simpleLoan.status = 3; + _mockLOAN(loanId, simpleLoan); + + (status,,,,,,,,,,) = loan.getLOAN(loanId); + assertEq(status, 3); + } + + function testFuzz_shouldReturnLOANTokenOwner(address _loanOwner) external { + _mockLOAN(loanId, simpleLoan); + _mockLOANTokenOwner(loanId, _loanOwner); + + (,,,,, address loanOwner,,,,,) = loan.getLOAN(loanId); + assertEq(loanOwner, _loanOwner); + } + + function testFuzz_shouldReturnRepaymentAmount( + uint256 _days, + uint256 _principalAmount, + uint40 _accruingInterestDailyRate, + uint256 _fixedInterestAmount + ) external { + _days = bound(_days, 0, 2 * loanDurationInDays); + _principalAmount = bound(_principalAmount, 1, 1e40); + _accruingInterestDailyRate = uint40(bound(_accruingInterestDailyRate, 0, 274e8)); + _fixedInterestAmount = bound(_fixedInterestAmount, 0, _principalAmount); + + simpleLoan.accruingInterestDailyRate = _accruingInterestDailyRate; + simpleLoan.fixedInterestAmount = _fixedInterestAmount; + simpleLoan.principalAmount = _principalAmount; + _mockLOAN(loanId, simpleLoan); + + vm.warp(simpleLoan.startTimestamp + _days * 1 days); + + (,,,,,,,,,, uint256 repaymentAmount) = loan.getLOAN(loanId); + assertEq(repaymentAmount, loan.loanRepaymentAmount(loanId)); } function test_shouldReturnEmptyLOANDataForNonExistingLoan() external { - _assertLOANEq(loan.getLOAN(loanId), nonExistingLoan); + uint256 nonExistingLoanId = loanId + 1; + + ( + uint8 status, + uint40 startTimestamp, + uint40 defaultTimestamp, + address borrower, + address originalLender, + address loanOwner, + uint40 accruingInterestDailyRate, + uint256 fixedInterestAmount, + MultiToken.Asset memory loanAsset, + MultiToken.Asset memory collateral, + uint256 repaymentAmount + ) = loan.getLOAN(nonExistingLoanId); + + assertEq(status, 0); + assertEq(startTimestamp, 0); + assertEq(defaultTimestamp, 0); + assertEq(borrower, address(0)); + assertEq(originalLender, address(0)); + assertEq(loanOwner, address(0)); + assertEq(accruingInterestDailyRate, 0); + assertEq(fixedInterestAmount, 0); + assertEq(loanAsset.assetAddress, address(0)); + assertEq(loanAsset.amount, 0); + assertEq(collateral.assetAddress, address(0)); + assertEq(uint8(collateral.category), 0); + assertEq(collateral.id, 0); + assertEq(collateral.amount, 0); + assertEq(repaymentAmount, 0); } } From af5a58543a4b25c425d1718127d427eb733870fe Mon Sep 17 00:00:00 2001 From: ashhanai Date: Mon, 26 Feb 2024 14:47:49 +0100 Subject: [PATCH 024/129] feat(on-chain-offer-request): enrich on-chain offer and request events --- .../factory/offer/PWNSimpleLoanListOffer.sol | 14 +++++++++++++- .../factory/offer/PWNSimpleLoanSimpleOffer.sol | 14 +++++++++++++- .../factory/offer/base/PWNSimpleLoanOffer.sol | 11 ----------- .../request/PWNSimpleLoanSimpleRequest.sol | 14 +++++++++++++- .../request/base/PWNSimpleLoanRequest.sol | 11 ----------- test/unit/PWNSimpleLoanListOffer.t.sol | 18 ++++++++++++++---- test/unit/PWNSimpleLoanOffer.t.sol | 14 +------------- test/unit/PWNSimpleLoanRequest.t.sol | 16 ++-------------- test/unit/PWNSimpleLoanSimpleOffer.t.sol | 16 +++++++++++++--- test/unit/PWNSimpleLoanSimpleRequest.t.sol | 16 +++++++++++++--- 10 files changed, 82 insertions(+), 62 deletions(-) diff --git a/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol b/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol index fe20b5f..1be775d 100644 --- a/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol +++ b/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol @@ -81,6 +81,16 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanOffer { } + /*----------------------------------------------------------*| + |* # EVENTS DEFINITIONS *| + |*----------------------------------------------------------*/ + + /** + * @dev Emitted when an offer is made via an on-chain transaction. + */ + event OfferMade(bytes32 indexed offerHash, address indexed lender, Offer offer); + + /*----------------------------------------------------------*| |* # CONSTRUCTOR *| |*----------------------------------------------------------*/ @@ -106,7 +116,9 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanOffer { * @param offer Offer struct containing all needed offer data. */ function makeOffer(Offer calldata offer) external { - _makeOffer(getOfferHash(offer), offer.lender); + bytes32 offerHash = getOfferHash(offer); + _makeOffer(offerHash, offer.lender); + emit OfferMade(offerHash, offer.lender, offer); } diff --git a/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol b/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol index 7a040c1..d64f679 100644 --- a/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol +++ b/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol @@ -66,6 +66,16 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanOffer { } + /*----------------------------------------------------------*| + |* # EVENTS DEFINITIONS *| + |*----------------------------------------------------------*/ + + /** + * @dev Emitted when an offer is made via an on-chain transaction. + */ + event OfferMade(bytes32 indexed offerHash, address indexed lender, Offer offer); + + /*----------------------------------------------------------*| |* # CONSTRUCTOR *| |*----------------------------------------------------------*/ @@ -91,7 +101,9 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanOffer { * @param offer Offer struct containing all needed offer data. */ function makeOffer(Offer calldata offer) external { - _makeOffer(getOfferHash(offer), offer.lender); + bytes32 offerHash = getOfferHash(offer); + _makeOffer(offerHash, offer.lender); + emit OfferMade(offerHash, offer.lender, offer); } diff --git a/src/loan/terms/simple/factory/offer/base/PWNSimpleLoanOffer.sol b/src/loan/terms/simple/factory/offer/base/PWNSimpleLoanOffer.sol index b63ba92..acb17f2 100644 --- a/src/loan/terms/simple/factory/offer/base/PWNSimpleLoanOffer.sol +++ b/src/loan/terms/simple/factory/offer/base/PWNSimpleLoanOffer.sol @@ -22,15 +22,6 @@ abstract contract PWNSimpleLoanOffer is PWNSimpleLoanTermsFactory, PWNHubAccessC */ mapping (bytes32 => bool) public offersMade; - /*----------------------------------------------------------*| - |* # EVENTS & ERRORS DEFINITIONS *| - |*----------------------------------------------------------*/ - - /** - * @dev Emitted when an offer is made via an on-chain transaction. - */ - event OfferMade(bytes32 indexed offerHash, address indexed lender); - /*----------------------------------------------------------*| |* # CONSTRUCTOR *| @@ -58,8 +49,6 @@ abstract contract PWNSimpleLoanOffer is PWNSimpleLoanTermsFactory, PWNHubAccessC // Mark offer as made offersMade[offerStructHash] = true; - - emit OfferMade(offerStructHash, lender); } /** diff --git a/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol b/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol index 127f6aa..04ad13a 100644 --- a/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol +++ b/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol @@ -66,6 +66,16 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanRequest { } + /*----------------------------------------------------------*| + |* # EVENTS DEFINITIONS *| + |*----------------------------------------------------------*/ + + /** + * @dev Emitted when a request is made via an on-chain transaction. + */ + event RequestMade(bytes32 indexed requestHash, address indexed borrower, Request request); + + /*----------------------------------------------------------*| |* # CONSTRUCTOR *| |*----------------------------------------------------------*/ @@ -91,7 +101,9 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanRequest { * @param request Request struct containing all needed request data. */ function makeRequest(Request calldata request) external { - _makeRequest(getRequestHash(request), request.borrower); + bytes32 requestHash = getRequestHash(request); + _makeRequest(requestHash, request.borrower); + emit RequestMade(requestHash, request.borrower, request); } diff --git a/src/loan/terms/simple/factory/request/base/PWNSimpleLoanRequest.sol b/src/loan/terms/simple/factory/request/base/PWNSimpleLoanRequest.sol index 6bec8dd..4c46504 100644 --- a/src/loan/terms/simple/factory/request/base/PWNSimpleLoanRequest.sol +++ b/src/loan/terms/simple/factory/request/base/PWNSimpleLoanRequest.sol @@ -22,15 +22,6 @@ abstract contract PWNSimpleLoanRequest is PWNSimpleLoanTermsFactory, PWNHubAcces */ mapping (bytes32 => bool) public requestsMade; - /*----------------------------------------------------------*| - |* # EVENTS & ERRORS DEFINITIONS *| - |*----------------------------------------------------------*/ - - /** - * @dev Emitted when a request is made via an on-chain transaction. - */ - event RequestMade(bytes32 indexed requestHash, address indexed borrower); - /*----------------------------------------------------------*| |* # CONSTRUCTOR *| @@ -58,8 +49,6 @@ abstract contract PWNSimpleLoanRequest is PWNSimpleLoanTermsFactory, PWNHubAcces // Mark request as made requestsMade[requestStructHash] = true; - - emit RequestMade(requestStructHash, borrower); } /** diff --git a/test/unit/PWNSimpleLoanListOffer.t.sol b/test/unit/PWNSimpleLoanListOffer.t.sol index db12f85..68141a4 100644 --- a/test/unit/PWNSimpleLoanListOffer.t.sol +++ b/test/unit/PWNSimpleLoanListOffer.t.sol @@ -25,13 +25,13 @@ abstract contract PWNSimpleLoanListOfferTest is Test { uint256 lenderPK = uint256(73661723); address lender = vm.addr(lenderPK); - constructor() { + event OfferMade(bytes32 indexed offerHash, address indexed lender, PWNSimpleLoanListOffer.Offer offer); + + function setUp() virtual public { vm.etch(hub, bytes("data")); vm.etch(revokedOfferNonce, bytes("data")); vm.etch(token, bytes("data")); - } - function setUp() virtual public { offerContract = new PWNSimpleLoanListOffer(hub, revokedOfferNonce); offer = PWNSimpleLoanListOffer.Offer({ @@ -99,7 +99,17 @@ contract PWNSimpleLoanListOffer_MakeOffer_Test is PWNSimpleLoanListOfferTest { address(offerContract), keccak256(abi.encode(_offerHash(offer), OFFERS_MADE_SLOT)) ); - assertEq(isMadeValue, bytes32(uint256(1))); + assertEq(uint256(isMadeValue), 1); + } + + function test_shouldEmit_OfferMade() external { + bytes32 offerHash = _offerHash(offer); + + vm.expectEmit(); + emit OfferMade(offerHash, lender, offer); + + vm.prank(lender); + offerContract.makeOffer(offer); } } diff --git a/test/unit/PWNSimpleLoanOffer.t.sol b/test/unit/PWNSimpleLoanOffer.t.sol index 87c8274..4453b3e 100644 --- a/test/unit/PWNSimpleLoanOffer.t.sol +++ b/test/unit/PWNSimpleLoanOffer.t.sol @@ -46,14 +46,10 @@ abstract contract PWNSimpleLoanOfferTest is Test { address lender = address(0x070ce3); uint256 nonce = uint256(keccak256("nonce_1")); - event OfferMade(bytes32 indexed offerHash, address indexed lender); - - constructor() { + function setUp() virtual public { vm.etch(hub, bytes("data")); vm.etch(revokedOfferNonce, bytes("data")); - } - function setUp() virtual public { offerContract = new PWNSimpleLoanOfferExposed(hub, revokedOfferNonce); vm.mockCall( @@ -88,14 +84,6 @@ contract PWNSimpleLoanOffer_MakeOffer_Test is PWNSimpleLoanOfferTest { assertEq(isMadeValue, bytes32(uint256(1))); } - function test_shouldEmitEvent_OfferMade() external { - vm.expectEmit(true, true, false, false); - emit OfferMade(offerHash, lender); - - vm.prank(lender); - offerContract.makeOffer(offerHash, lender); - } - } diff --git a/test/unit/PWNSimpleLoanRequest.t.sol b/test/unit/PWNSimpleLoanRequest.t.sol index 3b3d88c..ef9cfc3 100644 --- a/test/unit/PWNSimpleLoanRequest.t.sol +++ b/test/unit/PWNSimpleLoanRequest.t.sol @@ -46,14 +46,10 @@ abstract contract PWNSimpleLoanRequestTest is Test { address borrower = address(0x070ce3); uint256 nonce = uint256(keccak256("nonce_1")); - event RequestMade(bytes32 indexed requestHash, address indexed borrower); - - constructor() { + function setUp() virtual public { vm.etch(hub, bytes("data")); vm.etch(revokedRequestNonce, bytes("data")); - } - function setUp() virtual public { requestContract = new PWNSimpleLoanRequestExposed(hub, revokedRequestNonce); vm.mockCall( @@ -85,15 +81,7 @@ contract PWNSimpleLoanRequest_MakeRequest_Test is PWNSimpleLoanRequestTest { address(requestContract), keccak256(abi.encode(requestHash, REQUESTS_MADE_SLOT)) ); - assertEq(isMadeValue, bytes32(uint256(1))); - } - - function test_shouldEmitEvent_RequestMade() external { - vm.expectEmit(true, true, false, false); - emit RequestMade(requestHash, borrower); - - vm.prank(borrower); - requestContract.makeRequest(requestHash, borrower); + assertEq(uint256(isMadeValue), 1); } } diff --git a/test/unit/PWNSimpleLoanSimpleOffer.t.sol b/test/unit/PWNSimpleLoanSimpleOffer.t.sol index 1df3bdd..5a377de 100644 --- a/test/unit/PWNSimpleLoanSimpleOffer.t.sol +++ b/test/unit/PWNSimpleLoanSimpleOffer.t.sol @@ -24,13 +24,13 @@ abstract contract PWNSimpleLoanSimpleOfferTest is Test { uint256 lenderPK = uint256(73661723); address lender = vm.addr(lenderPK); - constructor() { + event OfferMade(bytes32 indexed offerHash, address indexed lender, PWNSimpleLoanSimpleOffer.Offer offer); + + function setUp() virtual public { vm.etch(hub, bytes("data")); vm.etch(revokedOfferNonce, bytes("data")); vm.etch(token, bytes("data")); - } - function setUp() virtual public { offerContract = new PWNSimpleLoanSimpleOffer(hub, revokedOfferNonce); offer = PWNSimpleLoanSimpleOffer.Offer({ @@ -96,6 +96,16 @@ contract PWNSimpleLoanSimpleOffer_MakeOffer_Test is PWNSimpleLoanSimpleOfferTest assertEq(isMadeValue, bytes32(uint256(1))); } + function test_shouldEmit_OfferMade() external { + bytes32 offerHash = _offerHash(offer); + + vm.expectEmit(); + emit OfferMade(offerHash, lender, offer); + + vm.prank(lender); + offerContract.makeOffer(offer); + } + } diff --git a/test/unit/PWNSimpleLoanSimpleRequest.t.sol b/test/unit/PWNSimpleLoanSimpleRequest.t.sol index 20d7b59..591d701 100644 --- a/test/unit/PWNSimpleLoanSimpleRequest.t.sol +++ b/test/unit/PWNSimpleLoanSimpleRequest.t.sol @@ -24,13 +24,13 @@ abstract contract PWNSimpleLoanSimpleRequestTest is Test { uint256 borrowerPK = uint256(73661723); address borrower = vm.addr(borrowerPK); - constructor() { + event RequestMade(bytes32 indexed requestHash, address indexed borrower, PWNSimpleLoanSimpleRequest.Request request); + + function setUp() virtual public { vm.etch(hub, bytes("data")); vm.etch(revokedRequestNonce, bytes("data")); vm.etch(token, bytes("data")); - } - function setUp() virtual public { requestContract = new PWNSimpleLoanSimpleRequest(hub, revokedRequestNonce); request = PWNSimpleLoanSimpleRequest.Request({ @@ -96,6 +96,16 @@ contract PWNSimpleLoanSimpleRequest_MakeRequest_Test is PWNSimpleLoanSimpleReque assertEq(isMadeValue, bytes32(uint256(1))); } + function test_shouldEmit_RequestMade() external { + bytes32 requestHash = _requestHash(request); + + vm.expectEmit(); + emit RequestMade(requestHash, borrower, request); + + vm.prank(borrower); + requestContract.makeRequest(request); + } + } From f154419f57061c3e9043aaf25290454eb3e61e0c Mon Sep 17 00:00:00 2001 From: ashhanai Date: Tue, 27 Feb 2024 16:40:14 +0100 Subject: [PATCH 025/129] style(repayment-amount): move loan repayment amount function into separate section --- src/loan/terms/simple/loan/PWNSimpleLoan.sol | 89 +++++++++++--------- 1 file changed, 47 insertions(+), 42 deletions(-) diff --git a/src/loan/terms/simple/loan/PWNSimpleLoan.sol b/src/loan/terms/simple/loan/PWNSimpleLoan.sol index 00516a7..d639c27 100644 --- a/src/loan/terms/simple/loan/PWNSimpleLoan.sol +++ b/src/loan/terms/simple/loan/PWNSimpleLoan.sol @@ -517,48 +517,6 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { }); } - /** - * @notice Calculate the loan repayment amount with fixed and accrued interest. - * @param loanId Id of a loan. - * @return Repayment amount. - */ - function loanRepaymentAmount(uint256 loanId) public view returns (uint256) { - LOAN storage loan = LOANs[loanId]; - - // Check non-existent - if (loan.status == 0) return 0; - - return _loanRepaymentAmount(loanId); - } - - /** - * @notice Internal function to calculate the loan repayment amount with fixed and accrued interest. - * @param loanId Id of a loan. - * @return Repayment amount. - */ - function _loanRepaymentAmount(uint256 loanId) private view returns (uint256) { - LOAN storage loan = LOANs[loanId]; - - // Return loan principal with accrued interest - return loan.principalAmount + _loanAccruedInterest(loan); - } - - /** - * @notice Calculate the loan accrued interest. - * @param loan Loan data struct. - * @return Accrued interest amount. - */ - function _loanAccruedInterest(LOAN storage loan) private view returns (uint256) { - if (loan.accruingInterestDailyRate == 0) - return loan.fixedInterestAmount; - - uint256 accruingDays = (block.timestamp - loan.startTimestamp) / 1 days; - uint256 accruedInterest = Math.mulDiv( - loan.principalAmount, loan.accruingInterestDailyRate * accruingDays, DAILY_INTEREST_DENOMINATOR - ); - return loan.fixedInterestAmount + accruedInterest; - } - /** * @notice Check if the loan can be repaid. * @dev The function will revert if the loan cannot be repaid. @@ -661,6 +619,53 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { } + /*----------------------------------------------------------*| + |* # LOAN REPAYMENT AMOUNT *| + |*----------------------------------------------------------*/ + + /** + * @notice Calculate the loan repayment amount with fixed and accrued interest. + * @param loanId Id of a loan. + * @return Repayment amount. + */ + function loanRepaymentAmount(uint256 loanId) public view returns (uint256) { + LOAN storage loan = LOANs[loanId]; + + // Check non-existent + if (loan.status == 0) return 0; + + return _loanRepaymentAmount(loanId); + } + + /** + * @notice Internal function to calculate the loan repayment amount with fixed and accrued interest. + * @param loanId Id of a loan. + * @return Repayment amount. + */ + function _loanRepaymentAmount(uint256 loanId) private view returns (uint256) { + LOAN storage loan = LOANs[loanId]; + + // Return loan principal with accrued interest + return loan.principalAmount + _loanAccruedInterest(loan); + } + + /** + * @notice Calculate the loan accrued interest. + * @param loan Loan data struct. + * @return Accrued interest amount. + */ + function _loanAccruedInterest(LOAN storage loan) private view returns (uint256) { + if (loan.accruingInterestDailyRate == 0) + return loan.fixedInterestAmount; + + uint256 accruingDays = (block.timestamp - loan.startTimestamp) / 1 days; + uint256 accruedInterest = Math.mulDiv( + loan.principalAmount, loan.accruingInterestDailyRate * accruingDays, DAILY_INTEREST_DENOMINATOR + ); + return loan.fixedInterestAmount + accruedInterest; + } + + /*----------------------------------------------------------*| |* # CLAIM LOAN *| |*----------------------------------------------------------*/ From 45575be16ca0c9d5db680900b4bd20935e32fb1e Mon Sep 17 00:00:00 2001 From: ashhanai Date: Thu, 29 Feb 2024 13:55:28 +0100 Subject: [PATCH 026/129] feat(loan-extension): implement loan extension with price --- src/PWNErrors.sol | 6 +- src/loan/terms/simple/loan/PWNSimpleLoan.sol | 203 +++++++++-- test/helper/DeploymentTest.t.sol | 4 +- test/unit/PWNSimpleLoan.t.sol | 343 ++++++++++++++++--- 4 files changed, 477 insertions(+), 79 deletions(-) diff --git a/src/PWNErrors.sol b/src/PWNErrors.sol index 489f740..ea8ba49 100644 --- a/src/PWNErrors.sol +++ b/src/PWNErrors.sol @@ -10,9 +10,13 @@ error LoanDefaulted(uint40); error InvalidLoanStatus(uint256); error NonExistingLoan(); error CallerNotLOANTokenHolder(); -error InvalidExtendedExpirationDate(); error BorrowerMismatch(address currentBorrower, address newBorrower); +// Loan extension +error InvalidExtensionDuration(uint256 duration, uint256 limit); +error InvalidExtensionSigner(address allowed, address current); +error InvalidExtensionCaller(); + // Invalid asset error InvalidLoanAsset(); error InvalidCollateralAsset(); diff --git a/src/loan/terms/simple/loan/PWNSimpleLoan.sol b/src/loan/terms/simple/loan/PWNSimpleLoan.sol index d639c27..244aa07 100644 --- a/src/loan/terms/simple/loan/PWNSimpleLoan.sol +++ b/src/loan/terms/simple/loan/PWNSimpleLoan.sol @@ -10,12 +10,14 @@ import { PWNConfig } from "@pwn/config/PWNConfig.sol"; import { PWNHub } from "@pwn/hub/PWNHub.sol"; import { PWNHubTags } from "@pwn/hub/PWNHubTags.sol"; import { PWNFeeCalculator } from "@pwn/loan/lib/PWNFeeCalculator.sol"; +import { PWNSignatureChecker } from "@pwn/loan/lib/PWNSignatureChecker.sol"; import { PWNLOANTerms } from "@pwn/loan/terms/PWNLOANTerms.sol"; import { PWNSimpleLoanTermsFactory } from "@pwn/loan/terms/simple/factory/PWNSimpleLoanTermsFactory.sol"; import { IERC5646 } from "@pwn/loan/token/IERC5646.sol"; import { IPWNLoanMetadataProvider } from "@pwn/loan/token/IPWNLoanMetadataProvider.sol"; import { PWNLOAN } from "@pwn/loan/token/PWNLOAN.sol"; import { PWNVault } from "@pwn/loan/PWNVault.sol"; +import { PWNRevokedNonce } from "@pwn/nonce/PWNRevokedNonce.sol"; import "@pwn/PWNErrors.sol"; @@ -28,21 +30,35 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { string public constant VERSION = "1.2"; + /*----------------------------------------------------------*| + |* # VARIABLES & CONSTANTS DEFINITIONS *| + |*----------------------------------------------------------*/ + uint256 public constant APR_INTEREST_DENOMINATOR = 1e4; uint256 public constant DAILY_INTEREST_DENOMINATOR = 1e10; uint256 public constant APR_TO_DAILY_INTEREST_NUMERATOR = 274; uint256 public constant APR_TO_DAILY_INTEREST_DENOMINATOR = 1e5; - uint256 public constant MAX_EXPIRATION_EXTENSION = 2_592_000; // 30 days + uint256 public constant MAX_EXTENSION_DURATION = 90 days; + uint256 public constant MIN_EXTENSION_DURATION = 1 days; - /*----------------------------------------------------------*| - |* # VARIABLES & CONSTANTS DEFINITIONS *| - |*----------------------------------------------------------*/ + bytes32 public constant EXTENSION_TYPEHASH = keccak256( + "Extension(uint256 loanId,uint256 price,uint40 duration,uint40 expiration,address proposer,uint256 nonce)" + ); + + bytes32 public immutable DOMAIN_SEPARATOR = keccak256(abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256("PWNSimpleLoan"), + keccak256(abi.encodePacked(VERSION)), + block.chainid, + address(this) + )); PWNHub internal immutable hub; PWNLOAN internal immutable loanToken; PWNConfig internal immutable config; + PWNRevokedNonce internal immutable revokedNonce; IMultiTokenCategoryRegistry public immutable categoryRegistry; @@ -79,6 +95,28 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { */ mapping (uint256 => LOAN) private LOANs; + /** + * @notice Struct defining a loan extension offer. Offer can be signed by a borrower or a lender. + * @param loanId Id of a loan to be extended. + * @param price Price of the extension in loan asset tokens. + * @param duration Duration of the extension in seconds. + * @param expiration Unix timestamp (in seconds) of an expiration date. + * @param proposer Address of a proposer that signed the extension offer. + * @param nonce Nonce of the extension offer. + */ + struct Extension { + uint256 loanId; + uint256 price; + uint40 duration; + uint40 expiration; + address proposer; + uint256 nonce; + } + + /** + * Mapping of extension offers made via on-chain transaction by extension hash. + */ + mapping (bytes32 => bool) public extensionOffersMade; /*----------------------------------------------------------*| |* # EVENTS & ERRORS DEFINITIONS *| @@ -105,19 +143,31 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { event LOANRefinanced(uint256 indexed loanId, uint256 indexed refinancedLoanId); /** - * @dev Emitted when a LOAN token holder extends loan expiration date. + * @dev Emitted when a LOAN token holder extends a loan. */ - event LOANExpirationDateExtended(uint256 indexed loanId, uint40 extendedExpirationDate); + event LOANExtended(uint256 indexed loanId, uint40 originalDefaultTimestamp, uint40 extendedDefaultTimestamp); + + /** + * @dev Emitted when a loan extension offer is made. + */ + event ExtensionOfferMade(bytes32 indexed extensionHash, address indexed proposer, Extension extension); /*----------------------------------------------------------*| |* # CONSTRUCTOR *| |*----------------------------------------------------------*/ - constructor(address _hub, address _loanToken, address _config, address _categoryRegistry) { + constructor( + address _hub, + address _loanToken, + address _config, + address _revokedNonce, + address _categoryRegistry + ) { hub = PWNHub(_hub); loanToken = PWNLOAN(_loanToken); config = PWNConfig(_config); + revokedNonce = PWNRevokedNonce(_revokedNonce); categoryRegistry = IMultiTokenCategoryRegistry(_categoryRegistry); } @@ -731,37 +781,126 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { /*----------------------------------------------------------*| - |* # EXTEND LOAN EXPIRATION DATE *| + |* # EXTEND LOAN *| |*----------------------------------------------------------*/ /** - * @notice Enable lender to extend loans expiration date. - * @dev Only LOAN token holder can call this function. - * Extending the expiration date of a repaid loan is allowed, but considered a lender mistake. - * The extended expiration date has to be in the future, be later than the current expiration date, and cannot be extending the date by more than `MAX_EXPIRATION_EXTENSION`. - * @param loanId Id of a LOAN to extend its expiration date. - * @param extendedExpirationDate New LOAN expiration date. + * @notice Make an extension offer for a loan on-chain. + * @param extension Extension struct. */ - function extendLOANExpirationDate(uint256 loanId, uint40 extendedExpirationDate) external { - // Check that caller is LOAN token holder - // This prevents from extending non-existing loans - if (loanToken.ownerOf(loanId) != msg.sender) - revert CallerNotLOANTokenHolder(); + function makeExtensionOffer(Extension calldata extension) external { + // Check that caller is a proposer + if (msg.sender != extension.proposer) + revert InvalidExtensionSigner({ allowed: extension.proposer, current: msg.sender }); - LOAN storage loan = LOANs[loanId]; + // Mark extension offer as made + bytes32 extensionHash = getExtensionHash(extension); + extensionOffersMade[extensionHash] = true; - // Check extended expiration date - if (extendedExpirationDate > uint40(block.timestamp + MAX_EXPIRATION_EXTENSION)) // to protect lender - revert InvalidExtendedExpirationDate(); - if (extendedExpirationDate <= uint40(block.timestamp)) // have to extend expiration futher in time - revert InvalidExtendedExpirationDate(); - if (extendedExpirationDate <= loan.defaultTimestamp) // have to be later than current expiration date - revert InvalidExtendedExpirationDate(); + emit ExtensionOfferMade(extensionHash, extension.proposer, extension); + } - // Extend expiration date - loan.defaultTimestamp = extendedExpirationDate; + /** + * @notice Extend loans default date with signed extension offer / request from borrower or LOAN token owner. + * @dev The function assumes a prior token approval to a contract address or a signed permit. + * @param extension Extension struct. + * @param signature Signature of the extension offer / request. + * @param loanAssetPermit Permit data for a loan asset signed by a borrower. + */ + function extendLOAN( + Extension calldata extension, + bytes calldata signature, + bytes calldata loanAssetPermit + ) external { + LOAN storage loan = LOANs[extension.loanId]; + + // Check that loan is in the right state + if (loan.status == 0) + revert NonExistingLoan(); + if (loan.status == 3) // cannot extend repaid loan + revert InvalidLoanStatus(loan.status); - emit LOANExpirationDateExtended({ loanId: loanId, extendedExpirationDate: extendedExpirationDate }); + // Check extension validity + bytes32 extensionHash = getExtensionHash(extension); + if (!extensionOffersMade[extensionHash]) + if (!PWNSignatureChecker.isValidSignatureNow(extension.proposer, extensionHash, signature)) + revert InvalidSignature(); + if (extension.expiration != 0 && block.timestamp >= extension.expiration) + revert OfferExpired(); + if (revokedNonce.isNonceRevoked(extension.proposer, extension.nonce)) + revert NonceAlreadyRevoked(); + + // Check caller and signer + address loanOwner = loanToken.ownerOf(extension.loanId); + if (msg.sender == loanOwner) { + if (extension.proposer != loan.borrower) { + // If caller is loan owner, proposer must be borrower + revert InvalidExtensionSigner({ + allowed: loan.borrower, + current: extension.proposer + }); + } + } else if (msg.sender == loan.borrower) { + if (extension.proposer != loanOwner) { + // If caller is borrower, proposer must be loan owner + revert InvalidExtensionSigner({ + allowed: loanOwner, + current: extension.proposer + }); + } + } else { + // Caller must be loan owner or borrower + revert InvalidExtensionCaller(); + } + + // Check duration range + if (extension.duration < MIN_EXTENSION_DURATION) + revert InvalidExtensionDuration({ + duration: extension.duration, + limit: MIN_EXTENSION_DURATION + }); + if (extension.duration > MAX_EXTENSION_DURATION) + revert InvalidExtensionDuration({ + duration: extension.duration, + limit: MAX_EXTENSION_DURATION + }); + + // Revoke extension offer nonce + revokedNonce.revokeNonce(extension.proposer, extension.nonce); + + // Update loan + uint40 originalDefaultTimestamp = loan.defaultTimestamp; + loan.defaultTimestamp = originalDefaultTimestamp + extension.duration; + + // Emit event + emit LOANExtended({ + loanId: extension.loanId, + originalDefaultTimestamp: originalDefaultTimestamp, + extendedDefaultTimestamp: loan.defaultTimestamp + }); + + // Transfer extension price to the loan owner + if (extension.price > 0) { + MultiToken.Asset memory loanAsset = MultiToken.ERC20(loan.loanAssetAddress, extension.price); + _permit(loanAsset, loan.borrower, loanAssetPermit); + _pushFrom(loanAsset, loan.borrower, loanOwner); + } + } + + /** + * @notice Get the hash of the extension struct. + * @param extension Extension struct. + * @return Hash of the extension struct. + */ + function getExtensionHash(Extension calldata extension) public view returns (bytes32) { + return keccak256(abi.encodePacked( + hex"1901", + DOMAIN_SEPARATOR, + keccak256(abi.encodePacked( + EXTENSION_TYPEHASH, + abi.encode(extension) + )) + )); } @@ -774,7 +913,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { * @param loanId Id of a loan in question. * @return status LOAN status. * @return startTimestamp Unix timestamp (in seconds) of a loan creation date. - * @return defaultTimestamp Unix timestamp (in seconds) of a loan expiration date. + * @return defaultTimestamp Unix timestamp (in seconds) of a loan default date. * @return borrower Address of a loan borrower. * @return originalLender Address of a loan original lender. * @return loanOwner Address of a LOAN token holder. @@ -865,7 +1004,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { // The only mutable state properties are: // - status: updated for expired loans based on block.timestamp - // - defaultTimestamp: updated when the loan expiration date is extended + // - defaultTimestamp: updated when the loan is extended // - fixedInterestAmount: updated when the loan is repaid and waiting to be claimed // - accruingInterestDailyRate: updated when the loan is repaid and waiting to be claimed // Others don't have to be part of the state fingerprint as it does not act as a token identification. diff --git a/test/helper/DeploymentTest.t.sol b/test/helper/DeploymentTest.t.sol index 072b23f..bc59937 100644 --- a/test/helper/DeploymentTest.t.sol +++ b/test/helper/DeploymentTest.t.sol @@ -39,7 +39,9 @@ abstract contract DeploymentTest is Deployments, Test { hub = new PWNHub(); loanToken = new PWNLOAN(address(hub)); - simpleLoan = new PWNSimpleLoan(address(hub), address(loanToken), address(config), address(categoryRegistry)); + simpleLoan = new PWNSimpleLoan( // todo: + address(hub), address(loanToken), address(config), address(0), address(categoryRegistry) + ); revokedOfferNonce = new PWNRevokedNonce(address(hub), PWNHubTags.LOAN_OFFER); simpleLoanSimpleOffer = new PWNSimpleLoanSimpleOffer(address(hub), address(revokedOfferNonce)); diff --git a/test/unit/PWNSimpleLoan.t.sol b/test/unit/PWNSimpleLoan.t.sol index c283d00..e61529d 100644 --- a/test/unit/PWNSimpleLoan.t.sol +++ b/test/unit/PWNSimpleLoan.t.sol @@ -19,13 +19,13 @@ import "@pwn-test/helper/token/T721.sol"; abstract contract PWNSimpleLoanTest is Test { bytes32 internal constant LOANS_SLOT = bytes32(uint256(0)); // `LOANs` mapping position - - uint256 public constant MAX_EXPIRATION_EXTENSION = 2_592_000; // 30 days + bytes32 internal constant EXTENSION_OFFERS_MADE_SLOT = bytes32(uint256(1)); // `extensionOffersMade` mapping position PWNSimpleLoan loan; address hub = makeAddr("hub"); address loanToken = makeAddr("loanToken"); address config = makeAddr("config"); + address revokedNonce = makeAddr("revokedNonce"); address categoryRegistry = makeAddr("categoryRegistry"); address feeCollector = makeAddr("feeCollector"); address alice = makeAddr("alice"); @@ -37,6 +37,7 @@ abstract contract PWNSimpleLoanTest is Test { PWNSimpleLoan.LOAN simpleLoan; PWNSimpleLoan.LOAN nonExistingLoan; PWNLOANTerms.Simple simpleLoanTerms; + PWNSimpleLoan.Extension extension; T20 fungibleAsset; T721 nonFungibleAsset; @@ -50,7 +51,8 @@ abstract contract PWNSimpleLoanTest is Test { event LOANPaidBack(uint256 indexed loanId); event LOANClaimed(uint256 indexed loanId, bool indexed defaulted); event LOANRefinanced(uint256 indexed loanId, uint256 indexed refinancedLoanId); - event LOANExpirationDateExtended(uint256 indexed loanId, uint40 extendedExpirationDate); + event LOANExtended(uint256 indexed loanId, uint40 originalDefaultTimestamp, uint40 extendedDefaultTimestamp); + event ExtensionOfferMade(bytes32 indexed extensionHash, address indexed proposer, PWNSimpleLoan.Extension extension); function setUp() virtual public { vm.etch(hub, bytes("data")); @@ -58,7 +60,7 @@ abstract contract PWNSimpleLoanTest is Test { vm.etch(loanFactory, bytes("data")); vm.etch(config, bytes("data")); - loan = new PWNSimpleLoan(hub, loanToken, config, categoryRegistry); + loan = new PWNSimpleLoan(hub, loanToken, config, revokedNonce, categoryRegistry); fungibleAsset = new T20(); nonFungibleAsset = new T721(); @@ -124,6 +126,15 @@ abstract contract PWNSimpleLoanTest is Test { collateral: MultiToken.Asset(MultiToken.Category(0), address(0), 0, 0) }); + extension = PWNSimpleLoan.Extension({ + loanId: loanId, + price: 100, + duration: 2 days, + expiration: simpleLoan.defaultTimestamp, + proposer: borrower, + nonce: 0 + }); + loanFactoryDataHash = keccak256("factoryData"); vm.mockCall( @@ -229,6 +240,11 @@ abstract contract PWNSimpleLoanTest is Test { vm.mockCall(loanToken, abi.encodeWithSignature("ownerOf(uint256)", _loanId), abi.encode(_owner)); } + function _mockExtensionOfferMade(PWNSimpleLoan.Extension memory _extension) internal { + bytes32 extensionOfferSlot = keccak256(abi.encode(_extensionHash(_extension), EXTENSION_OFFERS_MADE_SLOT)); + vm.store(address(loan), extensionOfferSlot, bytes32(uint256(1))); + } + function _assertLOANWord(uint256 wordSlot, bytes memory word) private { assertEq( @@ -247,6 +263,23 @@ abstract contract PWNSimpleLoanTest is Test { } } + function _extensionHash(PWNSimpleLoan.Extension memory _extension) internal view returns (bytes32) { + return keccak256(abi.encodePacked( + "\x19\x01", + keccak256(abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256("PWNSimpleLoan"), + keccak256("1.2"), + block.chainid, + address(loan) + )), + keccak256(abi.encodePacked( + keccak256("Extension(uint256 loanId,uint256 price,uint40 duration,uint40 expiration,address proposer,uint256 nonce)"), + abi.encode(_extension) + )) + )); + } + } @@ -1541,83 +1574,303 @@ contract PWNSimpleLoan_ClaimLOAN_Test is PWNSimpleLoanTest { /*----------------------------------------------------------*| -|* # EXTEND LOAN EXPIRATION DATE *| +|* # MAKE EXTENSION OFFER *| +|*----------------------------------------------------------*/ + +contract PWNSimpleLoan_MakeExtensionOffer_Test is PWNSimpleLoanTest { + + function testFuzz_shouldFail_whenCallerNotProposer(address caller) external { + vm.assume(caller != extension.proposer); + + vm.expectRevert(abi.encodeWithSelector(InvalidExtensionSigner.selector, extension.proposer, caller)); + vm.prank(caller); + loan.makeExtensionOffer(extension); + } + + function test_shouldStoreMadeFlag() external { + vm.prank(extension.proposer); + loan.makeExtensionOffer(extension); + + bytes32 extensionOfferSlot = keccak256(abi.encode(_extensionHash(extension), EXTENSION_OFFERS_MADE_SLOT)); + bytes32 isMadeValue = vm.load(address(loan), extensionOfferSlot); + assertEq(uint256(isMadeValue), 1); + } + + function test_shouldEmit_ExtensionOfferMade() external { + bytes32 extensionHash = _extensionHash(extension); + + vm.expectEmit(); + emit ExtensionOfferMade(extensionHash, extension.proposer, extension); + + vm.prank(extension.proposer); + loan.makeExtensionOffer(extension); + } + +} + + +/*----------------------------------------------------------*| +|* # EXTEND LOAN *| |*----------------------------------------------------------*/ -contract PWNSimpleLoan_ExtendExpirationDate_Test is PWNSimpleLoanTest { +contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { + + uint256 lenderPk; + uint256 borrowerPk; function setUp() override public { super.setUp(); _mockLOAN(loanId, simpleLoan); - // Set current timestamp to 5 days before loan default - vm.warp(simpleLoan.defaultTimestamp - 5 days); + (, lenderPk) = makeAddrAndKey("lender"); + (, borrowerPk) = makeAddrAndKey("borrower"); + + // borrower as proposer, lender accepting extension + extension.proposer = borrower; + + vm.mockCall( + revokedNonce, + abi.encodeWithSignature("isNonceRevoked(address,uint256)"), + abi.encode(false) + ); } - function testFuzz_shouldFail_whenCallerIsNotLOANTokenHolder(address caller) external { - vm.assume(caller != lender); + // Helpers - vm.expectRevert(abi.encodeWithSelector(CallerNotLOANTokenHolder.selector)); + function _signExtension(uint256 pk, PWNSimpleLoan.Extension memory _extension) private view returns (bytes memory) { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(pk, _extensionHash(_extension)); + return abi.encodePacked(r, s, v); + } + + // Tests + + function test_shouldFail_whenLoanDoesNotExist() external { + simpleLoan.status = 0; + _mockLOAN(loanId, simpleLoan); + + vm.expectRevert(abi.encodeWithSelector(NonExistingLoan.selector)); + vm.prank(lender); + loan.extendLOAN(extension, "", ""); + } + + function test_shouldFail_whenLoanIsRepaid() external { + simpleLoan.status = 3; + _mockLOAN(loanId, simpleLoan); + + vm.expectRevert(abi.encodeWithSelector(InvalidLoanStatus.selector, 3)); + vm.prank(lender); + loan.extendLOAN(extension, "", ""); + } + + function testFuzz_shouldFail_whenInvalidSignature_whenEOA(uint256 pk) external { + pk = boundPrivateKey(pk); + vm.assume(pk != borrowerPk); + + signature = _signExtension(pk, extension); + + vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector)); + vm.prank(lender); + loan.extendLOAN(extension, signature, ""); + } + + function testFuzz_shouldFail_whenOfferExpirated(uint256 timestamp) external { + timestamp = bound(timestamp, extension.expiration, type(uint256).max); + _mockExtensionOfferMade(extension); + + vm.warp(timestamp); + + vm.expectRevert(abi.encodeWithSelector(OfferExpired.selector)); + vm.prank(lender); + loan.extendLOAN(extension, "", ""); + } + + function test_shouldFail_whenOfferNonceRevoked() external { + _mockExtensionOfferMade(extension); + + vm.mockCall( + revokedNonce, + abi.encodeWithSignature("isNonceRevoked(address,uint256)", extension.proposer, extension.nonce), + abi.encode(true) + ); + + vm.expectRevert(abi.encodeWithSelector(NonceAlreadyRevoked.selector)); + vm.prank(lender); + loan.extendLOAN(extension, "", ""); + } + + function testFuzz_shouldFail_whenCallerIsNotBorrowerNorLoanOwner(address caller) external { + vm.assume(caller != borrower && caller != lender); + _mockExtensionOfferMade(extension); + + vm.expectRevert(abi.encodeWithSelector(InvalidExtensionCaller.selector)); vm.prank(caller); - loan.extendLOANExpirationDate(loanId, simpleLoan.defaultTimestamp + 1); + loan.extendLOAN(extension, "", ""); } - function testFuzz_shouldFail_whenExtendedExpirationDateIsSmallerThanOrEqualToCurrentExpirationDate( - uint40 newExpiration - ) external { - newExpiration = uint40(bound(newExpiration, block.timestamp + 1, simpleLoan.defaultTimestamp)); + function testFuzz_shouldFail_whenCallerIsBorrower_andProposerIsNotLoanOwner(address proposer) external { + vm.assume(proposer != lender); + + extension.proposer = proposer; + _mockExtensionOfferMade(extension); - vm.expectRevert(abi.encodeWithSelector(InvalidExtendedExpirationDate.selector)); + vm.expectRevert(abi.encodeWithSelector(InvalidExtensionSigner.selector, lender, proposer)); + vm.prank(borrower); + loan.extendLOAN(extension, "", ""); + } + + function testFuzz_shouldFail_whenCallerIsLoanOwner_andProposerIsNotBorrower(address proposer) external { + vm.assume(proposer != borrower); + + extension.proposer = proposer; + _mockExtensionOfferMade(extension); + + vm.expectRevert(abi.encodeWithSelector(InvalidExtensionSigner.selector, borrower, proposer)); vm.prank(lender); - loan.extendLOANExpirationDate(loanId, newExpiration); + loan.extendLOAN(extension, "", ""); } - function testFuzz_shouldFail_whenExtendedExpirationDateIsSmallerThanOrEqualToCurrentDate(uint40 newExpiration) external { - newExpiration = uint40(bound(newExpiration, 0, block.timestamp)); + function testFuzz_shouldFail_whenExtensionDurationLessThanMin(uint40 duration) external { + uint256 minDuration = loan.MIN_EXTENSION_DURATION(); + duration = uint40(bound(duration, 0, minDuration - 1)); + + extension.duration = duration; + _mockExtensionOfferMade(extension); - vm.expectRevert(abi.encodeWithSelector(InvalidExtendedExpirationDate.selector)); + vm.expectRevert(abi.encodeWithSelector(InvalidExtensionDuration.selector, duration, minDuration)); vm.prank(lender); - loan.extendLOANExpirationDate(loanId, newExpiration); + loan.extendLOAN(extension, "", ""); } - function testFuzz_shouldFail_whenExtendedExpirationDateIsBiggerThanMaxExpirationExtension( - uint40 newExpiration - ) external { - newExpiration = uint40(bound( - newExpiration, block.timestamp + MAX_EXPIRATION_EXTENSION + 1, type(uint40).max - )); + function testFuzz_shouldFail_whenExtensionDurationMoreThanMax(uint40 duration) external { + uint256 maxDuration = loan.MAX_EXTENSION_DURATION(); + duration = uint40(bound(duration, maxDuration + 1, type(uint40).max)); + + extension.duration = duration; + _mockExtensionOfferMade(extension); - vm.expectRevert(abi.encodeWithSelector(InvalidExtendedExpirationDate.selector)); + vm.expectRevert(abi.encodeWithSelector(InvalidExtensionDuration.selector, duration, maxDuration)); vm.prank(lender); - loan.extendLOANExpirationDate(loanId, newExpiration); + loan.extendLOAN(extension, "", ""); } - function testFuzz_shouldStoreExtendedExpirationDate(uint40 newExpiration) external { - newExpiration = uint40(bound( - newExpiration, simpleLoan.defaultTimestamp + 1, block.timestamp + MAX_EXPIRATION_EXTENSION - )); + function testFuzz_shouldRevokeExtensionNonce(uint256 nonce) external { + extension.nonce = nonce; + _mockExtensionOfferMade(extension); + + vm.expectCall( + revokedNonce, + abi.encodeWithSignature("revokeNonce(address,uint256)", extension.proposer, nonce) + ); + + vm.prank(lender); + loan.extendLOAN(extension, "", ""); + } + + function testFuzz_shouldUpdateLoanData(uint40 duration) external { + duration = uint40(bound(duration, loan.MIN_EXTENSION_DURATION(), loan.MAX_EXTENSION_DURATION())); + + extension.duration = duration; + _mockExtensionOfferMade(extension); vm.prank(lender); - loan.extendLOANExpirationDate(loanId, newExpiration); + loan.extendLOAN(extension, "", ""); - bytes32 loanFirstSlot = keccak256(abi.encode(loanId, LOANS_SLOT)); - bytes32 firstSlotValue = vm.load(address(loan), loanFirstSlot); - bytes32 expirationDateValue = firstSlotValue << 8 >> 216; - assertEq(uint256(expirationDateValue), newExpiration); + simpleLoan.defaultTimestamp = simpleLoan.defaultTimestamp + duration; + _assertLOANEq(loanId, simpleLoan); } - function testFuzz_shouldEmitEvent_LOANExpirationDateExtended(uint40 newExpiration) external { - newExpiration = uint40(bound( - newExpiration, simpleLoan.defaultTimestamp + 1, block.timestamp + MAX_EXPIRATION_EXTENSION - )); + function testFuzz_shouldEmit_LOANExtended(uint40 duration) external { + duration = uint40(bound(duration, loan.MIN_EXTENSION_DURATION(), loan.MAX_EXTENSION_DURATION())); + + extension.duration = duration; + _mockExtensionOfferMade(extension); vm.expectEmit(); - emit LOANExpirationDateExtended(loanId, newExpiration); + emit LOANExtended(loanId, simpleLoan.defaultTimestamp, simpleLoan.defaultTimestamp + duration); + + vm.prank(lender); + loan.extendLOAN(extension, "", ""); + } + + function testFuzz_shouldTransferLoanAsset_whenPriceMoreThanZero(uint256 price) external { + price = bound(price, 1, 1e40); + + extension.price = price; + _mockExtensionOfferMade(extension); + fungibleAsset.mint(borrower, price); + + vm.expectCall( + simpleLoan.loanAssetAddress, + abi.encodeWithSignature("transferFrom(address,address,uint256)", borrower, lender, price) + ); vm.prank(lender); - loan.extendLOANExpirationDate(loanId, newExpiration); + loan.extendLOAN(extension, "", ""); + } + + function test_shouldNotTransferLoanAsset_whenPriceZero() external { + extension.price = 0; + _mockExtensionOfferMade(extension); + + vm.expectCall({ + callee: simpleLoan.loanAssetAddress, + data: abi.encodeWithSignature("transferFrom(address,address,uint256)", borrower, lender, 0), + count: 0 + }); + + vm.prank(lender); + loan.extendLOAN(extension, "", ""); + } + + function testFuzz_shouldCallPermit_whenPriceMoreThanZero_whenPermitData(uint256 price) external { + price = bound(price, 1, 1e40); + + extension.price = price; + _mockExtensionOfferMade(extension); + fungibleAsset.mint(borrower, price); + loanAssetPermit = abi.encodePacked(uint256(1), uint256(2), uint256(3), uint8(4)); + + vm.expectCall( + simpleLoan.loanAssetAddress, + abi.encodeWithSignature( + "permit(address,address,uint256,uint256,uint8,bytes32,bytes32)", + borrower, address(loan), price, 1, uint8(4), uint256(2), uint256(3) + ) + ); + + vm.prank(lender); + loan.extendLOAN(extension, "", loanAssetPermit); + } + + function test_shouldPass_whenBorrowerSignature_whenLenderAccepts() external { + extension.proposer = borrower; + signature = _signExtension(borrowerPk, extension); + + vm.prank(lender); + loan.extendLOAN(extension, signature, ""); + } + + function test_shouldPass_whenLenderSignature_whenBorrowerAccepts() external { + extension.proposer = lender; + signature = _signExtension(lenderPk, extension); + + vm.prank(borrower); + loan.extendLOAN(extension, signature, ""); + } + +} + + +/*----------------------------------------------------------*| +|* # GET EXTENSION HASH *| +|*----------------------------------------------------------*/ + +contract PWNSimpleLoan_GetExtensionHash_Test is PWNSimpleLoanTest { + + function test_shouldHaveCorrectDomainSeparator() external { + assertEq(_extensionHash(extension), loan.getExtensionHash(extension)); } } From 34ad5468b48654efc30730eec9621fa90b50bc58 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Thu, 29 Feb 2024 16:00:37 +0100 Subject: [PATCH 027/129] refactor: make simple loan properties public --- src/loan/terms/simple/loan/PWNSimpleLoan.sol | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/loan/terms/simple/loan/PWNSimpleLoan.sol b/src/loan/terms/simple/loan/PWNSimpleLoan.sol index 244aa07..61f2ed9 100644 --- a/src/loan/terms/simple/loan/PWNSimpleLoan.sol +++ b/src/loan/terms/simple/loan/PWNSimpleLoan.sol @@ -55,11 +55,10 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { address(this) )); - PWNHub internal immutable hub; - PWNLOAN internal immutable loanToken; - PWNConfig internal immutable config; - PWNRevokedNonce internal immutable revokedNonce; - + PWNHub public immutable hub; + PWNLOAN public immutable loanToken; + PWNConfig public immutable config; + PWNRevokedNonce public immutable revokedNonce; IMultiTokenCategoryRegistry public immutable categoryRegistry; /** From ed387f453777791bad0d61508d529f337d71854d Mon Sep 17 00:00:00 2001 From: ashhanai Date: Thu, 29 Feb 2024 17:26:11 +0100 Subject: [PATCH 028/129] feat(no-expiration): remove infinite expirations in offers and requests --- .../simple/factory/offer/PWNSimpleLoanListOffer.sol | 2 +- .../simple/factory/offer/PWNSimpleLoanSimpleOffer.sol | 2 +- .../factory/request/PWNSimpleLoanSimpleRequest.sol | 2 +- src/loan/terms/simple/loan/PWNSimpleLoan.sol | 2 +- test/unit/PWNSimpleLoan.t.sol | 9 +++++---- test/unit/PWNSimpleLoanListOffer.t.sol | 11 +---------- test/unit/PWNSimpleLoanSimpleOffer.t.sol | 11 +---------- test/unit/PWNSimpleLoanSimpleRequest.t.sol | 11 +---------- 8 files changed, 12 insertions(+), 38 deletions(-) diff --git a/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol b/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol index 1be775d..32a46ef 100644 --- a/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol +++ b/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol @@ -147,7 +147,7 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanOffer { revert InvalidSignature(); // Check valid offer - if (offer.expiration != 0 && block.timestamp >= offer.expiration) + if (block.timestamp >= offer.expiration) revert OfferExpired(); if (revokedOfferNonce.isNonceRevoked(lender, offer.nonce) == true) diff --git a/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol b/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol index d64f679..a26974c 100644 --- a/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol +++ b/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol @@ -132,7 +132,7 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanOffer { revert InvalidSignature(); // Check valid offer - if (offer.expiration != 0 && block.timestamp >= offer.expiration) + if (block.timestamp >= offer.expiration) revert OfferExpired(); if (revokedOfferNonce.isNonceRevoked(lender, offer.nonce) == true) diff --git a/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol b/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol index 04ad13a..32959af 100644 --- a/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol +++ b/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol @@ -132,7 +132,7 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanRequest { revert InvalidSignature(); // Check valid request - if (request.expiration != 0 && block.timestamp >= request.expiration) + if (block.timestamp >= request.expiration) revert RequestExpired(); if (revokedRequestNonce.isNonceRevoked(borrower, request.nonce) == true) diff --git a/src/loan/terms/simple/loan/PWNSimpleLoan.sol b/src/loan/terms/simple/loan/PWNSimpleLoan.sol index 61f2ed9..025c0d7 100644 --- a/src/loan/terms/simple/loan/PWNSimpleLoan.sol +++ b/src/loan/terms/simple/loan/PWNSimpleLoan.sol @@ -824,7 +824,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { if (!extensionOffersMade[extensionHash]) if (!PWNSignatureChecker.isValidSignatureNow(extension.proposer, extensionHash, signature)) revert InvalidSignature(); - if (extension.expiration != 0 && block.timestamp >= extension.expiration) + if (block.timestamp >= extension.expiration) revert OfferExpired(); if (revokedNonce.isNonceRevoked(extension.proposer, extension.nonce)) revert NonceAlreadyRevoked(); diff --git a/test/unit/PWNSimpleLoan.t.sol b/test/unit/PWNSimpleLoan.t.sol index e61529d..22a3648 100644 --- a/test/unit/PWNSimpleLoan.t.sol +++ b/test/unit/PWNSimpleLoan.t.sol @@ -1675,12 +1675,13 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { loan.extendLOAN(extension, signature, ""); } - function testFuzz_shouldFail_whenOfferExpirated(uint256 timestamp) external { - timestamp = bound(timestamp, extension.expiration, type(uint256).max); - _mockExtensionOfferMade(extension); - + function testFuzz_shouldFail_whenOfferExpirated(uint40 expiration) external { + uint256 timestamp = 300; vm.warp(timestamp); + extension.expiration = uint40(bound(expiration, 0, timestamp)); + _mockExtensionOfferMade(extension); + vm.expectRevert(abi.encodeWithSelector(OfferExpired.selector)); vm.prank(lender); loan.extendLOAN(extension, "", ""); diff --git a/test/unit/PWNSimpleLoanListOffer.t.sol b/test/unit/PWNSimpleLoanListOffer.t.sol index 68141a4..7088a8b 100644 --- a/test/unit/PWNSimpleLoanListOffer.t.sol +++ b/test/unit/PWNSimpleLoanListOffer.t.sol @@ -44,7 +44,7 @@ abstract contract PWNSimpleLoanListOfferTest is Test { fixedInterestAmount: 1, accruingInterestAPR: 0, duration: 1000, - expiration: 0, + expiration: 60303, allowedBorrower: address(0), lender: lender, isPersistent: false, @@ -231,15 +231,6 @@ contract PWNSimpleLoanListOffer_CreateLOANTerms_Test is PWNSimpleLoanListOfferTe offerContract.createLOANTerms(borrower, abi.encode(offer, offerValues), signature); } - function test_shouldPass_whenOfferHasNoExpiration() external { - vm.warp(40303); - offer.expiration = 0; - signature = _signOfferCompact(lenderPK, offer); - - vm.prank(activeLoanContract); - offerContract.createLOANTerms(borrower, abi.encode(offer, offerValues), signature); - } - function test_shouldPass_whenOfferIsNotExpired() external { vm.warp(40303); offer.expiration = 50303; diff --git a/test/unit/PWNSimpleLoanSimpleOffer.t.sol b/test/unit/PWNSimpleLoanSimpleOffer.t.sol index 5a377de..b92c3ec 100644 --- a/test/unit/PWNSimpleLoanSimpleOffer.t.sol +++ b/test/unit/PWNSimpleLoanSimpleOffer.t.sol @@ -43,7 +43,7 @@ abstract contract PWNSimpleLoanSimpleOfferTest is Test { fixedInterestAmount: 1, accruingInterestAPR: 0, duration: 1000, - expiration: 0, + expiration: 60303, allowedBorrower: address(0), lender: lender, isPersistent: false, @@ -225,15 +225,6 @@ contract PWNSimpleLoanSimpleOffer_CreateLOANTerms_Test is PWNSimpleLoanSimpleOff offerContract.createLOANTerms(borrower, abi.encode(offer), signature); } - function test_shouldPass_whenOfferHasNoExpiration() external { - vm.warp(40303); - offer.expiration = 0; - signature = _signOfferCompact(lenderPK, offer); - - vm.prank(activeLoanContract); - offerContract.createLOANTerms(borrower, abi.encode(offer), signature); - } - function test_shouldPass_whenOfferIsNotExpired() external { vm.warp(40303); offer.expiration = 50303; diff --git a/test/unit/PWNSimpleLoanSimpleRequest.t.sol b/test/unit/PWNSimpleLoanSimpleRequest.t.sol index 591d701..767dc50 100644 --- a/test/unit/PWNSimpleLoanSimpleRequest.t.sol +++ b/test/unit/PWNSimpleLoanSimpleRequest.t.sol @@ -43,7 +43,7 @@ abstract contract PWNSimpleLoanSimpleRequestTest is Test { fixedInterestAmount: 1, accruingInterestAPR: 0, duration: 1000, - expiration: 0, + expiration: 60303, allowedLender: address(0), borrower: borrower, refinancingLoanId: 0, @@ -225,15 +225,6 @@ contract PWNSimpleLoanSimpleRequest_CreateLOANTerms_Test is PWNSimpleLoanSimpleR requestContract.createLOANTerms(lender, abi.encode(request), signature); } - function test_shouldPass_whenRequestHasNoExpiration() external { - vm.warp(40303); - request.expiration = 0; - signature = _signRequestCompact(borrowerPK, request); - - vm.prank(activeLoanContract); - requestContract.createLOANTerms(lender, abi.encode(request), signature); - } - function test_shouldPass_whenRequestIsNotExpired() external { vm.warp(40303); request.expiration = 50303; From 1d9f1541a329791a3d2e671c24087d5513b94614 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Thu, 29 Feb 2024 19:03:47 +0100 Subject: [PATCH 029/129] feat(create-and-revoke): implement option to revoke callers nonce while creating a loan --- src/loan/terms/simple/loan/PWNSimpleLoan.sol | 35 ++++++++++++- test/unit/PWNSimpleLoan.t.sol | 52 +++++++++++++++++--- 2 files changed, 79 insertions(+), 8 deletions(-) diff --git a/src/loan/terms/simple/loan/PWNSimpleLoan.sol b/src/loan/terms/simple/loan/PWNSimpleLoan.sol index 025c0d7..e48e1b9 100644 --- a/src/loan/terms/simple/loan/PWNSimpleLoan.sol +++ b/src/loan/terms/simple/loan/PWNSimpleLoan.sol @@ -191,7 +191,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { bytes calldata signature, bytes calldata loanAssetPermit, bytes calldata collateralPermit - ) external returns (uint256 loanId) { + ) public returns (uint256 loanId) { // Create loan terms or revert if factory contract is not tagged in PWN Hub (PWNLOANTerms.Simple memory loanTerms, bytes32 factoryDataHash) = _createLoanTerms(loanTermsFactoryContract, loanTermsFactoryData, signature); @@ -206,6 +206,39 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { _settleNewLoan(loanTerms, loanAssetPermit, collateralPermit); } + /** + * @notice Create a new loan by minting LOAN token for lender, transferring loan asset to a borrower and a collateral to a vault. + Revoke a nonce on behalf of the caller. + * @dev The function assumes a prior token approval to a contract address or signed permits. + * @param loanTermsFactoryContract Address of a loan terms factory contract. Need to have `SIMPLE_LOAN_TERMS_FACTORY` tag in PWN Hub. + * @param loanTermsFactoryData Encoded data for a loan terms factory. + * @param signature Signed loan factory data. Could be empty if an offer / request has been made via on-chain transaction. + * @param loanAssetPermit Permit data for a loan asset signed by a lender. + * @param collateralPermit Permit data for a collateral signed by a borrower. + * @param callersNonceToRevoke Nonce to revoke on callers behalf. + * @return loanId Id of a newly minted LOAN token. + */ + function createLOANAndRevokeNonce( + address loanTermsFactoryContract, + bytes calldata loanTermsFactoryData, + bytes calldata signature, + bytes calldata loanAssetPermit, + bytes calldata collateralPermit, + uint256 callersNonceToRevoke + ) external returns (uint256 loanId) { + if (revokedNonce.isNonceRevoked(msg.sender, callersNonceToRevoke)) + revert NonceAlreadyRevoked(); + + revokedNonce.revokeNonce(msg.sender, callersNonceToRevoke); + loanId = createLOAN({ + loanTermsFactoryContract: loanTermsFactoryContract, + loanTermsFactoryData: loanTermsFactoryData, + signature: signature, + loanAssetPermit: loanAssetPermit, + collateralPermit: collateralPermit + }); + } + /** * @notice Create a loan terms by a loan terms factory contract. * @dev The function will revert if the loan terms factory contract is not tagged in PWN Hub. diff --git a/test/unit/PWNSimpleLoan.t.sol b/test/unit/PWNSimpleLoan.t.sol index 22a3648..70ba899 100644 --- a/test/unit/PWNSimpleLoan.t.sol +++ b/test/unit/PWNSimpleLoan.t.sol @@ -162,6 +162,10 @@ abstract contract PWNSimpleLoanTest is Test { _mockLoanTerms(simpleLoanTerms, loanFactoryDataHash); _mockLOANMint(loanId); _mockLOANTokenOwner(loanId, lender); + + vm.mockCall( + revokedNonce, abi.encodeWithSignature("isNonceRevoked(address,uint256)"), abi.encode(false) + ); } @@ -287,7 +291,7 @@ abstract contract PWNSimpleLoanTest is Test { |* # CREATE LOAN *| |*----------------------------------------------------------*/ -contract PWNSimpleLoan_CreateLoan_Test is PWNSimpleLoanTest { +contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { function testFuzz_shouldFail_whenLoanFactoryContractIsNotTaggerInPWNHub(address notLoanFactory) external { vm.assume(notLoanFactory != loanFactory); @@ -436,6 +440,46 @@ contract PWNSimpleLoan_CreateLoan_Test is PWNSimpleLoanTest { } +/*----------------------------------------------------------*| +|* # CREATE LOAN AND REVOKE NONCE *| +|*----------------------------------------------------------*/ + +contract PWNSimpleLoan_CreateLOANAndRevokeNonce_Test is PWNSimpleLoanTest { + + function testFuzz_shouldFail_whenNonceAlreadyRevoked(uint256 nonce) external { + vm.mockCall( + revokedNonce, + abi.encodeWithSignature("isNonceRevoked(address,uint256)", borrower, nonce), + abi.encode(true) + ); + + vm.expectRevert(abi.encodeWithSelector(NonceAlreadyRevoked.selector)); + vm.prank(borrower); + loan.createLOANAndRevokeNonce(loanFactory, loanFactoryData, "", "", "", nonce); + } + + function testFuzz_shouldRevokeCallersNonce(address caller, uint256 nonce) external { + assumeAddressIsNot(caller, AddressType.ZeroAddress); + + vm.expectCall( + revokedNonce, + abi.encodeWithSignature("revokeNonce(address,uint256)", caller, nonce) + ); + + vm.prank(caller); + loan.createLOANAndRevokeNonce(loanFactory, loanFactoryData, "", "", "", nonce); + } + + function test_shouldCreateLoan() external { + uint256 _loanId = loan.createLOANAndRevokeNonce(loanFactory, loanFactoryData, "", "", "", 1); + + assertEq(_loanId, loanId); + _assertLOANEq(_loanId, simpleLoan); + } + +} + + /*----------------------------------------------------------*| |* # REFINANCE LOAN *| |*----------------------------------------------------------*/ @@ -1628,12 +1672,6 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { // borrower as proposer, lender accepting extension extension.proposer = borrower; - - vm.mockCall( - revokedNonce, - abi.encodeWithSignature("isNonceRevoked(address,uint256)"), - abi.encode(false) - ); } From aeb4c2189e059f160b51253e801584d39b8b7329 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Fri, 1 Mar 2024 13:00:02 +0100 Subject: [PATCH 030/129] feat(nonce-space): implement nonce space and use them in offers and requests Nonce space is incremental and can revoke all nonces that are in the same space at once. --- .../factory/offer/PWNSimpleLoanListOffer.sol | 24 +-- .../offer/PWNSimpleLoanSimpleOffer.sol | 22 +-- .../factory/offer/base/PWNSimpleLoanOffer.sol | 13 +- .../request/PWNSimpleLoanSimpleRequest.sol | 22 +-- .../request/base/PWNSimpleLoanRequest.sol | 11 +- src/loan/terms/simple/loan/PWNSimpleLoan.sol | 13 +- src/nonce/PWNRevokedNonce.sol | 107 ++++++------ test/unit/PWNRevokedNonce.t.sol | 159 +++++++++--------- test/unit/PWNSimpleLoan.t.sol | 28 +-- test/unit/PWNSimpleLoanListOffer.t.sol | 13 +- test/unit/PWNSimpleLoanOffer.t.sol | 10 +- test/unit/PWNSimpleLoanRequest.t.sol | 10 +- test/unit/PWNSimpleLoanSimpleOffer.t.sol | 13 +- test/unit/PWNSimpleLoanSimpleRequest.t.sol | 11 +- 14 files changed, 237 insertions(+), 219 deletions(-) diff --git a/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol b/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol index 32a46ef..9a4ae7f 100644 --- a/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol +++ b/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol @@ -1,13 +1,13 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; -import "MultiToken/MultiToken.sol"; +import { MultiToken } from "MultiToken/MultiToken.sol"; -import "openzeppelin-contracts/contracts/utils/cryptography/MerkleProof.sol"; +import { MerkleProof } from "openzeppelin-contracts/contracts/utils/cryptography/MerkleProof.sol"; -import "@pwn/loan/lib/PWNSignatureChecker.sol"; -import "@pwn/loan/terms/simple/factory/offer/base/PWNSimpleLoanOffer.sol"; -import "@pwn/loan/terms/PWNLOANTerms.sol"; +import { PWNSignatureChecker } from "@pwn/loan/lib/PWNSignatureChecker.sol"; +import { PWNSimpleLoanOffer, PWNSimpleLoanTermsFactory } from "@pwn/loan/terms/simple/factory/offer/base/PWNSimpleLoanOffer.sol"; +import { PWNLOANTerms } from "@pwn/loan/terms/PWNLOANTerms.sol"; import "@pwn/PWNErrors.sol"; @@ -28,7 +28,7 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanOffer { * @dev EIP-712 simple offer struct type hash. */ bytes32 public constant OFFER_TYPEHASH = keccak256( - "Offer(uint8 collateralCategory,address collateralAddress,bytes32 collateralIdsWhitelistMerkleRoot,uint256 collateralAmount,address loanAssetAddress,uint256 loanAmount,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedBorrower,address lender,bool isPersistent,uint256 nonce)" + "Offer(uint8 collateralCategory,address collateralAddress,bytes32 collateralIdsWhitelistMerkleRoot,uint256 collateralAmount,address loanAssetAddress,uint256 loanAmount,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedBorrower,address lender,bool isPersistent,uint256 nonceSpace,uint256 nonce)" ); bytes32 public immutable DOMAIN_SEPARATOR; @@ -48,6 +48,7 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanOffer { * @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 isPersistent If true, offer will not be revoked on acceptance. Persistent offer can be revoked manually. + * @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. */ @@ -65,6 +66,7 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanOffer { address allowedBorrower; address lender; bool isPersistent; + uint256 nonceSpace; uint256 nonce; } @@ -123,7 +125,7 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanOffer { /*----------------------------------------------------------*| - |* # IPWNSimpleLoanFactory *| + |* # PWNSimpleLoanTermsFactory *| |*----------------------------------------------------------*/ /** @@ -142,15 +144,15 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanOffer { address borrower = caller; // Check that offer has been made via on-chain tx, EIP-1271 or signed off-chain - if (offersMade[offerHash] == false) - if (PWNSignatureChecker.isValidSignatureNow(lender, offerHash, signature) == false) + if (!offersMade[offerHash]) + if (!PWNSignatureChecker.isValidSignatureNow(lender, offerHash, signature)) revert InvalidSignature(); // Check valid offer if (block.timestamp >= offer.expiration) revert OfferExpired(); - if (revokedOfferNonce.isNonceRevoked(lender, offer.nonce) == true) + if (revokedOfferNonce.isNonceRevoked(lender, offer.nonceSpace, offer.nonce)) revert NonceAlreadyRevoked(); if (offer.allowedBorrower != address(0)) @@ -209,7 +211,7 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanOffer { // Revoke offer if not persistent if (!offer.isPersistent) - revokedOfferNonce.revokeNonce(lender, offer.nonce); + revokedOfferNonce.revokeNonce(lender, offer.nonceSpace, offer.nonce); } diff --git a/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol b/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol index a26974c..adf2ee7 100644 --- a/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol +++ b/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; -import "MultiToken/MultiToken.sol"; +import { MultiToken } from "MultiToken/MultiToken.sol"; -import "@pwn/loan/lib/PWNSignatureChecker.sol"; -import "@pwn/loan/terms/simple/factory/offer/base/PWNSimpleLoanOffer.sol"; -import "@pwn/loan/terms/PWNLOANTerms.sol"; +import { PWNSignatureChecker } from "@pwn/loan/lib/PWNSignatureChecker.sol"; +import { PWNSimpleLoanOffer, PWNSimpleLoanTermsFactory } from "@pwn/loan/terms/simple/factory/offer/base/PWNSimpleLoanOffer.sol"; +import { PWNLOANTerms } from "@pwn/loan/terms/PWNLOANTerms.sol"; import "@pwn/PWNErrors.sol"; @@ -25,7 +25,7 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanOffer { * @dev EIP-712 simple offer struct type hash. */ bytes32 public constant OFFER_TYPEHASH = keccak256( - "Offer(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,address loanAssetAddress,uint256 loanAmount,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedBorrower,address lender,bool isPersistent,uint256 nonce)" + "Offer(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,address loanAssetAddress,uint256 loanAmount,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedBorrower,address lender,bool isPersistent,uint256 nonceSpace,uint256 nonce)" ); bytes32 public immutable DOMAIN_SEPARATOR; @@ -45,6 +45,7 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanOffer { * @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 isPersistent If true, offer will not be revoked on acceptance. Persistent offer can be revoked manually. + * @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. */ @@ -62,6 +63,7 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanOffer { address allowedBorrower; address lender; bool isPersistent; + uint256 nonceSpace; uint256 nonce; } @@ -108,7 +110,7 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanOffer { /*----------------------------------------------------------*| - |* # IPWNSimpleLoanFactory *| + |* # PWNSimpleLoanTermsFactory *| |*----------------------------------------------------------*/ /** @@ -127,15 +129,15 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanOffer { address borrower = caller; // Check that offer has been made via on-chain tx, EIP-1271 or signed off-chain - if (offersMade[offerHash] == false) - if (PWNSignatureChecker.isValidSignatureNow(lender, offerHash, signature) == false) + if (!offersMade[offerHash]) + if (!PWNSignatureChecker.isValidSignatureNow(lender, offerHash, signature)) revert InvalidSignature(); // Check valid offer if (block.timestamp >= offer.expiration) revert OfferExpired(); - if (revokedOfferNonce.isNonceRevoked(lender, offer.nonce) == true) + if (revokedOfferNonce.isNonceRevoked(lender, offer.nonceSpace, offer.nonce)) revert NonceAlreadyRevoked(); if (offer.allowedBorrower != address(0)) @@ -182,7 +184,7 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanOffer { // Revoke offer if not persistent if (!offer.isPersistent) - revokedOfferNonce.revokeNonce(lender, offer.nonce); + revokedOfferNonce.revokeNonce(lender, offer.nonceSpace, offer.nonce); } diff --git a/src/loan/terms/simple/factory/offer/base/PWNSimpleLoanOffer.sol b/src/loan/terms/simple/factory/offer/base/PWNSimpleLoanOffer.sol index acb17f2..bf22c8c 100644 --- a/src/loan/terms/simple/factory/offer/base/PWNSimpleLoanOffer.sol +++ b/src/loan/terms/simple/factory/offer/base/PWNSimpleLoanOffer.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; -import "@pwn/hub/PWNHubAccessControl.sol"; -import "@pwn/loan/terms/simple/factory/PWNSimpleLoanTermsFactory.sol"; -import "@pwn/nonce/PWNRevokedNonce.sol"; +import { PWNHubAccessControl } from "@pwn/hub/PWNHubAccessControl.sol"; +import { PWNSimpleLoanTermsFactory } from "@pwn/loan/terms/simple/factory/PWNSimpleLoanTermsFactory.sol"; +import { PWNRevokedNonce } from "@pwn/nonce/PWNRevokedNonce.sol"; import "@pwn/PWNErrors.sol"; @@ -13,7 +13,7 @@ abstract contract PWNSimpleLoanOffer is PWNSimpleLoanTermsFactory, PWNHubAccessC |* # VARIABLES & CONSTANTS DEFINITIONS *| |*----------------------------------------------------------*/ - PWNRevokedNonce internal immutable revokedOfferNonce; + PWNRevokedNonce public immutable revokedOfferNonce; /** * @dev Mapping of offers made via on-chain transactions. @@ -53,10 +53,11 @@ abstract contract PWNSimpleLoanOffer is PWNSimpleLoanTermsFactory, PWNHubAccessC /** * @notice Helper function for revoking an offer nonce on behalf of a caller. + * @param offerNonceSpace Nonce space of an offer nonce to be revoked. * @param offerNonce Offer nonce to be revoked. */ - function revokeOfferNonce(uint256 offerNonce) external { - revokedOfferNonce.revokeNonce(msg.sender, offerNonce); + function revokeOfferNonce(uint256 offerNonceSpace, uint256 offerNonce) external { + revokedOfferNonce.revokeNonce(msg.sender, offerNonceSpace, offerNonce); } } diff --git a/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol b/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol index 32959af..1846e80 100644 --- a/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol +++ b/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; -import "MultiToken/MultiToken.sol"; +import { MultiToken } from "MultiToken/MultiToken.sol"; -import "@pwn/loan/lib/PWNSignatureChecker.sol"; -import "@pwn/loan/terms/simple/factory/request/base/PWNSimpleLoanRequest.sol"; -import "@pwn/loan/terms/PWNLOANTerms.sol"; +import { PWNSignatureChecker } from "@pwn/loan/lib/PWNSignatureChecker.sol"; +import { PWNSimpleLoanRequest, PWNSimpleLoanTermsFactory } from "@pwn/loan/terms/simple/factory/request/base/PWNSimpleLoanRequest.sol"; +import { PWNLOANTerms } from "@pwn/loan/terms/PWNLOANTerms.sol"; import "@pwn/PWNErrors.sol"; @@ -25,7 +25,7 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanRequest { * @dev EIP-712 simple request struct type hash. */ bytes32 public constant REQUEST_TYPEHASH = keccak256( - "Request(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,address loanAssetAddress,uint256 loanAmount,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedLender,address borrower,uint256 refinancingLoanId,uint256 nonce)" + "Request(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,address loanAssetAddress,uint256 loanAmount,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedLender,address borrower,uint256 refinancingLoanId,uint256 nonceSpace,uint256 nonce)" ); bytes32 public immutable DOMAIN_SEPARATOR; @@ -45,6 +45,7 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanRequest { * @param allowedLender Address of an allowed lender. Only this address can accept a request. If the address is zero address, anybody with a loan asset can accept the request. * @param borrower Address of a borrower. This address has to sign a request to be valid. * @param refinancingLoanId Id of a loan which is refinanced by this request. If the id is 0, the request is not a refinancing request. + * @param nonceSpace Nonce space of a request nonce. All nonces in the same space can be revoked at once. * @param nonce Additional value to enable identical requests in time. Without it, it would be impossible to make again request, which was once revoked. * Can be used to create a group of requests, where accepting one request will make other requests in the group revoked. */ @@ -62,6 +63,7 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanRequest { address allowedLender; address borrower; uint256 refinancingLoanId; + uint256 nonceSpace; uint256 nonce; } @@ -108,7 +110,7 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanRequest { /*----------------------------------------------------------*| - |* # IPWNSimpleLoanFactory *| + |* # PWNSimpleLoanTermsFactory *| |*----------------------------------------------------------*/ /** @@ -127,15 +129,15 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanRequest { address borrower = request.borrower; // Check that request has been made via on-chain tx, EIP-1271 or signed off-chain - if (requestsMade[requestHash] == false) - if (PWNSignatureChecker.isValidSignatureNow(borrower, requestHash, signature) == false) + if (!requestsMade[requestHash]) + if (!PWNSignatureChecker.isValidSignatureNow(borrower, requestHash, signature)) revert InvalidSignature(); // Check valid request if (block.timestamp >= request.expiration) revert RequestExpired(); - if (revokedRequestNonce.isNonceRevoked(borrower, request.nonce) == true) + if (revokedRequestNonce.isNonceRevoked(borrower, request.nonceSpace, request.nonce)) revert NonceAlreadyRevoked(); if (request.allowedLender != address(0)) @@ -180,7 +182,7 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanRequest { refinancingLoanId: request.refinancingLoanId }); - revokedRequestNonce.revokeNonce(borrower, request.nonce); + revokedRequestNonce.revokeNonce(borrower, request.nonceSpace, request.nonce); } diff --git a/src/loan/terms/simple/factory/request/base/PWNSimpleLoanRequest.sol b/src/loan/terms/simple/factory/request/base/PWNSimpleLoanRequest.sol index 4c46504..e35d220 100644 --- a/src/loan/terms/simple/factory/request/base/PWNSimpleLoanRequest.sol +++ b/src/loan/terms/simple/factory/request/base/PWNSimpleLoanRequest.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; -import "@pwn/hub/PWNHubAccessControl.sol"; -import "@pwn/loan/terms/simple/factory/PWNSimpleLoanTermsFactory.sol"; -import "@pwn/nonce/PWNRevokedNonce.sol"; +import { PWNHubAccessControl } from "@pwn/hub/PWNHubAccessControl.sol"; +import { PWNSimpleLoanTermsFactory } from "@pwn/loan/terms/simple/factory/PWNSimpleLoanTermsFactory.sol"; +import { PWNRevokedNonce } from "@pwn/nonce/PWNRevokedNonce.sol"; import "@pwn/PWNErrors.sol"; @@ -53,10 +53,11 @@ abstract contract PWNSimpleLoanRequest is PWNSimpleLoanTermsFactory, PWNHubAcces /** * @notice Helper function for revoking a request nonce on behalf of a caller. + * @param requestNonceSpace Nonce space of a request nonce to be revoked. * @param requestNonce Request nonce to be revoked. */ - function revokeRequestNonce(uint256 requestNonce) external { - revokedRequestNonce.revokeNonce(msg.sender, requestNonce); + function revokeRequestNonce(uint256 requestNonceSpace, uint256 requestNonce) external { + revokedRequestNonce.revokeNonce(msg.sender, requestNonceSpace, requestNonce); } } diff --git a/src/loan/terms/simple/loan/PWNSimpleLoan.sol b/src/loan/terms/simple/loan/PWNSimpleLoan.sol index e48e1b9..9ae650b 100644 --- a/src/loan/terms/simple/loan/PWNSimpleLoan.sol +++ b/src/loan/terms/simple/loan/PWNSimpleLoan.sol @@ -44,7 +44,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { uint256 public constant MIN_EXTENSION_DURATION = 1 days; bytes32 public constant EXTENSION_TYPEHASH = keccak256( - "Extension(uint256 loanId,uint256 price,uint40 duration,uint40 expiration,address proposer,uint256 nonce)" + "Extension(uint256 loanId,uint256 price,uint40 duration,uint40 expiration,address proposer,uint256 nonceSpace,uint256 nonce)" ); bytes32 public immutable DOMAIN_SEPARATOR = keccak256(abi.encode( @@ -101,6 +101,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { * @param duration Duration of the extension in seconds. * @param expiration Unix timestamp (in seconds) of an expiration date. * @param proposer Address of a proposer that signed the extension offer. + * @param nonceSpace Nonce space of the extension offer nonce. * @param nonce Nonce of the extension offer. */ struct Extension { @@ -109,6 +110,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { uint40 duration; uint40 expiration; address proposer; + uint256 nonceSpace; uint256 nonce; } @@ -224,12 +226,13 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { bytes calldata signature, bytes calldata loanAssetPermit, bytes calldata collateralPermit, + uint256 callersNonceSpace, uint256 callersNonceToRevoke ) external returns (uint256 loanId) { - if (revokedNonce.isNonceRevoked(msg.sender, callersNonceToRevoke)) + if (revokedNonce.isNonceRevoked(msg.sender, callersNonceSpace, callersNonceToRevoke)) revert NonceAlreadyRevoked(); - revokedNonce.revokeNonce(msg.sender, callersNonceToRevoke); + revokedNonce.revokeNonce(msg.sender, callersNonceSpace, callersNonceToRevoke); loanId = createLOAN({ loanTermsFactoryContract: loanTermsFactoryContract, loanTermsFactoryData: loanTermsFactoryData, @@ -859,7 +862,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { revert InvalidSignature(); if (block.timestamp >= extension.expiration) revert OfferExpired(); - if (revokedNonce.isNonceRevoked(extension.proposer, extension.nonce)) + if (revokedNonce.isNonceRevoked(extension.proposer, extension.nonceSpace, extension.nonce)) revert NonceAlreadyRevoked(); // Check caller and signer @@ -898,7 +901,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { }); // Revoke extension offer nonce - revokedNonce.revokeNonce(extension.proposer, extension.nonce); + revokedNonce.revokeNonce(extension.proposer, extension.nonceSpace, extension.nonce); // Update loan uint40 originalDefaultTimestamp = loan.defaultTimestamp; diff --git a/src/nonce/PWNRevokedNonce.sol b/src/nonce/PWNRevokedNonce.sol index d098c2e..b872ea7 100644 --- a/src/nonce/PWNRevokedNonce.sol +++ b/src/nonce/PWNRevokedNonce.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; -import "@pwn/hub/PWNHubAccessControl.sol"; +import { PWNHubAccessControl } from "@pwn/hub/PWNHubAccessControl.sol"; import "@pwn/PWNErrors.sol"; @@ -15,20 +15,22 @@ contract PWNRevokedNonce is PWNHubAccessControl { |* # VARIABLES & CONSTANTS DEFINITIONS *| |*----------------------------------------------------------*/ - bytes32 immutable internal accessTag; + /** + * @notice Access tag that needs to be assigned to a caller in PWN Hub + * to call functions that revoke nonces on behalf of an owner. + */ + bytes32 public immutable accessTag; /** - * @dev Mapping of revoked nonces by an address. - * Every address has its own nonce space. - * (owner => nonce => is revoked) + * @notice Mapping of revoked nonces by an address. Every address has its own nonce space. + * (owner => nonce space => nonce => is revoked) */ - mapping (address => mapping (uint256 => bool)) private revokedNonces; + mapping (address => mapping (uint256 => mapping (uint256 => bool))) private _revokedNonce; /** - * @dev Mapping of minimal nonce value per address. - * (owner => minimal nonce value) + * @notice Mapping of current nonce space for an address. */ - mapping (address => uint256) private minNonces; + mapping (address => uint256) private _nonceSpace; /*----------------------------------------------------------*| @@ -38,13 +40,12 @@ contract PWNRevokedNonce is PWNHubAccessControl { /** * @dev Emitted when a nonce is revoked. */ - event NonceRevoked(address indexed owner, uint256 indexed nonce); - + event NonceRevoked(address indexed owner, uint256 indexed nonceSpace, uint256 indexed nonce); /** - * @dev Emitted when a new min nonce value is set. + * @dev Emitted when a nonce is revoked. */ - event MinNonceSet(address indexed owner, uint256 indexed minNonce); + event NonceSpaceRevoked(address indexed owner, uint256 indexed nonceSpace); /*----------------------------------------------------------*| @@ -57,76 +58,74 @@ contract PWNRevokedNonce is PWNHubAccessControl { /*----------------------------------------------------------*| - |* # REVOKE NONCE *| + |* # NONCE *| |*----------------------------------------------------------*/ /** - * @notice Revoke a nonce. + * @notice Revoke a nonce in a nonce space. * @dev Caller is used as a nonce owner. + * @param nonceSpace Nonce space where a nonce will be revoked. * @param nonce Nonce to be revoked. */ - function revokeNonce(uint256 nonce) external { - _revokeNonce(msg.sender, nonce); + function revokeNonce(uint256 nonceSpace, uint256 nonce) external { + _revokeNonce(msg.sender, nonceSpace, nonce); } /** - * @notice Revoke a nonce on behalf of an owner. + * @notice Revoke a nonce in a nonce space on behalf of an owner. * @dev Only an address with associated access tag in PWN Hub can call this function. * @param owner Owner address of a revoking nonce. + * @param nonceSpace Nonce space where a nonce will be revoked. * @param nonce Nonce to be revoked. */ - function revokeNonce(address owner, uint256 nonce) external onlyWithTag(accessTag) { - _revokeNonce(owner, nonce); + function revokeNonce(address owner, uint256 nonceSpace, uint256 nonce) external onlyWithTag(accessTag) { + _revokeNonce(owner, nonceSpace, nonce); } - function _revokeNonce(address owner, uint256 nonce) private { - // Revoke nonce - revokedNonces[owner][nonce] = true; - - // Emit event - emit NonceRevoked(owner, nonce); + /** + * @notice Internal function to revoke a nonce in a nonce space. + */ + function _revokeNonce(address owner, uint256 nonceSpace, uint256 nonce) private { + _revokedNonce[owner][nonceSpace][nonce] = true; + emit NonceRevoked(owner, nonceSpace, nonce); } - - /*----------------------------------------------------------*| - |* # SET MIN NONCE *| - |*----------------------------------------------------------*/ - /** - * @notice Set a minimal nonce. - * @dev Nonce is considered revoked when smaller than minimal nonce. - * @param minNonce New value of a minimal nonce. + * @notice Return true if owners nonce is revoked in the given nonce space, or if the whole nonce space is revoked. + * @param owner Address of a nonce owner. + * @param nonceSpace Value of a nonce space. + * @param nonce Value of a nonce. + * @return True if nonce is revoked. */ - function setMinNonce(uint256 minNonce) external { - // Check that nonce is greater than current min nonce - uint256 currentMinNonce = minNonces[msg.sender]; - if (currentMinNonce >= minNonce) - revert InvalidMinNonce(); - - // Set new min nonce value - minNonces[msg.sender] = minNonce; + function isNonceRevoked(address owner, uint256 nonceSpace, uint256 nonce) external view returns (bool) { + if (_nonceSpace[owner] > nonceSpace) + return true; - // Emit event - emit MinNonceSet(msg.sender, minNonce); + return _revokedNonce[owner][nonceSpace][nonce]; } /*----------------------------------------------------------*| - |* # IS NONCE REVOKED *| + |* # NONCE SPACE *| |*----------------------------------------------------------*/ /** - * @notice Get information if owners nonce is revoked or not. - * @dev Nonce is considered revoked if is smaller than owners min nonce value or if is explicitly revoked. - * @param owner Address of a nonce owner. - * @param nonce Nonce in question. - * @return True if owners nonce is revoked. + * @notice Revoke all nonces in the current nonce space and increment nonce space. + * @dev Caller is used as a nonce owner. + * @return New nonce space. */ - function isNonceRevoked(address owner, uint256 nonce) external view returns (bool) { - if (nonce < minNonces[owner]) - return true; + function revokeNonceSpace() external returns (uint256) { + emit NonceSpaceRevoked(msg.sender, _nonceSpace[msg.sender]); + return ++_nonceSpace[msg.sender]; + } - return revokedNonces[owner][nonce]; + /** + * @notice Return current nonce space for an address. + * @param owner Address of a nonce owner. + * @return Current nonce space. + */ + function currentNonceSpace(address owner) external view returns (uint256) { + return _nonceSpace[owner]; } } diff --git a/test/unit/PWNRevokedNonce.t.sol b/test/unit/PWNRevokedNonce.t.sol index 59f7008..e395945 100644 --- a/test/unit/PWNRevokedNonce.t.sol +++ b/test/unit/PWNRevokedNonce.t.sol @@ -10,17 +10,16 @@ import "@pwn/PWNErrors.sol"; abstract contract PWNRevokedNonceTest is Test { - bytes32 internal constant REVOKED_NONCES_SLOT = bytes32(uint256(0)); // `revokedNonces` mapping position - bytes32 internal constant MIN_NONCES_SLOT = bytes32(uint256(1)); // `minNonces` mapping position + bytes32 internal constant REVOKED_NONCE_SLOT = bytes32(uint256(0)); // `_revokedNonce` mapping position + bytes32 internal constant NONCE_SPACE_SLOT = bytes32(uint256(1)); // `_nonceSpace` mapping position PWNRevokedNonce revokedNonce; bytes32 accessTag = keccak256("Some nice pwn tag"); address hub = address(0x80b); address alice = address(0xa11ce); - uint256 nonce = uint256(keccak256("nonce_1")); - event NonceRevoked(address indexed owner, uint256 indexed nonce); - event MinNonceSet(address indexed owner, uint256 indexed minNonce); + event NonceRevoked(address indexed owner, uint256 indexed nonceSpace, uint256 indexed nonce); + event NonceSpaceRevoked(address indexed owner, uint256 indexed nonceSpace); function setUp() public virtual { @@ -28,21 +27,18 @@ abstract contract PWNRevokedNonceTest is Test { } - function _revokedNonceSlot(address owner, uint256 _nonce) internal pure returns (bytes32) { + function _revokedNonceSlot(address _owner, uint256 _nonceSpace, uint256 _nonce) internal pure returns (bytes32) { return keccak256(abi.encode( _nonce, keccak256(abi.encode( - owner, - REVOKED_NONCES_SLOT + _nonceSpace, + keccak256(abi.encode(_owner, REVOKED_NONCE_SLOT)) )) )); } - function _minNonceSlot(address owner) internal pure returns (bytes32) { - return keccak256(abi.encode( - owner, - MIN_NONCES_SLOT - )); + function _nonceSpaceSlot(address _owner) internal pure returns (bytes32) { + return keccak256(abi.encode(_owner, NONCE_SPACE_SLOT)); } } @@ -54,23 +50,23 @@ abstract contract PWNRevokedNonceTest is Test { contract PWNRevokedNonce_RevokeNonceByOwner_Test is PWNRevokedNonceTest { - function test_shouldStoreNonceAsRevoked() external { + function testFuzz_shouldStoreNonceAsRevoked(uint256 nonceSpace, uint256 nonce) external { vm.prank(alice); - revokedNonce.revokeNonce(nonce); + revokedNonce.revokeNonce(nonceSpace, nonce); bytes32 isRevokedValue = vm.load( address(revokedNonce), - _revokedNonceSlot(alice, nonce) + _revokedNonceSlot(alice, nonceSpace, nonce) ); assertTrue(uint256(isRevokedValue) == 1); } - function test_shouldEmitEvent_NonceRevoked() external { - vm.expectEmit(true, true, false, false); - emit NonceRevoked(alice, nonce); + function testFuzz_shouldEmit_NonceRevoked(uint256 nonceSpace, uint256 nonce) external { + vm.expectEmit(); + emit NonceRevoked(alice, nonceSpace, nonce); vm.prank(alice); - revokedNonce.revokeNonce(nonce); + revokedNonce.revokeNonce(nonceSpace, nonce); } } @@ -100,108 +96,119 @@ contract PWNRevokedNonce_RevokeNonceWithOwner_Test is PWNRevokedNonceTest { } - function test_shouldFail_whenCallerIsDoesNotHaveAccessTag() external { + function testFuzz_shouldFail_whenCallerIsDoesNotHaveAccessTag(address caller) external { + vm.assume(caller != accessEnabledAddress); + vm.expectRevert(abi.encodeWithSelector(CallerMissingHubTag.selector, accessTag)); - vm.prank(alice); - revokedNonce.revokeNonce(alice, nonce); + vm.prank(caller); + revokedNonce.revokeNonce(caller, 1, 1); } - function test_shouldStoreNonceAsRevoked() external { + function testFuzz_shouldStoreNonceAsRevoked(address owner, uint256 nonceSpace, uint256 nonce) external { vm.prank(accessEnabledAddress); - revokedNonce.revokeNonce(alice, nonce); + revokedNonce.revokeNonce(owner, nonceSpace, nonce); bytes32 isRevokedValue = vm.load( address(revokedNonce), - _revokedNonceSlot(alice, nonce) + _revokedNonceSlot(owner, nonceSpace, nonce) ); assertTrue(uint256(isRevokedValue) == 1); } - function test_shouldEmitEvent_NonceRevoked() external { - vm.expectEmit(true, true, false, false); - emit NonceRevoked(alice, nonce); + function testFuzz_shouldEmit_NonceRevoked(address owner, uint256 nonceSpace, uint256 nonce) external { + vm.expectEmit(); + emit NonceRevoked(owner, nonceSpace, nonce); vm.prank(accessEnabledAddress); - revokedNonce.revokeNonce(alice, nonce); + revokedNonce.revokeNonce(owner, nonceSpace, nonce); } } /*----------------------------------------------------------*| -|* # SET MIN NONCE *| +|* # IS NONCE REVOKED *| |*----------------------------------------------------------*/ -contract PWNRevokedNonce_SetMinNonceByOwner_Test is PWNRevokedNonceTest { +contract PWNRevokedNonce_IsNonceRevoked_Test is PWNRevokedNonceTest { - function test_shouldFail_whenNewValueIsSmallerThanCurrent() external { - vm.store( - address(revokedNonce), - _minNonceSlot(alice), - bytes32(nonce + 1) - ); + function testFuzz_shouldReturnTrue_whenNonceSpaceIsSmallerThanCurrentNonceSpace(uint256 currentNonceSpace, uint256 nonce) external { + currentNonceSpace = bound(currentNonceSpace, 1, type(uint256).max); + uint256 nonceSpace = bound(currentNonceSpace, 0, currentNonceSpace - 1); - vm.expectRevert(abi.encodeWithSelector(InvalidMinNonce.selector)); - vm.prank(alice); - revokedNonce.setMinNonce(nonce); + vm.store(address(revokedNonce), _nonceSpaceSlot(alice), bytes32(currentNonceSpace)); + + assertTrue(revokedNonce.isNonceRevoked(alice, nonceSpace, nonce)); } - function test_shouldStoreNewMinNonce() external { - vm.prank(alice); - revokedNonce.setMinNonce(nonce); + function testFuzz_shouldReturnTrue_whenNonceIsRevoked(uint256 nonce) external { + vm.store(address(revokedNonce), _revokedNonceSlot(alice, 0, nonce), bytes32(uint256(1))); - bytes32 minNonce = vm.load( - address(revokedNonce), - _minNonceSlot(alice) - ); - assertTrue(uint256(minNonce) == nonce); + assertTrue(revokedNonce.isNonceRevoked(alice, 0, nonce)); } - function test_shouldEmitEvent_MinNonceSet() external { - vm.expectEmit(true, true, false, false); - emit MinNonceSet(alice, nonce); - - vm.prank(alice); - revokedNonce.setMinNonce(nonce); + function testFuzz_shouldReturnFalse_whenNonceIsNotRevoked(uint256 nonceSpace, uint256 nonce) external { + assertFalse(revokedNonce.isNonceRevoked(alice, nonceSpace, nonce)); } } /*----------------------------------------------------------*| -|* # IS NONCE REVOKED *| +|* # REVOKE NONCE SPACE *| |*----------------------------------------------------------*/ -contract PWNRevokedNonce_IsNonceRevoked_Test is PWNRevokedNonceTest { +contract PWNRevokedNonce_RevokeNonceSpace_Test is PWNRevokedNonceTest { - function test_shouldReturnTrue_whenNonceIsSmallerThanMinNonce() external { - vm.store( - address(revokedNonce), - _minNonceSlot(alice), - bytes32(nonce + 1) - ); + function testFuzz_shouldIncrementCurrentNonceSpace(uint256 nonceSpace) external { + nonceSpace = bound(nonceSpace, 0, type(uint256).max - 1); + bytes32 nonceSpaceSlot = _nonceSpaceSlot(alice); + vm.store(address(revokedNonce), nonceSpaceSlot, bytes32(nonceSpace)); - bool isRevoked = revokedNonce.isNonceRevoked(alice, nonce); + vm.prank(alice); + revokedNonce.revokeNonceSpace(); - assertTrue(isRevoked); + assertEq(revokedNonce.currentNonceSpace(alice), nonceSpace + 1); } - function test_shouldReturnTrue_whenNonceIsRevoked() external { - vm.store( - address(revokedNonce), - _revokedNonceSlot(alice, nonce), - bytes32(uint256(1)) - ); + function testFuzz_shouldEmit_NonceSpaceRevoked(uint256 nonceSpace) external { + nonceSpace = bound(nonceSpace, 0, type(uint256).max - 1); + bytes32 nonceSpaceSlot = _nonceSpaceSlot(alice); + vm.store(address(revokedNonce), nonceSpaceSlot, bytes32(nonceSpace)); + + vm.expectEmit(); + emit NonceSpaceRevoked(alice, nonceSpace); + + vm.prank(alice); + revokedNonce.revokeNonceSpace(); + } + + function testFuzz_shouldReturnNewNonceSpace(uint256 nonceSpace) external { + nonceSpace = bound(nonceSpace, 0, type(uint256).max - 1); + bytes32 nonceSpaceSlot = _nonceSpaceSlot(alice); + vm.store(address(revokedNonce), nonceSpaceSlot, bytes32(nonceSpace)); - bool isRevoked = revokedNonce.isNonceRevoked(alice, nonce); + vm.prank(alice); + uint256 currentNonceSpace = revokedNonce.revokeNonceSpace(); - assertTrue(isRevoked); + assertEq(currentNonceSpace, nonceSpace + 1); } - function test_shouldReturnFalse_whenNonceIsNotRevoked() external { - bool isRevoked = revokedNonce.isNonceRevoked(alice, nonce); +} + + +/*----------------------------------------------------------*| +|* # CURRENT NONCE SPACE *| +|*----------------------------------------------------------*/ + +contract PWNRevokedNonce_CurrentNonceSpace_Test is PWNRevokedNonceTest { + + function testFuzz_shouldReturnCurrentNonceSpace(uint256 nonceSpace) external { + vm.store(address(revokedNonce), _nonceSpaceSlot(alice), bytes32(nonceSpace)); + + uint256 currentNonceSpace = revokedNonce.currentNonceSpace(alice); - assertFalse(isRevoked); + assertEq(currentNonceSpace, nonceSpace); } } diff --git a/test/unit/PWNSimpleLoan.t.sol b/test/unit/PWNSimpleLoan.t.sol index 70ba899..5ab973b 100644 --- a/test/unit/PWNSimpleLoan.t.sol +++ b/test/unit/PWNSimpleLoan.t.sol @@ -132,7 +132,8 @@ abstract contract PWNSimpleLoanTest is Test { duration: 2 days, expiration: simpleLoan.defaultTimestamp, proposer: borrower, - nonce: 0 + nonceSpace: 1, + nonce: 1 }); loanFactoryDataHash = keccak256("factoryData"); @@ -164,7 +165,7 @@ abstract contract PWNSimpleLoanTest is Test { _mockLOANTokenOwner(loanId, lender); vm.mockCall( - revokedNonce, abi.encodeWithSignature("isNonceRevoked(address,uint256)"), abi.encode(false) + revokedNonce, abi.encodeWithSignature("isNonceRevoked(address,uint256,uint256)"), abi.encode(false) ); } @@ -278,7 +279,7 @@ abstract contract PWNSimpleLoanTest is Test { address(loan) )), keccak256(abi.encodePacked( - keccak256("Extension(uint256 loanId,uint256 price,uint40 duration,uint40 expiration,address proposer,uint256 nonce)"), + keccak256("Extension(uint256 loanId,uint256 price,uint40 duration,uint40 expiration,address proposer,uint256 nonceSpace,uint256 nonce)"), abi.encode(_extension) )) )); @@ -446,32 +447,32 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { contract PWNSimpleLoan_CreateLOANAndRevokeNonce_Test is PWNSimpleLoanTest { - function testFuzz_shouldFail_whenNonceAlreadyRevoked(uint256 nonce) external { + function testFuzz_shouldFail_whenNonceAlreadyRevoked(uint256 nonceSpace, uint256 nonce) external { vm.mockCall( revokedNonce, - abi.encodeWithSignature("isNonceRevoked(address,uint256)", borrower, nonce), + abi.encodeWithSignature("isNonceRevoked(address,uint256,uint256)", borrower, nonceSpace, nonce), abi.encode(true) ); vm.expectRevert(abi.encodeWithSelector(NonceAlreadyRevoked.selector)); vm.prank(borrower); - loan.createLOANAndRevokeNonce(loanFactory, loanFactoryData, "", "", "", nonce); + loan.createLOANAndRevokeNonce(loanFactory, loanFactoryData, "", "", "", nonceSpace, nonce); } - function testFuzz_shouldRevokeCallersNonce(address caller, uint256 nonce) external { + function testFuzz_shouldRevokeCallersNonce(address caller, uint256 nonceSpace, uint256 nonce) external { assumeAddressIsNot(caller, AddressType.ZeroAddress); vm.expectCall( revokedNonce, - abi.encodeWithSignature("revokeNonce(address,uint256)", caller, nonce) + abi.encodeWithSignature("revokeNonce(address,uint256,uint256)", caller, nonceSpace, nonce) ); vm.prank(caller); - loan.createLOANAndRevokeNonce(loanFactory, loanFactoryData, "", "", "", nonce); + loan.createLOANAndRevokeNonce(loanFactory, loanFactoryData, "", "", "", nonceSpace, nonce); } function test_shouldCreateLoan() external { - uint256 _loanId = loan.createLOANAndRevokeNonce(loanFactory, loanFactoryData, "", "", "", 1); + uint256 _loanId = loan.createLOANAndRevokeNonce(loanFactory, loanFactoryData, "", "", "", 0, 1); assertEq(_loanId, loanId); _assertLOANEq(_loanId, simpleLoan); @@ -1730,7 +1731,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { vm.mockCall( revokedNonce, - abi.encodeWithSignature("isNonceRevoked(address,uint256)", extension.proposer, extension.nonce), + abi.encodeWithSignature("isNonceRevoked(address,uint256,uint256)", extension.proposer, extension.nonceSpace, extension.nonce), abi.encode(true) ); @@ -1794,13 +1795,14 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { loan.extendLOAN(extension, "", ""); } - function testFuzz_shouldRevokeExtensionNonce(uint256 nonce) external { + function testFuzz_shouldRevokeExtensionNonce(uint256 nonceSpace, uint256 nonce) external { + extension.nonceSpace = nonceSpace; extension.nonce = nonce; _mockExtensionOfferMade(extension); vm.expectCall( revokedNonce, - abi.encodeWithSignature("revokeNonce(address,uint256)", extension.proposer, nonce) + abi.encodeWithSignature("revokeNonce(address,uint256,uint256)", extension.proposer, nonceSpace, nonce) ); vm.prank(lender); diff --git a/test/unit/PWNSimpleLoanListOffer.t.sol b/test/unit/PWNSimpleLoanListOffer.t.sol index 7088a8b..969aa83 100644 --- a/test/unit/PWNSimpleLoanListOffer.t.sol +++ b/test/unit/PWNSimpleLoanListOffer.t.sol @@ -48,6 +48,7 @@ abstract contract PWNSimpleLoanListOfferTest is Test { allowedBorrower: address(0), lender: lender, isPersistent: false, + nonceSpace: 1, nonce: uint256(keccak256("nonce_1")) }); @@ -58,7 +59,7 @@ abstract contract PWNSimpleLoanListOfferTest is Test { vm.mockCall( revokedOfferNonce, - abi.encodeWithSignature("isNonceRevoked(address,uint256)"), + abi.encodeWithSignature("isNonceRevoked(address,uint256,uint256)"), abi.encode(false) ); } @@ -75,7 +76,7 @@ abstract contract PWNSimpleLoanListOfferTest is Test { address(offerContract) )), keccak256(abi.encodePacked( - keccak256("Offer(uint8 collateralCategory,address collateralAddress,bytes32 collateralIdsWhitelistMerkleRoot,uint256 collateralAmount,address loanAssetAddress,uint256 loanAmount,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedBorrower,address lender,bool isPersistent,uint256 nonce)"), + keccak256("Offer(uint8 collateralCategory,address collateralAddress,bytes32 collateralIdsWhitelistMerkleRoot,uint256 collateralAmount,address loanAssetAddress,uint256 loanAmount,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedBorrower,address lender,bool isPersistent,uint256 nonceSpace,uint256 nonce)"), abi.encode(_offer) )) )); @@ -245,12 +246,12 @@ contract PWNSimpleLoanListOffer_CreateLOANTerms_Test is PWNSimpleLoanListOfferTe vm.mockCall( revokedOfferNonce, - abi.encodeWithSignature("isNonceRevoked(address,uint256)"), + abi.encodeWithSignature("isNonceRevoked(address,uint256,uint256)"), abi.encode(true) ); vm.expectCall( revokedOfferNonce, - abi.encodeWithSignature("isNonceRevoked(address,uint256)", offer.lender, offer.nonce) + abi.encodeWithSignature("isNonceRevoked(address,uint256,uint256)", offer.lender, offer.nonceSpace, offer.nonce) ); vm.expectRevert(abi.encodeWithSelector(NonceAlreadyRevoked.selector)); @@ -296,7 +297,7 @@ contract PWNSimpleLoanListOffer_CreateLOANTerms_Test is PWNSimpleLoanListOfferTe vm.expectCall( revokedOfferNonce, - abi.encodeWithSignature("revokeNonce(address,uint256)", offer.lender, offer.nonce) + abi.encodeWithSignature("revokeNonce(address,uint256,uint256)", offer.lender, offer.nonceSpace, offer.nonce) ); vm.prank(activeLoanContract); @@ -309,7 +310,7 @@ contract PWNSimpleLoanListOffer_CreateLOANTerms_Test is PWNSimpleLoanListOfferTe vm.expectCall({ callee: revokedOfferNonce, - data: abi.encodeWithSignature("revokeNonce(address,uint256)", offer.lender, offer.nonce), + data: abi.encodeWithSignature("revokeNonce(address,uint256,uint256)", offer.lender, offer.nonceSpace, offer.nonce), count: 0 }); diff --git a/test/unit/PWNSimpleLoanOffer.t.sol b/test/unit/PWNSimpleLoanOffer.t.sol index 4453b3e..9702dac 100644 --- a/test/unit/PWNSimpleLoanOffer.t.sol +++ b/test/unit/PWNSimpleLoanOffer.t.sol @@ -54,7 +54,7 @@ abstract contract PWNSimpleLoanOfferTest is Test { vm.mockCall( revokedOfferNonce, - abi.encodeWithSignature("isNonceRevoked(address,uint256)"), + abi.encodeWithSignature("isNonceRevoked(address,uint256,uint256)"), abi.encode(false) ); } @@ -93,16 +93,14 @@ contract PWNSimpleLoanOffer_MakeOffer_Test is PWNSimpleLoanOfferTest { contract PWNSimpleLoanOffer_RevokeOfferNonce_Test is PWNSimpleLoanOfferTest { - function test_shouldCallRevokeOfferNonce() external { - uint256 nonce = uint256(keccak256("its my monkey")); - + function testFuzz_shouldCallRevokeOfferNonce(uint256 nonceSpace, uint256 nonce) external { vm.expectCall( revokedOfferNonce, - abi.encodeWithSignature("revokeNonce(address,uint256)", lender, nonce) + abi.encodeWithSignature("revokeNonce(address,uint256,uint256)", lender, nonceSpace, nonce) ); vm.prank(lender); - offerContract.revokeOfferNonce(nonce); + offerContract.revokeOfferNonce(nonceSpace, nonce); } } diff --git a/test/unit/PWNSimpleLoanRequest.t.sol b/test/unit/PWNSimpleLoanRequest.t.sol index ef9cfc3..422876e 100644 --- a/test/unit/PWNSimpleLoanRequest.t.sol +++ b/test/unit/PWNSimpleLoanRequest.t.sol @@ -54,7 +54,7 @@ abstract contract PWNSimpleLoanRequestTest is Test { vm.mockCall( revokedRequestNonce, - abi.encodeWithSignature("isNonceRevoked(address,uint256)"), + abi.encodeWithSignature("isNonceRevoked(address,uint256,uint256)"), abi.encode(false) ); } @@ -93,16 +93,14 @@ contract PWNSimpleLoanRequest_MakeRequest_Test is PWNSimpleLoanRequestTest { contract PWNSimpleLoanRequest_RevokeRequestNonce_Test is PWNSimpleLoanRequestTest { - function test_shouldCallRevokeRequestNonce() external { - uint256 nonce = uint256(keccak256("its my monkey")); - + function testFuzz_shouldCallRevokeRequestNonce(uint256 nonceSpace, uint256 nonce) external { vm.expectCall( revokedRequestNonce, - abi.encodeWithSignature("revokeNonce(address,uint256)", borrower, nonce) + abi.encodeWithSignature("revokeNonce(address,uint256,uint256)", borrower, nonceSpace, nonce) ); vm.prank(borrower); - requestContract.revokeRequestNonce(nonce); + requestContract.revokeRequestNonce(nonceSpace, nonce); } } diff --git a/test/unit/PWNSimpleLoanSimpleOffer.t.sol b/test/unit/PWNSimpleLoanSimpleOffer.t.sol index b92c3ec..55e4a22 100644 --- a/test/unit/PWNSimpleLoanSimpleOffer.t.sol +++ b/test/unit/PWNSimpleLoanSimpleOffer.t.sol @@ -47,12 +47,13 @@ abstract contract PWNSimpleLoanSimpleOfferTest is Test { allowedBorrower: address(0), lender: lender, isPersistent: false, + nonceSpace: 1, nonce: uint256(keccak256("nonce_1")) }); vm.mockCall( revokedOfferNonce, - abi.encodeWithSignature("isNonceRevoked(address,uint256)"), + abi.encodeWithSignature("isNonceRevoked(address,uint256,uint256)"), abi.encode(false) ); } @@ -69,7 +70,7 @@ abstract contract PWNSimpleLoanSimpleOfferTest is Test { address(offerContract) )), keccak256(abi.encodePacked( - keccak256("Offer(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,address loanAssetAddress,uint256 loanAmount,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedBorrower,address lender,bool isPersistent,uint256 nonce)"), + keccak256("Offer(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,address loanAssetAddress,uint256 loanAmount,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedBorrower,address lender,bool isPersistent,uint256 nonceSpace,uint256 nonce)"), abi.encode(_offer) )) )); @@ -239,12 +240,12 @@ contract PWNSimpleLoanSimpleOffer_CreateLOANTerms_Test is PWNSimpleLoanSimpleOff vm.mockCall( revokedOfferNonce, - abi.encodeWithSignature("isNonceRevoked(address,uint256)"), + abi.encodeWithSignature("isNonceRevoked(address,uint256,uint256)"), abi.encode(true) ); vm.expectCall( revokedOfferNonce, - abi.encodeWithSignature("isNonceRevoked(address,uint256)", offer.lender, offer.nonce) + abi.encodeWithSignature("isNonceRevoked(address,uint256,uint256)", offer.lender, offer.nonceSpace, offer.nonce) ); vm.expectRevert(abi.encodeWithSelector(NonceAlreadyRevoked.selector)); @@ -290,7 +291,7 @@ contract PWNSimpleLoanSimpleOffer_CreateLOANTerms_Test is PWNSimpleLoanSimpleOff vm.expectCall( revokedOfferNonce, - abi.encodeWithSignature("revokeNonce(address,uint256)", offer.lender, offer.nonce) + abi.encodeWithSignature("revokeNonce(address,uint256,uint256)", offer.lender, offer.nonceSpace, offer.nonce) ); vm.prank(activeLoanContract); @@ -303,7 +304,7 @@ contract PWNSimpleLoanSimpleOffer_CreateLOANTerms_Test is PWNSimpleLoanSimpleOff vm.expectCall({ callee: revokedOfferNonce, - data: abi.encodeWithSignature("revokeNonce(address,uint256)", offer.lender, offer.nonce), + data: abi.encodeWithSignature("revokeNonce(address,uint256,uint256)", offer.lender, offer.nonceSpace, offer.nonce), count: 0 }); diff --git a/test/unit/PWNSimpleLoanSimpleRequest.t.sol b/test/unit/PWNSimpleLoanSimpleRequest.t.sol index 767dc50..9f5cb00 100644 --- a/test/unit/PWNSimpleLoanSimpleRequest.t.sol +++ b/test/unit/PWNSimpleLoanSimpleRequest.t.sol @@ -47,12 +47,13 @@ abstract contract PWNSimpleLoanSimpleRequestTest is Test { allowedLender: address(0), borrower: borrower, refinancingLoanId: 0, + nonceSpace: 1, nonce: uint256(keccak256("nonce_1")) }); vm.mockCall( revokedRequestNonce, - abi.encodeWithSignature("isNonceRevoked(address,uint256)"), + abi.encodeWithSignature("isNonceRevoked(address,uint256,uint256)"), abi.encode(false) ); } @@ -69,7 +70,7 @@ abstract contract PWNSimpleLoanSimpleRequestTest is Test { address(requestContract) )), keccak256(abi.encodePacked( - keccak256("Request(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,address loanAssetAddress,uint256 loanAmount,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedLender,address borrower,uint256 refinancingLoanId,uint256 nonce)"), + keccak256("Request(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,address loanAssetAddress,uint256 loanAmount,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedLender,address borrower,uint256 refinancingLoanId,uint256 nonceSpace,uint256 nonce)"), abi.encode(_request) )) )); @@ -239,12 +240,12 @@ contract PWNSimpleLoanSimpleRequest_CreateLOANTerms_Test is PWNSimpleLoanSimpleR vm.mockCall( revokedRequestNonce, - abi.encodeWithSignature("isNonceRevoked(address,uint256)"), + abi.encodeWithSignature("isNonceRevoked(address,uint256,uint256)"), abi.encode(true) ); vm.expectCall( revokedRequestNonce, - abi.encodeWithSignature("isNonceRevoked(address,uint256)", request.borrower, request.nonce) + abi.encodeWithSignature("isNonceRevoked(address,uint256,uint256)", request.borrower, request.nonceSpace, request.nonce) ); vm.expectRevert(abi.encodeWithSelector(NonceAlreadyRevoked.selector)); @@ -289,7 +290,7 @@ contract PWNSimpleLoanSimpleRequest_CreateLOANTerms_Test is PWNSimpleLoanSimpleR vm.expectCall( revokedRequestNonce, - abi.encodeWithSignature("revokeNonce(address,uint256)", request.borrower, request.nonce) + abi.encodeWithSignature("revokeNonce(address,uint256,uint256)", request.borrower, request.nonceSpace, request.nonce) ); vm.prank(activeLoanContract); From 3f86a36519b8ab9c6e8c17372fc828db1d657fd1 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Fri, 1 Mar 2024 13:17:52 +0100 Subject: [PATCH 031/129] build(shared-nonces): update deployment script with shared revoked nonce contract --- script/PWN.s.sol | 83 ++++++++++++++++++++++-------------------------- 1 file changed, 38 insertions(+), 45 deletions(-) diff --git a/script/PWN.s.sol b/script/PWN.s.sol index 741efd3..46ff2db 100644 --- a/script/PWN.s.sol +++ b/script/PWN.s.sol @@ -8,34 +8,33 @@ from "openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableP import { GnosisSafeLike, GnosisSafeUtils } from "./lib/GnosisSafeUtils.sol"; -import "@pwn/config/PWNConfig.sol"; -import "@pwn/deployer/IPWNDeployer.sol"; -import "@pwn/hub/PWNHub.sol"; -import "@pwn/hub/PWNHubTags.sol"; -import "@pwn/loan/terms/simple/loan/PWNSimpleLoan.sol"; -import "@pwn/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol"; -import "@pwn/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol"; -import "@pwn/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol"; -import "@pwn/loan/token/PWNLOAN.sol"; -import "@pwn/nonce/PWNRevokedNonce.sol"; -import "@pwn/Deployments.sol"; - -import "@pwn-test/helper/token/T20.sol"; -import "@pwn-test/helper/token/T721.sol"; -import "@pwn-test/helper/token/T1155.sol"; +import { PWNConfig } from "@pwn/config/PWNConfig.sol"; +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/factory/offer/PWNSimpleLoanListOffer.sol"; +import { PWNSimpleLoanSimpleOffer } from "@pwn/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol"; +import { PWNSimpleLoanSimpleRequest } from "@pwn/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol"; +import { PWNLOAN } from "@pwn/loan/token/PWNLOAN.sol"; +import { PWNRevokedNonce } from "@pwn/nonce/PWNRevokedNonce.sol"; +import { Deployments } from "@pwn/Deployments.sol"; + +import { T20 } from "@pwn-test/helper/token/T20.sol"; +import { T721 } from "@pwn-test/helper/token/T721.sol"; +import { T1155 } from "@pwn-test/helper/token/T1155.sol"; library PWNContractDeployerSalt { - string internal constant VERSION = "1.1"; + string internal constant VERSION = "1.2"; // Singletons bytes32 internal constant CONFIG_V1 = keccak256("PWNConfigV1"); bytes32 internal constant CONFIG_PROXY = keccak256("PWNConfigProxy"); bytes32 internal constant HUB = keccak256("PWNHub"); bytes32 internal constant LOAN = keccak256("PWNLOAN"); - bytes32 internal constant REVOKED_OFFER_NONCE = keccak256("PWNRevokedOfferNonce"); - bytes32 internal constant REVOKED_REQUEST_NONCE = keccak256("PWNRevokedRequestNonce"); + bytes32 internal constant REVOKED_NONCE = keccak256("PWNRevokedNonce"); // Loan types bytes32 internal constant SIMPLE_LOAN = keccak256("PWNSimpleLoan"); @@ -149,18 +148,11 @@ forge script script/PWN.s.sol:Deploy \ })); // - Revoked nonces - revokedOfferNonce = PWNRevokedNonce(_deploy({ - salt: PWNContractDeployerSalt.REVOKED_OFFER_NONCE, + revokedNonce = PWNRevokedNonce(_deploy({ + salt: PWNContractDeployerSalt.REVOKED_NONCE, bytecode: abi.encodePacked( type(PWNRevokedNonce).creationCode, - abi.encode(address(hub), PWNHubTags.LOAN_OFFER) - ) - })); - revokedRequestNonce = PWNRevokedNonce(_deploy({ - salt: PWNContractDeployerSalt.REVOKED_REQUEST_NONCE, - bytecode: abi.encodePacked( - type(PWNRevokedNonce).creationCode, - abi.encode(address(hub), PWNHubTags.LOAN_REQUEST) + abi.encode(address(hub), PWNHubTags.NONCE_MANAGER) ) })); @@ -169,7 +161,7 @@ forge script script/PWN.s.sol:Deploy \ salt: PWNContractDeployerSalt.SIMPLE_LOAN, bytecode: abi.encodePacked( type(PWNSimpleLoan).creationCode, - abi.encode(address(hub), address(loanToken), address(config)) + abi.encode(address(hub), address(loanToken), address(revokedNonce), address(config)) ) })); @@ -178,14 +170,14 @@ forge script script/PWN.s.sol:Deploy \ salt: PWNContractDeployerSalt.SIMPLE_LOAN_SIMPLE_OFFER, bytecode: abi.encodePacked( type(PWNSimpleLoanSimpleOffer).creationCode, - abi.encode(address(hub), address(revokedOfferNonce)) + abi.encode(address(hub), address(revokedNonce)) ) })); simpleLoanListOffer = PWNSimpleLoanListOffer(_deploy({ salt: PWNContractDeployerSalt.SIMPLE_LOAN_LIST_OFFER, bytecode: abi.encodePacked( type(PWNSimpleLoanListOffer).creationCode, - abi.encode(address(hub), address(revokedOfferNonce)) + abi.encode(address(hub), address(revokedNonce)) ) })); @@ -194,7 +186,7 @@ forge script script/PWN.s.sol:Deploy \ salt: PWNContractDeployerSalt.SIMPLE_LOAN_SIMPLE_REQUEST, bytecode: abi.encodePacked( type(PWNSimpleLoanSimpleRequest).creationCode, - abi.encode(address(hub), address(revokedRequestNonce)) + abi.encode(address(hub), address(revokedNonce)) ) })); @@ -202,8 +194,7 @@ forge script script/PWN.s.sol:Deploy \ console2.log("PWNConfig - proxy:", address(config)); console2.log("PWNHub:", address(hub)); console2.log("PWNLOAN:", address(loanToken)); - console2.log("PWNRevokedNonce (offer):", address(revokedOfferNonce)); - console2.log("PWNRevokedNonce (request):", address(revokedRequestNonce)); + console2.log("PWNRevokedNonce:", address(revokedNonce)); console2.log("PWNSimpleLoan:", address(simpleLoan)); console2.log("PWNSimpleLoanSimpleOffer:", address(simpleLoanSimpleOffer)); console2.log("PWNSimpleLoanListOffer:", address(simpleLoanListOffer)); @@ -285,23 +276,25 @@ forge script script/PWN.s.sol:Setup \ } function _setTags() internal { - address[] memory addrs = new address[](7); + address[] memory addrs = new address[](8); addrs[0] = address(simpleLoan); - addrs[1] = address(simpleLoanSimpleOffer); + addrs[1] = address(simpleLoan); addrs[2] = address(simpleLoanSimpleOffer); - addrs[3] = address(simpleLoanListOffer); + addrs[3] = address(simpleLoanSimpleOffer); addrs[4] = address(simpleLoanListOffer); - addrs[5] = address(simpleLoanSimpleRequest); + addrs[5] = address(simpleLoanListOffer); addrs[6] = address(simpleLoanSimpleRequest); + addrs[7] = address(simpleLoanSimpleRequest); - bytes32[] memory tags = new bytes32[](7); + bytes32[] memory tags = new bytes32[](8); tags[0] = PWNHubTags.ACTIVE_LOAN; - tags[1] = PWNHubTags.SIMPLE_LOAN_TERMS_FACTORY; - tags[2] = PWNHubTags.LOAN_OFFER; - tags[3] = PWNHubTags.SIMPLE_LOAN_TERMS_FACTORY; - tags[4] = PWNHubTags.LOAN_OFFER; - tags[5] = PWNHubTags.SIMPLE_LOAN_TERMS_FACTORY; - tags[6] = PWNHubTags.LOAN_REQUEST; + tags[1] = PWNHubTags.NONCE_MANAGER; + tags[2] = PWNHubTags.SIMPLE_LOAN_TERMS_FACTORY; + tags[3] = PWNHubTags.NONCE_MANAGER; + tags[4] = PWNHubTags.SIMPLE_LOAN_TERMS_FACTORY; + tags[5] = PWNHubTags.NONCE_MANAGER; + tags[6] = PWNHubTags.SIMPLE_LOAN_TERMS_FACTORY; + tags[7] = PWNHubTags.NONCE_MANAGER; bool success = GnosisSafeLike(protocolSafe).execTransaction({ to: address(hub), From 01b5dd441222dbe5a06207739a9b6df04c9721ec Mon Sep 17 00:00:00 2001 From: ashhanai Date: Fri, 1 Mar 2024 13:18:05 +0100 Subject: [PATCH 032/129] test(shared-nonces): update deployment test with shared revoked nonce contract --- deployments.json | 195 +++++++++++++++---------------- src/Deployments.sol | 33 +++--- src/hub/PWNHubTags.sol | 8 +- test/helper/DeploymentTest.t.sol | 45 +++---- 4 files changed, 132 insertions(+), 149 deletions(-) diff --git a/deployments.json b/deployments.json index 740a639..0552851 100644 --- a/deployments.json +++ b/deployments.json @@ -10,16 +10,15 @@ "feeCollector": "0xd56635c0E91D31F88B89F195D3993a9e34516e59", "deployerSafe": "0x42Cad20c964067f8e8b5c3E13fd0aa3C20a964C4", "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", - "configSingleton": "0x9926651f452ac52c851Ca91c4c79C2B3CdF4F7cD", - "config": "0x03DeAfC9678ab25F059df59Be3B20875018e1d46", + "configSingleton": "0x0000000000000000000000000000000000000000", + "config": "0x0000000000000000000000000000000000000000", "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", - "revokedOfferNonce": "0xFFa73Eacce930BBd92a1Ef218400cBd1036c437e", - "revokedRequestNonce": "0x472361E75d28597b0a7F86146fbB4a86f173d10D", - "simpleLoan": "0x57c88D78f6D08b5c88b4A3b7BbB0C1AA34c3280A", - "simpleLoanSimpleOffer": "0x5E551f09b8d1353075A1FF3B484Ee688aCAc02F6", - "simpleLoanListOffer": "0xDA027058708961Be3676daEB68Fde1758B210065", - "simpleLoanSimpleRequest": "0x9Cb87eC6448299aBc326F32d60E191Ef32Ab225D", + "revokedNonce": "0x0000000000000000000000000000000000000000", + "simpleLoan": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", + "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", "categoryRegistry": "0x0000000000000000000000000000000000000000" }, "5": { @@ -31,16 +30,15 @@ "feeCollector": "0xd56635c0E91D31F88B89F195D3993a9e34516e59", "deployerSafe": "0x42Cad20c964067f8e8b5c3E13fd0aa3C20a964C4", "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", - "configSingleton": "0x9926651f452ac52c851Ca91c4c79C2B3CdF4F7cD", - "config": "0x03DeAfC9678ab25F059df59Be3B20875018e1d46", + "configSingleton": "0x0000000000000000000000000000000000000000", + "config": "0x0000000000000000000000000000000000000000", "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", - "revokedOfferNonce": "0xFFa73Eacce930BBd92a1Ef218400cBd1036c437e", - "revokedRequestNonce": "0x472361E75d28597b0a7F86146fbB4a86f173d10D", - "simpleLoan": "0x57c88D78f6D08b5c88b4A3b7BbB0C1AA34c3280A", - "simpleLoanSimpleOffer": "0x5E551f09b8d1353075A1FF3B484Ee688aCAc02F6", - "simpleLoanListOffer": "0xDA027058708961Be3676daEB68Fde1758B210065", - "simpleLoanSimpleRequest": "0x9Cb87eC6448299aBc326F32d60E191Ef32Ab225D", + "revokedNonce": "0x0000000000000000000000000000000000000000", + "simpleLoan": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", + "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", "categoryRegistry": "0x0000000000000000000000000000000000000000" }, "10": { @@ -52,16 +50,15 @@ "feeCollector": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", "deployerSafe": "0x1B4B37738De3bb9E6a7a4f99aFe4C145734c071d", "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", - "configSingleton": "0x9926651f452ac52c851Ca91c4c79C2B3CdF4F7cD", - "config": "0x55e6A9F4183CFC01ecE8f2258FC13b93e1B6c140", + "configSingleton": "0x0000000000000000000000000000000000000000", + "config": "0x0000000000000000000000000000000000000000", "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", - "revokedOfferNonce": "0xFFa73Eacce930BBd92a1Ef218400cBd1036c437e", - "revokedRequestNonce": "0x472361E75d28597b0a7F86146fbB4a86f173d10D", - "simpleLoan": "0x4188C513fd94B0458715287570c832d9560bc08a", - "simpleLoanSimpleOffer": "0x5E551f09b8d1353075A1FF3B484Ee688aCAc02F6", - "simpleLoanListOffer": "0xDA027058708961Be3676daEB68Fde1758B210065", - "simpleLoanSimpleRequest": "0x9Cb87eC6448299aBc326F32d60E191Ef32Ab225D", + "revokedNonce": "0x0000000000000000000000000000000000000000", + "simpleLoan": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", + "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", "categoryRegistry": "0x0000000000000000000000000000000000000000" }, "25": { @@ -73,16 +70,15 @@ "feeCollector": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", "deployerSafe": "0x1B4B37738De3bb9E6a7a4f99aFe4C145734c071d", "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", - "configSingleton": "0x9926651f452ac52c851Ca91c4c79C2B3CdF4F7cD", - "config": "0x55e6A9F4183CFC01ecE8f2258FC13b93e1B6c140", + "configSingleton": "0x0000000000000000000000000000000000000000", + "config": "0x0000000000000000000000000000000000000000", "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", - "revokedOfferNonce": "0xFFa73Eacce930BBd92a1Ef218400cBd1036c437e", - "revokedRequestNonce": "0x472361E75d28597b0a7F86146fbB4a86f173d10D", - "simpleLoan": "0x4188C513fd94B0458715287570c832d9560bc08a", - "simpleLoanSimpleOffer": "0x5E551f09b8d1353075A1FF3B484Ee688aCAc02F6", - "simpleLoanListOffer": "0xDA027058708961Be3676daEB68Fde1758B210065", - "simpleLoanSimpleRequest": "0x9Cb87eC6448299aBc326F32d60E191Ef32Ab225D", + "revokedNonce": "0x0000000000000000000000000000000000000000", + "simpleLoan": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", + "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", "categoryRegistry": "0x0000000000000000000000000000000000000000" }, "56": { @@ -94,16 +90,15 @@ "feeCollector": "0xd56635c0E91D31F88B89F195D3993a9e34516e59", "deployerSafe": "0x42Cad20c964067f8e8b5c3E13fd0aa3C20a964C4", "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", - "configSingleton": "0x9926651f452ac52c851Ca91c4c79C2B3CdF4F7cD", - "config": "0x03DeAfC9678ab25F059df59Be3B20875018e1d46", + "configSingleton": "0x0000000000000000000000000000000000000000", + "config": "0x0000000000000000000000000000000000000000", "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", - "revokedOfferNonce": "0xFFa73Eacce930BBd92a1Ef218400cBd1036c437e", - "revokedRequestNonce": "0x472361E75d28597b0a7F86146fbB4a86f173d10D", - "simpleLoan": "0x57c88D78f6D08b5c88b4A3b7BbB0C1AA34c3280A", - "simpleLoanSimpleOffer": "0x5E551f09b8d1353075A1FF3B484Ee688aCAc02F6", - "simpleLoanListOffer": "0xDA027058708961Be3676daEB68Fde1758B210065", - "simpleLoanSimpleRequest": "0x9Cb87eC6448299aBc326F32d60E191Ef32Ab225D", + "revokedNonce": "0x0000000000000000000000000000000000000000", + "simpleLoan": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", + "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", "categoryRegistry": "0x0000000000000000000000000000000000000000" }, "137": { @@ -115,16 +110,15 @@ "feeCollector": "0xd56635c0E91D31F88B89F195D3993a9e34516e59", "deployerSafe": "0x42Cad20c964067f8e8b5c3E13fd0aa3C20a964C4", "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", - "configSingleton": "0x9926651f452ac52c851Ca91c4c79C2B3CdF4F7cD", - "config": "0x03DeAfC9678ab25F059df59Be3B20875018e1d46", + "configSingleton": "0x0000000000000000000000000000000000000000", + "config": "0x0000000000000000000000000000000000000000", "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", - "revokedOfferNonce": "0xFFa73Eacce930BBd92a1Ef218400cBd1036c437e", - "revokedRequestNonce": "0x472361E75d28597b0a7F86146fbB4a86f173d10D", - "simpleLoan": "0x57c88D78f6D08b5c88b4A3b7BbB0C1AA34c3280A", - "simpleLoanSimpleOffer": "0x5E551f09b8d1353075A1FF3B484Ee688aCAc02F6", - "simpleLoanListOffer": "0xDA027058708961Be3676daEB68Fde1758B210065", - "simpleLoanSimpleRequest": "0x9Cb87eC6448299aBc326F32d60E191Ef32Ab225D", + "revokedNonce": "0x0000000000000000000000000000000000000000", + "simpleLoan": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", + "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", "categoryRegistry": "0x0000000000000000000000000000000000000000" }, "338": { @@ -136,16 +130,15 @@ "feeCollector": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", "deployerSafe": "0x0000000000000000000000000000000000000000", "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", - "configSingleton": "0x9926651f452ac52c851Ca91c4c79C2B3CdF4F7cD", - "config": "0x55e6A9F4183CFC01ecE8f2258FC13b93e1B6c140", + "configSingleton": "0x0000000000000000000000000000000000000000", + "config": "0x0000000000000000000000000000000000000000", "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", - "revokedOfferNonce": "0xFFa73Eacce930BBd92a1Ef218400cBd1036c437e", - "revokedRequestNonce": "0x472361E75d28597b0a7F86146fbB4a86f173d10D", - "simpleLoan": "0x4188C513fd94B0458715287570c832d9560bc08a", - "simpleLoanSimpleOffer": "0x5E551f09b8d1353075A1FF3B484Ee688aCAc02F6", - "simpleLoanListOffer": "0xDA027058708961Be3676daEB68Fde1758B210065", - "simpleLoanSimpleRequest": "0x9Cb87eC6448299aBc326F32d60E191Ef32Ab225D", + "revokedNonce": "0x0000000000000000000000000000000000000000", + "simpleLoan": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", + "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", "categoryRegistry": "0x0000000000000000000000000000000000000000" }, "5000": { @@ -157,16 +150,15 @@ "feeCollector": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", "deployerSafe": "0x1B4B37738De3bb9E6a7a4f99aFe4C145734c071d", "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", - "configSingleton": "0x9926651f452ac52c851Ca91c4c79C2B3CdF4F7cD", - "config": "0x55e6A9F4183CFC01ecE8f2258FC13b93e1B6c140", + "configSingleton": "0x0000000000000000000000000000000000000000", + "config": "0x0000000000000000000000000000000000000000", "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", - "revokedOfferNonce": "0xFFa73Eacce930BBd92a1Ef218400cBd1036c437e", - "revokedRequestNonce": "0x472361E75d28597b0a7F86146fbB4a86f173d10D", - "simpleLoan": "0x4188C513fd94B0458715287570c832d9560bc08a", - "simpleLoanSimpleOffer": "0x5E551f09b8d1353075A1FF3B484Ee688aCAc02F6", - "simpleLoanListOffer": "0xDA027058708961Be3676daEB68Fde1758B210065", - "simpleLoanSimpleRequest": "0x9Cb87eC6448299aBc326F32d60E191Ef32Ab225D", + "revokedNonce": "0x0000000000000000000000000000000000000000", + "simpleLoan": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", + "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", "categoryRegistry": "0x0000000000000000000000000000000000000000" }, "5001": { @@ -178,16 +170,15 @@ "feeCollector": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", "deployerSafe": "0x0000000000000000000000000000000000000000", "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", - "configSingleton": "0x9926651f452ac52c851Ca91c4c79C2B3CdF4F7cD", - "config": "0x55e6A9F4183CFC01ecE8f2258FC13b93e1B6c140", + "configSingleton": "0x0000000000000000000000000000000000000000", + "config": "0x0000000000000000000000000000000000000000", "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", - "revokedOfferNonce": "0xFFa73Eacce930BBd92a1Ef218400cBd1036c437e", - "revokedRequestNonce": "0x472361E75d28597b0a7F86146fbB4a86f173d10D", - "simpleLoan": "0x4188C513fd94B0458715287570c832d9560bc08a", - "simpleLoanSimpleOffer": "0x5E551f09b8d1353075A1FF3B484Ee688aCAc02F6", - "simpleLoanListOffer": "0xDA027058708961Be3676daEB68Fde1758B210065", - "simpleLoanSimpleRequest": "0x9Cb87eC6448299aBc326F32d60E191Ef32Ab225D", + "revokedNonce": "0x0000000000000000000000000000000000000000", + "simpleLoan": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", + "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", "categoryRegistry": "0x0000000000000000000000000000000000000000" }, "8453": { @@ -199,16 +190,15 @@ "feeCollector": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", "deployerSafe": "0x1B4B37738De3bb9E6a7a4f99aFe4C145734c071d", "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", - "configSingleton": "0x9926651f452ac52c851Ca91c4c79C2B3CdF4F7cD", - "config": "0x55e6A9F4183CFC01ecE8f2258FC13b93e1B6c140", + "configSingleton": "0x0000000000000000000000000000000000000000", + "config": "0x0000000000000000000000000000000000000000", "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", - "revokedOfferNonce": "0xFFa73Eacce930BBd92a1Ef218400cBd1036c437e", - "revokedRequestNonce": "0x472361E75d28597b0a7F86146fbB4a86f173d10D", - "simpleLoan": "0x4188C513fd94B0458715287570c832d9560bc08a", - "simpleLoanSimpleOffer": "0x5E551f09b8d1353075A1FF3B484Ee688aCAc02F6", - "simpleLoanListOffer": "0xDA027058708961Be3676daEB68Fde1758B210065", - "simpleLoanSimpleRequest": "0x9Cb87eC6448299aBc326F32d60E191Ef32Ab225D", + "revokedNonce": "0x0000000000000000000000000000000000000000", + "simpleLoan": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", + "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", "categoryRegistry": "0x0000000000000000000000000000000000000000" }, "42161": { @@ -220,16 +210,15 @@ "feeCollector": "0xd56635c0E91D31F88B89F195D3993a9e34516e59", "deployerSafe": "0x42Cad20c964067f8e8b5c3E13fd0aa3C20a964C4", "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", - "configSingleton": "0x9926651f452ac52c851Ca91c4c79C2B3CdF4F7cD", - "config": "0x03DeAfC9678ab25F059df59Be3B20875018e1d46", + "configSingleton": "0x0000000000000000000000000000000000000000", + "config": "0x0000000000000000000000000000000000000000", "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", - "revokedOfferNonce": "0xFFa73Eacce930BBd92a1Ef218400cBd1036c437e", - "revokedRequestNonce": "0x472361E75d28597b0a7F86146fbB4a86f173d10D", - "simpleLoan": "0x57c88D78f6D08b5c88b4A3b7BbB0C1AA34c3280A", - "simpleLoanSimpleOffer": "0x5E551f09b8d1353075A1FF3B484Ee688aCAc02F6", - "simpleLoanListOffer": "0xDA027058708961Be3676daEB68Fde1758B210065", - "simpleLoanSimpleRequest": "0x9Cb87eC6448299aBc326F32d60E191Ef32Ab225D", + "revokedNonce": "0x0000000000000000000000000000000000000000", + "simpleLoan": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", + "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", "categoryRegistry": "0x0000000000000000000000000000000000000000" }, "84531": { @@ -241,16 +230,15 @@ "feeCollector": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", "deployerSafe": "0x0000000000000000000000000000000000000000", "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", - "configSingleton": "0x9926651f452ac52c851Ca91c4c79C2B3CdF4F7cD", - "config": "0x55e6A9F4183CFC01ecE8f2258FC13b93e1B6c140", + "configSingleton": "0x0000000000000000000000000000000000000000", + "config": "0x0000000000000000000000000000000000000000", "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", - "revokedOfferNonce": "0xFFa73Eacce930BBd92a1Ef218400cBd1036c437e", - "revokedRequestNonce": "0x472361E75d28597b0a7F86146fbB4a86f173d10D", - "simpleLoan": "0x4188C513fd94B0458715287570c832d9560bc08a", - "simpleLoanSimpleOffer": "0x5E551f09b8d1353075A1FF3B484Ee688aCAc02F6", - "simpleLoanListOffer": "0xDA027058708961Be3676daEB68Fde1758B210065", - "simpleLoanSimpleRequest": "0x9Cb87eC6448299aBc326F32d60E191Ef32Ab225D", + "revokedNonce": "0x0000000000000000000000000000000000000000", + "simpleLoan": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", + "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", "categoryRegistry": "0x0000000000000000000000000000000000000000" }, "11155111": { @@ -262,16 +250,15 @@ "feeCollector": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", "deployerSafe": "0x1B4B37738De3bb9E6a7a4f99aFe4C145734c071d", "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", - "configSingleton": "0x9926651f452ac52c851Ca91c4c79C2B3CdF4F7cD", - "config": "0x55e6A9F4183CFC01ecE8f2258FC13b93e1B6c140", + "configSingleton": "0x0000000000000000000000000000000000000000", + "config": "0x0000000000000000000000000000000000000000", "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", - "revokedOfferNonce": "0xFFa73Eacce930BBd92a1Ef218400cBd1036c437e", - "revokedRequestNonce": "0x472361E75d28597b0a7F86146fbB4a86f173d10D", - "simpleLoan": "0x4188C513fd94B0458715287570c832d9560bc08a", - "simpleLoanSimpleOffer": "0x5E551f09b8d1353075A1FF3B484Ee688aCAc02F6", - "simpleLoanListOffer": "0xDA027058708961Be3676daEB68Fde1758B210065", - "simpleLoanSimpleRequest": "0x9Cb87eC6448299aBc326F32d60E191Ef32Ab225D", + "revokedNonce": "0x0000000000000000000000000000000000000000", + "simpleLoan": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", + "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", "categoryRegistry": "0x0000000000000000000000000000000000000000" } } diff --git a/src/Deployments.sol b/src/Deployments.sol index a3e51e5..cb2fe86 100644 --- a/src/Deployments.sol +++ b/src/Deployments.sol @@ -4,20 +4,20 @@ pragma solidity 0.8.16; import "forge-std/StdJson.sol"; import "forge-std/Base.sol"; -import "MultiToken/interfaces/IMultiTokenCategoryRegistry.sol"; +import { IMultiTokenCategoryRegistry } from "MultiToken/interfaces/IMultiTokenCategoryRegistry.sol"; -import "openzeppelin-contracts/contracts/utils/Strings.sol"; +import { Strings } from "openzeppelin-contracts/contracts/utils/Strings.sol"; -import "@pwn/config/PWNConfig.sol"; -import "@pwn/deployer/IPWNDeployer.sol"; -import "@pwn/hub/PWNHub.sol"; -import "@pwn/hub/PWNHubTags.sol"; -import "@pwn/loan/terms/simple/loan/PWNSimpleLoan.sol"; -import "@pwn/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol"; -import "@pwn/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol"; -import "@pwn/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol"; -import "@pwn/loan/token/PWNLOAN.sol"; -import "@pwn/nonce/PWNRevokedNonce.sol"; +import { PWNConfig } from "@pwn/config/PWNConfig.sol"; +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/factory/offer/PWNSimpleLoanListOffer.sol"; +import { PWNSimpleLoanSimpleOffer } from "@pwn/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol"; +import { PWNSimpleLoanSimpleRequest } from "@pwn/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol"; +import { PWNLOAN } from "@pwn/loan/token/PWNLOAN.sol"; +import { PWNRevokedNonce } from "@pwn/nonce/PWNRevokedNonce.sol"; abstract contract Deployments is CommonBase { @@ -42,8 +42,7 @@ abstract contract Deployments is CommonBase { address productTimelock; address protocolSafe; address protocolTimelock; - PWNRevokedNonce revokedOfferNonce; - PWNRevokedNonce revokedRequestNonce; + PWNRevokedNonce revokedNonce; PWNSimpleLoan simpleLoan; PWNSimpleLoanListOffer simpleLoanListOffer; PWNSimpleLoanSimpleOffer simpleLoanSimpleOffer; @@ -68,8 +67,7 @@ abstract contract Deployments is CommonBase { PWNConfig config; PWNLOAN loanToken; PWNSimpleLoan simpleLoan; - PWNRevokedNonce revokedOfferNonce; - PWNRevokedNonce revokedRequestNonce; + PWNRevokedNonce revokedNonce; PWNSimpleLoanSimpleOffer simpleLoanSimpleOffer; PWNSimpleLoanListOffer simpleLoanListOffer; PWNSimpleLoanSimpleRequest simpleLoanSimpleRequest; @@ -99,8 +97,7 @@ abstract contract Deployments is CommonBase { config = deployment.config; loanToken = deployment.loanToken; simpleLoan = deployment.simpleLoan; - revokedOfferNonce = deployment.revokedOfferNonce; - revokedRequestNonce = deployment.revokedRequestNonce; + revokedNonce = deployment.revokedNonce; simpleLoanSimpleOffer = deployment.simpleLoanSimpleOffer; simpleLoanListOffer = deployment.simpleLoanListOffer; simpleLoanSimpleRequest = deployment.simpleLoanSimpleRequest; diff --git a/src/hub/PWNHubTags.sol b/src/hub/PWNHubTags.sol index 9845cce..be22bca 100644 --- a/src/hub/PWNHubTags.sol +++ b/src/hub/PWNHubTags.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.16; library PWNHubTags { - string internal constant VERSION = "1.0"; + string internal constant VERSION = "1.2"; /// @dev Address can mint LOAN tokens and create LOANs via loan factory contracts. bytes32 internal constant ACTIVE_LOAN = keccak256("PWN_ACTIVE_LOAN"); @@ -11,9 +11,7 @@ library PWNHubTags { /// @dev Address can be used as a loan terms factory for creating simple loans. bytes32 internal constant SIMPLE_LOAN_TERMS_FACTORY = keccak256("PWN_SIMPLE_LOAN_TERMS_FACTORY"); - /// @dev Address can revoke loan request nonces. - bytes32 internal constant LOAN_REQUEST = keccak256("PWN_LOAN_REQUEST"); - /// @dev Address can revoke loan offer nonces. - bytes32 internal constant LOAN_OFFER = keccak256("PWN_LOAN_OFFER"); + /// @dev Address can revoke nonces on other addresses behalf. + bytes32 internal constant NONCE_MANAGER = keccak256("PWN_NONCE_MANAGER"); } diff --git a/test/helper/DeploymentTest.t.sol b/test/helper/DeploymentTest.t.sol index bc59937..ec1c5fb 100644 --- a/test/helper/DeploymentTest.t.sol +++ b/test/helper/DeploymentTest.t.sol @@ -3,10 +3,10 @@ pragma solidity 0.8.16; import "forge-std/Test.sol"; -import "MultiToken/MultiTokenCategoryRegistry.sol"; -import "MultiToken/interfaces/IMultiTokenCategoryRegistry.sol"; +import { MultiTokenCategoryRegistry, IMultiTokenCategoryRegistry } from "MultiToken/MultiTokenCategoryRegistry.sol"; -import "openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import { TransparentUpgradeableProxy } + from "openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; import "@pwn/Deployments.sol"; @@ -38,36 +38,37 @@ abstract contract DeploymentTest is Deployments, Test { vm.prank(protocolSafe); hub = new PWNHub(); + revokedNonce = new PWNRevokedNonce(address(hub), PWNHubTags.NONCE_MANAGER); + loanToken = new PWNLOAN(address(hub)); - simpleLoan = new PWNSimpleLoan( // todo: - address(hub), address(loanToken), address(config), address(0), address(categoryRegistry) + simpleLoan = new PWNSimpleLoan( + address(hub), address(loanToken), address(config), address(revokedNonce), address(categoryRegistry) ); - revokedOfferNonce = new PWNRevokedNonce(address(hub), PWNHubTags.LOAN_OFFER); - simpleLoanSimpleOffer = new PWNSimpleLoanSimpleOffer(address(hub), address(revokedOfferNonce)); - simpleLoanListOffer = new PWNSimpleLoanListOffer(address(hub), address(revokedOfferNonce)); - - revokedRequestNonce = new PWNRevokedNonce(address(hub), PWNHubTags.LOAN_REQUEST); - simpleLoanSimpleRequest = new PWNSimpleLoanSimpleRequest(address(hub), address(revokedRequestNonce)); + simpleLoanSimpleOffer = new PWNSimpleLoanSimpleOffer(address(hub), address(revokedNonce)); + simpleLoanListOffer = new PWNSimpleLoanListOffer(address(hub), address(revokedNonce)); + simpleLoanSimpleRequest = new PWNSimpleLoanSimpleRequest(address(hub), address(revokedNonce)); // Set hub tags - address[] memory addrs = new address[](7); + address[] memory addrs = new address[](8); addrs[0] = address(simpleLoan); - addrs[1] = address(simpleLoanSimpleOffer); + addrs[1] = address(simpleLoan); addrs[2] = address(simpleLoanSimpleOffer); - addrs[3] = address(simpleLoanListOffer); + addrs[3] = address(simpleLoanSimpleOffer); addrs[4] = address(simpleLoanListOffer); - addrs[5] = address(simpleLoanSimpleRequest); + addrs[5] = address(simpleLoanListOffer); addrs[6] = address(simpleLoanSimpleRequest); + addrs[7] = address(simpleLoanSimpleRequest); - bytes32[] memory tags = new bytes32[](7); + bytes32[] memory tags = new bytes32[](8); tags[0] = PWNHubTags.ACTIVE_LOAN; - tags[1] = PWNHubTags.SIMPLE_LOAN_TERMS_FACTORY; - tags[2] = PWNHubTags.LOAN_OFFER; - tags[3] = PWNHubTags.SIMPLE_LOAN_TERMS_FACTORY; - tags[4] = PWNHubTags.LOAN_OFFER; - tags[5] = PWNHubTags.SIMPLE_LOAN_TERMS_FACTORY; - tags[6] = PWNHubTags.LOAN_REQUEST; + tags[1] = PWNHubTags.NONCE_MANAGER; + tags[2] = PWNHubTags.SIMPLE_LOAN_TERMS_FACTORY; + tags[3] = PWNHubTags.NONCE_MANAGER; + tags[4] = PWNHubTags.SIMPLE_LOAN_TERMS_FACTORY; + tags[5] = PWNHubTags.NONCE_MANAGER; + tags[6] = PWNHubTags.SIMPLE_LOAN_TERMS_FACTORY; + tags[7] = PWNHubTags.NONCE_MANAGER; vm.prank(protocolSafe); hub.setTags(addrs, tags, true); From 5efacebbb79bf45d6a9b196e48333fc91a13057c Mon Sep 17 00:00:00 2001 From: ashhanai Date: Mon, 4 Mar 2024 10:45:17 +0100 Subject: [PATCH 033/129] fix(script): pass category registry to simple loan constructor --- script/PWN.s.sol | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/script/PWN.s.sol b/script/PWN.s.sol index 46ff2db..919d814 100644 --- a/script/PWN.s.sol +++ b/script/PWN.s.sol @@ -94,7 +94,7 @@ forge script script/PWN.s.sol:Deploy \ --verify --etherscan-api-key $ETHERSCAN_API_KEY \ --broadcast */ - /// @dev Expecting to have deployer, deployerSafe, protocolSafe, daoSafe & feeCollector addresses set in the `deployments.json` + /// @dev Expecting to have deployer, deployerSafe, protocolSafe, daoSafe, feeCollector & categoryRegistry addresses set in the `deployments.json` function deployProtocol() external { _loadDeployedAddresses(); @@ -103,6 +103,7 @@ forge script script/PWN.s.sol:Deploy \ require(protocolSafe != address(0), "Protocol safe not set"); require(daoSafe != address(0), "DAO safe not set"); require(feeCollector != address(0), "Fee collector not set"); + require(address(categoryRegistry) != address(0), "Category registry not set"); uint256 initialConfigHelper = vmSafe.envUint("INITIAL_CONFIG_HELPER"); @@ -161,7 +162,13 @@ forge script script/PWN.s.sol:Deploy \ salt: PWNContractDeployerSalt.SIMPLE_LOAN, bytecode: abi.encodePacked( type(PWNSimpleLoan).creationCode, - abi.encode(address(hub), address(loanToken), address(revokedNonce), address(config)) + abi.encode( + address(hub), + address(loanToken), + address(config), + address(revokedNonce), + address(categoryRegistry) + ) ) })); From 5cbe8f8abf7367ea780e14012e1853ad92dce846 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Wed, 6 Mar 2024 13:13:56 +0100 Subject: [PATCH 034/129] feat(state-fingerprint-computer-registry): implement state fingerprint computer registry --- .../StateFingerprintComputerRegistry.sol | 57 +++++++++ .../StateFingerprintComputerRegistry.t.sol | 121 ++++++++++++++++++ 2 files changed, 178 insertions(+) create mode 100644 src/state-fingerprint/StateFingerprintComputerRegistry.sol create mode 100644 test/unit/StateFingerprintComputerRegistry.t.sol diff --git a/src/state-fingerprint/StateFingerprintComputerRegistry.sol b/src/state-fingerprint/StateFingerprintComputerRegistry.sol new file mode 100644 index 0000000..d037023 --- /dev/null +++ b/src/state-fingerprint/StateFingerprintComputerRegistry.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.16; + +import { Ownable2Step } from "openzeppelin-contracts/contracts/access/Ownable2Step.sol"; +import { ERC165Checker } from "openzeppelin-contracts/contracts/utils/introspection/ERC165Checker.sol"; + +import { IERC5646 } from "@pwn/loan/token/IERC5646.sol"; + + +/** + * @title State Fingerprint Computer Registry + * @notice Registry for state fingerprint computers. + * @dev The computers are used to calculate the state fingerprint of an asset. + * It can be a dedicated contract or the asset itself if it implements the IERC5646 interface. + */ +contract StateFingerprintComputerRegistry is Ownable2Step { + + /** + * @notice Error emitted when registering a computer which does not implement the IERC5646 interface. + */ + error InvalidComputerContract(); + + /** + * @notice Mapping holding registered computer to an asset. + * @dev Only owner can update the mapping. + */ + mapping (address => address) private _computerRegistry; + + /** + * @notice Returns the ERC5646 computer for a given asset. + * @param asset The asset for which the computer is requested. + * @return The computer for the given asset. + */ + function getStateFingerprintComputer(address asset) external view returns (IERC5646) { + address computer = _computerRegistry[asset]; + if (computer == address(0)) + if (ERC165Checker.supportsInterface(asset, type(IERC5646).interfaceId)) + computer = asset; + + return IERC5646(computer); + } + + /** + * @notice Registers a state fingerprint computer for a given asset. + * @dev Only owner can register a computer. Computer can be set to address(0) to remove the computer. + * @param asset The asset for which the computer is registered. + * @param computer The computer to be registered. + */ + function registerStateFingerprintComputer(address asset, address computer) external onlyOwner { + if (computer != address(0)) + if (!ERC165Checker.supportsInterface(computer, type(IERC5646).interfaceId)) + revert InvalidComputerContract(); + + _computerRegistry[asset] = computer; + } + +} diff --git a/test/unit/StateFingerprintComputerRegistry.t.sol b/test/unit/StateFingerprintComputerRegistry.t.sol new file mode 100644 index 0000000..0e5d6b2 --- /dev/null +++ b/test/unit/StateFingerprintComputerRegistry.t.sol @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.16; + +import "forge-std/Test.sol"; + +import { IERC165 } from "openzeppelin-contracts/contracts/utils/introspection/IERC165.sol"; + +import { StateFingerprintComputerRegistry, IERC5646 } from "@pwn/state-fingerprint/StateFingerprintComputerRegistry.sol"; + + +abstract contract StateFingerprintComputerRegistryTest is Test { + + bytes32 internal constant OWNER_SLOT = bytes32(uint256(0)); + bytes32 internal constant REGISTRY_SLOT = bytes32(uint256(2)); + + address owner = makeAddr("owner"); + StateFingerprintComputerRegistry registry; + + function setUp() external { + vm.prank(owner); + registry = new StateFingerprintComputerRegistry(); + } + + function _mockERC5646Support(address asset, bool result) internal { + _mockERC165Call(asset, type(IERC165).interfaceId, true); + _mockERC165Call(asset, hex"ffffffff", false); + _mockERC165Call(asset, type(IERC5646).interfaceId, result); + } + + function _mockERC165Call(address asset, bytes4 interfaceId, bool result) internal { + vm.mockCall( + asset, + abi.encodeWithSignature("supportsInterface(bytes4)", interfaceId), + abi.encode(result) + ); + } + +} + + +/*----------------------------------------------------------*| +|* # GET STATE FINGERPRINT COMPUTER *| +|*----------------------------------------------------------*/ + +contract StateFingerprintComputerRegistry_GetStateFingerprintComputer_Test is StateFingerprintComputerRegistryTest { + + function testFuzz_shouldReturnStoredComputer_whenIsRegistered(address asset, address computer) external { + bytes32 assetSlot = keccak256(abi.encode(asset, REGISTRY_SLOT)); + vm.store(address(registry), assetSlot, bytes32(uint256(uint160(computer)))); + + assertEq(address(registry.getStateFingerprintComputer(asset)), computer); + } + + function testFuzz_shouldReturnAsset_whenComputerIsNotRegistered_whenAssetImplementsERC5646(address asset) external { + assumeAddressIsNot(asset, AddressType.ForgeAddress, AddressType.Precompile); + + _mockERC5646Support(asset, true); + + assertEq(address(registry.getStateFingerprintComputer(asset)), asset); + } + + function testFuzz_shouldReturnZeroAddress_whenComputerIsNotRegistered_whenAssetNotImplementsERC5646(address asset) external { + assertEq(address(registry.getStateFingerprintComputer(asset)), address(0)); + } + +} + + +/*----------------------------------------------------------*| +|* # REGISTER STATE FINGERPRINT COMPUTER *| +|*----------------------------------------------------------*/ + +contract StateFingerprintComputerRegistry_RegisterStateFingerprintComputer_Test is StateFingerprintComputerRegistryTest { + + function testFuzz_shouldFail_whenCallerIsNotOwner(address caller) external { + vm.assume(caller != owner); + + vm.expectRevert("Ownable: caller is not the owner"); + vm.prank(caller); + registry.registerStateFingerprintComputer(address(0), address(0)); + } + + function testFuzz_shouldUnregisterComputer_whenComputerIsZeroAddress(address asset) external { + address computer = makeAddr("computer"); + bytes32 assetSlot = keccak256(abi.encode(asset, REGISTRY_SLOT)); + vm.store(address(registry), assetSlot, bytes32(uint256(uint160(computer)))); + + vm.prank(owner); + registry.registerStateFingerprintComputer(asset, address(0)); + + assertEq(address(registry.getStateFingerprintComputer(asset)), address(0)); + } + + function testFuzz_shouldFail_whenComputerDoesNotImplementERC165(address asset, address computer) external { + assumeAddressIsNot(computer, AddressType.ForgeAddress, AddressType.Precompile, AddressType.ZeroAddress); + + vm.expectRevert(abi.encodeWithSelector(StateFingerprintComputerRegistry.InvalidComputerContract.selector)); + vm.prank(owner); + registry.registerStateFingerprintComputer(asset, computer); + } + + function testFuzz_shouldFail_whenComputerDoesNotImplementERC5646(address asset, address computer) external { + assumeAddressIsNot(computer, AddressType.ForgeAddress, AddressType.Precompile, AddressType.ZeroAddress); + _mockERC5646Support(computer, false); + + vm.expectRevert(abi.encodeWithSelector(StateFingerprintComputerRegistry.InvalidComputerContract.selector)); + vm.prank(owner); + registry.registerStateFingerprintComputer(asset, computer); + } + + function testFuzz_shouldRegisterComputer(address asset, address computer) external { + assumeAddressIsNot(computer, AddressType.ForgeAddress, AddressType.Precompile, AddressType.ZeroAddress); + _mockERC5646Support(computer, true); + + vm.prank(owner); + registry.registerStateFingerprintComputer(asset, computer); + + assertEq(address(registry.getStateFingerprintComputer(asset)), computer); + } + +} From b11d992b38d3bfd74e2d97aef06546c4447e0920 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Wed, 6 Mar 2024 13:19:28 +0100 Subject: [PATCH 035/129] feat(state-fingerprint): extend offers and request by an optional collateral state fingerprint that is checked on loan terms creation --- deployments.json | 13 +++ src/Deployments.sol | 4 + src/PWNErrors.sol | 4 + .../factory/offer/PWNSimpleLoanListOffer.sol | 62 ++++++++++----- .../offer/PWNSimpleLoanSimpleOffer.sol | 62 ++++++++++----- .../request/PWNSimpleLoanSimpleRequest.sol | 62 ++++++++++----- test/helper/DeploymentTest.t.sol | 6 +- test/unit/PWNSimpleLoanListOffer.t.sol | 79 ++++++++++++++++++- test/unit/PWNSimpleLoanSimpleOffer.t.sol | 79 ++++++++++++++++++- test/unit/PWNSimpleLoanSimpleRequest.t.sol | 79 ++++++++++++++++++- 10 files changed, 381 insertions(+), 69 deletions(-) diff --git a/deployments.json b/deployments.json index 0552851..184f569 100644 --- a/deployments.json +++ b/deployments.json @@ -19,6 +19,7 @@ "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", + "stateFingerprintComputerRegistry": "0x0000000000000000000000000000000000000000", "categoryRegistry": "0x0000000000000000000000000000000000000000" }, "5": { @@ -39,6 +40,7 @@ "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", + "stateFingerprintComputerRegistry": "0x0000000000000000000000000000000000000000", "categoryRegistry": "0x0000000000000000000000000000000000000000" }, "10": { @@ -59,6 +61,7 @@ "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", + "stateFingerprintComputerRegistry": "0x0000000000000000000000000000000000000000", "categoryRegistry": "0x0000000000000000000000000000000000000000" }, "25": { @@ -79,6 +82,7 @@ "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", + "stateFingerprintComputerRegistry": "0x0000000000000000000000000000000000000000", "categoryRegistry": "0x0000000000000000000000000000000000000000" }, "56": { @@ -99,6 +103,7 @@ "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", + "stateFingerprintComputerRegistry": "0x0000000000000000000000000000000000000000", "categoryRegistry": "0x0000000000000000000000000000000000000000" }, "137": { @@ -119,6 +124,7 @@ "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", + "stateFingerprintComputerRegistry": "0x0000000000000000000000000000000000000000", "categoryRegistry": "0x0000000000000000000000000000000000000000" }, "338": { @@ -139,6 +145,7 @@ "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", + "stateFingerprintComputerRegistry": "0x0000000000000000000000000000000000000000", "categoryRegistry": "0x0000000000000000000000000000000000000000" }, "5000": { @@ -159,6 +166,7 @@ "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", + "stateFingerprintComputerRegistry": "0x0000000000000000000000000000000000000000", "categoryRegistry": "0x0000000000000000000000000000000000000000" }, "5001": { @@ -179,6 +187,7 @@ "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", + "stateFingerprintComputerRegistry": "0x0000000000000000000000000000000000000000", "categoryRegistry": "0x0000000000000000000000000000000000000000" }, "8453": { @@ -199,6 +208,7 @@ "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", + "stateFingerprintComputerRegistry": "0x0000000000000000000000000000000000000000", "categoryRegistry": "0x0000000000000000000000000000000000000000" }, "42161": { @@ -219,6 +229,7 @@ "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", + "stateFingerprintComputerRegistry": "0x0000000000000000000000000000000000000000", "categoryRegistry": "0x0000000000000000000000000000000000000000" }, "84531": { @@ -239,6 +250,7 @@ "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", + "stateFingerprintComputerRegistry": "0x0000000000000000000000000000000000000000", "categoryRegistry": "0x0000000000000000000000000000000000000000" }, "11155111": { @@ -259,6 +271,7 @@ "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", + "stateFingerprintComputerRegistry": "0x0000000000000000000000000000000000000000", "categoryRegistry": "0x0000000000000000000000000000000000000000" } } diff --git a/src/Deployments.sol b/src/Deployments.sol index cb2fe86..ee36944 100644 --- a/src/Deployments.sol +++ b/src/Deployments.sol @@ -18,6 +18,7 @@ import { PWNSimpleLoanSimpleOffer } from "@pwn/loan/terms/simple/factory/offer/P import { PWNSimpleLoanSimpleRequest } from "@pwn/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol"; import { PWNLOAN } from "@pwn/loan/token/PWNLOAN.sol"; import { PWNRevokedNonce } from "@pwn/nonce/PWNRevokedNonce.sol"; +import { StateFingerprintComputerRegistry } from "@pwn/state-fingerprint/StateFingerprintComputerRegistry.sol"; abstract contract Deployments is CommonBase { @@ -47,6 +48,7 @@ abstract contract Deployments is CommonBase { PWNSimpleLoanListOffer simpleLoanListOffer; PWNSimpleLoanSimpleOffer simpleLoanSimpleOffer; PWNSimpleLoanSimpleRequest simpleLoanSimpleRequest; + StateFingerprintComputerRegistry stateFingerprintComputerRegistry; } address dao; @@ -60,6 +62,7 @@ abstract contract Deployments is CommonBase { address feeCollector; IMultiTokenCategoryRegistry categoryRegistry; + StateFingerprintComputerRegistry stateFingerprintComputerRegistry; IPWNDeployer deployer; PWNHub hub; @@ -101,6 +104,7 @@ abstract contract Deployments is CommonBase { simpleLoanSimpleOffer = deployment.simpleLoanSimpleOffer; simpleLoanListOffer = deployment.simpleLoanListOffer; simpleLoanSimpleRequest = deployment.simpleLoanSimpleRequest; + stateFingerprintComputerRegistry = deployment.stateFingerprintComputerRegistry; categoryRegistry = deployment.categoryRegistry; } else { _protocolNotDeployedOnSelectedChain(); diff --git a/src/PWNErrors.sol b/src/PWNErrors.sol index ea8ba49..fa29bcf 100644 --- a/src/PWNErrors.sol +++ b/src/PWNErrors.sol @@ -20,6 +20,10 @@ error InvalidExtensionCaller(); // Invalid asset error InvalidLoanAsset(); error InvalidCollateralAsset(); +error InvalidCollateralStateFingerprint(bytes32 offered, bytes32 current); + +// State fingerprint computer registry +error MissingStateFingerprintComputer(); // LOAN token error InvalidLoanContractCaller(); diff --git a/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol b/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol index 9a4ae7f..a68750c 100644 --- a/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol +++ b/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol @@ -6,8 +6,11 @@ import { MultiToken } from "MultiToken/MultiToken.sol"; import { MerkleProof } from "openzeppelin-contracts/contracts/utils/cryptography/MerkleProof.sol"; import { PWNSignatureChecker } from "@pwn/loan/lib/PWNSignatureChecker.sol"; -import { PWNSimpleLoanOffer, PWNSimpleLoanTermsFactory } from "@pwn/loan/terms/simple/factory/offer/base/PWNSimpleLoanOffer.sol"; +import { PWNSimpleLoanOffer, PWNSimpleLoanTermsFactory } + from "@pwn/loan/terms/simple/factory/offer/base/PWNSimpleLoanOffer.sol"; import { PWNLOANTerms } from "@pwn/loan/terms/PWNLOANTerms.sol"; +import { StateFingerprintComputerRegistry, IERC5646 } + from "@pwn/state-fingerprint/StateFingerprintComputerRegistry.sol"; import "@pwn/PWNErrors.sol"; @@ -28,17 +31,21 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanOffer { * @dev EIP-712 simple offer struct type hash. */ bytes32 public constant OFFER_TYPEHASH = keccak256( - "Offer(uint8 collateralCategory,address collateralAddress,bytes32 collateralIdsWhitelistMerkleRoot,uint256 collateralAmount,address loanAssetAddress,uint256 loanAmount,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedBorrower,address lender,bool isPersistent,uint256 nonceSpace,uint256 nonce)" + "Offer(uint8 collateralCategory,address collateralAddress,bytes32 collateralIdsWhitelistMerkleRoot,uint256 collateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address loanAssetAddress,uint256 loanAmount,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedBorrower,address lender,bool isPersistent,uint256 nonceSpace,uint256 nonce)" ); bytes32 public immutable DOMAIN_SEPARATOR; + StateFingerprintComputerRegistry public immutable stateFingerprintComputerRegistry; + /** * @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 loanAssetAddress Address of an asset which is lender to a borrower. * @param loanAmount Amount of tokens which is offered as a loan to a borrower. * @param fixedInterestAmount Fixed interest amount in loan asset tokens. It is the minimum amount of interest which has to be paid by a borrower. @@ -57,6 +64,8 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanOffer { address collateralAddress; bytes32 collateralIdsWhitelistMerkleRoot; uint256 collateralAmount; + bool checkCollateralStateFingerprint; + bytes32 collateralStateFingerprint; address loanAssetAddress; uint256 loanAmount; uint256 fixedInterestAmount; @@ -97,7 +106,11 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanOffer { |* # CONSTRUCTOR *| |*----------------------------------------------------------*/ - constructor(address hub, address _revokedOfferNonce) PWNSimpleLoanOffer(hub, _revokedOfferNonce) { + constructor( + address hub, + address revokedOfferNonce, + address _stateFingerprintComputerRegistry + ) PWNSimpleLoanOffer(hub, revokedOfferNonce) { DOMAIN_SEPARATOR = keccak256(abi.encode( keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), keccak256("PWNSimpleLoanListOffer"), @@ -105,6 +118,7 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanOffer { block.chainid, address(this) )); + stateFingerprintComputerRegistry = StateFingerprintComputerRegistry(_stateFingerprintComputerRegistry); } @@ -181,27 +195,39 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanOffer { revert CollateralIdIsNotWhitelisted(); } // else: Any collateral id - collection offer - // Prepare collateral and loan asset - MultiToken.Asset memory collateral = MultiToken.Asset({ - category: offer.collateralCategory, - assetAddress: offer.collateralAddress, - id: offerValues.collateralId, - amount: offer.collateralAmount - }); - MultiToken.Asset memory loanAsset = MultiToken.Asset({ - category: MultiToken.Category.ERC20, - assetAddress: offer.loanAssetAddress, - id: 0, - amount: offer.loanAmount - }); + // Check that the collateral state fingerprint matches the current state + if (offer.checkCollateralStateFingerprint) { + IERC5646 computer = stateFingerprintComputerRegistry.getStateFingerprintComputer(offer.collateralAddress); + if (address(computer) == address(0)) { + // Asset is not implementing ERC5646 and no computer is registered + revert MissingStateFingerprintComputer(); + } + + bytes32 currentFingerprint = computer.getStateFingerprint(offerValues.collateralId); + if (offer.collateralStateFingerprint != currentFingerprint) { + // Fingerprint mismatch + revert InvalidCollateralStateFingerprint({ + offered: offer.collateralStateFingerprint, + current: currentFingerprint + }); + } + } // Create loan terms object loanTerms = PWNLOANTerms.Simple({ lender: lender, borrower: borrower, defaultTimestamp: uint40(block.timestamp) + offer.duration, - collateral: collateral, - asset: loanAsset, + collateral: MultiToken.Asset({ + category: offer.collateralCategory, + assetAddress: offer.collateralAddress, + id: offerValues.collateralId, + amount: offer.collateralAmount + }), + asset: MultiToken.ERC20({ + assetAddress: offer.loanAssetAddress, + amount: offer.loanAmount + }), fixedInterestAmount: offer.fixedInterestAmount, accruingInterestAPR: offer.accruingInterestAPR, canCreate: true, diff --git a/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol b/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol index adf2ee7..57e9ba4 100644 --- a/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol +++ b/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol @@ -4,8 +4,11 @@ pragma solidity 0.8.16; import { MultiToken } from "MultiToken/MultiToken.sol"; import { PWNSignatureChecker } from "@pwn/loan/lib/PWNSignatureChecker.sol"; -import { PWNSimpleLoanOffer, PWNSimpleLoanTermsFactory } from "@pwn/loan/terms/simple/factory/offer/base/PWNSimpleLoanOffer.sol"; +import { PWNSimpleLoanOffer, PWNSimpleLoanTermsFactory } + from "@pwn/loan/terms/simple/factory/offer/base/PWNSimpleLoanOffer.sol"; import { PWNLOANTerms } from "@pwn/loan/terms/PWNLOANTerms.sol"; +import { StateFingerprintComputerRegistry, IERC5646 } + from "@pwn/state-fingerprint/StateFingerprintComputerRegistry.sol"; import "@pwn/PWNErrors.sol"; @@ -25,17 +28,21 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanOffer { * @dev EIP-712 simple offer struct type hash. */ bytes32 public constant OFFER_TYPEHASH = keccak256( - "Offer(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,address loanAssetAddress,uint256 loanAmount,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedBorrower,address lender,bool isPersistent,uint256 nonceSpace,uint256 nonce)" + "Offer(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address loanAssetAddress,uint256 loanAmount,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedBorrower,address lender,bool isPersistent,uint256 nonceSpace,uint256 nonce)" ); bytes32 public immutable DOMAIN_SEPARATOR; + StateFingerprintComputerRegistry public immutable stateFingerprintComputerRegistry; + /** * @notice Construct defining a simple 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 collateralId Token id of an asset used as a collateral, in case of ERC20 should be 0. * @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 loanAssetAddress Address of an asset which is lender to a borrower. * @param loanAmount Amount of tokens which is offered as a loan to a borrower. * @param fixedInterestAmount Fixed interest amount in loan asset tokens. It is the minimum amount of interest which has to be paid by a borrower. @@ -54,6 +61,8 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanOffer { address collateralAddress; uint256 collateralId; uint256 collateralAmount; + bool checkCollateralStateFingerprint; + bytes32 collateralStateFingerprint; address loanAssetAddress; uint256 loanAmount; uint256 fixedInterestAmount; @@ -82,7 +91,11 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanOffer { |* # CONSTRUCTOR *| |*----------------------------------------------------------*/ - constructor(address hub, address revokedOfferNonce) PWNSimpleLoanOffer(hub, revokedOfferNonce) { + constructor( + address hub, + address revokedOfferNonce, + address _stateFingerprintComputerRegistry + ) PWNSimpleLoanOffer(hub, revokedOfferNonce) { DOMAIN_SEPARATOR = keccak256(abi.encode( keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), keccak256("PWNSimpleLoanSimpleOffer"), @@ -90,6 +103,7 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanOffer { block.chainid, address(this) )); + stateFingerprintComputerRegistry = StateFingerprintComputerRegistry(_stateFingerprintComputerRegistry); } @@ -154,27 +168,39 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanOffer { maxAPR: MAX_ACCRUING_INTEREST_APR }); - // Prepare collateral and loan asset - MultiToken.Asset memory collateral = MultiToken.Asset({ - category: offer.collateralCategory, - assetAddress: offer.collateralAddress, - id: offer.collateralId, - amount: offer.collateralAmount - }); - MultiToken.Asset memory loanAsset = MultiToken.Asset({ - category: MultiToken.Category.ERC20, - assetAddress: offer.loanAssetAddress, - id: 0, - amount: offer.loanAmount - }); + // Check that the collateral state fingerprint matches the current state + if (offer.checkCollateralStateFingerprint) { + IERC5646 computer = stateFingerprintComputerRegistry.getStateFingerprintComputer(offer.collateralAddress); + if (address(computer) == address(0)) { + // Asset is not implementing ERC5646 and no computer is registered + revert MissingStateFingerprintComputer(); + } + + bytes32 currentFingerprint = computer.getStateFingerprint(offer.collateralId); + if (offer.collateralStateFingerprint != currentFingerprint) { + // Fingerprint mismatch + revert InvalidCollateralStateFingerprint({ + offered: offer.collateralStateFingerprint, + current: currentFingerprint + }); + } + } // Create loan terms object loanTerms = PWNLOANTerms.Simple({ lender: lender, borrower: borrower, defaultTimestamp: uint40(block.timestamp) + offer.duration, - collateral: collateral, - asset: loanAsset, + collateral: MultiToken.Asset({ + category: offer.collateralCategory, + assetAddress: offer.collateralAddress, + id: offer.collateralId, + amount: offer.collateralAmount + }), + asset: MultiToken.ERC20({ + assetAddress: offer.loanAssetAddress, + amount: offer.loanAmount + }), fixedInterestAmount: offer.fixedInterestAmount, accruingInterestAPR: offer.accruingInterestAPR, canCreate: true, diff --git a/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol b/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol index 1846e80..ac52a2a 100644 --- a/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol +++ b/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol @@ -4,8 +4,11 @@ pragma solidity 0.8.16; import { MultiToken } from "MultiToken/MultiToken.sol"; import { PWNSignatureChecker } from "@pwn/loan/lib/PWNSignatureChecker.sol"; -import { PWNSimpleLoanRequest, PWNSimpleLoanTermsFactory } from "@pwn/loan/terms/simple/factory/request/base/PWNSimpleLoanRequest.sol"; +import { PWNSimpleLoanRequest, PWNSimpleLoanTermsFactory } + from "@pwn/loan/terms/simple/factory/request/base/PWNSimpleLoanRequest.sol"; import { PWNLOANTerms } from "@pwn/loan/terms/PWNLOANTerms.sol"; +import { StateFingerprintComputerRegistry, IERC5646 } + from "@pwn/state-fingerprint/StateFingerprintComputerRegistry.sol"; import "@pwn/PWNErrors.sol"; @@ -25,17 +28,21 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanRequest { * @dev EIP-712 simple request struct type hash. */ bytes32 public constant REQUEST_TYPEHASH = keccak256( - "Request(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,address loanAssetAddress,uint256 loanAmount,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedLender,address borrower,uint256 refinancingLoanId,uint256 nonceSpace,uint256 nonce)" + "Request(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address loanAssetAddress,uint256 loanAmount,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedLender,address borrower,uint256 refinancingLoanId,uint256 nonceSpace,uint256 nonce)" ); bytes32 public immutable DOMAIN_SEPARATOR; + StateFingerprintComputerRegistry public immutable stateFingerprintComputerRegistry; + /** * @notice Construct defining a simple request. * @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 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 loanAssetAddress Address of an asset which is lender to a borrower. * @param loanAmount Amount of tokens which is requested as a loan to a borrower. * @param fixedInterestAmount Fixed interest amount in loan asset tokens. It is the minimum amount of interest which has to be paid by a borrower. @@ -54,6 +61,8 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanRequest { address collateralAddress; uint256 collateralId; uint256 collateralAmount; + bool checkCollateralStateFingerprint; + bytes32 collateralStateFingerprint; address loanAssetAddress; uint256 loanAmount; uint256 fixedInterestAmount; @@ -82,7 +91,11 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanRequest { |* # CONSTRUCTOR *| |*----------------------------------------------------------*/ - constructor(address hub, address revokedRequestNonce) PWNSimpleLoanRequest(hub, revokedRequestNonce) { + constructor( + address hub, + address revokedRequestNonce, + address _stateFingerprintComputerRegistry + ) PWNSimpleLoanRequest(hub, revokedRequestNonce) { DOMAIN_SEPARATOR = keccak256(abi.encode( keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), keccak256("PWNSimpleLoanSimpleRequest"), @@ -90,6 +103,7 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanRequest { block.chainid, address(this) )); + stateFingerprintComputerRegistry = StateFingerprintComputerRegistry(_stateFingerprintComputerRegistry); } @@ -154,27 +168,39 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanRequest { maxAPR: MAX_ACCRUING_INTEREST_APR }); - // Prepare collateral and loan asset - MultiToken.Asset memory collateral = MultiToken.Asset({ - category: request.collateralCategory, - assetAddress: request.collateralAddress, - id: request.collateralId, - amount: request.collateralAmount - }); - MultiToken.Asset memory loanAsset = MultiToken.Asset({ - category: MultiToken.Category.ERC20, - assetAddress: request.loanAssetAddress, - id: 0, - amount: request.loanAmount - }); + // Check that the collateral state fingerprint matches the current state + if (request.checkCollateralStateFingerprint) { + IERC5646 computer = stateFingerprintComputerRegistry.getStateFingerprintComputer(request.collateralAddress); + if (address(computer) == address(0)) { + // Asset is not implementing ERC5646 and no computer is registered + revert MissingStateFingerprintComputer(); + } + + bytes32 currentFingerprint = computer.getStateFingerprint(request.collateralId); + if (request.collateralStateFingerprint != currentFingerprint) { + // Fingerprint mismatch + revert InvalidCollateralStateFingerprint({ + offered: request.collateralStateFingerprint, + current: currentFingerprint + }); + } + } // Create loan terms object loanTerms = PWNLOANTerms.Simple({ lender: lender, borrower: borrower, defaultTimestamp: uint40(block.timestamp) + request.duration, - collateral: collateral, - asset: loanAsset, + collateral: MultiToken.Asset({ + category: request.collateralCategory, + assetAddress: request.collateralAddress, + id: request.collateralId, + amount: request.collateralAmount + }), + asset: MultiToken.ERC20({ + assetAddress: request.loanAssetAddress, + amount: request.loanAmount + }), fixedInterestAmount: request.fixedInterestAmount, accruingInterestAPR: request.accruingInterestAPR, canCreate: request.refinancingLoanId == 0, diff --git a/test/helper/DeploymentTest.t.sol b/test/helper/DeploymentTest.t.sol index ec1c5fb..3c4743d 100644 --- a/test/helper/DeploymentTest.t.sol +++ b/test/helper/DeploymentTest.t.sol @@ -45,9 +45,9 @@ abstract contract DeploymentTest is Deployments, Test { address(hub), address(loanToken), address(config), address(revokedNonce), address(categoryRegistry) ); - simpleLoanSimpleOffer = new PWNSimpleLoanSimpleOffer(address(hub), address(revokedNonce)); - simpleLoanListOffer = new PWNSimpleLoanListOffer(address(hub), address(revokedNonce)); - simpleLoanSimpleRequest = new PWNSimpleLoanSimpleRequest(address(hub), address(revokedNonce)); + simpleLoanSimpleOffer = new PWNSimpleLoanSimpleOffer(address(hub), address(revokedNonce), address(stateFingerprintComputerRegistry)); + simpleLoanListOffer = new PWNSimpleLoanListOffer(address(hub), address(revokedNonce), address(stateFingerprintComputerRegistry)); + simpleLoanSimpleRequest = new PWNSimpleLoanSimpleRequest(address(hub), address(revokedNonce), address(stateFingerprintComputerRegistry)); // Set hub tags address[] memory addrs = new address[](8); diff --git a/test/unit/PWNSimpleLoanListOffer.t.sol b/test/unit/PWNSimpleLoanListOffer.t.sol index 969aa83..9af2456 100644 --- a/test/unit/PWNSimpleLoanListOffer.t.sol +++ b/test/unit/PWNSimpleLoanListOffer.t.sol @@ -18,6 +18,7 @@ abstract contract PWNSimpleLoanListOfferTest is Test { PWNSimpleLoanListOffer offerContract; address hub = address(0x80b); address revokedOfferNonce = address(0x80c); + address stateFingerprintComputerRegistry = makeAddr("stateFingerprintComputerRegistry"); address activeLoanContract = address(0x80d); PWNSimpleLoanListOffer.Offer offer; PWNSimpleLoanListOffer.OfferValues offerValues; @@ -32,13 +33,15 @@ abstract contract PWNSimpleLoanListOfferTest is Test { vm.etch(revokedOfferNonce, bytes("data")); vm.etch(token, bytes("data")); - offerContract = new PWNSimpleLoanListOffer(hub, revokedOfferNonce); + offerContract = new PWNSimpleLoanListOffer(hub, revokedOfferNonce, stateFingerprintComputerRegistry); offer = PWNSimpleLoanListOffer.Offer({ collateralCategory: MultiToken.Category.ERC721, collateralAddress: token, collateralIdsWhitelistMerkleRoot: bytes32(0), collateralAmount: 1032, + checkCollateralStateFingerprint: true, + collateralStateFingerprint: keccak256("some state fingerprint"), loanAssetAddress: token, loanAmount: 1101001, fixedInterestAmount: 1, @@ -76,7 +79,7 @@ abstract contract PWNSimpleLoanListOfferTest is Test { address(offerContract) )), keccak256(abi.encodePacked( - keccak256("Offer(uint8 collateralCategory,address collateralAddress,bytes32 collateralIdsWhitelistMerkleRoot,uint256 collateralAmount,address loanAssetAddress,uint256 loanAmount,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedBorrower,address lender,bool isPersistent,uint256 nonceSpace,uint256 nonce)"), + keccak256("Offer(uint8 collateralCategory,address collateralAddress,bytes32 collateralIdsWhitelistMerkleRoot,uint256 collateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address loanAssetAddress,uint256 loanAmount,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedBorrower,address lender,bool isPersistent,uint256 nonceSpace,uint256 nonce)"), abi.encode(_offer) )) )); @@ -123,7 +126,8 @@ contract PWNSimpleLoanListOffer_MakeOffer_Test is PWNSimpleLoanListOfferTest { contract PWNSimpleLoanListOffer_CreateLOANTerms_Test is PWNSimpleLoanListOfferTest { bytes signature; - address borrower = address(0x0303030303); + address borrower = makeAddr("borrower"); + address stateFingerprintComputer = makeAddr("stateFingerprintComputer"); function setUp() override public { super.setUp(); @@ -139,7 +143,16 @@ contract PWNSimpleLoanListOffer_CreateLOANTerms_Test is PWNSimpleLoanListOfferTe abi.encode(true) ); - signature = ""; + vm.mockCall( + stateFingerprintComputerRegistry, + abi.encodeWithSignature("getStateFingerprintComputer(address)", offer.collateralAddress), + abi.encode(stateFingerprintComputer) + ); + vm.mockCall( + stateFingerprintComputer, + abi.encodeWithSignature("getStateFingerprint(uint256)" /* any collateral id */ ), + abi.encode(offer.collateralStateFingerprint) + ); } // Helpers @@ -291,6 +304,64 @@ contract PWNSimpleLoanListOffer_CreateLOANTerms_Test is PWNSimpleLoanListOfferTe offerContract.createLOANTerms(borrower, abi.encode(offer, offerValues), signature); } + function test_shouldNotCallComputerRegistry_whenShouldNotCheckStateFingerprint() external { + offer.checkCollateralStateFingerprint = false; + signature = _signOfferCompact(lenderPK, offer); + + vm.expectCall({ + callee: stateFingerprintComputerRegistry, + data: abi.encodeWithSignature("getStateFingerprintComputer(address)", offer.collateralAddress), + count: 0 + }); + + vm.prank(activeLoanContract); + offerContract.createLOANTerms(borrower, abi.encode(offer, offerValues), signature); + } + + function test_shouldFail_whenComputerRegistryReturnsZeroAddress_whenShouldCheckStateFingerprint() external { + signature = _signOfferCompact(lenderPK, offer); + + 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)); + vm.prank(activeLoanContract); + offerContract.createLOANTerms(borrower, abi.encode(offer, offerValues), signature); + } + + function testFuzz_shouldFail_whenComputerReturnsDifferentStateFingerprint_whenShouldCheckStateFingerprint( + bytes32 stateFingerprint + ) external { + vm.assume(stateFingerprint != offer.collateralStateFingerprint); + + signature = _signOfferCompact(lenderPK, offer); + + 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, offer.collateralStateFingerprint, stateFingerprint + )); + vm.prank(activeLoanContract); + offerContract.createLOANTerms(borrower, abi.encode(offer, offerValues), signature); + } + function test_shouldRevokeOffer_whenIsNotPersistent() external { offer.isPersistent = false; signature = _signOfferCompact(lenderPK, offer); diff --git a/test/unit/PWNSimpleLoanSimpleOffer.t.sol b/test/unit/PWNSimpleLoanSimpleOffer.t.sol index 55e4a22..723a8f7 100644 --- a/test/unit/PWNSimpleLoanSimpleOffer.t.sol +++ b/test/unit/PWNSimpleLoanSimpleOffer.t.sol @@ -18,6 +18,7 @@ abstract contract PWNSimpleLoanSimpleOfferTest is Test { PWNSimpleLoanSimpleOffer offerContract; address hub = address(0x80b); address revokedOfferNonce = address(0x80c); + address stateFingerprintComputerRegistry = makeAddr("stateFingerprintComputerRegistry"); address activeLoanContract = address(0x80d); PWNSimpleLoanSimpleOffer.Offer offer; address token = address(0x070ce2); @@ -31,13 +32,15 @@ abstract contract PWNSimpleLoanSimpleOfferTest is Test { vm.etch(revokedOfferNonce, bytes("data")); vm.etch(token, bytes("data")); - offerContract = new PWNSimpleLoanSimpleOffer(hub, revokedOfferNonce); + offerContract = new PWNSimpleLoanSimpleOffer(hub, revokedOfferNonce, stateFingerprintComputerRegistry); offer = PWNSimpleLoanSimpleOffer.Offer({ collateralCategory: MultiToken.Category.ERC721, collateralAddress: token, collateralId: 42, collateralAmount: 1032, + checkCollateralStateFingerprint: true, + collateralStateFingerprint: keccak256("some state fingerprint"), loanAssetAddress: token, loanAmount: 1101001, fixedInterestAmount: 1, @@ -70,7 +73,7 @@ abstract contract PWNSimpleLoanSimpleOfferTest is Test { address(offerContract) )), keccak256(abi.encodePacked( - keccak256("Offer(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,address loanAssetAddress,uint256 loanAmount,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedBorrower,address lender,bool isPersistent,uint256 nonceSpace,uint256 nonce)"), + keccak256("Offer(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address loanAssetAddress,uint256 loanAmount,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedBorrower,address lender,bool isPersistent,uint256 nonceSpace,uint256 nonce)"), abi.encode(_offer) )) )); @@ -117,7 +120,8 @@ contract PWNSimpleLoanSimpleOffer_MakeOffer_Test is PWNSimpleLoanSimpleOfferTest contract PWNSimpleLoanSimpleOffer_CreateLOANTerms_Test is PWNSimpleLoanSimpleOfferTest { bytes signature; - address borrower = address(0x0303030303); + address borrower = makeAddr("borrower"); + address stateFingerprintComputer = makeAddr("stateFingerprintComputer"); function setUp() override public { super.setUp(); @@ -133,7 +137,16 @@ contract PWNSimpleLoanSimpleOffer_CreateLOANTerms_Test is PWNSimpleLoanSimpleOff abi.encode(true) ); - signature = ""; + vm.mockCall( + stateFingerprintComputerRegistry, + abi.encodeWithSignature("getStateFingerprintComputer(address)", offer.collateralAddress), + abi.encode(stateFingerprintComputer) + ); + vm.mockCall( + stateFingerprintComputer, + abi.encodeWithSignature("getStateFingerprint(uint256)", offer.collateralId), + abi.encode(offer.collateralStateFingerprint) + ); } // Helpers @@ -285,6 +298,64 @@ contract PWNSimpleLoanSimpleOffer_CreateLOANTerms_Test is PWNSimpleLoanSimpleOff offerContract.createLOANTerms(borrower, abi.encode(offer), signature); } + function test_shouldNotCallComputerRegistry_whenShouldNotCheckStateFingerprint() external { + offer.checkCollateralStateFingerprint = false; + signature = _signOfferCompact(lenderPK, offer); + + vm.expectCall({ + callee: stateFingerprintComputerRegistry, + data: abi.encodeWithSignature("getStateFingerprintComputer(address)", offer.collateralAddress), + count: 0 + }); + + vm.prank(activeLoanContract); + offerContract.createLOANTerms(borrower, abi.encode(offer), signature); + } + + function test_shouldFail_whenComputerRegistryReturnsZeroAddress_whenShouldCheckStateFingerprint() external { + signature = _signOfferCompact(lenderPK, offer); + + 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)); + vm.prank(activeLoanContract); + offerContract.createLOANTerms(borrower, abi.encode(offer), signature); + } + + function testFuzz_shouldFail_whenComputerReturnsDifferentStateFingerprint_whenShouldCheckStateFingerprint( + bytes32 stateFingerprint + ) external { + vm.assume(stateFingerprint != offer.collateralStateFingerprint); + + signature = _signOfferCompact(lenderPK, offer); + + vm.mockCall( + stateFingerprintComputer, + abi.encodeWithSignature("getStateFingerprint(uint256)", offer.collateralId), + abi.encode(stateFingerprint) + ); + + vm.expectCall( + stateFingerprintComputer, + abi.encodeWithSignature("getStateFingerprint(uint256)", offer.collateralId) + ); + + vm.expectRevert(abi.encodeWithSelector( + InvalidCollateralStateFingerprint.selector, offer.collateralStateFingerprint, stateFingerprint + )); + vm.prank(activeLoanContract); + offerContract.createLOANTerms(borrower, abi.encode(offer), signature); + } + function test_shouldRevokeOffer_whenIsNotPersistent() external { offer.isPersistent = false; signature = _signOfferCompact(lenderPK, offer); diff --git a/test/unit/PWNSimpleLoanSimpleRequest.t.sol b/test/unit/PWNSimpleLoanSimpleRequest.t.sol index 9f5cb00..373bb39 100644 --- a/test/unit/PWNSimpleLoanSimpleRequest.t.sol +++ b/test/unit/PWNSimpleLoanSimpleRequest.t.sol @@ -18,6 +18,7 @@ abstract contract PWNSimpleLoanSimpleRequestTest is Test { PWNSimpleLoanSimpleRequest requestContract; address hub = address(0x80b); address revokedRequestNonce = address(0x80c); + address stateFingerprintComputerRegistry = makeAddr("stateFingerprintComputerRegistry"); address activeLoanContract = address(0x80d); PWNSimpleLoanSimpleRequest.Request request; address token = address(0x070ce2); @@ -31,13 +32,15 @@ abstract contract PWNSimpleLoanSimpleRequestTest is Test { vm.etch(revokedRequestNonce, bytes("data")); vm.etch(token, bytes("data")); - requestContract = new PWNSimpleLoanSimpleRequest(hub, revokedRequestNonce); + requestContract = new PWNSimpleLoanSimpleRequest(hub, revokedRequestNonce, stateFingerprintComputerRegistry); request = PWNSimpleLoanSimpleRequest.Request({ collateralCategory: MultiToken.Category.ERC721, collateralAddress: token, collateralId: 42, collateralAmount: 1032, + checkCollateralStateFingerprint: true, + collateralStateFingerprint: keccak256("some state fingerprint"), loanAssetAddress: token, loanAmount: 1101001, fixedInterestAmount: 1, @@ -70,7 +73,7 @@ abstract contract PWNSimpleLoanSimpleRequestTest is Test { address(requestContract) )), keccak256(abi.encodePacked( - keccak256("Request(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,address loanAssetAddress,uint256 loanAmount,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedLender,address borrower,uint256 refinancingLoanId,uint256 nonceSpace,uint256 nonce)"), + keccak256("Request(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address loanAssetAddress,uint256 loanAmount,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedLender,address borrower,uint256 refinancingLoanId,uint256 nonceSpace,uint256 nonce)"), abi.encode(_request) )) )); @@ -117,7 +120,8 @@ contract PWNSimpleLoanSimpleRequest_MakeRequest_Test is PWNSimpleLoanSimpleReque contract PWNSimpleLoanSimpleRequest_CreateLOANTerms_Test is PWNSimpleLoanSimpleRequestTest { bytes signature; - address lender = address(0x0303030303); + address lender = makeAddr("lender"); + address stateFingerprintComputer = makeAddr("stateFingerprintComputer"); function setUp() override public { super.setUp(); @@ -133,7 +137,16 @@ contract PWNSimpleLoanSimpleRequest_CreateLOANTerms_Test is PWNSimpleLoanSimpleR abi.encode(true) ); - signature = ""; + vm.mockCall( + stateFingerprintComputerRegistry, + abi.encodeWithSignature("getStateFingerprintComputer(address)", request.collateralAddress), + abi.encode(stateFingerprintComputer) + ); + vm.mockCall( + stateFingerprintComputer, + abi.encodeWithSignature("getStateFingerprint(uint256)", request.collateralId), + abi.encode(request.collateralStateFingerprint) + ); } // Helpers @@ -285,6 +298,64 @@ contract PWNSimpleLoanSimpleRequest_CreateLOANTerms_Test is PWNSimpleLoanSimpleR requestContract.createLOANTerms(lender, abi.encode(request), signature); } + function test_shouldNotCallComputerRegistry_whenShouldNotCheckStateFingerprint() external { + request.checkCollateralStateFingerprint = false; + signature = _signRequestCompact(borrowerPK, request); + + vm.expectCall({ + callee: stateFingerprintComputerRegistry, + data: abi.encodeWithSignature("getStateFingerprintComputer(address)", request.collateralAddress), + count: 0 + }); + + vm.prank(activeLoanContract); + requestContract.createLOANTerms(lender, abi.encode(request), signature); + } + + function test_shouldFail_whenComputerRegistryReturnsZeroAddress_whenShouldCheckStateFingerprint() external { + signature = _signRequestCompact(borrowerPK, request); + + vm.mockCall( + stateFingerprintComputerRegistry, + abi.encodeWithSignature("getStateFingerprintComputer(address)", request.collateralAddress), + abi.encode(address(0)) + ); + + vm.expectCall( + stateFingerprintComputerRegistry, + abi.encodeWithSignature("getStateFingerprintComputer(address)", request.collateralAddress) + ); + + vm.expectRevert(abi.encodeWithSelector(MissingStateFingerprintComputer.selector)); + vm.prank(activeLoanContract); + requestContract.createLOANTerms(lender, abi.encode(request), signature); + } + + function testFuzz_shouldFail_whenComputerReturnsDifferentStateFingerprint_whenShouldCheckStateFingerprint( + bytes32 stateFingerprint + ) external { + vm.assume(stateFingerprint != request.collateralStateFingerprint); + + signature = _signRequestCompact(borrowerPK, request); + + vm.mockCall( + stateFingerprintComputer, + abi.encodeWithSignature("getStateFingerprint(uint256)", request.collateralId), + abi.encode(stateFingerprint) + ); + + vm.expectCall( + stateFingerprintComputer, + abi.encodeWithSignature("getStateFingerprint(uint256)", request.collateralId) + ); + + vm.expectRevert(abi.encodeWithSelector( + InvalidCollateralStateFingerprint.selector, request.collateralStateFingerprint, stateFingerprint + )); + vm.prank(activeLoanContract); + requestContract.createLOANTerms(lender, abi.encode(request), signature); + } + function test_shouldRevokeRequest() external { signature = _signRequestCompact(borrowerPK, request); From 9bcd05f9b09aa86538bb27672e6a03c59f6d6884 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Wed, 6 Mar 2024 14:28:25 +0100 Subject: [PATCH 036/129] refactor: remove base offer and request contracts --- .../factory/offer/PWNSimpleLoanListOffer.sol | 41 +++++-- .../offer/PWNSimpleLoanSimpleOffer.sol | 41 +++++-- .../factory/offer/base/PWNSimpleLoanOffer.sol | 63 ----------- .../request/PWNSimpleLoanSimpleRequest.sol | 41 +++++-- .../request/base/PWNSimpleLoanRequest.sol | 63 ----------- test/unit/PWNSimpleLoanListOffer.t.sol | 43 +++++-- test/unit/PWNSimpleLoanOffer.t.sol | 106 ------------------ test/unit/PWNSimpleLoanRequest.t.sol | 106 ------------------ test/unit/PWNSimpleLoanSimpleOffer.t.sol | 43 +++++-- test/unit/PWNSimpleLoanSimpleRequest.t.sol | 44 ++++++-- 10 files changed, 193 insertions(+), 398 deletions(-) delete mode 100644 src/loan/terms/simple/factory/offer/base/PWNSimpleLoanOffer.sol delete mode 100644 src/loan/terms/simple/factory/request/base/PWNSimpleLoanRequest.sol delete mode 100644 test/unit/PWNSimpleLoanOffer.t.sol delete mode 100644 test/unit/PWNSimpleLoanRequest.t.sol diff --git a/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol b/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol index a68750c..a0c8866 100644 --- a/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol +++ b/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol @@ -5,12 +5,12 @@ import { MultiToken } from "MultiToken/MultiToken.sol"; import { MerkleProof } from "openzeppelin-contracts/contracts/utils/cryptography/MerkleProof.sol"; +import { PWNHubAccessControl } from "@pwn/hub/PWNHubAccessControl.sol"; import { PWNSignatureChecker } from "@pwn/loan/lib/PWNSignatureChecker.sol"; -import { PWNSimpleLoanOffer, PWNSimpleLoanTermsFactory } - from "@pwn/loan/terms/simple/factory/offer/base/PWNSimpleLoanOffer.sol"; +import { PWNSimpleLoanTermsFactory } from "@pwn/loan/terms/simple/factory/PWNSimpleLoanTermsFactory.sol"; import { PWNLOANTerms } from "@pwn/loan/terms/PWNLOANTerms.sol"; -import { StateFingerprintComputerRegistry, IERC5646 } - from "@pwn/state-fingerprint/StateFingerprintComputerRegistry.sol"; +import { PWNRevokedNonce } from "@pwn/nonce/PWNRevokedNonce.sol"; +import { StateFingerprintComputerRegistry, IERC5646 } from "@pwn/state-fingerprint/StateFingerprintComputerRegistry.sol"; import "@pwn/PWNErrors.sol"; @@ -19,7 +19,7 @@ import "@pwn/PWNErrors.sol"; * @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 PWNSimpleLoanOffer { +contract PWNSimpleLoanListOffer is PWNSimpleLoanTermsFactory, PWNHubAccessControl { string public constant VERSION = "1.2"; @@ -36,8 +36,16 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanOffer { bytes32 public immutable DOMAIN_SEPARATOR; + PWNRevokedNonce public immutable revokedOfferNonce; StateFingerprintComputerRegistry public immutable stateFingerprintComputerRegistry; + /** + * @dev Mapping of offers made via on-chain transactions. + * Could be used by contract wallets instead of EIP-1271. + * (offer hash => is made) + */ + mapping (bytes32 => bool) public offersMade; + /** * @notice Construct defining a list offer. * @param collateralCategory Category of an asset used as a collateral (0 == ERC20, 1 == ERC721, 2 == ERC1155). @@ -108,9 +116,9 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanOffer { constructor( address hub, - address revokedOfferNonce, + address _revokedOfferNonce, address _stateFingerprintComputerRegistry - ) PWNSimpleLoanOffer(hub, revokedOfferNonce) { + ) PWNHubAccessControl(hub) { DOMAIN_SEPARATOR = keccak256(abi.encode( keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), keccak256("PWNSimpleLoanListOffer"), @@ -118,6 +126,8 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanOffer { block.chainid, address(this) )); + + revokedOfferNonce = PWNRevokedNonce(_revokedOfferNonce); stateFingerprintComputerRegistry = StateFingerprintComputerRegistry(_stateFingerprintComputerRegistry); } @@ -132,9 +142,24 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanOffer { * @param offer Offer struct containing all needed offer data. */ function makeOffer(Offer calldata offer) external { + // Check that caller is a lender + if (msg.sender != offer.lender) + revert CallerIsNotStatedLender(offer.lender); + bytes32 offerHash = getOfferHash(offer); - _makeOffer(offerHash, offer.lender); emit OfferMade(offerHash, offer.lender, offer); + + // Mark offer as made + offersMade[offerHash] = true; + } + + /** + * @notice Helper function for revoking an offer nonce on behalf of a caller. + * @param offerNonceSpace Nonce space of an offer nonce to be revoked. + * @param offerNonce Offer nonce to be revoked. + */ + function revokeOfferNonce(uint256 offerNonceSpace, uint256 offerNonce) external { + revokedOfferNonce.revokeNonce(msg.sender, offerNonceSpace, offerNonce); } diff --git a/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol b/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol index 57e9ba4..cf51b64 100644 --- a/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol +++ b/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol @@ -3,12 +3,12 @@ pragma solidity 0.8.16; import { MultiToken } from "MultiToken/MultiToken.sol"; +import { PWNHubAccessControl } from "@pwn/hub/PWNHubAccessControl.sol"; import { PWNSignatureChecker } from "@pwn/loan/lib/PWNSignatureChecker.sol"; -import { PWNSimpleLoanOffer, PWNSimpleLoanTermsFactory } - from "@pwn/loan/terms/simple/factory/offer/base/PWNSimpleLoanOffer.sol"; +import { PWNSimpleLoanTermsFactory } from "@pwn/loan/terms/simple/factory/PWNSimpleLoanTermsFactory.sol"; import { PWNLOANTerms } from "@pwn/loan/terms/PWNLOANTerms.sol"; -import { StateFingerprintComputerRegistry, IERC5646 } - from "@pwn/state-fingerprint/StateFingerprintComputerRegistry.sol"; +import { PWNRevokedNonce } from "@pwn/nonce/PWNRevokedNonce.sol"; +import { StateFingerprintComputerRegistry, IERC5646 } from "@pwn/state-fingerprint/StateFingerprintComputerRegistry.sol"; import "@pwn/PWNErrors.sol"; @@ -16,7 +16,7 @@ import "@pwn/PWNErrors.sol"; * @title PWN Simple Loan Simple Offer * @notice Loan terms factory contract creating a simple loan terms from a simple offer. */ -contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanOffer { +contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanTermsFactory, PWNHubAccessControl { string public constant VERSION = "1.2"; @@ -33,8 +33,16 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanOffer { bytes32 public immutable DOMAIN_SEPARATOR; + PWNRevokedNonce public immutable revokedOfferNonce; StateFingerprintComputerRegistry public immutable stateFingerprintComputerRegistry; + /** + * @dev Mapping of offers made via on-chain transactions. + * Could be used by contract wallets instead of EIP-1271. + * (offer hash => is made) + */ + mapping (bytes32 => bool) public offersMade; + /** * @notice Construct defining a simple offer. * @param collateralCategory Category of an asset used as a collateral (0 == ERC20, 1 == ERC721, 2 == ERC1155). @@ -93,9 +101,9 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanOffer { constructor( address hub, - address revokedOfferNonce, + address _revokedOfferNonce, address _stateFingerprintComputerRegistry - ) PWNSimpleLoanOffer(hub, revokedOfferNonce) { + ) PWNHubAccessControl(hub) { DOMAIN_SEPARATOR = keccak256(abi.encode( keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), keccak256("PWNSimpleLoanSimpleOffer"), @@ -103,6 +111,8 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanOffer { block.chainid, address(this) )); + + revokedOfferNonce = PWNRevokedNonce(_revokedOfferNonce); stateFingerprintComputerRegistry = StateFingerprintComputerRegistry(_stateFingerprintComputerRegistry); } @@ -117,9 +127,24 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanOffer { * @param offer Offer struct containing all needed offer data. */ function makeOffer(Offer calldata offer) external { + // Check that caller is a lender + if (msg.sender != offer.lender) + revert CallerIsNotStatedLender(offer.lender); + bytes32 offerHash = getOfferHash(offer); - _makeOffer(offerHash, offer.lender); emit OfferMade(offerHash, offer.lender, offer); + + // Mark offer as made + offersMade[offerHash] = true; + } + + /** + * @notice Helper function for revoking an offer nonce on behalf of a caller. + * @param offerNonceSpace Nonce space of an offer nonce to be revoked. + * @param offerNonce Offer nonce to be revoked. + */ + function revokeOfferNonce(uint256 offerNonceSpace, uint256 offerNonce) external { + revokedOfferNonce.revokeNonce(msg.sender, offerNonceSpace, offerNonce); } diff --git a/src/loan/terms/simple/factory/offer/base/PWNSimpleLoanOffer.sol b/src/loan/terms/simple/factory/offer/base/PWNSimpleLoanOffer.sol deleted file mode 100644 index bf22c8c..0000000 --- a/src/loan/terms/simple/factory/offer/base/PWNSimpleLoanOffer.sol +++ /dev/null @@ -1,63 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity 0.8.16; - -import { PWNHubAccessControl } from "@pwn/hub/PWNHubAccessControl.sol"; -import { PWNSimpleLoanTermsFactory } from "@pwn/loan/terms/simple/factory/PWNSimpleLoanTermsFactory.sol"; -import { PWNRevokedNonce } from "@pwn/nonce/PWNRevokedNonce.sol"; -import "@pwn/PWNErrors.sol"; - - -abstract contract PWNSimpleLoanOffer is PWNSimpleLoanTermsFactory, PWNHubAccessControl { - - /*----------------------------------------------------------*| - |* # VARIABLES & CONSTANTS DEFINITIONS *| - |*----------------------------------------------------------*/ - - PWNRevokedNonce public immutable revokedOfferNonce; - - /** - * @dev Mapping of offers made via on-chain transactions. - * Could be used by contract wallets instead of EIP-1271. - * (offer hash => is made) - */ - mapping (bytes32 => bool) public offersMade; - - - /*----------------------------------------------------------*| - |* # CONSTRUCTOR *| - |*----------------------------------------------------------*/ - - constructor(address hub, address _revokedOfferNonce) PWNHubAccessControl(hub) { - revokedOfferNonce = PWNRevokedNonce(_revokedOfferNonce); - } - - - /*----------------------------------------------------------*| - |* # OFFER MANAGEMENT *| - |*----------------------------------------------------------*/ - - /** - * @notice Make an on-chain offer. - * @dev Function will mark an offer hash as proposed. Offer will become acceptable by a borrower without an offer signature. - * @param offerStructHash Hash of a proposed offer. - * @param lender Address of an offer proposer (lender). - */ - function _makeOffer(bytes32 offerStructHash, address lender) internal { - // Check that caller is a lender - if (msg.sender != lender) - revert CallerIsNotStatedLender(lender); - - // Mark offer as made - offersMade[offerStructHash] = true; - } - - /** - * @notice Helper function for revoking an offer nonce on behalf of a caller. - * @param offerNonceSpace Nonce space of an offer nonce to be revoked. - * @param offerNonce Offer nonce to be revoked. - */ - function revokeOfferNonce(uint256 offerNonceSpace, uint256 offerNonce) external { - revokedOfferNonce.revokeNonce(msg.sender, offerNonceSpace, offerNonce); - } - -} diff --git a/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol b/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol index ac52a2a..02ad62d 100644 --- a/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol +++ b/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol @@ -3,12 +3,12 @@ pragma solidity 0.8.16; import { MultiToken } from "MultiToken/MultiToken.sol"; +import { PWNHubAccessControl } from "@pwn/hub/PWNHubAccessControl.sol"; import { PWNSignatureChecker } from "@pwn/loan/lib/PWNSignatureChecker.sol"; -import { PWNSimpleLoanRequest, PWNSimpleLoanTermsFactory } - from "@pwn/loan/terms/simple/factory/request/base/PWNSimpleLoanRequest.sol"; +import { PWNSimpleLoanTermsFactory } from "@pwn/loan/terms/simple/factory/PWNSimpleLoanTermsFactory.sol"; import { PWNLOANTerms } from "@pwn/loan/terms/PWNLOANTerms.sol"; -import { StateFingerprintComputerRegistry, IERC5646 } - from "@pwn/state-fingerprint/StateFingerprintComputerRegistry.sol"; +import { PWNRevokedNonce } from "@pwn/nonce/PWNRevokedNonce.sol"; +import { StateFingerprintComputerRegistry, IERC5646 } from "@pwn/state-fingerprint/StateFingerprintComputerRegistry.sol"; import "@pwn/PWNErrors.sol"; @@ -16,7 +16,7 @@ import "@pwn/PWNErrors.sol"; * @title PWN Simple Loan Simple Request * @notice Loan terms factory contract creating a simple loan terms from a simple request. */ -contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanRequest { +contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanTermsFactory, PWNHubAccessControl { string public constant VERSION = "1.2"; @@ -33,8 +33,16 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanRequest { bytes32 public immutable DOMAIN_SEPARATOR; + PWNRevokedNonce public immutable revokedRequestNonce; StateFingerprintComputerRegistry public immutable stateFingerprintComputerRegistry; + /** + * @dev Mapping of requests made via on-chain transactions. + * Could be used by contract wallets instead of EIP-1271. + * (request hash => is made) + */ + mapping (bytes32 => bool) public requestsMade; + /** * @notice Construct defining a simple request. * @param collateralCategory Category of an asset used as a collateral (0 == ERC20, 1 == ERC721, 2 == ERC1155). @@ -93,9 +101,9 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanRequest { constructor( address hub, - address revokedRequestNonce, + address _revokedRequestNonce, address _stateFingerprintComputerRegistry - ) PWNSimpleLoanRequest(hub, revokedRequestNonce) { + ) PWNHubAccessControl(hub) { DOMAIN_SEPARATOR = keccak256(abi.encode( keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), keccak256("PWNSimpleLoanSimpleRequest"), @@ -103,6 +111,8 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanRequest { block.chainid, address(this) )); + + revokedRequestNonce = PWNRevokedNonce(_revokedRequestNonce); stateFingerprintComputerRegistry = StateFingerprintComputerRegistry(_stateFingerprintComputerRegistry); } @@ -117,9 +127,24 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanRequest { * @param request Request struct containing all needed request data. */ function makeRequest(Request calldata request) external { + // Check that caller is a borrower + if (msg.sender != request.borrower) + revert CallerIsNotStatedBorrower(request.borrower); + bytes32 requestHash = getRequestHash(request); - _makeRequest(requestHash, request.borrower); emit RequestMade(requestHash, request.borrower, request); + + // Mark request as made + requestsMade[requestHash] = true; + } + + /** + * @notice Helper function for revoking a request nonce on behalf of a caller. + * @param requestNonceSpace Nonce space of a request nonce to be revoked. + * @param requestNonce Request nonce to be revoked. + */ + function revokeRequestNonce(uint256 requestNonceSpace, uint256 requestNonce) external { + revokedRequestNonce.revokeNonce(msg.sender, requestNonceSpace, requestNonce); } diff --git a/src/loan/terms/simple/factory/request/base/PWNSimpleLoanRequest.sol b/src/loan/terms/simple/factory/request/base/PWNSimpleLoanRequest.sol deleted file mode 100644 index e35d220..0000000 --- a/src/loan/terms/simple/factory/request/base/PWNSimpleLoanRequest.sol +++ /dev/null @@ -1,63 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity 0.8.16; - -import { PWNHubAccessControl } from "@pwn/hub/PWNHubAccessControl.sol"; -import { PWNSimpleLoanTermsFactory } from "@pwn/loan/terms/simple/factory/PWNSimpleLoanTermsFactory.sol"; -import { PWNRevokedNonce } from "@pwn/nonce/PWNRevokedNonce.sol"; -import "@pwn/PWNErrors.sol"; - - -abstract contract PWNSimpleLoanRequest is PWNSimpleLoanTermsFactory, PWNHubAccessControl { - - /*----------------------------------------------------------*| - |* # VARIABLES & CONSTANTS DEFINITIONS *| - |*----------------------------------------------------------*/ - - PWNRevokedNonce internal immutable revokedRequestNonce; - - /** - * @dev Mapping of requests made via on-chain transactions. - * Could be used by contract wallets instead of EIP-1271. - * (request hash => is made) - */ - mapping (bytes32 => bool) public requestsMade; - - - /*----------------------------------------------------------*| - |* # CONSTRUCTOR *| - |*----------------------------------------------------------*/ - - constructor(address hub, address _revokedRequestNonce) PWNHubAccessControl(hub) { - revokedRequestNonce = PWNRevokedNonce(_revokedRequestNonce); - } - - - /*----------------------------------------------------------*| - |* # REQUEST MANAGEMENT *| - |*----------------------------------------------------------*/ - - /** - * @notice Make an on-chain request. - * @dev Function will mark a request hash as proposed. Request will become acceptable by a borrower without a request signature. - * @param requestStructHash Hash of a proposed request. - * @param borrower Address of a request proposer (borrower). - */ - function _makeRequest(bytes32 requestStructHash, address borrower) internal { - // Check that caller is a borrower - if (msg.sender != borrower) - revert CallerIsNotStatedBorrower(borrower); - - // Mark request as made - requestsMade[requestStructHash] = true; - } - - /** - * @notice Helper function for revoking a request nonce on behalf of a caller. - * @param requestNonceSpace Nonce space of a request nonce to be revoked. - * @param requestNonce Request nonce to be revoked. - */ - function revokeRequestNonce(uint256 requestNonceSpace, uint256 requestNonce) external { - revokedRequestNonce.revokeNonce(msg.sender, requestNonceSpace, requestNonce); - } - -} diff --git a/test/unit/PWNSimpleLoanListOffer.t.sol b/test/unit/PWNSimpleLoanListOffer.t.sol index 9af2456..bc50309 100644 --- a/test/unit/PWNSimpleLoanListOffer.t.sol +++ b/test/unit/PWNSimpleLoanListOffer.t.sol @@ -92,28 +92,47 @@ abstract contract PWNSimpleLoanListOfferTest is Test { |* # MAKE OFFER *| |*----------------------------------------------------------*/ -// Feature tested in PWNSimpleLoanOffer.t.sol contract PWNSimpleLoanListOffer_MakeOffer_Test is PWNSimpleLoanListOfferTest { + function testFuzz_shouldFail_whenCallerIsNotLender(address caller) external { + vm.assume(caller != offer.lender); + + vm.expectRevert(abi.encodeWithSelector(CallerIsNotStatedLender.selector, lender)); + 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(lender); + vm.prank(offer.lender); offerContract.makeOffer(offer); - bytes32 isMadeValue = vm.load( - address(offerContract), - keccak256(abi.encode(_offerHash(offer), OFFERS_MADE_SLOT)) - ); - assertEq(uint256(isMadeValue), 1); + assertTrue(offerContract.offersMade(_offerHash(offer))); } - function test_shouldEmit_OfferMade() external { - bytes32 offerHash = _offerHash(offer); +} - vm.expectEmit(); - emit OfferMade(offerHash, lender, offer); + +/*----------------------------------------------------------*| +|* # REVOKE OFFER NONCE *| +|*----------------------------------------------------------*/ + +contract PWNSimpleLoanOffer_RevokeOfferNonce_Test is PWNSimpleLoanListOfferTest { + + function testFuzz_shouldCallRevokeOfferNonce(uint256 nonceSpace, uint256 nonce) external { + vm.expectCall( + revokedOfferNonce, + abi.encodeWithSignature("revokeNonce(address,uint256,uint256)", lender, nonceSpace, nonce) + ); vm.prank(lender); - offerContract.makeOffer(offer); + offerContract.revokeOfferNonce(nonceSpace, nonce); } } diff --git a/test/unit/PWNSimpleLoanOffer.t.sol b/test/unit/PWNSimpleLoanOffer.t.sol deleted file mode 100644 index 9702dac..0000000 --- a/test/unit/PWNSimpleLoanOffer.t.sol +++ /dev/null @@ -1,106 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity 0.8.16; - -import "forge-std/Test.sol"; - -import "MultiToken/MultiToken.sol"; - -import "@pwn/hub/PWNHubTags.sol"; -import "@pwn/loan/terms/simple/factory/offer/base/PWNSimpleLoanOffer.sol"; -import "@pwn/loan/terms/PWNLOANTerms.sol"; -import "@pwn/PWNErrors.sol"; - - -// The only reason for this contract is to expose internal functions of PWNSimpleLoanOffer -// No additional logic is applied here -contract PWNSimpleLoanOfferExposed is PWNSimpleLoanOffer { - - constructor(address hub, address _revokedOfferNonce) PWNSimpleLoanOffer(hub, _revokedOfferNonce) { - - } - - function makeOffer(bytes32 offerHash, address lender) external { - _makeOffer(offerHash, lender); - } - - // Dummy implementation, is not tester here - function createLOANTerms( - address /*caller*/, - bytes calldata /*factoryData*/, - bytes calldata /*signature*/ - ) override external pure returns (PWNLOANTerms.Simple memory, bytes32) { - revert("Missing implementation"); - } - -} - -abstract contract PWNSimpleLoanOfferTest is Test { - - bytes32 internal constant OFFERS_MADE_SLOT = bytes32(uint256(0)); // `offersMade` mapping position - - PWNSimpleLoanOfferExposed offerContract; - address hub = address(0x80b); - address revokedOfferNonce = address(0x80c); - - bytes32 offerHash = keccak256("offer_hash_1"); - address lender = address(0x070ce3); - uint256 nonce = uint256(keccak256("nonce_1")); - - function setUp() virtual public { - vm.etch(hub, bytes("data")); - vm.etch(revokedOfferNonce, bytes("data")); - - offerContract = new PWNSimpleLoanOfferExposed(hub, revokedOfferNonce); - - vm.mockCall( - revokedOfferNonce, - abi.encodeWithSignature("isNonceRevoked(address,uint256,uint256)"), - abi.encode(false) - ); - } - -} - - -/*----------------------------------------------------------*| -|* # MAKE OFFER *| -|*----------------------------------------------------------*/ - -contract PWNSimpleLoanOffer_MakeOffer_Test is PWNSimpleLoanOfferTest { - - function test_shouldFail_whenCallerIsNotLender() external { - vm.expectRevert(abi.encodeWithSelector(CallerIsNotStatedLender.selector, lender)); - offerContract.makeOffer(offerHash, lender); - } - - function test_shouldMarkOfferAsMade() external { - vm.prank(lender); - offerContract.makeOffer(offerHash, lender); - - bytes32 isMadeValue = vm.load( - address(offerContract), - keccak256(abi.encode(offerHash, OFFERS_MADE_SLOT)) - ); - assertEq(isMadeValue, bytes32(uint256(1))); - } - -} - - -/*----------------------------------------------------------*| -|* # REVOKE OFFER NONCE *| -|*----------------------------------------------------------*/ - -contract PWNSimpleLoanOffer_RevokeOfferNonce_Test is PWNSimpleLoanOfferTest { - - function testFuzz_shouldCallRevokeOfferNonce(uint256 nonceSpace, uint256 nonce) external { - vm.expectCall( - revokedOfferNonce, - abi.encodeWithSignature("revokeNonce(address,uint256,uint256)", lender, nonceSpace, nonce) - ); - - vm.prank(lender); - offerContract.revokeOfferNonce(nonceSpace, nonce); - } - -} diff --git a/test/unit/PWNSimpleLoanRequest.t.sol b/test/unit/PWNSimpleLoanRequest.t.sol deleted file mode 100644 index 422876e..0000000 --- a/test/unit/PWNSimpleLoanRequest.t.sol +++ /dev/null @@ -1,106 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity 0.8.16; - -import "forge-std/Test.sol"; - -import "MultiToken/MultiToken.sol"; - -import "@pwn/hub/PWNHubTags.sol"; -import "@pwn/loan/terms/simple/factory/request/base/PWNSimpleLoanRequest.sol"; -import "@pwn/loan/terms/PWNLOANTerms.sol"; -import "@pwn/PWNErrors.sol"; - - -// The only reason for this contract is to expose internal functions of PWNSimpleLoanRequest -// No additional logic is applied here -contract PWNSimpleLoanRequestExposed is PWNSimpleLoanRequest { - - constructor(address hub, address revokedRequestNonce) PWNSimpleLoanRequest(hub, revokedRequestNonce) { - - } - - function makeRequest(bytes32 requestHash, address borrower) external { - _makeRequest(requestHash, borrower); - } - - // Dummy implementation, is not tester here - function createLOANTerms( - address /*caller*/, - bytes calldata /*factoryData*/, - bytes calldata /*signature*/ - ) override external pure returns (PWNLOANTerms.Simple memory, bytes32) { - revert("Missing implementation"); - } - -} - -abstract contract PWNSimpleLoanRequestTest is Test { - - bytes32 internal constant REQUESTS_MADE_SLOT = bytes32(uint256(0)); // `requestsMade` mapping position - - PWNSimpleLoanRequestExposed requestContract; - address hub = address(0x80b); - address revokedRequestNonce = address(0x80c); - - bytes32 requestHash = keccak256("request_hash_1"); - address borrower = address(0x070ce3); - uint256 nonce = uint256(keccak256("nonce_1")); - - function setUp() virtual public { - vm.etch(hub, bytes("data")); - vm.etch(revokedRequestNonce, bytes("data")); - - requestContract = new PWNSimpleLoanRequestExposed(hub, revokedRequestNonce); - - vm.mockCall( - revokedRequestNonce, - abi.encodeWithSignature("isNonceRevoked(address,uint256,uint256)"), - abi.encode(false) - ); - } - -} - - -/*----------------------------------------------------------*| -|* # MAKE REQUEST *| -|*----------------------------------------------------------*/ - -contract PWNSimpleLoanRequest_MakeRequest_Test is PWNSimpleLoanRequestTest { - - function test_shouldFail_whenCallerIsNotBorrower() external { - vm.expectRevert(abi.encodeWithSelector(CallerIsNotStatedBorrower.selector, borrower)); - requestContract.makeRequest(requestHash, borrower); - } - - function test_shouldMarkRequestAsMade() external { - vm.prank(borrower); - requestContract.makeRequest(requestHash, borrower); - - bytes32 isMadeValue = vm.load( - address(requestContract), - keccak256(abi.encode(requestHash, REQUESTS_MADE_SLOT)) - ); - assertEq(uint256(isMadeValue), 1); - } - -} - - -/*----------------------------------------------------------*| -|* # REVOKE REQUEST NONCE *| -|*----------------------------------------------------------*/ - -contract PWNSimpleLoanRequest_RevokeRequestNonce_Test is PWNSimpleLoanRequestTest { - - function testFuzz_shouldCallRevokeRequestNonce(uint256 nonceSpace, uint256 nonce) external { - vm.expectCall( - revokedRequestNonce, - abi.encodeWithSignature("revokeNonce(address,uint256,uint256)", borrower, nonceSpace, nonce) - ); - - vm.prank(borrower); - requestContract.revokeRequestNonce(nonceSpace, nonce); - } - -} diff --git a/test/unit/PWNSimpleLoanSimpleOffer.t.sol b/test/unit/PWNSimpleLoanSimpleOffer.t.sol index 723a8f7..6949c8e 100644 --- a/test/unit/PWNSimpleLoanSimpleOffer.t.sol +++ b/test/unit/PWNSimpleLoanSimpleOffer.t.sol @@ -86,28 +86,47 @@ abstract contract PWNSimpleLoanSimpleOfferTest is Test { |* # MAKE OFFER *| |*----------------------------------------------------------*/ -// Feature tested in PWNSimpleLoanOffer.t.sol contract PWNSimpleLoanSimpleOffer_MakeOffer_Test is PWNSimpleLoanSimpleOfferTest { + function testFuzz_shouldFail_whenCallerIsNotLender(address caller) external { + vm.assume(caller != offer.lender); + + vm.expectRevert(abi.encodeWithSelector(CallerIsNotStatedLender.selector, lender)); + 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(lender); + vm.prank(offer.lender); offerContract.makeOffer(offer); - bytes32 isMadeValue = vm.load( - address(offerContract), - keccak256(abi.encode(_offerHash(offer), OFFERS_MADE_SLOT)) - ); - assertEq(isMadeValue, bytes32(uint256(1))); + assertTrue(offerContract.offersMade(_offerHash(offer))); } - function test_shouldEmit_OfferMade() external { - bytes32 offerHash = _offerHash(offer); +} - vm.expectEmit(); - emit OfferMade(offerHash, lender, offer); + +/*----------------------------------------------------------*| +|* # REVOKE OFFER NONCE *| +|*----------------------------------------------------------*/ + +contract PWNSimpleLoanOffer_RevokeOfferNonce_Test is PWNSimpleLoanSimpleOfferTest { + + function testFuzz_shouldCallRevokeOfferNonce(uint256 nonceSpace, uint256 nonce) external { + vm.expectCall( + revokedOfferNonce, + abi.encodeWithSignature("revokeNonce(address,uint256,uint256)", lender, nonceSpace, nonce) + ); vm.prank(lender); - offerContract.makeOffer(offer); + offerContract.revokeOfferNonce(nonceSpace, nonce); } } diff --git a/test/unit/PWNSimpleLoanSimpleRequest.t.sol b/test/unit/PWNSimpleLoanSimpleRequest.t.sol index 373bb39..fe2a653 100644 --- a/test/unit/PWNSimpleLoanSimpleRequest.t.sol +++ b/test/unit/PWNSimpleLoanSimpleRequest.t.sol @@ -86,28 +86,48 @@ abstract contract PWNSimpleLoanSimpleRequestTest is Test { |* # MAKE REQUEST *| |*----------------------------------------------------------*/ -// Feature tested in PWNSimpleLoanRequest.t.sol contract PWNSimpleLoanSimpleRequest_MakeRequest_Test is PWNSimpleLoanSimpleRequestTest { + function testFuzz_shouldFail_whenCallerIsNotBorrower(address caller) external { + vm.assume(caller != request.borrower); + + vm.expectRevert(abi.encodeWithSelector(CallerIsNotStatedBorrower.selector, borrower)); + vm.prank(caller); + requestContract.makeRequest(request); + } + + function test_shouldEmit_RequestMade() external { + vm.expectEmit(); + emit RequestMade(_requestHash(request), request.borrower, request); + + vm.prank(request.borrower); + requestContract.makeRequest(request); + } + function test_shouldMakeRequest() external { - vm.prank(borrower); + vm.prank(request.borrower); requestContract.makeRequest(request); - bytes32 isMadeValue = vm.load( - address(requestContract), - keccak256(abi.encode(_requestHash(request), REQUESTS_MADE_SLOT)) - ); - assertEq(isMadeValue, bytes32(uint256(1))); + assertTrue(requestContract.requestsMade(_requestHash(request))); } - function test_shouldEmit_RequestMade() external { - bytes32 requestHash = _requestHash(request); +} - vm.expectEmit(); - emit RequestMade(requestHash, borrower, request); + +/*----------------------------------------------------------*| +|* # REVOKE REQUEST NONCE *| +|*----------------------------------------------------------*/ + +contract PWNSimpleLoanRequest_RevokeRequestNonce_Test is PWNSimpleLoanSimpleRequestTest { + + function testFuzz_shouldCallRevokeRequestNonce(uint256 nonceSpace, uint256 nonce) external { + vm.expectCall( + revokedRequestNonce, + abi.encodeWithSignature("revokeNonce(address,uint256,uint256)", borrower, nonceSpace, nonce) + ); vm.prank(borrower); - requestContract.makeRequest(request); + requestContract.revokeRequestNonce(nonceSpace, nonce); } } From 50dd63ecd7007aaebed1902426ba6832841a8285 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Wed, 6 Mar 2024 16:23:51 +0100 Subject: [PATCH 037/129] feat(revoked-nonce): implement is nonce usable getter to distinguish between revoked nonce and unusable nonce --- src/PWNErrors.sol | 3 +- .../factory/offer/PWNSimpleLoanListOffer.sol | 4 +- .../offer/PWNSimpleLoanSimpleOffer.sol | 4 +- .../request/PWNSimpleLoanSimpleRequest.sol | 4 +- src/loan/terms/simple/loan/PWNSimpleLoan.sol | 8 +- src/nonce/PWNRevokedNonce.sol | 30 ++++++- test/unit/PWNRevokedNonce.t.sol | 86 ++++++++++++++----- test/unit/PWNSimpleLoan.t.sol | 18 ++-- test/unit/PWNSimpleLoanListOffer.t.sol | 14 +-- test/unit/PWNSimpleLoanSimpleOffer.t.sol | 14 +-- test/unit/PWNSimpleLoanSimpleRequest.t.sol | 14 +-- 11 files changed, 133 insertions(+), 66 deletions(-) diff --git a/src/PWNErrors.sol b/src/PWNErrors.sol index fa29bcf..71e7356 100644 --- a/src/PWNErrors.sol +++ b/src/PWNErrors.sol @@ -33,8 +33,7 @@ error UnsupportedTransferFunction(); error IncompleteTransfer(); // Nonce -error NonceAlreadyRevoked(); -error InvalidMinNonce(); +error NonceNotUsable(); // Signature checks error InvalidSignatureLength(uint256); diff --git a/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol b/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol index a0c8866..b1da33f 100644 --- a/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol +++ b/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol @@ -191,8 +191,8 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanTermsFactory, PWNHubAccessContro if (block.timestamp >= offer.expiration) revert OfferExpired(); - if (revokedOfferNonce.isNonceRevoked(lender, offer.nonceSpace, offer.nonce)) - revert NonceAlreadyRevoked(); + if (!revokedOfferNonce.isNonceUsable(lender, offer.nonceSpace, offer.nonce)) + revert NonceNotUsable(); if (offer.allowedBorrower != address(0)) if (borrower != offer.allowedBorrower) diff --git a/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol b/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol index cf51b64..2b735e1 100644 --- a/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol +++ b/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol @@ -176,8 +176,8 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanTermsFactory, PWNHubAccessCont if (block.timestamp >= offer.expiration) revert OfferExpired(); - if (revokedOfferNonce.isNonceRevoked(lender, offer.nonceSpace, offer.nonce)) - revert NonceAlreadyRevoked(); + if (!revokedOfferNonce.isNonceUsable(lender, offer.nonceSpace, offer.nonce)) + revert NonceNotUsable(); if (offer.allowedBorrower != address(0)) if (borrower != offer.allowedBorrower) diff --git a/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol b/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol index 02ad62d..f37c4d9 100644 --- a/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol +++ b/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol @@ -176,8 +176,8 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanTermsFactory, PWNHubAccessCo if (block.timestamp >= request.expiration) revert RequestExpired(); - if (revokedRequestNonce.isNonceRevoked(borrower, request.nonceSpace, request.nonce)) - revert NonceAlreadyRevoked(); + if (!revokedRequestNonce.isNonceUsable(borrower, request.nonceSpace, request.nonce)) + revert NonceNotUsable(); if (request.allowedLender != address(0)) if (lender != request.allowedLender) diff --git a/src/loan/terms/simple/loan/PWNSimpleLoan.sol b/src/loan/terms/simple/loan/PWNSimpleLoan.sol index 9ae650b..37a71bc 100644 --- a/src/loan/terms/simple/loan/PWNSimpleLoan.sol +++ b/src/loan/terms/simple/loan/PWNSimpleLoan.sol @@ -229,8 +229,8 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { uint256 callersNonceSpace, uint256 callersNonceToRevoke ) external returns (uint256 loanId) { - if (revokedNonce.isNonceRevoked(msg.sender, callersNonceSpace, callersNonceToRevoke)) - revert NonceAlreadyRevoked(); + if (!revokedNonce.isNonceUsable(msg.sender, callersNonceSpace, callersNonceToRevoke)) + revert NonceNotUsable(); revokedNonce.revokeNonce(msg.sender, callersNonceSpace, callersNonceToRevoke); loanId = createLOAN({ @@ -862,8 +862,8 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { revert InvalidSignature(); if (block.timestamp >= extension.expiration) revert OfferExpired(); - if (revokedNonce.isNonceRevoked(extension.proposer, extension.nonceSpace, extension.nonce)) - revert NonceAlreadyRevoked(); + if (!revokedNonce.isNonceUsable(extension.proposer, extension.nonceSpace, extension.nonce)) + revert NonceNotUsable(); // Check caller and signer address loanOwner = loanToken.ownerOf(extension.loanId); diff --git a/src/nonce/PWNRevokedNonce.sol b/src/nonce/PWNRevokedNonce.sol index b872ea7..486c04c 100644 --- a/src/nonce/PWNRevokedNonce.sol +++ b/src/nonce/PWNRevokedNonce.sol @@ -61,6 +61,15 @@ contract PWNRevokedNonce is PWNHubAccessControl { |* # NONCE *| |*----------------------------------------------------------*/ + /** + * @notice Revoke a nonce in the current nonce space. + * @dev Caller is used as a nonce owner. + * @param nonce Nonce to be revoked. + */ + function revokeNonce(uint256 nonce) external { + _revokeNonce(msg.sender, _nonceSpace[msg.sender], nonce); + } + /** * @notice Revoke a nonce in a nonce space. * @dev Caller is used as a nonce owner. @@ -91,19 +100,32 @@ contract PWNRevokedNonce is PWNHubAccessControl { } /** - * @notice Return true if owners nonce is revoked in the given nonce space, or if the whole nonce space is revoked. + * @notice Return true if owners nonce is revoked in the given nonce space. + * @dev Do not use this function to check if nonce is usable. + * Use `isNonceUsable` instead, which checks nonce space as well. * @param owner Address of a nonce owner. * @param nonceSpace Value of a nonce space. * @param nonce Value of a nonce. * @return True if nonce is revoked. */ function isNonceRevoked(address owner, uint256 nonceSpace, uint256 nonce) external view returns (bool) { - if (_nonceSpace[owner] > nonceSpace) - return true; - return _revokedNonce[owner][nonceSpace][nonce]; } + /** + * @notice Return true if owners nonce is usable. Nonce is usable if it is not revoked and in the current nonce space. + * @param owner Address of a nonce owner. + * @param nonceSpace Value of a nonce space. + * @param nonce Value of a nonce. + * @return True if nonce is usable. + */ + function isNonceUsable(address owner, uint256 nonceSpace, uint256 nonce) external view returns (bool) { + if (_nonceSpace[owner] != nonceSpace) + return false; + + return !_revokedNonce[owner][nonceSpace][nonce]; + } + /*----------------------------------------------------------*| |* # NONCE SPACE *| diff --git a/test/unit/PWNRevokedNonce.t.sol b/test/unit/PWNRevokedNonce.t.sol index e395945..6af5834 100644 --- a/test/unit/PWNRevokedNonce.t.sol +++ b/test/unit/PWNRevokedNonce.t.sol @@ -45,20 +45,44 @@ abstract contract PWNRevokedNonceTest is Test { /*----------------------------------------------------------*| -|* # REVOKE NONCE BY OWNER *| +|* # REVOKE NONCE *| |*----------------------------------------------------------*/ -contract PWNRevokedNonce_RevokeNonceByOwner_Test is PWNRevokedNonceTest { +contract PWNRevokedNonce_RevokeNonce_Test is PWNRevokedNonceTest { + + function testFuzz_shouldStoreNonceAsRevoked(uint256 nonceSpace, uint256 nonce) external { + vm.store(address(revokedNonce), _nonceSpaceSlot(alice), bytes32(nonceSpace)); + + vm.prank(alice); + revokedNonce.revokeNonce(nonce); + + assertTrue(revokedNonce.isNonceRevoked(alice, nonceSpace, nonce)); + } + + function testFuzz_shouldEmit_NonceRevoked(uint256 nonceSpace, uint256 nonce) external { + vm.store(address(revokedNonce), _nonceSpaceSlot(alice), bytes32(nonceSpace)); + + vm.expectEmit(); + emit NonceRevoked(alice, nonceSpace, nonce); + + vm.prank(alice); + revokedNonce.revokeNonce(nonce); + } + +} + + +/*----------------------------------------------------------*| +|* # REVOKE NONCE WITH NONCE SPACE *| +|*----------------------------------------------------------*/ + +contract PWNRevokedNonce_RevokeNonceWithNonceSpace_Test is PWNRevokedNonceTest { function testFuzz_shouldStoreNonceAsRevoked(uint256 nonceSpace, uint256 nonce) external { vm.prank(alice); revokedNonce.revokeNonce(nonceSpace, nonce); - bytes32 isRevokedValue = vm.load( - address(revokedNonce), - _revokedNonceSlot(alice, nonceSpace, nonce) - ); - assertTrue(uint256(isRevokedValue) == 1); + assertTrue(revokedNonce.isNonceRevoked(alice, nonceSpace, nonce)); } function testFuzz_shouldEmit_NonceRevoked(uint256 nonceSpace, uint256 nonce) external { @@ -108,11 +132,7 @@ contract PWNRevokedNonce_RevokeNonceWithOwner_Test is PWNRevokedNonceTest { vm.prank(accessEnabledAddress); revokedNonce.revokeNonce(owner, nonceSpace, nonce); - bytes32 isRevokedValue = vm.load( - address(revokedNonce), - _revokedNonceSlot(owner, nonceSpace, nonce) - ); - assertTrue(uint256(isRevokedValue) == 1); + assertTrue(revokedNonce.isNonceRevoked(owner, nonceSpace, nonce)); } function testFuzz_shouldEmit_NonceRevoked(address owner, uint256 nonceSpace, uint256 nonce) external { @@ -132,23 +152,49 @@ contract PWNRevokedNonce_RevokeNonceWithOwner_Test is PWNRevokedNonceTest { contract PWNRevokedNonce_IsNonceRevoked_Test is PWNRevokedNonceTest { - function testFuzz_shouldReturnTrue_whenNonceSpaceIsSmallerThanCurrentNonceSpace(uint256 currentNonceSpace, uint256 nonce) external { - currentNonceSpace = bound(currentNonceSpace, 1, type(uint256).max); - uint256 nonceSpace = bound(currentNonceSpace, 0, currentNonceSpace - 1); + function testFuzz_shouldReturnStoredValue(uint256 nonceSpace, uint256 nonce, bool revoked) external { + vm.store( + address(revokedNonce), + _revokedNonceSlot(alice, nonceSpace, nonce), + bytes32(uint256(revoked ? 1 : 0)) + ); + + assertEq(revokedNonce.isNonceRevoked(alice, nonceSpace, nonce), revoked); + } + +} + + +/*----------------------------------------------------------*| +|* # IS NONCE USABLE *| +|*----------------------------------------------------------*/ + +contract PWNRevokedNonce_IsNonceUsable_Test is PWNRevokedNonceTest { + + function testFuzz_shouldReturnFalse_whenNonceSpaceIsNotEqualToCurrentNonceSpace( + uint256 currentNonceSpace, + uint256 nonceSpace, + uint256 nonce + ) external { + vm.assume(nonceSpace != currentNonceSpace); vm.store(address(revokedNonce), _nonceSpaceSlot(alice), bytes32(currentNonceSpace)); - assertTrue(revokedNonce.isNonceRevoked(alice, nonceSpace, nonce)); + assertFalse(revokedNonce.isNonceUsable(alice, nonceSpace, nonce)); } - function testFuzz_shouldReturnTrue_whenNonceIsRevoked(uint256 nonce) external { + function testFuzz_shouldReturnFalse_whenNonceIsRevoked(uint256 nonce) external { vm.store(address(revokedNonce), _revokedNonceSlot(alice, 0, nonce), bytes32(uint256(1))); - assertTrue(revokedNonce.isNonceRevoked(alice, 0, nonce)); + assertFalse(revokedNonce.isNonceUsable(alice, 0, nonce)); } - function testFuzz_shouldReturnFalse_whenNonceIsNotRevoked(uint256 nonceSpace, uint256 nonce) external { - assertFalse(revokedNonce.isNonceRevoked(alice, nonceSpace, nonce)); + function testFuzz_shouldReturnTrue__whenNonceSpaceIsEqualToCurrentNonceSpace_whenNonceIsNotRevoked( + uint256 nonceSpace, uint256 nonce + ) external { + vm.store(address(revokedNonce), _nonceSpaceSlot(alice), bytes32(nonceSpace)); + + assertTrue(revokedNonce.isNonceUsable(alice, nonceSpace, nonce)); } } diff --git a/test/unit/PWNSimpleLoan.t.sol b/test/unit/PWNSimpleLoan.t.sol index 5ab973b..c1fe05f 100644 --- a/test/unit/PWNSimpleLoan.t.sol +++ b/test/unit/PWNSimpleLoan.t.sol @@ -165,7 +165,7 @@ abstract contract PWNSimpleLoanTest is Test { _mockLOANTokenOwner(loanId, lender); vm.mockCall( - revokedNonce, abi.encodeWithSignature("isNonceRevoked(address,uint256,uint256)"), abi.encode(false) + revokedNonce, abi.encodeWithSignature("isNonceUsable(address,uint256,uint256)"), abi.encode(true) ); } @@ -447,14 +447,14 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { contract PWNSimpleLoan_CreateLOANAndRevokeNonce_Test is PWNSimpleLoanTest { - function testFuzz_shouldFail_whenNonceAlreadyRevoked(uint256 nonceSpace, uint256 nonce) external { + function testFuzz_shouldFail_whenNonceNotUsable(uint256 nonceSpace, uint256 nonce) external { vm.mockCall( revokedNonce, - abi.encodeWithSignature("isNonceRevoked(address,uint256,uint256)", borrower, nonceSpace, nonce), - abi.encode(true) + abi.encodeWithSignature("isNonceUsable(address,uint256,uint256)", borrower, nonceSpace, nonce), + abi.encode(false) ); - vm.expectRevert(abi.encodeWithSelector(NonceAlreadyRevoked.selector)); + vm.expectRevert(abi.encodeWithSelector(NonceNotUsable.selector)); vm.prank(borrower); loan.createLOANAndRevokeNonce(loanFactory, loanFactoryData, "", "", "", nonceSpace, nonce); } @@ -1726,16 +1726,16 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { loan.extendLOAN(extension, "", ""); } - function test_shouldFail_whenOfferNonceRevoked() external { + function test_shouldFail_whenOfferNonceNotUsable() external { _mockExtensionOfferMade(extension); vm.mockCall( revokedNonce, - abi.encodeWithSignature("isNonceRevoked(address,uint256,uint256)", extension.proposer, extension.nonceSpace, extension.nonce), - abi.encode(true) + abi.encodeWithSignature("isNonceUsable(address,uint256,uint256)", extension.proposer, extension.nonceSpace, extension.nonce), + abi.encode(false) ); - vm.expectRevert(abi.encodeWithSelector(NonceAlreadyRevoked.selector)); + vm.expectRevert(abi.encodeWithSelector(NonceNotUsable.selector)); vm.prank(lender); loan.extendLOAN(extension, "", ""); } diff --git a/test/unit/PWNSimpleLoanListOffer.t.sol b/test/unit/PWNSimpleLoanListOffer.t.sol index bc50309..dade485 100644 --- a/test/unit/PWNSimpleLoanListOffer.t.sol +++ b/test/unit/PWNSimpleLoanListOffer.t.sol @@ -62,8 +62,8 @@ abstract contract PWNSimpleLoanListOfferTest is Test { vm.mockCall( revokedOfferNonce, - abi.encodeWithSignature("isNonceRevoked(address,uint256,uint256)"), - abi.encode(false) + abi.encodeWithSignature("isNonceUsable(address,uint256,uint256)"), + abi.encode(true) ); } @@ -273,20 +273,20 @@ contract PWNSimpleLoanListOffer_CreateLOANTerms_Test is PWNSimpleLoanListOfferTe offerContract.createLOANTerms(borrower, abi.encode(offer, offerValues), signature); } - function test_shouldFail_whenOfferIsRevoked() external { + function test_shouldFail_whenOfferNonceNotUsable() external { signature = _signOfferCompact(lenderPK, offer); vm.mockCall( revokedOfferNonce, - abi.encodeWithSignature("isNonceRevoked(address,uint256,uint256)"), - abi.encode(true) + abi.encodeWithSignature("isNonceUsable(address,uint256,uint256)"), + abi.encode(false) ); vm.expectCall( revokedOfferNonce, - abi.encodeWithSignature("isNonceRevoked(address,uint256,uint256)", offer.lender, offer.nonceSpace, offer.nonce) + abi.encodeWithSignature("isNonceUsable(address,uint256,uint256)", offer.lender, offer.nonceSpace, offer.nonce) ); - vm.expectRevert(abi.encodeWithSelector(NonceAlreadyRevoked.selector)); + vm.expectRevert(abi.encodeWithSelector(NonceNotUsable.selector)); vm.prank(activeLoanContract); offerContract.createLOANTerms(borrower, abi.encode(offer, offerValues), signature); } diff --git a/test/unit/PWNSimpleLoanSimpleOffer.t.sol b/test/unit/PWNSimpleLoanSimpleOffer.t.sol index 6949c8e..2224710 100644 --- a/test/unit/PWNSimpleLoanSimpleOffer.t.sol +++ b/test/unit/PWNSimpleLoanSimpleOffer.t.sol @@ -56,8 +56,8 @@ abstract contract PWNSimpleLoanSimpleOfferTest is Test { vm.mockCall( revokedOfferNonce, - abi.encodeWithSignature("isNonceRevoked(address,uint256,uint256)"), - abi.encode(false) + abi.encodeWithSignature("isNonceUsable(address,uint256,uint256)"), + abi.encode(true) ); } @@ -267,20 +267,20 @@ contract PWNSimpleLoanSimpleOffer_CreateLOANTerms_Test is PWNSimpleLoanSimpleOff offerContract.createLOANTerms(borrower, abi.encode(offer), signature); } - function test_shouldFail_whenOfferIsRevoked() external { + function test_shouldFail_whenOfferNonceNotUsable() external { signature = _signOfferCompact(lenderPK, offer); vm.mockCall( revokedOfferNonce, - abi.encodeWithSignature("isNonceRevoked(address,uint256,uint256)"), - abi.encode(true) + abi.encodeWithSignature("isNonceUsable(address,uint256,uint256)"), + abi.encode(false) ); vm.expectCall( revokedOfferNonce, - abi.encodeWithSignature("isNonceRevoked(address,uint256,uint256)", offer.lender, offer.nonceSpace, offer.nonce) + abi.encodeWithSignature("isNonceUsable(address,uint256,uint256)", offer.lender, offer.nonceSpace, offer.nonce) ); - vm.expectRevert(abi.encodeWithSelector(NonceAlreadyRevoked.selector)); + vm.expectRevert(abi.encodeWithSelector(NonceNotUsable.selector)); vm.prank(activeLoanContract); offerContract.createLOANTerms(borrower, abi.encode(offer), signature); } diff --git a/test/unit/PWNSimpleLoanSimpleRequest.t.sol b/test/unit/PWNSimpleLoanSimpleRequest.t.sol index fe2a653..416782a 100644 --- a/test/unit/PWNSimpleLoanSimpleRequest.t.sol +++ b/test/unit/PWNSimpleLoanSimpleRequest.t.sol @@ -56,8 +56,8 @@ abstract contract PWNSimpleLoanSimpleRequestTest is Test { vm.mockCall( revokedRequestNonce, - abi.encodeWithSignature("isNonceRevoked(address,uint256,uint256)"), - abi.encode(false) + abi.encodeWithSignature("isNonceUsable(address,uint256,uint256)"), + abi.encode(true) ); } @@ -268,20 +268,20 @@ contract PWNSimpleLoanSimpleRequest_CreateLOANTerms_Test is PWNSimpleLoanSimpleR requestContract.createLOANTerms(lender, abi.encode(request), signature); } - function test_shouldFail_whenRequestIsRevoked() external { + function test_shouldFail_whenRequestNonceNotUsable() external { signature = _signRequestCompact(borrowerPK, request); vm.mockCall( revokedRequestNonce, - abi.encodeWithSignature("isNonceRevoked(address,uint256,uint256)"), - abi.encode(true) + abi.encodeWithSignature("isNonceUsable(address,uint256,uint256)"), + abi.encode(false) ); vm.expectCall( revokedRequestNonce, - abi.encodeWithSignature("isNonceRevoked(address,uint256,uint256)", request.borrower, request.nonceSpace, request.nonce) + abi.encodeWithSignature("isNonceUsable(address,uint256,uint256)", request.borrower, request.nonceSpace, request.nonce) ); - vm.expectRevert(abi.encodeWithSelector(NonceAlreadyRevoked.selector)); + vm.expectRevert(abi.encodeWithSelector(NonceNotUsable.selector)); vm.prank(activeLoanContract); requestContract.createLOANTerms(lender, abi.encode(request), signature); } From 6bed8704b09aa26e43d6c0519c2bfd152c57cc94 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Thu, 7 Mar 2024 16:08:47 +0100 Subject: [PATCH 038/129] refactor: change import style --- src/loan/terms/PWNLOANTerms.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/loan/terms/PWNLOANTerms.sol b/src/loan/terms/PWNLOANTerms.sol index ed77c08..85a28cb 100644 --- a/src/loan/terms/PWNLOANTerms.sol +++ b/src/loan/terms/PWNLOANTerms.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; -import "MultiToken/MultiToken.sol"; +import { MultiToken } from "MultiToken/MultiToken.sol"; library PWNLOANTerms { From b7257fb65adef0f56cd079bfa993476e640b9bdf Mon Sep 17 00:00:00 2001 From: ashhanai Date: Thu, 7 Mar 2024 16:58:15 +0100 Subject: [PATCH 039/129] feat(available-credit-limit): add availabel credit limit value to offers / requests to allow accepting them more than once --- src/PWNErrors.sol | 1 + .../factory/offer/PWNSimpleLoanListOffer.sol | 46 ++++++++++-- .../offer/PWNSimpleLoanSimpleOffer.sol | 46 ++++++++++-- .../request/PWNSimpleLoanSimpleRequest.sol | 42 ++++++++++- test/unit/PWNSimpleLoanListOffer.t.sol | 71 +++++++++++++++---- test/unit/PWNSimpleLoanSimpleOffer.t.sol | 71 +++++++++++++++---- test/unit/PWNSimpleLoanSimpleRequest.t.sol | 69 +++++++++++++++++- 7 files changed, 302 insertions(+), 44 deletions(-) diff --git a/src/PWNErrors.sol b/src/PWNErrors.sol index 71e7356..b941440 100644 --- a/src/PWNErrors.sol +++ b/src/PWNErrors.sol @@ -54,6 +54,7 @@ error InvalidCreateTerms(); error InvalidRefinanceTerms(); error InvalidRefinancingLoanId(uint256 refinancingLoanId); error AccruingInterestAPROutOfBounds(uint40 providedAPR, uint40 maxAPR); +error AvailableCreditLimitExceeded(uint256 usedCredit, uint256 availableCreditLimit); // Input data error InvalidInputData(); diff --git a/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol b/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol index b1da33f..e9e480a 100644 --- a/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol +++ b/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol @@ -31,7 +31,7 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanTermsFactory, PWNHubAccessContro * @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 loanAssetAddress,uint256 loanAmount,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedBorrower,address lender,bool isPersistent,uint256 nonceSpace,uint256 nonce)" + "Offer(uint8 collateralCategory,address collateralAddress,bytes32 collateralIdsWhitelistMerkleRoot,uint256 collateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address loanAssetAddress,uint256 loanAmount,uint256 availableCreditLimit,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedBorrower,address lender,uint256 nonceSpace,uint256 nonce)" ); bytes32 public immutable DOMAIN_SEPARATOR; @@ -46,6 +46,12 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanTermsFactory, PWNHubAccessContro */ mapping (bytes32 => bool) public offersMade; + /** + * @dev Mapping of credit used by an offer. + * (offer hash => credit used) + */ + mapping (bytes32 => uint256) private _creditUsed; + /** * @notice Construct defining a list offer. * @param collateralCategory Category of an asset used as a collateral (0 == ERC20, 1 == ERC721, 2 == ERC1155). @@ -56,13 +62,13 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanTermsFactory, PWNHubAccessContro * @param collateralStateFingerprint Fingerprint of a collateral state. It is used to check if a collateral is in a valid state. * @param loanAssetAddress Address of an asset which is lender to a borrower. * @param loanAmount 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 loan asset 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 isPersistent If true, offer will not be revoked on acceptance. Persistent offer can be revoked manually. * @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. @@ -76,13 +82,13 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanTermsFactory, PWNHubAccessContro bytes32 collateralStateFingerprint; address loanAssetAddress; uint256 loanAmount; + uint256 availableCreditLimit; uint256 fixedInterestAmount; uint40 accruingInterestAPR; uint32 duration; uint40 expiration; address allowedBorrower; address lender; - bool isPersistent; uint256 nonceSpace; uint256 nonce; } @@ -153,6 +159,24 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanTermsFactory, PWNHubAccessContro offersMade[offerHash] = true; } + /** + * @notice Get available credit for an offer. + * @param offer Offer struct containing all needed offer data. + * @return Available credit for an offer. + */ + function availableCredit(Offer calldata offer) external view returns (uint256) { + return offer.availableCreditLimit - _creditUsed[getOfferHash(offer)]; + } + + /** + * @notice Get credit used for an offer. + * @param offer Offer struct containing all needed offer data. + * @return Credit used for an offer. + */ + function creditUsed(Offer calldata offer) external view returns (uint256) { + return _creditUsed[getOfferHash(offer)]; + } + /** * @notice Helper function for revoking an offer nonce on behalf of a caller. * @param offerNonceSpace Nonce space of an offer nonce to be revoked. @@ -238,6 +262,18 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanTermsFactory, PWNHubAccessContro } } + // Check that the available credit limit is not exceeded + if (offer.availableCreditLimit == 0) { + revokedOfferNonce.revokeNonce(lender, offer.nonceSpace, offer.nonce); + } else if (_creditUsed[offerHash] + offer.loanAmount <= offer.availableCreditLimit) { + _creditUsed[offerHash] += offer.loanAmount; + } else { + revert AvailableCreditLimitExceeded({ + usedCredit: _creditUsed[offerHash] + offer.loanAmount, + availableCreditLimit: offer.availableCreditLimit + }); + } + // Create loan terms object loanTerms = PWNLOANTerms.Simple({ lender: lender, @@ -259,10 +295,6 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanTermsFactory, PWNHubAccessContro canRefinance: true, refinancingLoanId: 0 }); - - // Revoke offer if not persistent - if (!offer.isPersistent) - revokedOfferNonce.revokeNonce(lender, offer.nonceSpace, offer.nonce); } diff --git a/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol b/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol index 2b735e1..456d542 100644 --- a/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol +++ b/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol @@ -28,7 +28,7 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanTermsFactory, PWNHubAccessCont * @dev EIP-712 simple offer struct type hash. */ bytes32 public constant OFFER_TYPEHASH = keccak256( - "Offer(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address loanAssetAddress,uint256 loanAmount,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedBorrower,address lender,bool isPersistent,uint256 nonceSpace,uint256 nonce)" + "Offer(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address loanAssetAddress,uint256 loanAmount,uint256 availableCreditLimit,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedBorrower,address lender,uint256 nonceSpace,uint256 nonce)" ); bytes32 public immutable DOMAIN_SEPARATOR; @@ -43,6 +43,12 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanTermsFactory, PWNHubAccessCont */ mapping (bytes32 => bool) public offersMade; + /** + * @dev Mapping of credit used by an offer. + * (offer hash => credit used) + */ + mapping (bytes32 => uint256) private _creditUsed; + /** * @notice Construct defining a simple offer. * @param collateralCategory Category of an asset used as a collateral (0 == ERC20, 1 == ERC721, 2 == ERC1155). @@ -53,13 +59,13 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanTermsFactory, PWNHubAccessCont * @param collateralStateFingerprint Fingerprint of a collateral state. It is used to check if a collateral is in a valid state. * @param loanAssetAddress Address of an asset which is lender to a borrower. * @param loanAmount 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 loan asset 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 isPersistent If true, offer will not be revoked on acceptance. Persistent offer can be revoked manually. * @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. @@ -73,13 +79,13 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanTermsFactory, PWNHubAccessCont bytes32 collateralStateFingerprint; address loanAssetAddress; uint256 loanAmount; + uint256 availableCreditLimit; uint256 fixedInterestAmount; uint40 accruingInterestAPR; uint32 duration; uint40 expiration; address allowedBorrower; address lender; - bool isPersistent; uint256 nonceSpace; uint256 nonce; } @@ -138,6 +144,24 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanTermsFactory, PWNHubAccessCont offersMade[offerHash] = true; } + /** + * @notice Get available credit for an offer. + * @param offer Offer struct containing all needed offer data. + * @return Available credit for an offer. + */ + function availableCredit(Offer calldata offer) external view returns (uint256) { + return offer.availableCreditLimit - _creditUsed[getOfferHash(offer)]; + } + + /** + * @notice Get credit used for an offer. + * @param offer Offer struct containing all needed offer data. + * @return Credit used for an offer. + */ + function creditUsed(Offer calldata offer) external view returns (uint256) { + return _creditUsed[getOfferHash(offer)]; + } + /** * @notice Helper function for revoking an offer nonce on behalf of a caller. * @param offerNonceSpace Nonce space of an offer nonce to be revoked. @@ -211,6 +235,18 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanTermsFactory, PWNHubAccessCont } } + // Check that the available credit limit is not exceeded + if (offer.availableCreditLimit == 0) { + revokedOfferNonce.revokeNonce(lender, offer.nonceSpace, offer.nonce); + } else if (_creditUsed[offerHash] + offer.loanAmount <= offer.availableCreditLimit) { + _creditUsed[offerHash] += offer.loanAmount; + } else { + revert AvailableCreditLimitExceeded({ + usedCredit: _creditUsed[offerHash] + offer.loanAmount, + availableCreditLimit: offer.availableCreditLimit + }); + } + // Create loan terms object loanTerms = PWNLOANTerms.Simple({ lender: lender, @@ -232,10 +268,6 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanTermsFactory, PWNHubAccessCont canRefinance: true, refinancingLoanId: 0 }); - - // Revoke offer if not persistent - if (!offer.isPersistent) - revokedOfferNonce.revokeNonce(lender, offer.nonceSpace, offer.nonce); } diff --git a/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol b/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol index f37c4d9..598a6df 100644 --- a/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol +++ b/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol @@ -28,7 +28,7 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanTermsFactory, PWNHubAccessCo * @dev EIP-712 simple request struct type hash. */ bytes32 public constant REQUEST_TYPEHASH = keccak256( - "Request(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address loanAssetAddress,uint256 loanAmount,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedLender,address borrower,uint256 refinancingLoanId,uint256 nonceSpace,uint256 nonce)" + "Request(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address loanAssetAddress,uint256 loanAmount,uint256 availableCreditLimit,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedLender,address borrower,uint256 refinancingLoanId,uint256 nonceSpace,uint256 nonce)" ); bytes32 public immutable DOMAIN_SEPARATOR; @@ -43,6 +43,12 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanTermsFactory, PWNHubAccessCo */ mapping (bytes32 => bool) public requestsMade; + /** + * @dev Mapping of credit used by a request. + * (request hash => credit used) + */ + mapping (bytes32 => uint256) private _creditUsed; + /** * @notice Construct defining a simple request. * @param collateralCategory Category of an asset used as a collateral (0 == ERC20, 1 == ERC721, 2 == ERC1155). @@ -53,6 +59,7 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanTermsFactory, PWNHubAccessCo * @param collateralStateFingerprint Fingerprint of a collateral state defined by ERC5646. * @param loanAssetAddress Address of an asset which is lender to a borrower. * @param loanAmount Amount of tokens which is requested as a loan to a borrower. + * @param availableCreditLimit Available credit limit for the request. It is the maximum amount of tokens which can be borrowed using the request. * @param fixedInterestAmount Fixed interest amount in loan asset 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. @@ -73,6 +80,7 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanTermsFactory, PWNHubAccessCo bytes32 collateralStateFingerprint; address loanAssetAddress; uint256 loanAmount; + uint256 availableCreditLimit; uint256 fixedInterestAmount; uint40 accruingInterestAPR; uint32 duration; @@ -138,6 +146,24 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanTermsFactory, PWNHubAccessCo requestsMade[requestHash] = true; } + /** + * @notice Get available credit for a request. + * @param request Request struct containing all needed request data. + * @return Available credit for a request. + */ + function availableCredit(Request calldata request) external view returns (uint256) { + return request.availableCreditLimit - _creditUsed[getRequestHash(request)]; + } + + /** + * @notice Get credit used for a request. + * @param request Request struct containing all needed request data. + * @return Credit used for a request. + */ + function creditUsed(Request calldata request) external view returns (uint256) { + return _creditUsed[getRequestHash(request)]; + } + /** * @notice Helper function for revoking a request nonce on behalf of a caller. * @param requestNonceSpace Nonce space of a request nonce to be revoked. @@ -211,6 +237,18 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanTermsFactory, PWNHubAccessCo } } + // Check that the available credit limit is not exceeded + if (request.availableCreditLimit == 0) { + revokedRequestNonce.revokeNonce(borrower, request.nonceSpace, request.nonce); + } else if (_creditUsed[requestHash] + request.loanAmount <= request.availableCreditLimit) { + _creditUsed[requestHash] += request.loanAmount; + } else { + revert AvailableCreditLimitExceeded({ + usedCredit: _creditUsed[requestHash] + request.loanAmount, + availableCreditLimit: request.availableCreditLimit + }); + } + // Create loan terms object loanTerms = PWNLOANTerms.Simple({ lender: lender, @@ -232,8 +270,6 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanTermsFactory, PWNHubAccessCo canRefinance: request.refinancingLoanId != 0, refinancingLoanId: request.refinancingLoanId }); - - revokedRequestNonce.revokeNonce(borrower, request.nonceSpace, request.nonce); } diff --git a/test/unit/PWNSimpleLoanListOffer.t.sol b/test/unit/PWNSimpleLoanListOffer.t.sol index dade485..b2bf46a 100644 --- a/test/unit/PWNSimpleLoanListOffer.t.sol +++ b/test/unit/PWNSimpleLoanListOffer.t.sol @@ -14,6 +14,7 @@ import "@pwn/PWNErrors.sol"; abstract contract PWNSimpleLoanListOfferTest is Test { bytes32 internal constant OFFERS_MADE_SLOT = bytes32(uint256(0)); // `offersMade` mapping position + bytes32 internal constant CREDIT_USED_SLOT = bytes32(uint256(1)); // `_creditUsed` mapping position PWNSimpleLoanListOffer offerContract; address hub = address(0x80b); @@ -44,13 +45,13 @@ abstract contract PWNSimpleLoanListOfferTest is Test { collateralStateFingerprint: keccak256("some state fingerprint"), loanAssetAddress: token, loanAmount: 1101001, + availableCreditLimit: 0, fixedInterestAmount: 1, accruingInterestAPR: 0, duration: 1000, expiration: 60303, allowedBorrower: address(0), lender: lender, - isPersistent: false, nonceSpace: 1, nonce: uint256(keccak256("nonce_1")) }); @@ -79,7 +80,7 @@ abstract contract PWNSimpleLoanListOfferTest is Test { address(offerContract) )), keccak256(abi.encodePacked( - keccak256("Offer(uint8 collateralCategory,address collateralAddress,bytes32 collateralIdsWhitelistMerkleRoot,uint256 collateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address loanAssetAddress,uint256 loanAmount,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedBorrower,address lender,bool isPersistent,uint256 nonceSpace,uint256 nonce)"), + keccak256("Offer(uint8 collateralCategory,address collateralAddress,bytes32 collateralIdsWhitelistMerkleRoot,uint256 collateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address loanAssetAddress,uint256 loanAmount,uint256 availableCreditLimit,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedBorrower,address lender,uint256 nonceSpace,uint256 nonce)"), abi.encode(_offer) )) )); @@ -119,11 +120,44 @@ contract PWNSimpleLoanListOffer_MakeOffer_Test is PWNSimpleLoanListOfferTest { } +/*----------------------------------------------------------*| +|* # AVAILABLE CREDIT *| +|*----------------------------------------------------------*/ + +contract PWNSimpleLoanListOffer_AvailableCredit_Test is PWNSimpleLoanListOfferTest { + + function testFuzz_shouldReturnAvailableCredit(uint256 used, uint256 limit) external { + limit = bound(limit, used, type(uint256).max); + offer.availableCreditLimit = limit; + + vm.store(address(offerContract), keccak256(abi.encode(_offerHash(offer), CREDIT_USED_SLOT)), bytes32(used)); + + assertEq(offerContract.availableCredit(offer), limit - used); + } + +} + + +/*----------------------------------------------------------*| +|* # 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(offer), used); + } + +} + + /*----------------------------------------------------------*| |* # REVOKE OFFER NONCE *| |*----------------------------------------------------------*/ -contract PWNSimpleLoanOffer_RevokeOfferNonce_Test is PWNSimpleLoanListOfferTest { +contract PWNSimpleLoanListOffer_RevokeOfferNonce_Test is PWNSimpleLoanListOfferTest { function testFuzz_shouldCallRevokeOfferNonce(uint256 nonceSpace, uint256 nonce) external { vm.expectCall( @@ -381,8 +415,8 @@ contract PWNSimpleLoanListOffer_CreateLOANTerms_Test is PWNSimpleLoanListOfferTe offerContract.createLOANTerms(borrower, abi.encode(offer, offerValues), signature); } - function test_shouldRevokeOffer_whenIsNotPersistent() external { - offer.isPersistent = false; + function test_shouldRevokeOffer_whenAvailableCreditLimitEqualToZero() external { + offer.availableCreditLimit = 0; signature = _signOfferCompact(lenderPK, offer); vm.expectCall( @@ -394,20 +428,33 @@ contract PWNSimpleLoanListOffer_CreateLOANTerms_Test is PWNSimpleLoanListOfferTe offerContract.createLOANTerms(borrower, abi.encode(offer, offerValues), signature); } - function test_shouldNotRevokeOffer_whenIsPersistent() external { - offer.isPersistent = true; + function testFuzz_shouldFail_whenUsedCreditExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { + used = bound(used, 1, type(uint256).max - offer.loanAmount); + limit = bound(limit, used, used + offer.loanAmount - 1); + offer.availableCreditLimit = limit; signature = _signOfferCompact(lenderPK, offer); - vm.expectCall({ - callee: revokedOfferNonce, - data: abi.encodeWithSignature("revokeNonce(address,uint256,uint256)", offer.lender, offer.nonceSpace, offer.nonce), - count: 0 - }); + vm.store(address(offerContract), keccak256(abi.encode(_offerHash(offer), CREDIT_USED_SLOT)), bytes32(used)); + vm.expectRevert(abi.encodeWithSelector(AvailableCreditLimitExceeded.selector, used + offer.loanAmount, limit)); vm.prank(activeLoanContract); offerContract.createLOANTerms(borrower, abi.encode(offer, offerValues), signature); } + function testFuzz_shouldIncreaseUsedCredit_whenUsedCreditNotExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { + used = bound(used, 1, type(uint256).max - offer.loanAmount); + limit = bound(limit, used + offer.loanAmount, type(uint256).max); + offer.availableCreditLimit = limit; + signature = _signOfferCompact(lenderPK, offer); + + vm.store(address(offerContract), keccak256(abi.encode(_offerHash(offer), CREDIT_USED_SLOT)), bytes32(used)); + + vm.prank(activeLoanContract); + offerContract.createLOANTerms(borrower, abi.encode(offer, offerValues), signature); + + assertEq(offerContract.creditUsed(offer), used + offer.loanAmount); + } + function test_shouldAcceptAnyCollateralId_whenMerkleRootIsZero() external { offerValues.collateralId = 331; offer.collateralIdsWhitelistMerkleRoot = bytes32(0); diff --git a/test/unit/PWNSimpleLoanSimpleOffer.t.sol b/test/unit/PWNSimpleLoanSimpleOffer.t.sol index 2224710..a08d7dd 100644 --- a/test/unit/PWNSimpleLoanSimpleOffer.t.sol +++ b/test/unit/PWNSimpleLoanSimpleOffer.t.sol @@ -14,6 +14,7 @@ import "@pwn/PWNErrors.sol"; abstract contract PWNSimpleLoanSimpleOfferTest is Test { bytes32 internal constant OFFERS_MADE_SLOT = bytes32(uint256(0)); // `offersMade` mapping position + bytes32 internal constant CREDIT_USED_SLOT = bytes32(uint256(1)); // `_creditUsed` mapping position PWNSimpleLoanSimpleOffer offerContract; address hub = address(0x80b); @@ -43,13 +44,13 @@ abstract contract PWNSimpleLoanSimpleOfferTest is Test { collateralStateFingerprint: keccak256("some state fingerprint"), loanAssetAddress: token, loanAmount: 1101001, + availableCreditLimit: 0, fixedInterestAmount: 1, accruingInterestAPR: 0, duration: 1000, expiration: 60303, allowedBorrower: address(0), lender: lender, - isPersistent: false, nonceSpace: 1, nonce: uint256(keccak256("nonce_1")) }); @@ -73,7 +74,7 @@ abstract contract PWNSimpleLoanSimpleOfferTest is Test { address(offerContract) )), keccak256(abi.encodePacked( - keccak256("Offer(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address loanAssetAddress,uint256 loanAmount,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedBorrower,address lender,bool isPersistent,uint256 nonceSpace,uint256 nonce)"), + keccak256("Offer(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address loanAssetAddress,uint256 loanAmount,uint256 availableCreditLimit,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedBorrower,address lender,uint256 nonceSpace,uint256 nonce)"), abi.encode(_offer) )) )); @@ -113,11 +114,44 @@ contract PWNSimpleLoanSimpleOffer_MakeOffer_Test is PWNSimpleLoanSimpleOfferTest } +/*----------------------------------------------------------*| +|* # AVAILABLE CREDIT *| +|*----------------------------------------------------------*/ + +contract PWNSimpleLoanSimpleOffer_AvailableCredit_Test is PWNSimpleLoanSimpleOfferTest { + + function testFuzz_shouldReturnAvailableCredit(uint256 used, uint256 limit) external { + limit = bound(limit, used, type(uint256).max); + offer.availableCreditLimit = limit; + + vm.store(address(offerContract), keccak256(abi.encode(_offerHash(offer), CREDIT_USED_SLOT)), bytes32(used)); + + assertEq(offerContract.availableCredit(offer), limit - used); + } + +} + + +/*----------------------------------------------------------*| +|* # CREDIT USED *| +|*----------------------------------------------------------*/ + +contract PWNSimpleLoanSimpleOffer_CreditUsed_Test is PWNSimpleLoanSimpleOfferTest { + + function testFuzz_shouldReturnUsedCredit(uint256 used) external { + vm.store(address(offerContract), keccak256(abi.encode(_offerHash(offer), CREDIT_USED_SLOT)), bytes32(used)); + + assertEq(offerContract.creditUsed(offer), used); + } + +} + + /*----------------------------------------------------------*| |* # REVOKE OFFER NONCE *| |*----------------------------------------------------------*/ -contract PWNSimpleLoanOffer_RevokeOfferNonce_Test is PWNSimpleLoanSimpleOfferTest { +contract PWNSimpleLoanSimpleOffer_RevokeOfferNonce_Test is PWNSimpleLoanSimpleOfferTest { function testFuzz_shouldCallRevokeOfferNonce(uint256 nonceSpace, uint256 nonce) external { vm.expectCall( @@ -375,8 +409,8 @@ contract PWNSimpleLoanSimpleOffer_CreateLOANTerms_Test is PWNSimpleLoanSimpleOff offerContract.createLOANTerms(borrower, abi.encode(offer), signature); } - function test_shouldRevokeOffer_whenIsNotPersistent() external { - offer.isPersistent = false; + function test_shouldRevokeOffer_whenAvailableCreditLimitEqualToZero() external { + offer.availableCreditLimit = 0; signature = _signOfferCompact(lenderPK, offer); vm.expectCall( @@ -388,20 +422,33 @@ contract PWNSimpleLoanSimpleOffer_CreateLOANTerms_Test is PWNSimpleLoanSimpleOff offerContract.createLOANTerms(borrower, abi.encode(offer), signature); } - function test_shouldNotRevokeOffer_whenIsPersistent() external { - offer.isPersistent = true; + function testFuzz_shouldFail_whenUsedCreditExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { + used = bound(used, 1, type(uint256).max - offer.loanAmount); + limit = bound(limit, used, used + offer.loanAmount - 1); + offer.availableCreditLimit = limit; signature = _signOfferCompact(lenderPK, offer); - vm.expectCall({ - callee: revokedOfferNonce, - data: abi.encodeWithSignature("revokeNonce(address,uint256,uint256)", offer.lender, offer.nonceSpace, offer.nonce), - count: 0 - }); + vm.store(address(offerContract), keccak256(abi.encode(_offerHash(offer), CREDIT_USED_SLOT)), bytes32(used)); + vm.expectRevert(abi.encodeWithSelector(AvailableCreditLimitExceeded.selector, used + offer.loanAmount, limit)); vm.prank(activeLoanContract); offerContract.createLOANTerms(borrower, abi.encode(offer), signature); } + function testFuzz_shouldIncreaseUsedCredit_whenUsedCreditNotExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { + used = bound(used, 1, type(uint256).max - offer.loanAmount); + limit = bound(limit, used + offer.loanAmount, type(uint256).max); + offer.availableCreditLimit = limit; + signature = _signOfferCompact(lenderPK, offer); + + vm.store(address(offerContract), keccak256(abi.encode(_offerHash(offer), CREDIT_USED_SLOT)), bytes32(used)); + + vm.prank(activeLoanContract); + offerContract.createLOANTerms(borrower, abi.encode(offer), signature); + + assertEq(offerContract.creditUsed(offer), used + offer.loanAmount); + } + function test_shouldReturnCorrectValues() external { uint256 currentTimestamp = 40303; vm.warp(currentTimestamp); diff --git a/test/unit/PWNSimpleLoanSimpleRequest.t.sol b/test/unit/PWNSimpleLoanSimpleRequest.t.sol index 416782a..f548e6e 100644 --- a/test/unit/PWNSimpleLoanSimpleRequest.t.sol +++ b/test/unit/PWNSimpleLoanSimpleRequest.t.sol @@ -14,6 +14,7 @@ import "@pwn/PWNErrors.sol"; abstract contract PWNSimpleLoanSimpleRequestTest is Test { bytes32 internal constant REQUESTS_MADE_SLOT = bytes32(uint256(0)); // `requestsMade` mapping position + bytes32 internal constant CREDIT_USED_SLOT = bytes32(uint256(1)); // `_creditUsed` mapping position PWNSimpleLoanSimpleRequest requestContract; address hub = address(0x80b); @@ -43,6 +44,7 @@ abstract contract PWNSimpleLoanSimpleRequestTest is Test { collateralStateFingerprint: keccak256("some state fingerprint"), loanAssetAddress: token, loanAmount: 1101001, + availableCreditLimit: 0, fixedInterestAmount: 1, accruingInterestAPR: 0, duration: 1000, @@ -73,7 +75,7 @@ abstract contract PWNSimpleLoanSimpleRequestTest is Test { address(requestContract) )), keccak256(abi.encodePacked( - keccak256("Request(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address loanAssetAddress,uint256 loanAmount,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedLender,address borrower,uint256 refinancingLoanId,uint256 nonceSpace,uint256 nonce)"), + keccak256("Request(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address loanAssetAddress,uint256 loanAmount,uint256 availableCreditLimit,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedLender,address borrower,uint256 refinancingLoanId,uint256 nonceSpace,uint256 nonce)"), abi.encode(_request) )) )); @@ -114,11 +116,44 @@ contract PWNSimpleLoanSimpleRequest_MakeRequest_Test is PWNSimpleLoanSimpleReque } +/*----------------------------------------------------------*| +|* # AVAILABLE CREDIT *| +|*----------------------------------------------------------*/ + +contract PWNSimpleLoanSimpleRequest_AvailableCredit_Test is PWNSimpleLoanSimpleRequestTest { + + function testFuzz_shouldReturnAvailableCredit(uint256 used, uint256 limit) external { + limit = bound(limit, used, type(uint256).max); + request.availableCreditLimit = limit; + + vm.store(address(requestContract), keccak256(abi.encode(_requestHash(request), CREDIT_USED_SLOT)), bytes32(used)); + + assertEq(requestContract.availableCredit(request), limit - used); + } + +} + + +/*----------------------------------------------------------*| +|* # CREDIT USED *| +|*----------------------------------------------------------*/ + +contract PWNSimpleLoanSimpleRequest_CreditUsed_Test is PWNSimpleLoanSimpleRequestTest { + + function testFuzz_shouldReturnUsedCredit(uint256 used) external { + vm.store(address(requestContract), keccak256(abi.encode(_requestHash(request), CREDIT_USED_SLOT)), bytes32(used)); + + assertEq(requestContract.creditUsed(request), used); + } + +} + + /*----------------------------------------------------------*| |* # REVOKE REQUEST NONCE *| |*----------------------------------------------------------*/ -contract PWNSimpleLoanRequest_RevokeRequestNonce_Test is PWNSimpleLoanSimpleRequestTest { +contract PWNSimpleLoanSimpleRequest_RevokeRequestNonce_Test is PWNSimpleLoanSimpleRequestTest { function testFuzz_shouldCallRevokeRequestNonce(uint256 nonceSpace, uint256 nonce) external { vm.expectCall( @@ -376,7 +411,8 @@ contract PWNSimpleLoanSimpleRequest_CreateLOANTerms_Test is PWNSimpleLoanSimpleR requestContract.createLOANTerms(lender, abi.encode(request), signature); } - function test_shouldRevokeRequest() external { + function test_shouldRevokeRequest_whenAvailableCreditLimitEqualToZero() external { + request.availableCreditLimit = 0; signature = _signRequestCompact(borrowerPK, request); vm.expectCall( @@ -388,6 +424,33 @@ contract PWNSimpleLoanSimpleRequest_CreateLOANTerms_Test is PWNSimpleLoanSimpleR requestContract.createLOANTerms(lender, abi.encode(request), signature); } + function testFuzz_shouldFail_whenUsedCreditExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { + used = bound(used, 1, type(uint256).max - request.loanAmount); + limit = bound(limit, used, used + request.loanAmount - 1); + request.availableCreditLimit = limit; + signature = _signRequestCompact(borrowerPK, request); + + vm.store(address(requestContract), keccak256(abi.encode(_requestHash(request), CREDIT_USED_SLOT)), bytes32(used)); + + vm.expectRevert(abi.encodeWithSelector(AvailableCreditLimitExceeded.selector, used + request.loanAmount, limit)); + vm.prank(activeLoanContract); + requestContract.createLOANTerms(lender, abi.encode(request), signature); + } + + function testFuzz_shouldIncreaseUsedCredit_whenUsedCreditNotExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { + used = bound(used, 1, type(uint256).max - request.loanAmount); + limit = bound(limit, used + request.loanAmount, type(uint256).max); + request.availableCreditLimit = limit; + signature = _signRequestCompact(borrowerPK, request); + + vm.store(address(requestContract), keccak256(abi.encode(_requestHash(request), CREDIT_USED_SLOT)), bytes32(used)); + + vm.prank(activeLoanContract); + requestContract.createLOANTerms(lender, abi.encode(request), signature); + + assertEq(requestContract.creditUsed(request), used + request.loanAmount); + } + function testFuzz_shouldReturnCorrectValues(uint256 _refinancingLoanId) external { request.refinancingLoanId = _refinancingLoanId; From 394f9effff41bd9bede99dd78f865927cbbe3ac2 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Tue, 12 Mar 2024 17:46:42 +0100 Subject: [PATCH 040/129] feat: refactor loan creation flow Loan creation / refactor starts in proposal contract, passing a loan contract address in the propsal. --- script/PWN.s.sol | 12 +- src/Deployments.sol | 6 +- src/PWNErrors.sol | 35 +- src/hub/PWNHubTags.sol | 6 +- src/loan/terms/PWNLOANTerms.sol | 36 - .../factory/PWNSimpleLoanTermsFactory.sol | 31 - .../factory/offer/PWNSimpleLoanListOffer.sol | 336 ------- .../offer/PWNSimpleLoanSimpleOffer.sol | 308 ------- .../request/PWNSimpleLoanSimpleRequest.sol | 310 ------- src/loan/terms/simple/loan/PWNSimpleLoan.sol | 232 ++--- .../simple/proposal/PWNSimpleLoanProposal.sol | 233 +++++ .../proposal/offer/PWNSimpleLoanListOffer.sol | 277 ++++++ .../offer/PWNSimpleLoanSimpleOffer.sol | 234 +++++ .../request/PWNSimpleLoanSimpleRequest.sol | 235 +++++ test/helper/DeploymentTest.t.sol | 6 +- test/unit/PWNSimpleLoan.t.sol | 620 ++++++++----- test/unit/PWNSimpleLoanListOffer.t.sol | 863 +++++++++++++----- test/unit/PWNSimpleLoanSimpleOffer.t.sol | 777 +++++++++++----- test/unit/PWNSimpleLoanSimpleRequest.t.sol | 800 +++++++++++----- 19 files changed, 3234 insertions(+), 2123 deletions(-) delete mode 100644 src/loan/terms/PWNLOANTerms.sol delete mode 100644 src/loan/terms/simple/factory/PWNSimpleLoanTermsFactory.sol delete mode 100644 src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol delete mode 100644 src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol delete mode 100644 src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol create mode 100644 src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol create mode 100644 src/loan/terms/simple/proposal/offer/PWNSimpleLoanListOffer.sol create mode 100644 src/loan/terms/simple/proposal/offer/PWNSimpleLoanSimpleOffer.sol create mode 100644 src/loan/terms/simple/proposal/request/PWNSimpleLoanSimpleRequest.sol diff --git a/script/PWN.s.sol b/script/PWN.s.sol index 919d814..08480b4 100644 --- a/script/PWN.s.sol +++ b/script/PWN.s.sol @@ -13,9 +13,9 @@ 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/factory/offer/PWNSimpleLoanListOffer.sol"; -import { PWNSimpleLoanSimpleOffer } from "@pwn/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol"; -import { PWNSimpleLoanSimpleRequest } from "@pwn/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol"; +import { PWNSimpleLoanListOffer } from "@pwn/loan/terms/simple/proposal/offer/PWNSimpleLoanListOffer.sol"; +import { PWNSimpleLoanSimpleOffer } from "@pwn/loan/terms/simple/proposal/offer/PWNSimpleLoanSimpleOffer.sol"; +import { PWNSimpleLoanSimpleRequest } from "@pwn/loan/terms/simple/proposal/request/PWNSimpleLoanSimpleRequest.sol"; import { PWNLOAN } from "@pwn/loan/token/PWNLOAN.sol"; import { PWNRevokedNonce } from "@pwn/nonce/PWNRevokedNonce.sol"; import { Deployments } from "@pwn/Deployments.sol"; @@ -296,11 +296,11 @@ forge script script/PWN.s.sol:Setup \ bytes32[] memory tags = new bytes32[](8); tags[0] = PWNHubTags.ACTIVE_LOAN; tags[1] = PWNHubTags.NONCE_MANAGER; - tags[2] = PWNHubTags.SIMPLE_LOAN_TERMS_FACTORY; + tags[2] = PWNHubTags.LOAN_PROPOSAL; tags[3] = PWNHubTags.NONCE_MANAGER; - tags[4] = PWNHubTags.SIMPLE_LOAN_TERMS_FACTORY; + tags[4] = PWNHubTags.LOAN_PROPOSAL; tags[5] = PWNHubTags.NONCE_MANAGER; - tags[6] = PWNHubTags.SIMPLE_LOAN_TERMS_FACTORY; + tags[6] = PWNHubTags.LOAN_PROPOSAL; tags[7] = PWNHubTags.NONCE_MANAGER; bool success = GnosisSafeLike(protocolSafe).execTransaction({ diff --git a/src/Deployments.sol b/src/Deployments.sol index ee36944..44ff143 100644 --- a/src/Deployments.sol +++ b/src/Deployments.sol @@ -13,9 +13,9 @@ 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/factory/offer/PWNSimpleLoanListOffer.sol"; -import { PWNSimpleLoanSimpleOffer } from "@pwn/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol"; -import { PWNSimpleLoanSimpleRequest } from "@pwn/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol"; +import { PWNSimpleLoanListOffer } from "@pwn/loan/terms/simple/proposal/offer/PWNSimpleLoanListOffer.sol"; +import { PWNSimpleLoanSimpleOffer } from "@pwn/loan/terms/simple/proposal/offer/PWNSimpleLoanSimpleOffer.sol"; +import { PWNSimpleLoanSimpleRequest } from "@pwn/loan/terms/simple/proposal/request/PWNSimpleLoanSimpleRequest.sol"; import { PWNLOAN } from "@pwn/loan/token/PWNLOAN.sol"; import { PWNRevokedNonce } from "@pwn/nonce/PWNRevokedNonce.sol"; import { StateFingerprintComputerRegistry } from "@pwn/state-fingerprint/StateFingerprintComputerRegistry.sol"; diff --git a/src/PWNErrors.sol b/src/PWNErrors.sol index b941440..d63313d 100644 --- a/src/PWNErrors.sol +++ b/src/PWNErrors.sol @@ -4,13 +4,16 @@ pragma solidity 0.8.16; // Access control error CallerMissingHubTag(bytes32); +error AddressMissingHubTag(address addr, bytes32 tag); // Loan contract error LoanDefaulted(uint40); error InvalidLoanStatus(uint256); error NonExistingLoan(); error CallerNotLOANTokenHolder(); -error BorrowerMismatch(address currentBorrower, address newBorrower); +error RefinanceBorrowerMismatch(address currentBorrower, address newBorrower); +error RefinanceCreditMismatch(); +error RefinanceCollateralMismatch(); // Loan extension error InvalidExtensionDuration(uint256 duration, uint256 limit); @@ -18,9 +21,8 @@ error InvalidExtensionSigner(address allowed, address current); error InvalidExtensionCaller(); // Invalid asset -error InvalidLoanAsset(); -error InvalidCollateralAsset(); -error InvalidCollateralStateFingerprint(bytes32 offered, bytes32 current); +error InvalidMultiTokenAsset(uint8 category, address addr, uint256 id, uint256 amount); +error InvalidCollateralStateFingerprint(bytes32 current, bytes32 proposed); // State fingerprint computer registry error MissingStateFingerprintComputer(); @@ -33,28 +35,23 @@ error UnsupportedTransferFunction(); error IncompleteTransfer(); // Nonce -error NonceNotUsable(); +error NonceNotUsable(address addr, uint256 nonceSpace, uint256 nonce); // Signature checks error InvalidSignatureLength(uint256); -error InvalidSignature(); +error InvalidSignature(address signer, bytes32 digest); // Offer -error CallerIsNotStatedBorrower(address); -error OfferExpired(); -error CollateralIdIsNotWhitelisted(); +error CollateralIdNotWhitelisted(uint256 id); -// Request -error CallerIsNotStatedLender(address); -error RequestExpired(); - -// Request & Offer -error InvalidDuration(); -error InvalidCreateTerms(); -error InvalidRefinanceTerms(); +// Proposal +error CallerIsNotStatedProposer(address); +error InvalidDuration(uint256 current, uint256 limit); error InvalidRefinancingLoanId(uint256 refinancingLoanId); -error AccruingInterestAPROutOfBounds(uint40 providedAPR, uint40 maxAPR); -error AvailableCreditLimitExceeded(uint256 usedCredit, uint256 availableCreditLimit); +error AccruingInterestAPROutOfBounds(uint256 current, uint256 limit); +error AvailableCreditLimitExceeded(uint256 used, uint256 limit); +error Expired(uint256 current, uint256 expiration); +error CallerNotAllowedAcceptor(address current, address allowed); // Input data error InvalidInputData(); diff --git a/src/hub/PWNHubTags.sol b/src/hub/PWNHubTags.sol index be22bca..44dd32b 100644 --- a/src/hub/PWNHubTags.sol +++ b/src/hub/PWNHubTags.sol @@ -7,10 +7,8 @@ library PWNHubTags { /// @dev Address can mint LOAN tokens and create LOANs via loan factory contracts. bytes32 internal constant ACTIVE_LOAN = keccak256("PWN_ACTIVE_LOAN"); - - /// @dev Address can be used as a loan terms factory for creating simple loans. - bytes32 internal constant SIMPLE_LOAN_TERMS_FACTORY = keccak256("PWN_SIMPLE_LOAN_TERMS_FACTORY"); - + /// @dev Address can call loan contracts to create and/or refinance a loan. + bytes32 internal constant LOAN_PROPOSAL = keccak256("PWN_LOAN_PROPOSAL"); /// @dev Address can revoke nonces on other addresses behalf. bytes32 internal constant NONCE_MANAGER = keccak256("PWN_NONCE_MANAGER"); diff --git a/src/loan/terms/PWNLOANTerms.sol b/src/loan/terms/PWNLOANTerms.sol deleted file mode 100644 index 85a28cb..0000000 --- a/src/loan/terms/PWNLOANTerms.sol +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity 0.8.16; - -import { MultiToken } from "MultiToken/MultiToken.sol"; - - -library PWNLOANTerms { - - /** - * @notice Struct defining a simple loan terms. - * @dev This struct is created by loan factories and never stored. - * @param lender Address of a lender. - * @param borrower Address of a borrower. - * @param defaultTimestamp Unix timestamp (in seconds) setting up a default date. - * @param collateral Asset used as a loan collateral. For a definition see { MultiToken dependency lib }. - * @param asset Asset used as a loan credit. For a definition see { MultiToken dependency lib }. - * @param fixedInterestAmount Fixed interest amount in loan asset tokens. It is the minimum amount of interest which has to be paid by a borrower. - * @param accruingInterestAPR Accruing interest APR. - * @param canCreate If true, the terms can be used to create a new loan. - * @param canRefinance If true, the terms can be used to refinance a running loan. - * @param refinancingLoanId Id of a loan which is refinanced by this terms. If the id is 0, any loan can be refinanced. - */ - struct Simple { - address lender; - address borrower; - uint40 defaultTimestamp; - MultiToken.Asset collateral; - MultiToken.Asset asset; - uint256 fixedInterestAmount; - uint40 accruingInterestAPR; - bool canCreate; - bool canRefinance; - uint256 refinancingLoanId; - } - -} diff --git a/src/loan/terms/simple/factory/PWNSimpleLoanTermsFactory.sol b/src/loan/terms/simple/factory/PWNSimpleLoanTermsFactory.sol deleted file mode 100644 index dc2adc8..0000000 --- a/src/loan/terms/simple/factory/PWNSimpleLoanTermsFactory.sol +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity 0.8.16; - -import "@pwn/loan/terms/PWNLOANTerms.sol"; - - -/** - * @title PWN Simple Loan Terms Factory Interface - * @notice Interface of a loan factory contract that builds a simple loan terms. - */ -abstract contract PWNSimpleLoanTermsFactory { - - uint32 public constant MIN_LOAN_DURATION = 600; // 10 min - uint40 public constant MAX_ACCRUING_INTEREST_APR = 1e11; // 1,000,000% APR - - /** - * @notice Build a simple loan terms from given data. - * @dev This function should be called only by contracts working with simple loan terms. - * @param caller Caller of a create loan function on a loan contract. - * @param factoryData Encoded data for a loan terms factory. - * @param signature Signed loan factory data. - * @return loanTerms Simple loan terms struct created from a loan factory data. - * @return factoryDataHash Hash of a loan offer / request that is signed by a lender / borrower. Used to uniquely identify a loan offer / request. - */ - function createLOANTerms( - address caller, - bytes calldata factoryData, - bytes calldata signature - ) external virtual returns (PWNLOANTerms.Simple memory loanTerms, bytes32 factoryDataHash); - -} diff --git a/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol b/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol deleted file mode 100644 index e9e480a..0000000 --- a/src/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol +++ /dev/null @@ -1,336 +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 { PWNHubAccessControl } from "@pwn/hub/PWNHubAccessControl.sol"; -import { PWNSignatureChecker } from "@pwn/loan/lib/PWNSignatureChecker.sol"; -import { PWNSimpleLoanTermsFactory } from "@pwn/loan/terms/simple/factory/PWNSimpleLoanTermsFactory.sol"; -import { PWNLOANTerms } from "@pwn/loan/terms/PWNLOANTerms.sol"; -import { PWNRevokedNonce } from "@pwn/nonce/PWNRevokedNonce.sol"; -import { StateFingerprintComputerRegistry, IERC5646 } from "@pwn/state-fingerprint/StateFingerprintComputerRegistry.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 PWNSimpleLoanTermsFactory, PWNHubAccessControl { - - string public constant VERSION = "1.2"; - - /*----------------------------------------------------------*| - |* # VARIABLES & CONSTANTS DEFINITIONS *| - |*----------------------------------------------------------*/ - - /** - * @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 loanAssetAddress,uint256 loanAmount,uint256 availableCreditLimit,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedBorrower,address lender,uint256 nonceSpace,uint256 nonce)" - ); - - bytes32 public immutable DOMAIN_SEPARATOR; - - PWNRevokedNonce public immutable revokedOfferNonce; - StateFingerprintComputerRegistry public immutable stateFingerprintComputerRegistry; - - /** - * @dev Mapping of offers made via on-chain transactions. - * Could be used by contract wallets instead of EIP-1271. - * (offer hash => is made) - */ - mapping (bytes32 => bool) public offersMade; - - /** - * @dev Mapping of credit used by an offer. - * (offer hash => credit used) - */ - mapping (bytes32 => uint256) private _creditUsed; - - /** - * @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 loanAssetAddress Address of an asset which is lender to a borrower. - * @param loanAmount 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 loan asset 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 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. - */ - struct Offer { - MultiToken.Category collateralCategory; - address collateralAddress; - bytes32 collateralIdsWhitelistMerkleRoot; - uint256 collateralAmount; - bool checkCollateralStateFingerprint; - bytes32 collateralStateFingerprint; - address loanAssetAddress; - uint256 loanAmount; - uint256 availableCreditLimit; - uint256 fixedInterestAmount; - uint40 accruingInterestAPR; - uint32 duration; - uint40 expiration; - address allowedBorrower; - address lender; - uint256 nonceSpace; - uint256 nonce; - } - - /** - * 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; - } - - - /*----------------------------------------------------------*| - |* # EVENTS DEFINITIONS *| - |*----------------------------------------------------------*/ - - /** - * @dev Emitted when an offer is made via an on-chain transaction. - */ - event OfferMade(bytes32 indexed offerHash, address indexed lender, Offer offer); - - - /*----------------------------------------------------------*| - |* # CONSTRUCTOR *| - |*----------------------------------------------------------*/ - - constructor( - address hub, - address _revokedOfferNonce, - address _stateFingerprintComputerRegistry - ) PWNHubAccessControl(hub) { - DOMAIN_SEPARATOR = keccak256(abi.encode( - keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), - keccak256("PWNSimpleLoanListOffer"), - keccak256(abi.encodePacked(VERSION)), - block.chainid, - address(this) - )); - - revokedOfferNonce = PWNRevokedNonce(_revokedOfferNonce); - stateFingerprintComputerRegistry = StateFingerprintComputerRegistry(_stateFingerprintComputerRegistry); - } - - - /*----------------------------------------------------------*| - |* # OFFER MANAGEMENT *| - |*----------------------------------------------------------*/ - - /** - * @notice Make an on-chain offer. - * @dev Function will mark an offer hash as proposed. Offer will become acceptable by a borrower without an offer signature. - * @param offer Offer struct containing all needed offer data. - */ - function makeOffer(Offer calldata offer) external { - // Check that caller is a lender - if (msg.sender != offer.lender) - revert CallerIsNotStatedLender(offer.lender); - - bytes32 offerHash = getOfferHash(offer); - emit OfferMade(offerHash, offer.lender, offer); - - // Mark offer as made - offersMade[offerHash] = true; - } - - /** - * @notice Get available credit for an offer. - * @param offer Offer struct containing all needed offer data. - * @return Available credit for an offer. - */ - function availableCredit(Offer calldata offer) external view returns (uint256) { - return offer.availableCreditLimit - _creditUsed[getOfferHash(offer)]; - } - - /** - * @notice Get credit used for an offer. - * @param offer Offer struct containing all needed offer data. - * @return Credit used for an offer. - */ - function creditUsed(Offer calldata offer) external view returns (uint256) { - return _creditUsed[getOfferHash(offer)]; - } - - /** - * @notice Helper function for revoking an offer nonce on behalf of a caller. - * @param offerNonceSpace Nonce space of an offer nonce to be revoked. - * @param offerNonce Offer nonce to be revoked. - */ - function revokeOfferNonce(uint256 offerNonceSpace, uint256 offerNonce) external { - revokedOfferNonce.revokeNonce(msg.sender, offerNonceSpace, offerNonce); - } - - - /*----------------------------------------------------------*| - |* # PWNSimpleLoanTermsFactory *| - |*----------------------------------------------------------*/ - - /** - * @inheritdoc PWNSimpleLoanTermsFactory - */ - function createLOANTerms( - address caller, - bytes calldata factoryData, - bytes calldata signature - ) external override onlyActiveLoan returns (PWNLOANTerms.Simple memory loanTerms, bytes32 offerHash) { - - (Offer memory offer, OfferValues memory offerValues) = abi.decode(factoryData, (Offer, OfferValues)); - offerHash = getOfferHash(offer); - - address lender = offer.lender; - address borrower = caller; - - // Check that offer has been made via on-chain tx, EIP-1271 or signed off-chain - if (!offersMade[offerHash]) - if (!PWNSignatureChecker.isValidSignatureNow(lender, offerHash, signature)) - revert InvalidSignature(); - - // Check valid offer - if (block.timestamp >= offer.expiration) - revert OfferExpired(); - - if (!revokedOfferNonce.isNonceUsable(lender, offer.nonceSpace, offer.nonce)) - revert NonceNotUsable(); - - if (offer.allowedBorrower != address(0)) - if (borrower != offer.allowedBorrower) - revert CallerIsNotStatedBorrower(offer.allowedBorrower); - - if (offer.duration < MIN_LOAN_DURATION) - revert InvalidDuration(); - - // Check APR - if (offer.accruingInterestAPR > MAX_ACCRUING_INTEREST_APR) - revert AccruingInterestAPROutOfBounds({ - providedAPR: offer.accruingInterestAPR, - maxAPR: MAX_ACCRUING_INTEREST_APR - }); - - // Collateral id list - if (offer.collateralIdsWhitelistMerkleRoot != bytes32(0)) { - // Verify whitelisted collateral id - bool isVerifiedId = MerkleProof.verify( - offerValues.merkleInclusionProof, - offer.collateralIdsWhitelistMerkleRoot, - keccak256(abi.encodePacked(offerValues.collateralId)) - ); - if (isVerifiedId == false) - revert CollateralIdIsNotWhitelisted(); - } // else: Any collateral id - collection offer - - // Check that the collateral state fingerprint matches the current state - if (offer.checkCollateralStateFingerprint) { - IERC5646 computer = stateFingerprintComputerRegistry.getStateFingerprintComputer(offer.collateralAddress); - if (address(computer) == address(0)) { - // Asset is not implementing ERC5646 and no computer is registered - revert MissingStateFingerprintComputer(); - } - - bytes32 currentFingerprint = computer.getStateFingerprint(offerValues.collateralId); - if (offer.collateralStateFingerprint != currentFingerprint) { - // Fingerprint mismatch - revert InvalidCollateralStateFingerprint({ - offered: offer.collateralStateFingerprint, - current: currentFingerprint - }); - } - } - - // Check that the available credit limit is not exceeded - if (offer.availableCreditLimit == 0) { - revokedOfferNonce.revokeNonce(lender, offer.nonceSpace, offer.nonce); - } else if (_creditUsed[offerHash] + offer.loanAmount <= offer.availableCreditLimit) { - _creditUsed[offerHash] += offer.loanAmount; - } else { - revert AvailableCreditLimitExceeded({ - usedCredit: _creditUsed[offerHash] + offer.loanAmount, - availableCreditLimit: offer.availableCreditLimit - }); - } - - // Create loan terms object - loanTerms = PWNLOANTerms.Simple({ - lender: lender, - borrower: borrower, - defaultTimestamp: uint40(block.timestamp) + offer.duration, - collateral: MultiToken.Asset({ - category: offer.collateralCategory, - assetAddress: offer.collateralAddress, - id: offerValues.collateralId, - amount: offer.collateralAmount - }), - asset: MultiToken.ERC20({ - assetAddress: offer.loanAssetAddress, - amount: offer.loanAmount - }), - fixedInterestAmount: offer.fixedInterestAmount, - accruingInterestAPR: offer.accruingInterestAPR, - canCreate: true, - canRefinance: true, - refinancingLoanId: 0 - }); - } - - - /*----------------------------------------------------------*| - |* # GET OFFER HASH *| - |*----------------------------------------------------------*/ - - /** - * @notice Get an offer hash according to EIP-712 - * @param offer Offer struct to be hashed. - * @return Offer struct hash. - */ - function getOfferHash(Offer memory offer) public view returns (bytes32) { - return keccak256(abi.encodePacked( - hex"1901", - DOMAIN_SEPARATOR, - keccak256(abi.encodePacked( - OFFER_TYPEHASH, - abi.encode(offer) - )) - )); - } - - - /*----------------------------------------------------------*| - |* # LOAN TERMS FACTORY DATA ENCODING *| - |*----------------------------------------------------------*/ - - /** - * @notice Return encoded input data for this loan terms factory. - * @param offer Simple loan list offer struct to encode. - * @param offerValues Simple loan list offer concrete values from borrower. - * @return Encoded loan terms factory data that can be used as an input of `createLOANTerms` function with this factory. - */ - function encodeLoanTermsFactoryData(Offer memory offer, OfferValues memory offerValues) external pure returns (bytes memory) { - return abi.encode(offer, offerValues); - } - -} diff --git a/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol b/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol deleted file mode 100644 index 456d542..0000000 --- a/src/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol +++ /dev/null @@ -1,308 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity 0.8.16; - -import { MultiToken } from "MultiToken/MultiToken.sol"; - -import { PWNHubAccessControl } from "@pwn/hub/PWNHubAccessControl.sol"; -import { PWNSignatureChecker } from "@pwn/loan/lib/PWNSignatureChecker.sol"; -import { PWNSimpleLoanTermsFactory } from "@pwn/loan/terms/simple/factory/PWNSimpleLoanTermsFactory.sol"; -import { PWNLOANTerms } from "@pwn/loan/terms/PWNLOANTerms.sol"; -import { PWNRevokedNonce } from "@pwn/nonce/PWNRevokedNonce.sol"; -import { StateFingerprintComputerRegistry, IERC5646 } from "@pwn/state-fingerprint/StateFingerprintComputerRegistry.sol"; -import "@pwn/PWNErrors.sol"; - - -/** - * @title PWN Simple Loan Simple Offer - * @notice Loan terms factory contract creating a simple loan terms from a simple offer. - */ -contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanTermsFactory, PWNHubAccessControl { - - string public constant VERSION = "1.2"; - - /*----------------------------------------------------------*| - |* # VARIABLES & CONSTANTS DEFINITIONS *| - |*----------------------------------------------------------*/ - - /** - * @dev EIP-712 simple offer struct type hash. - */ - bytes32 public constant OFFER_TYPEHASH = keccak256( - "Offer(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address loanAssetAddress,uint256 loanAmount,uint256 availableCreditLimit,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedBorrower,address lender,uint256 nonceSpace,uint256 nonce)" - ); - - bytes32 public immutable DOMAIN_SEPARATOR; - - PWNRevokedNonce public immutable revokedOfferNonce; - StateFingerprintComputerRegistry public immutable stateFingerprintComputerRegistry; - - /** - * @dev Mapping of offers made via on-chain transactions. - * Could be used by contract wallets instead of EIP-1271. - * (offer hash => is made) - */ - mapping (bytes32 => bool) public offersMade; - - /** - * @dev Mapping of credit used by an offer. - * (offer hash => credit used) - */ - mapping (bytes32 => uint256) private _creditUsed; - - /** - * @notice Construct defining a simple 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 collateralId Token id of an asset used as a collateral, in case of ERC20 should be 0. - * @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 loanAssetAddress Address of an asset which is lender to a borrower. - * @param loanAmount 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 loan asset 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 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. - */ - struct Offer { - MultiToken.Category collateralCategory; - address collateralAddress; - uint256 collateralId; - uint256 collateralAmount; - bool checkCollateralStateFingerprint; - bytes32 collateralStateFingerprint; - address loanAssetAddress; - uint256 loanAmount; - uint256 availableCreditLimit; - uint256 fixedInterestAmount; - uint40 accruingInterestAPR; - uint32 duration; - uint40 expiration; - address allowedBorrower; - address lender; - uint256 nonceSpace; - uint256 nonce; - } - - - /*----------------------------------------------------------*| - |* # EVENTS DEFINITIONS *| - |*----------------------------------------------------------*/ - - /** - * @dev Emitted when an offer is made via an on-chain transaction. - */ - event OfferMade(bytes32 indexed offerHash, address indexed lender, Offer offer); - - - /*----------------------------------------------------------*| - |* # CONSTRUCTOR *| - |*----------------------------------------------------------*/ - - constructor( - address hub, - address _revokedOfferNonce, - address _stateFingerprintComputerRegistry - ) PWNHubAccessControl(hub) { - DOMAIN_SEPARATOR = keccak256(abi.encode( - keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), - keccak256("PWNSimpleLoanSimpleOffer"), - keccak256(abi.encodePacked(VERSION)), - block.chainid, - address(this) - )); - - revokedOfferNonce = PWNRevokedNonce(_revokedOfferNonce); - stateFingerprintComputerRegistry = StateFingerprintComputerRegistry(_stateFingerprintComputerRegistry); - } - - - /*----------------------------------------------------------*| - |* # OFFER MANAGEMENT *| - |*----------------------------------------------------------*/ - - /** - * @notice Make an on-chain offer. - * @dev Function will mark an offer hash as proposed. Offer will become acceptable by a borrower without an offer signature. - * @param offer Offer struct containing all needed offer data. - */ - function makeOffer(Offer calldata offer) external { - // Check that caller is a lender - if (msg.sender != offer.lender) - revert CallerIsNotStatedLender(offer.lender); - - bytes32 offerHash = getOfferHash(offer); - emit OfferMade(offerHash, offer.lender, offer); - - // Mark offer as made - offersMade[offerHash] = true; - } - - /** - * @notice Get available credit for an offer. - * @param offer Offer struct containing all needed offer data. - * @return Available credit for an offer. - */ - function availableCredit(Offer calldata offer) external view returns (uint256) { - return offer.availableCreditLimit - _creditUsed[getOfferHash(offer)]; - } - - /** - * @notice Get credit used for an offer. - * @param offer Offer struct containing all needed offer data. - * @return Credit used for an offer. - */ - function creditUsed(Offer calldata offer) external view returns (uint256) { - return _creditUsed[getOfferHash(offer)]; - } - - /** - * @notice Helper function for revoking an offer nonce on behalf of a caller. - * @param offerNonceSpace Nonce space of an offer nonce to be revoked. - * @param offerNonce Offer nonce to be revoked. - */ - function revokeOfferNonce(uint256 offerNonceSpace, uint256 offerNonce) external { - revokedOfferNonce.revokeNonce(msg.sender, offerNonceSpace, offerNonce); - } - - - /*----------------------------------------------------------*| - |* # PWNSimpleLoanTermsFactory *| - |*----------------------------------------------------------*/ - - /** - * @inheritdoc PWNSimpleLoanTermsFactory - */ - function createLOANTerms( - address caller, - bytes calldata factoryData, - bytes calldata signature - ) external override onlyActiveLoan returns (PWNLOANTerms.Simple memory loanTerms, bytes32 offerHash) { - - Offer memory offer = abi.decode(factoryData, (Offer)); - offerHash = getOfferHash(offer); - - address lender = offer.lender; - address borrower = caller; - - // Check that offer has been made via on-chain tx, EIP-1271 or signed off-chain - if (!offersMade[offerHash]) - if (!PWNSignatureChecker.isValidSignatureNow(lender, offerHash, signature)) - revert InvalidSignature(); - - // Check valid offer - if (block.timestamp >= offer.expiration) - revert OfferExpired(); - - if (!revokedOfferNonce.isNonceUsable(lender, offer.nonceSpace, offer.nonce)) - revert NonceNotUsable(); - - if (offer.allowedBorrower != address(0)) - if (borrower != offer.allowedBorrower) - revert CallerIsNotStatedBorrower(offer.allowedBorrower); - - if (offer.duration < MIN_LOAN_DURATION) - revert InvalidDuration(); - - // Check APR - if (offer.accruingInterestAPR > MAX_ACCRUING_INTEREST_APR) - revert AccruingInterestAPROutOfBounds({ - providedAPR: offer.accruingInterestAPR, - maxAPR: MAX_ACCRUING_INTEREST_APR - }); - - // Check that the collateral state fingerprint matches the current state - if (offer.checkCollateralStateFingerprint) { - IERC5646 computer = stateFingerprintComputerRegistry.getStateFingerprintComputer(offer.collateralAddress); - if (address(computer) == address(0)) { - // Asset is not implementing ERC5646 and no computer is registered - revert MissingStateFingerprintComputer(); - } - - bytes32 currentFingerprint = computer.getStateFingerprint(offer.collateralId); - if (offer.collateralStateFingerprint != currentFingerprint) { - // Fingerprint mismatch - revert InvalidCollateralStateFingerprint({ - offered: offer.collateralStateFingerprint, - current: currentFingerprint - }); - } - } - - // Check that the available credit limit is not exceeded - if (offer.availableCreditLimit == 0) { - revokedOfferNonce.revokeNonce(lender, offer.nonceSpace, offer.nonce); - } else if (_creditUsed[offerHash] + offer.loanAmount <= offer.availableCreditLimit) { - _creditUsed[offerHash] += offer.loanAmount; - } else { - revert AvailableCreditLimitExceeded({ - usedCredit: _creditUsed[offerHash] + offer.loanAmount, - availableCreditLimit: offer.availableCreditLimit - }); - } - - // Create loan terms object - loanTerms = PWNLOANTerms.Simple({ - lender: lender, - borrower: borrower, - defaultTimestamp: uint40(block.timestamp) + offer.duration, - collateral: MultiToken.Asset({ - category: offer.collateralCategory, - assetAddress: offer.collateralAddress, - id: offer.collateralId, - amount: offer.collateralAmount - }), - asset: MultiToken.ERC20({ - assetAddress: offer.loanAssetAddress, - amount: offer.loanAmount - }), - fixedInterestAmount: offer.fixedInterestAmount, - accruingInterestAPR: offer.accruingInterestAPR, - canCreate: true, - canRefinance: true, - refinancingLoanId: 0 - }); - } - - - /*----------------------------------------------------------*| - |* # GET OFFER HASH *| - |*----------------------------------------------------------*/ - - /** - * @notice Get an offer hash according to EIP-712. - * @param offer Offer struct to be hashed. - * @return Offer struct hash. - */ - function getOfferHash(Offer memory offer) public view returns (bytes32) { - return keccak256(abi.encodePacked( - hex"1901", - DOMAIN_SEPARATOR, - keccak256(abi.encodePacked( - OFFER_TYPEHASH, - abi.encode(offer) - )) - )); - } - - - /*----------------------------------------------------------*| - |* # LOAN TERMS FACTORY DATA ENCODING *| - |*----------------------------------------------------------*/ - - /** - * @notice Return encoded input data for this loan terms factory. - * @param offer Simple loan simple offer struct to encode. - * @return Encoded loan terms factory data that can be used as an input of `createLOANTerms` function with this factory. - */ - function encodeLoanTermsFactoryData(Offer memory offer) external pure returns (bytes memory) { - return abi.encode(offer); - } - -} diff --git a/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol b/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol deleted file mode 100644 index 598a6df..0000000 --- a/src/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol +++ /dev/null @@ -1,310 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity 0.8.16; - -import { MultiToken } from "MultiToken/MultiToken.sol"; - -import { PWNHubAccessControl } from "@pwn/hub/PWNHubAccessControl.sol"; -import { PWNSignatureChecker } from "@pwn/loan/lib/PWNSignatureChecker.sol"; -import { PWNSimpleLoanTermsFactory } from "@pwn/loan/terms/simple/factory/PWNSimpleLoanTermsFactory.sol"; -import { PWNLOANTerms } from "@pwn/loan/terms/PWNLOANTerms.sol"; -import { PWNRevokedNonce } from "@pwn/nonce/PWNRevokedNonce.sol"; -import { StateFingerprintComputerRegistry, IERC5646 } from "@pwn/state-fingerprint/StateFingerprintComputerRegistry.sol"; -import "@pwn/PWNErrors.sol"; - - -/** - * @title PWN Simple Loan Simple Request - * @notice Loan terms factory contract creating a simple loan terms from a simple request. - */ -contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanTermsFactory, PWNHubAccessControl { - - string public constant VERSION = "1.2"; - - /*----------------------------------------------------------*| - |* # VARIABLES & CONSTANTS DEFINITIONS *| - |*----------------------------------------------------------*/ - - /** - * @dev EIP-712 simple request struct type hash. - */ - bytes32 public constant REQUEST_TYPEHASH = keccak256( - "Request(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address loanAssetAddress,uint256 loanAmount,uint256 availableCreditLimit,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedLender,address borrower,uint256 refinancingLoanId,uint256 nonceSpace,uint256 nonce)" - ); - - bytes32 public immutable DOMAIN_SEPARATOR; - - PWNRevokedNonce public immutable revokedRequestNonce; - StateFingerprintComputerRegistry public immutable stateFingerprintComputerRegistry; - - /** - * @dev Mapping of requests made via on-chain transactions. - * Could be used by contract wallets instead of EIP-1271. - * (request hash => is made) - */ - mapping (bytes32 => bool) public requestsMade; - - /** - * @dev Mapping of credit used by a request. - * (request hash => credit used) - */ - mapping (bytes32 => uint256) private _creditUsed; - - /** - * @notice Construct defining a simple request. - * @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 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 loanAssetAddress Address of an asset which is lender to a borrower. - * @param loanAmount Amount of tokens which is requested as a loan to a borrower. - * @param availableCreditLimit Available credit limit for the request. It is the maximum amount of tokens which can be borrowed using the request. - * @param fixedInterestAmount Fixed interest amount in loan asset 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 Request expiration timestamp in seconds. - * @param allowedLender Address of an allowed lender. Only this address can accept a request. If the address is zero address, anybody with a loan asset can accept the request. - * @param borrower Address of a borrower. This address has to sign a request to be valid. - * @param refinancingLoanId Id of a loan which is refinanced by this request. If the id is 0, the request is not a refinancing request. - * @param nonceSpace Nonce space of a request nonce. All nonces in the same space can be revoked at once. - * @param nonce Additional value to enable identical requests in time. Without it, it would be impossible to make again request, which was once revoked. - * Can be used to create a group of requests, where accepting one request will make other requests in the group revoked. - */ - struct Request { - MultiToken.Category collateralCategory; - address collateralAddress; - uint256 collateralId; - uint256 collateralAmount; - bool checkCollateralStateFingerprint; - bytes32 collateralStateFingerprint; - address loanAssetAddress; - uint256 loanAmount; - uint256 availableCreditLimit; - uint256 fixedInterestAmount; - uint40 accruingInterestAPR; - uint32 duration; - uint40 expiration; - address allowedLender; - address borrower; - uint256 refinancingLoanId; - uint256 nonceSpace; - uint256 nonce; - } - - - /*----------------------------------------------------------*| - |* # EVENTS DEFINITIONS *| - |*----------------------------------------------------------*/ - - /** - * @dev Emitted when a request is made via an on-chain transaction. - */ - event RequestMade(bytes32 indexed requestHash, address indexed borrower, Request request); - - - /*----------------------------------------------------------*| - |* # CONSTRUCTOR *| - |*----------------------------------------------------------*/ - - constructor( - address hub, - address _revokedRequestNonce, - address _stateFingerprintComputerRegistry - ) PWNHubAccessControl(hub) { - DOMAIN_SEPARATOR = keccak256(abi.encode( - keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), - keccak256("PWNSimpleLoanSimpleRequest"), - keccak256(abi.encodePacked(VERSION)), - block.chainid, - address(this) - )); - - revokedRequestNonce = PWNRevokedNonce(_revokedRequestNonce); - stateFingerprintComputerRegistry = StateFingerprintComputerRegistry(_stateFingerprintComputerRegistry); - } - - - /*----------------------------------------------------------*| - |* # REQUEST MANAGEMENT *| - |*----------------------------------------------------------*/ - - /** - * @notice Make an on-chain request. - * @dev Function will mark a request hash as proposed. Request will become acceptable by a lender without a request signature. - * @param request Request struct containing all needed request data. - */ - function makeRequest(Request calldata request) external { - // Check that caller is a borrower - if (msg.sender != request.borrower) - revert CallerIsNotStatedBorrower(request.borrower); - - bytes32 requestHash = getRequestHash(request); - emit RequestMade(requestHash, request.borrower, request); - - // Mark request as made - requestsMade[requestHash] = true; - } - - /** - * @notice Get available credit for a request. - * @param request Request struct containing all needed request data. - * @return Available credit for a request. - */ - function availableCredit(Request calldata request) external view returns (uint256) { - return request.availableCreditLimit - _creditUsed[getRequestHash(request)]; - } - - /** - * @notice Get credit used for a request. - * @param request Request struct containing all needed request data. - * @return Credit used for a request. - */ - function creditUsed(Request calldata request) external view returns (uint256) { - return _creditUsed[getRequestHash(request)]; - } - - /** - * @notice Helper function for revoking a request nonce on behalf of a caller. - * @param requestNonceSpace Nonce space of a request nonce to be revoked. - * @param requestNonce Request nonce to be revoked. - */ - function revokeRequestNonce(uint256 requestNonceSpace, uint256 requestNonce) external { - revokedRequestNonce.revokeNonce(msg.sender, requestNonceSpace, requestNonce); - } - - - /*----------------------------------------------------------*| - |* # PWNSimpleLoanTermsFactory *| - |*----------------------------------------------------------*/ - - /** - * @inheritdoc PWNSimpleLoanTermsFactory - */ - function createLOANTerms( - address caller, - bytes calldata factoryData, - bytes calldata signature - ) external override onlyActiveLoan returns (PWNLOANTerms.Simple memory loanTerms, bytes32 requestHash) { - - Request memory request = abi.decode(factoryData, (Request)); - requestHash = getRequestHash(request); - - address lender = caller; - address borrower = request.borrower; - - // Check that request has been made via on-chain tx, EIP-1271 or signed off-chain - if (!requestsMade[requestHash]) - if (!PWNSignatureChecker.isValidSignatureNow(borrower, requestHash, signature)) - revert InvalidSignature(); - - // Check valid request - if (block.timestamp >= request.expiration) - revert RequestExpired(); - - if (!revokedRequestNonce.isNonceUsable(borrower, request.nonceSpace, request.nonce)) - revert NonceNotUsable(); - - if (request.allowedLender != address(0)) - if (lender != request.allowedLender) - revert CallerIsNotStatedLender(request.allowedLender); - - if (request.duration < MIN_LOAN_DURATION) - revert InvalidDuration(); - - // Check APR - if (request.accruingInterestAPR > MAX_ACCRUING_INTEREST_APR) - revert AccruingInterestAPROutOfBounds({ - providedAPR: request.accruingInterestAPR, - maxAPR: MAX_ACCRUING_INTEREST_APR - }); - - // Check that the collateral state fingerprint matches the current state - if (request.checkCollateralStateFingerprint) { - IERC5646 computer = stateFingerprintComputerRegistry.getStateFingerprintComputer(request.collateralAddress); - if (address(computer) == address(0)) { - // Asset is not implementing ERC5646 and no computer is registered - revert MissingStateFingerprintComputer(); - } - - bytes32 currentFingerprint = computer.getStateFingerprint(request.collateralId); - if (request.collateralStateFingerprint != currentFingerprint) { - // Fingerprint mismatch - revert InvalidCollateralStateFingerprint({ - offered: request.collateralStateFingerprint, - current: currentFingerprint - }); - } - } - - // Check that the available credit limit is not exceeded - if (request.availableCreditLimit == 0) { - revokedRequestNonce.revokeNonce(borrower, request.nonceSpace, request.nonce); - } else if (_creditUsed[requestHash] + request.loanAmount <= request.availableCreditLimit) { - _creditUsed[requestHash] += request.loanAmount; - } else { - revert AvailableCreditLimitExceeded({ - usedCredit: _creditUsed[requestHash] + request.loanAmount, - availableCreditLimit: request.availableCreditLimit - }); - } - - // Create loan terms object - loanTerms = PWNLOANTerms.Simple({ - lender: lender, - borrower: borrower, - defaultTimestamp: uint40(block.timestamp) + request.duration, - collateral: MultiToken.Asset({ - category: request.collateralCategory, - assetAddress: request.collateralAddress, - id: request.collateralId, - amount: request.collateralAmount - }), - asset: MultiToken.ERC20({ - assetAddress: request.loanAssetAddress, - amount: request.loanAmount - }), - fixedInterestAmount: request.fixedInterestAmount, - accruingInterestAPR: request.accruingInterestAPR, - canCreate: request.refinancingLoanId == 0, - canRefinance: request.refinancingLoanId != 0, - refinancingLoanId: request.refinancingLoanId - }); - } - - - /*----------------------------------------------------------*| - |* # GET REQUEST HASH *| - |*----------------------------------------------------------*/ - - /** - * @notice Get a request hash according to EIP-712. - * @param request Request struct to be hashed. - * @return Request struct hash. - */ - function getRequestHash(Request memory request) public view returns (bytes32) { - return keccak256(abi.encodePacked( - hex"1901", - DOMAIN_SEPARATOR, - keccak256(abi.encodePacked( - REQUEST_TYPEHASH, - abi.encode(request) - )) - )); - } - - - /*----------------------------------------------------------*| - |* # LOAN TERMS FACTORY DATA ENCODING *| - |*----------------------------------------------------------*/ - - /** - * @notice Return encoded input data for this loan terms factory. - * @param request Simple loan simple request struct to encode. - * @return Encoded loan terms factory data that can be used as an input of `createLOANTerms` function with this factory. - */ - function encodeLoanTermsFactoryData(Request memory request) external pure returns (bytes memory) { - return abi.encode(request); - } - -} diff --git a/src/loan/terms/simple/loan/PWNSimpleLoan.sol b/src/loan/terms/simple/loan/PWNSimpleLoan.sol index 37a71bc..624f414 100644 --- a/src/loan/terms/simple/loan/PWNSimpleLoan.sol +++ b/src/loan/terms/simple/loan/PWNSimpleLoan.sol @@ -11,8 +11,7 @@ import { PWNHub } from "@pwn/hub/PWNHub.sol"; import { PWNHubTags } from "@pwn/hub/PWNHubTags.sol"; import { PWNFeeCalculator } from "@pwn/loan/lib/PWNFeeCalculator.sol"; import { PWNSignatureChecker } from "@pwn/loan/lib/PWNSignatureChecker.sol"; -import { PWNLOANTerms } from "@pwn/loan/terms/PWNLOANTerms.sol"; -import { PWNSimpleLoanTermsFactory } from "@pwn/loan/terms/simple/factory/PWNSimpleLoanTermsFactory.sol"; +import { PWNSimpleLoanProposal } from "@pwn/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol"; import { IERC5646 } from "@pwn/loan/token/IERC5646.sol"; import { IPWNLoanMetadataProvider } from "@pwn/loan/token/IPWNLoanMetadataProvider.sol"; import { PWNLOAN } from "@pwn/loan/token/PWNLOAN.sol"; @@ -61,6 +60,27 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { PWNRevokedNonce public immutable revokedNonce; IMultiTokenCategoryRegistry public immutable categoryRegistry; + /** + * @notice Struct defining a simple loan terms. + * @dev This struct is created by proposal contracts and never stored. + * @param lender Address of a lender. + * @param borrower Address of a borrower. + * @param duration Loan duration in seconds. + * @param collateral Asset used as a loan collateral. For a definition see { MultiToken dependency lib }. + * @param asset Asset used as a loan credit. For a definition see { MultiToken dependency lib }. + * @param fixedInterestAmount Fixed interest amount in loan asset tokens. It is the minimum amount of interest which has to be paid by a borrower. + * @param accruingInterestAPR Accruing interest APR. + */ + struct Terms { + address lender; + address borrower; + uint32 duration; + MultiToken.Asset collateral; + MultiToken.Asset asset; + uint256 fixedInterestAmount; + uint40 accruingInterestAPR; + } + /** * @notice Struct defining a simple loan. * @param status 0 == none/dead || 2 == running/accepted offer/accepted request || 3 == paid back || 4 == expired. @@ -126,7 +146,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { /** * @dev Emitted when a new loan in created. */ - event LOANCreated(uint256 indexed loanId, PWNLOANTerms.Simple terms, bytes32 indexed factoryDataHash, address indexed factoryAddress); + event LOANCreated(uint256 indexed loanId, Terms terms, bytes32 indexed proposalHash, address indexed proposalContract); /** * @dev Emitted when a loan is paid back. @@ -178,126 +198,60 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { |*----------------------------------------------------------*/ /** - * @notice Create a new loan by minting LOAN token for lender, transferring loan asset to a borrower and a collateral to a vault. + * @notice Create a new loan. * @dev The function assumes a prior token approval to a contract address or signed permits. - * @param loanTermsFactoryContract Address of a loan terms factory contract. Need to have `SIMPLE_LOAN_TERMS_FACTORY` tag in PWN Hub. - * @param loanTermsFactoryData Encoded data for a loan terms factory. - * @param signature Signed loan factory data. Could be empty if an offer / request has been made via on-chain transaction. + * @param proposalHash Hash of a loan offer / request that is signed by a lender / borrower. + * @param loanTerms Loan terms struct. * @param loanAssetPermit Permit data for a loan asset signed by a lender. * @param collateralPermit Permit data for a collateral signed by a borrower. - * @return loanId Id of a newly minted LOAN token. + * @return loanId Id of the created LOAN token. */ function createLOAN( - address loanTermsFactoryContract, - bytes calldata loanTermsFactoryData, - bytes calldata signature, + bytes32 proposalHash, + Terms calldata loanTerms, bytes calldata loanAssetPermit, bytes calldata collateralPermit - ) public returns (uint256 loanId) { - // Create loan terms or revert if factory contract is not tagged in PWN Hub - (PWNLOANTerms.Simple memory loanTerms, bytes32 factoryDataHash) - = _createLoanTerms(loanTermsFactoryContract, loanTermsFactoryData, signature); + ) 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 loan terms validity, revert if not - _checkNewLoanTerms(loanTerms); + // Check loan terms + _checkLoanTerms(loanTerms); // Create a new loan - loanId = _createLoan(loanTerms, factoryDataHash, loanTermsFactoryContract); + loanId = _createLoan({ + proposalHash: proposalHash, + proposalContract: msg.sender, + loanTerms: loanTerms + }); // Transfer collateral to Vault and loan asset to borrower _settleNewLoan(loanTerms, loanAssetPermit, collateralPermit); } /** - * @notice Create a new loan by minting LOAN token for lender, transferring loan asset to a borrower and a collateral to a vault. - Revoke a nonce on behalf of the caller. - * @dev The function assumes a prior token approval to a contract address or signed permits. - * @param loanTermsFactoryContract Address of a loan terms factory contract. Need to have `SIMPLE_LOAN_TERMS_FACTORY` tag in PWN Hub. - * @param loanTermsFactoryData Encoded data for a loan terms factory. - * @param signature Signed loan factory data. Could be empty if an offer / request has been made via on-chain transaction. - * @param loanAssetPermit Permit data for a loan asset signed by a lender. - * @param collateralPermit Permit data for a collateral signed by a borrower. - * @param callersNonceToRevoke Nonce to revoke on callers behalf. - * @return loanId Id of a newly minted LOAN token. - */ - function createLOANAndRevokeNonce( - address loanTermsFactoryContract, - bytes calldata loanTermsFactoryData, - bytes calldata signature, - bytes calldata loanAssetPermit, - bytes calldata collateralPermit, - uint256 callersNonceSpace, - uint256 callersNonceToRevoke - ) external returns (uint256 loanId) { - if (!revokedNonce.isNonceUsable(msg.sender, callersNonceSpace, callersNonceToRevoke)) - revert NonceNotUsable(); - - revokedNonce.revokeNonce(msg.sender, callersNonceSpace, callersNonceToRevoke); - loanId = createLOAN({ - loanTermsFactoryContract: loanTermsFactoryContract, - loanTermsFactoryData: loanTermsFactoryData, - signature: signature, - loanAssetPermit: loanAssetPermit, - collateralPermit: collateralPermit - }); - } - - /** - * @notice Create a loan terms by a loan terms factory contract. - * @dev The function will revert if the loan terms factory contract is not tagged in PWN Hub. - * @param loanTermsFactoryContract Address of a loan terms factory contract. Need to have `SIMPLE_LOAN_TERMS_FACTORY` tag in PWN Hub. - * @param loanTermsFactoryData Encoded data for a loan terms factory. - * @param signature Signed loan factory data. Could be empty if an offer / request has been made via on-chain transaction. - * @return loanTerms Loan terms struct. - * @return factoryDataHash Hash of the factory data. - */ - function _createLoanTerms( - address loanTermsFactoryContract, - bytes calldata loanTermsFactoryData, - bytes calldata signature - ) private returns (PWNLOANTerms.Simple memory loanTerms, bytes32 factoryDataHash) { - // Check that loan terms factory contract is tagged in PWNHub - if (!hub.hasTag(loanTermsFactoryContract, PWNHubTags.SIMPLE_LOAN_TERMS_FACTORY)) - revert CallerMissingHubTag(PWNHubTags.SIMPLE_LOAN_TERMS_FACTORY); - - // Build PWNLOANTerms.Simple by loan factory - (loanTerms, factoryDataHash) = PWNSimpleLoanTermsFactory(loanTermsFactoryContract).createLOANTerms({ - caller: msg.sender, - factoryData: loanTermsFactoryData, - signature: signature - }); - } - - /** - * @notice Check if the loan terms are valid for creating a new loan. - * @dev The function will revert if the loan terms are not valid for creating a new loan. - * @param loanTerms New loan terms struct. + * @notice Check loan terms validity. + * @dev The function will revert if the loan terms are not valid. + * @param loanTerms Loan terms struct. */ - function _checkNewLoanTerms(PWNLOANTerms.Simple memory loanTerms) private view { - // Check loan asset validity - if (!isValidAsset(loanTerms.asset)) - revert InvalidLoanAsset(); - - // Check collateral validity - if (!isValidAsset(loanTerms.collateral)) - revert InvalidCollateralAsset(); - - // Check that the terms can create a new loan - if (!loanTerms.canCreate) - revert InvalidCreateTerms(); + function _checkLoanTerms(Terms calldata loanTerms) private view { + // Check loan credit and collateral validity + _checkValidAsset(loanTerms.asset); + _checkValidAsset(loanTerms.collateral); } /** - * @notice Store a new loan in the contract state, mints new LOAN token, and emit a `LOANCreated` event. + * @notice Mint LOAN token and store loan data under loan id. + * @param proposalHash Hash of a loan offer / request that is signed by a lender / borrower. + * @param proposalContract Address of a loan proposal contract. * @param loanTerms Loan terms struct. - * @param factoryDataHash Hash of the factory data. - * @param loanTermsFactoryContract Address of a loan terms factory contract. - * @return loanId Id of a newly minted LOAN token. */ function _createLoan( - PWNLOANTerms.Simple memory loanTerms, - bytes32 factoryDataHash, - address loanTermsFactoryContract + bytes32 proposalHash, + address proposalContract, + Terms calldata loanTerms ) private returns (uint256 loanId) { // Mint LOAN token for lender loanId = loanToken.mint(loanTerms.lender); @@ -307,7 +261,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { loan.status = 2; loan.loanAssetAddress = loanTerms.asset.assetAddress; loan.startTimestamp = uint40(block.timestamp); - loan.defaultTimestamp = loanTerms.defaultTimestamp; + loan.defaultTimestamp = uint40(block.timestamp) + loanTerms.duration; loan.borrower = loanTerms.borrower; loan.originalLender = loanTerms.lender; loan.accruingInterestDailyRate = SafeCast.toUint40(Math.mulDiv( @@ -320,8 +274,8 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { emit LOANCreated({ loanId: loanId, terms: loanTerms, - factoryDataHash: factoryDataHash, - factoryAddress: loanTermsFactoryContract + proposalHash: proposalHash, + proposalContract: proposalContract }); } @@ -333,7 +287,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { * @param collateralPermit Permit data for a collateral signed by a borrower. */ function _settleNewLoan( - PWNLOANTerms.Simple memory loanTerms, + Terms memory loanTerms, bytes calldata loanAssetPermit, bytes calldata collateralPermit ) private { @@ -356,6 +310,8 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { loanTerms.asset.amount = newLoanAmount; } + // Note: If the fee amount is greater than zero, the loan asset amount is already updated to the new loan amount. + // Transfer loan asset to borrower _pushFrom(loanTerms.asset, loanTerms.lender, loanTerms.borrower); } @@ -371,36 +327,38 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { * 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 loanId Id of a loan that is being refinanced. - * @param loanTermsFactoryContract Address of a loan terms factory contract. Need to have `SIMPLE_LOAN_TERMS_FACTORY` tag in PWN Hub. - * @param loanTermsFactoryData Encoded data for a loan terms factory. - * @param signature Signed loan factory data. Could be empty if an offer / request has been made via on-chain transaction. + * @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 lenderLoanAssetPermit Permit data for a loan asset signed by a lender. * @param borrowerLoanAssetPermit Permit data for a loan asset signed by a borrower. * @return refinancedLoanId Id of the refinanced LOAN token. */ function refinanceLOAN( uint256 loanId, - address loanTermsFactoryContract, - bytes calldata loanTermsFactoryData, - bytes calldata signature, + bytes32 proposalHash, + Terms calldata loanTerms, bytes calldata lenderLoanAssetPermit, bytes calldata borrowerLoanAssetPermit ) 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); - // Create loan terms or revert if factory contract is not tagged in PWN Hub - (PWNLOANTerms.Simple memory loanTerms, bytes32 factoryDataHash) - = _createLoanTerms(loanTermsFactoryContract, loanTermsFactoryData, signature); - - // Check loan terms validity, revert if not + // Check refinance loan terms _checkRefinanceLoanTerms(loanId, loanTerms); // Create a new loan - refinancedLoanId = _createLoan(loanTerms, factoryDataHash, loanTermsFactoryContract); + refinancedLoanId = _createLoan({ + proposalHash: proposalHash, + proposalContract: msg.sender, + loanTerms: loanTerms + }); // Refinance the original loan _refinanceOriginalLoan( @@ -419,7 +377,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { * @param loanId Original loan id. * @param loanTerms Refinancing loan terms struct. */ - function _checkRefinanceLoanTerms(uint256 loanId, PWNLOANTerms.Simple memory loanTerms) private view { + function _checkRefinanceLoanTerms(uint256 loanId, Terms memory loanTerms) private view { LOAN storage loan = LOANs[loanId]; // Check that the loan asset is the same as in the original loan @@ -428,7 +386,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { if ( loan.loanAssetAddress != loanTerms.asset.assetAddress || loanTerms.asset.amount == 0 - ) revert InvalidLoanAsset(); + ) revert RefinanceCreditMismatch(); // Check that the collateral is identical to the original one if ( @@ -436,21 +394,15 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { loan.collateral.assetAddress != loanTerms.collateral.assetAddress || loan.collateral.id != loanTerms.collateral.id || loan.collateral.amount != loanTerms.collateral.amount - ) revert InvalidCollateralAsset(); + ) revert RefinanceCollateralMismatch(); // Check that the borrower is the same as in the original loan if (loan.borrower != loanTerms.borrower) { - revert BorrowerMismatch({ + revert RefinanceBorrowerMismatch({ currentBorrower: loan.borrower, newBorrower: loanTerms.borrower }); } - - // Check that the terms can refinance a loan - if (!loanTerms.canRefinance) - revert InvalidRefinanceTerms(); - if (loanTerms.refinancingLoanId != 0 && loanTerms.refinancingLoanId != loanId) - revert InvalidRefinancingLoanId({ refinancingLoanId: loanTerms.refinancingLoanId }); } /** @@ -466,7 +418,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { */ function _refinanceOriginalLoan( uint256 loanId, - PWNLOANTerms.Simple memory loanTerms, + Terms memory loanTerms, bytes calldata lenderLoanAssetPermit, bytes calldata borrowerLoanAssetPermit ) private { @@ -502,7 +454,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { bool repayLoanDirectly, address loanOwner, uint256 repaymentAmount, - PWNLOANTerms.Simple memory loanTerms, + Terms memory loanTerms, bytes calldata lenderPermit, bytes calldata borrowerPermit ) private { @@ -859,11 +811,11 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { bytes32 extensionHash = getExtensionHash(extension); if (!extensionOffersMade[extensionHash]) if (!PWNSignatureChecker.isValidSignatureNow(extension.proposer, extensionHash, signature)) - revert InvalidSignature(); + revert InvalidSignature({ signer: extension.proposer, digest: extensionHash }); if (block.timestamp >= extension.expiration) - revert OfferExpired(); + revert Expired({ current: block.timestamp, expiration: extension.expiration }); if (!revokedNonce.isNonceUsable(extension.proposer, extension.nonceSpace, extension.nonce)) - revert NonceNotUsable(); + revert NonceNotUsable({ addr: extension.proposer, nonceSpace: extension.nonceSpace, nonce: extension.nonce }); // Check caller and signer address loanOwner = loanToken.ownerOf(extension.loanId); @@ -1011,6 +963,22 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { return MultiToken.isValid(asset, categoryRegistry); } + /** + * @notice Check if the asset is valid with the MultiToken lib and the category registry. + * @dev The function will revert if the asset is not valid. + * @param asset Asset to be checked. + */ + function _checkValidAsset(MultiToken.Asset memory asset) private view { + if (!isValidAsset(asset)) { + revert InvalidMultiTokenAsset({ + category: uint8(asset.category), + addr: asset.assetAddress, + id: asset.id, + amount: asset.amount + }); + } + } + /*----------------------------------------------------------*| |* # IPWNLoanMetadataProvider *| diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol new file mode 100644 index 0000000..1005761 --- /dev/null +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol @@ -0,0 +1,233 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.16; + +import { PWNHub } from "@pwn/hub/PWNHub.sol"; +import { PWNHubTags } from "@pwn/hub/PWNHubTags.sol"; +import { PWNSignatureChecker } from "@pwn/loan/lib/PWNSignatureChecker.sol"; +import { PWNRevokedNonce } from "@pwn/nonce/PWNRevokedNonce.sol"; +import { StateFingerprintComputerRegistry, IERC5646 } from "@pwn/state-fingerprint/StateFingerprintComputerRegistry.sol"; +import "@pwn/PWNErrors.sol"; + +/** + * @title PWN Simple Loan Proposal Base Contract + * @notice Base contract of loan proposals that builds a simple loan terms. + */ +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; + StateFingerprintComputerRegistry public immutable stateFingerprintComputerRegistry; + + /** + * @dev Mapping of proposals made via on-chain transactions. + * Could be used by contract wallets instead of EIP-1271. + * (proposal hash => is made) + */ + mapping (bytes32 => bool) public proposalsMade; + + /** + * @dev Mapping of credit used by a proposal with defined available credit limit. + * (proposal hash => credit used) + */ + mapping (bytes32 => uint256) public creditUsed; + + /** + * @dev Emitted when a proposal is made via an on-chain transaction. + */ + event ProposalMade(bytes32 indexed proposalHash, address indexed proposer, bytes proposal); + + constructor( + address _hub, + address _revokedNonce, + address _stateFingerprintComputerRegistry, + string memory name, + string memory version + ) { + hub = PWNHub(_hub); + revokedNonce = PWNRevokedNonce(_revokedNonce); + stateFingerprintComputerRegistry = StateFingerprintComputerRegistry(_stateFingerprintComputerRegistry); + + DOMAIN_SEPARATOR = keccak256(abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256(abi.encodePacked(name)), + keccak256(abi.encodePacked(version)), + block.chainid, + address(this) + )); + } + + + /** + * @notice Helper function for revoking a proposal nonce on behalf of a caller. + * @param nonceSpace Nonce space of a proposal nonce to be revoked. + * @param nonce Proposal nonce to be revoked. + */ + function revokeNonce(uint256 nonceSpace, uint256 nonce) external { + revokedNonce.revokeNonce(msg.sender, nonceSpace, nonce); + } + + + /*----------------------------------------------------------*| + |* # INTERNALS *| + |*----------------------------------------------------------*/ + + /** + * @notice Try to accept a proposal. + * @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. + */ + function _tryAcceptProposal( + bytes32 proposalHash, + uint256 creditAmount, + uint256 availableCreditLimit, + uint40 apr, + uint32 duration, + uint40 expiration, + uint256 nonceSpace, + uint256 nonce, + address allowedAcceptor, + address acceptor, + address signer, + bytes memory signature + ) internal { + // 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 }); + } + } + + // Check proposal is not expired + if (block.timestamp >= expiration) { + revert Expired({ current: block.timestamp, expiration: expiration }); + } + + // Check proposal is not revoked + if (!revokedNonce.isNonceUsable(signer, nonceSpace, nonce)) { + revert NonceNotUsable({ addr: signer, nonceSpace: nonceSpace, nonce: 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 }); + } + + // Check maximum accruing interest APR + if (apr > MAX_ACCRUING_INTEREST_APR) { + revert AccruingInterestAPROutOfBounds({ current: apr, limit: MAX_ACCRUING_INTEREST_APR }); + } + + if (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) { + // Increase used credit if credit limit is not exceeded + creditUsed[proposalHash] += creditAmount; + } else { + // Revert if credit limit is exceeded + revert AvailableCreditLimitExceeded({ + used: creditUsed[proposalHash] + creditAmount, + limit: 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 = stateFingerprintComputerRegistry.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 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, bytes memory proposal) internal { + if (msg.sender != proposer) { + revert CallerIsNotStatedProposer(proposer); + } + + proposalsMade[proposalHash] = true; + + emit ProposalMade(proposalHash, proposer, 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 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/offer/PWNSimpleLoanListOffer.sol b/src/loan/terms/simple/proposal/offer/PWNSimpleLoanListOffer.sol new file mode 100644 index 0000000..8d4d29e --- /dev/null +++ b/src/loan/terms/simple/proposal/offer/PWNSimpleLoanListOffer.sol @@ -0,0 +1,277 @@ +// 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 "@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 loanAssetAddress,uint256 loanAmount,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 loanAssetAddress Address of an asset which is lender to a borrower. + * @param loanAmount 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 loan asset 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 loanAssetAddress; + uint256 loanAmount; + uint256 availableCreditLimit; + uint256 fixedInterestAmount; + uint40 accruingInterestAPR; + uint32 duration; + uint40 expiration; + address allowedBorrower; + address lender; + uint256 refinancingLoanId; + uint256 nonceSpace; + uint256 nonce; + address loanContract; + } + + /** + * 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; + } + + 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. + */ + function makeOffer(Offer calldata offer) external { + _makeProposal(getOfferHash(offer), offer.lender, abi.encode(offer)); + } + + + function acceptOffer( + Offer calldata offer, + OfferValues calldata offerValues, + bytes calldata signature, + bytes calldata loanAssetPermit, + bytes calldata collateralPermit + ) public returns (uint256 loanId) { + // Check if the offer is refinancing offer + if (offer.refinancingLoanId != 0) { + revert InvalidRefinancingLoanId({ refinancingLoanId: offer.refinancingLoanId }); + } + + (bytes32 offerHash, PWNSimpleLoan.Terms memory loanTerms) = _acceptOffer(offer, offerValues, signature); + + // Create loan + return PWNSimpleLoan(offer.loanContract).createLOAN({ + proposalHash: offerHash, + loanTerms: loanTerms, + loanAssetPermit: loanAssetPermit, + collateralPermit: collateralPermit + }); + } + + function acceptRefinanceOffer( + uint256 loanId, + Offer calldata offer, + OfferValues calldata offerValues, + bytes calldata signature, + bytes calldata lenderLoanAssetPermit, + bytes calldata borrowerLoanAssetPermit + ) public returns (uint256 refinancedLoanId) { + // Check if the offer is refinancing offer + if (offer.refinancingLoanId != 0 && offer.refinancingLoanId != loanId) { + revert InvalidRefinancingLoanId({ refinancingLoanId: offer.refinancingLoanId }); + } + + (bytes32 offerHash, PWNSimpleLoan.Terms memory loanTerms) = _acceptOffer(offer, offerValues, signature); + + // Refinance loan + return PWNSimpleLoan(offer.loanContract).refinanceLOAN({ + loanId: loanId, + proposalHash: offerHash, + loanTerms: loanTerms, + lenderLoanAssetPermit: lenderLoanAssetPermit, + borrowerLoanAssetPermit: borrowerLoanAssetPermit + }); + } + + function acceptOffer( + Offer calldata offer, + OfferValues calldata offerValues, + bytes calldata signature, + bytes calldata loanAssetPermit, + bytes calldata collateralPermit, + uint256 callersNonceSpace, + uint256 callersNonceToRevoke + ) external returns (uint256 loanId) { + _revokeCallersNonce(msg.sender, callersNonceSpace, callersNonceToRevoke); + return acceptOffer(offer, offerValues, signature, loanAssetPermit, collateralPermit); + } + + function acceptRefinanceOffer( + uint256 loanId, + Offer calldata offer, + OfferValues calldata offerValues, + bytes calldata signature, + bytes calldata lenderLoanAssetPermit, + bytes calldata borrowerLoanAssetPermit, + uint256 callersNonceSpace, + uint256 callersNonceToRevoke + ) external returns (uint256 refinancedLoanId) { + _revokeCallersNonce(msg.sender, callersNonceSpace, callersNonceToRevoke); + return acceptRefinanceOffer(loanId, offer, offerValues, signature, lenderLoanAssetPermit, borrowerLoanAssetPermit); + } + + + 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.loanAmount, + 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 + }), + asset: MultiToken.ERC20({ + assetAddress: offer.loanAssetAddress, + amount: offer.loanAmount + }), + fixedInterestAmount: offer.fixedInterestAmount, + accruingInterestAPR: offer.accruingInterestAPR + }); + } + + function decodeProposal(bytes calldata proposal) external pure returns (Offer memory offer) { + return abi.decode(proposal, (Offer)); + } + +} diff --git a/src/loan/terms/simple/proposal/offer/PWNSimpleLoanSimpleOffer.sol b/src/loan/terms/simple/proposal/offer/PWNSimpleLoanSimpleOffer.sol new file mode 100644 index 0000000..eaf4382 --- /dev/null +++ b/src/loan/terms/simple/proposal/offer/PWNSimpleLoanSimpleOffer.sol @@ -0,0 +1,234 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.16; + +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 "@pwn/PWNErrors.sol"; + + +/** + * @title PWN Simple Loan Simple Offer + * @notice Loan terms factory contract creating a simple loan terms from a simple offer. + */ +contract PWNSimpleLoanSimpleOffer 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,uint256 collateralId,uint256 collateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address loanAssetAddress,uint256 loanAmount,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 simple 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 collateralId Token id of an asset used as a collateral, in case of ERC20 should be 0. + * @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 loanAssetAddress Address of an asset which is lender to a borrower. + * @param loanAmount 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 loan asset 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; + uint256 collateralId; + uint256 collateralAmount; + bool checkCollateralStateFingerprint; + bytes32 collateralStateFingerprint; + address loanAssetAddress; + uint256 loanAmount; + uint256 availableCreditLimit; + uint256 fixedInterestAmount; + uint40 accruingInterestAPR; + uint32 duration; + uint40 expiration; + address allowedBorrower; + address lender; + uint256 refinancingLoanId; + uint256 nonceSpace; + uint256 nonce; + address loanContract; + } + + constructor( + address _hub, + address _revokedNonce, + address _stateFingerprintComputerRegistry + ) PWNSimpleLoanProposal( + _hub, _revokedNonce, _stateFingerprintComputerRegistry, "PWNSimpleLoanSimpleOffer", 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. + */ + function makeOffer(Offer calldata offer) external { + _makeProposal(getOfferHash(offer), offer.lender, abi.encode(offer)); + } + + function acceptOffer( + Offer calldata offer, + bytes calldata signature, + bytes calldata loanAssetPermit, + bytes calldata collateralPermit + ) public returns (uint256 loanId) { + // Check if the offer is refinancing offer + if (offer.refinancingLoanId != 0) { + revert InvalidRefinancingLoanId({ refinancingLoanId: offer.refinancingLoanId }); + } + + (bytes32 offerHash, PWNSimpleLoan.Terms memory loanTerms) = _acceptOffer(offer, signature); + + // Create loan + return PWNSimpleLoan(offer.loanContract).createLOAN({ + proposalHash: offerHash, + loanTerms: loanTerms, + loanAssetPermit: loanAssetPermit, + collateralPermit: collateralPermit + }); + } + + function acceptRefinanceOffer( + uint256 loanId, + Offer calldata offer, + bytes calldata signature, + bytes calldata lenderLoanAssetPermit, + bytes calldata borrowerLoanAssetPermit + ) public returns (uint256 refinancedLoanId) { + // Check if the offer is refinancing offer + if (offer.refinancingLoanId != 0 && offer.refinancingLoanId != loanId) { + revert InvalidRefinancingLoanId({ refinancingLoanId: offer.refinancingLoanId }); + } + + (bytes32 offerHash, PWNSimpleLoan.Terms memory loanTerms) = _acceptOffer(offer, signature); + + // Refinance loan + return PWNSimpleLoan(offer.loanContract).refinanceLOAN({ + loanId: loanId, + proposalHash: offerHash, + loanTerms: loanTerms, + lenderLoanAssetPermit: lenderLoanAssetPermit, + borrowerLoanAssetPermit: borrowerLoanAssetPermit + }); + } + + function acceptOffer( + Offer calldata offer, + bytes calldata signature, + bytes calldata loanAssetPermit, + bytes calldata collateralPermit, + uint256 callersNonceSpace, + uint256 callersNonceToRevoke + ) external returns (uint256 loanId) { + _revokeCallersNonce(msg.sender, callersNonceSpace, callersNonceToRevoke); + return acceptOffer(offer, signature, loanAssetPermit, collateralPermit); + } + + function acceptRefinanceOffer( + uint256 loanId, + Offer calldata offer, + bytes calldata signature, + bytes calldata lenderLoanAssetPermit, + bytes calldata borrowerLoanAssetPermit, + uint256 callersNonceSpace, + uint256 callersNonceToRevoke + ) external returns (uint256 refinancedLoanId) { + _revokeCallersNonce(msg.sender, callersNonceSpace, callersNonceToRevoke); + return acceptRefinanceOffer(loanId, offer, signature, lenderLoanAssetPermit, borrowerLoanAssetPermit); + } + + + function _acceptOffer( + Offer calldata offer, + bytes calldata signature + ) private returns (bytes32 offerHash, PWNSimpleLoan.Terms memory loanTerms) { + // Check if the loan contract has a tag + _checkLoanContractTag(offer.loanContract); + + // Check collateral state fingerprint if needed + if (offer.checkCollateralStateFingerprint) { + _checkCollateralState({ + addr: offer.collateralAddress, + id: offer.collateralId, + stateFingerprint: offer.collateralStateFingerprint + }); + } + + // Try to accept offer + offerHash = _tryAcceptOffer(offer, signature); + + // Create loan terms object + loanTerms = _createLoanTerms(offer); + } + + function _tryAcceptOffer(Offer calldata offer, bytes calldata signature) private returns (bytes32 offerHash) { + offerHash = getOfferHash(offer); + _tryAcceptProposal({ + proposalHash: offerHash, + creditAmount: offer.loanAmount, + 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) 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: offer.collateralId, + amount: offer.collateralAmount + }), + asset: MultiToken.ERC20({ + assetAddress: offer.loanAssetAddress, + amount: offer.loanAmount + }), + fixedInterestAmount: offer.fixedInterestAmount, + accruingInterestAPR: offer.accruingInterestAPR + }); + } + + function decodeProposal(bytes calldata proposal) external pure returns (Offer memory offer) { + return abi.decode(proposal, (Offer)); + } + +} diff --git a/src/loan/terms/simple/proposal/request/PWNSimpleLoanSimpleRequest.sol b/src/loan/terms/simple/proposal/request/PWNSimpleLoanSimpleRequest.sol new file mode 100644 index 0000000..4e9bec7 --- /dev/null +++ b/src/loan/terms/simple/proposal/request/PWNSimpleLoanSimpleRequest.sol @@ -0,0 +1,235 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.16; + +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 "@pwn/PWNErrors.sol"; + + +/** + * @title PWN Simple Loan Simple Request + * @notice Loan terms factory contract creating a simple loan terms from a simple request. + */ +contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanProposal { + + string public constant VERSION = "1.2"; + + /** + * @dev EIP-712 simple request struct type hash. + */ + bytes32 public constant REQUEST_TYPEHASH = keccak256( + "Request(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address loanAssetAddress,uint256 loanAmount,uint256 availableCreditLimit,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedLender,address borrower,uint256 refinancingLoanId,uint256 nonceSpace,uint256 nonce,address loanContract)" + ); + + /** + * @notice Construct defining a simple request. + * @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 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 loanAssetAddress Address of an asset which is lender to a borrower. + * @param loanAmount Amount of tokens which is requested as a loan to a borrower. + * @param availableCreditLimit Available credit limit for the request. It is the maximum amount of tokens which can be borrowed using the request. + * @param fixedInterestAmount Fixed interest amount in loan asset 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 Request expiration timestamp in seconds. + * @param allowedLender Address of an allowed lender. Only this address can accept a request. If the address is zero address, anybody with a loan asset can accept the request. + * @param borrower Address of a borrower. This address has to sign a request to be valid. + * @param refinancingLoanId Id of a loan which is refinanced by this request. If the id is 0, the request is not a refinancing request. + * @param nonceSpace Nonce space of a request nonce. All nonces in the same space can be revoked at once. + * @param nonce Additional value to enable identical requests in time. Without it, it would be impossible to make again request, which was once revoked. + * Can be used to create a group of requests, where accepting one request will make other requests in the group revoked. + * @param loanContract Address of a loan contract that will create a loan from the request. + */ + struct Request { + MultiToken.Category collateralCategory; + address collateralAddress; + uint256 collateralId; + uint256 collateralAmount; + bool checkCollateralStateFingerprint; + bytes32 collateralStateFingerprint; + address loanAssetAddress; + uint256 loanAmount; + uint256 availableCreditLimit; + uint256 fixedInterestAmount; + uint40 accruingInterestAPR; + uint32 duration; + uint40 expiration; + address allowedLender; + address borrower; + uint256 refinancingLoanId; + uint256 nonceSpace; + uint256 nonce; + address loanContract; + } + + constructor( + address _hub, + address _revokedNonce, + address _stateFingerprintComputerRegistry + ) PWNSimpleLoanProposal( + _hub, _revokedNonce, _stateFingerprintComputerRegistry, "PWNSimpleLoanSimpleRequest", VERSION + ) {} + + /** + * @notice Get a request hash according to EIP-712. + * @param request Request struct to be hashed. + * @return Request struct hash. + */ + function getRequestHash(Request calldata request) public view returns (bytes32) { + return _getProposalHash(REQUEST_TYPEHASH, abi.encode(request)); + } + + /** + * @notice Make an on-chain request. + * @dev Function will mark a request hash as proposed. Request will become acceptable by a lender without a request signature. + * @param request Request struct containing all needed request data. + */ + function makeRequest(Request calldata request) external { + _makeProposal(getRequestHash(request), request.borrower, abi.encode(request)); + } + + + function acceptRequest( + Request calldata request, + bytes calldata signature, + bytes calldata loanAssetPermit, + bytes calldata collateralPermit + ) public returns (uint256 loanId) { + // Check if the request is refinancing request + if (request.refinancingLoanId != 0) { + revert InvalidRefinancingLoanId({ refinancingLoanId: request.refinancingLoanId }); + } + + (bytes32 requestHash, PWNSimpleLoan.Terms memory loanTerms) = _acceptRequest(request, signature); + + // Create loan + return PWNSimpleLoan(request.loanContract).createLOAN({ + proposalHash: requestHash, + loanTerms: loanTerms, + loanAssetPermit: loanAssetPermit, + collateralPermit: collateralPermit + }); + } + + function acceptRefinanceRequest( + uint256 loanId, + Request calldata request, + bytes calldata signature, + bytes calldata lenderLoanAssetPermit, + bytes calldata borrowerLoanAssetPermit + ) public returns (uint256 refinancedLoanId) { + // Check if the request is refinancing request + if (request.refinancingLoanId == 0 || request.refinancingLoanId != loanId) { + revert InvalidRefinancingLoanId({ refinancingLoanId: request.refinancingLoanId }); + } + + (bytes32 requestHash, PWNSimpleLoan.Terms memory loanTerms) = _acceptRequest(request, signature); + + // Refinance loan + return PWNSimpleLoan(request.loanContract).refinanceLOAN({ + loanId: loanId, + proposalHash: requestHash, + loanTerms: loanTerms, + lenderLoanAssetPermit: lenderLoanAssetPermit, + borrowerLoanAssetPermit: borrowerLoanAssetPermit + }); + } + + function acceptRequest( + Request calldata request, + bytes calldata signature, + bytes calldata loanAssetPermit, + bytes calldata collateralPermit, + uint256 callersNonceSpace, + uint256 callersNonceToRevoke + ) external returns (uint256 loanId) { + _revokeCallersNonce(msg.sender, callersNonceSpace, callersNonceToRevoke); + return acceptRequest(request, signature, loanAssetPermit, collateralPermit); + } + + function acceptRefinanceRequest( + uint256 loanId, + Request calldata request, + bytes calldata signature, + bytes calldata lenderLoanAssetPermit, + bytes calldata borrowerLoanAssetPermit, + uint256 callersNonceSpace, + uint256 callersNonceToRevoke + ) external returns (uint256 refinancedLoanId) { + _revokeCallersNonce(msg.sender, callersNonceSpace, callersNonceToRevoke); + return acceptRefinanceRequest(loanId, request, signature, lenderLoanAssetPermit, borrowerLoanAssetPermit); + } + + + function _acceptRequest( + Request calldata request, + bytes calldata signature + ) private returns (bytes32 requestHash, PWNSimpleLoan.Terms memory loanTerms) { + // Check if the loan contract has a tag + _checkLoanContractTag(request.loanContract); + + // Check collateral state fingerprint if needed + if (request.checkCollateralStateFingerprint) { + _checkCollateralState({ + addr: request.collateralAddress, + id: request.collateralId, + stateFingerprint: request.collateralStateFingerprint + }); + } + + // Try to accept request + requestHash = _tryAcceptRequest(request, signature); + + // Create loan terms object + loanTerms = _createLoanTerms(request); + } + + function _tryAcceptRequest(Request calldata request, bytes calldata signature) private returns (bytes32 requestHash) { + requestHash = getRequestHash(request); + _tryAcceptProposal({ + proposalHash: requestHash, + creditAmount: request.loanAmount, + availableCreditLimit: request.availableCreditLimit, + apr: request.accruingInterestAPR, + duration: request.duration, + expiration: request.expiration, + nonceSpace: request.nonceSpace, + nonce: request.nonce, + allowedAcceptor: request.allowedLender, + acceptor: msg.sender, + signer: request.borrower, + signature: signature + }); + } + + function _createLoanTerms(Request calldata request) private view returns (PWNSimpleLoan.Terms memory) { + return PWNSimpleLoan.Terms({ + lender: msg.sender, + borrower: request.borrower, + duration: request.duration, + collateral: MultiToken.Asset({ + category: request.collateralCategory, + assetAddress: request.collateralAddress, + id: request.collateralId, + amount: request.collateralAmount + }), + asset: MultiToken.ERC20({ + assetAddress: request.loanAssetAddress, + amount: request.loanAmount + }), + fixedInterestAmount: request.fixedInterestAmount, + accruingInterestAPR: request.accruingInterestAPR + }); + } + + function decodeProposal(bytes calldata proposal) external pure returns (Request memory request) { + return abi.decode(proposal, (Request)); + } + +} diff --git a/test/helper/DeploymentTest.t.sol b/test/helper/DeploymentTest.t.sol index 3c4743d..6132d58 100644 --- a/test/helper/DeploymentTest.t.sol +++ b/test/helper/DeploymentTest.t.sol @@ -63,11 +63,11 @@ abstract contract DeploymentTest is Deployments, Test { bytes32[] memory tags = new bytes32[](8); tags[0] = PWNHubTags.ACTIVE_LOAN; tags[1] = PWNHubTags.NONCE_MANAGER; - tags[2] = PWNHubTags.SIMPLE_LOAN_TERMS_FACTORY; + tags[2] = PWNHubTags.LOAN_PROPOSAL; tags[3] = PWNHubTags.NONCE_MANAGER; - tags[4] = PWNHubTags.SIMPLE_LOAN_TERMS_FACTORY; + tags[4] = PWNHubTags.LOAN_PROPOSAL; tags[5] = PWNHubTags.NONCE_MANAGER; - tags[6] = PWNHubTags.SIMPLE_LOAN_TERMS_FACTORY; + tags[6] = PWNHubTags.LOAN_PROPOSAL; tags[7] = PWNHubTags.NONCE_MANAGER; vm.prank(protocolSafe); diff --git a/test/unit/PWNSimpleLoan.t.sol b/test/unit/PWNSimpleLoan.t.sol index c1fe05f..9becadd 100644 --- a/test/unit/PWNSimpleLoan.t.sol +++ b/test/unit/PWNSimpleLoan.t.sol @@ -3,17 +3,16 @@ pragma solidity 0.8.16; import "forge-std/Test.sol"; -import "MultiToken/MultiToken.sol"; +import { MultiToken } from "MultiToken/MultiToken.sol"; import { Math } from "openzeppelin-contracts/contracts/utils/math/Math.sol"; -import "@pwn/hub/PWNHubTags.sol"; -import "@pwn/loan/terms/simple/loan/PWNSimpleLoan.sol"; -import "@pwn/loan/terms/PWNLOANTerms.sol"; +import { PWNHubTags } from "@pwn/hub/PWNHubTags.sol"; +import { PWNSimpleLoan } from "@pwn/loan/terms/simple/loan/PWNSimpleLoan.sol"; import "@pwn/PWNErrors.sol"; -import "@pwn-test/helper/token/T20.sol"; -import "@pwn-test/helper/token/T721.sol"; +import { T20 } from "@pwn-test/helper/token/T20.sol"; +import { T721 } from "@pwn-test/helper/token/T721.sol"; abstract contract PWNSimpleLoanTest is Test { @@ -29,25 +28,23 @@ abstract contract PWNSimpleLoanTest is Test { address categoryRegistry = makeAddr("categoryRegistry"); address feeCollector = makeAddr("feeCollector"); address alice = makeAddr("alice"); - address loanFactory = makeAddr("loanFactory"); + address proposalContract = makeAddr("proposalContract"); uint256 loanId = 42; address lender = makeAddr("lender"); address borrower = makeAddr("borrower"); uint256 loanDurationInDays = 101; PWNSimpleLoan.LOAN simpleLoan; PWNSimpleLoan.LOAN nonExistingLoan; - PWNLOANTerms.Simple simpleLoanTerms; + PWNSimpleLoan.Terms simpleLoanTerms; PWNSimpleLoan.Extension extension; T20 fungibleAsset; T721 nonFungibleAsset; - bytes loanFactoryData; - bytes signature; - bytes loanAssetPermit; - bytes collateralPermit; - bytes32 loanFactoryDataHash; + bytes loanAssetPermit = abi.encodePacked(uint256(1), uint256(2), uint256(3), uint8(4)); + bytes collateralPermit = abi.encodePacked(uint256(1), uint256(2), uint256(3), uint8(4)); + bytes32 proposalHash = keccak256("proposalHash"); - event LOANCreated(uint256 indexed loanId, PWNLOANTerms.Simple terms, bytes32 indexed factoryDataHash, address indexed factoryAddress); + event LOANCreated(uint256 indexed loanId, PWNSimpleLoan.Terms terms, bytes32 indexed factoryDataHash, address indexed factoryAddress); event LOANPaidBack(uint256 indexed loanId); event LOANClaimed(uint256 indexed loanId, bool indexed defaulted); event LOANRefinanced(uint256 indexed loanId, uint256 indexed refinancedLoanId); @@ -57,7 +54,7 @@ abstract contract PWNSimpleLoanTest is Test { function setUp() virtual public { vm.etch(hub, bytes("data")); vm.etch(loanToken, bytes("data")); - vm.etch(loanFactory, bytes("data")); + vm.etch(proposalContract, bytes("data")); vm.etch(config, bytes("data")); loan = new PWNSimpleLoan(hub, loanToken, config, revokedNonce, categoryRegistry); @@ -82,11 +79,6 @@ abstract contract PWNSimpleLoanTest is Test { vm.prank(borrower); nonFungibleAsset.approve(address(loan), 2); - loanFactoryData = ""; - signature = ""; - loanAssetPermit = ""; - collateralPermit = ""; - simpleLoan = PWNSimpleLoan.LOAN({ status: 2, loanAssetAddress: address(fungibleAsset), @@ -100,17 +92,14 @@ abstract contract PWNSimpleLoanTest is Test { collateral: MultiToken.ERC721(address(nonFungibleAsset), 2) }); - simpleLoanTerms = PWNLOANTerms.Simple({ + simpleLoanTerms = PWNSimpleLoan.Terms({ lender: lender, borrower: borrower, - defaultTimestamp: uint40(block.timestamp + loanDurationInDays * 1 days), + duration: uint32(loanDurationInDays * 1 days), collateral: MultiToken.ERC721(address(nonFungibleAsset), 2), asset: MultiToken.ERC20(address(fungibleAsset), 100), fixedInterestAmount: 6631, - accruingInterestAPR: 0, - canCreate: true, - canRefinance: true, - refinancingLoanId: 0 + accruingInterestAPR: 0 }); nonExistingLoan = PWNSimpleLoan.LOAN({ @@ -136,8 +125,6 @@ abstract contract PWNSimpleLoanTest is Test { nonce: 1 }); - loanFactoryDataHash = keccak256("factoryData"); - vm.mockCall( address(fungibleAsset), abi.encodeWithSignature("permit(address,address,uint256,uint256,uint8,bytes32,bytes32)"), @@ -156,11 +143,10 @@ abstract contract PWNSimpleLoanTest is Test { vm.mockCall(hub, abi.encodeWithSignature("hasTag(address,bytes32)"), abi.encode(false)); vm.mockCall( hub, - abi.encodeWithSignature("hasTag(address,bytes32)", loanFactory, PWNHubTags.SIMPLE_LOAN_TERMS_FACTORY), + abi.encodeWithSignature("hasTag(address,bytes32)", proposalContract, PWNHubTags.LOAN_PROPOSAL), abi.encode(true) ); - _mockLoanTerms(simpleLoanTerms, loanFactoryDataHash); _mockLOANMint(loanId); _mockLOANTokenOwner(loanId, lender); @@ -229,14 +215,6 @@ abstract contract PWNSimpleLoanTest is Test { _storeLOANWord(loanSlot + 7, abi.encodePacked(_simpleLoan.collateral.amount)); } - function _mockLoanTerms(PWNLOANTerms.Simple memory _loanTerms, bytes32 _loanFactoryDataHash) internal { - vm.mockCall( - loanFactory, - abi.encodeWithSignature("createLOANTerms(address,bytes,bytes)"), - abi.encode(_loanTerms, _loanFactoryDataHash) - ); - } - function _mockLOANMint(uint256 _loanId) internal { vm.mockCall(loanToken, abi.encodeWithSignature("mint(address)"), abi.encode(_loanId)); } @@ -294,71 +272,92 @@ abstract contract PWNSimpleLoanTest is Test { contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { - function testFuzz_shouldFail_whenLoanFactoryContractIsNotTaggerInPWNHub(address notLoanFactory) external { - vm.assume(notLoanFactory != loanFactory); - - vm.expectRevert(abi.encodeWithSelector(CallerMissingHubTag.selector, PWNHubTags.SIMPLE_LOAN_TERMS_FACTORY)); - loan.createLOAN(notLoanFactory, loanFactoryData, signature, loanAssetPermit, collateralPermit); - } - - function test_shouldCall_createLOANTerms_onProvidedFactoryContract() external { - loanFactoryData = abi.encode(1, 2, "data"); - signature = abi.encode("other data", "whaat?", uint256(312312)); + function testFuzz_shouldFail_whenCallerNotTagged_LOAN_PROPOSAL(address caller) external { + vm.assume(caller != proposalContract); - vm.expectCall( - address(loanFactory), - abi.encodeWithSignature("createLOANTerms(address,bytes,bytes)", address(this), loanFactoryData, signature) - ); - - loan.createLOAN(loanFactory, loanFactoryData, signature, loanAssetPermit, collateralPermit); + vm.expectRevert(abi.encodeWithSelector(CallerMissingHubTag.selector, PWNHubTags.LOAN_PROPOSAL)); + vm.prank(caller); + loan.createLOAN({ + proposalHash: proposalHash, + loanTerms: simpleLoanTerms, + loanAssetPermit: "", + collateralPermit: "" + }); } - function test_shouldFailWhenLoanAssetIsInvalid() external { + function test_shouldFail_whenInvalidCreditAsset() external { vm.mockCall( categoryRegistry, abi.encodeWithSignature("registeredCategoryValue(address)", simpleLoanTerms.asset.assetAddress), abi.encode(1) ); - vm.expectRevert(InvalidLoanAsset.selector); - loan.createLOAN(loanFactory, loanFactoryData, signature, loanAssetPermit, collateralPermit); + vm.expectRevert( + abi.encodeWithSelector( + InvalidMultiTokenAsset.selector, + uint8(simpleLoanTerms.asset.category), + simpleLoanTerms.asset.assetAddress, + simpleLoanTerms.asset.id, + simpleLoanTerms.asset.amount + ) + ); + vm.prank(proposalContract); + loan.createLOAN({ + proposalHash: proposalHash, + loanTerms: simpleLoanTerms, + loanAssetPermit: "", + collateralPermit: "" + }); } - function test_shouldFailWhenCollateralAssetIsInvalid() external { + function test_shouldFail_whenInvalidCollateralAsset() external { vm.mockCall( categoryRegistry, abi.encodeWithSignature("registeredCategoryValue(address)", simpleLoanTerms.collateral.assetAddress), abi.encode(0) ); - vm.expectRevert(InvalidCollateralAsset.selector); - loan.createLOAN(loanFactory, loanFactoryData, signature, loanAssetPermit, collateralPermit); - } - - function test_shouldFail_whenInvalidCreateTerms() external { - simpleLoanTerms.canCreate = false; - _mockLoanTerms(simpleLoanTerms, loanFactoryDataHash); - - vm.expectRevert(abi.encodeWithSelector(InvalidCreateTerms.selector)); - loan.createLOAN(loanFactory, loanFactoryData, signature, loanAssetPermit, collateralPermit); + vm.expectRevert( + abi.encodeWithSelector( + InvalidMultiTokenAsset.selector, + uint8(simpleLoanTerms.collateral.category), + simpleLoanTerms.collateral.assetAddress, + simpleLoanTerms.collateral.id, + simpleLoanTerms.collateral.amount + ) + ); + vm.prank(proposalContract); + loan.createLOAN({ + proposalHash: proposalHash, + loanTerms: simpleLoanTerms, + loanAssetPermit: "", + collateralPermit: "" + }); } function test_shouldMintLOANToken() external { - vm.expectCall( - address(loanToken), - abi.encodeWithSignature("mint(address)", lender) - ); - - loan.createLOAN(loanFactory, loanFactoryData, signature, loanAssetPermit, collateralPermit); + vm.expectCall(address(loanToken), abi.encodeWithSignature("mint(address)", lender)); + + vm.prank(proposalContract); + loan.createLOAN({ + proposalHash: proposalHash, + loanTerms: simpleLoanTerms, + loanAssetPermit: "", + collateralPermit: "" + }); } function testFuzz_shouldStoreLoanData(uint40 accruingInterestAPR) external { accruingInterestAPR = uint40(bound(accruingInterestAPR, 0, 1e11)); - simpleLoanTerms.accruingInterestAPR = accruingInterestAPR; - _mockLoanTerms(simpleLoanTerms, loanFactoryDataHash); - loan.createLOAN(loanFactory, loanFactoryData, signature, loanAssetPermit, collateralPermit); + vm.prank(proposalContract); + loan.createLOAN({ + proposalHash: proposalHash, + loanTerms: simpleLoanTerms, + loanAssetPermit: "", + collateralPermit: "" + }); simpleLoan.accruingInterestDailyRate = uint40(uint256(accruingInterestAPR) * 274 / 1e5); _assertLOANEq(loanId, simpleLoan); @@ -369,8 +368,6 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { simpleLoanTerms.collateral.assetAddress = address(fungibleAsset); simpleLoanTerms.collateral.id = 0; simpleLoanTerms.collateral.amount = 100; - _mockLoanTerms(simpleLoanTerms, loanFactoryDataHash); - collateralPermit = abi.encodePacked(uint256(1), uint256(2), uint256(3), uint8(4)); vm.expectCall( simpleLoanTerms.collateral.assetAddress, @@ -384,7 +381,13 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { abi.encodeWithSignature("transferFrom(address,address,uint256)", borrower, address(loan), simpleLoanTerms.collateral.amount) ); - loan.createLOAN(loanFactory, loanFactoryData, signature, loanAssetPermit, collateralPermit); + vm.prank(proposalContract); + loan.createLOAN({ + proposalHash: proposalHash, + loanTerms: simpleLoanTerms, + loanAssetPermit: "", + collateralPermit: collateralPermit + }); } function testFuzz_shouldTransferLoanAsset_fromLender_toBorrowerAndFeeCollector( @@ -394,11 +397,9 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { loanAmount = bound(loanAmount, 1, 1e40); simpleLoanTerms.asset.amount = loanAmount; - _mockLoanTerms(simpleLoanTerms, loanFactoryDataHash); fungibleAsset.mint(lender, loanAmount); vm.mockCall(config, abi.encodeWithSignature("fee()"), abi.encode(fee)); - loanAssetPermit = abi.encodePacked(uint256(1), uint256(2), uint256(3), uint8(4)); uint256 feeAmount = Math.mulDiv(loanAmount, fee, 1e4); uint256 newAmount = loanAmount - feeAmount; @@ -422,18 +423,36 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { abi.encodeWithSignature("transferFrom(address,address,uint256)", lender, borrower, newAmount) ); - loan.createLOAN(loanFactory, loanFactoryData, signature, loanAssetPermit, collateralPermit); + vm.prank(proposalContract); + loan.createLOAN({ + proposalHash: proposalHash, + loanTerms: simpleLoanTerms, + loanAssetPermit: loanAssetPermit, + collateralPermit: "" + }); } function test_shouldEmitEvent_LOANCreated() external { vm.expectEmit(); - emit LOANCreated(loanId, simpleLoanTerms, loanFactoryDataHash, loanFactory); - - loan.createLOAN(loanFactory, loanFactoryData, signature, loanAssetPermit, collateralPermit); + emit LOANCreated(loanId, simpleLoanTerms, proposalHash, proposalContract); + + vm.prank(proposalContract); + loan.createLOAN({ + proposalHash: proposalHash, + loanTerms: simpleLoanTerms, + loanAssetPermit: "", + collateralPermit: "" + }); } function test_shouldReturnCreatedLoanId() external { - uint256 createdLoanId = loan.createLOAN(loanFactory, loanFactoryData, signature, loanAssetPermit, collateralPermit); + vm.prank(proposalContract); + uint256 createdLoanId = loan.createLOAN({ + proposalHash: proposalHash, + loanTerms: simpleLoanTerms, + loanAssetPermit: "", + collateralPermit: "" + }); assertEq(createdLoanId, loanId); } @@ -441,46 +460,6 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { } -/*----------------------------------------------------------*| -|* # CREATE LOAN AND REVOKE NONCE *| -|*----------------------------------------------------------*/ - -contract PWNSimpleLoan_CreateLOANAndRevokeNonce_Test is PWNSimpleLoanTest { - - function testFuzz_shouldFail_whenNonceNotUsable(uint256 nonceSpace, uint256 nonce) external { - vm.mockCall( - revokedNonce, - abi.encodeWithSignature("isNonceUsable(address,uint256,uint256)", borrower, nonceSpace, nonce), - abi.encode(false) - ); - - vm.expectRevert(abi.encodeWithSelector(NonceNotUsable.selector)); - vm.prank(borrower); - loan.createLOANAndRevokeNonce(loanFactory, loanFactoryData, "", "", "", nonceSpace, nonce); - } - - function testFuzz_shouldRevokeCallersNonce(address caller, uint256 nonceSpace, uint256 nonce) external { - assumeAddressIsNot(caller, AddressType.ZeroAddress); - - vm.expectCall( - revokedNonce, - abi.encodeWithSignature("revokeNonce(address,uint256,uint256)", caller, nonceSpace, nonce) - ); - - vm.prank(caller); - loan.createLOANAndRevokeNonce(loanFactory, loanFactoryData, "", "", "", nonceSpace, nonce); - } - - function test_shouldCreateLoan() external { - uint256 _loanId = loan.createLOANAndRevokeNonce(loanFactory, loanFactoryData, "", "", "", 0, 1); - - assertEq(_loanId, loanId); - _assertLOANEq(_loanId, simpleLoan); - } - -} - - /*----------------------------------------------------------*| |* # REFINANCE LOAN *| |*----------------------------------------------------------*/ @@ -488,7 +467,7 @@ contract PWNSimpleLoan_CreateLOANAndRevokeNonce_Test is PWNSimpleLoanTest { contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { PWNSimpleLoan.LOAN refinancedLoan; - PWNLOANTerms.Simple refinancedLoanTerms; + PWNSimpleLoan.Terms refinancedLoanTerms; uint256 ferinancedLoanId = 44; address newLender = makeAddr("newLender"); @@ -512,36 +491,51 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { collateral: MultiToken.ERC721(address(nonFungibleAsset), 2) }); - refinancedLoanTerms = PWNLOANTerms.Simple({ + refinancedLoanTerms = PWNSimpleLoan.Terms({ lender: lender, borrower: borrower, - defaultTimestamp: uint40(block.timestamp + 40039), + duration: 40039, collateral: MultiToken.ERC721(address(nonFungibleAsset), 2), asset: MultiToken.ERC20(address(fungibleAsset), 100), fixedInterestAmount: 6631, - accruingInterestAPR: 0, - canCreate: false, - canRefinance: true, - refinancingLoanId: 0 + accruingInterestAPR: 0 }); - loanAssetPermit = abi.encodePacked(uint256(1), uint256(2), uint256(3), uint8(4)); - _mockLOAN(loanId, simpleLoan); _mockLOANMint(ferinancedLoanId); - _mockLoanTerms(refinancedLoanTerms, loanFactoryDataHash); 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, + lenderLoanAssetPermit: "", + borrowerLoanAssetPermit: "" + }); + } + function test_shouldFail_whenLoanDoesNotExist() external { simpleLoan.status = 0; _mockLOAN(loanId, simpleLoan); vm.expectRevert(abi.encodeWithSelector(NonExistingLoan.selector)); - loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); + vm.prank(proposalContract); + loan.refinanceLOAN({ + loanId: loanId, + proposalHash: proposalHash, + loanTerms: refinancedLoanTerms, + lenderLoanAssetPermit: "", + borrowerLoanAssetPermit: "" + }); } function test_shouldFail_whenLoanIsNotRunning() external { @@ -549,143 +543,184 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { _mockLOAN(loanId, simpleLoan); vm.expectRevert(abi.encodeWithSelector(InvalidLoanStatus.selector, 3)); - loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); + vm.prank(proposalContract); + loan.refinanceLOAN({ + loanId: loanId, + proposalHash: proposalHash, + loanTerms: refinancedLoanTerms, + lenderLoanAssetPermit: "", + borrowerLoanAssetPermit: "" + }); } function test_shouldFail_whenLoanIsDefaulted() external { vm.warp(simpleLoan.defaultTimestamp); vm.expectRevert(abi.encodeWithSelector(LoanDefaulted.selector, simpleLoan.defaultTimestamp)); - loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); - } - - function test_shouldFail_whenLoanFactoryContractIsNotTaggerInPWNHub() external { - address notLoanFactory = makeAddr("notLoanFactory"); - - vm.expectRevert(abi.encodeWithSelector(CallerMissingHubTag.selector, PWNHubTags.SIMPLE_LOAN_TERMS_FACTORY)); - loan.refinanceLOAN(loanId, notLoanFactory, loanFactoryData, signature, "", ""); - } - - function test_shouldGetLOANTermsStructFromGivenFactoryContract() external { - loanFactoryData = abi.encode(1, 2, "data"); - signature = abi.encode("other data", "whaat?", uint256(312312)); - - vm.expectCall( - address(loanFactory), - abi.encodeWithSignature("createLOANTerms(address,bytes,bytes)", address(this), loanFactoryData, signature) - ); - - loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); + vm.prank(proposalContract); + loan.refinanceLOAN({ + loanId: loanId, + proposalHash: proposalHash, + loanTerms: refinancedLoanTerms, + lenderLoanAssetPermit: "", + borrowerLoanAssetPermit: "" + }); } - function testFuzz_shouldFail_whenLoanAssetMismatch(address _assetAddress) external { + function testFuzz_shouldFail_whenCreditAssetMismatch(address _assetAddress) external { vm.assume(_assetAddress != simpleLoan.loanAssetAddress); - refinancedLoanTerms.asset.assetAddress = _assetAddress; - _mockLoanTerms(refinancedLoanTerms, loanFactoryDataHash); - vm.expectRevert(abi.encodeWithSelector(InvalidLoanAsset.selector)); - loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); + vm.expectRevert(abi.encodeWithSelector(RefinanceCreditMismatch.selector)); + vm.prank(proposalContract); + loan.refinanceLOAN({ + loanId: loanId, + proposalHash: proposalHash, + loanTerms: refinancedLoanTerms, + lenderLoanAssetPermit: "", + borrowerLoanAssetPermit: "" + }); } - function test_shouldFail_whenLoanAssetAmountZero() external { + function test_shouldFail_whenCreditAssetAmountZero() external { refinancedLoanTerms.asset.amount = 0; - _mockLoanTerms(refinancedLoanTerms, loanFactoryDataHash); - vm.expectRevert(abi.encodeWithSelector(InvalidLoanAsset.selector)); - loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); + vm.expectRevert(abi.encodeWithSelector(RefinanceCreditMismatch.selector)); + vm.prank(proposalContract); + loan.refinanceLOAN({ + loanId: loanId, + proposalHash: proposalHash, + loanTerms: refinancedLoanTerms, + lenderLoanAssetPermit: "", + borrowerLoanAssetPermit: "" + }); } function testFuzz_shouldFail_whenCollateralCategoryMismatch(uint8 _category) external { _category = _category % 4; vm.assume(_category != uint8(simpleLoan.collateral.category)); - refinancedLoanTerms.collateral.category = MultiToken.Category(_category); - _mockLoanTerms(refinancedLoanTerms, loanFactoryDataHash); - vm.expectRevert(abi.encodeWithSelector(InvalidCollateralAsset.selector)); - loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); + vm.expectRevert(abi.encodeWithSelector(RefinanceCollateralMismatch.selector)); + vm.prank(proposalContract); + loan.refinanceLOAN({ + loanId: loanId, + proposalHash: proposalHash, + loanTerms: refinancedLoanTerms, + lenderLoanAssetPermit: "", + borrowerLoanAssetPermit: "" + }); } function testFuzz_shouldFail_whenCollateralAddressMismatch(address _assetAddress) external { vm.assume(_assetAddress != simpleLoan.collateral.assetAddress); - refinancedLoanTerms.collateral.assetAddress = _assetAddress; - _mockLoanTerms(refinancedLoanTerms, loanFactoryDataHash); - vm.expectRevert(abi.encodeWithSelector(InvalidCollateralAsset.selector)); - loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); + vm.expectRevert(abi.encodeWithSelector(RefinanceCollateralMismatch.selector)); + vm.prank(proposalContract); + loan.refinanceLOAN({ + loanId: loanId, + proposalHash: proposalHash, + loanTerms: refinancedLoanTerms, + lenderLoanAssetPermit: "", + borrowerLoanAssetPermit: "" + }); } function testFuzz_shouldFail_whenCollateralIdMismatch(uint256 _id) external { vm.assume(_id != simpleLoan.collateral.id); - refinancedLoanTerms.collateral.id = _id; - _mockLoanTerms(refinancedLoanTerms, loanFactoryDataHash); - vm.expectRevert(abi.encodeWithSelector(InvalidCollateralAsset.selector)); - loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); + vm.expectRevert(abi.encodeWithSelector(RefinanceCollateralMismatch.selector)); + vm.prank(proposalContract); + loan.refinanceLOAN({ + loanId: loanId, + proposalHash: proposalHash, + loanTerms: refinancedLoanTerms, + lenderLoanAssetPermit: "", + borrowerLoanAssetPermit: "" + }); } function testFuzz_shouldFail_whenCollateralAmountMismatch(uint256 _amount) external { vm.assume(_amount != simpleLoan.collateral.amount); - refinancedLoanTerms.collateral.amount = _amount; - _mockLoanTerms(refinancedLoanTerms, loanFactoryDataHash); - vm.expectRevert(abi.encodeWithSelector(InvalidCollateralAsset.selector)); - loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); + vm.expectRevert(abi.encodeWithSelector(RefinanceCollateralMismatch.selector)); + vm.prank(proposalContract); + loan.refinanceLOAN({ + loanId: loanId, + proposalHash: proposalHash, + loanTerms: refinancedLoanTerms, + lenderLoanAssetPermit: "", + borrowerLoanAssetPermit: "" + }); } function testFuzz_shouldFail_whenBorrowerMismatch(address _borrower) external { vm.assume(_borrower != simpleLoan.borrower); - refinancedLoanTerms.borrower = _borrower; - _mockLoanTerms(refinancedLoanTerms, loanFactoryDataHash); - - vm.expectRevert(abi.encodeWithSelector(BorrowerMismatch.selector, simpleLoan.borrower, _borrower)); - loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); - } - function test_shouldFail_whenInvalidRefinanceTerms() external { - refinancedLoanTerms.canRefinance = false; - _mockLoanTerms(refinancedLoanTerms, loanFactoryDataHash); - - vm.expectRevert(abi.encodeWithSelector(InvalidRefinanceTerms.selector)); - loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); - } - - function testFuzz_shouldFail_whenInvalidRefinancingLoanId(uint256 _loanId) external { - vm.assume(_loanId != loanId && _loanId != 0); - - refinancedLoanTerms.refinancingLoanId = _loanId; - _mockLoanTerms(refinancedLoanTerms, loanFactoryDataHash); - - vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, _loanId)); - loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); + vm.expectRevert(abi.encodeWithSelector(RefinanceBorrowerMismatch.selector, simpleLoan.borrower, _borrower)); + vm.prank(proposalContract); + loan.refinanceLOAN({ + loanId: loanId, + proposalHash: proposalHash, + loanTerms: refinancedLoanTerms, + lenderLoanAssetPermit: "", + borrowerLoanAssetPermit: "" + }); } function test_shouldMintLOANToken() external { vm.expectCall(address(loanToken), abi.encodeWithSignature("mint(address)")); - loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); + vm.prank(proposalContract); + loan.refinanceLOAN({ + loanId: loanId, + proposalHash: proposalHash, + loanTerms: refinancedLoanTerms, + lenderLoanAssetPermit: "", + borrowerLoanAssetPermit: "" + }); } function test_shouldStoreRefinancedLoanData() external { - loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); + vm.prank(proposalContract); + loan.refinanceLOAN({ + loanId: loanId, + proposalHash: proposalHash, + loanTerms: refinancedLoanTerms, + lenderLoanAssetPermit: "", + borrowerLoanAssetPermit: "" + }); _assertLOANEq(ferinancedLoanId, refinancedLoan); } function test_shouldEmit_LOANCreated() external { vm.expectEmit(); - emit LOANCreated(ferinancedLoanId, refinancedLoanTerms, loanFactoryDataHash, loanFactory); + emit LOANCreated(ferinancedLoanId, refinancedLoanTerms, proposalHash, proposalContract); - loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); + vm.prank(proposalContract); + loan.refinanceLOAN({ + loanId: loanId, + proposalHash: proposalHash, + loanTerms: refinancedLoanTerms, + lenderLoanAssetPermit: "", + borrowerLoanAssetPermit: "" + }); } function test_shouldReturnNewLoanId() external { - uint256 newLoanId = loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); + vm.prank(proposalContract); + uint256 newLoanId = loan.refinanceLOAN({ + loanId: loanId, + proposalHash: proposalHash, + loanTerms: refinancedLoanTerms, + lenderLoanAssetPermit: "", + borrowerLoanAssetPermit: "" + }); assertEq(newLoanId, ferinancedLoanId); } @@ -694,18 +729,39 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { vm.expectEmit(); emit LOANPaidBack(loanId); - loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); + vm.prank(proposalContract); + loan.refinanceLOAN({ + loanId: loanId, + proposalHash: proposalHash, + loanTerms: refinancedLoanTerms, + lenderLoanAssetPermit: "", + borrowerLoanAssetPermit: "" + }); } function test_shouldEmit_LOANRefinanced() external { vm.expectEmit(); emit LOANRefinanced(loanId, ferinancedLoanId); - loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); + vm.prank(proposalContract); + loan.refinanceLOAN({ + loanId: loanId, + proposalHash: proposalHash, + loanTerms: refinancedLoanTerms, + lenderLoanAssetPermit: "", + borrowerLoanAssetPermit: "" + }); } function test_shouldDeleteOldLoanData_whenLOANOwnerIsOriginalLender() external { - loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); + vm.prank(proposalContract); + loan.refinanceLOAN({ + loanId: loanId, + proposalHash: proposalHash, + loanTerms: refinancedLoanTerms, + lenderLoanAssetPermit: "", + borrowerLoanAssetPermit: "" + }); _assertLOANEq(loanId, nonExistingLoan); } @@ -714,7 +770,14 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { vm.expectEmit(); emit LOANClaimed(loanId, false); - loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); + vm.prank(proposalContract); + loan.refinanceLOAN({ + loanId: loanId, + proposalHash: proposalHash, + loanTerms: refinancedLoanTerms, + lenderLoanAssetPermit: "", + borrowerLoanAssetPermit: "" + }); } function testFuzz_shouldUpdateLoanData_whenLOANOwnerIsNotOriginalLender( @@ -737,7 +800,14 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { uint256 loanRepaymentAmount = loan.loanRepaymentAmount(loanId); fungibleAsset.mint(borrower, loanRepaymentAmount); - loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); + vm.prank(proposalContract); + loan.refinanceLOAN({ + loanId: loanId, + proposalHash: proposalHash, + loanTerms: refinancedLoanTerms, + lenderLoanAssetPermit: "", + borrowerLoanAssetPermit: "" + }); // Update loan and compare simpleLoan.status = 3; // move loan to repaid state @@ -762,7 +832,6 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { refinancedLoanTerms.asset.amount = refinanceAmount; refinancedLoanTerms.lender = newLender; - _mockLoanTerms(refinancedLoanTerms, loanFactoryDataHash); _mockLOANTokenOwner(loanId, simpleLoan.originalLender); vm.mockCall(config, abi.encodeWithSignature("fee()"), abi.encode(fee)); @@ -800,7 +869,14 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { count: borrowerSurplus > 0 ? 1 : 0 }); - loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, loanAssetPermit, ""); + vm.prank(proposalContract); + loan.refinanceLOAN({ + loanId: loanId, + proposalHash: proposalHash, + loanTerms: refinancedLoanTerms, + lenderLoanAssetPermit: loanAssetPermit, + borrowerLoanAssetPermit: "" + }); } function testFuzz_shouldTransferOriginalLoanRepaymentToVault_andTransferSurplusToBorrower_whenLOANOwnerIsNotOriginalLender_whenRefinanceLoanMoreThanOrEqualToOriginalLoan( @@ -819,7 +895,6 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { refinancedLoanTerms.asset.amount = refinanceAmount; refinancedLoanTerms.lender = newLender; - _mockLoanTerms(refinancedLoanTerms, loanFactoryDataHash); _mockLOANTokenOwner(loanId, makeAddr("notOriginalLender")); vm.mockCall(config, abi.encodeWithSignature("fee()"), abi.encode(fee)); @@ -857,7 +932,14 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { count: borrowerSurplus > 0 ? 1 : 0 }); - loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, loanAssetPermit, ""); + vm.prank(proposalContract); + loan.refinanceLOAN({ + loanId: loanId, + proposalHash: proposalHash, + loanTerms: refinancedLoanTerms, + lenderLoanAssetPermit: loanAssetPermit, + borrowerLoanAssetPermit: "" + }); } function testFuzz_shouldNotTransferOriginalLoanRepayment_andTransferSurplusToBorrower_whenLOANOwnerIsNewLender_whenRefinanceLoanMoreThanOrEqualOriginalLoan( @@ -876,7 +958,6 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { refinancedLoanTerms.asset.amount = refinanceAmount; refinancedLoanTerms.lender = newLender; - _mockLoanTerms(refinancedLoanTerms, loanFactoryDataHash); _mockLOANTokenOwner(loanId, newLender); vm.mockCall(config, abi.encodeWithSignature("fee()"), abi.encode(fee)); @@ -916,7 +997,14 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { count: borrowerSurplus > 0 ? 1 : 0 }); - loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, loanAssetPermit, ""); + vm.prank(proposalContract); + loan.refinanceLOAN({ + loanId: loanId, + proposalHash: proposalHash, + loanTerms: refinancedLoanTerms, + lenderLoanAssetPermit: loanAssetPermit, + borrowerLoanAssetPermit: "" + }); } function testFuzz_shouldTransferOriginalLoanRepaymentDirectly_andContributeFromBorrower_whenLOANOwnerIsOriginalLender_whenRefinanceLoanLessThanOriginalLoan( @@ -931,7 +1019,6 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { refinancedLoanTerms.asset.amount = refinanceAmount; refinancedLoanTerms.lender = newLender; - _mockLoanTerms(refinancedLoanTerms, loanFactoryDataHash); _mockLOANTokenOwner(loanId, simpleLoan.originalLender); vm.mockCall(config, abi.encodeWithSignature("fee()"), abi.encode(fee)); @@ -976,7 +1063,14 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { count: borrowerContribution > 0 ? 1 : 0 }); - loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, loanAssetPermit, loanAssetPermit); + vm.prank(proposalContract); + loan.refinanceLOAN({ + loanId: loanId, + proposalHash: proposalHash, + loanTerms: refinancedLoanTerms, + lenderLoanAssetPermit: loanAssetPermit, + borrowerLoanAssetPermit: loanAssetPermit + }); } function testFuzz_shouldTransferOriginalLoanRepaymentToVault_andContributeFromBorrower_whenLOANOwnerIsNotOriginalLender_whenRefinanceLoanLessThanOriginalLoan( @@ -991,7 +1085,6 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { refinancedLoanTerms.asset.amount = refinanceAmount; refinancedLoanTerms.lender = newLender; - _mockLoanTerms(refinancedLoanTerms, loanFactoryDataHash); _mockLOANTokenOwner(loanId, makeAddr("notOriginalLender")); vm.mockCall(config, abi.encodeWithSignature("fee()"), abi.encode(fee)); @@ -1036,7 +1129,14 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { count: borrowerContribution > 0 ? 1 : 0 }); - loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, loanAssetPermit, loanAssetPermit); + vm.prank(proposalContract); + loan.refinanceLOAN({ + loanId: loanId, + proposalHash: proposalHash, + loanTerms: refinancedLoanTerms, + lenderLoanAssetPermit: loanAssetPermit, + borrowerLoanAssetPermit: loanAssetPermit + }); } function testFuzz_shouldNotTransferOriginalLoanRepayment_andContributeFromBorrower_whenLOANOwnerIsNewLender_whenRefinanceLoanLessThanOriginalLoan( @@ -1051,7 +1151,6 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { refinancedLoanTerms.asset.amount = refinanceAmount; refinancedLoanTerms.lender = newLender; - _mockLoanTerms(refinancedLoanTerms, loanFactoryDataHash); _mockLOANTokenOwner(loanId, newLender); vm.mockCall(config, abi.encodeWithSignature("fee()"), abi.encode(fee)); @@ -1098,7 +1197,14 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { count: borrowerContribution > 0 ? 1 : 0 }); - loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, loanAssetPermit, loanAssetPermit); + vm.prank(proposalContract); + loan.refinanceLOAN({ + loanId: loanId, + proposalHash: proposalHash, + loanTerms: refinancedLoanTerms, + lenderLoanAssetPermit: loanAssetPermit, + borrowerLoanAssetPermit: loanAssetPermit + }); } function testFuzz_shouldRepayOriginalLoan( @@ -1123,7 +1229,6 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { refinancedLoanTerms.asset.amount = refinanceAmount; refinancedLoanTerms.lender = newLender; - _mockLoanTerms(refinancedLoanTerms, loanFactoryDataHash); _mockLOANTokenOwner(loanId, lender); fungibleAsset.mint(newLender, refinanceAmount); @@ -1133,7 +1238,14 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { uint256 originalBalance = fungibleAsset.balanceOf(lender); - loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); + vm.prank(proposalContract); + loan.refinanceLOAN({ + loanId: loanId, + proposalHash: proposalHash, + loanTerms: refinancedLoanTerms, + lenderLoanAssetPermit: "", + borrowerLoanAssetPermit: "" + }); assertEq(fungibleAsset.balanceOf(lender), originalBalance + loanRepaymentAmount); } @@ -1162,7 +1274,6 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { refinancedLoanTerms.asset.amount = refinanceAmount; refinancedLoanTerms.lender = newLender; - _mockLoanTerms(refinancedLoanTerms, loanFactoryDataHash); _mockLOANTokenOwner(loanId, lender); vm.mockCall(config, abi.encodeWithSignature("fee()"), abi.encode(fee)); @@ -1173,7 +1284,14 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { uint256 originalBalance = fungibleAsset.balanceOf(feeCollector); - loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); + vm.prank(proposalContract); + loan.refinanceLOAN({ + loanId: loanId, + proposalHash: proposalHash, + loanTerms: refinancedLoanTerms, + lenderLoanAssetPermit: "", + borrowerLoanAssetPermit: "" + }); assertEq(fungibleAsset.balanceOf(feeCollector), originalBalance + feeAmount); } @@ -1187,13 +1305,19 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { refinancedLoanTerms.asset.amount = refinanceAmount; refinancedLoanTerms.lender = newLender; - _mockLoanTerms(refinancedLoanTerms, loanFactoryDataHash); _mockLOANTokenOwner(loanId, lender); fungibleAsset.mint(newLender, refinanceAmount); uint256 originalBalance = fungibleAsset.balanceOf(borrower); - loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); + vm.prank(proposalContract); + loan.refinanceLOAN({ + loanId: loanId, + proposalHash: proposalHash, + loanTerms: refinancedLoanTerms, + lenderLoanAssetPermit: "", + borrowerLoanAssetPermit: "" + }); assertEq(fungibleAsset.balanceOf(borrower), originalBalance + surplus); } @@ -1205,13 +1329,19 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { refinancedLoanTerms.asset.amount = refinanceAmount; refinancedLoanTerms.lender = newLender; - _mockLoanTerms(refinancedLoanTerms, loanFactoryDataHash); _mockLOANTokenOwner(loanId, lender); fungibleAsset.mint(newLender, refinanceAmount); uint256 originalBalance = fungibleAsset.balanceOf(borrower); - loan.refinanceLOAN(loanId, loanFactory, loanFactoryData, signature, "", ""); + vm.prank(proposalContract); + loan.refinanceLOAN({ + loanId: loanId, + proposalHash: proposalHash, + loanTerms: refinancedLoanTerms, + lenderLoanAssetPermit: "", + borrowerLoanAssetPermit: "" + }); assertEq(fungibleAsset.balanceOf(borrower), originalBalance - contribution); } @@ -1707,11 +1837,9 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { pk = boundPrivateKey(pk); vm.assume(pk != borrowerPk); - signature = _signExtension(pk, extension); - - vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector)); + vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, extension.proposer, _extensionHash(extension))); vm.prank(lender); - loan.extendLOAN(extension, signature, ""); + loan.extendLOAN(extension, _signExtension(pk, extension), ""); } function testFuzz_shouldFail_whenOfferExpirated(uint40 expiration) external { @@ -1721,7 +1849,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { extension.expiration = uint40(bound(expiration, 0, timestamp)); _mockExtensionOfferMade(extension); - vm.expectRevert(abi.encodeWithSelector(OfferExpired.selector)); + vm.expectRevert(abi.encodeWithSelector(Expired.selector, block.timestamp, extension.expiration)); vm.prank(lender); loan.extendLOAN(extension, "", ""); } @@ -1735,7 +1863,9 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { abi.encode(false) ); - vm.expectRevert(abi.encodeWithSelector(NonceNotUsable.selector)); + vm.expectRevert(abi.encodeWithSelector( + NonceNotUsable.selector, extension.proposer, extension.nonceSpace, extension.nonce + )); vm.prank(lender); loan.extendLOAN(extension, "", ""); } @@ -1887,18 +2017,16 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { function test_shouldPass_whenBorrowerSignature_whenLenderAccepts() external { extension.proposer = borrower; - signature = _signExtension(borrowerPk, extension); vm.prank(lender); - loan.extendLOAN(extension, signature, ""); + loan.extendLOAN(extension, _signExtension(borrowerPk, extension), ""); } function test_shouldPass_whenLenderSignature_whenBorrowerAccepts() external { extension.proposer = lender; - signature = _signExtension(lenderPk, extension); vm.prank(borrower); - loan.extendLOAN(extension, signature, ""); + loan.extendLOAN(extension, _signExtension(lenderPk, extension), ""); } } diff --git a/test/unit/PWNSimpleLoanListOffer.t.sol b/test/unit/PWNSimpleLoanListOffer.t.sol index b2bf46a..88cd548 100644 --- a/test/unit/PWNSimpleLoanListOffer.t.sol +++ b/test/unit/PWNSimpleLoanListOffer.t.sol @@ -3,38 +3,42 @@ pragma solidity 0.8.16; import "forge-std/Test.sol"; -import "MultiToken/MultiToken.sol"; +import { MultiToken } from "MultiToken/MultiToken.sol"; -import "@pwn/hub/PWNHubTags.sol"; -import "@pwn/loan/terms/simple/factory/offer/PWNSimpleLoanListOffer.sol"; -import "@pwn/loan/terms/PWNLOANTerms.sol"; +import { PWNHubTags } from "@pwn/hub/PWNHubTags.sol"; +import { PWNSimpleLoanListOffer, PWNSimpleLoan } + from "@pwn/loan/terms/simple/proposal/offer/PWNSimpleLoanListOffer.sol"; import "@pwn/PWNErrors.sol"; abstract contract PWNSimpleLoanListOfferTest is Test { - bytes32 internal constant OFFERS_MADE_SLOT = bytes32(uint256(0)); // `offersMade` mapping position - bytes32 internal constant CREDIT_USED_SLOT = bytes32(uint256(1)); // `_creditUsed` mapping position + 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 = address(0x80b); - address revokedOfferNonce = address(0x80c); + address hub = makeAddr("hub"); + address revokedNonce = makeAddr("revokedNonce"); address stateFingerprintComputerRegistry = makeAddr("stateFingerprintComputerRegistry"); - address activeLoanContract = address(0x80d); + address activeLoanContract = makeAddr("activeLoanContract"); PWNSimpleLoanListOffer.Offer offer; PWNSimpleLoanListOffer.OfferValues offerValues; - address token = address(0x070ce2); - uint256 lenderPK = uint256(73661723); + 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; - event OfferMade(bytes32 indexed offerHash, address indexed lender, PWNSimpleLoanListOffer.Offer offer); + event ProposalMade(bytes32 indexed proposalHash, address indexed proposer, bytes proposal); function setUp() virtual public { vm.etch(hub, bytes("data")); - vm.etch(revokedOfferNonce, bytes("data")); + vm.etch(revokedNonce, bytes("data")); vm.etch(token, bytes("data")); - offerContract = new PWNSimpleLoanListOffer(hub, revokedOfferNonce, stateFingerprintComputerRegistry); + offerContract = new PWNSimpleLoanListOffer(hub, revokedNonce, stateFingerprintComputerRegistry); offer = PWNSimpleLoanListOffer.Offer({ collateralCategory: MultiToken.Category.ERC721, @@ -52,8 +56,10 @@ abstract contract PWNSimpleLoanListOfferTest is Test { expiration: 60303, allowedBorrower: address(0), lender: lender, + refinancingLoanId: 0, nonceSpace: 1, - nonce: uint256(keccak256("nonce_1")) + nonce: uint256(keccak256("nonce_1")), + loanContract: activeLoanContract }); offerValues = PWNSimpleLoanListOffer.OfferValues({ @@ -62,10 +68,35 @@ abstract contract PWNSimpleLoanListOfferTest is Test { }); vm.mockCall( - revokedOfferNonce, + 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) + ); } @@ -80,12 +111,54 @@ abstract contract PWNSimpleLoanListOfferTest is Test { address(offerContract) )), keccak256(abi.encodePacked( - keccak256("Offer(uint8 collateralCategory,address collateralAddress,bytes32 collateralIdsWhitelistMerkleRoot,uint256 collateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address loanAssetAddress,uint256 loanAmount,uint256 availableCreditLimit,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedBorrower,address lender,uint256 nonceSpace,uint256 nonce)"), + keccak256("Offer(uint8 collateralCategory,address collateralAddress,bytes32 collateralIdsWhitelistMerkleRoot,uint256 collateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address loanAssetAddress,uint256 loanAmount,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)); + } + } @@ -98,13 +171,14 @@ contract PWNSimpleLoanListOffer_MakeOffer_Test is PWNSimpleLoanListOfferTest { function testFuzz_shouldFail_whenCallerIsNotLender(address caller) external { vm.assume(caller != offer.lender); - vm.expectRevert(abi.encodeWithSelector(CallerIsNotStatedLender.selector, lender)); + vm.expectRevert(abi.encodeWithSelector(CallerIsNotStatedProposer.selector, lender)); + vm.prank(caller); offerContract.makeOffer(offer); } - function test_shouldEmit_OfferMade() external { + function test_shouldEmit_ProposalMade() external { vm.expectEmit(); - emit OfferMade(_offerHash(offer), offer.lender, offer); + emit ProposalMade(_offerHash(offer), offer.lender, abi.encode(offer)); vm.prank(offer.lender); offerContract.makeOffer(offer); @@ -114,165 +188,163 @@ contract PWNSimpleLoanListOffer_MakeOffer_Test is PWNSimpleLoanListOfferTest { vm.prank(offer.lender); offerContract.makeOffer(offer); - assertTrue(offerContract.offersMade(_offerHash(offer))); + assertTrue(offerContract.proposalsMade(_offerHash(offer))); } } /*----------------------------------------------------------*| -|* # AVAILABLE CREDIT *| +|* # REVOKE NONCE *| |*----------------------------------------------------------*/ -contract PWNSimpleLoanListOffer_AvailableCredit_Test is PWNSimpleLoanListOfferTest { - - function testFuzz_shouldReturnAvailableCredit(uint256 used, uint256 limit) external { - limit = bound(limit, used, type(uint256).max); - offer.availableCreditLimit = limit; +contract PWNSimpleLoanListOffer_RevokeNonce_Test is PWNSimpleLoanListOfferTest { - vm.store(address(offerContract), keccak256(abi.encode(_offerHash(offer), CREDIT_USED_SLOT)), bytes32(used)); + function testFuzz_shouldCallRevokeNonce(address caller, uint256 nonceSpace, uint256 nonce) external { + vm.expectCall( + revokedNonce, + abi.encodeWithSignature("revokeNonce(address,uint256,uint256)", caller, nonceSpace, nonce) + ); - assertEq(offerContract.availableCredit(offer), limit - used); + vm.prank(caller); + offerContract.revokeNonce(nonceSpace, nonce); } } /*----------------------------------------------------------*| -|* # CREDIT USED *| +|* # ACCEPT OFFER *| |*----------------------------------------------------------*/ -contract PWNSimpleLoanListOffer_CreditUsed_Test is PWNSimpleLoanListOfferTest { +contract PWNSimpleLoanListOffer_AcceptOffer_Test is PWNSimpleLoanListOfferTest { - function testFuzz_shouldReturnUsedCredit(uint256 used) external { - vm.store(address(offerContract), keccak256(abi.encode(_offerHash(offer), CREDIT_USED_SLOT)), bytes32(used)); + function testFuzz_shouldFail_whenRefinancingLoanIdNotZero(uint256 refinancingLoanId) external { + vm.assume(refinancingLoanId != 0); + offer.refinancingLoanId = refinancingLoanId; - assertEq(offerContract.creditUsed(offer), used); + vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, refinancingLoanId)); + offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", ""); } -} + 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), "", ""); + } -/*----------------------------------------------------------*| -|* # REVOKE OFFER NONCE *| -|*----------------------------------------------------------*/ + function test_shouldAcceptAnyCollateralId_whenMerkleRootIsZero() external { + offerValues.collateralId = 331; + offer.collateralIdsWhitelistMerkleRoot = bytes32(0); -contract PWNSimpleLoanListOffer_RevokeOfferNonce_Test is PWNSimpleLoanListOfferTest { + offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", ""); + } - function testFuzz_shouldCallRevokeOfferNonce(uint256 nonceSpace, uint256 nonce) external { - vm.expectCall( - revokedOfferNonce, - abi.encodeWithSignature("revokeNonce(address,uint256,uint256)", lender, nonceSpace, nonce) - ); + 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)); - vm.prank(lender); - offerContract.revokeOfferNonce(nonceSpace, nonce); - } + offerValues.collateralId = 331; + offerValues.merkleInclusionProof = new bytes32[](1); + offerValues.merkleInclusionProof[0] = id2Hash; -} + offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", ""); + } + 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)); -/*----------------------------------------------------------*| -|* # CREATE LOAN TERMS *| -|*----------------------------------------------------------*/ + offerValues.collateralId = 333; + offerValues.merkleInclusionProof = new bytes32[](1); + offerValues.merkleInclusionProof[0] = id2Hash; -contract PWNSimpleLoanListOffer_CreateLOANTerms_Test is PWNSimpleLoanListOfferTest { + vm.expectRevert(abi.encodeWithSelector(CollateralIdNotWhitelisted.selector, offerValues.collateralId)); + offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", ""); + } - bytes signature; - address borrower = makeAddr("borrower"); - address stateFingerprintComputer = makeAddr("stateFingerprintComputer"); + function test_shouldNotCallComputerRegistry_whenShouldNotCheckStateFingerprint() external { + offer.checkCollateralStateFingerprint = false; - function setUp() override public { - super.setUp(); + vm.expectCall({ + callee: stateFingerprintComputerRegistry, + data: abi.encodeWithSignature("getStateFingerprintComputer(address)", offer.collateralAddress), + count: 0 + }); - 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) - ); + offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", ""); + } + function test_shouldFail_whenComputerRegistryReturnsZeroAddress_whenShouldCheckStateFingerprint() external { vm.mockCall( stateFingerprintComputerRegistry, abi.encodeWithSignature("getStateFingerprintComputer(address)", offer.collateralAddress), - abi.encode(stateFingerprintComputer) - ); - vm.mockCall( - stateFingerprintComputer, - abi.encodeWithSignature("getStateFingerprint(uint256)" /* any collateral id */ ), - abi.encode(offer.collateralStateFingerprint) + abi.encode(address(0)) ); - } - - // Helpers - function _signOffer(uint256 pk, PWNSimpleLoanListOffer.Offer memory _offer) private view returns (bytes memory) { - (uint8 v, bytes32 r, bytes32 s) = vm.sign(pk, _offerHash(_offer)); - return abi.encodePacked(r, s, v); - } + vm.expectCall( + stateFingerprintComputerRegistry, + abi.encodeWithSignature("getStateFingerprintComputer(address)", offer.collateralAddress) + ); - function _signOfferCompact(uint256 pk, PWNSimpleLoanListOffer.Offer memory _offer) private 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); + vm.expectRevert(abi.encodeWithSelector(MissingStateFingerprintComputer.selector)); + offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", ""); } + function testFuzz_shouldFail_whenComputerReturnsDifferentStateFingerprint_whenShouldCheckStateFingerprint( + bytes32 stateFingerprint + ) external { + vm.assume(stateFingerprint != offer.collateralStateFingerprint); - // Tests + vm.mockCall( + stateFingerprintComputer, + abi.encodeWithSignature("getStateFingerprint(uint256)", offerValues.collateralId), + abi.encode(stateFingerprint) + ); - function test_shouldFail_whenCallerIsNotActiveLoan() external { - vm.expectRevert(abi.encodeWithSelector(CallerMissingHubTag.selector, PWNHubTags.ACTIVE_LOAN)); - offerContract.createLOANTerms(borrower, abi.encode(offer, offerValues), signature); - } + vm.expectCall( + stateFingerprintComputer, + abi.encodeWithSignature("getStateFingerprint(uint256)", offerValues.collateralId) + ); - function test_shouldFail_whenPassingInvalidOfferData() external { - vm.expectRevert(); - vm.prank(activeLoanContract); - offerContract.createLOANTerms(borrower, abi.encode(uint16(1), uint256(3213), address(0x01320), false, "whaaaaat?"), signature); + vm.expectRevert(abi.encodeWithSelector( + InvalidCollateralStateFingerprint.selector, stateFingerprint, offer.collateralStateFingerprint + )); + offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", ""); } function test_shouldFail_whenInvalidSignature_whenEOA() external { - signature = _signOffer(1, offer); - - vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector)); - vm.prank(activeLoanContract); - offerContract.createLOANTerms(borrower, abi.encode(offer, offerValues), signature); + vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, offer.lender, _offerHash(offer))); + offerContract.acceptOffer(offer, offerValues, _signOffer(1, offer), "", ""); } function test_shouldFail_whenInvalidSignature_whenContractAccount() external { vm.etch(lender, bytes("data")); - vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector)); - vm.prank(activeLoanContract); - offerContract.createLOANTerms(borrower, abi.encode(offer, offerValues), signature); + vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, offer.lender, _offerHash(offer))); + offerContract.acceptOffer(offer, offerValues, "", "", ""); } function test_shouldPass_whenOfferHasBeenMadeOnchain() external { vm.store( address(offerContract), - keccak256(abi.encode(_offerHash(offer), OFFERS_MADE_SLOT)), + keccak256(abi.encode(_offerHash(offer), PROPOSALS_MADE_SLOT)), bytes32(uint256(1)) ); - vm.prank(activeLoanContract); - offerContract.createLOANTerms(borrower, abi.encode(offer, offerValues), signature); + offerContract.acceptOffer(offer, offerValues, "", "", ""); } function test_shouldPass_withValidSignature_whenEOA_whenStandardSignature() external { - signature = _signOffer(lenderPK, offer); - - vm.prank(activeLoanContract); - offerContract.createLOANTerms(borrower, abi.encode(offer, offerValues), signature); + offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", ""); } function test_shouldPass_withValidSignature_whenEOA_whenCompactEIP2098Signature() external { - signature = _signOfferCompact(lenderPK, offer); - - vm.prank(activeLoanContract); - offerContract.createLOANTerms(borrower, abi.encode(offer, offerValues), signature); + offerContract.acceptOffer(offer, offerValues, _signOfferCompact(lenderPK, offer), "", ""); } function test_shouldPass_whenValidSignature_whenContractAccount() external { @@ -284,82 +356,265 @@ contract PWNSimpleLoanListOffer_CreateLOANTerms_Test is PWNSimpleLoanListOfferTe abi.encode(bytes4(0x1626ba7e)) ); - vm.prank(activeLoanContract); - offerContract.createLOANTerms(borrower, abi.encode(offer, offerValues), signature); - } - - function test_shouldFail_whenOfferIsExpired() external { - vm.warp(40303); - offer.expiration = 30303; - signature = _signOfferCompact(lenderPK, offer); - - vm.expectRevert(abi.encodeWithSelector(OfferExpired.selector)); - vm.prank(activeLoanContract); - offerContract.createLOANTerms(borrower, abi.encode(offer, offerValues), signature); + offerContract.acceptOffer(offer, offerValues, "", "", ""); } - function test_shouldPass_whenOfferIsNotExpired() external { - vm.warp(40303); - offer.expiration = 50303; - signature = _signOfferCompact(lenderPK, offer); + function testFuzz_shouldFail_whenOfferIsExpired(uint256 timestamp) external { + timestamp = bound(timestamp, offer.expiration, type(uint256).max); + vm.warp(timestamp); - vm.prank(activeLoanContract); - offerContract.createLOANTerms(borrower, abi.encode(offer, offerValues), signature); + vm.expectRevert(abi.encodeWithSelector(Expired.selector, timestamp, offer.expiration)); + offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", ""); } function test_shouldFail_whenOfferNonceNotUsable() external { - signature = _signOfferCompact(lenderPK, offer); - vm.mockCall( - revokedOfferNonce, + revokedNonce, abi.encodeWithSignature("isNonceUsable(address,uint256,uint256)"), abi.encode(false) ); vm.expectCall( - revokedOfferNonce, + revokedNonce, abi.encodeWithSignature("isNonceUsable(address,uint256,uint256)", offer.lender, offer.nonceSpace, offer.nonce) ); - vm.expectRevert(abi.encodeWithSelector(NonceNotUsable.selector)); - vm.prank(activeLoanContract); - offerContract.createLOANTerms(borrower, abi.encode(offer, offerValues), signature); + vm.expectRevert(abi.encodeWithSelector( + NonceNotUsable.selector, offer.lender, offer.nonceSpace, offer.nonce + )); + offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", ""); } - function test_shouldFail_whenCallerIsNotAllowedBorrower() external { - offer.allowedBorrower = address(0x50303); - signature = _signOfferCompact(lenderPK, offer); + function testFuzz_shouldFail_whenCallerIsNotAllowedBorrower(address caller) external { + address allowedBorrower = makeAddr("allowedBorrower"); + vm.assume(caller != allowedBorrower); + offer.allowedBorrower = allowedBorrower; - vm.expectRevert(abi.encodeWithSelector(CallerIsNotStatedBorrower.selector, offer.allowedBorrower)); - vm.prank(activeLoanContract); - offerContract.createLOANTerms(borrower, abi.encode(offer, offerValues), signature); + vm.expectRevert(abi.encodeWithSelector(CallerNotAllowedAcceptor.selector, caller, offer.allowedBorrower)); + vm.prank(caller); + offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", ""); } - function testFuzz_shouldFail_whenLessThanMinDuration(uint32 duration) external { + 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), "", ""); + } + + 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); - offer.duration = duration; - signature = _signOfferCompact(lenderPK, offer); + vm.expectRevert(abi.encodeWithSelector(AccruingInterestAPROutOfBounds.selector, interestAPR, maxInterest)); + offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", ""); + } + + function test_shouldRevokeOffer_whenAvailableCreditLimitEqualToZero() external { + offer.availableCreditLimit = 0; + + vm.expectCall( + revokedNonce, + abi.encodeWithSignature( + "revokeNonce(address,uint256,uint256)", offer.lender, offer.nonceSpace, offer.nonce + ) + ); - vm.expectRevert(abi.encodeWithSelector(InvalidDuration.selector)); - vm.prank(activeLoanContract); - offerContract.createLOANTerms(borrower, abi.encode(offer, offerValues), signature); + offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", ""); } - function testFuzz_shouldFail_whenAccruingInterestAPROutOfBounds(uint40 interestAPR) external { - uint40 maxInterest = offerContract.MAX_ACCRUING_INTEREST_APR(); - interestAPR = uint40(bound(interestAPR, maxInterest + 1, type(uint40).max)); + function testFuzz_shouldFail_whenUsedCreditExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { + used = bound(used, 1, type(uint256).max - offer.loanAmount); + limit = bound(limit, used, used + offer.loanAmount - 1); + offer.availableCreditLimit = limit; - offer.accruingInterestAPR = interestAPR; - signature = _signOfferCompact(lenderPK, offer); + vm.store(address(offerContract), keccak256(abi.encode(_offerHash(offer), CREDIT_USED_SLOT)), bytes32(used)); - vm.expectRevert(abi.encodeWithSelector(AccruingInterestAPROutOfBounds.selector, interestAPR, maxInterest)); - vm.prank(activeLoanContract); - offerContract.createLOANTerms(borrower, abi.encode(offer, offerValues), signature); + vm.expectRevert(abi.encodeWithSelector(AvailableCreditLimitExceeded.selector, used + offer.loanAmount, limit)); + offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", ""); + } + + function testFuzz_shouldIncreaseUsedCredit_whenUsedCreditNotExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { + used = bound(used, 1, type(uint256).max - offer.loanAmount); + limit = bound(limit, used + offer.loanAmount, 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), "", ""); + + assertEq(offerContract.creditUsed(_offerHash(offer)), used + offer.loanAmount); + } + + function test_shouldCallLoanContractWithLoanTerms() external { + bytes memory loanAssetPermit = "loanAssetPermit"; + bytes memory collateralPermit = "collateralPermit"; + + 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 + }), + asset: MultiToken.Asset({ + category: MultiToken.Category.ERC20, + assetAddress: offer.loanAssetAddress, + id: 0, + amount: offer.loanAmount + }), + fixedInterestAmount: offer.fixedInterestAmount, + accruingInterestAPR: offer.accruingInterestAPR + }); + + vm.expectCall( + activeLoanContract, + abi.encodeWithSelector( + PWNSimpleLoan.createLOAN.selector, + _offerHash(offer), loanTerms, loanAssetPermit, collateralPermit + ) + ); + + vm.prank(borrower); + offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), loanAssetPermit, collateralPermit); + } + + function test_shouldReturnNewLoanId() external { + assertEq( + offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", ""), + 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), + loanAssetPermit: "", + collateralPermit: "", + 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), + loanAssetPermit: "", + collateralPermit: "", + 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), + loanAssetPermit: "", + collateralPermit: "", + 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), "", ""); + } + + 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), "", ""); + } + + function test_shouldAcceptAnyCollateralId_whenMerkleRootIsZero() external { + offerValues.collateralId = 331; + offer.collateralIdsWhitelistMerkleRoot = bytes32(0); + + offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", ""); + } + + 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), "", ""); + } + + 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), "", ""); } function test_shouldNotCallComputerRegistry_whenShouldNotCheckStateFingerprint() external { offer.checkCollateralStateFingerprint = false; - signature = _signOfferCompact(lenderPK, offer); vm.expectCall({ callee: stateFingerprintComputerRegistry, @@ -367,13 +622,10 @@ contract PWNSimpleLoanListOffer_CreateLOANTerms_Test is PWNSimpleLoanListOfferTe count: 0 }); - vm.prank(activeLoanContract); - offerContract.createLOANTerms(borrower, abi.encode(offer, offerValues), signature); + offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", ""); } function test_shouldFail_whenComputerRegistryReturnsZeroAddress_whenShouldCheckStateFingerprint() external { - signature = _signOfferCompact(lenderPK, offer); - vm.mockCall( stateFingerprintComputerRegistry, abi.encodeWithSignature("getStateFingerprintComputer(address)", offer.collateralAddress), @@ -386,8 +638,7 @@ contract PWNSimpleLoanListOffer_CreateLOANTerms_Test is PWNSimpleLoanListOfferTe ); vm.expectRevert(abi.encodeWithSelector(MissingStateFingerprintComputer.selector)); - vm.prank(activeLoanContract); - offerContract.createLOANTerms(borrower, abi.encode(offer, offerValues), signature); + offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", ""); } function testFuzz_shouldFail_whenComputerReturnsDifferentStateFingerprint_whenShouldCheckStateFingerprint( @@ -395,8 +646,6 @@ contract PWNSimpleLoanListOffer_CreateLOANTerms_Test is PWNSimpleLoanListOfferTe ) external { vm.assume(stateFingerprint != offer.collateralStateFingerprint); - signature = _signOfferCompact(lenderPK, offer); - vm.mockCall( stateFingerprintComputer, abi.encodeWithSignature("getStateFingerprint(uint256)", offerValues.collateralId), @@ -409,143 +658,265 @@ contract PWNSimpleLoanListOffer_CreateLOANTerms_Test is PWNSimpleLoanListOfferTe ); vm.expectRevert(abi.encodeWithSelector( - InvalidCollateralStateFingerprint.selector, offer.collateralStateFingerprint, stateFingerprint + InvalidCollateralStateFingerprint.selector, stateFingerprint, offer.collateralStateFingerprint + )); + offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", ""); + } + + function test_shouldFail_whenInvalidSignature_whenEOA() external { + vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, offer.lender, _offerHash(offer))); + offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(1, offer), "", ""); + } + + 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, "", "", ""); + } + + 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, "", "", ""); + } + + function test_shouldPass_withValidSignature_whenEOA_whenStandardSignature() external { + offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", ""); + } + + function test_shouldPass_withValidSignature_whenEOA_whenCompactEIP2098Signature() external { + offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOfferCompact(lenderPK, offer), "", ""); + } + + 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, "", "", ""); + } + + 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), "", ""); + } + + 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 )); - vm.prank(activeLoanContract); - offerContract.createLOANTerms(borrower, abi.encode(offer, offerValues), signature); + offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", ""); + } + + 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), "", ""); + } + + 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), "", ""); + } + + 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), "", ""); } function test_shouldRevokeOffer_whenAvailableCreditLimitEqualToZero() external { offer.availableCreditLimit = 0; - signature = _signOfferCompact(lenderPK, offer); vm.expectCall( - revokedOfferNonce, - abi.encodeWithSignature("revokeNonce(address,uint256,uint256)", offer.lender, offer.nonceSpace, offer.nonce) + revokedNonce, + abi.encodeWithSignature( + "revokeNonce(address,uint256,uint256)", offer.lender, offer.nonceSpace, offer.nonce + ) ); - vm.prank(activeLoanContract); - offerContract.createLOANTerms(borrower, abi.encode(offer, offerValues), signature); + offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", ""); } function testFuzz_shouldFail_whenUsedCreditExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { used = bound(used, 1, type(uint256).max - offer.loanAmount); limit = bound(limit, used, used + offer.loanAmount - 1); offer.availableCreditLimit = limit; - signature = _signOfferCompact(lenderPK, offer); vm.store(address(offerContract), keccak256(abi.encode(_offerHash(offer), CREDIT_USED_SLOT)), bytes32(used)); vm.expectRevert(abi.encodeWithSelector(AvailableCreditLimitExceeded.selector, used + offer.loanAmount, limit)); - vm.prank(activeLoanContract); - offerContract.createLOANTerms(borrower, abi.encode(offer, offerValues), signature); + offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", ""); } function testFuzz_shouldIncreaseUsedCredit_whenUsedCreditNotExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { used = bound(used, 1, type(uint256).max - offer.loanAmount); limit = bound(limit, used + offer.loanAmount, type(uint256).max); offer.availableCreditLimit = limit; - signature = _signOfferCompact(lenderPK, offer); vm.store(address(offerContract), keccak256(abi.encode(_offerHash(offer), CREDIT_USED_SLOT)), bytes32(used)); - vm.prank(activeLoanContract); - offerContract.createLOANTerms(borrower, abi.encode(offer, offerValues), signature); + offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", ""); - assertEq(offerContract.creditUsed(offer), used + offer.loanAmount); + assertEq(offerContract.creditUsed(_offerHash(offer)), used + offer.loanAmount); } - function test_shouldAcceptAnyCollateralId_whenMerkleRootIsZero() external { - offerValues.collateralId = 331; - offer.collateralIdsWhitelistMerkleRoot = bytes32(0); - signature = _signOfferCompact(lenderPK, offer); + function test_shouldCallLoanContract() external { + bytes memory loanAssetPermit = "loanAssetPermit"; + bytes memory collateralPermit = "collateralPermit"; - vm.prank(activeLoanContract); - offerContract.createLOANTerms(borrower, abi.encode(offer, offerValues), signature); - } - - 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)); - signature = _signOfferCompact(lenderPK, offer); + PWNSimpleLoan.Terms memory loanTerms = PWNSimpleLoan.Terms({ + lender: lender, + borrower: offer.lender, + duration: offer.duration, + collateral: MultiToken.Asset({ + category: offer.collateralCategory, + assetAddress: offer.collateralAddress, + id: offerValues.collateralId, + amount: offer.collateralAmount + }), + asset: MultiToken.Asset({ + category: MultiToken.Category.ERC20, + assetAddress: offer.loanAssetAddress, + id: 0, + amount: offer.loanAmount + }), + fixedInterestAmount: offer.fixedInterestAmount, + accruingInterestAPR: offer.accruingInterestAPR + }); - offerValues.collateralId = 331; - offerValues.merkleInclusionProof = new bytes32[](1); - offerValues.merkleInclusionProof[0] = id2Hash; + vm.expectCall( + activeLoanContract, + abi.encodeWithSelector( + PWNSimpleLoan.refinanceLOAN.selector, + loanId, _offerHash(offer), loanTerms, loanAssetPermit, collateralPermit + ) + ); - vm.prank(activeLoanContract); - offerContract.createLOANTerms(borrower, abi.encode(offer, offerValues), signature); + vm.prank(lender); + offerContract.acceptRefinanceOffer( + loanId, offer, offerValues, _signOffer(lenderPK, offer), loanAssetPermit, collateralPermit + ); } - 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)); - signature = _signOfferCompact(lenderPK, offer); + function test_shouldReturnRefinancedLoanId() external { + assertEq( + offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", ""), + refinancedLoanId + ); + } - offerValues.collateralId = 333; - offerValues.merkleInclusionProof = new bytes32[](1); - offerValues.merkleInclusionProof[0] = id2Hash; +} - vm.expectRevert(abi.encodeWithSelector(CollateralIdIsNotWhitelisted.selector)); - vm.prank(activeLoanContract); - offerContract.createLOANTerms(borrower, abi.encode(offer, offerValues), signature); - } - function test_shouldReturnCorrectValues() external { - uint256 currentTimestamp = 40303; - vm.warp(currentTimestamp); - signature = _signOfferCompact(lenderPK, offer); +/*----------------------------------------------------------*| +|* # ACCEPT REFINANCE OFFER AND REVOKE CALLERS NONCE *| +|*----------------------------------------------------------*/ - vm.prank(activeLoanContract); - (PWNLOANTerms.Simple memory loanTerms, bytes32 offerHash) - = offerContract.createLOANTerms(borrower, abi.encode(offer, offerValues), signature); +contract PWNSimpleLoanListOffer_AcceptRefinanceOfferAndRevokeCallersNonce_Test is PWNSimpleLoanListOfferTest { - assertTrue(loanTerms.lender == offer.lender); - assertTrue(loanTerms.borrower == borrower); - assertTrue(loanTerms.defaultTimestamp == currentTimestamp + offer.duration); - assertTrue(loanTerms.collateral.category == offer.collateralCategory); - assertTrue(loanTerms.collateral.assetAddress == offer.collateralAddress); - assertTrue(loanTerms.collateral.id == offerValues.collateralId); - assertTrue(loanTerms.collateral.amount == offer.collateralAmount); - assertTrue(loanTerms.asset.category == MultiToken.Category.ERC20); - assertTrue(loanTerms.asset.assetAddress == offer.loanAssetAddress); - assertTrue(loanTerms.asset.id == 0); - assertTrue(loanTerms.asset.amount == offer.loanAmount); - assertTrue(loanTerms.fixedInterestAmount == offer.fixedInterestAmount); - assertTrue(loanTerms.accruingInterestAPR == offer.accruingInterestAPR); - assertTrue(loanTerms.canCreate == true); - assertTrue(loanTerms.canRefinance == true); - assertTrue(loanTerms.refinancingLoanId == 0); + 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) + ); - assertTrue(offerHash == _offerHash(offer)); + vm.expectRevert(abi.encodeWithSelector(NonceNotUsable.selector, caller, nonceSpace, nonce)); + vm.prank(caller); + offerContract.acceptRefinanceOffer({ + loanId: loanId, + offer: offer, + offerValues: offerValues, + signature: _signOffer(lenderPK, offer), + lenderLoanAssetPermit: "", + borrowerLoanAssetPermit: "", + 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) + ); -/*----------------------------------------------------------*| -|* # GET OFFER HASH *| -|*----------------------------------------------------------*/ + vm.prank(caller); + offerContract.acceptRefinanceOffer({ + loanId: loanId, + offer: offer, + offerValues: offerValues, + signature: _signOffer(lenderPK, offer), + lenderLoanAssetPermit: "", + borrowerLoanAssetPermit: "", + callersNonceSpace: nonceSpace, + callersNonceToRevoke: nonce + }); + } -contract PWNSimpleLoanListOffer_GetOfferHash_Test is PWNSimpleLoanListOfferTest { + // 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), + lenderLoanAssetPermit: "", + borrowerLoanAssetPermit: "", + callersNonceSpace: 1, + callersNonceToRevoke: 2 + }); - function test_shouldReturnOfferHash() external { - assertEq(_offerHash(offer), offerContract.getOfferHash(offer)); + assertEq(newLoanId, refinancedLoanId); } } /*----------------------------------------------------------*| -|* # LOAN TERMS FACTORY DATA ENCODING *| +|* # DECODE PROPOSAL *| |*----------------------------------------------------------*/ -contract PWNSimpleLoanListOffer_EncodeLoanTermsFactoryData_Test is PWNSimpleLoanListOfferTest { +contract PWNSimpleLoanListOffer_DecodeProposal_Test is PWNSimpleLoanListOfferTest { + + function test_shouldReturnDecodedOfferData() external { + PWNSimpleLoanListOffer.Offer memory decodedOffer = offerContract.decodeProposal(abi.encode(offer)); - function test_shouldReturnEncodedLoanTermsFactoryData() external { - assertEq(abi.encode(offer, offerValues), offerContract.encodeLoanTermsFactoryData(offer, offerValues)); + assertEq(_offerHash(decodedOffer), _offerHash(offer)); } } diff --git a/test/unit/PWNSimpleLoanSimpleOffer.t.sol b/test/unit/PWNSimpleLoanSimpleOffer.t.sol index a08d7dd..9a6a7a6 100644 --- a/test/unit/PWNSimpleLoanSimpleOffer.t.sol +++ b/test/unit/PWNSimpleLoanSimpleOffer.t.sol @@ -3,37 +3,41 @@ pragma solidity 0.8.16; import "forge-std/Test.sol"; -import "MultiToken/MultiToken.sol"; +import { MultiToken } from "MultiToken/MultiToken.sol"; -import "@pwn/hub/PWNHubTags.sol"; -import "@pwn/loan/terms/simple/factory/offer/PWNSimpleLoanSimpleOffer.sol"; -import "@pwn/loan/terms/PWNLOANTerms.sol"; +import { PWNHubTags } from "@pwn/hub/PWNHubTags.sol"; +import { PWNSimpleLoanSimpleOffer, PWNSimpleLoan } + from "@pwn/loan/terms/simple/proposal/offer/PWNSimpleLoanSimpleOffer.sol"; import "@pwn/PWNErrors.sol"; abstract contract PWNSimpleLoanSimpleOfferTest is Test { - bytes32 internal constant OFFERS_MADE_SLOT = bytes32(uint256(0)); // `offersMade` mapping position - bytes32 internal constant CREDIT_USED_SLOT = bytes32(uint256(1)); // `_creditUsed` mapping position + bytes32 internal constant PROPOSALS_MADE_SLOT = bytes32(uint256(0)); // `proposalsMade` mapping position + bytes32 internal constant CREDIT_USED_SLOT = bytes32(uint256(1)); // `creditUsed` mapping position PWNSimpleLoanSimpleOffer offerContract; - address hub = address(0x80b); - address revokedOfferNonce = address(0x80c); + address hub = makeAddr("hub"); + address revokedNonce = makeAddr("revokedNonce"); address stateFingerprintComputerRegistry = makeAddr("stateFingerprintComputerRegistry"); - address activeLoanContract = address(0x80d); + address activeLoanContract = makeAddr("activeLoanContract"); PWNSimpleLoanSimpleOffer.Offer offer; - address token = address(0x070ce2); - uint256 lenderPK = uint256(73661723); + 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; - event OfferMade(bytes32 indexed offerHash, address indexed lender, PWNSimpleLoanSimpleOffer.Offer offer); + event ProposalMade(bytes32 indexed proposalHash, address indexed proposer, bytes proposal); function setUp() virtual public { vm.etch(hub, bytes("data")); - vm.etch(revokedOfferNonce, bytes("data")); + vm.etch(revokedNonce, bytes("data")); vm.etch(token, bytes("data")); - offerContract = new PWNSimpleLoanSimpleOffer(hub, revokedOfferNonce, stateFingerprintComputerRegistry); + offerContract = new PWNSimpleLoanSimpleOffer(hub, revokedNonce, stateFingerprintComputerRegistry); offer = PWNSimpleLoanSimpleOffer.Offer({ collateralCategory: MultiToken.Category.ERC721, @@ -51,15 +55,42 @@ abstract contract PWNSimpleLoanSimpleOfferTest is Test { expiration: 60303, allowedBorrower: address(0), lender: lender, + refinancingLoanId: 0, nonceSpace: 1, - nonce: uint256(keccak256("nonce_1")) + nonce: uint256(keccak256("nonce_1")), + loanContract: activeLoanContract }); vm.mockCall( - revokedOfferNonce, + 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)", offer.collateralId), + 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) + ); } @@ -74,12 +105,54 @@ abstract contract PWNSimpleLoanSimpleOfferTest is Test { address(offerContract) )), keccak256(abi.encodePacked( - keccak256("Offer(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address loanAssetAddress,uint256 loanAmount,uint256 availableCreditLimit,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedBorrower,address lender,uint256 nonceSpace,uint256 nonce)"), + keccak256("Offer(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address loanAssetAddress,uint256 loanAmount,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, PWNSimpleLoanSimpleOffer.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, PWNSimpleLoanSimpleOffer.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 PWNSimpleLoanSimpleOffer_CreditUsed_Test is PWNSimpleLoanSimpleOfferTest { + + 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 PWNSimpleLoanSimpleOffer_GetOfferHash_Test is PWNSimpleLoanSimpleOfferTest { + + function test_shouldReturnOfferHash() external { + assertEq(_offerHash(offer), offerContract.getOfferHash(offer)); + } + } @@ -92,13 +165,14 @@ contract PWNSimpleLoanSimpleOffer_MakeOffer_Test is PWNSimpleLoanSimpleOfferTest function testFuzz_shouldFail_whenCallerIsNotLender(address caller) external { vm.assume(caller != offer.lender); - vm.expectRevert(abi.encodeWithSelector(CallerIsNotStatedLender.selector, 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); + emit ProposalMade(_offerHash(offer), offer.lender, abi.encode(offer)); vm.prank(offer.lender); offerContract.makeOffer(offer); @@ -108,165 +182,131 @@ contract PWNSimpleLoanSimpleOffer_MakeOffer_Test is PWNSimpleLoanSimpleOfferTest vm.prank(offer.lender); offerContract.makeOffer(offer); - assertTrue(offerContract.offersMade(_offerHash(offer))); + assertTrue(offerContract.proposalsMade(_offerHash(offer))); } } /*----------------------------------------------------------*| -|* # AVAILABLE CREDIT *| +|* # REVOKE NONCE *| |*----------------------------------------------------------*/ -contract PWNSimpleLoanSimpleOffer_AvailableCredit_Test is PWNSimpleLoanSimpleOfferTest { +contract PWNSimpleLoanSimpleOffer_RevokeNonce_Test is PWNSimpleLoanSimpleOfferTest { - function testFuzz_shouldReturnAvailableCredit(uint256 used, uint256 limit) external { - limit = bound(limit, used, type(uint256).max); - offer.availableCreditLimit = limit; - - vm.store(address(offerContract), keccak256(abi.encode(_offerHash(offer), CREDIT_USED_SLOT)), bytes32(used)); + function testFuzz_shouldCallRevokeNonce(address caller, uint256 nonceSpace, uint256 nonce) external { + vm.expectCall( + revokedNonce, + abi.encodeWithSignature("revokeNonce(address,uint256,uint256)", caller, nonceSpace, nonce) + ); - assertEq(offerContract.availableCredit(offer), limit - used); + vm.prank(caller); + offerContract.revokeNonce(nonceSpace, nonce); } } /*----------------------------------------------------------*| -|* # CREDIT USED *| +|* # ACCEPT OFFER *| |*----------------------------------------------------------*/ -contract PWNSimpleLoanSimpleOffer_CreditUsed_Test is PWNSimpleLoanSimpleOfferTest { +contract PWNSimpleLoanSimpleOffer_AcceptOffer_Test is PWNSimpleLoanSimpleOfferTest { - function testFuzz_shouldReturnUsedCredit(uint256 used) external { - vm.store(address(offerContract), keccak256(abi.encode(_offerHash(offer), CREDIT_USED_SLOT)), bytes32(used)); + function testFuzz_shouldFail_whenRefinancingLoanIdNotZero(uint256 refinancingLoanId) external { + vm.assume(refinancingLoanId != 0); + offer.refinancingLoanId = refinancingLoanId; - assertEq(offerContract.creditUsed(offer), used); + vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, refinancingLoanId)); + offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", ""); } -} - + function testFuzz_shouldFail_whenLoanContractNotTagged_ACTIVE_LOAN(address loanContract) external { + vm.assume(loanContract != activeLoanContract); + offer.loanContract = loanContract; -/*----------------------------------------------------------*| -|* # REVOKE OFFER NONCE *| -|*----------------------------------------------------------*/ - -contract PWNSimpleLoanSimpleOffer_RevokeOfferNonce_Test is PWNSimpleLoanSimpleOfferTest { - - function testFuzz_shouldCallRevokeOfferNonce(uint256 nonceSpace, uint256 nonce) external { - vm.expectCall( - revokedOfferNonce, - abi.encodeWithSignature("revokeNonce(address,uint256,uint256)", lender, nonceSpace, nonce) - ); - - vm.prank(lender); - offerContract.revokeOfferNonce(nonceSpace, nonce); + vm.expectRevert(abi.encodeWithSelector(AddressMissingHubTag.selector, loanContract, PWNHubTags.ACTIVE_LOAN)); + offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", ""); } -} - - -/*----------------------------------------------------------*| -|* # CREATE LOAN TERMS *| -|*----------------------------------------------------------*/ - -contract PWNSimpleLoanSimpleOffer_CreateLOANTerms_Test is PWNSimpleLoanSimpleOfferTest { - - bytes signature; - address borrower = makeAddr("borrower"); - address stateFingerprintComputer = makeAddr("stateFingerprintComputer"); + function test_shouldNotCallComputerRegistry_whenShouldNotCheckStateFingerprint() external { + offer.checkCollateralStateFingerprint = false; - function setUp() override public { - super.setUp(); + vm.expectCall({ + callee: stateFingerprintComputerRegistry, + data: abi.encodeWithSignature("getStateFingerprintComputer(address)", offer.collateralAddress), + count: 0 + }); - 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) - ); + offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", ""); + } + function test_shouldFail_whenComputerRegistryReturnsZeroAddress_whenShouldCheckStateFingerprint() external { vm.mockCall( stateFingerprintComputerRegistry, abi.encodeWithSignature("getStateFingerprintComputer(address)", offer.collateralAddress), - abi.encode(stateFingerprintComputer) - ); - vm.mockCall( - stateFingerprintComputer, - abi.encodeWithSignature("getStateFingerprint(uint256)", offer.collateralId), - abi.encode(offer.collateralStateFingerprint) + abi.encode(address(0)) ); - } - - // Helpers - function _signOffer(uint256 pk, PWNSimpleLoanSimpleOffer.Offer memory _offer) private view returns (bytes memory) { - (uint8 v, bytes32 r, bytes32 s) = vm.sign(pk, _offerHash(_offer)); - return abi.encodePacked(r, s, v); - } + vm.expectCall( + stateFingerprintComputerRegistry, + abi.encodeWithSignature("getStateFingerprintComputer(address)", offer.collateralAddress) + ); - function _signOfferCompact(uint256 pk, PWNSimpleLoanSimpleOffer.Offer memory _offer) private 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); + vm.expectRevert(abi.encodeWithSelector(MissingStateFingerprintComputer.selector)); + offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", ""); } + function testFuzz_shouldFail_whenComputerReturnsDifferentStateFingerprint_whenShouldCheckStateFingerprint( + bytes32 stateFingerprint + ) external { + vm.assume(stateFingerprint != offer.collateralStateFingerprint); - // Tests + vm.mockCall( + stateFingerprintComputer, + abi.encodeWithSignature("getStateFingerprint(uint256)", offer.collateralId), + abi.encode(stateFingerprint) + ); - function test_shouldFail_whenCallerIsNotActiveLoan() external { - vm.expectRevert(abi.encodeWithSelector(CallerMissingHubTag.selector, PWNHubTags.ACTIVE_LOAN)); - offerContract.createLOANTerms(borrower, abi.encode(offer), signature); - } + vm.expectCall( + stateFingerprintComputer, + abi.encodeWithSignature("getStateFingerprint(uint256)", offer.collateralId) + ); - function test_shouldFail_whenPassingInvalidOfferData() external { - vm.expectRevert(); - vm.prank(activeLoanContract); - offerContract.createLOANTerms(borrower, abi.encode(uint16(1), uint256(3213), address(0x01320), false, "whaaaaat?"), signature); + vm.expectRevert(abi.encodeWithSelector( + InvalidCollateralStateFingerprint.selector, stateFingerprint, offer.collateralStateFingerprint + )); + offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", ""); } function test_shouldFail_whenInvalidSignature_whenEOA() external { - signature = _signOffer(1, offer); - - vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector)); - vm.prank(activeLoanContract); - offerContract.createLOANTerms(borrower, abi.encode(offer), signature); + vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, offer.lender, _offerHash(offer))); + offerContract.acceptOffer(offer, _signOffer(1, offer), "", ""); } function test_shouldFail_whenInvalidSignature_whenContractAccount() external { vm.etch(lender, bytes("data")); - vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector)); - vm.prank(activeLoanContract); - offerContract.createLOANTerms(borrower, abi.encode(offer), signature); + vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, offer.lender, _offerHash(offer))); + offerContract.acceptOffer(offer, "", "", ""); } function test_shouldPass_whenOfferHasBeenMadeOnchain() external { vm.store( address(offerContract), - keccak256(abi.encode(_offerHash(offer), OFFERS_MADE_SLOT)), + keccak256(abi.encode(_offerHash(offer), PROPOSALS_MADE_SLOT)), bytes32(uint256(1)) ); - vm.prank(activeLoanContract); - offerContract.createLOANTerms(borrower, abi.encode(offer), signature); + offerContract.acceptOffer(offer, "", "", ""); } function test_shouldPass_withValidSignature_whenEOA_whenStandardSignature() external { - signature = _signOffer(lenderPK, offer); - - vm.prank(activeLoanContract); - offerContract.createLOANTerms(borrower, abi.encode(offer), signature); + offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", ""); } function test_shouldPass_withValidSignature_whenEOA_whenCompactEIP2098Signature() external { - signature = _signOfferCompact(lenderPK, offer); - - vm.prank(activeLoanContract); - offerContract.createLOANTerms(borrower, abi.encode(offer), signature); + offerContract.acceptOffer(offer, _signOfferCompact(lenderPK, offer), "", ""); } function test_shouldPass_whenValidSignature_whenContractAccount() external { @@ -278,82 +318,230 @@ contract PWNSimpleLoanSimpleOffer_CreateLOANTerms_Test is PWNSimpleLoanSimpleOff abi.encode(bytes4(0x1626ba7e)) ); - vm.prank(activeLoanContract); - offerContract.createLOANTerms(borrower, abi.encode(offer), signature); + offerContract.acceptOffer(offer, "", "", ""); } - function test_shouldFail_whenOfferIsExpired() external { - vm.warp(40303); - offer.expiration = 30303; - signature = _signOfferCompact(lenderPK, offer); + function testFuzz_shouldFail_whenOfferIsExpired(uint256 timestamp) external { + timestamp = bound(timestamp, offer.expiration, type(uint256).max); + vm.warp(timestamp); - vm.expectRevert(abi.encodeWithSelector(OfferExpired.selector)); - vm.prank(activeLoanContract); - offerContract.createLOANTerms(borrower, abi.encode(offer), signature); - } - - function test_shouldPass_whenOfferIsNotExpired() external { - vm.warp(40303); - offer.expiration = 50303; - signature = _signOfferCompact(lenderPK, offer); - - vm.prank(activeLoanContract); - offerContract.createLOANTerms(borrower, abi.encode(offer), signature); + vm.expectRevert(abi.encodeWithSelector(Expired.selector, timestamp, offer.expiration)); + offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", ""); } function test_shouldFail_whenOfferNonceNotUsable() external { - signature = _signOfferCompact(lenderPK, offer); - vm.mockCall( - revokedOfferNonce, + revokedNonce, abi.encodeWithSignature("isNonceUsable(address,uint256,uint256)"), abi.encode(false) ); vm.expectCall( - revokedOfferNonce, + revokedNonce, abi.encodeWithSignature("isNonceUsable(address,uint256,uint256)", offer.lender, offer.nonceSpace, offer.nonce) ); - vm.expectRevert(abi.encodeWithSelector(NonceNotUsable.selector)); - vm.prank(activeLoanContract); - offerContract.createLOANTerms(borrower, abi.encode(offer), signature); + vm.expectRevert(abi.encodeWithSelector( + NonceNotUsable.selector, offer.lender, offer.nonceSpace, offer.nonce + )); + offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", ""); } - function test_shouldFail_whenCallerIsNotAllowedBorrower() external { - offer.allowedBorrower = address(0x50303); - signature = _signOfferCompact(lenderPK, offer); + function testFuzz_shouldFail_whenCallerIsNotAllowedBorrower(address caller) external { + address allowedBorrower = makeAddr("allowedBorrower"); + vm.assume(caller != allowedBorrower); + offer.allowedBorrower = allowedBorrower; - vm.expectRevert(abi.encodeWithSelector(CallerIsNotStatedBorrower.selector, offer.allowedBorrower)); - vm.prank(activeLoanContract); - offerContract.createLOANTerms(borrower, abi.encode(offer), signature); + vm.expectRevert(abi.encodeWithSelector(CallerNotAllowedAcceptor.selector, caller, offer.allowedBorrower)); + vm.prank(caller); + offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", ""); } - function testFuzz_shouldFail_whenLessThanMinDuration(uint32 duration) external { + 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, _signOffer(lenderPK, offer), "", ""); + } - offer.duration = duration; - signature = _signOfferCompact(lenderPK, offer); + 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(InvalidDuration.selector)); - vm.prank(activeLoanContract); - offerContract.createLOANTerms(borrower, abi.encode(offer), signature); + vm.expectRevert(abi.encodeWithSelector(AccruingInterestAPROutOfBounds.selector, interestAPR, maxInterest)); + offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", ""); } - function testFuzz_shouldFail_whenAccruingInterestAPROutOfBounds(uint40 interestAPR) external { - uint40 maxInterest = offerContract.MAX_ACCRUING_INTEREST_APR(); - interestAPR = uint40(bound(interestAPR, maxInterest + 1, type(uint40).max)); + function test_shouldRevokeOffer_whenAvailableCreditLimitEqualToZero() external { + offer.availableCreditLimit = 0; - offer.accruingInterestAPR = interestAPR; - signature = _signOfferCompact(lenderPK, offer); + vm.expectCall( + revokedNonce, + abi.encodeWithSignature( + "revokeNonce(address,uint256,uint256)", offer.lender, offer.nonceSpace, offer.nonce + ) + ); - vm.expectRevert(abi.encodeWithSelector(AccruingInterestAPROutOfBounds.selector, interestAPR, maxInterest)); - vm.prank(activeLoanContract); - offerContract.createLOANTerms(borrower, abi.encode(offer), signature); + offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", ""); + } + + function testFuzz_shouldFail_whenUsedCreditExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { + used = bound(used, 1, type(uint256).max - offer.loanAmount); + limit = bound(limit, used, used + offer.loanAmount - 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.loanAmount, limit)); + offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", ""); + } + + function testFuzz_shouldIncreaseUsedCredit_whenUsedCreditNotExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { + used = bound(used, 1, type(uint256).max - offer.loanAmount); + limit = bound(limit, used + offer.loanAmount, type(uint256).max); + offer.availableCreditLimit = limit; + + vm.store(address(offerContract), keccak256(abi.encode(_offerHash(offer), CREDIT_USED_SLOT)), bytes32(used)); + + offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", ""); + + assertEq(offerContract.creditUsed(_offerHash(offer)), used + offer.loanAmount); + } + + function test_shouldCallLoanContractWithLoanTerms() external { + bytes memory loanAssetPermit = "loanAssetPermit"; + bytes memory collateralPermit = "collateralPermit"; + + PWNSimpleLoan.Terms memory loanTerms = PWNSimpleLoan.Terms({ + lender: offer.lender, + borrower: borrower, + duration: offer.duration, + collateral: MultiToken.Asset({ + category: offer.collateralCategory, + assetAddress: offer.collateralAddress, + id: offer.collateralId, + amount: offer.collateralAmount + }), + asset: MultiToken.Asset({ + category: MultiToken.Category.ERC20, + assetAddress: offer.loanAssetAddress, + id: 0, + amount: offer.loanAmount + }), + fixedInterestAmount: offer.fixedInterestAmount, + accruingInterestAPR: offer.accruingInterestAPR + }); + + vm.expectCall( + activeLoanContract, + abi.encodeWithSelector( + PWNSimpleLoan.createLOAN.selector, + _offerHash(offer), loanTerms, loanAssetPermit, collateralPermit + ) + ); + + vm.prank(borrower); + offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), loanAssetPermit, collateralPermit); + } + + function test_shouldReturnNewLoanId() external { + assertEq( + offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", ""), + loanId + ); + } + +} + + +/*----------------------------------------------------------*| +|* # ACCEPT OFFER AND REVOKE CALLERS NONCE *| +|*----------------------------------------------------------*/ + +contract PWNSimpleLoanSimpleOffer_AcceptOfferAndRevokeCallersNonce_Test is PWNSimpleLoanSimpleOfferTest { + + 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, + signature: _signOffer(lenderPK, offer), + loanAssetPermit: "", + collateralPermit: "", + 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, + signature: _signOffer(lenderPK, offer), + loanAssetPermit: "", + collateralPermit: "", + callersNonceSpace: nonceSpace, + callersNonceToRevoke: nonce + }); + } + + // function is calling `acceptOffer`, no need to test it again + function test_shouldCallLoanContract() external { + uint256 newLoanId = offerContract.acceptOffer({ + offer: offer, + signature: _signOffer(lenderPK, offer), + loanAssetPermit: "", + collateralPermit: "", + callersNonceSpace: 1, + callersNonceToRevoke: 2 + }); + + assertEq(newLoanId, loanId); + } + +} + + +/*----------------------------------------------------------*| +|* # ACCEPT REFINANCE OFFER *| +|*----------------------------------------------------------*/ + +contract PWNSimpleLoanSimpleOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanSimpleOfferTest { + + 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, _signOffer(lenderPK, offer), "", ""); + } + + 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, _signOffer(lenderPK, offer), "", ""); } function test_shouldNotCallComputerRegistry_whenShouldNotCheckStateFingerprint() external { offer.checkCollateralStateFingerprint = false; - signature = _signOfferCompact(lenderPK, offer); vm.expectCall({ callee: stateFingerprintComputerRegistry, @@ -361,13 +549,10 @@ contract PWNSimpleLoanSimpleOffer_CreateLOANTerms_Test is PWNSimpleLoanSimpleOff count: 0 }); - vm.prank(activeLoanContract); - offerContract.createLOANTerms(borrower, abi.encode(offer), signature); + offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), "", ""); } function test_shouldFail_whenComputerRegistryReturnsZeroAddress_whenShouldCheckStateFingerprint() external { - signature = _signOfferCompact(lenderPK, offer); - vm.mockCall( stateFingerprintComputerRegistry, abi.encodeWithSignature("getStateFingerprintComputer(address)", offer.collateralAddress), @@ -380,8 +565,7 @@ contract PWNSimpleLoanSimpleOffer_CreateLOANTerms_Test is PWNSimpleLoanSimpleOff ); vm.expectRevert(abi.encodeWithSelector(MissingStateFingerprintComputer.selector)); - vm.prank(activeLoanContract); - offerContract.createLOANTerms(borrower, abi.encode(offer), signature); + offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), "", ""); } function testFuzz_shouldFail_whenComputerReturnsDifferentStateFingerprint_whenShouldCheckStateFingerprint( @@ -389,8 +573,6 @@ contract PWNSimpleLoanSimpleOffer_CreateLOANTerms_Test is PWNSimpleLoanSimpleOff ) external { vm.assume(stateFingerprint != offer.collateralStateFingerprint); - signature = _signOfferCompact(lenderPK, offer); - vm.mockCall( stateFingerprintComputer, abi.encodeWithSignature("getStateFingerprint(uint256)", offer.collateralId), @@ -403,105 +585,262 @@ contract PWNSimpleLoanSimpleOffer_CreateLOANTerms_Test is PWNSimpleLoanSimpleOff ); vm.expectRevert(abi.encodeWithSelector( - InvalidCollateralStateFingerprint.selector, offer.collateralStateFingerprint, stateFingerprint + InvalidCollateralStateFingerprint.selector, stateFingerprint, offer.collateralStateFingerprint )); - vm.prank(activeLoanContract); - offerContract.createLOANTerms(borrower, abi.encode(offer), signature); + offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), "", ""); + } + + function test_shouldFail_whenInvalidSignature_whenEOA() external { + vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, offer.lender, _offerHash(offer))); + offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(1, offer), "", ""); + } + + 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, "", "", ""); + } + + function test_shouldPass_whenOfferHasBeenMadeOnchain() external { + vm.store( + address(offerContract), + keccak256(abi.encode(_offerHash(offer), PROPOSALS_MADE_SLOT)), + bytes32(uint256(1)) + ); + + offerContract.acceptRefinanceOffer(loanId, offer, "", "", ""); + } + + function test_shouldPass_withValidSignature_whenEOA_whenStandardSignature() external { + offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), "", ""); + } + + function test_shouldPass_withValidSignature_whenEOA_whenCompactEIP2098Signature() external { + offerContract.acceptRefinanceOffer(loanId, offer, _signOfferCompact(lenderPK, offer), "", ""); + } + + 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, "", "", ""); + } + + 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, _signOffer(lenderPK, offer), "", ""); + } + + 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, _signOffer(lenderPK, offer), "", ""); + } + + 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, _signOffer(lenderPK, offer), "", ""); + } + + 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, _signOffer(lenderPK, offer), "", ""); + } + + 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, _signOffer(lenderPK, offer), "", ""); } function test_shouldRevokeOffer_whenAvailableCreditLimitEqualToZero() external { offer.availableCreditLimit = 0; - signature = _signOfferCompact(lenderPK, offer); vm.expectCall( - revokedOfferNonce, - abi.encodeWithSignature("revokeNonce(address,uint256,uint256)", offer.lender, offer.nonceSpace, offer.nonce) + revokedNonce, + abi.encodeWithSignature( + "revokeNonce(address,uint256,uint256)", offer.lender, offer.nonceSpace, offer.nonce + ) ); - vm.prank(activeLoanContract); - offerContract.createLOANTerms(borrower, abi.encode(offer), signature); + offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), "", ""); } function testFuzz_shouldFail_whenUsedCreditExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { used = bound(used, 1, type(uint256).max - offer.loanAmount); limit = bound(limit, used, used + offer.loanAmount - 1); offer.availableCreditLimit = limit; - signature = _signOfferCompact(lenderPK, offer); vm.store(address(offerContract), keccak256(abi.encode(_offerHash(offer), CREDIT_USED_SLOT)), bytes32(used)); vm.expectRevert(abi.encodeWithSelector(AvailableCreditLimitExceeded.selector, used + offer.loanAmount, limit)); - vm.prank(activeLoanContract); - offerContract.createLOANTerms(borrower, abi.encode(offer), signature); + offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), "", ""); } function testFuzz_shouldIncreaseUsedCredit_whenUsedCreditNotExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { used = bound(used, 1, type(uint256).max - offer.loanAmount); limit = bound(limit, used + offer.loanAmount, type(uint256).max); offer.availableCreditLimit = limit; - signature = _signOfferCompact(lenderPK, offer); vm.store(address(offerContract), keccak256(abi.encode(_offerHash(offer), CREDIT_USED_SLOT)), bytes32(used)); - vm.prank(activeLoanContract); - offerContract.createLOANTerms(borrower, abi.encode(offer), signature); + offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), "", ""); - assertEq(offerContract.creditUsed(offer), used + offer.loanAmount); + assertEq(offerContract.creditUsed(_offerHash(offer)), used + offer.loanAmount); } - function test_shouldReturnCorrectValues() external { - uint256 currentTimestamp = 40303; - vm.warp(currentTimestamp); - signature = _signOfferCompact(lenderPK, offer); + function test_shouldCallLoanContract() external { + bytes memory loanAssetPermit = "loanAssetPermit"; + bytes memory collateralPermit = "collateralPermit"; + + PWNSimpleLoan.Terms memory loanTerms = PWNSimpleLoan.Terms({ + lender: lender, + borrower: offer.lender, + duration: offer.duration, + collateral: MultiToken.Asset({ + category: offer.collateralCategory, + assetAddress: offer.collateralAddress, + id: offer.collateralId, + amount: offer.collateralAmount + }), + asset: MultiToken.Asset({ + category: MultiToken.Category.ERC20, + assetAddress: offer.loanAssetAddress, + id: 0, + amount: offer.loanAmount + }), + fixedInterestAmount: offer.fixedInterestAmount, + accruingInterestAPR: offer.accruingInterestAPR + }); - vm.prank(activeLoanContract); - (PWNLOANTerms.Simple memory loanTerms, bytes32 offerHash) - = offerContract.createLOANTerms(borrower, abi.encode(offer), signature); + vm.expectCall( + activeLoanContract, + abi.encodeWithSelector( + PWNSimpleLoan.refinanceLOAN.selector, + loanId, _offerHash(offer), loanTerms, loanAssetPermit, collateralPermit + ) + ); - assertTrue(loanTerms.lender == offer.lender); - assertTrue(loanTerms.borrower == borrower); - assertTrue(loanTerms.defaultTimestamp == currentTimestamp + offer.duration); - assertTrue(loanTerms.collateral.category == offer.collateralCategory); - assertTrue(loanTerms.collateral.assetAddress == offer.collateralAddress); - assertTrue(loanTerms.collateral.id == offer.collateralId); - assertTrue(loanTerms.collateral.amount == offer.collateralAmount); - assertTrue(loanTerms.asset.category == MultiToken.Category.ERC20); - assertTrue(loanTerms.asset.assetAddress == offer.loanAssetAddress); - assertTrue(loanTerms.asset.id == 0); - assertTrue(loanTerms.asset.amount == offer.loanAmount); - assertTrue(loanTerms.fixedInterestAmount == offer.fixedInterestAmount); - assertTrue(loanTerms.accruingInterestAPR == offer.accruingInterestAPR); - assertTrue(loanTerms.canCreate == true); - assertTrue(loanTerms.canRefinance == true); - assertTrue(loanTerms.refinancingLoanId == 0); + vm.prank(lender); + offerContract.acceptRefinanceOffer( + loanId, offer, _signOffer(lenderPK, offer), loanAssetPermit, collateralPermit + ); + } - assertTrue(offerHash == _offerHash(offer)); + function test_shouldReturnRefinancedLoanId() external { + assertEq( + offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), "", ""), + refinancedLoanId + ); } } /*----------------------------------------------------------*| -|* # GET OFFER HASH *| +|* # ACCEPT REFINANCE OFFER AND REVOKE CALLERS NONCE *| |*----------------------------------------------------------*/ -contract PWNSimpleLoanSimpleOffer_GetOfferHash_Test is PWNSimpleLoanSimpleOfferTest { +contract PWNSimpleLoanSimpleOffer_AcceptRefinanceOfferAndRevokeCallersNonce_Test is PWNSimpleLoanSimpleOfferTest { - function test_shouldReturnOfferHash() external { - assertEq(_offerHash(offer), offerContract.getOfferHash(offer)); + 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, + signature: _signOffer(lenderPK, offer), + lenderLoanAssetPermit: "", + borrowerLoanAssetPermit: "", + 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, + signature: _signOffer(lenderPK, offer), + lenderLoanAssetPermit: "", + borrowerLoanAssetPermit: "", + 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, + signature: _signOffer(lenderPK, offer), + lenderLoanAssetPermit: "", + borrowerLoanAssetPermit: "", + callersNonceSpace: 1, + callersNonceToRevoke: 2 + }); + + assertEq(newLoanId, refinancedLoanId); } } /*----------------------------------------------------------*| -|* # LOAN TERMS FACTORY DATA ENCODING *| +|* # DECODE PROPOSAL *| |*----------------------------------------------------------*/ -contract PWNSimpleLoanSimpleOffer_EncodeLoanTermsFactoryData_Test is PWNSimpleLoanSimpleOfferTest { +contract PWNSimpleLoanSimpleOffer_DecodeProposal_Test is PWNSimpleLoanSimpleOfferTest { - function test_shouldReturnEncodedLoanTermsFactoryData() external { - assertEq(abi.encode(offer), offerContract.encodeLoanTermsFactoryData(offer)); + function test_shouldReturnDecodedOfferData() external { + PWNSimpleLoanSimpleOffer.Offer memory decodedOffer = offerContract.decodeProposal(abi.encode(offer)); + + assertEq(_offerHash(decodedOffer), _offerHash(offer)); } -} \ No newline at end of file +} diff --git a/test/unit/PWNSimpleLoanSimpleRequest.t.sol b/test/unit/PWNSimpleLoanSimpleRequest.t.sol index f548e6e..9e4ec14 100644 --- a/test/unit/PWNSimpleLoanSimpleRequest.t.sol +++ b/test/unit/PWNSimpleLoanSimpleRequest.t.sol @@ -3,37 +3,41 @@ pragma solidity 0.8.16; import "forge-std/Test.sol"; -import "MultiToken/MultiToken.sol"; +import { MultiToken } from "MultiToken/MultiToken.sol"; -import "@pwn/hub/PWNHubTags.sol"; -import "@pwn/loan/terms/simple/factory/request/PWNSimpleLoanSimpleRequest.sol"; -import "@pwn/loan/terms/PWNLOANTerms.sol"; +import { PWNHubTags } from "@pwn/hub/PWNHubTags.sol"; +import { PWNSimpleLoanSimpleRequest, PWNSimpleLoan } + from "@pwn/loan/terms/simple/proposal/request/PWNSimpleLoanSimpleRequest.sol"; import "@pwn/PWNErrors.sol"; abstract contract PWNSimpleLoanSimpleRequestTest is Test { - bytes32 internal constant REQUESTS_MADE_SLOT = bytes32(uint256(0)); // `requestsMade` mapping position - bytes32 internal constant CREDIT_USED_SLOT = bytes32(uint256(1)); // `_creditUsed` mapping position + bytes32 internal constant PROPOSALS_MADE_SLOT = bytes32(uint256(0)); // `proposalsMade` mapping position + bytes32 internal constant CREDIT_USED_SLOT = bytes32(uint256(1)); // `creditUsed` mapping position PWNSimpleLoanSimpleRequest requestContract; - address hub = address(0x80b); - address revokedRequestNonce = address(0x80c); + address hub = makeAddr("hub"); + address revokedNonce = makeAddr("revokedNonce"); address stateFingerprintComputerRegistry = makeAddr("stateFingerprintComputerRegistry"); - address activeLoanContract = address(0x80d); + address activeLoanContract = makeAddr("activeLoanContract"); PWNSimpleLoanSimpleRequest.Request request; - address token = address(0x070ce2); - uint256 borrowerPK = uint256(73661723); + address token = makeAddr("token"); + uint256 borrowerPK = 73661723; address borrower = vm.addr(borrowerPK); + address lender = makeAddr("lender"); + address stateFingerprintComputer = makeAddr("stateFingerprintComputer"); + uint256 loanId = 421; + uint256 refinancedLoanId = 123; - event RequestMade(bytes32 indexed requestHash, address indexed borrower, PWNSimpleLoanSimpleRequest.Request request); + event ProposalMade(bytes32 indexed proposalHash, address indexed proposer, bytes proposal); function setUp() virtual public { vm.etch(hub, bytes("data")); - vm.etch(revokedRequestNonce, bytes("data")); + vm.etch(revokedNonce, bytes("data")); vm.etch(token, bytes("data")); - requestContract = new PWNSimpleLoanSimpleRequest(hub, revokedRequestNonce, stateFingerprintComputerRegistry); + requestContract = new PWNSimpleLoanSimpleRequest(hub, revokedNonce, stateFingerprintComputerRegistry); request = PWNSimpleLoanSimpleRequest.Request({ collateralCategory: MultiToken.Category.ERC721, @@ -53,14 +57,40 @@ abstract contract PWNSimpleLoanSimpleRequestTest is Test { borrower: borrower, refinancingLoanId: 0, nonceSpace: 1, - nonce: uint256(keccak256("nonce_1")) + nonce: uint256(keccak256("nonce_1")), + loanContract: activeLoanContract }); vm.mockCall( - revokedRequestNonce, + 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)", request.collateralAddress), + abi.encode(stateFingerprintComputer) + ); + vm.mockCall( + stateFingerprintComputer, + abi.encodeWithSignature("getStateFingerprint(uint256)", request.collateralId), + abi.encode(request.collateralStateFingerprint) + ); + + vm.mockCall( + activeLoanContract, abi.encodeWithSelector(PWNSimpleLoan.createLOAN.selector), abi.encode(loanId) + ); + vm.mockCall( + activeLoanContract, abi.encodeWithSelector(PWNSimpleLoan.refinanceLOAN.selector), abi.encode(refinancedLoanId) + ); } @@ -75,12 +105,54 @@ abstract contract PWNSimpleLoanSimpleRequestTest is Test { address(requestContract) )), keccak256(abi.encodePacked( - keccak256("Request(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address loanAssetAddress,uint256 loanAmount,uint256 availableCreditLimit,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedLender,address borrower,uint256 refinancingLoanId,uint256 nonceSpace,uint256 nonce)"), + keccak256("Request(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address loanAssetAddress,uint256 loanAmount,uint256 availableCreditLimit,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedLender,address borrower,uint256 refinancingLoanId,uint256 nonceSpace,uint256 nonce,address loanContract)"), abi.encode(_request) )) )); } + function _signRequest( + uint256 pk, PWNSimpleLoanSimpleRequest.Request memory _request + ) internal view returns (bytes memory) { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(pk, _requestHash(_request)); + return abi.encodePacked(r, s, v); + } + + function _signRequestCompact( + uint256 pk, PWNSimpleLoanSimpleRequest.Request memory _request + ) internal view returns (bytes memory) { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(pk, _requestHash(_request)); + return abi.encodePacked(r, bytes32(uint256(v) - 27) << 255 | s); + } + +} + + +/*----------------------------------------------------------*| +|* # CREDIT USED *| +|*----------------------------------------------------------*/ + +contract PWNSimpleLoanSimpleRequest_CreditUsed_Test is PWNSimpleLoanSimpleRequestTest { + + function testFuzz_shouldReturnUsedCredit(uint256 used) external { + vm.store(address(requestContract), keccak256(abi.encode(_requestHash(request), CREDIT_USED_SLOT)), bytes32(used)); + + assertEq(requestContract.creditUsed(_requestHash(request)), used); + } + +} + + +/*----------------------------------------------------------*| +|* # GET REQUEST HASH *| +|*----------------------------------------------------------*/ + +contract PWNSimpleLoanSimpleRequest_GetRequestHash_Test is PWNSimpleLoanSimpleRequestTest { + + function test_shouldReturnRequestHash() external { + assertEq(_requestHash(request), requestContract.getRequestHash(request)); + } + } @@ -93,14 +165,14 @@ contract PWNSimpleLoanSimpleRequest_MakeRequest_Test is PWNSimpleLoanSimpleReque function testFuzz_shouldFail_whenCallerIsNotBorrower(address caller) external { vm.assume(caller != request.borrower); - vm.expectRevert(abi.encodeWithSelector(CallerIsNotStatedBorrower.selector, borrower)); + vm.expectRevert(abi.encodeWithSelector(CallerIsNotStatedProposer.selector, borrower)); vm.prank(caller); requestContract.makeRequest(request); } function test_shouldEmit_RequestMade() external { vm.expectEmit(); - emit RequestMade(_requestHash(request), request.borrower, request); + emit ProposalMade(_requestHash(request), request.borrower, abi.encode(request)); vm.prank(request.borrower); requestContract.makeRequest(request); @@ -110,165 +182,131 @@ contract PWNSimpleLoanSimpleRequest_MakeRequest_Test is PWNSimpleLoanSimpleReque vm.prank(request.borrower); requestContract.makeRequest(request); - assertTrue(requestContract.requestsMade(_requestHash(request))); + assertTrue(requestContract.proposalsMade(_requestHash(request))); } } /*----------------------------------------------------------*| -|* # AVAILABLE CREDIT *| +|* # REVOKE NONCE *| |*----------------------------------------------------------*/ -contract PWNSimpleLoanSimpleRequest_AvailableCredit_Test is PWNSimpleLoanSimpleRequestTest { - - function testFuzz_shouldReturnAvailableCredit(uint256 used, uint256 limit) external { - limit = bound(limit, used, type(uint256).max); - request.availableCreditLimit = limit; +contract PWNSimpleLoanSimpleRequest_RevokeNonce_Test is PWNSimpleLoanSimpleRequestTest { - vm.store(address(requestContract), keccak256(abi.encode(_requestHash(request), CREDIT_USED_SLOT)), bytes32(used)); + function testFuzz_shouldCallRevokeNonce(address caller, uint256 nonceSpace, uint256 nonce) external { + vm.expectCall( + revokedNonce, + abi.encodeWithSignature("revokeNonce(address,uint256,uint256)", caller, nonceSpace, nonce) + ); - assertEq(requestContract.availableCredit(request), limit - used); + vm.prank(caller); + requestContract.revokeNonce(nonceSpace, nonce); } } /*----------------------------------------------------------*| -|* # CREDIT USED *| +|* # ACCEPT REQUEST *| |*----------------------------------------------------------*/ -contract PWNSimpleLoanSimpleRequest_CreditUsed_Test is PWNSimpleLoanSimpleRequestTest { +contract PWNSimpleLoanSimpleRequest_AcceptRequest_Test is PWNSimpleLoanSimpleRequestTest { - function testFuzz_shouldReturnUsedCredit(uint256 used) external { - vm.store(address(requestContract), keccak256(abi.encode(_requestHash(request), CREDIT_USED_SLOT)), bytes32(used)); + function testFuzz_shouldFail_whenRefinancingLoanIdNotZero(uint256 refinancingLoanId) external { + vm.assume(refinancingLoanId != 0); + request.refinancingLoanId = refinancingLoanId; - assertEq(requestContract.creditUsed(request), used); + vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, refinancingLoanId)); + requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", ""); } -} - - -/*----------------------------------------------------------*| -|* # REVOKE REQUEST NONCE *| -|*----------------------------------------------------------*/ - -contract PWNSimpleLoanSimpleRequest_RevokeRequestNonce_Test is PWNSimpleLoanSimpleRequestTest { - - function testFuzz_shouldCallRevokeRequestNonce(uint256 nonceSpace, uint256 nonce) external { - vm.expectCall( - revokedRequestNonce, - abi.encodeWithSignature("revokeNonce(address,uint256,uint256)", borrower, nonceSpace, nonce) - ); + function testFuzz_shouldFail_whenLoanContractNotTagged_ACTIVE_LOAN(address loanContract) external { + vm.assume(loanContract != activeLoanContract); + request.loanContract = loanContract; - vm.prank(borrower); - requestContract.revokeRequestNonce(nonceSpace, nonce); + vm.expectRevert(abi.encodeWithSelector(AddressMissingHubTag.selector, loanContract, PWNHubTags.ACTIVE_LOAN)); + requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", ""); } -} - - -/*----------------------------------------------------------*| -|* # CREATE LOAN TERMS *| -|*----------------------------------------------------------*/ - -contract PWNSimpleLoanSimpleRequest_CreateLOANTerms_Test is PWNSimpleLoanSimpleRequestTest { - - bytes signature; - address lender = makeAddr("lender"); - address stateFingerprintComputer = makeAddr("stateFingerprintComputer"); + function test_shouldNotCallComputerRegistry_whenShouldNotCheckStateFingerprint() external { + request.checkCollateralStateFingerprint = false; - function setUp() override public { - super.setUp(); + vm.expectCall({ + callee: stateFingerprintComputerRegistry, + data: abi.encodeWithSignature("getStateFingerprintComputer(address)", request.collateralAddress), + count: 0 + }); - 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) - ); + requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", ""); + } + function test_shouldFail_whenComputerRegistryReturnsZeroAddress_whenShouldCheckStateFingerprint() external { vm.mockCall( stateFingerprintComputerRegistry, abi.encodeWithSignature("getStateFingerprintComputer(address)", request.collateralAddress), - abi.encode(stateFingerprintComputer) - ); - vm.mockCall( - stateFingerprintComputer, - abi.encodeWithSignature("getStateFingerprint(uint256)", request.collateralId), - abi.encode(request.collateralStateFingerprint) + abi.encode(address(0)) ); - } - - // Helpers - function _signRequest(uint256 pk, PWNSimpleLoanSimpleRequest.Request memory _request) private view returns (bytes memory) { - (uint8 v, bytes32 r, bytes32 s) = vm.sign(pk, _requestHash(_request)); - return abi.encodePacked(r, s, v); - } + vm.expectCall( + stateFingerprintComputerRegistry, + abi.encodeWithSignature("getStateFingerprintComputer(address)", request.collateralAddress) + ); - function _signRequestCompact(uint256 pk, PWNSimpleLoanSimpleRequest.Request memory _request) private view returns (bytes memory) { - (uint8 v, bytes32 r, bytes32 s) = vm.sign(pk, _requestHash(_request)); - return abi.encodePacked(r, bytes32(uint256(v) - 27) << 255 | s); + vm.expectRevert(abi.encodeWithSelector(MissingStateFingerprintComputer.selector)); + requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", ""); } + function testFuzz_shouldFail_whenComputerReturnsDifferentStateFingerprint_whenShouldCheckStateFingerprint( + bytes32 stateFingerprint + ) external { + vm.assume(stateFingerprint != request.collateralStateFingerprint); - // Tests + vm.mockCall( + stateFingerprintComputer, + abi.encodeWithSignature("getStateFingerprint(uint256)", request.collateralId), + abi.encode(stateFingerprint) + ); - function test_shouldFail_whenCallerIsNotActiveLoan() external { - vm.expectRevert(abi.encodeWithSelector(CallerMissingHubTag.selector, PWNHubTags.ACTIVE_LOAN)); - requestContract.createLOANTerms(lender, abi.encode(request), signature); - } + vm.expectCall( + stateFingerprintComputer, + abi.encodeWithSignature("getStateFingerprint(uint256)", request.collateralId) + ); - function test_shouldFail_whenPassingInvalidRequestData() external { - vm.expectRevert(); - vm.prank(activeLoanContract); - requestContract.createLOANTerms(lender, abi.encode(uint16(1), uint256(3213), address(0x01320), false, "whaaaaat?"), signature); + vm.expectRevert(abi.encodeWithSelector( + InvalidCollateralStateFingerprint.selector, stateFingerprint, request.collateralStateFingerprint + )); + requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", ""); } function test_shouldFail_whenInvalidSignature_whenEOA() external { - signature = _signRequest(1, request); - - vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector)); - vm.prank(activeLoanContract); - requestContract.createLOANTerms(lender, abi.encode(request), signature); + vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, request.borrower, _requestHash(request))); + requestContract.acceptRequest(request, _signRequest(1, request), "", ""); } function test_shouldFail_whenInvalidSignature_whenContractAccount() external { vm.etch(borrower, bytes("data")); - vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector)); - vm.prank(activeLoanContract); - requestContract.createLOANTerms(lender, abi.encode(request), signature); + vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, request.borrower, _requestHash(request))); + requestContract.acceptRequest(request, "", "", ""); } function test_shouldPass_whenRequestHasBeenMadeOnchain() external { vm.store( address(requestContract), - keccak256(abi.encode(_requestHash(request), REQUESTS_MADE_SLOT)), + keccak256(abi.encode(_requestHash(request), PROPOSALS_MADE_SLOT)), bytes32(uint256(1)) ); - vm.prank(activeLoanContract); - requestContract.createLOANTerms(lender, abi.encode(request), signature); + requestContract.acceptRequest(request, "", "", ""); } function test_shouldPass_withValidSignature_whenEOA_whenStandardSignature() external { - signature = _signRequest(borrowerPK, request); - - vm.prank(activeLoanContract); - requestContract.createLOANTerms(lender, abi.encode(request), signature); + requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", ""); } function test_shouldPass_withValidSignature_whenEOA_whenCompactEIP2098Signature() external { - signature = _signRequestCompact(borrowerPK, request); - - vm.prank(activeLoanContract); - requestContract.createLOANTerms(lender, abi.encode(request), signature); + requestContract.acceptRequest(request, _signRequestCompact(borrowerPK, request), "", ""); } function test_shouldPass_whenValidSignature_whenContractAccount() external { @@ -280,82 +318,241 @@ contract PWNSimpleLoanSimpleRequest_CreateLOANTerms_Test is PWNSimpleLoanSimpleR abi.encode(bytes4(0x1626ba7e)) ); - vm.prank(activeLoanContract); - requestContract.createLOANTerms(lender, abi.encode(request), signature); + requestContract.acceptRequest(request, "", "", ""); } - function test_shouldFail_whenRequestIsExpired() external { - vm.warp(40303); - request.expiration = 30303; - signature = _signRequestCompact(borrowerPK, request); + function testFuzz_shouldFail_whenRequestIsExpired(uint256 timestamp) external { + timestamp = bound(timestamp, request.expiration, type(uint256).max); + vm.warp(timestamp); - vm.expectRevert(abi.encodeWithSelector(RequestExpired.selector)); - vm.prank(activeLoanContract); - requestContract.createLOANTerms(lender, abi.encode(request), signature); - } - - function test_shouldPass_whenRequestIsNotExpired() external { - vm.warp(40303); - request.expiration = 50303; - signature = _signRequestCompact(borrowerPK, request); - - vm.prank(activeLoanContract); - requestContract.createLOANTerms(lender, abi.encode(request), signature); + vm.expectRevert(abi.encodeWithSelector(Expired.selector, timestamp, request.expiration)); + requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", ""); } function test_shouldFail_whenRequestNonceNotUsable() external { - signature = _signRequestCompact(borrowerPK, request); - vm.mockCall( - revokedRequestNonce, + revokedNonce, abi.encodeWithSignature("isNonceUsable(address,uint256,uint256)"), abi.encode(false) ); vm.expectCall( - revokedRequestNonce, + revokedNonce, abi.encodeWithSignature("isNonceUsable(address,uint256,uint256)", request.borrower, request.nonceSpace, request.nonce) ); - vm.expectRevert(abi.encodeWithSelector(NonceNotUsable.selector)); - vm.prank(activeLoanContract); - requestContract.createLOANTerms(lender, abi.encode(request), signature); + vm.expectRevert(abi.encodeWithSelector( + NonceNotUsable.selector, request.borrower, request.nonceSpace, request.nonce + )); + requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", ""); } - function test_shouldFail_whenCallerIsNotAllowedLender() external { - request.allowedLender = address(0x50303); - signature = _signRequestCompact(borrowerPK, request); + function testFuzz_shouldFail_whenCallerIsNotAllowedLender(address caller) external { + address allowedLender = makeAddr("allowedLender"); + vm.assume(caller != allowedLender); + request.allowedLender = allowedLender; - vm.expectRevert(abi.encodeWithSelector(CallerIsNotStatedLender.selector, request.allowedLender)); - vm.prank(activeLoanContract); - requestContract.createLOANTerms(lender, abi.encode(request), signature); + vm.expectRevert(abi.encodeWithSelector(CallerNotAllowedAcceptor.selector, caller, request.allowedLender)); + vm.prank(caller); + requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", ""); } - function testFuzz_shouldFail_whenLessThanMinDuration(uint32 duration) external { + function testFuzz_shouldFail_whenLessThanMinDuration(uint256 duration) external { vm.assume(duration < requestContract.MIN_LOAN_DURATION()); + duration = bound(duration, 0, requestContract.MIN_LOAN_DURATION() - 1); + request.duration = uint32(duration); - request.duration = duration; - signature = _signRequestCompact(borrowerPK, request); + vm.expectRevert(abi.encodeWithSelector(InvalidDuration.selector, duration, requestContract.MIN_LOAN_DURATION())); + requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", ""); + } + + function testFuzz_shouldFail_whenAccruingInterestAPROutOfBounds(uint256 interestAPR) external { + uint256 maxInterest = requestContract.MAX_ACCRUING_INTEREST_APR(); + interestAPR = bound(interestAPR, maxInterest + 1, type(uint40).max); + request.accruingInterestAPR = uint40(interestAPR); - vm.expectRevert(abi.encodeWithSelector(InvalidDuration.selector)); - vm.prank(activeLoanContract); - requestContract.createLOANTerms(lender, abi.encode(request), signature); + vm.expectRevert(abi.encodeWithSelector(AccruingInterestAPROutOfBounds.selector, interestAPR, maxInterest)); + requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", ""); } - function testFuzz_shouldFail_whenAccruingInterestAPROutOfBounds(uint40 interestAPR) external { - uint40 maxInterest = requestContract.MAX_ACCRUING_INTEREST_APR(); - interestAPR = uint40(bound(interestAPR, maxInterest + 1, type(uint40).max)); + function test_shouldRevokeRequest_whenAvailableCreditLimitEqualToZero() external { + request.availableCreditLimit = 0; - request.accruingInterestAPR = interestAPR; - signature = _signRequestCompact(borrowerPK, request); + vm.expectCall( + revokedNonce, + abi.encodeWithSignature( + "revokeNonce(address,uint256,uint256)", request.borrower, request.nonceSpace, request.nonce + ) + ); - vm.expectRevert(abi.encodeWithSelector(AccruingInterestAPROutOfBounds.selector, interestAPR, maxInterest)); - vm.prank(activeLoanContract); - requestContract.createLOANTerms(lender, abi.encode(request), signature); + requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", ""); + } + + function testFuzz_shouldFail_whenUsedCreditExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { + used = bound(used, 1, type(uint256).max - request.loanAmount); + limit = bound(limit, used, used + request.loanAmount - 1); + request.availableCreditLimit = limit; + + vm.store(address(requestContract), keccak256(abi.encode(_requestHash(request), CREDIT_USED_SLOT)), bytes32(used)); + + vm.expectRevert(abi.encodeWithSelector(AvailableCreditLimitExceeded.selector, used + request.loanAmount, limit)); + requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", ""); + } + + function testFuzz_shouldIncreaseUsedCredit_whenUsedCreditNotExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { + used = bound(used, 1, type(uint256).max - request.loanAmount); + limit = bound(limit, used + request.loanAmount, type(uint256).max); + request.availableCreditLimit = limit; + + vm.store(address(requestContract), keccak256(abi.encode(_requestHash(request), CREDIT_USED_SLOT)), bytes32(used)); + + requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", ""); + + assertEq(requestContract.creditUsed(_requestHash(request)), used + request.loanAmount); + } + + function test_shouldCallLoanContractWithLoanTerms() external { + bytes memory loanAssetPermit = "loanAssetPermit"; + bytes memory collateralPermit = "collateralPermit"; + + PWNSimpleLoan.Terms memory loanTerms = PWNSimpleLoan.Terms({ + lender: lender, + borrower: request.borrower, + duration: request.duration, + collateral: MultiToken.Asset({ + category: request.collateralCategory, + assetAddress: request.collateralAddress, + id: request.collateralId, + amount: request.collateralAmount + }), + asset: MultiToken.Asset({ + category: MultiToken.Category.ERC20, + assetAddress: request.loanAssetAddress, + id: 0, + amount: request.loanAmount + }), + fixedInterestAmount: request.fixedInterestAmount, + accruingInterestAPR: request.accruingInterestAPR + }); + + vm.expectCall( + activeLoanContract, + abi.encodeWithSelector( + PWNSimpleLoan.createLOAN.selector, + _requestHash(request), loanTerms, loanAssetPermit, collateralPermit + ) + ); + + vm.prank(lender); + requestContract.acceptRequest(request, _signRequest(borrowerPK, request), loanAssetPermit, collateralPermit); + } + + function test_shouldReturnNewLoanId() external { + assertEq( + requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", ""), + loanId + ); + } + +} + + +/*----------------------------------------------------------*| +|* # ACCEPT REQUEST AND REVOKE CALLERS NONCE *| +|*----------------------------------------------------------*/ + +contract PWNSimpleLoanSimpleRequest_AcceptRequestAndRevokeCallersNonce_Test is PWNSimpleLoanSimpleRequestTest { + + 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); + requestContract.acceptRequest({ + request: request, + signature: _signRequest(borrowerPK, request), + loanAssetPermit: "", + collateralPermit: "", + 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); + requestContract.acceptRequest({ + request: request, + signature: _signRequest(borrowerPK, request), + loanAssetPermit: "", + collateralPermit: "", + callersNonceSpace: nonceSpace, + callersNonceToRevoke: nonce + }); + } + + // function is calling `acceptRequest`, no need to test it again + function test_shouldCallLoanContract() external { + uint256 newLoanId = requestContract.acceptRequest({ + request: request, + signature: _signRequest(borrowerPK, request), + loanAssetPermit: "", + collateralPermit: "", + callersNonceSpace: 1, + callersNonceToRevoke: 2 + }); + + assertEq(newLoanId, loanId); + } + +} + + +/*----------------------------------------------------------*| +|* # ACCEPT REFINANCE REQUEST *| +|*----------------------------------------------------------*/ + +contract PWNSimpleLoanSimpleRequest_AcceptRefinanceRequest_Test is PWNSimpleLoanSimpleRequestTest { + + function setUp() public override { + super.setUp(); + request.refinancingLoanId = loanId; + } + + + function test_shouldFail_whenRefinancingLoanIdZero() external { + request.refinancingLoanId = 0; + + vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, request.refinancingLoanId)); + requestContract.acceptRefinanceRequest(0, request, _signRequest(borrowerPK, request), "", ""); + } + + function testFuzz_shouldFail_whenRefinancingLoanIdIsNotEqualToLoanId(uint256 _loanId, uint256 _refinancingLoanId) external { + vm.assume(_loanId != _refinancingLoanId); + vm.assume(_loanId != 0); + request.refinancingLoanId = _refinancingLoanId; + + vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, request.refinancingLoanId)); + requestContract.acceptRefinanceRequest(_loanId, request, _signRequest(borrowerPK, request), "", ""); + } + + function testFuzz_shouldFail_whenLoanContractNotTagged_ACTIVE_LOAN(address loanContract) external { + vm.assume(loanContract != activeLoanContract); + request.loanContract = loanContract; + + vm.expectRevert(abi.encodeWithSelector(AddressMissingHubTag.selector, loanContract, PWNHubTags.ACTIVE_LOAN)); + requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", ""); } function test_shouldNotCallComputerRegistry_whenShouldNotCheckStateFingerprint() external { request.checkCollateralStateFingerprint = false; - signature = _signRequestCompact(borrowerPK, request); vm.expectCall({ callee: stateFingerprintComputerRegistry, @@ -363,13 +560,10 @@ contract PWNSimpleLoanSimpleRequest_CreateLOANTerms_Test is PWNSimpleLoanSimpleR count: 0 }); - vm.prank(activeLoanContract); - requestContract.createLOANTerms(lender, abi.encode(request), signature); + requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", ""); } function test_shouldFail_whenComputerRegistryReturnsZeroAddress_whenShouldCheckStateFingerprint() external { - signature = _signRequestCompact(borrowerPK, request); - vm.mockCall( stateFingerprintComputerRegistry, abi.encodeWithSignature("getStateFingerprintComputer(address)", request.collateralAddress), @@ -382,8 +576,7 @@ contract PWNSimpleLoanSimpleRequest_CreateLOANTerms_Test is PWNSimpleLoanSimpleR ); vm.expectRevert(abi.encodeWithSelector(MissingStateFingerprintComputer.selector)); - vm.prank(activeLoanContract); - requestContract.createLOANTerms(lender, abi.encode(request), signature); + requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", ""); } function testFuzz_shouldFail_whenComputerReturnsDifferentStateFingerprint_whenShouldCheckStateFingerprint( @@ -391,8 +584,6 @@ contract PWNSimpleLoanSimpleRequest_CreateLOANTerms_Test is PWNSimpleLoanSimpleR ) external { vm.assume(stateFingerprint != request.collateralStateFingerprint); - signature = _signRequestCompact(borrowerPK, request); - vm.mockCall( stateFingerprintComputer, abi.encodeWithSignature("getStateFingerprint(uint256)", request.collateralId), @@ -405,107 +596,268 @@ contract PWNSimpleLoanSimpleRequest_CreateLOANTerms_Test is PWNSimpleLoanSimpleR ); vm.expectRevert(abi.encodeWithSelector( - InvalidCollateralStateFingerprint.selector, request.collateralStateFingerprint, stateFingerprint + InvalidCollateralStateFingerprint.selector, stateFingerprint, request.collateralStateFingerprint )); - vm.prank(activeLoanContract); - requestContract.createLOANTerms(lender, abi.encode(request), signature); + requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", ""); + } + + function test_shouldFail_whenInvalidSignature_whenEOA() external { + vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, request.borrower, _requestHash(request))); + requestContract.acceptRefinanceRequest(loanId, request, _signRequest(1, request), "", ""); + } + + function test_shouldFail_whenInvalidSignature_whenContractAccount() external { + vm.etch(borrower, bytes("data")); + + vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, request.borrower, _requestHash(request))); + requestContract.acceptRefinanceRequest(loanId, request, "", "", ""); + } + + function test_shouldPass_whenRequestHasBeenMadeOnchain() external { + vm.store( + address(requestContract), + keccak256(abi.encode(_requestHash(request), PROPOSALS_MADE_SLOT)), + bytes32(uint256(1)) + ); + + requestContract.acceptRefinanceRequest(loanId, request, "", "", ""); + } + + function test_shouldPass_withValidSignature_whenEOA_whenStandardSignature() external { + requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", ""); + } + + function test_shouldPass_withValidSignature_whenEOA_whenCompactEIP2098Signature() external { + requestContract.acceptRefinanceRequest(loanId, request, _signRequestCompact(borrowerPK, request), "", ""); + } + + function test_shouldPass_whenValidSignature_whenContractAccount() external { + vm.etch(borrower, bytes("data")); + + vm.mockCall( + borrower, + abi.encodeWithSignature("isValidSignature(bytes32,bytes)"), + abi.encode(bytes4(0x1626ba7e)) + ); + + requestContract.acceptRefinanceRequest(loanId, request, "", "", ""); + } + + function testFuzz_shouldFail_whenRequestIsExpired(uint256 timestamp) external { + timestamp = bound(timestamp, request.expiration, type(uint256).max); + vm.warp(timestamp); + + vm.expectRevert(abi.encodeWithSelector(Expired.selector, timestamp, request.expiration)); + requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", ""); + } + + function test_shouldFail_whenRequestNonceNotUsable() external { + vm.mockCall( + revokedNonce, + abi.encodeWithSignature("isNonceUsable(address,uint256,uint256)"), + abi.encode(false) + ); + vm.expectCall( + revokedNonce, + abi.encodeWithSignature("isNonceUsable(address,uint256,uint256)", request.borrower, request.nonceSpace, request.nonce) + ); + + vm.expectRevert(abi.encodeWithSelector( + NonceNotUsable.selector, request.borrower, request.nonceSpace, request.nonce + )); + requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", ""); + } + + function testFuzz_shouldFail_whenCallerIsNotAllowedLender(address caller) external { + address allowedLender = makeAddr("allowedLender"); + vm.assume(caller != allowedLender); + request.allowedLender = allowedLender; + + vm.expectRevert(abi.encodeWithSelector(CallerNotAllowedAcceptor.selector, caller, request.allowedLender)); + vm.prank(caller); + requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", ""); + } + + function testFuzz_shouldFail_whenLessThanMinDuration(uint256 duration) external { + vm.assume(duration < requestContract.MIN_LOAN_DURATION()); + duration = bound(duration, 0, requestContract.MIN_LOAN_DURATION() - 1); + request.duration = uint32(duration); + + vm.expectRevert(abi.encodeWithSelector(InvalidDuration.selector, duration, requestContract.MIN_LOAN_DURATION())); + requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", ""); + } + + function testFuzz_shouldFail_whenAccruingInterestAPROutOfBounds(uint256 interestAPR) external { + uint256 maxInterest = requestContract.MAX_ACCRUING_INTEREST_APR(); + interestAPR = bound(interestAPR, maxInterest + 1, type(uint40).max); + request.accruingInterestAPR = uint40(interestAPR); + + vm.expectRevert(abi.encodeWithSelector(AccruingInterestAPROutOfBounds.selector, interestAPR, maxInterest)); + requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", ""); } function test_shouldRevokeRequest_whenAvailableCreditLimitEqualToZero() external { request.availableCreditLimit = 0; - signature = _signRequestCompact(borrowerPK, request); vm.expectCall( - revokedRequestNonce, - abi.encodeWithSignature("revokeNonce(address,uint256,uint256)", request.borrower, request.nonceSpace, request.nonce) + revokedNonce, + abi.encodeWithSignature( + "revokeNonce(address,uint256,uint256)", request.borrower, request.nonceSpace, request.nonce + ) ); - vm.prank(activeLoanContract); - requestContract.createLOANTerms(lender, abi.encode(request), signature); + requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", ""); } function testFuzz_shouldFail_whenUsedCreditExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { used = bound(used, 1, type(uint256).max - request.loanAmount); limit = bound(limit, used, used + request.loanAmount - 1); request.availableCreditLimit = limit; - signature = _signRequestCompact(borrowerPK, request); vm.store(address(requestContract), keccak256(abi.encode(_requestHash(request), CREDIT_USED_SLOT)), bytes32(used)); vm.expectRevert(abi.encodeWithSelector(AvailableCreditLimitExceeded.selector, used + request.loanAmount, limit)); - vm.prank(activeLoanContract); - requestContract.createLOANTerms(lender, abi.encode(request), signature); + requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", ""); } function testFuzz_shouldIncreaseUsedCredit_whenUsedCreditNotExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { used = bound(used, 1, type(uint256).max - request.loanAmount); limit = bound(limit, used + request.loanAmount, type(uint256).max); request.availableCreditLimit = limit; - signature = _signRequestCompact(borrowerPK, request); vm.store(address(requestContract), keccak256(abi.encode(_requestHash(request), CREDIT_USED_SLOT)), bytes32(used)); - vm.prank(activeLoanContract); - requestContract.createLOANTerms(lender, abi.encode(request), signature); + requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", ""); + + assertEq(requestContract.creditUsed(_requestHash(request)), used + request.loanAmount); + } + + function test_shouldCallLoanContract() external { + bytes memory loanAssetPermit = "loanAssetPermit"; + bytes memory collateralPermit = "collateralPermit"; + + PWNSimpleLoan.Terms memory loanTerms = PWNSimpleLoan.Terms({ + lender: lender, + borrower: request.borrower, + duration: request.duration, + collateral: MultiToken.Asset({ + category: request.collateralCategory, + assetAddress: request.collateralAddress, + id: request.collateralId, + amount: request.collateralAmount + }), + asset: MultiToken.Asset({ + category: MultiToken.Category.ERC20, + assetAddress: request.loanAssetAddress, + id: 0, + amount: request.loanAmount + }), + fixedInterestAmount: request.fixedInterestAmount, + accruingInterestAPR: request.accruingInterestAPR + }); - assertEq(requestContract.creditUsed(request), used + request.loanAmount); - } + vm.expectCall( + activeLoanContract, + abi.encodeWithSelector( + PWNSimpleLoan.refinanceLOAN.selector, + loanId, _requestHash(request), loanTerms, loanAssetPermit, collateralPermit + ) + ); - function testFuzz_shouldReturnCorrectValues(uint256 _refinancingLoanId) external { - request.refinancingLoanId = _refinancingLoanId; + vm.prank(lender); + requestContract.acceptRefinanceRequest( + loanId, request, _signRequest(borrowerPK, request), loanAssetPermit, collateralPermit + ); + } - uint256 currentTimestamp = 40303; - vm.warp(currentTimestamp); - signature = _signRequestCompact(borrowerPK, request); - - vm.prank(activeLoanContract); - (PWNLOANTerms.Simple memory loanTerms, bytes32 requestHash) - = requestContract.createLOANTerms(lender, abi.encode(request), signature); - - assertTrue(loanTerms.lender == lender); - assertTrue(loanTerms.borrower == request.borrower); - assertTrue(loanTerms.defaultTimestamp == currentTimestamp + request.duration); - assertTrue(loanTerms.collateral.category == request.collateralCategory); - assertTrue(loanTerms.collateral.assetAddress == request.collateralAddress); - assertTrue(loanTerms.collateral.id == request.collateralId); - assertTrue(loanTerms.collateral.amount == request.collateralAmount); - assertTrue(loanTerms.asset.category == MultiToken.Category.ERC20); - assertTrue(loanTerms.asset.assetAddress == request.loanAssetAddress); - assertTrue(loanTerms.asset.id == 0); - assertTrue(loanTerms.asset.amount == request.loanAmount); - assertTrue(loanTerms.fixedInterestAmount == request.fixedInterestAmount); - assertTrue(loanTerms.accruingInterestAPR == request.accruingInterestAPR); - assertTrue(loanTerms.canCreate == (request.refinancingLoanId == 0)); - assertTrue(loanTerms.canRefinance == (request.refinancingLoanId != 0)); - assertTrue(loanTerms.refinancingLoanId == request.refinancingLoanId); - - assertTrue(requestHash == _requestHash(request)); + function test_shouldReturnRefinancedLoanId() external { + assertEq( + requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", ""), + refinancedLoanId + ); } } /*----------------------------------------------------------*| -|* # GET REQUEST HASH *| +|* # ACCEPT REFINANCE REQUEST AND REVOKE CALLERS NONCE *| |*----------------------------------------------------------*/ -contract PWNSimpleLoanSimpleRequest_GetRequestHash_Test is PWNSimpleLoanSimpleRequestTest { +contract PWNSimpleLoanSimpleRequest_AcceptRefinanceRequestAndRevokeCallersNonce_Test is PWNSimpleLoanSimpleRequestTest { - function test_shouldReturnRequestHash() external { - assertEq(_requestHash(request), requestContract.getRequestHash(request)); + function setUp() public override { + super.setUp(); + request.refinancingLoanId = loanId; + } + + + 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); + requestContract.acceptRefinanceRequest({ + loanId: loanId, + request: request, + signature: _signRequest(borrowerPK, request), + lenderLoanAssetPermit: "", + borrowerLoanAssetPermit: "", + 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); + requestContract.acceptRefinanceRequest({ + loanId: loanId, + request: request, + signature: _signRequest(borrowerPK, request), + lenderLoanAssetPermit: "", + borrowerLoanAssetPermit: "", + callersNonceSpace: nonceSpace, + callersNonceToRevoke: nonce + }); + } + + // function is calling `acceptRefinanceRequest`, no need to test it again + function test_shouldCallLoanContract() external { + uint256 newLoanId = requestContract.acceptRefinanceRequest({ + loanId: loanId, + request: request, + signature: _signRequest(borrowerPK, request), + lenderLoanAssetPermit: "", + borrowerLoanAssetPermit: "", + callersNonceSpace: 1, + callersNonceToRevoke: 2 + }); + + assertEq(newLoanId, refinancedLoanId); } } /*----------------------------------------------------------*| -|* # LOAN TERMS FACTORY DATA ENCODING *| +|* # DECODE PROPOSAL *| |*----------------------------------------------------------*/ -contract PWNSimpleLoanSimpleRequest_EncodeLoanTermsFactoryData_Test is PWNSimpleLoanSimpleRequestTest { +contract PWNSimpleLoanSimpleRequest_DecodeProposal_Test is PWNSimpleLoanSimpleRequestTest { - function test_shouldReturnEncodedLoanTermsFactoryData() external { - assertEq(abi.encode(request), requestContract.encodeLoanTermsFactoryData(request)); + function test_shouldReturnDecodedRequestData() external { + PWNSimpleLoanSimpleRequest.Request memory decodedRequest = requestContract.decodeProposal(abi.encode(request)); + + assertEq(_requestHash(decodedRequest), _requestHash(request)); } -} \ No newline at end of file +} From aecf753185dc114bcc0df6e46e2fcb2207ea277a Mon Sep 17 00:00:00 2001 From: ashhanai Date: Tue, 12 Mar 2024 18:41:39 +0100 Subject: [PATCH 041/129] refactor: use calldata instead of memory where possible --- src/loan/terms/simple/loan/PWNSimpleLoan.sol | 45 ++++++++++---------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/src/loan/terms/simple/loan/PWNSimpleLoan.sol b/src/loan/terms/simple/loan/PWNSimpleLoan.sol index 624f414..2267a4b 100644 --- a/src/loan/terms/simple/loan/PWNSimpleLoan.sol +++ b/src/loan/terms/simple/loan/PWNSimpleLoan.sol @@ -287,7 +287,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { * @param collateralPermit Permit data for a collateral signed by a borrower. */ function _settleNewLoan( - Terms memory loanTerms, + Terms calldata loanTerms, bytes calldata loanAssetPermit, bytes calldata collateralPermit ) private { @@ -298,22 +298,24 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { // Permit loan asset spending if permit provided _permit(loanTerms.asset, loanTerms.lender, loanAssetPermit); + MultiToken.Asset memory loanAssetHelper = loanTerms.asset; + // Collect fee if any and update loan asset amount (uint256 feeAmount, uint256 newLoanAmount) = PWNFeeCalculator.calculateFeeAmount(config.fee(), loanTerms.asset.amount); if (feeAmount > 0) { // Transfer fee amount to fee collector - loanTerms.asset.amount = feeAmount; - _pushFrom(loanTerms.asset, loanTerms.lender, config.feeCollector()); + loanAssetHelper.amount = feeAmount; + _pushFrom(loanAssetHelper, loanTerms.lender, config.feeCollector()); // Set new loan amount value - loanTerms.asset.amount = newLoanAmount; + loanAssetHelper.amount = newLoanAmount; } // Note: If the fee amount is greater than zero, the loan asset amount is already updated to the new loan amount. // Transfer loan asset to borrower - _pushFrom(loanTerms.asset, loanTerms.lender, loanTerms.borrower); + _pushFrom(loanAssetHelper, loanTerms.lender, loanTerms.borrower); } @@ -377,7 +379,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { * @param loanId Original loan id. * @param loanTerms Refinancing loan terms struct. */ - function _checkRefinanceLoanTerms(uint256 loanId, Terms memory loanTerms) private view { + function _checkRefinanceLoanTerms(uint256 loanId, Terms calldata loanTerms) private view { LOAN storage loan = LOANs[loanId]; // Check that the loan asset is the same as in the original loan @@ -418,7 +420,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { */ function _refinanceOriginalLoan( uint256 loanId, - Terms memory loanTerms, + Terms calldata loanTerms, bytes calldata lenderLoanAssetPermit, bytes calldata borrowerLoanAssetPermit ) private { @@ -454,27 +456,24 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { bool repayLoanDirectly, address loanOwner, uint256 repaymentAmount, - Terms memory loanTerms, + Terms calldata loanTerms, bytes calldata lenderPermit, bytes calldata borrowerPermit ) private { - MultiToken.Asset memory loanAssetHelper = MultiToken.ERC20(loanTerms.asset.assetAddress, 0); + MultiToken.Asset memory loanAssetHelper = loanTerms.asset; // Compute fee size (uint256 feeAmount, uint256 newLoanAmount) = PWNFeeCalculator.calculateFeeAmount(config.fee(), loanTerms.asset.amount); - // Set new loan amount value - loanTerms.asset.amount = newLoanAmount; - - // Note: At this point `loanTerms` struct has loan asset amount deducted by the fee amount. - // Permit lenders loan asset spending if permit provided - loanAssetHelper.amount = loanTerms.asset.amount + feeAmount; // Permit the whole loan amount + fee loanAssetHelper.amount -= loanTerms.lender == loanOwner // Permit only the surplus transfer + fee - ? Math.min(repaymentAmount, loanTerms.asset.amount) : 0; - if (loanAssetHelper.amount > 0) + ? Math.min(repaymentAmount, newLoanAmount) + : 0; + + if (loanAssetHelper.amount > 0) { _permit(loanAssetHelper, loanTerms.lender, lenderPermit); + } // Collect fees if (feeAmount > 0) { @@ -485,7 +484,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { // If the new lender is the LOAN token owner, don't execute the transfer at all, // it would make transfer from the same address to the same address if (loanTerms.lender != loanOwner) { - loanAssetHelper.amount = Math.min(repaymentAmount, loanTerms.asset.amount); + loanAssetHelper.amount = Math.min(repaymentAmount, newLoanAmount); _transferLoanRepayment({ repayLoanDirectly: repayLoanDirectly, asset: loanAssetHelper, @@ -494,16 +493,16 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { }); } - if (loanTerms.asset.amount >= repaymentAmount) { + if (newLoanAmount >= repaymentAmount) { // New loan covers the whole original loan, transfer surplus to the borrower if any - uint256 surplus = loanTerms.asset.amount - repaymentAmount; + uint256 surplus = newLoanAmount - repaymentAmount; if (surplus > 0) { loanAssetHelper.amount = surplus; _pushFrom(loanAssetHelper, loanTerms.lender, loanTerms.borrower); } } else { // Permit borrowers loan asset spending if permit provided - loanAssetHelper.amount = repaymentAmount - loanTerms.asset.amount; + loanAssetHelper.amount = repaymentAmount - newLoanAmount; _permit(loanAssetHelper, loanTerms.borrower, borrowerPermit); // New loan covers only part of the original loan, borrower needs to contribute @@ -959,7 +958,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { * @param asset Asset to be checked. * @return True if the asset is valid. */ - function isValidAsset(MultiToken.Asset memory asset) public view returns (bool) { + function isValidAsset(MultiToken.Asset calldata asset) public view returns (bool) { return MultiToken.isValid(asset, categoryRegistry); } @@ -968,7 +967,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { * @dev The function will revert if the asset is not valid. * @param asset Asset to be checked. */ - function _checkValidAsset(MultiToken.Asset memory asset) private view { + function _checkValidAsset(MultiToken.Asset calldata asset) private view { if (!isValidAsset(asset)) { revert InvalidMultiTokenAsset({ category: uint8(asset.category), From 8c79ba4a36757c9638df55514033ddef9e189572 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Wed, 13 Mar 2024 10:38:04 +0100 Subject: [PATCH 042/129] refactor: rename loan asset to credit --- src/loan/terms/simple/loan/PWNSimpleLoan.sol | 194 +++++------ .../proposal/offer/PWNSimpleLoanListOffer.sol | 42 +-- .../offer/PWNSimpleLoanSimpleOffer.sol | 42 +-- .../request/PWNSimpleLoanSimpleRequest.sol | 44 +-- test/unit/PWNSimpleLoan.t.sol | 323 +++++++++--------- test/unit/PWNSimpleLoanListOffer.t.sol | 72 ++-- test/unit/PWNSimpleLoanSimpleOffer.t.sol | 72 ++-- test/unit/PWNSimpleLoanSimpleRequest.t.sol | 72 ++-- 8 files changed, 429 insertions(+), 432 deletions(-) diff --git a/src/loan/terms/simple/loan/PWNSimpleLoan.sol b/src/loan/terms/simple/loan/PWNSimpleLoan.sol index 2267a4b..355e1d2 100644 --- a/src/loan/terms/simple/loan/PWNSimpleLoan.sol +++ b/src/loan/terms/simple/loan/PWNSimpleLoan.sol @@ -67,8 +67,8 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { * @param borrower Address of a borrower. * @param duration Loan duration in seconds. * @param collateral Asset used as a loan collateral. For a definition see { MultiToken dependency lib }. - * @param asset Asset used as a loan credit. For a definition see { MultiToken dependency lib }. - * @param fixedInterestAmount Fixed interest amount in loan asset tokens. It is the minimum amount of interest which has to be paid by a borrower. + * @param credit Asset used as a loan credit. For a definition see { MultiToken dependency lib }. + * @param fixedInterestAmount Fixed interest amount in credit asset tokens. It is the minimum amount of interest which has to be paid by a borrower. * @param accruingInterestAPR Accruing interest APR. */ struct Terms { @@ -76,7 +76,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { address borrower; uint32 duration; MultiToken.Asset collateral; - MultiToken.Asset asset; + MultiToken.Asset credit; uint256 fixedInterestAmount; uint40 accruingInterestAPR; } @@ -84,21 +84,21 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { /** * @notice Struct defining a simple loan. * @param status 0 == none/dead || 2 == running/accepted offer/accepted request || 3 == paid back || 4 == expired. - * @param loanAssetAddress Address of an asset used as a loan credit. + * @param creditAddress Address of an asset used as a loan credit. * @param startTimestamp Unix timestamp (in seconds) of a start date. * @param defaultTimestamp Unix timestamp (in seconds) of a default date. * @param borrower Address of a borrower. * @param originalLender Address of a lender that funded the loan. * @param accruingInterestDailyRate Accruing daily interest rate. - * @param fixedInterestAmount Fixed interest amount in loan asset tokens. + * @param fixedInterestAmount Fixed interest amount in credit asset tokens. * It is the minimum amount of interest which has to be paid by a borrower. * This property is reused to store the final interest amount if the loan is repaid and waiting to be claimed. - * @param principalAmount Principal amount in loan asset tokens. + * @param principalAmount Principal amount in credit asset tokens. * @param collateral Asset used as a loan collateral. For a definition see { MultiToken dependency lib }. */ struct LOAN { uint8 status; - address loanAssetAddress; + address creditAddress; uint40 startTimestamp; uint40 defaultTimestamp; address borrower; @@ -117,7 +117,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { /** * @notice Struct defining a loan extension offer. Offer can be signed by a borrower or a lender. * @param loanId Id of a loan to be extended. - * @param price Price of the extension in loan asset tokens. + * @param price Price of the extension in credit asset tokens. * @param duration Duration of the extension in seconds. * @param expiration Unix timestamp (in seconds) of an expiration date. * @param proposer Address of a proposer that signed the extension offer. @@ -202,14 +202,14 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { * @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 loanAssetPermit Permit data for a loan asset signed by a lender. + * @param creditPermit Permit data for a credit asset signed by a lender. * @param collateralPermit Permit data for a collateral signed by a borrower. * @return loanId Id of the created LOAN token. */ function createLOAN( bytes32 proposalHash, Terms calldata loanTerms, - bytes calldata loanAssetPermit, + bytes calldata creditPermit, bytes calldata collateralPermit ) external returns (uint256 loanId) { // Check that caller is loan proposal contract @@ -227,8 +227,8 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { loanTerms: loanTerms }); - // Transfer collateral to Vault and loan asset to borrower - _settleNewLoan(loanTerms, loanAssetPermit, collateralPermit); + // Transfer collateral to Vault and credit to borrower + _settleNewLoan(loanTerms, creditPermit, collateralPermit); } /** @@ -238,7 +238,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { */ function _checkLoanTerms(Terms calldata loanTerms) private view { // Check loan credit and collateral validity - _checkValidAsset(loanTerms.asset); + _checkValidAsset(loanTerms.credit); _checkValidAsset(loanTerms.collateral); } @@ -259,7 +259,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { // Store loan data under loan id LOAN storage loan = LOANs[loanId]; loan.status = 2; - loan.loanAssetAddress = loanTerms.asset.assetAddress; + loan.creditAddress = loanTerms.credit.assetAddress; loan.startTimestamp = uint40(block.timestamp); loan.defaultTimestamp = uint40(block.timestamp) + loanTerms.duration; loan.borrower = loanTerms.borrower; @@ -268,7 +268,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { loanTerms.accruingInterestAPR, APR_TO_DAILY_INTEREST_NUMERATOR, APR_TO_DAILY_INTEREST_DENOMINATOR )); loan.fixedInterestAmount = loanTerms.fixedInterestAmount; - loan.principalAmount = loanTerms.asset.amount; + loan.principalAmount = loanTerms.credit.amount; loan.collateral = loanTerms.collateral; emit LOANCreated({ @@ -280,42 +280,42 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { } /** - * @notice Transfer collateral to Vault and loan asset to borrower. + * @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 loanAssetPermit Permit data for a loan asset signed by a lender. + * @param creditPermit Permit data for a credit asset signed by a lender. * @param collateralPermit Permit data for a collateral signed by a borrower. */ function _settleNewLoan( Terms calldata loanTerms, - bytes calldata loanAssetPermit, + bytes calldata creditPermit, bytes calldata collateralPermit ) private { // Transfer collateral to Vault _permit(loanTerms.collateral, loanTerms.borrower, collateralPermit); _pull(loanTerms.collateral, loanTerms.borrower); - // Permit loan asset spending if permit provided - _permit(loanTerms.asset, loanTerms.lender, loanAssetPermit); + // Permit credit spending if permit provided + _permit(loanTerms.credit, loanTerms.lender, creditPermit); - MultiToken.Asset memory loanAssetHelper = loanTerms.asset; + MultiToken.Asset memory creditHelper = loanTerms.credit; - // Collect fee if any and update loan asset amount + // Collect fee if any and update credit asset amount (uint256 feeAmount, uint256 newLoanAmount) - = PWNFeeCalculator.calculateFeeAmount(config.fee(), loanTerms.asset.amount); + = PWNFeeCalculator.calculateFeeAmount(config.fee(), loanTerms.credit.amount); if (feeAmount > 0) { // Transfer fee amount to fee collector - loanAssetHelper.amount = feeAmount; - _pushFrom(loanAssetHelper, loanTerms.lender, config.feeCollector()); + creditHelper.amount = feeAmount; + _pushFrom(creditHelper, loanTerms.lender, config.feeCollector()); // Set new loan amount value - loanAssetHelper.amount = newLoanAmount; + creditHelper.amount = newLoanAmount; } - // Note: If the fee amount is greater than zero, the loan asset amount is already updated to the new loan amount. + // Note: If the fee amount is greater than zero, the credit amount is already updated to the new loan amount. - // Transfer loan asset to borrower - _pushFrom(loanAssetHelper, loanTerms.lender, loanTerms.borrower); + // Transfer credit to borrower + _pushFrom(creditHelper, loanTerms.lender, loanTerms.borrower); } @@ -331,16 +331,16 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { * 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 lenderLoanAssetPermit Permit data for a loan asset signed by a lender. - * @param borrowerLoanAssetPermit Permit data for a loan asset signed by a borrower. + * @param lenderCreditPermit Permit data for a credit asset signed by a lender. + * @param borrowerCreditPermit Permit data for a credit asset signed by a borrower. * @return refinancedLoanId Id of the refinanced LOAN token. */ function refinanceLOAN( uint256 loanId, bytes32 proposalHash, Terms calldata loanTerms, - bytes calldata lenderLoanAssetPermit, - bytes calldata borrowerLoanAssetPermit + bytes calldata lenderCreditPermit, + bytes calldata borrowerCreditPermit ) external returns (uint256 refinancedLoanId) { // Check that caller is loan proposal contract if (!hub.hasTag(msg.sender, PWNHubTags.LOAN_PROPOSAL)) { @@ -366,8 +366,8 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { _refinanceOriginalLoan( loanId, loanTerms, - lenderLoanAssetPermit, - borrowerLoanAssetPermit + lenderCreditPermit, + borrowerCreditPermit ); emit LOANRefinanced({ loanId: loanId, refinancedLoanId: refinancedLoanId }); @@ -382,12 +382,12 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { function _checkRefinanceLoanTerms(uint256 loanId, Terms calldata loanTerms) private view { LOAN storage loan = LOANs[loanId]; - // Check that the loan asset is the same as in the original loan + // 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.loanAssetAddress != loanTerms.asset.assetAddress || - loanTerms.asset.amount == 0 + loan.creditAddress != loanTerms.credit.assetAddress || + loanTerms.credit.amount == 0 ) revert RefinanceCreditMismatch(); // Check that the collateral is identical to the original one @@ -415,14 +415,14 @@ 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 lenderLoanAssetPermit Permit data for a loan asset signed by a lender. - * @param borrowerLoanAssetPermit Permit data for a loan asset signed by a borrower. + * @param lenderCreditPermit Permit data for a credit asset signed by a lender. + * @param borrowerCreditPermit Permit data for a credit asset signed by a borrower. */ function _refinanceOriginalLoan( uint256 loanId, Terms calldata loanTerms, - bytes calldata lenderLoanAssetPermit, - bytes calldata borrowerLoanAssetPermit + bytes calldata lenderCreditPermit, + bytes calldata borrowerCreditPermit ) private { uint256 repaymentAmount = _loanRepaymentAmount(loanId); @@ -435,8 +435,8 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { loanOwner: loanOwner, repaymentAmount: repaymentAmount, loanTerms: loanTerms, - lenderPermit: lenderLoanAssetPermit, - borrowerPermit: borrowerLoanAssetPermit + lenderPermit: lenderCreditPermit, + borrowerPermit: borrowerCreditPermit }); } @@ -449,8 +449,8 @@ 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 lenderPermit Permit data for a loan asset signed by a lender. - * @param borrowerPermit Permit data for a loan asset signed by a borrower. + * @param lenderPermit Permit data for a credit asset signed by a lender. + * @param borrowerPermit Permit data for a credit asset signed by a borrower. */ function _settleLoanRefinance( bool repayLoanDirectly, @@ -460,34 +460,34 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { bytes calldata lenderPermit, bytes calldata borrowerPermit ) private { - MultiToken.Asset memory loanAssetHelper = loanTerms.asset; + MultiToken.Asset memory creditHelper = loanTerms.credit; // Compute fee size (uint256 feeAmount, uint256 newLoanAmount) - = PWNFeeCalculator.calculateFeeAmount(config.fee(), loanTerms.asset.amount); + = PWNFeeCalculator.calculateFeeAmount(config.fee(), loanTerms.credit.amount); - // Permit lenders loan asset spending if permit provided - loanAssetHelper.amount -= loanTerms.lender == loanOwner // Permit only the surplus transfer + fee + // Permit lenders credit spending if permit provided + creditHelper.amount -= loanTerms.lender == loanOwner // Permit only the surplus transfer + fee ? Math.min(repaymentAmount, newLoanAmount) : 0; - if (loanAssetHelper.amount > 0) { - _permit(loanAssetHelper, loanTerms.lender, lenderPermit); + if (creditHelper.amount > 0) { + _permit(creditHelper, loanTerms.lender, lenderPermit); } // Collect fees if (feeAmount > 0) { - loanAssetHelper.amount = feeAmount; - _pushFrom(loanAssetHelper, loanTerms.lender, config.feeCollector()); + creditHelper.amount = feeAmount; + _pushFrom(creditHelper, loanTerms.lender, config.feeCollector()); } // If the new lender is the LOAN token owner, don't execute the transfer at all, // it would make transfer from the same address to the same address if (loanTerms.lender != loanOwner) { - loanAssetHelper.amount = Math.min(repaymentAmount, newLoanAmount); + creditHelper.amount = Math.min(repaymentAmount, newLoanAmount); _transferLoanRepayment({ repayLoanDirectly: repayLoanDirectly, - asset: loanAssetHelper, + repaymentCredit: creditHelper, repayingAddress: loanTerms.lender, currentLoanOwner: loanOwner }); @@ -497,18 +497,18 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { // New loan covers the whole original loan, transfer surplus to the borrower if any uint256 surplus = newLoanAmount - repaymentAmount; if (surplus > 0) { - loanAssetHelper.amount = surplus; - _pushFrom(loanAssetHelper, loanTerms.lender, loanTerms.borrower); + creditHelper.amount = surplus; + _pushFrom(creditHelper, loanTerms.lender, loanTerms.borrower); } } else { - // Permit borrowers loan asset spending if permit provided - loanAssetHelper.amount = repaymentAmount - newLoanAmount; - _permit(loanAssetHelper, loanTerms.borrower, borrowerPermit); + // Permit borrowers credit spending if permit provided + creditHelper.amount = repaymentAmount - newLoanAmount; + _permit(creditHelper, loanTerms.borrower, borrowerPermit); // New loan covers only part of the original loan, borrower needs to contribute _transferLoanRepayment({ repayLoanDirectly: repayLoanDirectly || loanTerms.lender == loanOwner, - asset: loanAssetHelper, + repaymentCredit: creditHelper, repayingAddress: loanTerms.borrower, currentLoanOwner: loanOwner }); @@ -523,14 +523,14 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { /** * @notice Repay running loan. * @dev Any address can repay a running loan, but a collateral will be transferred to a borrower address associated with the loan. - * Repay will transfer a loan asset to a vault, waiting on a LOAN token holder to claim it. + * Repay will transfer a credit asset to a vault, waiting on a LOAN token holder to claim it. * The function assumes a prior token approval to a contract address or a signed permit. * @param loanId Id of a loan that is being repaid. - * @param loanAssetPermit Permit data for a loan asset signed by a borrower. + * @param creditPermit Permit data for a credit asset signed by a borrower. */ function repayLOAN( uint256 loanId, - bytes calldata loanAssetPermit + bytes calldata creditPermit ) external { LOAN storage loan = LOANs[loanId]; @@ -538,7 +538,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { address borrower = loan.borrower; MultiToken.Asset memory collateral = loan.collateral; - MultiToken.Asset memory repaymentLoanAsset = MultiToken.ERC20(loan.loanAssetAddress, _loanRepaymentAmount(loanId)); + MultiToken.Asset memory repaymentCredit = MultiToken.ERC20(loan.creditAddress, _loanRepaymentAmount(loanId)); (bool repayLoanDirectly, address loanOwner) = _deleteOrUpdateRepaidLoan(loanId); @@ -547,9 +547,9 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { loanOwner: loanOwner, repayingAddress: msg.sender, borrower: borrower, - repaymentLoanAsset: repaymentLoanAsset, + repaymentCredit: repaymentCredit, collateral: collateral, - loanAssetPermit: loanAssetPermit + creditPermit: creditPermit }); } @@ -573,7 +573,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { * @dev If the loan can be repaid directly to the current LOAN owner, * the function will delete the loan and burn the LOAN token. * If the loan cannot be repaid directly to the current LOAN owner, - * the function will move the loan to repaid state and wait for the lender to claim the repaid loan asset. + * the function will move the loan to repaid state and wait for the lender to claim the repaid credit. * @param loanId Id of a loan that is being repaid. * @return repayLoanDirectly If the loan can be repaid directly to the current LOAN owner. * @return loanOwner Address of the current LOAN owner. @@ -583,7 +583,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { emit LOANPaidBack({ loanId: loanId }); - // Note: Assuming that it is safe to transfer the loan asset to the original lender + // Note: Assuming that it is safe to transfer the credit asset to the original lender // if the lender still owns the LOAN token because the lender was able to sign an offer // or make a contract call, thus can handle incoming transfers. loanOwner = loanToken.ownerOf(loanId); @@ -594,7 +594,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { emit LOANClaimed({ loanId: loanId, defaulted: false }); } else { - // Move loan to repaid state and wait for the lender to claim the repaid loan asset + // Move loan to repaid state and wait for the lender to claim the repaid credit loan.status = 3; // Update accrued interest amount loan.fixedInterestAmount = _loanAccruedInterest(loan); @@ -611,46 +611,46 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { * @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 repaymentLoanAsset Loan asset to be repaid. + * @param repaymentCredit Credit asset to be repaid. * @param collateral Collateral to be transferred back to the borrower. - * @param loanAssetPermit Permit data for a loan asset signed by a borrower. + * @param creditPermit Permit data for a credit asset signed by a borrower. */ function _settleLoanRepayment( bool repayLoanDirectly, address loanOwner, address repayingAddress, address borrower, - MultiToken.Asset memory repaymentLoanAsset, + MultiToken.Asset memory repaymentCredit, MultiToken.Asset memory collateral, - bytes calldata loanAssetPermit + bytes calldata creditPermit ) private { - // Transfer loan asset to the original lender or to the Vault - _permit(repaymentLoanAsset, repayingAddress, loanAssetPermit); - _transferLoanRepayment(repayLoanDirectly, repaymentLoanAsset, repayingAddress, loanOwner); + // Transfer credit to the original lender or to the Vault + _permit(repaymentCredit, repayingAddress, creditPermit); + _transferLoanRepayment(repayLoanDirectly, repaymentCredit, repayingAddress, loanOwner); // Transfer collateral back to borrower _push(collateral, borrower); } /** - * @notice Transfer the repaid loan asset to the original lender or to the Vault. + * @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. - * @param asset Asset to be repaid. + * @param repaymentCredit Asset to be repaid. * @param repayingAddress Address of the account repaying the loan. * @param currentLoanOwner Address of the current LOAN owner. */ function _transferLoanRepayment( bool repayLoanDirectly, - MultiToken.Asset memory asset, + MultiToken.Asset memory repaymentCredit, address repayingAddress, address currentLoanOwner ) private { if (repayLoanDirectly) { - // Transfer the repaid loan asset to the LOAN token owner - _pushFrom(asset, repayingAddress, currentLoanOwner); + // Transfer the repaid credit to the LOAN token owner + _pushFrom(repaymentCredit, repayingAddress, currentLoanOwner); } else { - // Transfer the repaid loan asset to the Vault - _pull(asset, repayingAddress); + // Transfer the repaid credit to the Vault + _pull(repaymentCredit, repayingAddress); } } @@ -709,7 +709,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { /** * @notice Claim a repaid or defaulted loan. * @dev Only a LOAN token holder can claim a repaid or defaulted loan. - * Claim will transfer the repaid loan asset or collateral to a LOAN token holder address and burn the LOAN token. + * Claim will transfer the repaid credit or collateral to a LOAN token holder address and burn the LOAN token. * @param loanId Id of a loan that is being claimed. */ function claimLOAN(uint256 loanId) external { @@ -745,7 +745,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { // Store in memory before deleting the loan MultiToken.Asset memory asset = defaulted ? loan.collateral - : MultiToken.ERC20(loan.loanAssetAddress, _loanRepaymentAmount(loanId)); + : MultiToken.ERC20(loan.creditAddress, _loanRepaymentAmount(loanId)); // Delete loan data & burn LOAN token before calling safe transfer _deleteLoan(loanId); @@ -791,12 +791,12 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { * @dev The function assumes a prior token approval to a contract address or a signed permit. * @param extension Extension struct. * @param signature Signature of the extension offer / request. - * @param loanAssetPermit Permit data for a loan asset signed by a borrower. + * @param creditPermit Permit data for a credit asset signed by a borrower. */ function extendLOAN( Extension calldata extension, bytes calldata signature, - bytes calldata loanAssetPermit + bytes calldata creditPermit ) external { LOAN storage loan = LOANs[extension.loanId]; @@ -867,9 +867,9 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { // Transfer extension price to the loan owner if (extension.price > 0) { - MultiToken.Asset memory loanAsset = MultiToken.ERC20(loan.loanAssetAddress, extension.price); - _permit(loanAsset, loan.borrower, loanAssetPermit); - _pushFrom(loanAsset, loan.borrower, loanOwner); + MultiToken.Asset memory credit = MultiToken.ERC20(loan.creditAddress, extension.price); + _permit(credit, loan.borrower, creditPermit); + _pushFrom(credit, loan.borrower, loanOwner); } } @@ -904,10 +904,10 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { * @return originalLender Address of a loan original lender. * @return loanOwner Address of a LOAN token holder. * @return accruingInterestDailyRate Daily interest rate in basis points. - * @return fixedInterestAmount Fixed interest amount in loan asset tokens. - * @return loanAsset Asset used as a loan credit. For a definition see { MultiToken dependency lib }. + * @return fixedInterestAmount Fixed interest amount in credit asset tokens. + * @return credit Asset used as a loan credit. For a definition see { MultiToken dependency lib }. * @return collateral Asset used as a loan collateral. For a definition see { MultiToken dependency lib }. - * @return repaymentAmount Loan repayment amount in loan asset tokens. + * @return repaymentAmount Loan repayment amount in credit asset tokens. */ function getLOAN(uint256 loanId) external view returns ( uint8 status, @@ -918,7 +918,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { address loanOwner, uint40 accruingInterestDailyRate, uint256 fixedInterestAmount, - MultiToken.Asset memory loanAsset, + MultiToken.Asset memory credit, MultiToken.Asset memory collateral, uint256 repaymentAmount ) { @@ -932,7 +932,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { loanOwner = loan.status != 0 ? loanToken.ownerOf(loanId) : address(0); accruingInterestDailyRate = loan.accruingInterestDailyRate; fixedInterestAmount = loan.fixedInterestAmount; - loanAsset = MultiToken.ERC20(loan.loanAssetAddress, loan.principalAmount); + credit = MultiToken.ERC20(loan.creditAddress, loan.principalAmount); collateral = loan.collateral; repaymentAmount = loanRepaymentAmount(loanId); } diff --git a/src/loan/terms/simple/proposal/offer/PWNSimpleLoanListOffer.sol b/src/loan/terms/simple/proposal/offer/PWNSimpleLoanListOffer.sol index 8d4d29e..db451c5 100644 --- a/src/loan/terms/simple/proposal/offer/PWNSimpleLoanListOffer.sol +++ b/src/loan/terms/simple/proposal/offer/PWNSimpleLoanListOffer.sol @@ -23,7 +23,7 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanProposal { * @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 loanAssetAddress,uint256 loanAmount,uint256 availableCreditLimit,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedBorrower,address lender,uint256 refinancingLoanId,uint256 nonceSpace,uint256 nonce,address loanContract)" + "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)" ); /** @@ -34,10 +34,10 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanProposal { * @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 loanAssetAddress Address of an asset which is lender to a borrower. - * @param loanAmount Amount of tokens which is offered as a loan to a borrower. + * @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 loan asset tokens. It is the minimum amount of interest which has to be paid by a borrower. + * @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. @@ -56,8 +56,8 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanProposal { uint256 collateralAmount; bool checkCollateralStateFingerprint; bytes32 collateralStateFingerprint; - address loanAssetAddress; - uint256 loanAmount; + address creditAddress; + uint256 creditAmount; uint256 availableCreditLimit; uint256 fixedInterestAmount; uint40 accruingInterestAPR; @@ -114,7 +114,7 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanProposal { Offer calldata offer, OfferValues calldata offerValues, bytes calldata signature, - bytes calldata loanAssetPermit, + bytes calldata creditPermit, bytes calldata collateralPermit ) public returns (uint256 loanId) { // Check if the offer is refinancing offer @@ -128,7 +128,7 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanProposal { return PWNSimpleLoan(offer.loanContract).createLOAN({ proposalHash: offerHash, loanTerms: loanTerms, - loanAssetPermit: loanAssetPermit, + creditPermit: creditPermit, collateralPermit: collateralPermit }); } @@ -138,8 +138,8 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanProposal { Offer calldata offer, OfferValues calldata offerValues, bytes calldata signature, - bytes calldata lenderLoanAssetPermit, - bytes calldata borrowerLoanAssetPermit + bytes calldata lenderCreditPermit, + bytes calldata borrowerCreditPermit ) public returns (uint256 refinancedLoanId) { // Check if the offer is refinancing offer if (offer.refinancingLoanId != 0 && offer.refinancingLoanId != loanId) { @@ -153,8 +153,8 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanProposal { loanId: loanId, proposalHash: offerHash, loanTerms: loanTerms, - lenderLoanAssetPermit: lenderLoanAssetPermit, - borrowerLoanAssetPermit: borrowerLoanAssetPermit + lenderCreditPermit: lenderCreditPermit, + borrowerCreditPermit: borrowerCreditPermit }); } @@ -162,13 +162,13 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanProposal { Offer calldata offer, OfferValues calldata offerValues, bytes calldata signature, - bytes calldata loanAssetPermit, + bytes calldata creditPermit, bytes calldata collateralPermit, uint256 callersNonceSpace, uint256 callersNonceToRevoke ) external returns (uint256 loanId) { _revokeCallersNonce(msg.sender, callersNonceSpace, callersNonceToRevoke); - return acceptOffer(offer, offerValues, signature, loanAssetPermit, collateralPermit); + return acceptOffer(offer, offerValues, signature, creditPermit, collateralPermit); } function acceptRefinanceOffer( @@ -176,13 +176,13 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanProposal { Offer calldata offer, OfferValues calldata offerValues, bytes calldata signature, - bytes calldata lenderLoanAssetPermit, - bytes calldata borrowerLoanAssetPermit, + bytes calldata lenderCreditPermit, + bytes calldata borrowerCreditPermit, uint256 callersNonceSpace, uint256 callersNonceToRevoke ) external returns (uint256 refinancedLoanId) { _revokeCallersNonce(msg.sender, callersNonceSpace, callersNonceToRevoke); - return acceptRefinanceOffer(loanId, offer, offerValues, signature, lenderLoanAssetPermit, borrowerLoanAssetPermit); + return acceptRefinanceOffer(loanId, offer, offerValues, signature, lenderCreditPermit, borrowerCreditPermit); } @@ -233,7 +233,7 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanProposal { offerHash = getOfferHash(offer); _tryAcceptProposal({ proposalHash: offerHash, - creditAmount: offer.loanAmount, + creditAmount: offer.creditAmount, availableCreditLimit: offer.availableCreditLimit, apr: offer.accruingInterestAPR, duration: offer.duration, @@ -261,9 +261,9 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanProposal { id: offerValues.collateralId, amount: offer.collateralAmount }), - asset: MultiToken.ERC20({ - assetAddress: offer.loanAssetAddress, - amount: offer.loanAmount + credit: MultiToken.ERC20({ + assetAddress: offer.creditAddress, + amount: offer.creditAmount }), fixedInterestAmount: offer.fixedInterestAmount, accruingInterestAPR: offer.accruingInterestAPR diff --git a/src/loan/terms/simple/proposal/offer/PWNSimpleLoanSimpleOffer.sol b/src/loan/terms/simple/proposal/offer/PWNSimpleLoanSimpleOffer.sol index eaf4382..bfb7e7d 100644 --- a/src/loan/terms/simple/proposal/offer/PWNSimpleLoanSimpleOffer.sol +++ b/src/loan/terms/simple/proposal/offer/PWNSimpleLoanSimpleOffer.sol @@ -20,7 +20,7 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanProposal { * @dev EIP-712 simple offer struct type hash. */ bytes32 public constant OFFER_TYPEHASH = keccak256( - "Offer(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address loanAssetAddress,uint256 loanAmount,uint256 availableCreditLimit,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedBorrower,address lender,uint256 refinancingLoanId,uint256 nonceSpace,uint256 nonce,address loanContract)" + "Offer(uint8 collateralCategory,address collateralAddress,uint256 collateralId,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)" ); /** @@ -31,10 +31,10 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanProposal { * @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 loanAssetAddress Address of an asset which is lender to a borrower. - * @param loanAmount Amount of tokens which is offered as a loan to a borrower. + * @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 loan asset tokens. It is the minimum amount of interest which has to be paid by a borrower. + * @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. @@ -53,8 +53,8 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanProposal { uint256 collateralAmount; bool checkCollateralStateFingerprint; bytes32 collateralStateFingerprint; - address loanAssetAddress; - uint256 loanAmount; + address creditAddress; + uint256 creditAmount; uint256 availableCreditLimit; uint256 fixedInterestAmount; uint40 accruingInterestAPR; @@ -97,7 +97,7 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanProposal { function acceptOffer( Offer calldata offer, bytes calldata signature, - bytes calldata loanAssetPermit, + bytes calldata creditPermit, bytes calldata collateralPermit ) public returns (uint256 loanId) { // Check if the offer is refinancing offer @@ -111,7 +111,7 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanProposal { return PWNSimpleLoan(offer.loanContract).createLOAN({ proposalHash: offerHash, loanTerms: loanTerms, - loanAssetPermit: loanAssetPermit, + creditPermit: creditPermit, collateralPermit: collateralPermit }); } @@ -120,8 +120,8 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanProposal { uint256 loanId, Offer calldata offer, bytes calldata signature, - bytes calldata lenderLoanAssetPermit, - bytes calldata borrowerLoanAssetPermit + bytes calldata lenderCreditPermit, + bytes calldata borrowerCreditPermit ) public returns (uint256 refinancedLoanId) { // Check if the offer is refinancing offer if (offer.refinancingLoanId != 0 && offer.refinancingLoanId != loanId) { @@ -135,34 +135,34 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanProposal { loanId: loanId, proposalHash: offerHash, loanTerms: loanTerms, - lenderLoanAssetPermit: lenderLoanAssetPermit, - borrowerLoanAssetPermit: borrowerLoanAssetPermit + lenderCreditPermit: lenderCreditPermit, + borrowerCreditPermit: borrowerCreditPermit }); } function acceptOffer( Offer calldata offer, bytes calldata signature, - bytes calldata loanAssetPermit, + bytes calldata creditPermit, bytes calldata collateralPermit, uint256 callersNonceSpace, uint256 callersNonceToRevoke ) external returns (uint256 loanId) { _revokeCallersNonce(msg.sender, callersNonceSpace, callersNonceToRevoke); - return acceptOffer(offer, signature, loanAssetPermit, collateralPermit); + return acceptOffer(offer, signature, creditPermit, collateralPermit); } function acceptRefinanceOffer( uint256 loanId, Offer calldata offer, bytes calldata signature, - bytes calldata lenderLoanAssetPermit, - bytes calldata borrowerLoanAssetPermit, + bytes calldata lenderCreditPermit, + bytes calldata borrowerCreditPermit, uint256 callersNonceSpace, uint256 callersNonceToRevoke ) external returns (uint256 refinancedLoanId) { _revokeCallersNonce(msg.sender, callersNonceSpace, callersNonceToRevoke); - return acceptRefinanceOffer(loanId, offer, signature, lenderLoanAssetPermit, borrowerLoanAssetPermit); + return acceptRefinanceOffer(loanId, offer, signature, lenderCreditPermit, borrowerCreditPermit); } @@ -193,7 +193,7 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanProposal { offerHash = getOfferHash(offer); _tryAcceptProposal({ proposalHash: offerHash, - creditAmount: offer.loanAmount, + creditAmount: offer.creditAmount, availableCreditLimit: offer.availableCreditLimit, apr: offer.accruingInterestAPR, duration: offer.duration, @@ -218,9 +218,9 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanProposal { id: offer.collateralId, amount: offer.collateralAmount }), - asset: MultiToken.ERC20({ - assetAddress: offer.loanAssetAddress, - amount: offer.loanAmount + credit: MultiToken.ERC20({ + assetAddress: offer.creditAddress, + amount: offer.creditAmount }), fixedInterestAmount: offer.fixedInterestAmount, accruingInterestAPR: offer.accruingInterestAPR diff --git a/src/loan/terms/simple/proposal/request/PWNSimpleLoanSimpleRequest.sol b/src/loan/terms/simple/proposal/request/PWNSimpleLoanSimpleRequest.sol index 4e9bec7..e5f7149 100644 --- a/src/loan/terms/simple/proposal/request/PWNSimpleLoanSimpleRequest.sol +++ b/src/loan/terms/simple/proposal/request/PWNSimpleLoanSimpleRequest.sol @@ -20,7 +20,7 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanProposal { * @dev EIP-712 simple request struct type hash. */ bytes32 public constant REQUEST_TYPEHASH = keccak256( - "Request(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address loanAssetAddress,uint256 loanAmount,uint256 availableCreditLimit,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedLender,address borrower,uint256 refinancingLoanId,uint256 nonceSpace,uint256 nonce,address loanContract)" + "Request(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address creditAddress,uint256 creditAmount,uint256 availableCreditLimit,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedLender,address borrower,uint256 refinancingLoanId,uint256 nonceSpace,uint256 nonce,address loanContract)" ); /** @@ -31,14 +31,14 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanProposal { * @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 loanAssetAddress Address of an asset which is lender to a borrower. - * @param loanAmount Amount of tokens which is requested as a loan to a borrower. + * @param creditAddress Address of an asset which is lender to a borrower. + * @param creditAmount Amount of tokens which is requested as a loan to a borrower. * @param availableCreditLimit Available credit limit for the request. It is the maximum amount of tokens which can be borrowed using the request. - * @param fixedInterestAmount Fixed interest amount in loan asset tokens. It is the minimum amount of interest which has to be paid by a borrower. + * @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 Request expiration timestamp in seconds. - * @param allowedLender Address of an allowed lender. Only this address can accept a request. If the address is zero address, anybody with a loan asset can accept the request. + * @param allowedLender Address of an allowed lender. Only this address can accept a request. If the address is zero address, anybody with a credit asset can accept the request. * @param borrower Address of a borrower. This address has to sign a request to be valid. * @param refinancingLoanId Id of a loan which is refinanced by this request. If the id is 0, the request is not a refinancing request. * @param nonceSpace Nonce space of a request nonce. All nonces in the same space can be revoked at once. @@ -53,8 +53,8 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanProposal { uint256 collateralAmount; bool checkCollateralStateFingerprint; bytes32 collateralStateFingerprint; - address loanAssetAddress; - uint256 loanAmount; + address creditAddress; + uint256 creditAmount; uint256 availableCreditLimit; uint256 fixedInterestAmount; uint40 accruingInterestAPR; @@ -98,7 +98,7 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanProposal { function acceptRequest( Request calldata request, bytes calldata signature, - bytes calldata loanAssetPermit, + bytes calldata creditPermit, bytes calldata collateralPermit ) public returns (uint256 loanId) { // Check if the request is refinancing request @@ -112,7 +112,7 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanProposal { return PWNSimpleLoan(request.loanContract).createLOAN({ proposalHash: requestHash, loanTerms: loanTerms, - loanAssetPermit: loanAssetPermit, + creditPermit: creditPermit, collateralPermit: collateralPermit }); } @@ -121,8 +121,8 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanProposal { uint256 loanId, Request calldata request, bytes calldata signature, - bytes calldata lenderLoanAssetPermit, - bytes calldata borrowerLoanAssetPermit + bytes calldata lenderCreditPermit, + bytes calldata borrowerCreditPermit ) public returns (uint256 refinancedLoanId) { // Check if the request is refinancing request if (request.refinancingLoanId == 0 || request.refinancingLoanId != loanId) { @@ -136,34 +136,34 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanProposal { loanId: loanId, proposalHash: requestHash, loanTerms: loanTerms, - lenderLoanAssetPermit: lenderLoanAssetPermit, - borrowerLoanAssetPermit: borrowerLoanAssetPermit + lenderCreditPermit: lenderCreditPermit, + borrowerCreditPermit: borrowerCreditPermit }); } function acceptRequest( Request calldata request, bytes calldata signature, - bytes calldata loanAssetPermit, + bytes calldata creditPermit, bytes calldata collateralPermit, uint256 callersNonceSpace, uint256 callersNonceToRevoke ) external returns (uint256 loanId) { _revokeCallersNonce(msg.sender, callersNonceSpace, callersNonceToRevoke); - return acceptRequest(request, signature, loanAssetPermit, collateralPermit); + return acceptRequest(request, signature, creditPermit, collateralPermit); } function acceptRefinanceRequest( uint256 loanId, Request calldata request, bytes calldata signature, - bytes calldata lenderLoanAssetPermit, - bytes calldata borrowerLoanAssetPermit, + bytes calldata lenderCreditPermit, + bytes calldata borrowerCreditPermit, uint256 callersNonceSpace, uint256 callersNonceToRevoke ) external returns (uint256 refinancedLoanId) { _revokeCallersNonce(msg.sender, callersNonceSpace, callersNonceToRevoke); - return acceptRefinanceRequest(loanId, request, signature, lenderLoanAssetPermit, borrowerLoanAssetPermit); + return acceptRefinanceRequest(loanId, request, signature, lenderCreditPermit, borrowerCreditPermit); } @@ -194,7 +194,7 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanProposal { requestHash = getRequestHash(request); _tryAcceptProposal({ proposalHash: requestHash, - creditAmount: request.loanAmount, + creditAmount: request.creditAmount, availableCreditLimit: request.availableCreditLimit, apr: request.accruingInterestAPR, duration: request.duration, @@ -219,9 +219,9 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanProposal { id: request.collateralId, amount: request.collateralAmount }), - asset: MultiToken.ERC20({ - assetAddress: request.loanAssetAddress, - amount: request.loanAmount + credit: MultiToken.ERC20({ + assetAddress: request.creditAddress, + amount: request.creditAmount }), fixedInterestAmount: request.fixedInterestAmount, accruingInterestAPR: request.accruingInterestAPR diff --git a/test/unit/PWNSimpleLoan.t.sol b/test/unit/PWNSimpleLoan.t.sol index 9becadd..8aff3e9 100644 --- a/test/unit/PWNSimpleLoan.t.sol +++ b/test/unit/PWNSimpleLoan.t.sol @@ -40,7 +40,7 @@ abstract contract PWNSimpleLoanTest is Test { T20 fungibleAsset; T721 nonFungibleAsset; - bytes loanAssetPermit = abi.encodePacked(uint256(1), uint256(2), uint256(3), uint8(4)); + bytes creditPermit = abi.encodePacked(uint256(1), uint256(2), uint256(3), uint8(4)); bytes collateralPermit = abi.encodePacked(uint256(1), uint256(2), uint256(3), uint8(4)); bytes32 proposalHash = keccak256("proposalHash"); @@ -81,7 +81,7 @@ abstract contract PWNSimpleLoanTest is Test { simpleLoan = PWNSimpleLoan.LOAN({ status: 2, - loanAssetAddress: address(fungibleAsset), + creditAddress: address(fungibleAsset), startTimestamp: uint40(block.timestamp), defaultTimestamp: uint40(block.timestamp + loanDurationInDays * 1 days), borrower: borrower, @@ -97,14 +97,14 @@ abstract contract PWNSimpleLoanTest is Test { borrower: borrower, duration: uint32(loanDurationInDays * 1 days), collateral: MultiToken.ERC721(address(nonFungibleAsset), 2), - asset: MultiToken.ERC20(address(fungibleAsset), 100), + credit: MultiToken.ERC20(address(fungibleAsset), 100), fixedInterestAmount: 6631, accruingInterestAPR: 0 }); nonExistingLoan = PWNSimpleLoan.LOAN({ status: 0, - loanAssetAddress: address(0), + creditAddress: address(0), startTimestamp: 0, defaultTimestamp: 0, borrower: address(0), @@ -158,7 +158,7 @@ abstract contract PWNSimpleLoanTest is Test { function _assertLOANEq(PWNSimpleLoan.LOAN memory _simpleLoan1, PWNSimpleLoan.LOAN memory _simpleLoan2) internal { assertEq(_simpleLoan1.status, _simpleLoan2.status); - assertEq(_simpleLoan1.loanAssetAddress, _simpleLoan2.loanAssetAddress); + assertEq(_simpleLoan1.creditAddress, _simpleLoan2.creditAddress); assertEq(_simpleLoan1.startTimestamp, _simpleLoan2.startTimestamp); assertEq(_simpleLoan1.defaultTimestamp, _simpleLoan2.defaultTimestamp); assertEq(_simpleLoan1.borrower, _simpleLoan2.borrower); @@ -175,8 +175,8 @@ abstract contract PWNSimpleLoanTest is Test { function _assertLOANEq(uint256 _loanId, PWNSimpleLoan.LOAN memory _simpleLoan) internal { uint256 loanSlot = uint256(keccak256(abi.encode(_loanId, LOANS_SLOT))); - // Status, loan asset address, start timestamp, default timestamp - _assertLOANWord(loanSlot + 0, abi.encodePacked(uint8(0), _simpleLoan.defaultTimestamp, _simpleLoan.startTimestamp, _simpleLoan.loanAssetAddress, _simpleLoan.status)); + // Status, credit address, start timestamp, default timestamp + _assertLOANWord(loanSlot + 0, abi.encodePacked(uint8(0), _simpleLoan.defaultTimestamp, _simpleLoan.startTimestamp, _simpleLoan.creditAddress, _simpleLoan.status)); // Borrower address _assertLOANWord(loanSlot + 1, abi.encodePacked(uint96(0), _simpleLoan.borrower)); // Original lender, accruing interest daily rate @@ -197,8 +197,8 @@ abstract contract PWNSimpleLoanTest is Test { function _mockLOAN(uint256 _loanId, PWNSimpleLoan.LOAN memory _simpleLoan) internal { uint256 loanSlot = uint256(keccak256(abi.encode(_loanId, LOANS_SLOT))); - // Status, loan asset address, start timestamp, default timestamp - _storeLOANWord(loanSlot + 0, abi.encodePacked(uint8(0), _simpleLoan.defaultTimestamp, _simpleLoan.startTimestamp, _simpleLoan.loanAssetAddress, _simpleLoan.status)); + // Status, credit address, start timestamp, default timestamp + _storeLOANWord(loanSlot + 0, abi.encodePacked(uint8(0), _simpleLoan.defaultTimestamp, _simpleLoan.startTimestamp, _simpleLoan.creditAddress, _simpleLoan.status)); // Borrower address _storeLOANWord(loanSlot + 1, abi.encodePacked(uint96(0), _simpleLoan.borrower)); // Original lender, accruing interest daily rate @@ -280,7 +280,7 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { loan.createLOAN({ proposalHash: proposalHash, loanTerms: simpleLoanTerms, - loanAssetPermit: "", + creditPermit: "", collateralPermit: "" }); } @@ -288,24 +288,24 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { function test_shouldFail_whenInvalidCreditAsset() external { vm.mockCall( categoryRegistry, - abi.encodeWithSignature("registeredCategoryValue(address)", simpleLoanTerms.asset.assetAddress), + abi.encodeWithSignature("registeredCategoryValue(address)", simpleLoanTerms.credit.assetAddress), abi.encode(1) ); vm.expectRevert( abi.encodeWithSelector( InvalidMultiTokenAsset.selector, - uint8(simpleLoanTerms.asset.category), - simpleLoanTerms.asset.assetAddress, - simpleLoanTerms.asset.id, - simpleLoanTerms.asset.amount + uint8(simpleLoanTerms.credit.category), + simpleLoanTerms.credit.assetAddress, + simpleLoanTerms.credit.id, + simpleLoanTerms.credit.amount ) ); vm.prank(proposalContract); loan.createLOAN({ proposalHash: proposalHash, loanTerms: simpleLoanTerms, - loanAssetPermit: "", + creditPermit: "", collateralPermit: "" }); } @@ -330,7 +330,7 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { loan.createLOAN({ proposalHash: proposalHash, loanTerms: simpleLoanTerms, - loanAssetPermit: "", + creditPermit: "", collateralPermit: "" }); } @@ -342,7 +342,7 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { loan.createLOAN({ proposalHash: proposalHash, loanTerms: simpleLoanTerms, - loanAssetPermit: "", + creditPermit: "", collateralPermit: "" }); } @@ -355,7 +355,7 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { loan.createLOAN({ proposalHash: proposalHash, loanTerms: simpleLoanTerms, - loanAssetPermit: "", + creditPermit: "", collateralPermit: "" }); @@ -385,18 +385,18 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { loan.createLOAN({ proposalHash: proposalHash, loanTerms: simpleLoanTerms, - loanAssetPermit: "", + creditPermit: "", collateralPermit: collateralPermit }); } - function testFuzz_shouldTransferLoanAsset_fromLender_toBorrowerAndFeeCollector( + function testFuzz_shouldTransferCredit_fromLender_toBorrowerAndFeeCollector( uint256 fee, uint256 loanAmount ) external { fee = bound(fee, 0, 9999); loanAmount = bound(loanAmount, 1, 1e40); - simpleLoanTerms.asset.amount = loanAmount; + simpleLoanTerms.credit.amount = loanAmount; fungibleAsset.mint(lender, loanAmount); vm.mockCall(config, abi.encodeWithSignature("fee()"), abi.encode(fee)); @@ -405,7 +405,7 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { uint256 newAmount = loanAmount - feeAmount; vm.expectCall( - simpleLoanTerms.asset.assetAddress, + simpleLoanTerms.credit.assetAddress, abi.encodeWithSignature( "permit(address,address,uint256,uint256,uint8,bytes32,bytes32)", lender, address(loan), loanAmount, 1, uint8(4), uint256(2), uint256(3) @@ -413,13 +413,13 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { ); // Fee transfer vm.expectCall({ - callee: simpleLoanTerms.asset.assetAddress, + callee: simpleLoanTerms.credit.assetAddress, data: abi.encodeWithSignature("transferFrom(address,address,uint256)", lender, feeCollector, feeAmount), count: feeAmount > 0 ? 1 : 0 }); // Updated amount transfer vm.expectCall( - simpleLoanTerms.asset.assetAddress, + simpleLoanTerms.credit.assetAddress, abi.encodeWithSignature("transferFrom(address,address,uint256)", lender, borrower, newAmount) ); @@ -427,7 +427,7 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { loan.createLOAN({ proposalHash: proposalHash, loanTerms: simpleLoanTerms, - loanAssetPermit: loanAssetPermit, + creditPermit: creditPermit, collateralPermit: "" }); } @@ -440,7 +440,7 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { loan.createLOAN({ proposalHash: proposalHash, loanTerms: simpleLoanTerms, - loanAssetPermit: "", + creditPermit: "", collateralPermit: "" }); } @@ -450,7 +450,7 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { uint256 createdLoanId = loan.createLOAN({ proposalHash: proposalHash, loanTerms: simpleLoanTerms, - loanAssetPermit: "", + creditPermit: "", collateralPermit: "" }); @@ -480,7 +480,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { refinancedLoan = PWNSimpleLoan.LOAN({ status: 2, - loanAssetAddress: address(fungibleAsset), + creditAddress: address(fungibleAsset), startTimestamp: uint40(block.timestamp), defaultTimestamp: uint40(block.timestamp + 40039), borrower: borrower, @@ -496,7 +496,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { borrower: borrower, duration: 40039, collateral: MultiToken.ERC721(address(nonFungibleAsset), 2), - asset: MultiToken.ERC20(address(fungibleAsset), 100), + credit: MultiToken.ERC20(address(fungibleAsset), 100), fixedInterestAmount: 6631, accruingInterestAPR: 0 }); @@ -518,8 +518,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderLoanAssetPermit: "", - borrowerLoanAssetPermit: "" + lenderCreditPermit: "", + borrowerCreditPermit: "" }); } @@ -533,8 +533,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderLoanAssetPermit: "", - borrowerLoanAssetPermit: "" + lenderCreditPermit: "", + borrowerCreditPermit: "" }); } @@ -548,8 +548,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderLoanAssetPermit: "", - borrowerLoanAssetPermit: "" + lenderCreditPermit: "", + borrowerCreditPermit: "" }); } @@ -562,14 +562,14 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderLoanAssetPermit: "", - borrowerLoanAssetPermit: "" + lenderCreditPermit: "", + borrowerCreditPermit: "" }); } function testFuzz_shouldFail_whenCreditAssetMismatch(address _assetAddress) external { - vm.assume(_assetAddress != simpleLoan.loanAssetAddress); - refinancedLoanTerms.asset.assetAddress = _assetAddress; + vm.assume(_assetAddress != simpleLoan.creditAddress); + refinancedLoanTerms.credit.assetAddress = _assetAddress; vm.expectRevert(abi.encodeWithSelector(RefinanceCreditMismatch.selector)); vm.prank(proposalContract); @@ -577,13 +577,13 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderLoanAssetPermit: "", - borrowerLoanAssetPermit: "" + lenderCreditPermit: "", + borrowerCreditPermit: "" }); } function test_shouldFail_whenCreditAssetAmountZero() external { - refinancedLoanTerms.asset.amount = 0; + refinancedLoanTerms.credit.amount = 0; vm.expectRevert(abi.encodeWithSelector(RefinanceCreditMismatch.selector)); vm.prank(proposalContract); @@ -591,8 +591,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderLoanAssetPermit: "", - borrowerLoanAssetPermit: "" + lenderCreditPermit: "", + borrowerCreditPermit: "" }); } @@ -607,8 +607,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderLoanAssetPermit: "", - borrowerLoanAssetPermit: "" + lenderCreditPermit: "", + borrowerCreditPermit: "" }); } @@ -622,8 +622,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderLoanAssetPermit: "", - borrowerLoanAssetPermit: "" + lenderCreditPermit: "", + borrowerCreditPermit: "" }); } @@ -637,8 +637,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderLoanAssetPermit: "", - borrowerLoanAssetPermit: "" + lenderCreditPermit: "", + borrowerCreditPermit: "" }); } @@ -652,8 +652,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderLoanAssetPermit: "", - borrowerLoanAssetPermit: "" + lenderCreditPermit: "", + borrowerCreditPermit: "" }); } @@ -667,8 +667,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderLoanAssetPermit: "", - borrowerLoanAssetPermit: "" + lenderCreditPermit: "", + borrowerCreditPermit: "" }); } @@ -680,8 +680,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderLoanAssetPermit: "", - borrowerLoanAssetPermit: "" + lenderCreditPermit: "", + borrowerCreditPermit: "" }); } @@ -691,8 +691,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderLoanAssetPermit: "", - borrowerLoanAssetPermit: "" + lenderCreditPermit: "", + borrowerCreditPermit: "" }); _assertLOANEq(ferinancedLoanId, refinancedLoan); @@ -707,8 +707,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderLoanAssetPermit: "", - borrowerLoanAssetPermit: "" + lenderCreditPermit: "", + borrowerCreditPermit: "" }); } @@ -718,8 +718,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderLoanAssetPermit: "", - borrowerLoanAssetPermit: "" + lenderCreditPermit: "", + borrowerCreditPermit: "" }); assertEq(newLoanId, ferinancedLoanId); @@ -734,8 +734,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderLoanAssetPermit: "", - borrowerLoanAssetPermit: "" + lenderCreditPermit: "", + borrowerCreditPermit: "" }); } @@ -748,8 +748,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderLoanAssetPermit: "", - borrowerLoanAssetPermit: "" + lenderCreditPermit: "", + borrowerCreditPermit: "" }); } @@ -759,8 +759,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderLoanAssetPermit: "", - borrowerLoanAssetPermit: "" + lenderCreditPermit: "", + borrowerCreditPermit: "" }); _assertLOANEq(loanId, nonExistingLoan); @@ -775,8 +775,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderLoanAssetPermit: "", - borrowerLoanAssetPermit: "" + lenderCreditPermit: "", + borrowerCreditPermit: "" }); } @@ -805,8 +805,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderLoanAssetPermit: "", - borrowerLoanAssetPermit: "" + lenderCreditPermit: "", + borrowerCreditPermit: "" }); // Update loan and compare @@ -830,7 +830,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { uint256 feeAmount = Math.mulDiv(refinanceAmount, fee, 1e4); uint256 borrowerSurplus = refinanceAmount - feeAmount - loanRepaymentAmount; - refinancedLoanTerms.asset.amount = refinanceAmount; + refinancedLoanTerms.credit.amount = refinanceAmount; refinancedLoanTerms.lender = newLender; _mockLOANTokenOwner(loanId, simpleLoan.originalLender); vm.mockCall(config, abi.encodeWithSignature("fee()"), abi.encode(fee)); @@ -838,7 +838,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { fungibleAsset.mint(newLender, refinanceAmount); vm.expectCall( // lender permit - refinancedLoanTerms.asset.assetAddress, + refinancedLoanTerms.credit.assetAddress, abi.encodeWithSignature( "permit(address,address,uint256,uint256,uint8,bytes32,bytes32)", newLender, address(loan), refinanceAmount, 1, uint8(4), uint256(2), uint256(3) @@ -846,7 +846,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { ); // no borrower permit vm.expectCall({ // fee transfer - callee: refinancedLoanTerms.asset.assetAddress, + callee: refinancedLoanTerms.credit.assetAddress, data: abi.encodeWithSignature( "transferFrom(address,address,uint256)", newLender, feeCollector, feeAmount @@ -854,14 +854,14 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { count: feeAmount > 0 ? 1 : 0 }); vm.expectCall( // lender repayment - refinancedLoanTerms.asset.assetAddress, + refinancedLoanTerms.credit.assetAddress, abi.encodeWithSignature( "transferFrom(address,address,uint256)", newLender, simpleLoan.originalLender, loanRepaymentAmount ) ); vm.expectCall({ // borrower surplus - callee: refinancedLoanTerms.asset.assetAddress, + callee: refinancedLoanTerms.credit.assetAddress, data: abi.encodeWithSignature( "transferFrom(address,address,uint256)", newLender, borrower, borrowerSurplus @@ -874,8 +874,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderLoanAssetPermit: loanAssetPermit, - borrowerLoanAssetPermit: "" + lenderCreditPermit: creditPermit, + borrowerCreditPermit: "" }); } @@ -893,7 +893,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { uint256 feeAmount = Math.mulDiv(refinanceAmount, fee, 1e4); uint256 borrowerSurplus = refinanceAmount - feeAmount - loanRepaymentAmount; - refinancedLoanTerms.asset.amount = refinanceAmount; + refinancedLoanTerms.credit.amount = refinanceAmount; refinancedLoanTerms.lender = newLender; _mockLOANTokenOwner(loanId, makeAddr("notOriginalLender")); vm.mockCall(config, abi.encodeWithSignature("fee()"), abi.encode(fee)); @@ -901,7 +901,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { fungibleAsset.mint(newLender, refinanceAmount); vm.expectCall( // lender permit - refinancedLoanTerms.asset.assetAddress, + refinancedLoanTerms.credit.assetAddress, abi.encodeWithSignature( "permit(address,address,uint256,uint256,uint8,bytes32,bytes32)", newLender, address(loan), refinanceAmount, 1, uint8(4), uint256(2), uint256(3) @@ -909,7 +909,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { ); // no borrower permit vm.expectCall({ // fee transfer - callee: refinancedLoanTerms.asset.assetAddress, + callee: refinancedLoanTerms.credit.assetAddress, data: abi.encodeWithSignature( "transferFrom(address,address,uint256)", newLender, feeCollector, feeAmount @@ -917,14 +917,14 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { count: feeAmount > 0 ? 1 : 0 }); vm.expectCall( // lender repayment - refinancedLoanTerms.asset.assetAddress, + refinancedLoanTerms.credit.assetAddress, abi.encodeWithSignature( "transferFrom(address,address,uint256)", newLender, address(loan), loanRepaymentAmount ) ); vm.expectCall({ // borrower surplus - callee: refinancedLoanTerms.asset.assetAddress, + callee: refinancedLoanTerms.credit.assetAddress, data: abi.encodeWithSignature( "transferFrom(address,address,uint256)", newLender, borrower, borrowerSurplus @@ -937,8 +937,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderLoanAssetPermit: loanAssetPermit, - borrowerLoanAssetPermit: "" + lenderCreditPermit: creditPermit, + borrowerCreditPermit: "" }); } @@ -956,7 +956,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { uint256 feeAmount = Math.mulDiv(refinanceAmount, fee, 1e4); uint256 borrowerSurplus = refinanceAmount - feeAmount - loanRepaymentAmount; - refinancedLoanTerms.asset.amount = refinanceAmount; + refinancedLoanTerms.credit.amount = refinanceAmount; refinancedLoanTerms.lender = newLender; _mockLOANTokenOwner(loanId, newLender); vm.mockCall(config, abi.encodeWithSignature("fee()"), abi.encode(fee)); @@ -964,7 +964,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { fungibleAsset.mint(newLender, refinanceAmount); vm.expectCall({ // lender permit - callee: refinancedLoanTerms.asset.assetAddress, + callee: refinancedLoanTerms.credit.assetAddress, data: abi.encodeWithSignature( "permit(address,address,uint256,uint256,uint8,bytes32,bytes32)", newLender, address(loan), borrowerSurplus + feeAmount, 1, uint8(4), uint256(2), uint256(3) @@ -973,7 +973,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { }); // no borrower permit vm.expectCall({ // fee transfer - callee: refinancedLoanTerms.asset.assetAddress, + callee: refinancedLoanTerms.credit.assetAddress, data: abi.encodeWithSignature( "transferFrom(address,address,uint256)", newLender, feeCollector, feeAmount @@ -981,7 +981,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { count: feeAmount > 0 ? 1 : 0 }); vm.expectCall({ // lender repayment - callee: refinancedLoanTerms.asset.assetAddress, + callee: refinancedLoanTerms.credit.assetAddress, data: abi.encodeWithSignature( "transferFrom(address,address,uint256)", newLender, newLender, loanRepaymentAmount @@ -989,7 +989,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { count: 0 }); vm.expectCall({ // borrower surplus - callee: refinancedLoanTerms.asset.assetAddress, + callee: refinancedLoanTerms.credit.assetAddress, data: abi.encodeWithSignature( "transferFrom(address,address,uint256)", newLender, borrower, borrowerSurplus @@ -1002,8 +1002,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderLoanAssetPermit: loanAssetPermit, - borrowerLoanAssetPermit: "" + lenderCreditPermit: creditPermit, + borrowerCreditPermit: "" }); } @@ -1017,7 +1017,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { uint256 feeAmount = Math.mulDiv(refinanceAmount, fee, 1e4); uint256 borrowerContribution = loanRepaymentAmount - (refinanceAmount - feeAmount); - refinancedLoanTerms.asset.amount = refinanceAmount; + refinancedLoanTerms.credit.amount = refinanceAmount; refinancedLoanTerms.lender = newLender; _mockLOANTokenOwner(loanId, simpleLoan.originalLender); vm.mockCall(config, abi.encodeWithSignature("fee()"), abi.encode(fee)); @@ -1025,14 +1025,14 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { fungibleAsset.mint(newLender, refinanceAmount); vm.expectCall( // lender permit - refinancedLoanTerms.asset.assetAddress, + refinancedLoanTerms.credit.assetAddress, abi.encodeWithSignature( "permit(address,address,uint256,uint256,uint8,bytes32,bytes32)", newLender, address(loan), refinanceAmount, 1, uint8(4), uint256(2), uint256(3) ) ); vm.expectCall({ // borrower permit - callee: refinancedLoanTerms.asset.assetAddress, + callee: refinancedLoanTerms.credit.assetAddress, data: abi.encodeWithSignature( "permit(address,address,uint256,uint256,uint8,bytes32,bytes32)", borrower, address(loan), borrowerContribution, 1, uint8(4), uint256(2), uint256(3) @@ -1040,7 +1040,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { count: borrowerContribution > 0 ? 1 : 0 }); vm.expectCall({ // fee transfer - callee: refinancedLoanTerms.asset.assetAddress, + callee: refinancedLoanTerms.credit.assetAddress, data: abi.encodeWithSignature( "transferFrom(address,address,uint256)", newLender, feeCollector, feeAmount @@ -1048,14 +1048,14 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { count: feeAmount > 0 ? 1 : 0 }); vm.expectCall( // lender repayment - refinancedLoanTerms.asset.assetAddress, + refinancedLoanTerms.credit.assetAddress, abi.encodeWithSignature( "transferFrom(address,address,uint256)", newLender, simpleLoan.originalLender, refinanceAmount - feeAmount ) ); vm.expectCall({ // borrower contribution - callee: refinancedLoanTerms.asset.assetAddress, + callee: refinancedLoanTerms.credit.assetAddress, data: abi.encodeWithSignature( "transferFrom(address,address,uint256)", borrower, simpleLoan.originalLender, borrowerContribution @@ -1068,8 +1068,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderLoanAssetPermit: loanAssetPermit, - borrowerLoanAssetPermit: loanAssetPermit + lenderCreditPermit: creditPermit, + borrowerCreditPermit: creditPermit }); } @@ -1083,7 +1083,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { uint256 feeAmount = Math.mulDiv(refinanceAmount, fee, 1e4); uint256 borrowerContribution = loanRepaymentAmount - (refinanceAmount - feeAmount); - refinancedLoanTerms.asset.amount = refinanceAmount; + refinancedLoanTerms.credit.amount = refinanceAmount; refinancedLoanTerms.lender = newLender; _mockLOANTokenOwner(loanId, makeAddr("notOriginalLender")); vm.mockCall(config, abi.encodeWithSignature("fee()"), abi.encode(fee)); @@ -1091,14 +1091,14 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { fungibleAsset.mint(newLender, refinanceAmount); vm.expectCall( // lender permit - refinancedLoanTerms.asset.assetAddress, + refinancedLoanTerms.credit.assetAddress, abi.encodeWithSignature( "permit(address,address,uint256,uint256,uint8,bytes32,bytes32)", newLender, address(loan), refinanceAmount, 1, uint8(4), uint256(2), uint256(3) ) ); vm.expectCall({ // borrower permit - callee: refinancedLoanTerms.asset.assetAddress, + callee: refinancedLoanTerms.credit.assetAddress, data: abi.encodeWithSignature( "permit(address,address,uint256,uint256,uint8,bytes32,bytes32)", borrower, address(loan), borrowerContribution, 1, uint8(4), uint256(2), uint256(3) @@ -1106,7 +1106,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { count: borrowerContribution > 0 ? 1 : 0 }); vm.expectCall({ // fee transfer - callee: refinancedLoanTerms.asset.assetAddress, + callee: refinancedLoanTerms.credit.assetAddress, data: abi.encodeWithSignature( "transferFrom(address,address,uint256)", newLender, feeCollector, feeAmount @@ -1114,14 +1114,14 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { count: feeAmount > 0 ? 1 : 0 }); vm.expectCall( // lender repayment - refinancedLoanTerms.asset.assetAddress, + refinancedLoanTerms.credit.assetAddress, abi.encodeWithSignature( "transferFrom(address,address,uint256)", newLender, address(loan), refinanceAmount - feeAmount ) ); vm.expectCall({ // borrower contribution - callee: refinancedLoanTerms.asset.assetAddress, + callee: refinancedLoanTerms.credit.assetAddress, data: abi.encodeWithSignature( "transferFrom(address,address,uint256)", borrower, address(loan), borrowerContribution @@ -1134,8 +1134,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderLoanAssetPermit: loanAssetPermit, - borrowerLoanAssetPermit: loanAssetPermit + lenderCreditPermit: creditPermit, + borrowerCreditPermit: creditPermit }); } @@ -1149,7 +1149,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { uint256 feeAmount = Math.mulDiv(refinanceAmount, fee, 1e4); uint256 borrowerContribution = loanRepaymentAmount - (refinanceAmount - feeAmount); - refinancedLoanTerms.asset.amount = refinanceAmount; + refinancedLoanTerms.credit.amount = refinanceAmount; refinancedLoanTerms.lender = newLender; _mockLOANTokenOwner(loanId, newLender); vm.mockCall(config, abi.encodeWithSignature("fee()"), abi.encode(fee)); @@ -1157,7 +1157,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { fungibleAsset.mint(newLender, refinanceAmount); vm.expectCall({ // lender permit - callee: refinancedLoanTerms.asset.assetAddress, + callee: refinancedLoanTerms.credit.assetAddress, data: abi.encodeWithSignature( "permit(address,address,uint256,uint256,uint8,bytes32,bytes32)", newLender, address(loan), feeAmount, 1, uint8(4), uint256(2), uint256(3) @@ -1165,7 +1165,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { count: feeAmount > 0 ? 1 : 0 }); vm.expectCall({ // borrower permit - callee: refinancedLoanTerms.asset.assetAddress, + callee: refinancedLoanTerms.credit.assetAddress, data: abi.encodeWithSignature( "permit(address,address,uint256,uint256,uint8,bytes32,bytes32)", borrower, address(loan), borrowerContribution, 1, uint8(4), uint256(2), uint256(3) @@ -1173,7 +1173,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { count: borrowerContribution > 0 ? 1 : 0 }); vm.expectCall({ // fee transfer - callee: refinancedLoanTerms.asset.assetAddress, + callee: refinancedLoanTerms.credit.assetAddress, data: abi.encodeWithSignature( "transferFrom(address,address,uint256)", newLender, feeCollector, feeAmount @@ -1181,7 +1181,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { count: feeAmount > 0 ? 1 : 0 }); vm.expectCall({ // lender repayment - callee: refinancedLoanTerms.asset.assetAddress, + callee: refinancedLoanTerms.credit.assetAddress, data: abi.encodeWithSignature( "transferFrom(address,address,uint256)", newLender, newLender, refinanceAmount - feeAmount @@ -1189,7 +1189,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { count: 0 }); vm.expectCall({ // borrower contribution - callee: refinancedLoanTerms.asset.assetAddress, + callee: refinancedLoanTerms.credit.assetAddress, data: abi.encodeWithSignature( "transferFrom(address,address,uint256)", borrower, newLender, borrowerContribution @@ -1202,8 +1202,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderLoanAssetPermit: loanAssetPermit, - borrowerLoanAssetPermit: loanAssetPermit + lenderCreditPermit: creditPermit, + borrowerCreditPermit: creditPermit }); } @@ -1227,7 +1227,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { refinanceAmount, 1, type(uint256).max - loanRepaymentAmount - fungibleAsset.totalSupply() ); - refinancedLoanTerms.asset.amount = refinanceAmount; + refinancedLoanTerms.credit.amount = refinanceAmount; refinancedLoanTerms.lender = newLender; _mockLOANTokenOwner(loanId, lender); @@ -1243,8 +1243,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderLoanAssetPermit: "", - borrowerLoanAssetPermit: "" + lenderCreditPermit: "", + borrowerCreditPermit: "" }); assertEq(fungibleAsset.balanceOf(lender), originalBalance + loanRepaymentAmount); @@ -1272,7 +1272,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { ); uint256 feeAmount = Math.mulDiv(refinanceAmount, fee, 1e4); - refinancedLoanTerms.asset.amount = refinanceAmount; + refinancedLoanTerms.credit.amount = refinanceAmount; refinancedLoanTerms.lender = newLender; _mockLOANTokenOwner(loanId, lender); vm.mockCall(config, abi.encodeWithSignature("fee()"), abi.encode(fee)); @@ -1289,8 +1289,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderLoanAssetPermit: "", - borrowerLoanAssetPermit: "" + lenderCreditPermit: "", + borrowerCreditPermit: "" }); assertEq(fungibleAsset.balanceOf(feeCollector), originalBalance + feeAmount); @@ -1303,7 +1303,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { ); uint256 surplus = refinanceAmount - loanRepaymentAmount; - refinancedLoanTerms.asset.amount = refinanceAmount; + refinancedLoanTerms.credit.amount = refinanceAmount; refinancedLoanTerms.lender = newLender; _mockLOANTokenOwner(loanId, lender); @@ -1315,8 +1315,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderLoanAssetPermit: "", - borrowerLoanAssetPermit: "" + lenderCreditPermit: "", + borrowerCreditPermit: "" }); assertEq(fungibleAsset.balanceOf(borrower), originalBalance + surplus); @@ -1327,7 +1327,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { refinanceAmount = bound(refinanceAmount, 1, loanRepaymentAmount - 1); uint256 contribution = loanRepaymentAmount - refinanceAmount; - refinancedLoanTerms.asset.amount = refinanceAmount; + refinancedLoanTerms.credit.amount = refinanceAmount; refinancedLoanTerms.lender = newLender; _mockLOANTokenOwner(loanId, lender); @@ -1339,8 +1339,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderLoanAssetPermit: "", - borrowerLoanAssetPermit: "" + lenderCreditPermit: "", + borrowerCreditPermit: "" }); assertEq(fungibleAsset.balanceOf(borrower), originalBalance - contribution); @@ -1373,7 +1373,7 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { _mockLOAN(loanId, simpleLoan); vm.expectRevert(abi.encodeWithSelector(NonExistingLoan.selector)); - loan.repayLOAN(loanId, loanAssetPermit); + loan.repayLOAN(loanId, creditPermit); } function test_shouldFail_whenLoanIsNotRunning() external { @@ -1381,14 +1381,14 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { _mockLOAN(loanId, simpleLoan); vm.expectRevert(abi.encodeWithSelector(InvalidLoanStatus.selector, 3)); - loan.repayLOAN(loanId, loanAssetPermit); + loan.repayLOAN(loanId, creditPermit); } function test_shouldFail_whenLoanIsDefaulted() external { vm.warp(simpleLoan.defaultTimestamp); vm.expectRevert(abi.encodeWithSelector(LoanDefaulted.selector, simpleLoan.defaultTimestamp)); - loan.repayLOAN(loanId, loanAssetPermit); + loan.repayLOAN(loanId, creditPermit); } function testFuzz_shouldCallPermit_whenProvided( @@ -1409,10 +1409,8 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { uint256 loanRepaymentAmount = loan.loanRepaymentAmount(loanId); fungibleAsset.mint(borrower, loanRepaymentAmount); - loanAssetPermit = abi.encodePacked(uint256(1), uint256(2), uint256(3), uint8(4)); - vm.expectCall( - simpleLoan.loanAssetAddress, + simpleLoan.creditAddress, abi.encodeWithSignature( "permit(address,address,uint256,uint256,uint8,bytes32,bytes32)", borrower, address(loan), loanRepaymentAmount, 1, uint8(4), uint256(2), uint256(3) @@ -1420,11 +1418,11 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { ); vm.prank(borrower); - loan.repayLOAN(loanId, loanAssetPermit); + loan.repayLOAN(loanId, creditPermit); } function test_shouldDeleteLoanData_whenLOANOwnerIsOriginalLender() external { - loan.repayLOAN(loanId, loanAssetPermit); + loan.repayLOAN(loanId, creditPermit); _assertLOANEq(loanId, nonExistingLoan); } @@ -1432,7 +1430,7 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { function test_shouldBurnLOANToken_whenLOANOwnerIsOriginalLender() external { vm.expectCall(loanToken, abi.encodeWithSignature("burn(uint256)", loanId)); - loan.repayLOAN(loanId, loanAssetPermit); + loan.repayLOAN(loanId, creditPermit); } function testFuzz_shouldTransferRepaidAmountToLender_whenLOANOwnerIsOriginalLender( @@ -1454,14 +1452,14 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { fungibleAsset.mint(borrower, loanRepaymentAmount); vm.expectCall( - simpleLoan.loanAssetAddress, + simpleLoan.creditAddress, abi.encodeWithSignature( "transferFrom(address,address,uint256)", borrower, lender, loanRepaymentAmount ) ); vm.prank(borrower); - loan.repayLOAN(loanId, loanAssetPermit); + loan.repayLOAN(loanId, creditPermit); } function testFuzz_shouldUpdateLoanData_whenLOANOwnerIsNotOriginalLender( @@ -1485,7 +1483,7 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { fungibleAsset.mint(borrower, loanRepaymentAmount); vm.prank(borrower); - loan.repayLOAN(loanId, loanAssetPermit); + loan.repayLOAN(loanId, creditPermit); // Update loan and compare simpleLoan.status = 3; // move loan to repaid state @@ -1515,14 +1513,14 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { fungibleAsset.mint(borrower, loanRepaymentAmount); vm.expectCall( - simpleLoan.loanAssetAddress, + simpleLoan.creditAddress, abi.encodeWithSignature( "transferFrom(address,address,uint256)", borrower, address(loan), loanRepaymentAmount ) ); vm.prank(borrower); - loan.repayLOAN(loanId, loanAssetPermit); + loan.repayLOAN(loanId, creditPermit); } function test_shouldTransferCollateralToBorrower() external { @@ -1534,21 +1532,21 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { ) ); - loan.repayLOAN(loanId, loanAssetPermit); + loan.repayLOAN(loanId, creditPermit); } function test_shouldEmitEvent_LOANPaidBack() external { vm.expectEmit(); emit LOANPaidBack(loanId); - loan.repayLOAN(loanId, loanAssetPermit); + loan.repayLOAN(loanId, creditPermit); } function test_shouldEmitEvent_LOANClaimed_whenLOANOwnerIsOriginalLender() external { vm.expectEmit(); emit LOANClaimed(loanId, false); - loan.repayLOAN(loanId, loanAssetPermit); + loan.repayLOAN(loanId, creditPermit); } } @@ -1698,7 +1696,7 @@ contract PWNSimpleLoan_ClaimLOAN_Test is PWNSimpleLoanTest { fungibleAsset.mint(address(loan), loanRepaymentAmount); vm.expectCall( - simpleLoan.loanAssetAddress, + simpleLoan.creditAddress, abi.encodeWithSignature("transfer(address,uint256)", lender, loanRepaymentAmount) ); @@ -1965,7 +1963,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { loan.extendLOAN(extension, "", ""); } - function testFuzz_shouldTransferLoanAsset_whenPriceMoreThanZero(uint256 price) external { + function testFuzz_shouldTransferCredit_whenPriceMoreThanZero(uint256 price) external { price = bound(price, 1, 1e40); extension.price = price; @@ -1973,7 +1971,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { fungibleAsset.mint(borrower, price); vm.expectCall( - simpleLoan.loanAssetAddress, + simpleLoan.creditAddress, abi.encodeWithSignature("transferFrom(address,address,uint256)", borrower, lender, price) ); @@ -1981,12 +1979,12 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { loan.extendLOAN(extension, "", ""); } - function test_shouldNotTransferLoanAsset_whenPriceZero() external { + function test_shouldNotTransferCredit_whenPriceZero() external { extension.price = 0; _mockExtensionOfferMade(extension); vm.expectCall({ - callee: simpleLoan.loanAssetAddress, + callee: simpleLoan.creditAddress, data: abi.encodeWithSignature("transferFrom(address,address,uint256)", borrower, lender, 0), count: 0 }); @@ -2001,10 +1999,9 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { extension.price = price; _mockExtensionOfferMade(extension); fungibleAsset.mint(borrower, price); - loanAssetPermit = abi.encodePacked(uint256(1), uint256(2), uint256(3), uint8(4)); vm.expectCall( - simpleLoan.loanAssetAddress, + simpleLoan.creditAddress, abi.encodeWithSignature( "permit(address,address,uint256,uint256,uint8,bytes32,bytes32)", borrower, address(loan), price, 1, uint8(4), uint256(2), uint256(3) @@ -2012,7 +2009,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { ); vm.prank(lender); - loan.extendLOAN(extension, "", loanAssetPermit); + loan.extendLOAN(extension, "", creditPermit); } function test_shouldPass_whenBorrowerSignature_whenLenderAccepts() external { @@ -2058,7 +2055,7 @@ contract PWNSimpleLoan_GetLOAN_Test is PWNSimpleLoanTest { address _originalLender, uint40 _accruingInterestDailyRate, uint256 _fixedInterestAmount, - address _loanAssetAddress, + address _creditAddress, uint256 _principalAmount, uint8 _collateralCategory, address _collateralAssetAddress, @@ -2076,7 +2073,7 @@ contract PWNSimpleLoan_GetLOAN_Test is PWNSimpleLoanTest { simpleLoan.originalLender = _originalLender; simpleLoan.accruingInterestDailyRate = _accruingInterestDailyRate; simpleLoan.fixedInterestAmount = _fixedInterestAmount; - simpleLoan.loanAssetAddress = _loanAssetAddress; + simpleLoan.creditAddress = _creditAddress; simpleLoan.principalAmount = _principalAmount; simpleLoan.collateral.category = MultiToken.Category(_collateralCategory % 4); simpleLoan.collateral.assetAddress = _collateralAssetAddress; @@ -2112,9 +2109,9 @@ contract PWNSimpleLoan_GetLOAN_Test is PWNSimpleLoanTest { assertEq(fixedInterestAmount, _fixedInterestAmount); } { - (,,,,,,,,MultiToken.Asset memory loanAsset,,) = loan.getLOAN(loanId); - assertEq(loanAsset.assetAddress, _loanAssetAddress); - assertEq(loanAsset.amount, _principalAmount); + (,,,,,,,,MultiToken.Asset memory credit,,) = loan.getLOAN(loanId); + assertEq(credit.assetAddress, _creditAddress); + assertEq(credit.amount, _principalAmount); } { (,,,,,,,,,MultiToken.Asset memory collateral,) = loan.getLOAN(loanId); @@ -2185,7 +2182,7 @@ contract PWNSimpleLoan_GetLOAN_Test is PWNSimpleLoanTest { address loanOwner, uint40 accruingInterestDailyRate, uint256 fixedInterestAmount, - MultiToken.Asset memory loanAsset, + MultiToken.Asset memory credit, MultiToken.Asset memory collateral, uint256 repaymentAmount ) = loan.getLOAN(nonExistingLoanId); @@ -2198,8 +2195,8 @@ contract PWNSimpleLoan_GetLOAN_Test is PWNSimpleLoanTest { assertEq(loanOwner, address(0)); assertEq(accruingInterestDailyRate, 0); assertEq(fixedInterestAmount, 0); - assertEq(loanAsset.assetAddress, address(0)); - assertEq(loanAsset.amount, 0); + assertEq(credit.assetAddress, address(0)); + assertEq(credit.amount, 0); assertEq(collateral.assetAddress, address(0)); assertEq(uint8(collateral.category), 0); assertEq(collateral.id, 0); diff --git a/test/unit/PWNSimpleLoanListOffer.t.sol b/test/unit/PWNSimpleLoanListOffer.t.sol index 88cd548..45aa96b 100644 --- a/test/unit/PWNSimpleLoanListOffer.t.sol +++ b/test/unit/PWNSimpleLoanListOffer.t.sol @@ -47,8 +47,8 @@ abstract contract PWNSimpleLoanListOfferTest is Test { collateralAmount: 1032, checkCollateralStateFingerprint: true, collateralStateFingerprint: keccak256("some state fingerprint"), - loanAssetAddress: token, - loanAmount: 1101001, + creditAddress: token, + creditAmount: 1101001, availableCreditLimit: 0, fixedInterestAmount: 1, accruingInterestAPR: 0, @@ -111,7 +111,7 @@ abstract contract PWNSimpleLoanListOfferTest is Test { address(offerContract) )), keccak256(abi.encodePacked( - keccak256("Offer(uint8 collateralCategory,address collateralAddress,bytes32 collateralIdsWhitelistMerkleRoot,uint256 collateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address loanAssetAddress,uint256 loanAmount,uint256 availableCreditLimit,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedBorrower,address lender,uint256 refinancingLoanId,uint256 nonceSpace,uint256 nonce,address loanContract)"), + 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) )) )); @@ -426,30 +426,30 @@ contract PWNSimpleLoanListOffer_AcceptOffer_Test is PWNSimpleLoanListOfferTest { } function testFuzz_shouldFail_whenUsedCreditExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { - used = bound(used, 1, type(uint256).max - offer.loanAmount); - limit = bound(limit, used, used + offer.loanAmount - 1); + 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.loanAmount, limit)); + vm.expectRevert(abi.encodeWithSelector(AvailableCreditLimitExceeded.selector, used + offer.creditAmount, limit)); offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", ""); } function testFuzz_shouldIncreaseUsedCredit_whenUsedCreditNotExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { - used = bound(used, 1, type(uint256).max - offer.loanAmount); - limit = bound(limit, used + offer.loanAmount, type(uint256).max); + 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), "", ""); - assertEq(offerContract.creditUsed(_offerHash(offer)), used + offer.loanAmount); + assertEq(offerContract.creditUsed(_offerHash(offer)), used + offer.creditAmount); } function test_shouldCallLoanContractWithLoanTerms() external { - bytes memory loanAssetPermit = "loanAssetPermit"; + bytes memory creditPermit = "creditPermit"; bytes memory collateralPermit = "collateralPermit"; PWNSimpleLoan.Terms memory loanTerms = PWNSimpleLoan.Terms({ @@ -462,11 +462,11 @@ contract PWNSimpleLoanListOffer_AcceptOffer_Test is PWNSimpleLoanListOfferTest { id: offerValues.collateralId, amount: offer.collateralAmount }), - asset: MultiToken.Asset({ + credit: MultiToken.Asset({ category: MultiToken.Category.ERC20, - assetAddress: offer.loanAssetAddress, + assetAddress: offer.creditAddress, id: 0, - amount: offer.loanAmount + amount: offer.creditAmount }), fixedInterestAmount: offer.fixedInterestAmount, accruingInterestAPR: offer.accruingInterestAPR @@ -476,12 +476,12 @@ contract PWNSimpleLoanListOffer_AcceptOffer_Test is PWNSimpleLoanListOfferTest { activeLoanContract, abi.encodeWithSelector( PWNSimpleLoan.createLOAN.selector, - _offerHash(offer), loanTerms, loanAssetPermit, collateralPermit + _offerHash(offer), loanTerms, creditPermit, collateralPermit ) ); vm.prank(borrower); - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), loanAssetPermit, collateralPermit); + offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), creditPermit, collateralPermit); } function test_shouldReturnNewLoanId() external { @@ -513,7 +513,7 @@ contract PWNSimpleLoanListOffer_AcceptOfferAndRevokeCallersNonce_Test is PWNSimp offer: offer, offerValues: offerValues, signature: _signOffer(lenderPK, offer), - loanAssetPermit: "", + creditPermit: "", collateralPermit: "", callersNonceSpace: nonceSpace, callersNonceToRevoke: nonce @@ -531,7 +531,7 @@ contract PWNSimpleLoanListOffer_AcceptOfferAndRevokeCallersNonce_Test is PWNSimp offer: offer, offerValues: offerValues, signature: _signOffer(lenderPK, offer), - loanAssetPermit: "", + creditPermit: "", collateralPermit: "", callersNonceSpace: nonceSpace, callersNonceToRevoke: nonce @@ -544,7 +544,7 @@ contract PWNSimpleLoanListOffer_AcceptOfferAndRevokeCallersNonce_Test is PWNSimp offer: offer, offerValues: offerValues, signature: _signOffer(lenderPK, offer), - loanAssetPermit: "", + creditPermit: "", collateralPermit: "", callersNonceSpace: 1, callersNonceToRevoke: 2 @@ -772,30 +772,30 @@ contract PWNSimpleLoanListOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanListOf } function testFuzz_shouldFail_whenUsedCreditExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { - used = bound(used, 1, type(uint256).max - offer.loanAmount); - limit = bound(limit, used, used + offer.loanAmount - 1); + 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.loanAmount, limit)); + vm.expectRevert(abi.encodeWithSelector(AvailableCreditLimitExceeded.selector, used + offer.creditAmount, limit)); offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", ""); } function testFuzz_shouldIncreaseUsedCredit_whenUsedCreditNotExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { - used = bound(used, 1, type(uint256).max - offer.loanAmount); - limit = bound(limit, used + offer.loanAmount, type(uint256).max); + 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), "", ""); - assertEq(offerContract.creditUsed(_offerHash(offer)), used + offer.loanAmount); + assertEq(offerContract.creditUsed(_offerHash(offer)), used + offer.creditAmount); } function test_shouldCallLoanContract() external { - bytes memory loanAssetPermit = "loanAssetPermit"; + bytes memory creditPermit = "creditPermit"; bytes memory collateralPermit = "collateralPermit"; PWNSimpleLoan.Terms memory loanTerms = PWNSimpleLoan.Terms({ @@ -808,11 +808,11 @@ contract PWNSimpleLoanListOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanListOf id: offerValues.collateralId, amount: offer.collateralAmount }), - asset: MultiToken.Asset({ + credit: MultiToken.Asset({ category: MultiToken.Category.ERC20, - assetAddress: offer.loanAssetAddress, + assetAddress: offer.creditAddress, id: 0, - amount: offer.loanAmount + amount: offer.creditAmount }), fixedInterestAmount: offer.fixedInterestAmount, accruingInterestAPR: offer.accruingInterestAPR @@ -822,13 +822,13 @@ contract PWNSimpleLoanListOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanListOf activeLoanContract, abi.encodeWithSelector( PWNSimpleLoan.refinanceLOAN.selector, - loanId, _offerHash(offer), loanTerms, loanAssetPermit, collateralPermit + loanId, _offerHash(offer), loanTerms, creditPermit, collateralPermit ) ); vm.prank(lender); offerContract.acceptRefinanceOffer( - loanId, offer, offerValues, _signOffer(lenderPK, offer), loanAssetPermit, collateralPermit + loanId, offer, offerValues, _signOffer(lenderPK, offer), creditPermit, collateralPermit ); } @@ -862,8 +862,8 @@ contract PWNSimpleLoanListOffer_AcceptRefinanceOfferAndRevokeCallersNonce_Test i offer: offer, offerValues: offerValues, signature: _signOffer(lenderPK, offer), - lenderLoanAssetPermit: "", - borrowerLoanAssetPermit: "", + lenderCreditPermit: "", + borrowerCreditPermit: "", callersNonceSpace: nonceSpace, callersNonceToRevoke: nonce }); @@ -881,8 +881,8 @@ contract PWNSimpleLoanListOffer_AcceptRefinanceOfferAndRevokeCallersNonce_Test i offer: offer, offerValues: offerValues, signature: _signOffer(lenderPK, offer), - lenderLoanAssetPermit: "", - borrowerLoanAssetPermit: "", + lenderCreditPermit: "", + borrowerCreditPermit: "", callersNonceSpace: nonceSpace, callersNonceToRevoke: nonce }); @@ -895,8 +895,8 @@ contract PWNSimpleLoanListOffer_AcceptRefinanceOfferAndRevokeCallersNonce_Test i offer: offer, offerValues: offerValues, signature: _signOffer(lenderPK, offer), - lenderLoanAssetPermit: "", - borrowerLoanAssetPermit: "", + lenderCreditPermit: "", + borrowerCreditPermit: "", callersNonceSpace: 1, callersNonceToRevoke: 2 }); diff --git a/test/unit/PWNSimpleLoanSimpleOffer.t.sol b/test/unit/PWNSimpleLoanSimpleOffer.t.sol index 9a6a7a6..7cdb6f3 100644 --- a/test/unit/PWNSimpleLoanSimpleOffer.t.sol +++ b/test/unit/PWNSimpleLoanSimpleOffer.t.sol @@ -46,8 +46,8 @@ abstract contract PWNSimpleLoanSimpleOfferTest is Test { collateralAmount: 1032, checkCollateralStateFingerprint: true, collateralStateFingerprint: keccak256("some state fingerprint"), - loanAssetAddress: token, - loanAmount: 1101001, + creditAddress: token, + creditAmount: 1101001, availableCreditLimit: 0, fixedInterestAmount: 1, accruingInterestAPR: 0, @@ -105,7 +105,7 @@ abstract contract PWNSimpleLoanSimpleOfferTest is Test { address(offerContract) )), keccak256(abi.encodePacked( - keccak256("Offer(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address loanAssetAddress,uint256 loanAmount,uint256 availableCreditLimit,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedBorrower,address lender,uint256 refinancingLoanId,uint256 nonceSpace,uint256 nonce,address loanContract)"), + keccak256("Offer(uint8 collateralCategory,address collateralAddress,uint256 collateralId,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) )) )); @@ -388,30 +388,30 @@ contract PWNSimpleLoanSimpleOffer_AcceptOffer_Test is PWNSimpleLoanSimpleOfferTe } function testFuzz_shouldFail_whenUsedCreditExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { - used = bound(used, 1, type(uint256).max - offer.loanAmount); - limit = bound(limit, used, used + offer.loanAmount - 1); + 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.loanAmount, limit)); + vm.expectRevert(abi.encodeWithSelector(AvailableCreditLimitExceeded.selector, used + offer.creditAmount, limit)); offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", ""); } function testFuzz_shouldIncreaseUsedCredit_whenUsedCreditNotExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { - used = bound(used, 1, type(uint256).max - offer.loanAmount); - limit = bound(limit, used + offer.loanAmount, type(uint256).max); + 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, _signOffer(lenderPK, offer), "", ""); - assertEq(offerContract.creditUsed(_offerHash(offer)), used + offer.loanAmount); + assertEq(offerContract.creditUsed(_offerHash(offer)), used + offer.creditAmount); } function test_shouldCallLoanContractWithLoanTerms() external { - bytes memory loanAssetPermit = "loanAssetPermit"; + bytes memory creditPermit = "creditPermit"; bytes memory collateralPermit = "collateralPermit"; PWNSimpleLoan.Terms memory loanTerms = PWNSimpleLoan.Terms({ @@ -424,11 +424,11 @@ contract PWNSimpleLoanSimpleOffer_AcceptOffer_Test is PWNSimpleLoanSimpleOfferTe id: offer.collateralId, amount: offer.collateralAmount }), - asset: MultiToken.Asset({ + credit: MultiToken.Asset({ category: MultiToken.Category.ERC20, - assetAddress: offer.loanAssetAddress, + assetAddress: offer.creditAddress, id: 0, - amount: offer.loanAmount + amount: offer.creditAmount }), fixedInterestAmount: offer.fixedInterestAmount, accruingInterestAPR: offer.accruingInterestAPR @@ -438,12 +438,12 @@ contract PWNSimpleLoanSimpleOffer_AcceptOffer_Test is PWNSimpleLoanSimpleOfferTe activeLoanContract, abi.encodeWithSelector( PWNSimpleLoan.createLOAN.selector, - _offerHash(offer), loanTerms, loanAssetPermit, collateralPermit + _offerHash(offer), loanTerms, creditPermit, collateralPermit ) ); vm.prank(borrower); - offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), loanAssetPermit, collateralPermit); + offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), creditPermit, collateralPermit); } function test_shouldReturnNewLoanId() external { @@ -474,7 +474,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptOfferAndRevokeCallersNonce_Test is PWNSi offerContract.acceptOffer({ offer: offer, signature: _signOffer(lenderPK, offer), - loanAssetPermit: "", + creditPermit: "", collateralPermit: "", callersNonceSpace: nonceSpace, callersNonceToRevoke: nonce @@ -491,7 +491,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptOfferAndRevokeCallersNonce_Test is PWNSi offerContract.acceptOffer({ offer: offer, signature: _signOffer(lenderPK, offer), - loanAssetPermit: "", + creditPermit: "", collateralPermit: "", callersNonceSpace: nonceSpace, callersNonceToRevoke: nonce @@ -503,7 +503,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptOfferAndRevokeCallersNonce_Test is PWNSi uint256 newLoanId = offerContract.acceptOffer({ offer: offer, signature: _signOffer(lenderPK, offer), - loanAssetPermit: "", + creditPermit: "", collateralPermit: "", callersNonceSpace: 1, callersNonceToRevoke: 2 @@ -699,30 +699,30 @@ contract PWNSimpleLoanSimpleOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanSimp } function testFuzz_shouldFail_whenUsedCreditExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { - used = bound(used, 1, type(uint256).max - offer.loanAmount); - limit = bound(limit, used, used + offer.loanAmount - 1); + 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.loanAmount, limit)); + vm.expectRevert(abi.encodeWithSelector(AvailableCreditLimitExceeded.selector, used + offer.creditAmount, limit)); offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), "", ""); } function testFuzz_shouldIncreaseUsedCredit_whenUsedCreditNotExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { - used = bound(used, 1, type(uint256).max - offer.loanAmount); - limit = bound(limit, used + offer.loanAmount, type(uint256).max); + 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, _signOffer(lenderPK, offer), "", ""); - assertEq(offerContract.creditUsed(_offerHash(offer)), used + offer.loanAmount); + assertEq(offerContract.creditUsed(_offerHash(offer)), used + offer.creditAmount); } function test_shouldCallLoanContract() external { - bytes memory loanAssetPermit = "loanAssetPermit"; + bytes memory creditPermit = "creditPermit"; bytes memory collateralPermit = "collateralPermit"; PWNSimpleLoan.Terms memory loanTerms = PWNSimpleLoan.Terms({ @@ -735,11 +735,11 @@ contract PWNSimpleLoanSimpleOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanSimp id: offer.collateralId, amount: offer.collateralAmount }), - asset: MultiToken.Asset({ + credit: MultiToken.Asset({ category: MultiToken.Category.ERC20, - assetAddress: offer.loanAssetAddress, + assetAddress: offer.creditAddress, id: 0, - amount: offer.loanAmount + amount: offer.creditAmount }), fixedInterestAmount: offer.fixedInterestAmount, accruingInterestAPR: offer.accruingInterestAPR @@ -749,13 +749,13 @@ contract PWNSimpleLoanSimpleOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanSimp activeLoanContract, abi.encodeWithSelector( PWNSimpleLoan.refinanceLOAN.selector, - loanId, _offerHash(offer), loanTerms, loanAssetPermit, collateralPermit + loanId, _offerHash(offer), loanTerms, creditPermit, collateralPermit ) ); vm.prank(lender); offerContract.acceptRefinanceOffer( - loanId, offer, _signOffer(lenderPK, offer), loanAssetPermit, collateralPermit + loanId, offer, _signOffer(lenderPK, offer), creditPermit, collateralPermit ); } @@ -788,8 +788,8 @@ contract PWNSimpleLoanSimpleOffer_AcceptRefinanceOfferAndRevokeCallersNonce_Test loanId: loanId, offer: offer, signature: _signOffer(lenderPK, offer), - lenderLoanAssetPermit: "", - borrowerLoanAssetPermit: "", + lenderCreditPermit: "", + borrowerCreditPermit: "", callersNonceSpace: nonceSpace, callersNonceToRevoke: nonce }); @@ -806,8 +806,8 @@ contract PWNSimpleLoanSimpleOffer_AcceptRefinanceOfferAndRevokeCallersNonce_Test loanId: loanId, offer: offer, signature: _signOffer(lenderPK, offer), - lenderLoanAssetPermit: "", - borrowerLoanAssetPermit: "", + lenderCreditPermit: "", + borrowerCreditPermit: "", callersNonceSpace: nonceSpace, callersNonceToRevoke: nonce }); @@ -819,8 +819,8 @@ contract PWNSimpleLoanSimpleOffer_AcceptRefinanceOfferAndRevokeCallersNonce_Test loanId: loanId, offer: offer, signature: _signOffer(lenderPK, offer), - lenderLoanAssetPermit: "", - borrowerLoanAssetPermit: "", + lenderCreditPermit: "", + borrowerCreditPermit: "", callersNonceSpace: 1, callersNonceToRevoke: 2 }); diff --git a/test/unit/PWNSimpleLoanSimpleRequest.t.sol b/test/unit/PWNSimpleLoanSimpleRequest.t.sol index 9e4ec14..c7bfe50 100644 --- a/test/unit/PWNSimpleLoanSimpleRequest.t.sol +++ b/test/unit/PWNSimpleLoanSimpleRequest.t.sol @@ -46,8 +46,8 @@ abstract contract PWNSimpleLoanSimpleRequestTest is Test { collateralAmount: 1032, checkCollateralStateFingerprint: true, collateralStateFingerprint: keccak256("some state fingerprint"), - loanAssetAddress: token, - loanAmount: 1101001, + creditAddress: token, + creditAmount: 1101001, availableCreditLimit: 0, fixedInterestAmount: 1, accruingInterestAPR: 0, @@ -105,7 +105,7 @@ abstract contract PWNSimpleLoanSimpleRequestTest is Test { address(requestContract) )), keccak256(abi.encodePacked( - keccak256("Request(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address loanAssetAddress,uint256 loanAmount,uint256 availableCreditLimit,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedLender,address borrower,uint256 refinancingLoanId,uint256 nonceSpace,uint256 nonce,address loanContract)"), + keccak256("Request(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address creditAddress,uint256 creditAmount,uint256 availableCreditLimit,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedLender,address borrower,uint256 refinancingLoanId,uint256 nonceSpace,uint256 nonce,address loanContract)"), abi.encode(_request) )) )); @@ -388,30 +388,30 @@ contract PWNSimpleLoanSimpleRequest_AcceptRequest_Test is PWNSimpleLoanSimpleReq } function testFuzz_shouldFail_whenUsedCreditExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { - used = bound(used, 1, type(uint256).max - request.loanAmount); - limit = bound(limit, used, used + request.loanAmount - 1); + used = bound(used, 1, type(uint256).max - request.creditAmount); + limit = bound(limit, used, used + request.creditAmount - 1); request.availableCreditLimit = limit; vm.store(address(requestContract), keccak256(abi.encode(_requestHash(request), CREDIT_USED_SLOT)), bytes32(used)); - vm.expectRevert(abi.encodeWithSelector(AvailableCreditLimitExceeded.selector, used + request.loanAmount, limit)); + vm.expectRevert(abi.encodeWithSelector(AvailableCreditLimitExceeded.selector, used + request.creditAmount, limit)); requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", ""); } function testFuzz_shouldIncreaseUsedCredit_whenUsedCreditNotExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { - used = bound(used, 1, type(uint256).max - request.loanAmount); - limit = bound(limit, used + request.loanAmount, type(uint256).max); + used = bound(used, 1, type(uint256).max - request.creditAmount); + limit = bound(limit, used + request.creditAmount, type(uint256).max); request.availableCreditLimit = limit; vm.store(address(requestContract), keccak256(abi.encode(_requestHash(request), CREDIT_USED_SLOT)), bytes32(used)); requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", ""); - assertEq(requestContract.creditUsed(_requestHash(request)), used + request.loanAmount); + assertEq(requestContract.creditUsed(_requestHash(request)), used + request.creditAmount); } function test_shouldCallLoanContractWithLoanTerms() external { - bytes memory loanAssetPermit = "loanAssetPermit"; + bytes memory creditPermit = "creditPermit"; bytes memory collateralPermit = "collateralPermit"; PWNSimpleLoan.Terms memory loanTerms = PWNSimpleLoan.Terms({ @@ -424,11 +424,11 @@ contract PWNSimpleLoanSimpleRequest_AcceptRequest_Test is PWNSimpleLoanSimpleReq id: request.collateralId, amount: request.collateralAmount }), - asset: MultiToken.Asset({ + credit: MultiToken.Asset({ category: MultiToken.Category.ERC20, - assetAddress: request.loanAssetAddress, + assetAddress: request.creditAddress, id: 0, - amount: request.loanAmount + amount: request.creditAmount }), fixedInterestAmount: request.fixedInterestAmount, accruingInterestAPR: request.accruingInterestAPR @@ -438,12 +438,12 @@ contract PWNSimpleLoanSimpleRequest_AcceptRequest_Test is PWNSimpleLoanSimpleReq activeLoanContract, abi.encodeWithSelector( PWNSimpleLoan.createLOAN.selector, - _requestHash(request), loanTerms, loanAssetPermit, collateralPermit + _requestHash(request), loanTerms, creditPermit, collateralPermit ) ); vm.prank(lender); - requestContract.acceptRequest(request, _signRequest(borrowerPK, request), loanAssetPermit, collateralPermit); + requestContract.acceptRequest(request, _signRequest(borrowerPK, request), creditPermit, collateralPermit); } function test_shouldReturnNewLoanId() external { @@ -474,7 +474,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRequestAndRevokeCallersNonce_Test is P requestContract.acceptRequest({ request: request, signature: _signRequest(borrowerPK, request), - loanAssetPermit: "", + creditPermit: "", collateralPermit: "", callersNonceSpace: nonceSpace, callersNonceToRevoke: nonce @@ -491,7 +491,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRequestAndRevokeCallersNonce_Test is P requestContract.acceptRequest({ request: request, signature: _signRequest(borrowerPK, request), - loanAssetPermit: "", + creditPermit: "", collateralPermit: "", callersNonceSpace: nonceSpace, callersNonceToRevoke: nonce @@ -503,7 +503,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRequestAndRevokeCallersNonce_Test is P uint256 newLoanId = requestContract.acceptRequest({ request: request, signature: _signRequest(borrowerPK, request), - loanAssetPermit: "", + creditPermit: "", collateralPermit: "", callersNonceSpace: 1, callersNonceToRevoke: 2 @@ -710,30 +710,30 @@ contract PWNSimpleLoanSimpleRequest_AcceptRefinanceRequest_Test is PWNSimpleLoan } function testFuzz_shouldFail_whenUsedCreditExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { - used = bound(used, 1, type(uint256).max - request.loanAmount); - limit = bound(limit, used, used + request.loanAmount - 1); + used = bound(used, 1, type(uint256).max - request.creditAmount); + limit = bound(limit, used, used + request.creditAmount - 1); request.availableCreditLimit = limit; vm.store(address(requestContract), keccak256(abi.encode(_requestHash(request), CREDIT_USED_SLOT)), bytes32(used)); - vm.expectRevert(abi.encodeWithSelector(AvailableCreditLimitExceeded.selector, used + request.loanAmount, limit)); + vm.expectRevert(abi.encodeWithSelector(AvailableCreditLimitExceeded.selector, used + request.creditAmount, limit)); requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", ""); } function testFuzz_shouldIncreaseUsedCredit_whenUsedCreditNotExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { - used = bound(used, 1, type(uint256).max - request.loanAmount); - limit = bound(limit, used + request.loanAmount, type(uint256).max); + used = bound(used, 1, type(uint256).max - request.creditAmount); + limit = bound(limit, used + request.creditAmount, type(uint256).max); request.availableCreditLimit = limit; vm.store(address(requestContract), keccak256(abi.encode(_requestHash(request), CREDIT_USED_SLOT)), bytes32(used)); requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", ""); - assertEq(requestContract.creditUsed(_requestHash(request)), used + request.loanAmount); + assertEq(requestContract.creditUsed(_requestHash(request)), used + request.creditAmount); } function test_shouldCallLoanContract() external { - bytes memory loanAssetPermit = "loanAssetPermit"; + bytes memory creditPermit = "creditPermit"; bytes memory collateralPermit = "collateralPermit"; PWNSimpleLoan.Terms memory loanTerms = PWNSimpleLoan.Terms({ @@ -746,11 +746,11 @@ contract PWNSimpleLoanSimpleRequest_AcceptRefinanceRequest_Test is PWNSimpleLoan id: request.collateralId, amount: request.collateralAmount }), - asset: MultiToken.Asset({ + credit: MultiToken.Asset({ category: MultiToken.Category.ERC20, - assetAddress: request.loanAssetAddress, + assetAddress: request.creditAddress, id: 0, - amount: request.loanAmount + amount: request.creditAmount }), fixedInterestAmount: request.fixedInterestAmount, accruingInterestAPR: request.accruingInterestAPR @@ -760,13 +760,13 @@ contract PWNSimpleLoanSimpleRequest_AcceptRefinanceRequest_Test is PWNSimpleLoan activeLoanContract, abi.encodeWithSelector( PWNSimpleLoan.refinanceLOAN.selector, - loanId, _requestHash(request), loanTerms, loanAssetPermit, collateralPermit + loanId, _requestHash(request), loanTerms, creditPermit, collateralPermit ) ); vm.prank(lender); requestContract.acceptRefinanceRequest( - loanId, request, _signRequest(borrowerPK, request), loanAssetPermit, collateralPermit + loanId, request, _signRequest(borrowerPK, request), creditPermit, collateralPermit ); } @@ -805,8 +805,8 @@ contract PWNSimpleLoanSimpleRequest_AcceptRefinanceRequestAndRevokeCallersNonce_ loanId: loanId, request: request, signature: _signRequest(borrowerPK, request), - lenderLoanAssetPermit: "", - borrowerLoanAssetPermit: "", + lenderCreditPermit: "", + borrowerCreditPermit: "", callersNonceSpace: nonceSpace, callersNonceToRevoke: nonce }); @@ -823,8 +823,8 @@ contract PWNSimpleLoanSimpleRequest_AcceptRefinanceRequestAndRevokeCallersNonce_ loanId: loanId, request: request, signature: _signRequest(borrowerPK, request), - lenderLoanAssetPermit: "", - borrowerLoanAssetPermit: "", + lenderCreditPermit: "", + borrowerCreditPermit: "", callersNonceSpace: nonceSpace, callersNonceToRevoke: nonce }); @@ -836,8 +836,8 @@ contract PWNSimpleLoanSimpleRequest_AcceptRefinanceRequestAndRevokeCallersNonce_ loanId: loanId, request: request, signature: _signRequest(borrowerPK, request), - lenderLoanAssetPermit: "", - borrowerLoanAssetPermit: "", + lenderCreditPermit: "", + borrowerCreditPermit: "", callersNonceSpace: 1, callersNonceToRevoke: 2 }); From 6dba9de3bda1f9307544e984292ca6a700f4dce0 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Wed, 13 Mar 2024 16:33:30 +0100 Subject: [PATCH 043/129] feat(loan-extension): enable using any fungible token as lenders extension compensation --- src/loan/terms/simple/loan/PWNSimpleLoan.sol | 87 ++++++---- test/unit/PWNSimpleLoan.t.sol | 170 +++++++++++++------ 2 files changed, 167 insertions(+), 90 deletions(-) diff --git a/src/loan/terms/simple/loan/PWNSimpleLoan.sol b/src/loan/terms/simple/loan/PWNSimpleLoan.sol index 355e1d2..bd4652f 100644 --- a/src/loan/terms/simple/loan/PWNSimpleLoan.sol +++ b/src/loan/terms/simple/loan/PWNSimpleLoan.sol @@ -42,8 +42,8 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { uint256 public constant MAX_EXTENSION_DURATION = 90 days; uint256 public constant MIN_EXTENSION_DURATION = 1 days; - bytes32 public constant EXTENSION_TYPEHASH = keccak256( - "Extension(uint256 loanId,uint256 price,uint40 duration,uint40 expiration,address proposer,uint256 nonceSpace,uint256 nonce)" + bytes32 public constant EXTENSION_PROPOSAL_TYPEHASH = keccak256( + "ExtensionProposal(uint256 loanId,address compensationAddress,uint256 compensationAmount,uint40 duration,uint40 expiration,address proposer,uint256 nonceSpace,uint256 nonce)" ); bytes32 public immutable DOMAIN_SEPARATOR = keccak256(abi.encode( @@ -115,18 +115,20 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { mapping (uint256 => LOAN) private LOANs; /** - * @notice Struct defining a loan extension offer. Offer can be signed by a borrower or a lender. + * @notice Struct defining a loan extension proposal that can be signed by a borrower or a lender. * @param loanId Id of a loan to be extended. - * @param price Price of the extension in credit asset tokens. + * @param compensationAddress Address of a compensation asset. + * @param compensationAmount Amount of a compensation asset that a borrower has to pay to a lender. * @param duration Duration of the extension in seconds. * @param expiration Unix timestamp (in seconds) of an expiration date. - * @param proposer Address of a proposer that signed the extension offer. - * @param nonceSpace Nonce space of the extension offer nonce. - * @param nonce Nonce of the extension offer. + * @param proposer Address of a proposer that signed the extension proposal. + * @param nonceSpace Nonce space of the extension proposal nonce. + * @param nonce Nonce of the extension proposal. */ - struct Extension { + struct ExtensionProposal { uint256 loanId; - uint256 price; + address compensationAddress; + uint256 compensationAmount; uint40 duration; uint40 expiration; address proposer; @@ -135,9 +137,9 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { } /** - * Mapping of extension offers made via on-chain transaction by extension hash. + * Mapping of extension proposals made via on-chain transaction by extension hash. */ - mapping (bytes32 => bool) public extensionOffersMade; + mapping (bytes32 => bool) public extensionProposalsMade; /*----------------------------------------------------------*| |* # EVENTS & ERRORS DEFINITIONS *| @@ -169,9 +171,9 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { event LOANExtended(uint256 indexed loanId, uint40 originalDefaultTimestamp, uint40 extendedDefaultTimestamp); /** - * @dev Emitted when a loan extension offer is made. + * @dev Emitted when a loan extension proposal is made. */ - event ExtensionOfferMade(bytes32 indexed extensionHash, address indexed proposer, Extension extension); + event ExtensionProposalMade(bytes32 indexed extensionHash, address indexed proposer, ExtensionProposal proposal); /*----------------------------------------------------------*| @@ -771,32 +773,32 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { |*----------------------------------------------------------*/ /** - * @notice Make an extension offer for a loan on-chain. - * @param extension Extension struct. + * @notice Make an on-chain extension proposal. + * @param extension Extension proposal struct. */ - function makeExtensionOffer(Extension calldata extension) external { + function makeExtensionProposal(ExtensionProposal calldata extension) external { // Check that caller is a proposer if (msg.sender != extension.proposer) revert InvalidExtensionSigner({ allowed: extension.proposer, current: msg.sender }); - // Mark extension offer as made + // Mark extension proposal as made bytes32 extensionHash = getExtensionHash(extension); - extensionOffersMade[extensionHash] = true; + extensionProposalsMade[extensionHash] = true; - emit ExtensionOfferMade(extensionHash, extension.proposer, extension); + emit ExtensionProposalMade(extensionHash, extension.proposer, extension); } /** - * @notice Extend loans default date with signed extension offer / request from borrower or LOAN token owner. + * @notice Extend loans default date with signed extension proposal signed by borrower or LOAN token owner. * @dev The function assumes a prior token approval to a contract address or a signed permit. - * @param extension Extension struct. - * @param signature Signature of the extension offer / request. - * @param creditPermit Permit data for a credit asset signed by a borrower. + * @param extension Extension proposal struct. + * @param signature Signature of the extension proposal. + * @param compensationPermit Permit data for a fungible compensation asset signed by a borrower. */ function extendLOAN( - Extension calldata extension, + ExtensionProposal calldata extension, bytes calldata signature, - bytes calldata creditPermit + bytes calldata compensationPermit ) external { LOAN storage loan = LOANs[extension.loanId]; @@ -808,11 +810,15 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { // Check extension validity bytes32 extensionHash = getExtensionHash(extension); - if (!extensionOffersMade[extensionHash]) + if (!extensionProposalsMade[extensionHash]) if (!PWNSignatureChecker.isValidSignatureNow(extension.proposer, extensionHash, signature)) revert InvalidSignature({ signer: extension.proposer, digest: extensionHash }); + + // Check extension expiration if (block.timestamp >= extension.expiration) revert Expired({ current: block.timestamp, expiration: extension.expiration }); + + // Check extension nonce if (!revokedNonce.isNonceUsable(extension.proposer, extension.nonceSpace, extension.nonce)) revert NonceNotUsable({ addr: extension.proposer, nonceSpace: extension.nonceSpace, nonce: extension.nonce }); @@ -851,7 +857,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { limit: MAX_EXTENSION_DURATION }); - // Revoke extension offer nonce + // Revoke extension proposal nonce revokedNonce.revokeNonce(extension.proposer, extension.nonceSpace, extension.nonce); // Update loan @@ -865,25 +871,32 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { extendedDefaultTimestamp: loan.defaultTimestamp }); - // Transfer extension price to the loan owner - if (extension.price > 0) { - MultiToken.Asset memory credit = MultiToken.ERC20(loan.creditAddress, extension.price); - _permit(credit, loan.borrower, creditPermit); - _pushFrom(credit, loan.borrower, loanOwner); + // Skip compensation transfer if it's not set + if (extension.compensationAddress != address(0) && extension.compensationAmount > 0) { + MultiToken.Asset memory compensation = MultiToken.ERC20( + extension.compensationAddress, extension.compensationAmount + ); + + // Check compensation asset validity + _checkValidAsset(compensation); + + // Transfer compensation to the loan owner + _permit(compensation, loan.borrower, compensationPermit); + _pushFrom(compensation, loan.borrower, loanOwner); } } /** * @notice Get the hash of the extension struct. - * @param extension Extension struct. + * @param extension Extension proposal struct. * @return Hash of the extension struct. */ - function getExtensionHash(Extension calldata extension) public view returns (bytes32) { + function getExtensionHash(ExtensionProposal calldata extension) public view returns (bytes32) { return keccak256(abi.encodePacked( hex"1901", DOMAIN_SEPARATOR, keccak256(abi.encodePacked( - EXTENSION_TYPEHASH, + EXTENSION_PROPOSAL_TYPEHASH, abi.encode(extension) )) )); @@ -958,7 +971,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { * @param asset Asset to be checked. * @return True if the asset is valid. */ - function isValidAsset(MultiToken.Asset calldata asset) public view returns (bool) { + function isValidAsset(MultiToken.Asset memory asset) public view returns (bool) { return MultiToken.isValid(asset, categoryRegistry); } @@ -967,7 +980,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { * @dev The function will revert if the asset is not valid. * @param asset Asset to be checked. */ - function _checkValidAsset(MultiToken.Asset calldata asset) private view { + function _checkValidAsset(MultiToken.Asset memory asset) private view { if (!isValidAsset(asset)) { revert InvalidMultiTokenAsset({ category: uint8(asset.category), diff --git a/test/unit/PWNSimpleLoan.t.sol b/test/unit/PWNSimpleLoan.t.sol index 8aff3e9..fe13474 100644 --- a/test/unit/PWNSimpleLoan.t.sol +++ b/test/unit/PWNSimpleLoan.t.sol @@ -18,7 +18,7 @@ import { T721 } from "@pwn-test/helper/token/T721.sol"; abstract contract PWNSimpleLoanTest is Test { bytes32 internal constant LOANS_SLOT = bytes32(uint256(0)); // `LOANs` mapping position - bytes32 internal constant EXTENSION_OFFERS_MADE_SLOT = bytes32(uint256(1)); // `extensionOffersMade` mapping position + bytes32 internal constant EXTENSION_PROPOSALS_MADE_SLOT = bytes32(uint256(1)); // `extensionProposalsMade` mapping position PWNSimpleLoan loan; address hub = makeAddr("hub"); @@ -36,7 +36,7 @@ abstract contract PWNSimpleLoanTest is Test { PWNSimpleLoan.LOAN simpleLoan; PWNSimpleLoan.LOAN nonExistingLoan; PWNSimpleLoan.Terms simpleLoanTerms; - PWNSimpleLoan.Extension extension; + PWNSimpleLoan.ExtensionProposal extension; T20 fungibleAsset; T721 nonFungibleAsset; @@ -49,7 +49,7 @@ abstract contract PWNSimpleLoanTest is Test { event LOANClaimed(uint256 indexed loanId, bool indexed defaulted); event LOANRefinanced(uint256 indexed loanId, uint256 indexed refinancedLoanId); event LOANExtended(uint256 indexed loanId, uint40 originalDefaultTimestamp, uint40 extendedDefaultTimestamp); - event ExtensionOfferMade(bytes32 indexed extensionHash, address indexed proposer, PWNSimpleLoan.Extension extension); + event ExtensionProposalMade(bytes32 indexed extensionHash, address indexed proposer, PWNSimpleLoan.ExtensionProposal proposal); function setUp() virtual public { vm.etch(hub, bytes("data")); @@ -115,9 +115,10 @@ abstract contract PWNSimpleLoanTest is Test { collateral: MultiToken.Asset(MultiToken.Category(0), address(0), 0, 0) }); - extension = PWNSimpleLoan.Extension({ + extension = PWNSimpleLoan.ExtensionProposal({ loanId: loanId, - price: 100, + compensationAddress: address(fungibleAsset), + compensationAmount: 100, duration: 2 days, expiration: simpleLoan.defaultTimestamp, proposer: borrower, @@ -223,9 +224,9 @@ abstract contract PWNSimpleLoanTest is Test { vm.mockCall(loanToken, abi.encodeWithSignature("ownerOf(uint256)", _loanId), abi.encode(_owner)); } - function _mockExtensionOfferMade(PWNSimpleLoan.Extension memory _extension) internal { - bytes32 extensionOfferSlot = keccak256(abi.encode(_extensionHash(_extension), EXTENSION_OFFERS_MADE_SLOT)); - vm.store(address(loan), extensionOfferSlot, bytes32(uint256(1))); + function _mockExtensionProposalMade(PWNSimpleLoan.ExtensionProposal memory _extension) internal { + bytes32 extensionProposalSlot = keccak256(abi.encode(_extensionHash(_extension), EXTENSION_PROPOSALS_MADE_SLOT)); + vm.store(address(loan), extensionProposalSlot, bytes32(uint256(1))); } @@ -246,7 +247,7 @@ abstract contract PWNSimpleLoanTest is Test { } } - function _extensionHash(PWNSimpleLoan.Extension memory _extension) internal view returns (bytes32) { + function _extensionHash(PWNSimpleLoan.ExtensionProposal memory _extension) internal view returns (bytes32) { return keccak256(abi.encodePacked( "\x19\x01", keccak256(abi.encode( @@ -257,7 +258,7 @@ abstract contract PWNSimpleLoanTest is Test { address(loan) )), keccak256(abi.encodePacked( - keccak256("Extension(uint256 loanId,uint256 price,uint40 duration,uint40 expiration,address proposer,uint256 nonceSpace,uint256 nonce)"), + keccak256("ExtensionProposal(uint256 loanId,address compensationAddress,uint256 compensationAmount,uint40 duration,uint40 expiration,address proposer,uint256 nonceSpace,uint256 nonce)"), abi.encode(_extension) )) )); @@ -1747,36 +1748,36 @@ contract PWNSimpleLoan_ClaimLOAN_Test is PWNSimpleLoanTest { /*----------------------------------------------------------*| -|* # MAKE EXTENSION OFFER *| +|* # MAKE EXTENSION PROPOSAL *| |*----------------------------------------------------------*/ -contract PWNSimpleLoan_MakeExtensionOffer_Test is PWNSimpleLoanTest { +contract PWNSimpleLoan_MakeExtensionProposal_Test is PWNSimpleLoanTest { function testFuzz_shouldFail_whenCallerNotProposer(address caller) external { vm.assume(caller != extension.proposer); vm.expectRevert(abi.encodeWithSelector(InvalidExtensionSigner.selector, extension.proposer, caller)); vm.prank(caller); - loan.makeExtensionOffer(extension); + loan.makeExtensionProposal(extension); } function test_shouldStoreMadeFlag() external { vm.prank(extension.proposer); - loan.makeExtensionOffer(extension); + loan.makeExtensionProposal(extension); - bytes32 extensionOfferSlot = keccak256(abi.encode(_extensionHash(extension), EXTENSION_OFFERS_MADE_SLOT)); - bytes32 isMadeValue = vm.load(address(loan), extensionOfferSlot); + bytes32 extensionProposalSlot = keccak256(abi.encode(_extensionHash(extension), EXTENSION_PROPOSALS_MADE_SLOT)); + bytes32 isMadeValue = vm.load(address(loan), extensionProposalSlot); assertEq(uint256(isMadeValue), 1); } - function test_shouldEmit_ExtensionOfferMade() external { + function test_shouldEmit_ExtensionProposalMade() external { bytes32 extensionHash = _extensionHash(extension); vm.expectEmit(); - emit ExtensionOfferMade(extensionHash, extension.proposer, extension); + emit ExtensionProposalMade(extensionHash, extension.proposer, extension); vm.prank(extension.proposer); - loan.makeExtensionOffer(extension); + loan.makeExtensionProposal(extension); } } @@ -1806,7 +1807,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { // Helpers - function _signExtension(uint256 pk, PWNSimpleLoan.Extension memory _extension) private view returns (bytes memory) { + function _signExtension(uint256 pk, PWNSimpleLoan.ExtensionProposal memory _extension) private view returns (bytes memory) { (uint8 v, bytes32 r, bytes32 s) = vm.sign(pk, _extensionHash(_extension)); return abi.encodePacked(r, s, v); } @@ -1845,7 +1846,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { vm.warp(timestamp); extension.expiration = uint40(bound(expiration, 0, timestamp)); - _mockExtensionOfferMade(extension); + _mockExtensionProposalMade(extension); vm.expectRevert(abi.encodeWithSelector(Expired.selector, block.timestamp, extension.expiration)); vm.prank(lender); @@ -1853,7 +1854,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { } function test_shouldFail_whenOfferNonceNotUsable() external { - _mockExtensionOfferMade(extension); + _mockExtensionProposalMade(extension); vm.mockCall( revokedNonce, @@ -1870,7 +1871,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { function testFuzz_shouldFail_whenCallerIsNotBorrowerNorLoanOwner(address caller) external { vm.assume(caller != borrower && caller != lender); - _mockExtensionOfferMade(extension); + _mockExtensionProposalMade(extension); vm.expectRevert(abi.encodeWithSelector(InvalidExtensionCaller.selector)); vm.prank(caller); @@ -1881,7 +1882,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { vm.assume(proposer != lender); extension.proposer = proposer; - _mockExtensionOfferMade(extension); + _mockExtensionProposalMade(extension); vm.expectRevert(abi.encodeWithSelector(InvalidExtensionSigner.selector, lender, proposer)); vm.prank(borrower); @@ -1892,7 +1893,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { vm.assume(proposer != borrower); extension.proposer = proposer; - _mockExtensionOfferMade(extension); + _mockExtensionProposalMade(extension); vm.expectRevert(abi.encodeWithSelector(InvalidExtensionSigner.selector, borrower, proposer)); vm.prank(lender); @@ -1904,7 +1905,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { duration = uint40(bound(duration, 0, minDuration - 1)); extension.duration = duration; - _mockExtensionOfferMade(extension); + _mockExtensionProposalMade(extension); vm.expectRevert(abi.encodeWithSelector(InvalidExtensionDuration.selector, duration, minDuration)); vm.prank(lender); @@ -1916,7 +1917,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { duration = uint40(bound(duration, maxDuration + 1, type(uint40).max)); extension.duration = duration; - _mockExtensionOfferMade(extension); + _mockExtensionProposalMade(extension); vm.expectRevert(abi.encodeWithSelector(InvalidExtensionDuration.selector, duration, maxDuration)); vm.prank(lender); @@ -1926,7 +1927,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { function testFuzz_shouldRevokeExtensionNonce(uint256 nonceSpace, uint256 nonce) external { extension.nonceSpace = nonceSpace; extension.nonce = nonce; - _mockExtensionOfferMade(extension); + _mockExtensionProposalMade(extension); vm.expectCall( revokedNonce, @@ -1941,7 +1942,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { duration = uint40(bound(duration, loan.MIN_EXTENSION_DURATION(), loan.MAX_EXTENSION_DURATION())); extension.duration = duration; - _mockExtensionOfferMade(extension); + _mockExtensionProposalMade(extension); vm.prank(lender); loan.extendLOAN(extension, "", ""); @@ -1954,7 +1955,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { duration = uint40(bound(duration, loan.MIN_EXTENSION_DURATION(), loan.MAX_EXTENSION_DURATION())); extension.duration = duration; - _mockExtensionOfferMade(extension); + _mockExtensionProposalMade(extension); vm.expectEmit(); emit LOANExtended(loanId, simpleLoan.defaultTimestamp, simpleLoan.defaultTimestamp + duration); @@ -1963,29 +1964,29 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { loan.extendLOAN(extension, "", ""); } - function testFuzz_shouldTransferCredit_whenPriceMoreThanZero(uint256 price) external { - price = bound(price, 1, 1e40); + function test_shouldNotTransferCredit_whenAmountZero() external { + extension.compensationAddress = address(fungibleAsset); + extension.compensationAmount = 0; + _mockExtensionProposalMade(extension); - extension.price = price; - _mockExtensionOfferMade(extension); - fungibleAsset.mint(borrower, price); - - vm.expectCall( - simpleLoan.creditAddress, - abi.encodeWithSignature("transferFrom(address,address,uint256)", borrower, lender, price) - ); + vm.expectCall({ + callee: extension.compensationAddress, + data: abi.encodeWithSignature("transferFrom(address,address,uint256)", borrower, lender, 0), + count: 0 + }); vm.prank(lender); loan.extendLOAN(extension, "", ""); } - function test_shouldNotTransferCredit_whenPriceZero() external { - extension.price = 0; - _mockExtensionOfferMade(extension); + function test_shouldNotTransferCredit_whenAddressZero() external { + extension.compensationAddress = address(0); + extension.compensationAmount = 3123; + _mockExtensionProposalMade(extension); vm.expectCall({ - callee: simpleLoan.creditAddress, - data: abi.encodeWithSignature("transferFrom(address,address,uint256)", borrower, lender, 0), + callee: extension.compensationAddress, + data: abi.encodeWithSignature("transferFrom(address,address,uint256)", borrower, lender, extension.compensationAmount), count: 0 }); @@ -1993,18 +1994,52 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { loan.extendLOAN(extension, "", ""); } - function testFuzz_shouldCallPermit_whenPriceMoreThanZero_whenPermitData(uint256 price) external { - price = bound(price, 1, 1e40); + function test_shouldFail_whenInvalidCompensationAsset() external { + extension.compensationAddress = address(0x1); + extension.compensationAmount = 3123; + _mockExtensionProposalMade(extension); - extension.price = price; - _mockExtensionOfferMade(extension); - fungibleAsset.mint(borrower, price); + vm.mockCall( + categoryRegistry, + abi.encodeWithSignature("registeredCategoryValue(address)", extension.compensationAddress), + abi.encode(1) // ERC721 + ); + + vm.expectRevert(abi.encodeWithSelector(InvalidMultiTokenAsset.selector, 0, extension.compensationAddress, 0, extension.compensationAmount)); + vm.prank(lender); + loan.extendLOAN(extension, "", ""); + } + + function testFuzz_shouldCallPermit_whenPermitData(address addr, uint256 amount) external { + assumeAddressIsNot(addr, AddressType.ZeroAddress, AddressType.Precompile, AddressType.ForgeAddress); + amount = bound(amount, 1, 1e40); + + vm.etch(addr, address(fungibleAsset).code); + + extension.compensationAddress = addr; + extension.compensationAmount = amount; + _mockExtensionProposalMade(extension); + + T20(addr).mint(borrower, amount); + vm.prank(borrower); + T20(addr).approve(address(loan), type(uint256).max); + + vm.mockCall( + categoryRegistry, + abi.encodeWithSignature("registeredCategoryValue(address)", extension.compensationAddress), + abi.encode(0) // ER20 + ); + vm.mockCall( + extension.compensationAddress, + abi.encodeWithSignature("permit(address,address,uint256,uint256,uint8,bytes32,bytes32)"), + abi.encode("") + ); vm.expectCall( - simpleLoan.creditAddress, + extension.compensationAddress, abi.encodeWithSignature( "permit(address,address,uint256,uint256,uint8,bytes32,bytes32)", - borrower, address(loan), price, 1, uint8(4), uint256(2), uint256(3) + borrower, address(loan), amount, 1, uint8(4), uint256(2), uint256(3) ) ); @@ -2012,6 +2047,35 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { loan.extendLOAN(extension, "", creditPermit); } + function testFuzz_shouldTransferCompensation_whenDefined(address addr, uint256 amount) external { + assumeAddressIsNot(addr, AddressType.ZeroAddress, AddressType.Precompile, AddressType.ForgeAddress); + amount = bound(amount, 1, 1e40); + + vm.etch(addr, address(fungibleAsset).code); + + extension.compensationAddress = addr; + extension.compensationAmount = amount; + _mockExtensionProposalMade(extension); + + T20(addr).mint(borrower, amount); + vm.prank(borrower); + T20(addr).approve(address(loan), type(uint256).max); + + vm.mockCall( + categoryRegistry, + abi.encodeWithSignature("registeredCategoryValue(address)", extension.compensationAddress), + abi.encode(0) // ER20 + ); + + vm.expectCall( + extension.compensationAddress, + abi.encodeWithSignature("transferFrom(address,address,uint256)", borrower, lender, amount) + ); + + vm.prank(lender); + loan.extendLOAN(extension, "", ""); + } + function test_shouldPass_whenBorrowerSignature_whenLenderAccepts() external { extension.proposer = borrower; @@ -2035,7 +2099,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { contract PWNSimpleLoan_GetExtensionHash_Test is PWNSimpleLoanTest { - function test_shouldHaveCorrectDomainSeparator() external { + function test_shouldReturnExtensionHash() external { assertEq(_extensionHash(extension), loan.getExtensionHash(extension)); } From a30240ff4629b031f6bd5f5706e24dcb6cebc1b7 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Thu, 14 Mar 2024 15:14:53 +0100 Subject: [PATCH 044/129] feat: add auxiliary data to loan creation functions --- foundry.toml | 1 + src/loan/terms/simple/loan/PWNSimpleLoan.sol | 39 ++++-- .../proposal/offer/PWNSimpleLoanListOffer.sol | 69 +++++++++- .../offer/PWNSimpleLoanSimpleOffer.sol | 18 ++- .../request/PWNSimpleLoanSimpleRequest.sol | 18 ++- test/unit/PWNSimpleLoan.t.sol | 123 ++++++++++++------ test/unit/PWNSimpleLoanListOffer.t.sol | 108 ++++++++------- test/unit/PWNSimpleLoanSimpleOffer.t.sol | 96 +++++++------- test/unit/PWNSimpleLoanSimpleRequest.t.sol | 98 +++++++------- 9 files changed, 356 insertions(+), 214 deletions(-) diff --git a/foundry.toml b/foundry.toml index 6418914..852286b 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,6 +1,7 @@ [profile.default] solc_version = '0.8.16' fs_permissions = [{ access = "read", path = "./deployments.json"}] +via_ir = true [rpc_endpoints] diff --git a/src/loan/terms/simple/loan/PWNSimpleLoan.sol b/src/loan/terms/simple/loan/PWNSimpleLoan.sol index bd4652f..4a849b0 100644 --- a/src/loan/terms/simple/loan/PWNSimpleLoan.sol +++ b/src/loan/terms/simple/loan/PWNSimpleLoan.sol @@ -148,7 +148,12 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { /** * @dev Emitted when a new loan in created. */ - event LOANCreated(uint256 indexed loanId, Terms terms, bytes32 indexed proposalHash, address indexed proposalContract); + event LOANCreated(uint256 indexed loanId, Terms terms, bytes32 indexed proposalHash, address indexed proposalContract, bytes extra); + + /** + * @dev Emitted when a loan is refinanced. + */ + event LOANRefinanced(uint256 indexed loanId, uint256 indexed refinancedLoanId); /** * @dev Emitted when a loan is paid back. @@ -160,11 +165,6 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { */ event LOANClaimed(uint256 indexed loanId, bool indexed defaulted); - /** - * @dev Emitted when a loan is refinanced. - */ - event LOANRefinanced(uint256 indexed loanId, uint256 indexed refinancedLoanId); - /** * @dev Emitted when a LOAN token holder extends a loan. */ @@ -206,13 +206,15 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { * @param loanTerms Loan terms struct. * @param creditPermit Permit data for a credit asset signed by a lender. * @param collateralPermit Permit data for a collateral signed by a borrower. + * @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, bytes calldata creditPermit, - bytes calldata collateralPermit + bytes calldata collateralPermit, + bytes calldata extra ) external returns (uint256 loanId) { // Check that caller is loan proposal contract if (!hub.hasTag(msg.sender, PWNHubTags.LOAN_PROPOSAL)) { @@ -226,7 +228,8 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { loanId = _createLoan({ proposalHash: proposalHash, proposalContract: msg.sender, - loanTerms: loanTerms + loanTerms: loanTerms, + extra: extra }); // Transfer collateral to Vault and credit to borrower @@ -249,11 +252,13 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { * @param proposalHash Hash of a loan offer / request that is signed by a lender / borrower. * @param proposalContract Address of a loan proposal contract. * @param loanTerms Loan terms struct. + * @param extra Auxiliary data that are emitted in the loan creation event. They are not used in the contract logic. */ function _createLoan( bytes32 proposalHash, address proposalContract, - Terms calldata loanTerms + Terms calldata loanTerms, + bytes calldata extra ) private returns (uint256 loanId) { // Mint LOAN token for lender loanId = loanToken.mint(loanTerms.lender); @@ -277,7 +282,8 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { loanId: loanId, terms: loanTerms, proposalHash: proposalHash, - proposalContract: proposalContract + proposalContract: proposalContract, + extra: extra }); } @@ -335,6 +341,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { * @param loanTerms Loan terms struct. * @param lenderCreditPermit Permit data for a credit asset signed by a lender. * @param borrowerCreditPermit Permit data for a credit asset signed by a borrower. + * @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( @@ -342,7 +349,8 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { bytes32 proposalHash, Terms calldata loanTerms, bytes calldata lenderCreditPermit, - bytes calldata borrowerCreditPermit + bytes calldata borrowerCreditPermit, + bytes calldata extra ) external returns (uint256 refinancedLoanId) { // Check that caller is loan proposal contract if (!hub.hasTag(msg.sender, PWNHubTags.LOAN_PROPOSAL)) { @@ -361,7 +369,8 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { refinancedLoanId = _createLoan({ proposalHash: proposalHash, proposalContract: msg.sender, - loanTerms: loanTerms + loanTerms: loanTerms, + extra: extra }); // Refinance the original loan @@ -525,8 +534,10 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { /** * @notice Repay running loan. * @dev Any address can repay a running loan, but a collateral will be transferred to a borrower address associated with the loan. - * Repay will transfer a credit asset to a vault, waiting on a LOAN token holder to claim it. - * The function assumes a prior token approval to a contract address or a signed permit. + * If the LOAN token holder is the same as the original lender, the repayment credit asset will be + * transferred to the LOAN token holder directly. Otherwise it will transfer the repayment credit asset to + * a vault, waiting on a LOAN token holder to claim it. The function assumes a prior token approval to a contract address + * or a signed permit. * @param loanId Id of a loan that is being repaid. * @param creditPermit Permit data for a credit asset signed by a borrower. */ diff --git a/src/loan/terms/simple/proposal/offer/PWNSimpleLoanListOffer.sol b/src/loan/terms/simple/proposal/offer/PWNSimpleLoanListOffer.sol index db451c5..c01fd9e 100644 --- a/src/loan/terms/simple/proposal/offer/PWNSimpleLoanListOffer.sol +++ b/src/loan/terms/simple/proposal/offer/PWNSimpleLoanListOffer.sol @@ -109,13 +109,24 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanProposal { _makeProposal(getOfferHash(offer), offer.lender, abi.encode(offer)); } - + /** + * @notice Accept an offer. + * @dev Function will mark an offer hash 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 creditPermit Permit signature for a credit asset. + * @param collateralPermit Permit signature for a collateral asset. + * @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, bytes calldata creditPermit, - bytes calldata collateralPermit + bytes calldata collateralPermit, + bytes calldata extra ) public returns (uint256 loanId) { // Check if the offer is refinancing offer if (offer.refinancingLoanId != 0) { @@ -129,17 +140,31 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanProposal { proposalHash: offerHash, loanTerms: loanTerms, creditPermit: creditPermit, - collateralPermit: collateralPermit + collateralPermit: collateralPermit, + extra: extra }); } + /** + * @notice Accept a refinancing offer. + * @dev Function will mark an offer hash 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 lenderCreditPermit Lenders permit signature for a credit asset. + * @param borrowerCreditPermit Borrowers permit signature for a credit asset. + * @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, bytes calldata lenderCreditPermit, - bytes calldata borrowerCreditPermit + bytes calldata borrowerCreditPermit, + bytes calldata extra ) public returns (uint256 refinancedLoanId) { // Check if the offer is refinancing offer if (offer.refinancingLoanId != 0 && offer.refinancingLoanId != loanId) { @@ -154,23 +179,52 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanProposal { proposalHash: offerHash, loanTerms: loanTerms, lenderCreditPermit: lenderCreditPermit, - borrowerCreditPermit: borrowerCreditPermit + borrowerCreditPermit: borrowerCreditPermit, + 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 creditPermit Permit signature for a credit asset. + * @param collateralPermit Permit signature for a collateral asset. + * @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, bytes calldata creditPermit, bytes calldata collateralPermit, + bytes calldata extra, uint256 callersNonceSpace, uint256 callersNonceToRevoke ) external returns (uint256 loanId) { _revokeCallersNonce(msg.sender, callersNonceSpace, callersNonceToRevoke); - return acceptOffer(offer, offerValues, signature, creditPermit, collateralPermit); + return acceptOffer(offer, offerValues, signature, creditPermit, collateralPermit, 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 lenderCreditPermit Lenders permit signature for a credit asset. + * @param borrowerCreditPermit Borrowers permit signature for a credit asset. + * @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, @@ -178,11 +232,12 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanProposal { bytes calldata signature, bytes calldata lenderCreditPermit, bytes calldata borrowerCreditPermit, + bytes calldata extra, uint256 callersNonceSpace, uint256 callersNonceToRevoke ) external returns (uint256 refinancedLoanId) { _revokeCallersNonce(msg.sender, callersNonceSpace, callersNonceToRevoke); - return acceptRefinanceOffer(loanId, offer, offerValues, signature, lenderCreditPermit, borrowerCreditPermit); + return acceptRefinanceOffer(loanId, offer, offerValues, signature, lenderCreditPermit, borrowerCreditPermit, extra); } diff --git a/src/loan/terms/simple/proposal/offer/PWNSimpleLoanSimpleOffer.sol b/src/loan/terms/simple/proposal/offer/PWNSimpleLoanSimpleOffer.sol index bfb7e7d..73d0c49 100644 --- a/src/loan/terms/simple/proposal/offer/PWNSimpleLoanSimpleOffer.sol +++ b/src/loan/terms/simple/proposal/offer/PWNSimpleLoanSimpleOffer.sol @@ -98,7 +98,8 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanProposal { Offer calldata offer, bytes calldata signature, bytes calldata creditPermit, - bytes calldata collateralPermit + bytes calldata collateralPermit, + bytes calldata extra ) public returns (uint256 loanId) { // Check if the offer is refinancing offer if (offer.refinancingLoanId != 0) { @@ -112,7 +113,8 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanProposal { proposalHash: offerHash, loanTerms: loanTerms, creditPermit: creditPermit, - collateralPermit: collateralPermit + collateralPermit: collateralPermit, + extra: extra }); } @@ -121,7 +123,8 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanProposal { Offer calldata offer, bytes calldata signature, bytes calldata lenderCreditPermit, - bytes calldata borrowerCreditPermit + bytes calldata borrowerCreditPermit, + bytes calldata extra ) public returns (uint256 refinancedLoanId) { // Check if the offer is refinancing offer if (offer.refinancingLoanId != 0 && offer.refinancingLoanId != loanId) { @@ -136,7 +139,8 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanProposal { proposalHash: offerHash, loanTerms: loanTerms, lenderCreditPermit: lenderCreditPermit, - borrowerCreditPermit: borrowerCreditPermit + borrowerCreditPermit: borrowerCreditPermit, + extra: extra }); } @@ -145,11 +149,12 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanProposal { bytes calldata signature, bytes calldata creditPermit, bytes calldata collateralPermit, + bytes calldata extra, uint256 callersNonceSpace, uint256 callersNonceToRevoke ) external returns (uint256 loanId) { _revokeCallersNonce(msg.sender, callersNonceSpace, callersNonceToRevoke); - return acceptOffer(offer, signature, creditPermit, collateralPermit); + return acceptOffer(offer, signature, creditPermit, collateralPermit, extra); } function acceptRefinanceOffer( @@ -158,11 +163,12 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanProposal { bytes calldata signature, bytes calldata lenderCreditPermit, bytes calldata borrowerCreditPermit, + bytes calldata extra, uint256 callersNonceSpace, uint256 callersNonceToRevoke ) external returns (uint256 refinancedLoanId) { _revokeCallersNonce(msg.sender, callersNonceSpace, callersNonceToRevoke); - return acceptRefinanceOffer(loanId, offer, signature, lenderCreditPermit, borrowerCreditPermit); + return acceptRefinanceOffer(loanId, offer, signature, lenderCreditPermit, borrowerCreditPermit, extra); } diff --git a/src/loan/terms/simple/proposal/request/PWNSimpleLoanSimpleRequest.sol b/src/loan/terms/simple/proposal/request/PWNSimpleLoanSimpleRequest.sol index e5f7149..c7dcb86 100644 --- a/src/loan/terms/simple/proposal/request/PWNSimpleLoanSimpleRequest.sol +++ b/src/loan/terms/simple/proposal/request/PWNSimpleLoanSimpleRequest.sol @@ -99,7 +99,8 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanProposal { Request calldata request, bytes calldata signature, bytes calldata creditPermit, - bytes calldata collateralPermit + bytes calldata collateralPermit, + bytes calldata extra ) public returns (uint256 loanId) { // Check if the request is refinancing request if (request.refinancingLoanId != 0) { @@ -113,7 +114,8 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanProposal { proposalHash: requestHash, loanTerms: loanTerms, creditPermit: creditPermit, - collateralPermit: collateralPermit + collateralPermit: collateralPermit, + extra: extra }); } @@ -122,7 +124,8 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanProposal { Request calldata request, bytes calldata signature, bytes calldata lenderCreditPermit, - bytes calldata borrowerCreditPermit + bytes calldata borrowerCreditPermit, + bytes calldata extra ) public returns (uint256 refinancedLoanId) { // Check if the request is refinancing request if (request.refinancingLoanId == 0 || request.refinancingLoanId != loanId) { @@ -137,7 +140,8 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanProposal { proposalHash: requestHash, loanTerms: loanTerms, lenderCreditPermit: lenderCreditPermit, - borrowerCreditPermit: borrowerCreditPermit + borrowerCreditPermit: borrowerCreditPermit, + extra: extra }); } @@ -146,11 +150,12 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanProposal { bytes calldata signature, bytes calldata creditPermit, bytes calldata collateralPermit, + bytes calldata extra, uint256 callersNonceSpace, uint256 callersNonceToRevoke ) external returns (uint256 loanId) { _revokeCallersNonce(msg.sender, callersNonceSpace, callersNonceToRevoke); - return acceptRequest(request, signature, creditPermit, collateralPermit); + return acceptRequest(request, signature, creditPermit, collateralPermit, extra); } function acceptRefinanceRequest( @@ -159,11 +164,12 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanProposal { bytes calldata signature, bytes calldata lenderCreditPermit, bytes calldata borrowerCreditPermit, + bytes calldata extra, uint256 callersNonceSpace, uint256 callersNonceToRevoke ) external returns (uint256 refinancedLoanId) { _revokeCallersNonce(msg.sender, callersNonceSpace, callersNonceToRevoke); - return acceptRefinanceRequest(loanId, request, signature, lenderCreditPermit, borrowerCreditPermit); + return acceptRefinanceRequest(loanId, request, signature, lenderCreditPermit, borrowerCreditPermit, extra); } diff --git a/test/unit/PWNSimpleLoan.t.sol b/test/unit/PWNSimpleLoan.t.sol index fe13474..fa61dc7 100644 --- a/test/unit/PWNSimpleLoan.t.sol +++ b/test/unit/PWNSimpleLoan.t.sol @@ -44,7 +44,7 @@ abstract contract PWNSimpleLoanTest is Test { bytes collateralPermit = abi.encodePacked(uint256(1), uint256(2), uint256(3), uint8(4)); bytes32 proposalHash = keccak256("proposalHash"); - event LOANCreated(uint256 indexed loanId, PWNSimpleLoan.Terms terms, bytes32 indexed factoryDataHash, address indexed factoryAddress); + event LOANCreated(uint256 indexed loanId, PWNSimpleLoan.Terms terms, bytes32 indexed proposalHash, address indexed proposalContract, bytes extra); event LOANPaidBack(uint256 indexed loanId); event LOANClaimed(uint256 indexed loanId, bool indexed defaulted); event LOANRefinanced(uint256 indexed loanId, uint256 indexed refinancedLoanId); @@ -282,7 +282,8 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { proposalHash: proposalHash, loanTerms: simpleLoanTerms, creditPermit: "", - collateralPermit: "" + collateralPermit: "", + extra: "" }); } @@ -307,7 +308,8 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { proposalHash: proposalHash, loanTerms: simpleLoanTerms, creditPermit: "", - collateralPermit: "" + collateralPermit: "", + extra: "" }); } @@ -332,7 +334,8 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { proposalHash: proposalHash, loanTerms: simpleLoanTerms, creditPermit: "", - collateralPermit: "" + collateralPermit: "", + extra: "" }); } @@ -344,7 +347,8 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { proposalHash: proposalHash, loanTerms: simpleLoanTerms, creditPermit: "", - collateralPermit: "" + collateralPermit: "", + extra: "" }); } @@ -357,7 +361,8 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { proposalHash: proposalHash, loanTerms: simpleLoanTerms, creditPermit: "", - collateralPermit: "" + collateralPermit: "", + extra: "" }); simpleLoan.accruingInterestDailyRate = uint40(uint256(accruingInterestAPR) * 274 / 1e5); @@ -387,7 +392,8 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { proposalHash: proposalHash, loanTerms: simpleLoanTerms, creditPermit: "", - collateralPermit: collateralPermit + collateralPermit: collateralPermit, + extra: "" }); } @@ -429,20 +435,22 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { proposalHash: proposalHash, loanTerms: simpleLoanTerms, creditPermit: creditPermit, - collateralPermit: "" + collateralPermit: "", + extra: "" }); } function test_shouldEmitEvent_LOANCreated() external { vm.expectEmit(); - emit LOANCreated(loanId, simpleLoanTerms, proposalHash, proposalContract); + emit LOANCreated(loanId, simpleLoanTerms, proposalHash, proposalContract, "lil extra"); vm.prank(proposalContract); loan.createLOAN({ proposalHash: proposalHash, loanTerms: simpleLoanTerms, creditPermit: "", - collateralPermit: "" + collateralPermit: "", + extra: "lil extra" }); } @@ -452,7 +460,8 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { proposalHash: proposalHash, loanTerms: simpleLoanTerms, creditPermit: "", - collateralPermit: "" + collateralPermit: "", + extra: "" }); assertEq(createdLoanId, loanId); @@ -520,7 +529,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { proposalHash: proposalHash, loanTerms: refinancedLoanTerms, lenderCreditPermit: "", - borrowerCreditPermit: "" + borrowerCreditPermit: "", + extra: "" }); } @@ -535,7 +545,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { proposalHash: proposalHash, loanTerms: refinancedLoanTerms, lenderCreditPermit: "", - borrowerCreditPermit: "" + borrowerCreditPermit: "", + extra: "" }); } @@ -550,7 +561,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { proposalHash: proposalHash, loanTerms: refinancedLoanTerms, lenderCreditPermit: "", - borrowerCreditPermit: "" + borrowerCreditPermit: "", + extra: "" }); } @@ -564,7 +576,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { proposalHash: proposalHash, loanTerms: refinancedLoanTerms, lenderCreditPermit: "", - borrowerCreditPermit: "" + borrowerCreditPermit: "", + extra: "" }); } @@ -579,7 +592,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { proposalHash: proposalHash, loanTerms: refinancedLoanTerms, lenderCreditPermit: "", - borrowerCreditPermit: "" + borrowerCreditPermit: "", + extra: "" }); } @@ -593,7 +607,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { proposalHash: proposalHash, loanTerms: refinancedLoanTerms, lenderCreditPermit: "", - borrowerCreditPermit: "" + borrowerCreditPermit: "", + extra: "" }); } @@ -609,7 +624,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { proposalHash: proposalHash, loanTerms: refinancedLoanTerms, lenderCreditPermit: "", - borrowerCreditPermit: "" + borrowerCreditPermit: "", + extra: "" }); } @@ -624,7 +640,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { proposalHash: proposalHash, loanTerms: refinancedLoanTerms, lenderCreditPermit: "", - borrowerCreditPermit: "" + borrowerCreditPermit: "", + extra: "" }); } @@ -639,7 +656,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { proposalHash: proposalHash, loanTerms: refinancedLoanTerms, lenderCreditPermit: "", - borrowerCreditPermit: "" + borrowerCreditPermit: "", + extra: "" }); } @@ -654,7 +672,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { proposalHash: proposalHash, loanTerms: refinancedLoanTerms, lenderCreditPermit: "", - borrowerCreditPermit: "" + borrowerCreditPermit: "", + extra: "" }); } @@ -669,7 +688,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { proposalHash: proposalHash, loanTerms: refinancedLoanTerms, lenderCreditPermit: "", - borrowerCreditPermit: "" + borrowerCreditPermit: "", + extra: "" }); } @@ -682,7 +702,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { proposalHash: proposalHash, loanTerms: refinancedLoanTerms, lenderCreditPermit: "", - borrowerCreditPermit: "" + borrowerCreditPermit: "", + extra: "" }); } @@ -693,7 +714,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { proposalHash: proposalHash, loanTerms: refinancedLoanTerms, lenderCreditPermit: "", - borrowerCreditPermit: "" + borrowerCreditPermit: "", + extra: "" }); _assertLOANEq(ferinancedLoanId, refinancedLoan); @@ -701,7 +723,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { function test_shouldEmit_LOANCreated() external { vm.expectEmit(); - emit LOANCreated(ferinancedLoanId, refinancedLoanTerms, proposalHash, proposalContract); + emit LOANCreated(ferinancedLoanId, refinancedLoanTerms, proposalHash, proposalContract, "lil extra"); vm.prank(proposalContract); loan.refinanceLOAN({ @@ -709,7 +731,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { proposalHash: proposalHash, loanTerms: refinancedLoanTerms, lenderCreditPermit: "", - borrowerCreditPermit: "" + borrowerCreditPermit: "", + extra: "lil extra" }); } @@ -720,7 +743,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { proposalHash: proposalHash, loanTerms: refinancedLoanTerms, lenderCreditPermit: "", - borrowerCreditPermit: "" + borrowerCreditPermit: "", + extra: "" }); assertEq(newLoanId, ferinancedLoanId); @@ -736,7 +760,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { proposalHash: proposalHash, loanTerms: refinancedLoanTerms, lenderCreditPermit: "", - borrowerCreditPermit: "" + borrowerCreditPermit: "", + extra: "" }); } @@ -750,7 +775,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { proposalHash: proposalHash, loanTerms: refinancedLoanTerms, lenderCreditPermit: "", - borrowerCreditPermit: "" + borrowerCreditPermit: "", + extra: "" }); } @@ -761,7 +787,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { proposalHash: proposalHash, loanTerms: refinancedLoanTerms, lenderCreditPermit: "", - borrowerCreditPermit: "" + borrowerCreditPermit: "", + extra: "" }); _assertLOANEq(loanId, nonExistingLoan); @@ -777,7 +804,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { proposalHash: proposalHash, loanTerms: refinancedLoanTerms, lenderCreditPermit: "", - borrowerCreditPermit: "" + borrowerCreditPermit: "", + extra: "" }); } @@ -807,7 +835,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { proposalHash: proposalHash, loanTerms: refinancedLoanTerms, lenderCreditPermit: "", - borrowerCreditPermit: "" + borrowerCreditPermit: "", + extra: "" }); // Update loan and compare @@ -876,7 +905,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { proposalHash: proposalHash, loanTerms: refinancedLoanTerms, lenderCreditPermit: creditPermit, - borrowerCreditPermit: "" + borrowerCreditPermit: "", + extra: "" }); } @@ -939,7 +969,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { proposalHash: proposalHash, loanTerms: refinancedLoanTerms, lenderCreditPermit: creditPermit, - borrowerCreditPermit: "" + borrowerCreditPermit: "", + extra: "" }); } @@ -1004,7 +1035,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { proposalHash: proposalHash, loanTerms: refinancedLoanTerms, lenderCreditPermit: creditPermit, - borrowerCreditPermit: "" + borrowerCreditPermit: "", + extra: "" }); } @@ -1070,7 +1102,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { proposalHash: proposalHash, loanTerms: refinancedLoanTerms, lenderCreditPermit: creditPermit, - borrowerCreditPermit: creditPermit + borrowerCreditPermit: creditPermit, + extra: "" }); } @@ -1136,7 +1169,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { proposalHash: proposalHash, loanTerms: refinancedLoanTerms, lenderCreditPermit: creditPermit, - borrowerCreditPermit: creditPermit + borrowerCreditPermit: creditPermit, + extra: "" }); } @@ -1204,7 +1238,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { proposalHash: proposalHash, loanTerms: refinancedLoanTerms, lenderCreditPermit: creditPermit, - borrowerCreditPermit: creditPermit + borrowerCreditPermit: creditPermit, + extra: "" }); } @@ -1245,7 +1280,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { proposalHash: proposalHash, loanTerms: refinancedLoanTerms, lenderCreditPermit: "", - borrowerCreditPermit: "" + borrowerCreditPermit: "", + extra: "" }); assertEq(fungibleAsset.balanceOf(lender), originalBalance + loanRepaymentAmount); @@ -1291,7 +1327,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { proposalHash: proposalHash, loanTerms: refinancedLoanTerms, lenderCreditPermit: "", - borrowerCreditPermit: "" + borrowerCreditPermit: "", + extra: "" }); assertEq(fungibleAsset.balanceOf(feeCollector), originalBalance + feeAmount); @@ -1317,7 +1354,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { proposalHash: proposalHash, loanTerms: refinancedLoanTerms, lenderCreditPermit: "", - borrowerCreditPermit: "" + borrowerCreditPermit: "", + extra: "" }); assertEq(fungibleAsset.balanceOf(borrower), originalBalance + surplus); @@ -1341,7 +1379,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { proposalHash: proposalHash, loanTerms: refinancedLoanTerms, lenderCreditPermit: "", - borrowerCreditPermit: "" + borrowerCreditPermit: "", + extra: "" }); assertEq(fungibleAsset.balanceOf(borrower), originalBalance - contribution); diff --git a/test/unit/PWNSimpleLoanListOffer.t.sol b/test/unit/PWNSimpleLoanListOffer.t.sol index 45aa96b..439d8a6 100644 --- a/test/unit/PWNSimpleLoanListOffer.t.sol +++ b/test/unit/PWNSimpleLoanListOffer.t.sol @@ -224,7 +224,7 @@ contract PWNSimpleLoanListOffer_AcceptOffer_Test is PWNSimpleLoanListOfferTest { offer.refinancingLoanId = refinancingLoanId; vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, refinancingLoanId)); - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); } function testFuzz_shouldFail_whenLoanContractNotTagged_ACTIVE_LOAN(address loanContract) external { @@ -232,14 +232,14 @@ contract PWNSimpleLoanListOffer_AcceptOffer_Test is PWNSimpleLoanListOfferTest { offer.loanContract = loanContract; vm.expectRevert(abi.encodeWithSelector(AddressMissingHubTag.selector, loanContract, PWNHubTags.ACTIVE_LOAN)); - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); } function test_shouldAcceptAnyCollateralId_whenMerkleRootIsZero() external { offerValues.collateralId = 331; offer.collateralIdsWhitelistMerkleRoot = bytes32(0); - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); } function test_shouldPass_whenGivenCollateralIdIsWhitelisted() external { @@ -251,7 +251,7 @@ contract PWNSimpleLoanListOffer_AcceptOffer_Test is PWNSimpleLoanListOfferTest { offerValues.merkleInclusionProof = new bytes32[](1); offerValues.merkleInclusionProof[0] = id2Hash; - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); } function test_shouldFail_whenGivenCollateralIdIsNotWhitelisted() external { @@ -264,7 +264,7 @@ contract PWNSimpleLoanListOffer_AcceptOffer_Test is PWNSimpleLoanListOfferTest { offerValues.merkleInclusionProof[0] = id2Hash; vm.expectRevert(abi.encodeWithSelector(CollateralIdNotWhitelisted.selector, offerValues.collateralId)); - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); } function test_shouldNotCallComputerRegistry_whenShouldNotCheckStateFingerprint() external { @@ -276,7 +276,7 @@ contract PWNSimpleLoanListOffer_AcceptOffer_Test is PWNSimpleLoanListOfferTest { count: 0 }); - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); } function test_shouldFail_whenComputerRegistryReturnsZeroAddress_whenShouldCheckStateFingerprint() external { @@ -292,7 +292,7 @@ contract PWNSimpleLoanListOffer_AcceptOffer_Test is PWNSimpleLoanListOfferTest { ); vm.expectRevert(abi.encodeWithSelector(MissingStateFingerprintComputer.selector)); - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); } function testFuzz_shouldFail_whenComputerReturnsDifferentStateFingerprint_whenShouldCheckStateFingerprint( @@ -314,19 +314,19 @@ contract PWNSimpleLoanListOffer_AcceptOffer_Test is PWNSimpleLoanListOfferTest { vm.expectRevert(abi.encodeWithSelector( InvalidCollateralStateFingerprint.selector, stateFingerprint, offer.collateralStateFingerprint )); - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); } function test_shouldFail_whenInvalidSignature_whenEOA() external { vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, offer.lender, _offerHash(offer))); - offerContract.acceptOffer(offer, offerValues, _signOffer(1, offer), "", ""); + offerContract.acceptOffer(offer, offerValues, _signOffer(1, offer), "", "", ""); } 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, "", "", ""); + offerContract.acceptOffer(offer, offerValues, "", "", "", ""); } function test_shouldPass_whenOfferHasBeenMadeOnchain() external { @@ -336,15 +336,15 @@ contract PWNSimpleLoanListOffer_AcceptOffer_Test is PWNSimpleLoanListOfferTest { bytes32(uint256(1)) ); - offerContract.acceptOffer(offer, offerValues, "", "", ""); + offerContract.acceptOffer(offer, offerValues, "", "", "", ""); } function test_shouldPass_withValidSignature_whenEOA_whenStandardSignature() external { - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); } function test_shouldPass_withValidSignature_whenEOA_whenCompactEIP2098Signature() external { - offerContract.acceptOffer(offer, offerValues, _signOfferCompact(lenderPK, offer), "", ""); + offerContract.acceptOffer(offer, offerValues, _signOfferCompact(lenderPK, offer), "", "", ""); } function test_shouldPass_whenValidSignature_whenContractAccount() external { @@ -356,7 +356,7 @@ contract PWNSimpleLoanListOffer_AcceptOffer_Test is PWNSimpleLoanListOfferTest { abi.encode(bytes4(0x1626ba7e)) ); - offerContract.acceptOffer(offer, offerValues, "", "", ""); + offerContract.acceptOffer(offer, offerValues, "", "", "", ""); } function testFuzz_shouldFail_whenOfferIsExpired(uint256 timestamp) external { @@ -364,7 +364,7 @@ contract PWNSimpleLoanListOffer_AcceptOffer_Test is PWNSimpleLoanListOfferTest { vm.warp(timestamp); vm.expectRevert(abi.encodeWithSelector(Expired.selector, timestamp, offer.expiration)); - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); } function test_shouldFail_whenOfferNonceNotUsable() external { @@ -381,7 +381,7 @@ contract PWNSimpleLoanListOffer_AcceptOffer_Test is PWNSimpleLoanListOfferTest { vm.expectRevert(abi.encodeWithSelector( NonceNotUsable.selector, offer.lender, offer.nonceSpace, offer.nonce )); - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); } function testFuzz_shouldFail_whenCallerIsNotAllowedBorrower(address caller) external { @@ -391,7 +391,7 @@ contract PWNSimpleLoanListOffer_AcceptOffer_Test is PWNSimpleLoanListOfferTest { vm.expectRevert(abi.encodeWithSelector(CallerNotAllowedAcceptor.selector, caller, offer.allowedBorrower)); vm.prank(caller); - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); } function testFuzz_shouldFail_whenLessThanMinDuration(uint256 duration) external { @@ -400,7 +400,7 @@ contract PWNSimpleLoanListOffer_AcceptOffer_Test is PWNSimpleLoanListOfferTest { offer.duration = uint32(duration); vm.expectRevert(abi.encodeWithSelector(InvalidDuration.selector, duration, offerContract.MIN_LOAN_DURATION())); - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); } function testFuzz_shouldFail_whenAccruingInterestAPROutOfBounds(uint256 interestAPR) external { @@ -409,7 +409,7 @@ contract PWNSimpleLoanListOffer_AcceptOffer_Test is PWNSimpleLoanListOfferTest { offer.accruingInterestAPR = uint40(interestAPR); vm.expectRevert(abi.encodeWithSelector(AccruingInterestAPROutOfBounds.selector, interestAPR, maxInterest)); - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); } function test_shouldRevokeOffer_whenAvailableCreditLimitEqualToZero() external { @@ -422,7 +422,7 @@ contract PWNSimpleLoanListOffer_AcceptOffer_Test is PWNSimpleLoanListOfferTest { ) ); - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); } function testFuzz_shouldFail_whenUsedCreditExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { @@ -433,7 +433,7 @@ contract PWNSimpleLoanListOffer_AcceptOffer_Test is PWNSimpleLoanListOfferTest { 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), "", ""); + offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); } function testFuzz_shouldIncreaseUsedCredit_whenUsedCreditNotExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { @@ -443,7 +443,7 @@ contract PWNSimpleLoanListOffer_AcceptOffer_Test is PWNSimpleLoanListOfferTest { vm.store(address(offerContract), keccak256(abi.encode(_offerHash(offer), CREDIT_USED_SLOT)), bytes32(used)); - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); assertEq(offerContract.creditUsed(_offerHash(offer)), used + offer.creditAmount); } @@ -451,6 +451,7 @@ contract PWNSimpleLoanListOffer_AcceptOffer_Test is PWNSimpleLoanListOfferTest { function test_shouldCallLoanContractWithLoanTerms() external { bytes memory creditPermit = "creditPermit"; bytes memory collateralPermit = "collateralPermit"; + bytes memory extra = "lil extra"; PWNSimpleLoan.Terms memory loanTerms = PWNSimpleLoan.Terms({ lender: offer.lender, @@ -476,17 +477,17 @@ contract PWNSimpleLoanListOffer_AcceptOffer_Test is PWNSimpleLoanListOfferTest { activeLoanContract, abi.encodeWithSelector( PWNSimpleLoan.createLOAN.selector, - _offerHash(offer), loanTerms, creditPermit, collateralPermit + _offerHash(offer), loanTerms, creditPermit, collateralPermit, extra ) ); vm.prank(borrower); - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), creditPermit, collateralPermit); + offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), creditPermit, collateralPermit, extra); } function test_shouldReturnNewLoanId() external { assertEq( - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", ""), + offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", "", ""), loanId ); } @@ -515,6 +516,7 @@ contract PWNSimpleLoanListOffer_AcceptOfferAndRevokeCallersNonce_Test is PWNSimp signature: _signOffer(lenderPK, offer), creditPermit: "", collateralPermit: "", + extra: "", callersNonceSpace: nonceSpace, callersNonceToRevoke: nonce }); @@ -533,6 +535,7 @@ contract PWNSimpleLoanListOffer_AcceptOfferAndRevokeCallersNonce_Test is PWNSimp signature: _signOffer(lenderPK, offer), creditPermit: "", collateralPermit: "", + extra: "", callersNonceSpace: nonceSpace, callersNonceToRevoke: nonce }); @@ -546,6 +549,7 @@ contract PWNSimpleLoanListOffer_AcceptOfferAndRevokeCallersNonce_Test is PWNSimp signature: _signOffer(lenderPK, offer), creditPermit: "", collateralPermit: "", + extra: "", callersNonceSpace: 1, callersNonceToRevoke: 2 }); @@ -570,7 +574,7 @@ contract PWNSimpleLoanListOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanListOf offer.refinancingLoanId = _refinancingLoanId; vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, offer.refinancingLoanId)); - offerContract.acceptRefinanceOffer(_loanId, offer, offerValues, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptRefinanceOffer(_loanId, offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); } function testFuzz_shouldFail_whenLoanContractNotTagged_ACTIVE_LOAN(address loanContract) external { @@ -578,14 +582,14 @@ contract PWNSimpleLoanListOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanListOf offer.loanContract = loanContract; vm.expectRevert(abi.encodeWithSelector(AddressMissingHubTag.selector, loanContract, PWNHubTags.ACTIVE_LOAN)); - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); } function test_shouldAcceptAnyCollateralId_whenMerkleRootIsZero() external { offerValues.collateralId = 331; offer.collateralIdsWhitelistMerkleRoot = bytes32(0); - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); } function test_shouldPass_whenGivenCollateralIdIsWhitelisted() external { @@ -597,7 +601,7 @@ contract PWNSimpleLoanListOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanListOf offerValues.merkleInclusionProof = new bytes32[](1); offerValues.merkleInclusionProof[0] = id2Hash; - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); } function test_shouldFail_whenGivenCollateralIdIsNotWhitelisted() external { @@ -610,7 +614,7 @@ contract PWNSimpleLoanListOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanListOf offerValues.merkleInclusionProof[0] = id2Hash; vm.expectRevert(abi.encodeWithSelector(CollateralIdNotWhitelisted.selector, offerValues.collateralId)); - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); } function test_shouldNotCallComputerRegistry_whenShouldNotCheckStateFingerprint() external { @@ -622,7 +626,7 @@ contract PWNSimpleLoanListOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanListOf count: 0 }); - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); } function test_shouldFail_whenComputerRegistryReturnsZeroAddress_whenShouldCheckStateFingerprint() external { @@ -638,7 +642,7 @@ contract PWNSimpleLoanListOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanListOf ); vm.expectRevert(abi.encodeWithSelector(MissingStateFingerprintComputer.selector)); - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); } function testFuzz_shouldFail_whenComputerReturnsDifferentStateFingerprint_whenShouldCheckStateFingerprint( @@ -660,19 +664,19 @@ contract PWNSimpleLoanListOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanListOf vm.expectRevert(abi.encodeWithSelector( InvalidCollateralStateFingerprint.selector, stateFingerprint, offer.collateralStateFingerprint )); - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); } function test_shouldFail_whenInvalidSignature_whenEOA() external { vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, offer.lender, _offerHash(offer))); - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(1, offer), "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(1, offer), "", "", ""); } 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, "", "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, offerValues, "", "", "", ""); } function test_shouldPass_whenOfferHasBeenMadeOnchain() external { @@ -682,15 +686,15 @@ contract PWNSimpleLoanListOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanListOf bytes32(uint256(1)) ); - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, "", "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, offerValues, "", "", "", ""); } function test_shouldPass_withValidSignature_whenEOA_whenStandardSignature() external { - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); } function test_shouldPass_withValidSignature_whenEOA_whenCompactEIP2098Signature() external { - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOfferCompact(lenderPK, offer), "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOfferCompact(lenderPK, offer), "", "", ""); } function test_shouldPass_whenValidSignature_whenContractAccount() external { @@ -702,7 +706,7 @@ contract PWNSimpleLoanListOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanListOf abi.encode(bytes4(0x1626ba7e)) ); - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, "", "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, offerValues, "", "", "", ""); } function testFuzz_shouldFail_whenOfferIsExpired(uint256 timestamp) external { @@ -710,7 +714,7 @@ contract PWNSimpleLoanListOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanListOf vm.warp(timestamp); vm.expectRevert(abi.encodeWithSelector(Expired.selector, timestamp, offer.expiration)); - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); } function test_shouldFail_whenOfferNonceNotUsable() external { @@ -727,7 +731,7 @@ contract PWNSimpleLoanListOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanListOf vm.expectRevert(abi.encodeWithSelector( NonceNotUsable.selector, offer.lender, offer.nonceSpace, offer.nonce )); - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); } function testFuzz_shouldFail_whenCallerIsNotAllowedBorrower(address caller) external { @@ -737,7 +741,7 @@ contract PWNSimpleLoanListOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanListOf vm.expectRevert(abi.encodeWithSelector(CallerNotAllowedAcceptor.selector, caller, offer.allowedBorrower)); vm.prank(caller); - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); } function testFuzz_shouldFail_whenLessThanMinDuration(uint256 duration) external { @@ -746,7 +750,7 @@ contract PWNSimpleLoanListOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanListOf offer.duration = uint32(duration); vm.expectRevert(abi.encodeWithSelector(InvalidDuration.selector, duration, offerContract.MIN_LOAN_DURATION())); - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); } function testFuzz_shouldFail_whenAccruingInterestAPROutOfBounds(uint256 interestAPR) external { @@ -755,7 +759,7 @@ contract PWNSimpleLoanListOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanListOf offer.accruingInterestAPR = uint40(interestAPR); vm.expectRevert(abi.encodeWithSelector(AccruingInterestAPROutOfBounds.selector, interestAPR, maxInterest)); - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); } function test_shouldRevokeOffer_whenAvailableCreditLimitEqualToZero() external { @@ -768,7 +772,7 @@ contract PWNSimpleLoanListOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanListOf ) ); - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); } function testFuzz_shouldFail_whenUsedCreditExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { @@ -779,7 +783,7 @@ contract PWNSimpleLoanListOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanListOf 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), "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); } function testFuzz_shouldIncreaseUsedCredit_whenUsedCreditNotExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { @@ -789,7 +793,7 @@ contract PWNSimpleLoanListOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanListOf vm.store(address(offerContract), keccak256(abi.encode(_offerHash(offer), CREDIT_USED_SLOT)), bytes32(used)); - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); assertEq(offerContract.creditUsed(_offerHash(offer)), used + offer.creditAmount); } @@ -797,6 +801,7 @@ contract PWNSimpleLoanListOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanListOf function test_shouldCallLoanContract() external { bytes memory creditPermit = "creditPermit"; bytes memory collateralPermit = "collateralPermit"; + bytes memory extra = "lil extra"; PWNSimpleLoan.Terms memory loanTerms = PWNSimpleLoan.Terms({ lender: lender, @@ -822,19 +827,19 @@ contract PWNSimpleLoanListOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanListOf activeLoanContract, abi.encodeWithSelector( PWNSimpleLoan.refinanceLOAN.selector, - loanId, _offerHash(offer), loanTerms, creditPermit, collateralPermit + loanId, _offerHash(offer), loanTerms, creditPermit, collateralPermit, extra ) ); vm.prank(lender); offerContract.acceptRefinanceOffer( - loanId, offer, offerValues, _signOffer(lenderPK, offer), creditPermit, collateralPermit + loanId, offer, offerValues, _signOffer(lenderPK, offer), creditPermit, collateralPermit, extra ); } function test_shouldReturnRefinancedLoanId() external { assertEq( - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", ""), + offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", "", ""), refinancedLoanId ); } @@ -864,6 +869,7 @@ contract PWNSimpleLoanListOffer_AcceptRefinanceOfferAndRevokeCallersNonce_Test i signature: _signOffer(lenderPK, offer), lenderCreditPermit: "", borrowerCreditPermit: "", + extra: "", callersNonceSpace: nonceSpace, callersNonceToRevoke: nonce }); @@ -883,6 +889,7 @@ contract PWNSimpleLoanListOffer_AcceptRefinanceOfferAndRevokeCallersNonce_Test i signature: _signOffer(lenderPK, offer), lenderCreditPermit: "", borrowerCreditPermit: "", + extra: "", callersNonceSpace: nonceSpace, callersNonceToRevoke: nonce }); @@ -897,6 +904,7 @@ contract PWNSimpleLoanListOffer_AcceptRefinanceOfferAndRevokeCallersNonce_Test i signature: _signOffer(lenderPK, offer), lenderCreditPermit: "", borrowerCreditPermit: "", + extra: "", callersNonceSpace: 1, callersNonceToRevoke: 2 }); diff --git a/test/unit/PWNSimpleLoanSimpleOffer.t.sol b/test/unit/PWNSimpleLoanSimpleOffer.t.sol index 7cdb6f3..7bff348 100644 --- a/test/unit/PWNSimpleLoanSimpleOffer.t.sol +++ b/test/unit/PWNSimpleLoanSimpleOffer.t.sol @@ -218,7 +218,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptOffer_Test is PWNSimpleLoanSimpleOfferTe offer.refinancingLoanId = refinancingLoanId; vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, refinancingLoanId)); - offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", "", ""); } function testFuzz_shouldFail_whenLoanContractNotTagged_ACTIVE_LOAN(address loanContract) external { @@ -226,7 +226,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptOffer_Test is PWNSimpleLoanSimpleOfferTe offer.loanContract = loanContract; vm.expectRevert(abi.encodeWithSelector(AddressMissingHubTag.selector, loanContract, PWNHubTags.ACTIVE_LOAN)); - offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", "", ""); } function test_shouldNotCallComputerRegistry_whenShouldNotCheckStateFingerprint() external { @@ -238,7 +238,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptOffer_Test is PWNSimpleLoanSimpleOfferTe count: 0 }); - offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", "", ""); } function test_shouldFail_whenComputerRegistryReturnsZeroAddress_whenShouldCheckStateFingerprint() external { @@ -254,7 +254,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptOffer_Test is PWNSimpleLoanSimpleOfferTe ); vm.expectRevert(abi.encodeWithSelector(MissingStateFingerprintComputer.selector)); - offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", "", ""); } function testFuzz_shouldFail_whenComputerReturnsDifferentStateFingerprint_whenShouldCheckStateFingerprint( @@ -276,19 +276,19 @@ contract PWNSimpleLoanSimpleOffer_AcceptOffer_Test is PWNSimpleLoanSimpleOfferTe vm.expectRevert(abi.encodeWithSelector( InvalidCollateralStateFingerprint.selector, stateFingerprint, offer.collateralStateFingerprint )); - offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", "", ""); } function test_shouldFail_whenInvalidSignature_whenEOA() external { vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, offer.lender, _offerHash(offer))); - offerContract.acceptOffer(offer, _signOffer(1, offer), "", ""); + offerContract.acceptOffer(offer, _signOffer(1, offer), "", "", ""); } function test_shouldFail_whenInvalidSignature_whenContractAccount() external { vm.etch(lender, bytes("data")); vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, offer.lender, _offerHash(offer))); - offerContract.acceptOffer(offer, "", "", ""); + offerContract.acceptOffer(offer, "", "", "", ""); } function test_shouldPass_whenOfferHasBeenMadeOnchain() external { @@ -298,15 +298,15 @@ contract PWNSimpleLoanSimpleOffer_AcceptOffer_Test is PWNSimpleLoanSimpleOfferTe bytes32(uint256(1)) ); - offerContract.acceptOffer(offer, "", "", ""); + offerContract.acceptOffer(offer, "", "", "", ""); } function test_shouldPass_withValidSignature_whenEOA_whenStandardSignature() external { - offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", "", ""); } function test_shouldPass_withValidSignature_whenEOA_whenCompactEIP2098Signature() external { - offerContract.acceptOffer(offer, _signOfferCompact(lenderPK, offer), "", ""); + offerContract.acceptOffer(offer, _signOfferCompact(lenderPK, offer), "", "", ""); } function test_shouldPass_whenValidSignature_whenContractAccount() external { @@ -318,7 +318,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptOffer_Test is PWNSimpleLoanSimpleOfferTe abi.encode(bytes4(0x1626ba7e)) ); - offerContract.acceptOffer(offer, "", "", ""); + offerContract.acceptOffer(offer, "", "", "", ""); } function testFuzz_shouldFail_whenOfferIsExpired(uint256 timestamp) external { @@ -326,7 +326,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptOffer_Test is PWNSimpleLoanSimpleOfferTe vm.warp(timestamp); vm.expectRevert(abi.encodeWithSelector(Expired.selector, timestamp, offer.expiration)); - offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", "", ""); } function test_shouldFail_whenOfferNonceNotUsable() external { @@ -343,7 +343,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptOffer_Test is PWNSimpleLoanSimpleOfferTe vm.expectRevert(abi.encodeWithSelector( NonceNotUsable.selector, offer.lender, offer.nonceSpace, offer.nonce )); - offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", "", ""); } function testFuzz_shouldFail_whenCallerIsNotAllowedBorrower(address caller) external { @@ -353,7 +353,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptOffer_Test is PWNSimpleLoanSimpleOfferTe vm.expectRevert(abi.encodeWithSelector(CallerNotAllowedAcceptor.selector, caller, offer.allowedBorrower)); vm.prank(caller); - offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", "", ""); } function testFuzz_shouldFail_whenLessThanMinDuration(uint256 duration) external { @@ -362,7 +362,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptOffer_Test is PWNSimpleLoanSimpleOfferTe offer.duration = uint32(duration); vm.expectRevert(abi.encodeWithSelector(InvalidDuration.selector, duration, offerContract.MIN_LOAN_DURATION())); - offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", "", ""); } function testFuzz_shouldFail_whenAccruingInterestAPROutOfBounds(uint256 interestAPR) external { @@ -371,7 +371,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptOffer_Test is PWNSimpleLoanSimpleOfferTe offer.accruingInterestAPR = uint40(interestAPR); vm.expectRevert(abi.encodeWithSelector(AccruingInterestAPROutOfBounds.selector, interestAPR, maxInterest)); - offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", "", ""); } function test_shouldRevokeOffer_whenAvailableCreditLimitEqualToZero() external { @@ -384,7 +384,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptOffer_Test is PWNSimpleLoanSimpleOfferTe ) ); - offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", "", ""); } function testFuzz_shouldFail_whenUsedCreditExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { @@ -395,7 +395,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptOffer_Test is PWNSimpleLoanSimpleOfferTe 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, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", "", ""); } function testFuzz_shouldIncreaseUsedCredit_whenUsedCreditNotExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { @@ -405,7 +405,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptOffer_Test is PWNSimpleLoanSimpleOfferTe vm.store(address(offerContract), keccak256(abi.encode(_offerHash(offer), CREDIT_USED_SLOT)), bytes32(used)); - offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", "", ""); assertEq(offerContract.creditUsed(_offerHash(offer)), used + offer.creditAmount); } @@ -413,6 +413,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptOffer_Test is PWNSimpleLoanSimpleOfferTe function test_shouldCallLoanContractWithLoanTerms() external { bytes memory creditPermit = "creditPermit"; bytes memory collateralPermit = "collateralPermit"; + bytes memory extra = "lil extra"; PWNSimpleLoan.Terms memory loanTerms = PWNSimpleLoan.Terms({ lender: offer.lender, @@ -438,17 +439,17 @@ contract PWNSimpleLoanSimpleOffer_AcceptOffer_Test is PWNSimpleLoanSimpleOfferTe activeLoanContract, abi.encodeWithSelector( PWNSimpleLoan.createLOAN.selector, - _offerHash(offer), loanTerms, creditPermit, collateralPermit + _offerHash(offer), loanTerms, creditPermit, collateralPermit, extra ) ); vm.prank(borrower); - offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), creditPermit, collateralPermit); + offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), creditPermit, collateralPermit, extra); } function test_shouldReturnNewLoanId() external { assertEq( - offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", ""), + offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", "", ""), loanId ); } @@ -476,6 +477,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptOfferAndRevokeCallersNonce_Test is PWNSi signature: _signOffer(lenderPK, offer), creditPermit: "", collateralPermit: "", + extra: "", callersNonceSpace: nonceSpace, callersNonceToRevoke: nonce }); @@ -493,6 +495,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptOfferAndRevokeCallersNonce_Test is PWNSi signature: _signOffer(lenderPK, offer), creditPermit: "", collateralPermit: "", + extra: "", callersNonceSpace: nonceSpace, callersNonceToRevoke: nonce }); @@ -505,6 +508,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptOfferAndRevokeCallersNonce_Test is PWNSi signature: _signOffer(lenderPK, offer), creditPermit: "", collateralPermit: "", + extra: "", callersNonceSpace: 1, callersNonceToRevoke: 2 }); @@ -529,7 +533,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanSimp offer.refinancingLoanId = _refinancingLoanId; vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, offer.refinancingLoanId)); - offerContract.acceptRefinanceOffer(_loanId, offer, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptRefinanceOffer(_loanId, offer, _signOffer(lenderPK, offer), "", "", ""); } function testFuzz_shouldFail_whenLoanContractNotTagged_ACTIVE_LOAN(address loanContract) external { @@ -537,7 +541,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanSimp offer.loanContract = loanContract; vm.expectRevert(abi.encodeWithSelector(AddressMissingHubTag.selector, loanContract, PWNHubTags.ACTIVE_LOAN)); - offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), "", "", ""); } function test_shouldNotCallComputerRegistry_whenShouldNotCheckStateFingerprint() external { @@ -549,7 +553,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanSimp count: 0 }); - offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), "", "", ""); } function test_shouldFail_whenComputerRegistryReturnsZeroAddress_whenShouldCheckStateFingerprint() external { @@ -565,7 +569,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanSimp ); vm.expectRevert(abi.encodeWithSelector(MissingStateFingerprintComputer.selector)); - offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), "", "", ""); } function testFuzz_shouldFail_whenComputerReturnsDifferentStateFingerprint_whenShouldCheckStateFingerprint( @@ -587,19 +591,19 @@ contract PWNSimpleLoanSimpleOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanSimp vm.expectRevert(abi.encodeWithSelector( InvalidCollateralStateFingerprint.selector, stateFingerprint, offer.collateralStateFingerprint )); - offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), "", "", ""); } function test_shouldFail_whenInvalidSignature_whenEOA() external { vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, offer.lender, _offerHash(offer))); - offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(1, offer), "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(1, offer), "", "", ""); } 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, "", "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, "", "", "", ""); } function test_shouldPass_whenOfferHasBeenMadeOnchain() external { @@ -609,15 +613,15 @@ contract PWNSimpleLoanSimpleOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanSimp bytes32(uint256(1)) ); - offerContract.acceptRefinanceOffer(loanId, offer, "", "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, "", "", "", ""); } function test_shouldPass_withValidSignature_whenEOA_whenStandardSignature() external { - offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), "", "", ""); } function test_shouldPass_withValidSignature_whenEOA_whenCompactEIP2098Signature() external { - offerContract.acceptRefinanceOffer(loanId, offer, _signOfferCompact(lenderPK, offer), "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, _signOfferCompact(lenderPK, offer), "", "", ""); } function test_shouldPass_whenValidSignature_whenContractAccount() external { @@ -629,7 +633,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanSimp abi.encode(bytes4(0x1626ba7e)) ); - offerContract.acceptRefinanceOffer(loanId, offer, "", "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, "", "", "", ""); } function testFuzz_shouldFail_whenOfferIsExpired(uint256 timestamp) external { @@ -637,7 +641,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanSimp vm.warp(timestamp); vm.expectRevert(abi.encodeWithSelector(Expired.selector, timestamp, offer.expiration)); - offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), "", "", ""); } function test_shouldFail_whenOfferNonceNotUsable() external { @@ -654,7 +658,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanSimp vm.expectRevert(abi.encodeWithSelector( NonceNotUsable.selector, offer.lender, offer.nonceSpace, offer.nonce )); - offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), "", "", ""); } function testFuzz_shouldFail_whenCallerIsNotAllowedBorrower(address caller) external { @@ -664,7 +668,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanSimp vm.expectRevert(abi.encodeWithSelector(CallerNotAllowedAcceptor.selector, caller, offer.allowedBorrower)); vm.prank(caller); - offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), "", "", ""); } function testFuzz_shouldFail_whenLessThanMinDuration(uint256 duration) external { @@ -673,7 +677,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanSimp offer.duration = uint32(duration); vm.expectRevert(abi.encodeWithSelector(InvalidDuration.selector, duration, offerContract.MIN_LOAN_DURATION())); - offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), "", "", ""); } function testFuzz_shouldFail_whenAccruingInterestAPROutOfBounds(uint256 interestAPR) external { @@ -682,7 +686,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanSimp offer.accruingInterestAPR = uint40(interestAPR); vm.expectRevert(abi.encodeWithSelector(AccruingInterestAPROutOfBounds.selector, interestAPR, maxInterest)); - offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), "", "", ""); } function test_shouldRevokeOffer_whenAvailableCreditLimitEqualToZero() external { @@ -695,7 +699,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanSimp ) ); - offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), "", "", ""); } function testFuzz_shouldFail_whenUsedCreditExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { @@ -706,7 +710,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanSimp 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, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), "", "", ""); } function testFuzz_shouldIncreaseUsedCredit_whenUsedCreditNotExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { @@ -716,7 +720,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanSimp vm.store(address(offerContract), keccak256(abi.encode(_offerHash(offer), CREDIT_USED_SLOT)), bytes32(used)); - offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), "", "", ""); assertEq(offerContract.creditUsed(_offerHash(offer)), used + offer.creditAmount); } @@ -724,6 +728,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanSimp function test_shouldCallLoanContract() external { bytes memory creditPermit = "creditPermit"; bytes memory collateralPermit = "collateralPermit"; + bytes memory extra = "lil extra"; PWNSimpleLoan.Terms memory loanTerms = PWNSimpleLoan.Terms({ lender: lender, @@ -749,19 +754,19 @@ contract PWNSimpleLoanSimpleOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanSimp activeLoanContract, abi.encodeWithSelector( PWNSimpleLoan.refinanceLOAN.selector, - loanId, _offerHash(offer), loanTerms, creditPermit, collateralPermit + loanId, _offerHash(offer), loanTerms, creditPermit, collateralPermit, extra ) ); vm.prank(lender); offerContract.acceptRefinanceOffer( - loanId, offer, _signOffer(lenderPK, offer), creditPermit, collateralPermit + loanId, offer, _signOffer(lenderPK, offer), creditPermit, collateralPermit, extra ); } function test_shouldReturnRefinancedLoanId() external { assertEq( - offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), "", ""), + offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), "", "", ""), refinancedLoanId ); } @@ -790,6 +795,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptRefinanceOfferAndRevokeCallersNonce_Test signature: _signOffer(lenderPK, offer), lenderCreditPermit: "", borrowerCreditPermit: "", + extra: "", callersNonceSpace: nonceSpace, callersNonceToRevoke: nonce }); @@ -808,6 +814,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptRefinanceOfferAndRevokeCallersNonce_Test signature: _signOffer(lenderPK, offer), lenderCreditPermit: "", borrowerCreditPermit: "", + extra: "", callersNonceSpace: nonceSpace, callersNonceToRevoke: nonce }); @@ -821,6 +828,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptRefinanceOfferAndRevokeCallersNonce_Test signature: _signOffer(lenderPK, offer), lenderCreditPermit: "", borrowerCreditPermit: "", + extra: "", callersNonceSpace: 1, callersNonceToRevoke: 2 }); diff --git a/test/unit/PWNSimpleLoanSimpleRequest.t.sol b/test/unit/PWNSimpleLoanSimpleRequest.t.sol index c7bfe50..51f12a3 100644 --- a/test/unit/PWNSimpleLoanSimpleRequest.t.sol +++ b/test/unit/PWNSimpleLoanSimpleRequest.t.sol @@ -218,7 +218,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRequest_Test is PWNSimpleLoanSimpleReq request.refinancingLoanId = refinancingLoanId; vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, refinancingLoanId)); - requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", ""); + requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", "", ""); } function testFuzz_shouldFail_whenLoanContractNotTagged_ACTIVE_LOAN(address loanContract) external { @@ -226,7 +226,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRequest_Test is PWNSimpleLoanSimpleReq request.loanContract = loanContract; vm.expectRevert(abi.encodeWithSelector(AddressMissingHubTag.selector, loanContract, PWNHubTags.ACTIVE_LOAN)); - requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", ""); + requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", "", ""); } function test_shouldNotCallComputerRegistry_whenShouldNotCheckStateFingerprint() external { @@ -238,7 +238,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRequest_Test is PWNSimpleLoanSimpleReq count: 0 }); - requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", ""); + requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", "", ""); } function test_shouldFail_whenComputerRegistryReturnsZeroAddress_whenShouldCheckStateFingerprint() external { @@ -254,7 +254,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRequest_Test is PWNSimpleLoanSimpleReq ); vm.expectRevert(abi.encodeWithSelector(MissingStateFingerprintComputer.selector)); - requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", ""); + requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", "", ""); } function testFuzz_shouldFail_whenComputerReturnsDifferentStateFingerprint_whenShouldCheckStateFingerprint( @@ -276,19 +276,19 @@ contract PWNSimpleLoanSimpleRequest_AcceptRequest_Test is PWNSimpleLoanSimpleReq vm.expectRevert(abi.encodeWithSelector( InvalidCollateralStateFingerprint.selector, stateFingerprint, request.collateralStateFingerprint )); - requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", ""); + requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", "", ""); } function test_shouldFail_whenInvalidSignature_whenEOA() external { vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, request.borrower, _requestHash(request))); - requestContract.acceptRequest(request, _signRequest(1, request), "", ""); + requestContract.acceptRequest(request, _signRequest(1, request), "", "", ""); } function test_shouldFail_whenInvalidSignature_whenContractAccount() external { vm.etch(borrower, bytes("data")); vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, request.borrower, _requestHash(request))); - requestContract.acceptRequest(request, "", "", ""); + requestContract.acceptRequest(request, "", "", "", ""); } function test_shouldPass_whenRequestHasBeenMadeOnchain() external { @@ -298,15 +298,15 @@ contract PWNSimpleLoanSimpleRequest_AcceptRequest_Test is PWNSimpleLoanSimpleReq bytes32(uint256(1)) ); - requestContract.acceptRequest(request, "", "", ""); + requestContract.acceptRequest(request, "", "", "", ""); } function test_shouldPass_withValidSignature_whenEOA_whenStandardSignature() external { - requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", ""); + requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", "", ""); } function test_shouldPass_withValidSignature_whenEOA_whenCompactEIP2098Signature() external { - requestContract.acceptRequest(request, _signRequestCompact(borrowerPK, request), "", ""); + requestContract.acceptRequest(request, _signRequestCompact(borrowerPK, request), "", "", ""); } function test_shouldPass_whenValidSignature_whenContractAccount() external { @@ -318,7 +318,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRequest_Test is PWNSimpleLoanSimpleReq abi.encode(bytes4(0x1626ba7e)) ); - requestContract.acceptRequest(request, "", "", ""); + requestContract.acceptRequest(request, "", "", "", ""); } function testFuzz_shouldFail_whenRequestIsExpired(uint256 timestamp) external { @@ -326,7 +326,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRequest_Test is PWNSimpleLoanSimpleReq vm.warp(timestamp); vm.expectRevert(abi.encodeWithSelector(Expired.selector, timestamp, request.expiration)); - requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", ""); + requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", "", ""); } function test_shouldFail_whenRequestNonceNotUsable() external { @@ -343,7 +343,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRequest_Test is PWNSimpleLoanSimpleReq vm.expectRevert(abi.encodeWithSelector( NonceNotUsable.selector, request.borrower, request.nonceSpace, request.nonce )); - requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", ""); + requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", "", ""); } function testFuzz_shouldFail_whenCallerIsNotAllowedLender(address caller) external { @@ -353,7 +353,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRequest_Test is PWNSimpleLoanSimpleReq vm.expectRevert(abi.encodeWithSelector(CallerNotAllowedAcceptor.selector, caller, request.allowedLender)); vm.prank(caller); - requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", ""); + requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", "", ""); } function testFuzz_shouldFail_whenLessThanMinDuration(uint256 duration) external { @@ -362,7 +362,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRequest_Test is PWNSimpleLoanSimpleReq request.duration = uint32(duration); vm.expectRevert(abi.encodeWithSelector(InvalidDuration.selector, duration, requestContract.MIN_LOAN_DURATION())); - requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", ""); + requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", "", ""); } function testFuzz_shouldFail_whenAccruingInterestAPROutOfBounds(uint256 interestAPR) external { @@ -371,7 +371,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRequest_Test is PWNSimpleLoanSimpleReq request.accruingInterestAPR = uint40(interestAPR); vm.expectRevert(abi.encodeWithSelector(AccruingInterestAPROutOfBounds.selector, interestAPR, maxInterest)); - requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", ""); + requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", "", ""); } function test_shouldRevokeRequest_whenAvailableCreditLimitEqualToZero() external { @@ -384,7 +384,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRequest_Test is PWNSimpleLoanSimpleReq ) ); - requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", ""); + requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", "", ""); } function testFuzz_shouldFail_whenUsedCreditExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { @@ -395,7 +395,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRequest_Test is PWNSimpleLoanSimpleReq vm.store(address(requestContract), keccak256(abi.encode(_requestHash(request), CREDIT_USED_SLOT)), bytes32(used)); vm.expectRevert(abi.encodeWithSelector(AvailableCreditLimitExceeded.selector, used + request.creditAmount, limit)); - requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", ""); + requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", "", ""); } function testFuzz_shouldIncreaseUsedCredit_whenUsedCreditNotExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { @@ -405,7 +405,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRequest_Test is PWNSimpleLoanSimpleReq vm.store(address(requestContract), keccak256(abi.encode(_requestHash(request), CREDIT_USED_SLOT)), bytes32(used)); - requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", ""); + requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", "", ""); assertEq(requestContract.creditUsed(_requestHash(request)), used + request.creditAmount); } @@ -413,6 +413,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRequest_Test is PWNSimpleLoanSimpleReq function test_shouldCallLoanContractWithLoanTerms() external { bytes memory creditPermit = "creditPermit"; bytes memory collateralPermit = "collateralPermit"; + bytes memory extra = "lil extra"; PWNSimpleLoan.Terms memory loanTerms = PWNSimpleLoan.Terms({ lender: lender, @@ -438,17 +439,17 @@ contract PWNSimpleLoanSimpleRequest_AcceptRequest_Test is PWNSimpleLoanSimpleReq activeLoanContract, abi.encodeWithSelector( PWNSimpleLoan.createLOAN.selector, - _requestHash(request), loanTerms, creditPermit, collateralPermit + _requestHash(request), loanTerms, creditPermit, collateralPermit, extra ) ); vm.prank(lender); - requestContract.acceptRequest(request, _signRequest(borrowerPK, request), creditPermit, collateralPermit); + requestContract.acceptRequest(request, _signRequest(borrowerPK, request), creditPermit, collateralPermit, extra); } function test_shouldReturnNewLoanId() external { assertEq( - requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", ""), + requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", "", ""), loanId ); } @@ -476,6 +477,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRequestAndRevokeCallersNonce_Test is P signature: _signRequest(borrowerPK, request), creditPermit: "", collateralPermit: "", + extra: "", callersNonceSpace: nonceSpace, callersNonceToRevoke: nonce }); @@ -493,6 +495,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRequestAndRevokeCallersNonce_Test is P signature: _signRequest(borrowerPK, request), creditPermit: "", collateralPermit: "", + extra: "", callersNonceSpace: nonceSpace, callersNonceToRevoke: nonce }); @@ -505,6 +508,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRequestAndRevokeCallersNonce_Test is P signature: _signRequest(borrowerPK, request), creditPermit: "", collateralPermit: "", + extra: "", callersNonceSpace: 1, callersNonceToRevoke: 2 }); @@ -531,7 +535,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRefinanceRequest_Test is PWNSimpleLoan request.refinancingLoanId = 0; vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, request.refinancingLoanId)); - requestContract.acceptRefinanceRequest(0, request, _signRequest(borrowerPK, request), "", ""); + requestContract.acceptRefinanceRequest(0, request, _signRequest(borrowerPK, request), "", "", ""); } function testFuzz_shouldFail_whenRefinancingLoanIdIsNotEqualToLoanId(uint256 _loanId, uint256 _refinancingLoanId) external { @@ -540,7 +544,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRefinanceRequest_Test is PWNSimpleLoan request.refinancingLoanId = _refinancingLoanId; vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, request.refinancingLoanId)); - requestContract.acceptRefinanceRequest(_loanId, request, _signRequest(borrowerPK, request), "", ""); + requestContract.acceptRefinanceRequest(_loanId, request, _signRequest(borrowerPK, request), "", "", ""); } function testFuzz_shouldFail_whenLoanContractNotTagged_ACTIVE_LOAN(address loanContract) external { @@ -548,7 +552,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRefinanceRequest_Test is PWNSimpleLoan request.loanContract = loanContract; vm.expectRevert(abi.encodeWithSelector(AddressMissingHubTag.selector, loanContract, PWNHubTags.ACTIVE_LOAN)); - requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", ""); + requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", "", ""); } function test_shouldNotCallComputerRegistry_whenShouldNotCheckStateFingerprint() external { @@ -560,7 +564,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRefinanceRequest_Test is PWNSimpleLoan count: 0 }); - requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", ""); + requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", "", ""); } function test_shouldFail_whenComputerRegistryReturnsZeroAddress_whenShouldCheckStateFingerprint() external { @@ -576,7 +580,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRefinanceRequest_Test is PWNSimpleLoan ); vm.expectRevert(abi.encodeWithSelector(MissingStateFingerprintComputer.selector)); - requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", ""); + requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", "", ""); } function testFuzz_shouldFail_whenComputerReturnsDifferentStateFingerprint_whenShouldCheckStateFingerprint( @@ -598,19 +602,19 @@ contract PWNSimpleLoanSimpleRequest_AcceptRefinanceRequest_Test is PWNSimpleLoan vm.expectRevert(abi.encodeWithSelector( InvalidCollateralStateFingerprint.selector, stateFingerprint, request.collateralStateFingerprint )); - requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", ""); + requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", "", ""); } function test_shouldFail_whenInvalidSignature_whenEOA() external { vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, request.borrower, _requestHash(request))); - requestContract.acceptRefinanceRequest(loanId, request, _signRequest(1, request), "", ""); + requestContract.acceptRefinanceRequest(loanId, request, _signRequest(1, request), "", "", ""); } function test_shouldFail_whenInvalidSignature_whenContractAccount() external { vm.etch(borrower, bytes("data")); vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, request.borrower, _requestHash(request))); - requestContract.acceptRefinanceRequest(loanId, request, "", "", ""); + requestContract.acceptRefinanceRequest(loanId, request, "", "", "", ""); } function test_shouldPass_whenRequestHasBeenMadeOnchain() external { @@ -620,15 +624,15 @@ contract PWNSimpleLoanSimpleRequest_AcceptRefinanceRequest_Test is PWNSimpleLoan bytes32(uint256(1)) ); - requestContract.acceptRefinanceRequest(loanId, request, "", "", ""); + requestContract.acceptRefinanceRequest(loanId, request, "", "", "", ""); } function test_shouldPass_withValidSignature_whenEOA_whenStandardSignature() external { - requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", ""); + requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", "", ""); } function test_shouldPass_withValidSignature_whenEOA_whenCompactEIP2098Signature() external { - requestContract.acceptRefinanceRequest(loanId, request, _signRequestCompact(borrowerPK, request), "", ""); + requestContract.acceptRefinanceRequest(loanId, request, _signRequestCompact(borrowerPK, request), "", "", ""); } function test_shouldPass_whenValidSignature_whenContractAccount() external { @@ -640,7 +644,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRefinanceRequest_Test is PWNSimpleLoan abi.encode(bytes4(0x1626ba7e)) ); - requestContract.acceptRefinanceRequest(loanId, request, "", "", ""); + requestContract.acceptRefinanceRequest(loanId, request, "", "", "", ""); } function testFuzz_shouldFail_whenRequestIsExpired(uint256 timestamp) external { @@ -648,7 +652,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRefinanceRequest_Test is PWNSimpleLoan vm.warp(timestamp); vm.expectRevert(abi.encodeWithSelector(Expired.selector, timestamp, request.expiration)); - requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", ""); + requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", "", ""); } function test_shouldFail_whenRequestNonceNotUsable() external { @@ -665,7 +669,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRefinanceRequest_Test is PWNSimpleLoan vm.expectRevert(abi.encodeWithSelector( NonceNotUsable.selector, request.borrower, request.nonceSpace, request.nonce )); - requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", ""); + requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", "", ""); } function testFuzz_shouldFail_whenCallerIsNotAllowedLender(address caller) external { @@ -675,7 +679,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRefinanceRequest_Test is PWNSimpleLoan vm.expectRevert(abi.encodeWithSelector(CallerNotAllowedAcceptor.selector, caller, request.allowedLender)); vm.prank(caller); - requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", ""); + requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", "", ""); } function testFuzz_shouldFail_whenLessThanMinDuration(uint256 duration) external { @@ -684,7 +688,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRefinanceRequest_Test is PWNSimpleLoan request.duration = uint32(duration); vm.expectRevert(abi.encodeWithSelector(InvalidDuration.selector, duration, requestContract.MIN_LOAN_DURATION())); - requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", ""); + requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", "", ""); } function testFuzz_shouldFail_whenAccruingInterestAPROutOfBounds(uint256 interestAPR) external { @@ -693,7 +697,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRefinanceRequest_Test is PWNSimpleLoan request.accruingInterestAPR = uint40(interestAPR); vm.expectRevert(abi.encodeWithSelector(AccruingInterestAPROutOfBounds.selector, interestAPR, maxInterest)); - requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", ""); + requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", "", ""); } function test_shouldRevokeRequest_whenAvailableCreditLimitEqualToZero() external { @@ -706,7 +710,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRefinanceRequest_Test is PWNSimpleLoan ) ); - requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", ""); + requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", "", ""); } function testFuzz_shouldFail_whenUsedCreditExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { @@ -717,7 +721,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRefinanceRequest_Test is PWNSimpleLoan vm.store(address(requestContract), keccak256(abi.encode(_requestHash(request), CREDIT_USED_SLOT)), bytes32(used)); vm.expectRevert(abi.encodeWithSelector(AvailableCreditLimitExceeded.selector, used + request.creditAmount, limit)); - requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", ""); + requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", "", ""); } function testFuzz_shouldIncreaseUsedCredit_whenUsedCreditNotExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { @@ -727,7 +731,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRefinanceRequest_Test is PWNSimpleLoan vm.store(address(requestContract), keccak256(abi.encode(_requestHash(request), CREDIT_USED_SLOT)), bytes32(used)); - requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", ""); + requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", "", ""); assertEq(requestContract.creditUsed(_requestHash(request)), used + request.creditAmount); } @@ -735,6 +739,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRefinanceRequest_Test is PWNSimpleLoan function test_shouldCallLoanContract() external { bytes memory creditPermit = "creditPermit"; bytes memory collateralPermit = "collateralPermit"; + bytes memory extra = "lil extra"; PWNSimpleLoan.Terms memory loanTerms = PWNSimpleLoan.Terms({ lender: lender, @@ -760,19 +765,19 @@ contract PWNSimpleLoanSimpleRequest_AcceptRefinanceRequest_Test is PWNSimpleLoan activeLoanContract, abi.encodeWithSelector( PWNSimpleLoan.refinanceLOAN.selector, - loanId, _requestHash(request), loanTerms, creditPermit, collateralPermit + loanId, _requestHash(request), loanTerms, creditPermit, collateralPermit, extra ) ); vm.prank(lender); requestContract.acceptRefinanceRequest( - loanId, request, _signRequest(borrowerPK, request), creditPermit, collateralPermit + loanId, request, _signRequest(borrowerPK, request), creditPermit, collateralPermit, extra ); } function test_shouldReturnRefinancedLoanId() external { assertEq( - requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", ""), + requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", "", ""), refinancedLoanId ); } @@ -807,6 +812,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRefinanceRequestAndRevokeCallersNonce_ signature: _signRequest(borrowerPK, request), lenderCreditPermit: "", borrowerCreditPermit: "", + extra: "", callersNonceSpace: nonceSpace, callersNonceToRevoke: nonce }); @@ -825,6 +831,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRefinanceRequestAndRevokeCallersNonce_ signature: _signRequest(borrowerPK, request), lenderCreditPermit: "", borrowerCreditPermit: "", + extra: "", callersNonceSpace: nonceSpace, callersNonceToRevoke: nonce }); @@ -838,6 +845,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRefinanceRequestAndRevokeCallersNonce_ signature: _signRequest(borrowerPK, request), lenderCreditPermit: "", borrowerCreditPermit: "", + extra: "", callersNonceSpace: 1, callersNonceToRevoke: 2 }); From 0597cddf64aaa23a84c9364361cf0da4c5801c23 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Fri, 15 Mar 2024 15:57:27 +0100 Subject: [PATCH 045/129] feat: update how permit is handled --- foundry.toml | 1 - src/PWNErrors.sol | 2 + src/loan/terms/simple/loan/PWNSimpleLoan.sol | 99 ++--- .../simple/proposal/PWNSimpleLoanProposal.sol | 18 + .../proposal/offer/PWNSimpleLoanListOffer.sol | 43 +- .../offer/PWNSimpleLoanSimpleOffer.sol | 31 +- .../request/PWNSimpleLoanSimpleRequest.sol | 31 +- src/loan/{ => vault}/PWNVault.sol | 50 ++- src/loan/vault/Permit.sol | 22 + test/unit/PWNSimpleLoan.t.sol | 401 +++++++----------- test/unit/PWNSimpleLoanListOffer.t.sol | 193 ++++++--- test/unit/PWNSimpleLoanSimpleOffer.t.sol | 181 +++++--- test/unit/PWNSimpleLoanSimpleRequest.t.sol | 177 +++++--- test/unit/PWNVault.t.sol | 83 ++-- 14 files changed, 717 insertions(+), 615 deletions(-) rename src/loan/{ => vault}/PWNVault.sol (81%) create mode 100644 src/loan/vault/Permit.sol diff --git a/foundry.toml b/foundry.toml index 852286b..6418914 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,7 +1,6 @@ [profile.default] solc_version = '0.8.16' fs_permissions = [{ access = "read", path = "./deployments.json"}] -via_ir = true [rpc_endpoints] diff --git a/src/PWNErrors.sol b/src/PWNErrors.sol index d63313d..bddef09 100644 --- a/src/PWNErrors.sol +++ b/src/PWNErrors.sol @@ -52,6 +52,8 @@ error AccruingInterestAPROutOfBounds(uint256 current, uint256 limit); error AvailableCreditLimitExceeded(uint256 used, uint256 limit); error Expired(uint256 current, uint256 expiration); error CallerNotAllowedAcceptor(address current, address allowed); +error InvalidPermitOwner(address current, address expected); +error InvalidPermitAsset(address current, address expected); // Input data error InvalidInputData(); diff --git a/src/loan/terms/simple/loan/PWNSimpleLoan.sol b/src/loan/terms/simple/loan/PWNSimpleLoan.sol index 4a849b0..ece6578 100644 --- a/src/loan/terms/simple/loan/PWNSimpleLoan.sol +++ b/src/loan/terms/simple/loan/PWNSimpleLoan.sol @@ -15,7 +15,8 @@ import { PWNSimpleLoanProposal } from "@pwn/loan/terms/simple/proposal/PWNSimple import { IERC5646 } from "@pwn/loan/token/IERC5646.sol"; import { IPWNLoanMetadataProvider } from "@pwn/loan/token/IPWNLoanMetadataProvider.sol"; import { PWNLOAN } from "@pwn/loan/token/PWNLOAN.sol"; -import { PWNVault } from "@pwn/loan/PWNVault.sol"; +import { Permit } from "@pwn/loan/vault/Permit.sol"; +import { PWNVault } from "@pwn/loan/vault/PWNVault.sol"; import { PWNRevokedNonce } from "@pwn/nonce/PWNRevokedNonce.sol"; import "@pwn/PWNErrors.sol"; @@ -204,16 +205,14 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { * @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 creditPermit Permit data for a credit asset signed by a lender. - * @param collateralPermit Permit data for a collateral signed by a borrower. + * @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 loanId Id of the created LOAN token. */ function createLOAN( bytes32 proposalHash, Terms calldata loanTerms, - bytes calldata creditPermit, - bytes calldata collateralPermit, + Permit calldata permit, bytes calldata extra ) external returns (uint256 loanId) { // Check that caller is loan proposal contract @@ -233,7 +232,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { }); // Transfer collateral to Vault and credit to borrower - _settleNewLoan(loanTerms, creditPermit, collateralPermit); + _settleNewLoan(loanTerms, permit); } /** @@ -291,21 +290,18 @@ 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 creditPermit Permit data for a credit asset signed by a lender. - * @param collateralPermit Permit data for a collateral signed by a borrower. + * @param permit Callers credit permit data. */ function _settleNewLoan( Terms calldata loanTerms, - bytes calldata creditPermit, - bytes calldata collateralPermit + Permit calldata permit ) private { + // Execute permit for the caller + _tryPermit(permit); + // Transfer collateral to Vault - _permit(loanTerms.collateral, loanTerms.borrower, collateralPermit); _pull(loanTerms.collateral, loanTerms.borrower); - // Permit credit spending if permit provided - _permit(loanTerms.credit, loanTerms.lender, creditPermit); - MultiToken.Asset memory creditHelper = loanTerms.credit; // Collect fee if any and update credit asset amount @@ -339,8 +335,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { * 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 lenderCreditPermit Permit data for a credit asset signed by a lender. - * @param borrowerCreditPermit Permit data for a credit asset signed by a borrower. + * @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. */ @@ -348,8 +343,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { uint256 loanId, bytes32 proposalHash, Terms calldata loanTerms, - bytes calldata lenderCreditPermit, - bytes calldata borrowerCreditPermit, + Permit calldata permit, bytes calldata extra ) external returns (uint256 refinancedLoanId) { // Check that caller is loan proposal contract @@ -374,12 +368,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { }); // Refinance the original loan - _refinanceOriginalLoan( - loanId, - loanTerms, - lenderCreditPermit, - borrowerCreditPermit - ); + _refinanceOriginalLoan(loanId, loanTerms, permit); emit LOANRefinanced({ loanId: loanId, refinancedLoanId: refinancedLoanId }); } @@ -426,14 +415,12 @@ 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 lenderCreditPermit Permit data for a credit asset signed by a lender. - * @param borrowerCreditPermit Permit data for a credit asset signed by a borrower. + * @param permit Callers credit permit data. */ function _refinanceOriginalLoan( uint256 loanId, Terms calldata loanTerms, - bytes calldata lenderCreditPermit, - bytes calldata borrowerCreditPermit + Permit calldata permit ) private { uint256 repaymentAmount = _loanRepaymentAmount(loanId); @@ -446,8 +433,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { loanOwner: loanOwner, repaymentAmount: repaymentAmount, loanTerms: loanTerms, - lenderPermit: lenderCreditPermit, - borrowerPermit: borrowerCreditPermit + permit: permit }); } @@ -460,40 +446,31 @@ 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 lenderPermit Permit data for a credit asset signed by a lender. - * @param borrowerPermit Permit data for a credit asset signed by a borrower. + * @param permit Callers credit permit data. */ function _settleLoanRefinance( bool repayLoanDirectly, address loanOwner, uint256 repaymentAmount, Terms calldata loanTerms, - bytes calldata lenderPermit, - bytes calldata borrowerPermit + Permit calldata permit ) private { MultiToken.Asset memory creditHelper = loanTerms.credit; - // Compute fee size - (uint256 feeAmount, uint256 newLoanAmount) - = PWNFeeCalculator.calculateFeeAmount(config.fee(), loanTerms.credit.amount); - - // Permit lenders credit spending if permit provided - creditHelper.amount -= loanTerms.lender == loanOwner // Permit only the surplus transfer + fee - ? Math.min(repaymentAmount, newLoanAmount) - : 0; - - if (creditHelper.amount > 0) { - _permit(creditHelper, loanTerms.lender, lenderPermit); - } + // Execute permit for the caller + _tryPermit(permit); // Collect fees + (uint256 feeAmount, uint256 newLoanAmount) + = PWNFeeCalculator.calculateFeeAmount(config.fee(), loanTerms.credit.amount); if (feeAmount > 0) { creditHelper.amount = feeAmount; _pushFrom(creditHelper, loanTerms.lender, config.feeCollector()); } - // If the new lender is the LOAN token owner, don't execute the transfer at all, - // it would make transfer from the same address to the same address + // New lender repays the original loan + // Note: If the new lender is the LOAN token owner, don't execute the transfer at all, + // it would make transfer from the same address to the same address. if (loanTerms.lender != loanOwner) { creditHelper.amount = Math.min(repaymentAmount, newLoanAmount); _transferLoanRepayment({ @@ -504,6 +481,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { }); } + // Handle the surplus or the missing amount if (newLoanAmount >= repaymentAmount) { // New loan covers the whole original loan, transfer surplus to the borrower if any uint256 surplus = newLoanAmount - repaymentAmount; @@ -512,11 +490,8 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { _pushFrom(creditHelper, loanTerms.lender, loanTerms.borrower); } } else { - // Permit borrowers credit spending if permit provided - creditHelper.amount = repaymentAmount - newLoanAmount; - _permit(creditHelper, loanTerms.borrower, borrowerPermit); - // New loan covers only part of the original loan, borrower needs to contribute + creditHelper.amount = repaymentAmount - newLoanAmount; _transferLoanRepayment({ repayLoanDirectly: repayLoanDirectly || loanTerms.lender == loanOwner, repaymentCredit: creditHelper, @@ -539,11 +514,11 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { * a vault, waiting on a LOAN token holder to claim it. The function assumes a prior token approval to a contract address * or a signed permit. * @param loanId Id of a loan that is being repaid. - * @param creditPermit Permit data for a credit asset signed by a borrower. + * @param permit Callers credit permit data. */ function repayLOAN( uint256 loanId, - bytes calldata creditPermit + Permit calldata permit ) external { LOAN storage loan = LOANs[loanId]; @@ -562,7 +537,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { borrower: borrower, repaymentCredit: repaymentCredit, collateral: collateral, - creditPermit: creditPermit + permit: permit }); } @@ -626,7 +601,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { * @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 creditPermit Permit data for a credit asset signed by a borrower. + * @param permit Callers credit permit data. */ function _settleLoanRepayment( bool repayLoanDirectly, @@ -635,10 +610,12 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { address borrower, MultiToken.Asset memory repaymentCredit, MultiToken.Asset memory collateral, - bytes calldata creditPermit + Permit calldata permit ) private { + // Execute permit for the caller + _tryPermit(permit); + // Transfer credit to the original lender or to the Vault - _permit(repaymentCredit, repayingAddress, creditPermit); _transferLoanRepayment(repayLoanDirectly, repaymentCredit, repayingAddress, loanOwner); // Transfer collateral back to borrower @@ -804,12 +781,12 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { * @dev The function assumes a prior token approval to a contract address or a signed permit. * @param extension Extension proposal struct. * @param signature Signature of the extension proposal. - * @param compensationPermit Permit data for a fungible compensation asset signed by a borrower. + * @param permit Callers permit. */ function extendLOAN( ExtensionProposal calldata extension, bytes calldata signature, - bytes calldata compensationPermit + Permit calldata permit ) external { LOAN storage loan = LOANs[extension.loanId]; @@ -892,7 +869,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { _checkValidAsset(compensation); // Transfer compensation to the loan owner - _permit(compensation, loan.borrower, compensationPermit); + _tryPermit(permit); _pushFrom(compensation, loan.borrower, loanOwner); } } diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol index 1005761..cfff0ae 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.16; 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 { PWNRevokedNonce } from "@pwn/nonce/PWNRevokedNonce.sol"; import { StateFingerprintComputerRegistry, IERC5646 } from "@pwn/state-fingerprint/StateFingerprintComputerRegistry.sol"; import "@pwn/PWNErrors.sol"; @@ -185,6 +186,23 @@ abstract contract PWNSimpleLoanProposal { } } + /** + * @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 }); + } + } + } + /** * @notice Make an on-chain proposal. * @dev Function will mark a proposal hash as proposed. diff --git a/src/loan/terms/simple/proposal/offer/PWNSimpleLoanListOffer.sol b/src/loan/terms/simple/proposal/offer/PWNSimpleLoanListOffer.sol index c01fd9e..f12fac1 100644 --- a/src/loan/terms/simple/proposal/offer/PWNSimpleLoanListOffer.sol +++ b/src/loan/terms/simple/proposal/offer/PWNSimpleLoanListOffer.sol @@ -7,6 +7,7 @@ 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"; @@ -115,8 +116,7 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanProposal { * @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 creditPermit Permit signature for a credit asset. - * @param collateralPermit Permit signature for a collateral asset. + * @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. */ @@ -124,8 +124,7 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanProposal { Offer calldata offer, OfferValues calldata offerValues, bytes calldata signature, - bytes calldata creditPermit, - bytes calldata collateralPermit, + Permit calldata permit, bytes calldata extra ) public returns (uint256 loanId) { // Check if the offer is refinancing offer @@ -133,14 +132,17 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanProposal { 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, - creditPermit: creditPermit, - collateralPermit: collateralPermit, + permit: permit, extra: extra }); } @@ -152,8 +154,7 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanProposal { * @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 lenderCreditPermit Lenders permit signature for a credit asset. - * @param borrowerCreditPermit Borrowers permit signature for a credit asset. + * @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. */ @@ -162,8 +163,7 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanProposal { Offer calldata offer, OfferValues calldata offerValues, bytes calldata signature, - bytes calldata lenderCreditPermit, - bytes calldata borrowerCreditPermit, + Permit calldata permit, bytes calldata extra ) public returns (uint256 refinancedLoanId) { // Check if the offer is refinancing offer @@ -171,6 +171,10 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanProposal { 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 @@ -178,8 +182,7 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanProposal { loanId: loanId, proposalHash: offerHash, loanTerms: loanTerms, - lenderCreditPermit: lenderCreditPermit, - borrowerCreditPermit: borrowerCreditPermit, + permit: permit, extra: extra }); } @@ -190,8 +193,7 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanProposal { * @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 creditPermit Permit signature for a credit asset. - * @param collateralPermit Permit signature for a collateral asset. + * @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. @@ -201,14 +203,13 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanProposal { Offer calldata offer, OfferValues calldata offerValues, bytes calldata signature, - bytes calldata creditPermit, - bytes calldata collateralPermit, + Permit calldata permit, bytes calldata extra, uint256 callersNonceSpace, uint256 callersNonceToRevoke ) external returns (uint256 loanId) { _revokeCallersNonce(msg.sender, callersNonceSpace, callersNonceToRevoke); - return acceptOffer(offer, offerValues, signature, creditPermit, collateralPermit, extra); + return acceptOffer(offer, offerValues, signature, permit, extra); } /** @@ -218,8 +219,7 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanProposal { * @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 lenderCreditPermit Lenders permit signature for a credit asset. - * @param borrowerCreditPermit Borrowers permit signature for a credit asset. + * @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. @@ -230,14 +230,13 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanProposal { Offer calldata offer, OfferValues calldata offerValues, bytes calldata signature, - bytes calldata lenderCreditPermit, - bytes calldata borrowerCreditPermit, + 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, lenderCreditPermit, borrowerCreditPermit, extra); + return acceptRefinanceOffer(loanId, offer, offerValues, signature, permit, extra); } diff --git a/src/loan/terms/simple/proposal/offer/PWNSimpleLoanSimpleOffer.sol b/src/loan/terms/simple/proposal/offer/PWNSimpleLoanSimpleOffer.sol index 73d0c49..a736977 100644 --- a/src/loan/terms/simple/proposal/offer/PWNSimpleLoanSimpleOffer.sol +++ b/src/loan/terms/simple/proposal/offer/PWNSimpleLoanSimpleOffer.sol @@ -5,6 +5,7 @@ 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"; @@ -97,8 +98,7 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanProposal { function acceptOffer( Offer calldata offer, bytes calldata signature, - bytes calldata creditPermit, - bytes calldata collateralPermit, + Permit calldata permit, bytes calldata extra ) public returns (uint256 loanId) { // Check if the offer is refinancing offer @@ -106,14 +106,17 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanProposal { revert InvalidRefinancingLoanId({ refinancingLoanId: offer.refinancingLoanId }); } + // Check permit + _checkPermit(msg.sender, offer.creditAddress, permit); + + // Accept offer (bytes32 offerHash, PWNSimpleLoan.Terms memory loanTerms) = _acceptOffer(offer, signature); // Create loan return PWNSimpleLoan(offer.loanContract).createLOAN({ proposalHash: offerHash, loanTerms: loanTerms, - creditPermit: creditPermit, - collateralPermit: collateralPermit, + permit: permit, extra: extra }); } @@ -122,8 +125,7 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanProposal { uint256 loanId, Offer calldata offer, bytes calldata signature, - bytes calldata lenderCreditPermit, - bytes calldata borrowerCreditPermit, + Permit calldata permit, bytes calldata extra ) public returns (uint256 refinancedLoanId) { // Check if the offer is refinancing offer @@ -131,6 +133,10 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanProposal { revert InvalidRefinancingLoanId({ refinancingLoanId: offer.refinancingLoanId }); } + // Check permit + _checkPermit(msg.sender, offer.creditAddress, permit); + + // Accept offer (bytes32 offerHash, PWNSimpleLoan.Terms memory loanTerms) = _acceptOffer(offer, signature); // Refinance loan @@ -138,8 +144,7 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanProposal { loanId: loanId, proposalHash: offerHash, loanTerms: loanTerms, - lenderCreditPermit: lenderCreditPermit, - borrowerCreditPermit: borrowerCreditPermit, + permit: permit, extra: extra }); } @@ -147,28 +152,26 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanProposal { function acceptOffer( Offer calldata offer, bytes calldata signature, - bytes calldata creditPermit, - bytes calldata collateralPermit, + Permit calldata permit, bytes calldata extra, uint256 callersNonceSpace, uint256 callersNonceToRevoke ) external returns (uint256 loanId) { _revokeCallersNonce(msg.sender, callersNonceSpace, callersNonceToRevoke); - return acceptOffer(offer, signature, creditPermit, collateralPermit, extra); + return acceptOffer(offer, signature, permit, extra); } function acceptRefinanceOffer( uint256 loanId, Offer calldata offer, bytes calldata signature, - bytes calldata lenderCreditPermit, - bytes calldata borrowerCreditPermit, + Permit calldata permit, bytes calldata extra, uint256 callersNonceSpace, uint256 callersNonceToRevoke ) external returns (uint256 refinancedLoanId) { _revokeCallersNonce(msg.sender, callersNonceSpace, callersNonceToRevoke); - return acceptRefinanceOffer(loanId, offer, signature, lenderCreditPermit, borrowerCreditPermit, extra); + return acceptRefinanceOffer(loanId, offer, signature, permit, extra); } diff --git a/src/loan/terms/simple/proposal/request/PWNSimpleLoanSimpleRequest.sol b/src/loan/terms/simple/proposal/request/PWNSimpleLoanSimpleRequest.sol index c7dcb86..e4c0884 100644 --- a/src/loan/terms/simple/proposal/request/PWNSimpleLoanSimpleRequest.sol +++ b/src/loan/terms/simple/proposal/request/PWNSimpleLoanSimpleRequest.sol @@ -5,6 +5,7 @@ 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"; @@ -98,8 +99,7 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanProposal { function acceptRequest( Request calldata request, bytes calldata signature, - bytes calldata creditPermit, - bytes calldata collateralPermit, + Permit calldata permit, bytes calldata extra ) public returns (uint256 loanId) { // Check if the request is refinancing request @@ -107,14 +107,17 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanProposal { revert InvalidRefinancingLoanId({ refinancingLoanId: request.refinancingLoanId }); } + // Check permit + _checkPermit(msg.sender, request.creditAddress, permit); + + // Accept request (bytes32 requestHash, PWNSimpleLoan.Terms memory loanTerms) = _acceptRequest(request, signature); // Create loan return PWNSimpleLoan(request.loanContract).createLOAN({ proposalHash: requestHash, loanTerms: loanTerms, - creditPermit: creditPermit, - collateralPermit: collateralPermit, + permit: permit, extra: extra }); } @@ -123,8 +126,7 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanProposal { uint256 loanId, Request calldata request, bytes calldata signature, - bytes calldata lenderCreditPermit, - bytes calldata borrowerCreditPermit, + Permit calldata permit, bytes calldata extra ) public returns (uint256 refinancedLoanId) { // Check if the request is refinancing request @@ -132,6 +134,10 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanProposal { revert InvalidRefinancingLoanId({ refinancingLoanId: request.refinancingLoanId }); } + // Check permit + _checkPermit(msg.sender, request.creditAddress, permit); + + // Accept request (bytes32 requestHash, PWNSimpleLoan.Terms memory loanTerms) = _acceptRequest(request, signature); // Refinance loan @@ -139,8 +145,7 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanProposal { loanId: loanId, proposalHash: requestHash, loanTerms: loanTerms, - lenderCreditPermit: lenderCreditPermit, - borrowerCreditPermit: borrowerCreditPermit, + permit: permit, extra: extra }); } @@ -148,28 +153,26 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanProposal { function acceptRequest( Request calldata request, bytes calldata signature, - bytes calldata creditPermit, - bytes calldata collateralPermit, + Permit calldata permit, bytes calldata extra, uint256 callersNonceSpace, uint256 callersNonceToRevoke ) external returns (uint256 loanId) { _revokeCallersNonce(msg.sender, callersNonceSpace, callersNonceToRevoke); - return acceptRequest(request, signature, creditPermit, collateralPermit, extra); + return acceptRequest(request, signature, permit, extra); } function acceptRefinanceRequest( uint256 loanId, Request calldata request, bytes calldata signature, - bytes calldata lenderCreditPermit, - bytes calldata borrowerCreditPermit, + Permit calldata permit, bytes calldata extra, uint256 callersNonceSpace, uint256 callersNonceToRevoke ) external returns (uint256 refinancedLoanId) { _revokeCallersNonce(msg.sender, callersNonceSpace, callersNonceToRevoke); - return acceptRefinanceRequest(loanId, request, signature, lenderCreditPermit, borrowerCreditPermit, extra); + return acceptRefinanceRequest(loanId, request, signature, permit, extra); } diff --git a/src/loan/PWNVault.sol b/src/loan/vault/PWNVault.sol similarity index 81% rename from src/loan/PWNVault.sol rename to src/loan/vault/PWNVault.sol index aca9930..55aa8bc 100644 --- a/src/loan/PWNVault.sol +++ b/src/loan/vault/PWNVault.sol @@ -1,11 +1,13 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; -import "MultiToken/MultiToken.sol"; +import { MultiToken } from "MultiToken/MultiToken.sol"; -import "openzeppelin-contracts/contracts/token/ERC721/IERC721Receiver.sol"; -import "openzeppelin-contracts/contracts/token/ERC1155/IERC1155Receiver.sol"; +import { IERC20Permit } from "openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Permit.sol"; +import { IERC721Receiver } from "openzeppelin-contracts/contracts/token/ERC721/IERC721Receiver.sol"; +import { IERC1155Receiver, IERC165 } from "openzeppelin-contracts/contracts/token/ERC1155/IERC1155Receiver.sol"; +import { Permit } from "@pwn/loan/vault/Permit.sol"; import "@pwn/PWNErrors.sol"; @@ -42,9 +44,8 @@ abstract contract PWNVault is IERC721Receiver, IERC1155Receiver { |*----------------------------------------------------------*/ /** - * pull - * @dev Function accessing an asset and pulling it INTO a vault. - * The function assumes a prior token approval was made to a vault address. + * @notice Function pulling an asset into a vault. + * @dev The function assumes a prior token approval to a vault address. * @param asset An asset construct - for a definition see { MultiToken dependency lib }. * @param origin Borrower address that is transferring collateral to Vault or repaying a loan. */ @@ -58,9 +59,8 @@ abstract contract PWNVault is IERC721Receiver, IERC1155Receiver { } /** - * push - * @dev Function pushing an asset FROM a vault TO a defined recipient. - * This is used for claiming a paid back loan or a defaulted collateral, or returning collateral to a borrower. + * @notice Function pushing an asset from a vault to a recipient. + * @dev This is used for claiming a paid back loan or a defaulted collateral, or returning collateral to a borrower. * @param asset An asset construct - for a definition see { MultiToken dependency lib }. * @param beneficiary An address of a recipient of an asset. */ @@ -74,9 +74,8 @@ abstract contract PWNVault is IERC721Receiver, IERC1155Receiver { } /** - * pushFrom - * @dev Function pushing an asset FROM a lender TO a borrower. - * The function assumes a prior token approval was made to a vault address. + * @notice Function pushing an asset from an origin address to a beneficiary address. + * @dev The function assumes a prior token approval to a vault address. * @param asset An asset construct - for a definition see { MultiToken dependency lib }. * @param origin An address of a lender who is providing a loan asset. * @param beneficiary An address of the recipient of an asset. @@ -101,17 +100,24 @@ abstract contract PWNVault is IERC721Receiver, IERC1155Receiver { |*----------------------------------------------------------*/ /** - * permit - * @dev Function uses signed permit data to set vaults allowance for an asset. - * @param asset An asset construct - for a definition see { MultiToken dependency lib }. - * @param origin An address who is approving an asset. - * @param permit Data about permit deadline (uint256) and permit signature (64/65 bytes). - * Deadline and signature should be pack encoded together. - * Signature can be standard (65 bytes) or compact (64 bytes) defined in EIP-2098. + * @notice Try to execute a permit for an ERC20 token. + * @dev If the permit execution fails, the function will not revert. + * @param permit The permit data. */ - function _permit(MultiToken.Asset memory asset, address origin, bytes memory permit) internal { - if (permit.length > 0) - asset.permit(origin, address(this), permit); + function _tryPermit(Permit calldata permit) internal { + if (permit.asset != address(0)) { + try IERC20Permit(permit.asset).permit({ + owner: permit.owner, + spender: address(this), + value: permit.amount, + deadline: permit.deadline, + v: permit.v, + r: permit.r, + s: permit.s + }) {} catch { + // Note: Permit execution can be frontrun, so we don't revert on failure. + } + } } diff --git a/src/loan/vault/Permit.sol b/src/loan/vault/Permit.sol new file mode 100644 index 0000000..de430d1 --- /dev/null +++ b/src/loan/vault/Permit.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.16; + +/** + * @notice Struct to hold the permit data. + * @param asset The address of the ERC20 token. + * @param owner The owner of the tokens. + * @param amount The amount of tokens. + * @param deadline The deadline for the permit. + * @param v The v value of the signature. + * @param r The r value of the signature. + * @param s The s value of the signature. + */ +struct Permit { + address asset; + address owner; + uint256 amount; + uint256 deadline; + uint8 v; + bytes32 r; + bytes32 s; +} diff --git a/test/unit/PWNSimpleLoan.t.sol b/test/unit/PWNSimpleLoan.t.sol index fa61dc7..c030c41 100644 --- a/test/unit/PWNSimpleLoan.t.sol +++ b/test/unit/PWNSimpleLoan.t.sol @@ -3,12 +3,7 @@ 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 { PWNSimpleLoan } from "@pwn/loan/terms/simple/loan/PWNSimpleLoan.sol"; +import { PWNSimpleLoan, PWNHubTags, Math, MultiToken, Permit } from "@pwn/loan/terms/simple/loan/PWNSimpleLoan.sol"; import "@pwn/PWNErrors.sol"; import { T20 } from "@pwn-test/helper/token/T20.sol"; @@ -39,9 +34,8 @@ abstract contract PWNSimpleLoanTest is Test { PWNSimpleLoan.ExtensionProposal extension; T20 fungibleAsset; T721 nonFungibleAsset; + Permit permit; - bytes creditPermit = abi.encodePacked(uint256(1), uint256(2), uint256(3), uint8(4)); - bytes collateralPermit = abi.encodePacked(uint256(1), uint256(2), uint256(3), uint8(4)); bytes32 proposalHash = keccak256("proposalHash"); event LOANCreated(uint256 indexed loanId, PWNSimpleLoan.Terms terms, bytes32 indexed proposalHash, address indexed proposalContract, bytes extra); @@ -281,8 +275,7 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { loan.createLOAN({ proposalHash: proposalHash, loanTerms: simpleLoanTerms, - creditPermit: "", - collateralPermit: "", + permit: permit, extra: "" }); } @@ -307,8 +300,7 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { loan.createLOAN({ proposalHash: proposalHash, loanTerms: simpleLoanTerms, - creditPermit: "", - collateralPermit: "", + permit: permit, extra: "" }); } @@ -333,8 +325,7 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { loan.createLOAN({ proposalHash: proposalHash, loanTerms: simpleLoanTerms, - creditPermit: "", - collateralPermit: "", + permit: permit, extra: "" }); } @@ -346,8 +337,7 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { loan.createLOAN({ proposalHash: proposalHash, loanTerms: simpleLoanTerms, - creditPermit: "", - collateralPermit: "", + permit: permit, extra: "" }); } @@ -360,8 +350,7 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { loan.createLOAN({ proposalHash: proposalHash, loanTerms: simpleLoanTerms, - creditPermit: "", - collateralPermit: "", + permit: permit, extra: "" }); @@ -369,19 +358,38 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { _assertLOANEq(loanId, simpleLoan); } - function test_shouldTransferCollateral_fromBorrower_toVault() external { - simpleLoanTerms.collateral.category = MultiToken.Category.ERC20; - simpleLoanTerms.collateral.assetAddress = address(fungibleAsset); - simpleLoanTerms.collateral.id = 0; - simpleLoanTerms.collateral.amount = 100; + function test_shouldCallPermit_whenProvided() external { + permit.asset = simpleLoan.creditAddress; + permit.owner = borrower; + permit.amount = 101; + permit.deadline = 1; + permit.v = 4; + permit.r = bytes32(uint256(2)); + permit.s = bytes32(uint256(3)); vm.expectCall( - simpleLoanTerms.collateral.assetAddress, + permit.asset, abi.encodeWithSignature( "permit(address,address,uint256,uint256,uint8,bytes32,bytes32)", - borrower, address(loan), simpleLoanTerms.collateral.amount, 1, uint8(4), uint256(2), uint256(3) + permit.owner, address(loan), permit.amount, permit.deadline, permit.v, permit.r, permit.s ) ); + + vm.prank(proposalContract); + loan.createLOAN({ + proposalHash: proposalHash, + loanTerms: simpleLoanTerms, + permit: permit, + extra: "" + }); + } + + function test_shouldTransferCollateral_fromBorrower_toVault() external { + simpleLoanTerms.collateral.category = MultiToken.Category.ERC20; + simpleLoanTerms.collateral.assetAddress = address(fungibleAsset); + simpleLoanTerms.collateral.id = 0; + simpleLoanTerms.collateral.amount = 100; + vm.expectCall( simpleLoanTerms.collateral.assetAddress, abi.encodeWithSignature("transferFrom(address,address,uint256)", borrower, address(loan), simpleLoanTerms.collateral.amount) @@ -391,8 +399,7 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { loan.createLOAN({ proposalHash: proposalHash, loanTerms: simpleLoanTerms, - creditPermit: "", - collateralPermit: collateralPermit, + permit: permit, extra: "" }); } @@ -411,13 +418,6 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { uint256 feeAmount = Math.mulDiv(loanAmount, fee, 1e4); uint256 newAmount = loanAmount - feeAmount; - vm.expectCall( - simpleLoanTerms.credit.assetAddress, - abi.encodeWithSignature( - "permit(address,address,uint256,uint256,uint8,bytes32,bytes32)", - lender, address(loan), loanAmount, 1, uint8(4), uint256(2), uint256(3) - ) - ); // Fee transfer vm.expectCall({ callee: simpleLoanTerms.credit.assetAddress, @@ -434,8 +434,7 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { loan.createLOAN({ proposalHash: proposalHash, loanTerms: simpleLoanTerms, - creditPermit: creditPermit, - collateralPermit: "", + permit: permit, extra: "" }); } @@ -448,8 +447,7 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { loan.createLOAN({ proposalHash: proposalHash, loanTerms: simpleLoanTerms, - creditPermit: "", - collateralPermit: "", + permit: permit, extra: "lil extra" }); } @@ -459,8 +457,7 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { uint256 createdLoanId = loan.createLOAN({ proposalHash: proposalHash, loanTerms: simpleLoanTerms, - creditPermit: "", - collateralPermit: "", + permit: permit, extra: "" }); @@ -528,8 +525,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderCreditPermit: "", - borrowerCreditPermit: "", + permit: permit, extra: "" }); } @@ -544,8 +540,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderCreditPermit: "", - borrowerCreditPermit: "", + permit: permit, extra: "" }); } @@ -560,8 +555,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderCreditPermit: "", - borrowerCreditPermit: "", + permit: permit, extra: "" }); } @@ -575,8 +569,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderCreditPermit: "", - borrowerCreditPermit: "", + permit: permit, extra: "" }); } @@ -591,8 +584,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderCreditPermit: "", - borrowerCreditPermit: "", + permit: permit, extra: "" }); } @@ -606,8 +598,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderCreditPermit: "", - borrowerCreditPermit: "", + permit: permit, extra: "" }); } @@ -623,8 +614,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderCreditPermit: "", - borrowerCreditPermit: "", + permit: permit, extra: "" }); } @@ -639,8 +629,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderCreditPermit: "", - borrowerCreditPermit: "", + permit: permit, extra: "" }); } @@ -655,8 +644,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderCreditPermit: "", - borrowerCreditPermit: "", + permit: permit, extra: "" }); } @@ -671,8 +659,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderCreditPermit: "", - borrowerCreditPermit: "", + permit: permit, extra: "" }); } @@ -687,8 +674,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderCreditPermit: "", - borrowerCreditPermit: "", + permit: permit, extra: "" }); } @@ -701,8 +687,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderCreditPermit: "", - borrowerCreditPermit: "", + permit: permit, extra: "" }); } @@ -713,8 +698,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderCreditPermit: "", - borrowerCreditPermit: "", + permit: permit, extra: "" }); @@ -730,8 +714,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderCreditPermit: "", - borrowerCreditPermit: "", + permit: permit, extra: "lil extra" }); } @@ -742,8 +725,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderCreditPermit: "", - borrowerCreditPermit: "", + permit: permit, extra: "" }); @@ -759,8 +741,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderCreditPermit: "", - borrowerCreditPermit: "", + permit: permit, extra: "" }); } @@ -774,8 +755,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderCreditPermit: "", - borrowerCreditPermit: "", + permit: permit, extra: "" }); } @@ -786,8 +766,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderCreditPermit: "", - borrowerCreditPermit: "", + permit: permit, extra: "" }); @@ -803,8 +782,34 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderCreditPermit: "", - borrowerCreditPermit: "", + 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, extra: "" }); } @@ -834,8 +839,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderCreditPermit: "", - borrowerCreditPermit: "", + permit: permit, extra: "" }); @@ -867,14 +871,6 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { fungibleAsset.mint(newLender, refinanceAmount); - vm.expectCall( // lender permit - refinancedLoanTerms.credit.assetAddress, - abi.encodeWithSignature( - "permit(address,address,uint256,uint256,uint8,bytes32,bytes32)", - newLender, address(loan), refinanceAmount, 1, uint8(4), uint256(2), uint256(3) - ) - ); - // no borrower permit vm.expectCall({ // fee transfer callee: refinancedLoanTerms.credit.assetAddress, data: abi.encodeWithSignature( @@ -904,8 +900,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderCreditPermit: creditPermit, - borrowerCreditPermit: "", + permit: permit, extra: "" }); } @@ -931,14 +926,6 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { fungibleAsset.mint(newLender, refinanceAmount); - vm.expectCall( // lender permit - refinancedLoanTerms.credit.assetAddress, - abi.encodeWithSignature( - "permit(address,address,uint256,uint256,uint8,bytes32,bytes32)", - newLender, address(loan), refinanceAmount, 1, uint8(4), uint256(2), uint256(3) - ) - ); - // no borrower permit vm.expectCall({ // fee transfer callee: refinancedLoanTerms.credit.assetAddress, data: abi.encodeWithSignature( @@ -968,8 +955,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderCreditPermit: creditPermit, - borrowerCreditPermit: "", + permit: permit, extra: "" }); } @@ -995,15 +981,6 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { fungibleAsset.mint(newLender, refinanceAmount); - vm.expectCall({ // lender permit - callee: refinancedLoanTerms.credit.assetAddress, - data: abi.encodeWithSignature( - "permit(address,address,uint256,uint256,uint8,bytes32,bytes32)", - newLender, address(loan), borrowerSurplus + feeAmount, 1, uint8(4), uint256(2), uint256(3) - ), - count: borrowerSurplus + feeAmount > 0 ? 1 : 0 - }); - // no borrower permit vm.expectCall({ // fee transfer callee: refinancedLoanTerms.credit.assetAddress, data: abi.encodeWithSignature( @@ -1034,8 +1011,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderCreditPermit: creditPermit, - borrowerCreditPermit: "", + permit: permit, extra: "" }); } @@ -1057,21 +1033,6 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { fungibleAsset.mint(newLender, refinanceAmount); - vm.expectCall( // lender permit - refinancedLoanTerms.credit.assetAddress, - abi.encodeWithSignature( - "permit(address,address,uint256,uint256,uint8,bytes32,bytes32)", - newLender, address(loan), refinanceAmount, 1, uint8(4), uint256(2), uint256(3) - ) - ); - vm.expectCall({ // borrower permit - callee: refinancedLoanTerms.credit.assetAddress, - data: abi.encodeWithSignature( - "permit(address,address,uint256,uint256,uint8,bytes32,bytes32)", - borrower, address(loan), borrowerContribution, 1, uint8(4), uint256(2), uint256(3) - ), - count: borrowerContribution > 0 ? 1 : 0 - }); vm.expectCall({ // fee transfer callee: refinancedLoanTerms.credit.assetAddress, data: abi.encodeWithSignature( @@ -1101,8 +1062,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderCreditPermit: creditPermit, - borrowerCreditPermit: creditPermit, + permit: permit, extra: "" }); } @@ -1124,21 +1084,6 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { fungibleAsset.mint(newLender, refinanceAmount); - vm.expectCall( // lender permit - refinancedLoanTerms.credit.assetAddress, - abi.encodeWithSignature( - "permit(address,address,uint256,uint256,uint8,bytes32,bytes32)", - newLender, address(loan), refinanceAmount, 1, uint8(4), uint256(2), uint256(3) - ) - ); - vm.expectCall({ // borrower permit - callee: refinancedLoanTerms.credit.assetAddress, - data: abi.encodeWithSignature( - "permit(address,address,uint256,uint256,uint8,bytes32,bytes32)", - borrower, address(loan), borrowerContribution, 1, uint8(4), uint256(2), uint256(3) - ), - count: borrowerContribution > 0 ? 1 : 0 - }); vm.expectCall({ // fee transfer callee: refinancedLoanTerms.credit.assetAddress, data: abi.encodeWithSignature( @@ -1168,8 +1113,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderCreditPermit: creditPermit, - borrowerCreditPermit: creditPermit, + permit: permit, extra: "" }); } @@ -1191,22 +1135,6 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { fungibleAsset.mint(newLender, refinanceAmount); - vm.expectCall({ // lender permit - callee: refinancedLoanTerms.credit.assetAddress, - data: abi.encodeWithSignature( - "permit(address,address,uint256,uint256,uint8,bytes32,bytes32)", - newLender, address(loan), feeAmount, 1, uint8(4), uint256(2), uint256(3) - ), - count: feeAmount > 0 ? 1 : 0 - }); - vm.expectCall({ // borrower permit - callee: refinancedLoanTerms.credit.assetAddress, - data: abi.encodeWithSignature( - "permit(address,address,uint256,uint256,uint8,bytes32,bytes32)", - borrower, address(loan), borrowerContribution, 1, uint8(4), uint256(2), uint256(3) - ), - count: borrowerContribution > 0 ? 1 : 0 - }); vm.expectCall({ // fee transfer callee: refinancedLoanTerms.credit.assetAddress, data: abi.encodeWithSignature( @@ -1237,8 +1165,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderCreditPermit: creditPermit, - borrowerCreditPermit: creditPermit, + permit: permit, extra: "" }); } @@ -1279,8 +1206,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderCreditPermit: "", - borrowerCreditPermit: "", + permit: permit, extra: "" }); @@ -1326,8 +1252,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderCreditPermit: "", - borrowerCreditPermit: "", + permit: permit, extra: "" }); @@ -1353,8 +1278,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderCreditPermit: "", - borrowerCreditPermit: "", + permit: permit, extra: "" }); @@ -1378,8 +1302,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loanId: loanId, proposalHash: proposalHash, loanTerms: refinancedLoanTerms, - lenderCreditPermit: "", - borrowerCreditPermit: "", + permit: permit, extra: "" }); @@ -1413,7 +1336,7 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { _mockLOAN(loanId, simpleLoan); vm.expectRevert(abi.encodeWithSelector(NonExistingLoan.selector)); - loan.repayLOAN(loanId, creditPermit); + loan.repayLOAN(loanId, permit); } function test_shouldFail_whenLoanIsNotRunning() external { @@ -1421,48 +1344,39 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { _mockLOAN(loanId, simpleLoan); vm.expectRevert(abi.encodeWithSelector(InvalidLoanStatus.selector, 3)); - loan.repayLOAN(loanId, creditPermit); + loan.repayLOAN(loanId, permit); } function test_shouldFail_whenLoanIsDefaulted() external { vm.warp(simpleLoan.defaultTimestamp); vm.expectRevert(abi.encodeWithSelector(LoanDefaulted.selector, simpleLoan.defaultTimestamp)); - loan.repayLOAN(loanId, creditPermit); + loan.repayLOAN(loanId, permit); } - function testFuzz_shouldCallPermit_whenProvided( - uint256 _days, uint256 _principal, uint256 _fixedInterest, uint256 _dailyInterest - ) external { - _days = bound(_days, 0, loanDurationInDays - 1); - _principal = bound(_principal, 1, 1e40); - _fixedInterest = bound(_fixedInterest, 0, 1e40); - _dailyInterest = bound(_dailyInterest, 1, 274e8); - - simpleLoan.principalAmount = _principal; - simpleLoan.fixedInterestAmount = _fixedInterest; - simpleLoan.accruingInterestDailyRate = uint40(_dailyInterest); - _mockLOAN(loanId, simpleLoan); - - vm.warp(simpleLoan.startTimestamp + _days * 1 days); - - uint256 loanRepaymentAmount = loan.loanRepaymentAmount(loanId); - fungibleAsset.mint(borrower, loanRepaymentAmount); + 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( simpleLoan.creditAddress, abi.encodeWithSignature( "permit(address,address,uint256,uint256,uint8,bytes32,bytes32)", - borrower, address(loan), loanRepaymentAmount, 1, uint8(4), uint256(2), uint256(3) + permit.owner, address(loan), permit.amount, permit.deadline, permit.v, permit.r, permit.s ) ); vm.prank(borrower); - loan.repayLOAN(loanId, creditPermit); + loan.repayLOAN(loanId, permit); } function test_shouldDeleteLoanData_whenLOANOwnerIsOriginalLender() external { - loan.repayLOAN(loanId, creditPermit); + loan.repayLOAN(loanId, permit); _assertLOANEq(loanId, nonExistingLoan); } @@ -1470,7 +1384,7 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { function test_shouldBurnLOANToken_whenLOANOwnerIsOriginalLender() external { vm.expectCall(loanToken, abi.encodeWithSignature("burn(uint256)", loanId)); - loan.repayLOAN(loanId, creditPermit); + loan.repayLOAN(loanId, permit); } function testFuzz_shouldTransferRepaidAmountToLender_whenLOANOwnerIsOriginalLender( @@ -1499,7 +1413,7 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { ); vm.prank(borrower); - loan.repayLOAN(loanId, creditPermit); + loan.repayLOAN(loanId, permit); } function testFuzz_shouldUpdateLoanData_whenLOANOwnerIsNotOriginalLender( @@ -1523,7 +1437,7 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { fungibleAsset.mint(borrower, loanRepaymentAmount); vm.prank(borrower); - loan.repayLOAN(loanId, creditPermit); + loan.repayLOAN(loanId, permit); // Update loan and compare simpleLoan.status = 3; // move loan to repaid state @@ -1560,7 +1474,7 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { ); vm.prank(borrower); - loan.repayLOAN(loanId, creditPermit); + loan.repayLOAN(loanId, permit); } function test_shouldTransferCollateralToBorrower() external { @@ -1572,21 +1486,21 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { ) ); - loan.repayLOAN(loanId, creditPermit); + loan.repayLOAN(loanId, permit); } function test_shouldEmitEvent_LOANPaidBack() external { vm.expectEmit(); emit LOANPaidBack(loanId); - loan.repayLOAN(loanId, creditPermit); + loan.repayLOAN(loanId, permit); } function test_shouldEmitEvent_LOANClaimed_whenLOANOwnerIsOriginalLender() external { vm.expectEmit(); emit LOANClaimed(loanId, false); - loan.repayLOAN(loanId, creditPermit); + loan.repayLOAN(loanId, permit); } } @@ -1859,7 +1773,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { vm.expectRevert(abi.encodeWithSelector(NonExistingLoan.selector)); vm.prank(lender); - loan.extendLOAN(extension, "", ""); + loan.extendLOAN(extension, "", permit); } function test_shouldFail_whenLoanIsRepaid() external { @@ -1868,7 +1782,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { vm.expectRevert(abi.encodeWithSelector(InvalidLoanStatus.selector, 3)); vm.prank(lender); - loan.extendLOAN(extension, "", ""); + loan.extendLOAN(extension, "", permit); } function testFuzz_shouldFail_whenInvalidSignature_whenEOA(uint256 pk) external { @@ -1877,7 +1791,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, extension.proposer, _extensionHash(extension))); vm.prank(lender); - loan.extendLOAN(extension, _signExtension(pk, extension), ""); + loan.extendLOAN(extension, _signExtension(pk, extension), permit); } function testFuzz_shouldFail_whenOfferExpirated(uint40 expiration) external { @@ -1889,7 +1803,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { vm.expectRevert(abi.encodeWithSelector(Expired.selector, block.timestamp, extension.expiration)); vm.prank(lender); - loan.extendLOAN(extension, "", ""); + loan.extendLOAN(extension, "", permit); } function test_shouldFail_whenOfferNonceNotUsable() external { @@ -1905,7 +1819,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { NonceNotUsable.selector, extension.proposer, extension.nonceSpace, extension.nonce )); vm.prank(lender); - loan.extendLOAN(extension, "", ""); + loan.extendLOAN(extension, "", permit); } function testFuzz_shouldFail_whenCallerIsNotBorrowerNorLoanOwner(address caller) external { @@ -1914,7 +1828,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { vm.expectRevert(abi.encodeWithSelector(InvalidExtensionCaller.selector)); vm.prank(caller); - loan.extendLOAN(extension, "", ""); + loan.extendLOAN(extension, "", permit); } function testFuzz_shouldFail_whenCallerIsBorrower_andProposerIsNotLoanOwner(address proposer) external { @@ -1925,7 +1839,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { vm.expectRevert(abi.encodeWithSelector(InvalidExtensionSigner.selector, lender, proposer)); vm.prank(borrower); - loan.extendLOAN(extension, "", ""); + loan.extendLOAN(extension, "", permit); } function testFuzz_shouldFail_whenCallerIsLoanOwner_andProposerIsNotBorrower(address proposer) external { @@ -1936,7 +1850,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { vm.expectRevert(abi.encodeWithSelector(InvalidExtensionSigner.selector, borrower, proposer)); vm.prank(lender); - loan.extendLOAN(extension, "", ""); + loan.extendLOAN(extension, "", permit); } function testFuzz_shouldFail_whenExtensionDurationLessThanMin(uint40 duration) external { @@ -1948,7 +1862,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { vm.expectRevert(abi.encodeWithSelector(InvalidExtensionDuration.selector, duration, minDuration)); vm.prank(lender); - loan.extendLOAN(extension, "", ""); + loan.extendLOAN(extension, "", permit); } function testFuzz_shouldFail_whenExtensionDurationMoreThanMax(uint40 duration) external { @@ -1960,7 +1874,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { vm.expectRevert(abi.encodeWithSelector(InvalidExtensionDuration.selector, duration, maxDuration)); vm.prank(lender); - loan.extendLOAN(extension, "", ""); + loan.extendLOAN(extension, "", permit); } function testFuzz_shouldRevokeExtensionNonce(uint256 nonceSpace, uint256 nonce) external { @@ -1974,7 +1888,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { ); vm.prank(lender); - loan.extendLOAN(extension, "", ""); + loan.extendLOAN(extension, "", permit); } function testFuzz_shouldUpdateLoanData(uint40 duration) external { @@ -1984,7 +1898,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { _mockExtensionProposalMade(extension); vm.prank(lender); - loan.extendLOAN(extension, "", ""); + loan.extendLOAN(extension, "", permit); simpleLoan.defaultTimestamp = simpleLoan.defaultTimestamp + duration; _assertLOANEq(loanId, simpleLoan); @@ -2000,7 +1914,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { emit LOANExtended(loanId, simpleLoan.defaultTimestamp, simpleLoan.defaultTimestamp + duration); vm.prank(lender); - loan.extendLOAN(extension, "", ""); + loan.extendLOAN(extension, "", permit); } function test_shouldNotTransferCredit_whenAmountZero() external { @@ -2015,7 +1929,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { }); vm.prank(lender); - loan.extendLOAN(extension, "", ""); + loan.extendLOAN(extension, "", permit); } function test_shouldNotTransferCredit_whenAddressZero() external { @@ -2030,7 +1944,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { }); vm.prank(lender); - loan.extendLOAN(extension, "", ""); + loan.extendLOAN(extension, "", permit); } function test_shouldFail_whenInvalidCompensationAsset() external { @@ -2046,59 +1960,38 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { vm.expectRevert(abi.encodeWithSelector(InvalidMultiTokenAsset.selector, 0, extension.compensationAddress, 0, extension.compensationAmount)); vm.prank(lender); - loan.extendLOAN(extension, "", ""); + loan.extendLOAN(extension, "", permit); } - function testFuzz_shouldCallPermit_whenPermitData(address addr, uint256 amount) external { - assumeAddressIsNot(addr, AddressType.ZeroAddress, AddressType.Precompile, AddressType.ForgeAddress); - amount = bound(amount, 1, 1e40); - - vm.etch(addr, address(fungibleAsset).code); - - extension.compensationAddress = addr; - extension.compensationAmount = amount; + function test_shouldCallPermit_whenProvided() external { _mockExtensionProposalMade(extension); - T20(addr).mint(borrower, amount); - vm.prank(borrower); - T20(addr).approve(address(loan), type(uint256).max); - - vm.mockCall( - categoryRegistry, - abi.encodeWithSignature("registeredCategoryValue(address)", extension.compensationAddress), - abi.encode(0) // ER20 - ); - vm.mockCall( - extension.compensationAddress, - abi.encodeWithSignature("permit(address,address,uint256,uint256,uint8,bytes32,bytes32)"), - abi.encode("") - ); + permit.asset = extension.compensationAddress; + permit.owner = borrower; + permit.amount = 321; + permit.deadline = 2; + permit.v = 3; + permit.r = bytes32(uint256(4)); + permit.s = bytes32(uint256(5)); vm.expectCall( - extension.compensationAddress, + permit.asset, abi.encodeWithSignature( "permit(address,address,uint256,uint256,uint8,bytes32,bytes32)", - borrower, address(loan), amount, 1, uint8(4), uint256(2), uint256(3) + permit.owner, address(loan), permit.amount, permit.deadline, permit.v, permit.r, permit.s ) ); vm.prank(lender); - loan.extendLOAN(extension, "", creditPermit); + loan.extendLOAN(extension, "", permit); } - function testFuzz_shouldTransferCompensation_whenDefined(address addr, uint256 amount) external { - assumeAddressIsNot(addr, AddressType.ZeroAddress, AddressType.Precompile, AddressType.ForgeAddress); + function testFuzz_shouldTransferCompensation_whenDefined(uint256 amount) external { amount = bound(amount, 1, 1e40); - vm.etch(addr, address(fungibleAsset).code); - - extension.compensationAddress = addr; extension.compensationAmount = amount; _mockExtensionProposalMade(extension); - - T20(addr).mint(borrower, amount); - vm.prank(borrower); - T20(addr).approve(address(loan), type(uint256).max); + fungibleAsset.mint(borrower, amount); vm.mockCall( categoryRegistry, @@ -2112,21 +2005,21 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { ); vm.prank(lender); - loan.extendLOAN(extension, "", ""); + loan.extendLOAN(extension, "", permit); } function test_shouldPass_whenBorrowerSignature_whenLenderAccepts() external { extension.proposer = borrower; vm.prank(lender); - loan.extendLOAN(extension, _signExtension(borrowerPk, extension), ""); + loan.extendLOAN(extension, _signExtension(borrowerPk, extension), permit); } function test_shouldPass_whenLenderSignature_whenBorrowerAccepts() external { extension.proposer = lender; vm.prank(borrower); - loan.extendLOAN(extension, _signExtension(lenderPk, extension), ""); + loan.extendLOAN(extension, _signExtension(lenderPk, extension), permit); } } diff --git a/test/unit/PWNSimpleLoanListOffer.t.sol b/test/unit/PWNSimpleLoanListOffer.t.sol index 439d8a6..d1ea01a 100644 --- a/test/unit/PWNSimpleLoanListOffer.t.sol +++ b/test/unit/PWNSimpleLoanListOffer.t.sol @@ -6,7 +6,7 @@ import "forge-std/Test.sol"; import { MultiToken } from "MultiToken/MultiToken.sol"; import { PWNHubTags } from "@pwn/hub/PWNHubTags.sol"; -import { PWNSimpleLoanListOffer, PWNSimpleLoan } +import { PWNSimpleLoanListOffer, PWNSimpleLoan, Permit } from "@pwn/loan/terms/simple/proposal/offer/PWNSimpleLoanListOffer.sol"; import "@pwn/PWNErrors.sol"; @@ -30,6 +30,7 @@ abstract contract PWNSimpleLoanListOfferTest is Test { address stateFingerprintComputer = makeAddr("stateFingerprintComputer"); uint256 loanId = 421; uint256 refinancedLoanId = 123; + Permit permit; event ProposalMade(bytes32 indexed proposalHash, address indexed proposer, bytes proposal); @@ -224,7 +225,7 @@ contract PWNSimpleLoanListOffer_AcceptOffer_Test is PWNSimpleLoanListOfferTest { offer.refinancingLoanId = refinancingLoanId; vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, refinancingLoanId)); - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); + offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), permit, ""); } function testFuzz_shouldFail_whenLoanContractNotTagged_ACTIVE_LOAN(address loanContract) external { @@ -232,14 +233,14 @@ contract PWNSimpleLoanListOffer_AcceptOffer_Test is PWNSimpleLoanListOfferTest { offer.loanContract = loanContract; vm.expectRevert(abi.encodeWithSelector(AddressMissingHubTag.selector, loanContract, PWNHubTags.ACTIVE_LOAN)); - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); + 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), "", "", ""); + offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), permit, ""); } function test_shouldPass_whenGivenCollateralIdIsWhitelisted() external { @@ -251,7 +252,7 @@ contract PWNSimpleLoanListOffer_AcceptOffer_Test is PWNSimpleLoanListOfferTest { offerValues.merkleInclusionProof = new bytes32[](1); offerValues.merkleInclusionProof[0] = id2Hash; - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); + offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), permit, ""); } function test_shouldFail_whenGivenCollateralIdIsNotWhitelisted() external { @@ -264,7 +265,7 @@ contract PWNSimpleLoanListOffer_AcceptOffer_Test is PWNSimpleLoanListOfferTest { offerValues.merkleInclusionProof[0] = id2Hash; vm.expectRevert(abi.encodeWithSelector(CollateralIdNotWhitelisted.selector, offerValues.collateralId)); - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); + offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), permit, ""); } function test_shouldNotCallComputerRegistry_whenShouldNotCheckStateFingerprint() external { @@ -276,7 +277,7 @@ contract PWNSimpleLoanListOffer_AcceptOffer_Test is PWNSimpleLoanListOfferTest { count: 0 }); - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); + offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), permit, ""); } function test_shouldFail_whenComputerRegistryReturnsZeroAddress_whenShouldCheckStateFingerprint() external { @@ -292,7 +293,7 @@ contract PWNSimpleLoanListOffer_AcceptOffer_Test is PWNSimpleLoanListOfferTest { ); vm.expectRevert(abi.encodeWithSelector(MissingStateFingerprintComputer.selector)); - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); + offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), permit, ""); } function testFuzz_shouldFail_whenComputerReturnsDifferentStateFingerprint_whenShouldCheckStateFingerprint( @@ -314,19 +315,19 @@ contract PWNSimpleLoanListOffer_AcceptOffer_Test is PWNSimpleLoanListOfferTest { vm.expectRevert(abi.encodeWithSelector( InvalidCollateralStateFingerprint.selector, stateFingerprint, offer.collateralStateFingerprint )); - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); + 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), "", "", ""); + 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, "", "", "", ""); + offerContract.acceptOffer(offer, offerValues, "", permit, ""); } function test_shouldPass_whenOfferHasBeenMadeOnchain() external { @@ -336,15 +337,15 @@ contract PWNSimpleLoanListOffer_AcceptOffer_Test is PWNSimpleLoanListOfferTest { bytes32(uint256(1)) ); - offerContract.acceptOffer(offer, offerValues, "", "", "", ""); + offerContract.acceptOffer(offer, offerValues, "", permit, ""); } function test_shouldPass_withValidSignature_whenEOA_whenStandardSignature() external { - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); + offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), permit, ""); } function test_shouldPass_withValidSignature_whenEOA_whenCompactEIP2098Signature() external { - offerContract.acceptOffer(offer, offerValues, _signOfferCompact(lenderPK, offer), "", "", ""); + offerContract.acceptOffer(offer, offerValues, _signOfferCompact(lenderPK, offer), permit, ""); } function test_shouldPass_whenValidSignature_whenContractAccount() external { @@ -356,7 +357,7 @@ contract PWNSimpleLoanListOffer_AcceptOffer_Test is PWNSimpleLoanListOfferTest { abi.encode(bytes4(0x1626ba7e)) ); - offerContract.acceptOffer(offer, offerValues, "", "", "", ""); + offerContract.acceptOffer(offer, offerValues, "", permit, ""); } function testFuzz_shouldFail_whenOfferIsExpired(uint256 timestamp) external { @@ -364,7 +365,7 @@ contract PWNSimpleLoanListOffer_AcceptOffer_Test is PWNSimpleLoanListOfferTest { vm.warp(timestamp); vm.expectRevert(abi.encodeWithSelector(Expired.selector, timestamp, offer.expiration)); - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); + offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), permit, ""); } function test_shouldFail_whenOfferNonceNotUsable() external { @@ -381,7 +382,7 @@ contract PWNSimpleLoanListOffer_AcceptOffer_Test is PWNSimpleLoanListOfferTest { vm.expectRevert(abi.encodeWithSelector( NonceNotUsable.selector, offer.lender, offer.nonceSpace, offer.nonce )); - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); + offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), permit, ""); } function testFuzz_shouldFail_whenCallerIsNotAllowedBorrower(address caller) external { @@ -391,7 +392,7 @@ contract PWNSimpleLoanListOffer_AcceptOffer_Test is PWNSimpleLoanListOfferTest { vm.expectRevert(abi.encodeWithSelector(CallerNotAllowedAcceptor.selector, caller, offer.allowedBorrower)); vm.prank(caller); - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); + offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), permit, ""); } function testFuzz_shouldFail_whenLessThanMinDuration(uint256 duration) external { @@ -400,7 +401,7 @@ contract PWNSimpleLoanListOffer_AcceptOffer_Test is PWNSimpleLoanListOfferTest { offer.duration = uint32(duration); vm.expectRevert(abi.encodeWithSelector(InvalidDuration.selector, duration, offerContract.MIN_LOAN_DURATION())); - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); + offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), permit, ""); } function testFuzz_shouldFail_whenAccruingInterestAPROutOfBounds(uint256 interestAPR) external { @@ -409,7 +410,7 @@ contract PWNSimpleLoanListOffer_AcceptOffer_Test is PWNSimpleLoanListOfferTest { offer.accruingInterestAPR = uint40(interestAPR); vm.expectRevert(abi.encodeWithSelector(AccruingInterestAPROutOfBounds.selector, interestAPR, maxInterest)); - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); + offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), permit, ""); } function test_shouldRevokeOffer_whenAvailableCreditLimitEqualToZero() external { @@ -422,7 +423,7 @@ contract PWNSimpleLoanListOffer_AcceptOffer_Test is PWNSimpleLoanListOfferTest { ) ); - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); + offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), permit, ""); } function testFuzz_shouldFail_whenUsedCreditExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { @@ -433,7 +434,7 @@ contract PWNSimpleLoanListOffer_AcceptOffer_Test is PWNSimpleLoanListOfferTest { 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), "", "", ""); + offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), permit, ""); } function testFuzz_shouldIncreaseUsedCredit_whenUsedCreditNotExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { @@ -443,14 +444,43 @@ contract PWNSimpleLoanListOffer_AcceptOffer_Test is PWNSimpleLoanListOfferTest { vm.store(address(offerContract), keccak256(abi.encode(_offerHash(offer), CREDIT_USED_SLOT)), bytes32(used)); - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); + 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 { - bytes memory creditPermit = "creditPermit"; - bytes memory collateralPermit = "collateralPermit"; + 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({ @@ -477,17 +507,17 @@ contract PWNSimpleLoanListOffer_AcceptOffer_Test is PWNSimpleLoanListOfferTest { activeLoanContract, abi.encodeWithSelector( PWNSimpleLoan.createLOAN.selector, - _offerHash(offer), loanTerms, creditPermit, collateralPermit, extra + _offerHash(offer), loanTerms, permit, extra ) ); vm.prank(borrower); - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), creditPermit, collateralPermit, extra); + offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), permit, extra); } function test_shouldReturnNewLoanId() external { assertEq( - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), "", "", ""), + offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), permit, ""), loanId ); } @@ -514,8 +544,7 @@ contract PWNSimpleLoanListOffer_AcceptOfferAndRevokeCallersNonce_Test is PWNSimp offer: offer, offerValues: offerValues, signature: _signOffer(lenderPK, offer), - creditPermit: "", - collateralPermit: "", + permit: permit, extra: "", callersNonceSpace: nonceSpace, callersNonceToRevoke: nonce @@ -533,8 +562,7 @@ contract PWNSimpleLoanListOffer_AcceptOfferAndRevokeCallersNonce_Test is PWNSimp offer: offer, offerValues: offerValues, signature: _signOffer(lenderPK, offer), - creditPermit: "", - collateralPermit: "", + permit: permit, extra: "", callersNonceSpace: nonceSpace, callersNonceToRevoke: nonce @@ -547,8 +575,7 @@ contract PWNSimpleLoanListOffer_AcceptOfferAndRevokeCallersNonce_Test is PWNSimp offer: offer, offerValues: offerValues, signature: _signOffer(lenderPK, offer), - creditPermit: "", - collateralPermit: "", + permit: permit, extra: "", callersNonceSpace: 1, callersNonceToRevoke: 2 @@ -574,7 +601,7 @@ contract PWNSimpleLoanListOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanListOf offer.refinancingLoanId = _refinancingLoanId; vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, offer.refinancingLoanId)); - offerContract.acceptRefinanceOffer(_loanId, offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); + offerContract.acceptRefinanceOffer(_loanId, offer, offerValues, _signOffer(lenderPK, offer), permit, ""); } function testFuzz_shouldFail_whenLoanContractNotTagged_ACTIVE_LOAN(address loanContract) external { @@ -582,14 +609,14 @@ contract PWNSimpleLoanListOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanListOf offer.loanContract = loanContract; vm.expectRevert(abi.encodeWithSelector(AddressMissingHubTag.selector, loanContract, PWNHubTags.ACTIVE_LOAN)); - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); + 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), "", "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), permit, ""); } function test_shouldPass_whenGivenCollateralIdIsWhitelisted() external { @@ -601,7 +628,7 @@ contract PWNSimpleLoanListOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanListOf offerValues.merkleInclusionProof = new bytes32[](1); offerValues.merkleInclusionProof[0] = id2Hash; - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), permit, ""); } function test_shouldFail_whenGivenCollateralIdIsNotWhitelisted() external { @@ -614,7 +641,7 @@ contract PWNSimpleLoanListOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanListOf offerValues.merkleInclusionProof[0] = id2Hash; vm.expectRevert(abi.encodeWithSelector(CollateralIdNotWhitelisted.selector, offerValues.collateralId)); - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), permit, ""); } function test_shouldNotCallComputerRegistry_whenShouldNotCheckStateFingerprint() external { @@ -626,7 +653,7 @@ contract PWNSimpleLoanListOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanListOf count: 0 }); - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), permit, ""); } function test_shouldFail_whenComputerRegistryReturnsZeroAddress_whenShouldCheckStateFingerprint() external { @@ -642,7 +669,7 @@ contract PWNSimpleLoanListOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanListOf ); vm.expectRevert(abi.encodeWithSelector(MissingStateFingerprintComputer.selector)); - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), permit, ""); } function testFuzz_shouldFail_whenComputerReturnsDifferentStateFingerprint_whenShouldCheckStateFingerprint( @@ -664,19 +691,19 @@ contract PWNSimpleLoanListOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanListOf vm.expectRevert(abi.encodeWithSelector( InvalidCollateralStateFingerprint.selector, stateFingerprint, offer.collateralStateFingerprint )); - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); + 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), "", "", ""); + 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, "", "", "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, offerValues, "", permit, ""); } function test_shouldPass_whenOfferHasBeenMadeOnchain() external { @@ -686,15 +713,15 @@ contract PWNSimpleLoanListOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanListOf bytes32(uint256(1)) ); - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, "", "", "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, offerValues, "", permit, ""); } function test_shouldPass_withValidSignature_whenEOA_whenStandardSignature() external { - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), permit, ""); } function test_shouldPass_withValidSignature_whenEOA_whenCompactEIP2098Signature() external { - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOfferCompact(lenderPK, offer), "", "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOfferCompact(lenderPK, offer), permit, ""); } function test_shouldPass_whenValidSignature_whenContractAccount() external { @@ -706,7 +733,7 @@ contract PWNSimpleLoanListOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanListOf abi.encode(bytes4(0x1626ba7e)) ); - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, "", "", "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, offerValues, "", permit, ""); } function testFuzz_shouldFail_whenOfferIsExpired(uint256 timestamp) external { @@ -714,7 +741,7 @@ contract PWNSimpleLoanListOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanListOf vm.warp(timestamp); vm.expectRevert(abi.encodeWithSelector(Expired.selector, timestamp, offer.expiration)); - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), permit, ""); } function test_shouldFail_whenOfferNonceNotUsable() external { @@ -731,7 +758,7 @@ contract PWNSimpleLoanListOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanListOf vm.expectRevert(abi.encodeWithSelector( NonceNotUsable.selector, offer.lender, offer.nonceSpace, offer.nonce )); - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), permit, ""); } function testFuzz_shouldFail_whenCallerIsNotAllowedBorrower(address caller) external { @@ -741,7 +768,7 @@ contract PWNSimpleLoanListOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanListOf vm.expectRevert(abi.encodeWithSelector(CallerNotAllowedAcceptor.selector, caller, offer.allowedBorrower)); vm.prank(caller); - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), permit, ""); } function testFuzz_shouldFail_whenLessThanMinDuration(uint256 duration) external { @@ -750,7 +777,7 @@ contract PWNSimpleLoanListOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanListOf offer.duration = uint32(duration); vm.expectRevert(abi.encodeWithSelector(InvalidDuration.selector, duration, offerContract.MIN_LOAN_DURATION())); - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), permit, ""); } function testFuzz_shouldFail_whenAccruingInterestAPROutOfBounds(uint256 interestAPR) external { @@ -759,7 +786,7 @@ contract PWNSimpleLoanListOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanListOf offer.accruingInterestAPR = uint40(interestAPR); vm.expectRevert(abi.encodeWithSelector(AccruingInterestAPROutOfBounds.selector, interestAPR, maxInterest)); - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), permit, ""); } function test_shouldRevokeOffer_whenAvailableCreditLimitEqualToZero() external { @@ -772,7 +799,7 @@ contract PWNSimpleLoanListOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanListOf ) ); - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), permit, ""); } function testFuzz_shouldFail_whenUsedCreditExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { @@ -783,7 +810,7 @@ contract PWNSimpleLoanListOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanListOf 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), "", "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), permit, ""); } function testFuzz_shouldIncreaseUsedCredit_whenUsedCreditNotExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { @@ -793,19 +820,48 @@ contract PWNSimpleLoanListOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanListOf vm.store(address(offerContract), keccak256(abi.encode(_offerHash(offer), CREDIT_USED_SLOT)), bytes32(used)); - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", "", ""); + 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 { - bytes memory creditPermit = "creditPermit"; - bytes memory collateralPermit = "collateralPermit"; + 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: lender, - borrower: offer.lender, + lender: offer.lender, + borrower: borrower, duration: offer.duration, collateral: MultiToken.Asset({ category: offer.collateralCategory, @@ -827,19 +883,19 @@ contract PWNSimpleLoanListOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanListOf activeLoanContract, abi.encodeWithSelector( PWNSimpleLoan.refinanceLOAN.selector, - loanId, _offerHash(offer), loanTerms, creditPermit, collateralPermit, extra + loanId, _offerHash(offer), loanTerms, permit, extra ) ); - vm.prank(lender); + vm.prank(borrower); offerContract.acceptRefinanceOffer( - loanId, offer, offerValues, _signOffer(lenderPK, offer), creditPermit, collateralPermit, extra + loanId, offer, offerValues, _signOffer(lenderPK, offer), permit, extra ); } function test_shouldReturnRefinancedLoanId() external { assertEq( - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), "", "", ""), + offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), permit, ""), refinancedLoanId ); } @@ -867,8 +923,7 @@ contract PWNSimpleLoanListOffer_AcceptRefinanceOfferAndRevokeCallersNonce_Test i offer: offer, offerValues: offerValues, signature: _signOffer(lenderPK, offer), - lenderCreditPermit: "", - borrowerCreditPermit: "", + permit: permit, extra: "", callersNonceSpace: nonceSpace, callersNonceToRevoke: nonce @@ -887,8 +942,7 @@ contract PWNSimpleLoanListOffer_AcceptRefinanceOfferAndRevokeCallersNonce_Test i offer: offer, offerValues: offerValues, signature: _signOffer(lenderPK, offer), - lenderCreditPermit: "", - borrowerCreditPermit: "", + permit: permit, extra: "", callersNonceSpace: nonceSpace, callersNonceToRevoke: nonce @@ -902,8 +956,7 @@ contract PWNSimpleLoanListOffer_AcceptRefinanceOfferAndRevokeCallersNonce_Test i offer: offer, offerValues: offerValues, signature: _signOffer(lenderPK, offer), - lenderCreditPermit: "", - borrowerCreditPermit: "", + permit: permit, extra: "", callersNonceSpace: 1, callersNonceToRevoke: 2 diff --git a/test/unit/PWNSimpleLoanSimpleOffer.t.sol b/test/unit/PWNSimpleLoanSimpleOffer.t.sol index 7bff348..49bcb09 100644 --- a/test/unit/PWNSimpleLoanSimpleOffer.t.sol +++ b/test/unit/PWNSimpleLoanSimpleOffer.t.sol @@ -6,7 +6,7 @@ import "forge-std/Test.sol"; import { MultiToken } from "MultiToken/MultiToken.sol"; import { PWNHubTags } from "@pwn/hub/PWNHubTags.sol"; -import { PWNSimpleLoanSimpleOffer, PWNSimpleLoan } +import { PWNSimpleLoanSimpleOffer, PWNSimpleLoan, Permit } from "@pwn/loan/terms/simple/proposal/offer/PWNSimpleLoanSimpleOffer.sol"; import "@pwn/PWNErrors.sol"; @@ -29,6 +29,7 @@ abstract contract PWNSimpleLoanSimpleOfferTest is Test { address stateFingerprintComputer = makeAddr("stateFingerprintComputer"); uint256 loanId = 421; uint256 refinancedLoanId = 123; + Permit permit; event ProposalMade(bytes32 indexed proposalHash, address indexed proposer, bytes proposal); @@ -218,7 +219,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptOffer_Test is PWNSimpleLoanSimpleOfferTe offer.refinancingLoanId = refinancingLoanId; vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, refinancingLoanId)); - offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", "", ""); + offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), permit, ""); } function testFuzz_shouldFail_whenLoanContractNotTagged_ACTIVE_LOAN(address loanContract) external { @@ -226,7 +227,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptOffer_Test is PWNSimpleLoanSimpleOfferTe offer.loanContract = loanContract; vm.expectRevert(abi.encodeWithSelector(AddressMissingHubTag.selector, loanContract, PWNHubTags.ACTIVE_LOAN)); - offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", "", ""); + offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), permit, ""); } function test_shouldNotCallComputerRegistry_whenShouldNotCheckStateFingerprint() external { @@ -238,7 +239,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptOffer_Test is PWNSimpleLoanSimpleOfferTe count: 0 }); - offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", "", ""); + offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), permit, ""); } function test_shouldFail_whenComputerRegistryReturnsZeroAddress_whenShouldCheckStateFingerprint() external { @@ -254,7 +255,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptOffer_Test is PWNSimpleLoanSimpleOfferTe ); vm.expectRevert(abi.encodeWithSelector(MissingStateFingerprintComputer.selector)); - offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", "", ""); + offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), permit, ""); } function testFuzz_shouldFail_whenComputerReturnsDifferentStateFingerprint_whenShouldCheckStateFingerprint( @@ -276,19 +277,19 @@ contract PWNSimpleLoanSimpleOffer_AcceptOffer_Test is PWNSimpleLoanSimpleOfferTe vm.expectRevert(abi.encodeWithSelector( InvalidCollateralStateFingerprint.selector, stateFingerprint, offer.collateralStateFingerprint )); - offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", "", ""); + offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), permit, ""); } function test_shouldFail_whenInvalidSignature_whenEOA() external { vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, offer.lender, _offerHash(offer))); - offerContract.acceptOffer(offer, _signOffer(1, offer), "", "", ""); + offerContract.acceptOffer(offer, _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, "", "", "", ""); + offerContract.acceptOffer(offer, "", permit, ""); } function test_shouldPass_whenOfferHasBeenMadeOnchain() external { @@ -298,15 +299,15 @@ contract PWNSimpleLoanSimpleOffer_AcceptOffer_Test is PWNSimpleLoanSimpleOfferTe bytes32(uint256(1)) ); - offerContract.acceptOffer(offer, "", "", "", ""); + offerContract.acceptOffer(offer, "", permit, ""); } function test_shouldPass_withValidSignature_whenEOA_whenStandardSignature() external { - offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", "", ""); + offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), permit, ""); } function test_shouldPass_withValidSignature_whenEOA_whenCompactEIP2098Signature() external { - offerContract.acceptOffer(offer, _signOfferCompact(lenderPK, offer), "", "", ""); + offerContract.acceptOffer(offer, _signOfferCompact(lenderPK, offer), permit, ""); } function test_shouldPass_whenValidSignature_whenContractAccount() external { @@ -318,7 +319,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptOffer_Test is PWNSimpleLoanSimpleOfferTe abi.encode(bytes4(0x1626ba7e)) ); - offerContract.acceptOffer(offer, "", "", "", ""); + offerContract.acceptOffer(offer, "", permit, ""); } function testFuzz_shouldFail_whenOfferIsExpired(uint256 timestamp) external { @@ -326,7 +327,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptOffer_Test is PWNSimpleLoanSimpleOfferTe vm.warp(timestamp); vm.expectRevert(abi.encodeWithSelector(Expired.selector, timestamp, offer.expiration)); - offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", "", ""); + offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), permit, ""); } function test_shouldFail_whenOfferNonceNotUsable() external { @@ -343,7 +344,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptOffer_Test is PWNSimpleLoanSimpleOfferTe vm.expectRevert(abi.encodeWithSelector( NonceNotUsable.selector, offer.lender, offer.nonceSpace, offer.nonce )); - offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", "", ""); + offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), permit, ""); } function testFuzz_shouldFail_whenCallerIsNotAllowedBorrower(address caller) external { @@ -353,7 +354,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptOffer_Test is PWNSimpleLoanSimpleOfferTe vm.expectRevert(abi.encodeWithSelector(CallerNotAllowedAcceptor.selector, caller, offer.allowedBorrower)); vm.prank(caller); - offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", "", ""); + offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), permit, ""); } function testFuzz_shouldFail_whenLessThanMinDuration(uint256 duration) external { @@ -362,7 +363,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptOffer_Test is PWNSimpleLoanSimpleOfferTe offer.duration = uint32(duration); vm.expectRevert(abi.encodeWithSelector(InvalidDuration.selector, duration, offerContract.MIN_LOAN_DURATION())); - offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", "", ""); + offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), permit, ""); } function testFuzz_shouldFail_whenAccruingInterestAPROutOfBounds(uint256 interestAPR) external { @@ -371,7 +372,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptOffer_Test is PWNSimpleLoanSimpleOfferTe offer.accruingInterestAPR = uint40(interestAPR); vm.expectRevert(abi.encodeWithSelector(AccruingInterestAPROutOfBounds.selector, interestAPR, maxInterest)); - offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", "", ""); + offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), permit, ""); } function test_shouldRevokeOffer_whenAvailableCreditLimitEqualToZero() external { @@ -384,7 +385,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptOffer_Test is PWNSimpleLoanSimpleOfferTe ) ); - offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", "", ""); + offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), permit, ""); } function testFuzz_shouldFail_whenUsedCreditExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { @@ -395,7 +396,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptOffer_Test is PWNSimpleLoanSimpleOfferTe 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, _signOffer(lenderPK, offer), "", "", ""); + offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), permit, ""); } function testFuzz_shouldIncreaseUsedCredit_whenUsedCreditNotExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { @@ -405,14 +406,43 @@ contract PWNSimpleLoanSimpleOffer_AcceptOffer_Test is PWNSimpleLoanSimpleOfferTe vm.store(address(offerContract), keccak256(abi.encode(_offerHash(offer), CREDIT_USED_SLOT)), bytes32(used)); - offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", "", ""); + offerContract.acceptOffer(offer, _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, _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, _signOffer(lenderPK, offer), permit, ""); + } + function test_shouldCallLoanContractWithLoanTerms() external { - bytes memory creditPermit = "creditPermit"; - bytes memory collateralPermit = "collateralPermit"; + 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({ @@ -439,17 +469,17 @@ contract PWNSimpleLoanSimpleOffer_AcceptOffer_Test is PWNSimpleLoanSimpleOfferTe activeLoanContract, abi.encodeWithSelector( PWNSimpleLoan.createLOAN.selector, - _offerHash(offer), loanTerms, creditPermit, collateralPermit, extra + _offerHash(offer), loanTerms, permit, extra ) ); vm.prank(borrower); - offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), creditPermit, collateralPermit, extra); + offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), permit, extra); } function test_shouldReturnNewLoanId() external { assertEq( - offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), "", "", ""), + offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), permit, ""), loanId ); } @@ -475,8 +505,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptOfferAndRevokeCallersNonce_Test is PWNSi offerContract.acceptOffer({ offer: offer, signature: _signOffer(lenderPK, offer), - creditPermit: "", - collateralPermit: "", + permit: permit, extra: "", callersNonceSpace: nonceSpace, callersNonceToRevoke: nonce @@ -493,8 +522,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptOfferAndRevokeCallersNonce_Test is PWNSi offerContract.acceptOffer({ offer: offer, signature: _signOffer(lenderPK, offer), - creditPermit: "", - collateralPermit: "", + permit: permit, extra: "", callersNonceSpace: nonceSpace, callersNonceToRevoke: nonce @@ -506,8 +534,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptOfferAndRevokeCallersNonce_Test is PWNSi uint256 newLoanId = offerContract.acceptOffer({ offer: offer, signature: _signOffer(lenderPK, offer), - creditPermit: "", - collateralPermit: "", + permit: permit, extra: "", callersNonceSpace: 1, callersNonceToRevoke: 2 @@ -533,7 +560,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanSimp offer.refinancingLoanId = _refinancingLoanId; vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, offer.refinancingLoanId)); - offerContract.acceptRefinanceOffer(_loanId, offer, _signOffer(lenderPK, offer), "", "", ""); + offerContract.acceptRefinanceOffer(_loanId, offer, _signOffer(lenderPK, offer), permit, ""); } function testFuzz_shouldFail_whenLoanContractNotTagged_ACTIVE_LOAN(address loanContract) external { @@ -541,7 +568,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanSimp offer.loanContract = loanContract; vm.expectRevert(abi.encodeWithSelector(AddressMissingHubTag.selector, loanContract, PWNHubTags.ACTIVE_LOAN)); - offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), "", "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), permit, ""); } function test_shouldNotCallComputerRegistry_whenShouldNotCheckStateFingerprint() external { @@ -553,7 +580,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanSimp count: 0 }); - offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), "", "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), permit, ""); } function test_shouldFail_whenComputerRegistryReturnsZeroAddress_whenShouldCheckStateFingerprint() external { @@ -569,7 +596,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanSimp ); vm.expectRevert(abi.encodeWithSelector(MissingStateFingerprintComputer.selector)); - offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), "", "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), permit, ""); } function testFuzz_shouldFail_whenComputerReturnsDifferentStateFingerprint_whenShouldCheckStateFingerprint( @@ -591,19 +618,19 @@ contract PWNSimpleLoanSimpleOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanSimp vm.expectRevert(abi.encodeWithSelector( InvalidCollateralStateFingerprint.selector, stateFingerprint, offer.collateralStateFingerprint )); - offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), "", "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), permit, ""); } function test_shouldFail_whenInvalidSignature_whenEOA() external { vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, offer.lender, _offerHash(offer))); - offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(1, offer), "", "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, _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, "", "", "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, "", permit, ""); } function test_shouldPass_whenOfferHasBeenMadeOnchain() external { @@ -613,15 +640,15 @@ contract PWNSimpleLoanSimpleOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanSimp bytes32(uint256(1)) ); - offerContract.acceptRefinanceOffer(loanId, offer, "", "", "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, "", permit, ""); } function test_shouldPass_withValidSignature_whenEOA_whenStandardSignature() external { - offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), "", "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), permit, ""); } function test_shouldPass_withValidSignature_whenEOA_whenCompactEIP2098Signature() external { - offerContract.acceptRefinanceOffer(loanId, offer, _signOfferCompact(lenderPK, offer), "", "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, _signOfferCompact(lenderPK, offer), permit, ""); } function test_shouldPass_whenValidSignature_whenContractAccount() external { @@ -633,7 +660,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanSimp abi.encode(bytes4(0x1626ba7e)) ); - offerContract.acceptRefinanceOffer(loanId, offer, "", "", "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, "", permit, ""); } function testFuzz_shouldFail_whenOfferIsExpired(uint256 timestamp) external { @@ -641,7 +668,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanSimp vm.warp(timestamp); vm.expectRevert(abi.encodeWithSelector(Expired.selector, timestamp, offer.expiration)); - offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), "", "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), permit, ""); } function test_shouldFail_whenOfferNonceNotUsable() external { @@ -658,7 +685,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanSimp vm.expectRevert(abi.encodeWithSelector( NonceNotUsable.selector, offer.lender, offer.nonceSpace, offer.nonce )); - offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), "", "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), permit, ""); } function testFuzz_shouldFail_whenCallerIsNotAllowedBorrower(address caller) external { @@ -668,7 +695,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanSimp vm.expectRevert(abi.encodeWithSelector(CallerNotAllowedAcceptor.selector, caller, offer.allowedBorrower)); vm.prank(caller); - offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), "", "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), permit, ""); } function testFuzz_shouldFail_whenLessThanMinDuration(uint256 duration) external { @@ -677,7 +704,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanSimp offer.duration = uint32(duration); vm.expectRevert(abi.encodeWithSelector(InvalidDuration.selector, duration, offerContract.MIN_LOAN_DURATION())); - offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), "", "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), permit, ""); } function testFuzz_shouldFail_whenAccruingInterestAPROutOfBounds(uint256 interestAPR) external { @@ -686,7 +713,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanSimp offer.accruingInterestAPR = uint40(interestAPR); vm.expectRevert(abi.encodeWithSelector(AccruingInterestAPROutOfBounds.selector, interestAPR, maxInterest)); - offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), "", "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), permit, ""); } function test_shouldRevokeOffer_whenAvailableCreditLimitEqualToZero() external { @@ -699,7 +726,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanSimp ) ); - offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), "", "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), permit, ""); } function testFuzz_shouldFail_whenUsedCreditExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { @@ -710,7 +737,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanSimp 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, _signOffer(lenderPK, offer), "", "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), permit, ""); } function testFuzz_shouldIncreaseUsedCredit_whenUsedCreditNotExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { @@ -720,19 +747,48 @@ contract PWNSimpleLoanSimpleOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanSimp vm.store(address(offerContract), keccak256(abi.encode(_offerHash(offer), CREDIT_USED_SLOT)), bytes32(used)); - offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), "", "", ""); + offerContract.acceptRefinanceOffer(loanId, offer, _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, _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, _signOffer(lenderPK, offer), permit, ""); + } + function test_shouldCallLoanContract() external { - bytes memory creditPermit = "creditPermit"; - bytes memory collateralPermit = "collateralPermit"; + 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: lender, - borrower: offer.lender, + lender: offer.lender, + borrower: borrower, duration: offer.duration, collateral: MultiToken.Asset({ category: offer.collateralCategory, @@ -754,19 +810,19 @@ contract PWNSimpleLoanSimpleOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanSimp activeLoanContract, abi.encodeWithSelector( PWNSimpleLoan.refinanceLOAN.selector, - loanId, _offerHash(offer), loanTerms, creditPermit, collateralPermit, extra + loanId, _offerHash(offer), loanTerms, permit, extra ) ); - vm.prank(lender); + vm.prank(borrower); offerContract.acceptRefinanceOffer( - loanId, offer, _signOffer(lenderPK, offer), creditPermit, collateralPermit, extra + loanId, offer, _signOffer(lenderPK, offer), permit, extra ); } function test_shouldReturnRefinancedLoanId() external { assertEq( - offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), "", "", ""), + offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), permit, ""), refinancedLoanId ); } @@ -793,8 +849,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptRefinanceOfferAndRevokeCallersNonce_Test loanId: loanId, offer: offer, signature: _signOffer(lenderPK, offer), - lenderCreditPermit: "", - borrowerCreditPermit: "", + permit: permit, extra: "", callersNonceSpace: nonceSpace, callersNonceToRevoke: nonce @@ -812,8 +867,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptRefinanceOfferAndRevokeCallersNonce_Test loanId: loanId, offer: offer, signature: _signOffer(lenderPK, offer), - lenderCreditPermit: "", - borrowerCreditPermit: "", + permit: permit, extra: "", callersNonceSpace: nonceSpace, callersNonceToRevoke: nonce @@ -826,8 +880,7 @@ contract PWNSimpleLoanSimpleOffer_AcceptRefinanceOfferAndRevokeCallersNonce_Test loanId: loanId, offer: offer, signature: _signOffer(lenderPK, offer), - lenderCreditPermit: "", - borrowerCreditPermit: "", + permit: permit, extra: "", callersNonceSpace: 1, callersNonceToRevoke: 2 diff --git a/test/unit/PWNSimpleLoanSimpleRequest.t.sol b/test/unit/PWNSimpleLoanSimpleRequest.t.sol index 51f12a3..8f18283 100644 --- a/test/unit/PWNSimpleLoanSimpleRequest.t.sol +++ b/test/unit/PWNSimpleLoanSimpleRequest.t.sol @@ -6,7 +6,7 @@ import "forge-std/Test.sol"; import { MultiToken } from "MultiToken/MultiToken.sol"; import { PWNHubTags } from "@pwn/hub/PWNHubTags.sol"; -import { PWNSimpleLoanSimpleRequest, PWNSimpleLoan } +import { PWNSimpleLoanSimpleRequest, PWNSimpleLoan, Permit } from "@pwn/loan/terms/simple/proposal/request/PWNSimpleLoanSimpleRequest.sol"; import "@pwn/PWNErrors.sol"; @@ -29,6 +29,7 @@ abstract contract PWNSimpleLoanSimpleRequestTest is Test { address stateFingerprintComputer = makeAddr("stateFingerprintComputer"); uint256 loanId = 421; uint256 refinancedLoanId = 123; + Permit permit; event ProposalMade(bytes32 indexed proposalHash, address indexed proposer, bytes proposal); @@ -218,7 +219,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRequest_Test is PWNSimpleLoanSimpleReq request.refinancingLoanId = refinancingLoanId; vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, refinancingLoanId)); - requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", "", ""); + requestContract.acceptRequest(request, _signRequest(borrowerPK, request), permit, ""); } function testFuzz_shouldFail_whenLoanContractNotTagged_ACTIVE_LOAN(address loanContract) external { @@ -226,7 +227,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRequest_Test is PWNSimpleLoanSimpleReq request.loanContract = loanContract; vm.expectRevert(abi.encodeWithSelector(AddressMissingHubTag.selector, loanContract, PWNHubTags.ACTIVE_LOAN)); - requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", "", ""); + requestContract.acceptRequest(request, _signRequest(borrowerPK, request), permit, ""); } function test_shouldNotCallComputerRegistry_whenShouldNotCheckStateFingerprint() external { @@ -238,7 +239,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRequest_Test is PWNSimpleLoanSimpleReq count: 0 }); - requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", "", ""); + requestContract.acceptRequest(request, _signRequest(borrowerPK, request), permit, ""); } function test_shouldFail_whenComputerRegistryReturnsZeroAddress_whenShouldCheckStateFingerprint() external { @@ -254,7 +255,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRequest_Test is PWNSimpleLoanSimpleReq ); vm.expectRevert(abi.encodeWithSelector(MissingStateFingerprintComputer.selector)); - requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", "", ""); + requestContract.acceptRequest(request, _signRequest(borrowerPK, request), permit, ""); } function testFuzz_shouldFail_whenComputerReturnsDifferentStateFingerprint_whenShouldCheckStateFingerprint( @@ -276,19 +277,19 @@ contract PWNSimpleLoanSimpleRequest_AcceptRequest_Test is PWNSimpleLoanSimpleReq vm.expectRevert(abi.encodeWithSelector( InvalidCollateralStateFingerprint.selector, stateFingerprint, request.collateralStateFingerprint )); - requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", "", ""); + requestContract.acceptRequest(request, _signRequest(borrowerPK, request), permit, ""); } function test_shouldFail_whenInvalidSignature_whenEOA() external { vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, request.borrower, _requestHash(request))); - requestContract.acceptRequest(request, _signRequest(1, request), "", "", ""); + requestContract.acceptRequest(request, _signRequest(1, request), permit, ""); } function test_shouldFail_whenInvalidSignature_whenContractAccount() external { vm.etch(borrower, bytes("data")); vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, request.borrower, _requestHash(request))); - requestContract.acceptRequest(request, "", "", "", ""); + requestContract.acceptRequest(request, "", permit, ""); } function test_shouldPass_whenRequestHasBeenMadeOnchain() external { @@ -298,15 +299,15 @@ contract PWNSimpleLoanSimpleRequest_AcceptRequest_Test is PWNSimpleLoanSimpleReq bytes32(uint256(1)) ); - requestContract.acceptRequest(request, "", "", "", ""); + requestContract.acceptRequest(request, "", permit, ""); } function test_shouldPass_withValidSignature_whenEOA_whenStandardSignature() external { - requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", "", ""); + requestContract.acceptRequest(request, _signRequest(borrowerPK, request), permit, ""); } function test_shouldPass_withValidSignature_whenEOA_whenCompactEIP2098Signature() external { - requestContract.acceptRequest(request, _signRequestCompact(borrowerPK, request), "", "", ""); + requestContract.acceptRequest(request, _signRequestCompact(borrowerPK, request), permit, ""); } function test_shouldPass_whenValidSignature_whenContractAccount() external { @@ -318,7 +319,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRequest_Test is PWNSimpleLoanSimpleReq abi.encode(bytes4(0x1626ba7e)) ); - requestContract.acceptRequest(request, "", "", "", ""); + requestContract.acceptRequest(request, "", permit, ""); } function testFuzz_shouldFail_whenRequestIsExpired(uint256 timestamp) external { @@ -326,7 +327,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRequest_Test is PWNSimpleLoanSimpleReq vm.warp(timestamp); vm.expectRevert(abi.encodeWithSelector(Expired.selector, timestamp, request.expiration)); - requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", "", ""); + requestContract.acceptRequest(request, _signRequest(borrowerPK, request), permit, ""); } function test_shouldFail_whenRequestNonceNotUsable() external { @@ -343,7 +344,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRequest_Test is PWNSimpleLoanSimpleReq vm.expectRevert(abi.encodeWithSelector( NonceNotUsable.selector, request.borrower, request.nonceSpace, request.nonce )); - requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", "", ""); + requestContract.acceptRequest(request, _signRequest(borrowerPK, request), permit, ""); } function testFuzz_shouldFail_whenCallerIsNotAllowedLender(address caller) external { @@ -353,7 +354,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRequest_Test is PWNSimpleLoanSimpleReq vm.expectRevert(abi.encodeWithSelector(CallerNotAllowedAcceptor.selector, caller, request.allowedLender)); vm.prank(caller); - requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", "", ""); + requestContract.acceptRequest(request, _signRequest(borrowerPK, request), permit, ""); } function testFuzz_shouldFail_whenLessThanMinDuration(uint256 duration) external { @@ -362,7 +363,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRequest_Test is PWNSimpleLoanSimpleReq request.duration = uint32(duration); vm.expectRevert(abi.encodeWithSelector(InvalidDuration.selector, duration, requestContract.MIN_LOAN_DURATION())); - requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", "", ""); + requestContract.acceptRequest(request, _signRequest(borrowerPK, request), permit, ""); } function testFuzz_shouldFail_whenAccruingInterestAPROutOfBounds(uint256 interestAPR) external { @@ -371,7 +372,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRequest_Test is PWNSimpleLoanSimpleReq request.accruingInterestAPR = uint40(interestAPR); vm.expectRevert(abi.encodeWithSelector(AccruingInterestAPROutOfBounds.selector, interestAPR, maxInterest)); - requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", "", ""); + requestContract.acceptRequest(request, _signRequest(borrowerPK, request), permit, ""); } function test_shouldRevokeRequest_whenAvailableCreditLimitEqualToZero() external { @@ -384,7 +385,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRequest_Test is PWNSimpleLoanSimpleReq ) ); - requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", "", ""); + requestContract.acceptRequest(request, _signRequest(borrowerPK, request), permit, ""); } function testFuzz_shouldFail_whenUsedCreditExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { @@ -395,7 +396,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRequest_Test is PWNSimpleLoanSimpleReq vm.store(address(requestContract), keccak256(abi.encode(_requestHash(request), CREDIT_USED_SLOT)), bytes32(used)); vm.expectRevert(abi.encodeWithSelector(AvailableCreditLimitExceeded.selector, used + request.creditAmount, limit)); - requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", "", ""); + requestContract.acceptRequest(request, _signRequest(borrowerPK, request), permit, ""); } function testFuzz_shouldIncreaseUsedCredit_whenUsedCreditNotExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { @@ -405,14 +406,43 @@ contract PWNSimpleLoanSimpleRequest_AcceptRequest_Test is PWNSimpleLoanSimpleReq vm.store(address(requestContract), keccak256(abi.encode(_requestHash(request), CREDIT_USED_SLOT)), bytes32(used)); - requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", "", ""); + requestContract.acceptRequest(request, _signRequest(borrowerPK, request), permit, ""); assertEq(requestContract.creditUsed(_requestHash(request)), used + request.creditAmount); } + function testFuzz_shouldFail_whenPermitOwnerNotCaller(address owner) external { + vm.assume(owner != lender); + + permit.owner = owner; + permit.asset = request.creditAddress; + + vm.expectRevert(abi.encodeWithSelector(InvalidPermitOwner.selector, owner, lender)); + vm.prank(lender); + requestContract.acceptRequest(request, _signRequest(borrowerPK, request), permit, ""); + } + + function testFuzz_shouldFail_whenPermitAssetNotCreditAsset(address asset) external { + vm.assume(asset != request.creditAddress && asset != address(0)); + + permit.owner = lender; + permit.asset = asset; + + vm.expectRevert(abi.encodeWithSelector(InvalidPermitAsset.selector, asset, token)); + vm.prank(lender); + requestContract.acceptRequest(request, _signRequest(borrowerPK, request), permit, ""); + } + function test_shouldCallLoanContractWithLoanTerms() external { - bytes memory creditPermit = "creditPermit"; - bytes memory collateralPermit = "collateralPermit"; + permit = Permit({ + asset: token, + owner: lender, + 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({ @@ -439,17 +469,17 @@ contract PWNSimpleLoanSimpleRequest_AcceptRequest_Test is PWNSimpleLoanSimpleReq activeLoanContract, abi.encodeWithSelector( PWNSimpleLoan.createLOAN.selector, - _requestHash(request), loanTerms, creditPermit, collateralPermit, extra + _requestHash(request), loanTerms, permit, extra ) ); vm.prank(lender); - requestContract.acceptRequest(request, _signRequest(borrowerPK, request), creditPermit, collateralPermit, extra); + requestContract.acceptRequest(request, _signRequest(borrowerPK, request), permit, extra); } function test_shouldReturnNewLoanId() external { assertEq( - requestContract.acceptRequest(request, _signRequest(borrowerPK, request), "", "", ""), + requestContract.acceptRequest(request, _signRequest(borrowerPK, request), permit, ""), loanId ); } @@ -475,8 +505,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRequestAndRevokeCallersNonce_Test is P requestContract.acceptRequest({ request: request, signature: _signRequest(borrowerPK, request), - creditPermit: "", - collateralPermit: "", + permit: permit, extra: "", callersNonceSpace: nonceSpace, callersNonceToRevoke: nonce @@ -493,8 +522,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRequestAndRevokeCallersNonce_Test is P requestContract.acceptRequest({ request: request, signature: _signRequest(borrowerPK, request), - creditPermit: "", - collateralPermit: "", + permit: permit, extra: "", callersNonceSpace: nonceSpace, callersNonceToRevoke: nonce @@ -506,8 +534,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRequestAndRevokeCallersNonce_Test is P uint256 newLoanId = requestContract.acceptRequest({ request: request, signature: _signRequest(borrowerPK, request), - creditPermit: "", - collateralPermit: "", + permit: permit, extra: "", callersNonceSpace: 1, callersNonceToRevoke: 2 @@ -535,7 +562,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRefinanceRequest_Test is PWNSimpleLoan request.refinancingLoanId = 0; vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, request.refinancingLoanId)); - requestContract.acceptRefinanceRequest(0, request, _signRequest(borrowerPK, request), "", "", ""); + requestContract.acceptRefinanceRequest(0, request, _signRequest(borrowerPK, request), permit, ""); } function testFuzz_shouldFail_whenRefinancingLoanIdIsNotEqualToLoanId(uint256 _loanId, uint256 _refinancingLoanId) external { @@ -544,7 +571,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRefinanceRequest_Test is PWNSimpleLoan request.refinancingLoanId = _refinancingLoanId; vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, request.refinancingLoanId)); - requestContract.acceptRefinanceRequest(_loanId, request, _signRequest(borrowerPK, request), "", "", ""); + requestContract.acceptRefinanceRequest(_loanId, request, _signRequest(borrowerPK, request), permit, ""); } function testFuzz_shouldFail_whenLoanContractNotTagged_ACTIVE_LOAN(address loanContract) external { @@ -552,7 +579,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRefinanceRequest_Test is PWNSimpleLoan request.loanContract = loanContract; vm.expectRevert(abi.encodeWithSelector(AddressMissingHubTag.selector, loanContract, PWNHubTags.ACTIVE_LOAN)); - requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", "", ""); + requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), permit, ""); } function test_shouldNotCallComputerRegistry_whenShouldNotCheckStateFingerprint() external { @@ -564,7 +591,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRefinanceRequest_Test is PWNSimpleLoan count: 0 }); - requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", "", ""); + requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), permit, ""); } function test_shouldFail_whenComputerRegistryReturnsZeroAddress_whenShouldCheckStateFingerprint() external { @@ -580,7 +607,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRefinanceRequest_Test is PWNSimpleLoan ); vm.expectRevert(abi.encodeWithSelector(MissingStateFingerprintComputer.selector)); - requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", "", ""); + requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), permit, ""); } function testFuzz_shouldFail_whenComputerReturnsDifferentStateFingerprint_whenShouldCheckStateFingerprint( @@ -602,19 +629,19 @@ contract PWNSimpleLoanSimpleRequest_AcceptRefinanceRequest_Test is PWNSimpleLoan vm.expectRevert(abi.encodeWithSelector( InvalidCollateralStateFingerprint.selector, stateFingerprint, request.collateralStateFingerprint )); - requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", "", ""); + requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), permit, ""); } function test_shouldFail_whenInvalidSignature_whenEOA() external { vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, request.borrower, _requestHash(request))); - requestContract.acceptRefinanceRequest(loanId, request, _signRequest(1, request), "", "", ""); + requestContract.acceptRefinanceRequest(loanId, request, _signRequest(1, request), permit, ""); } function test_shouldFail_whenInvalidSignature_whenContractAccount() external { vm.etch(borrower, bytes("data")); vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, request.borrower, _requestHash(request))); - requestContract.acceptRefinanceRequest(loanId, request, "", "", "", ""); + requestContract.acceptRefinanceRequest(loanId, request, "", permit, ""); } function test_shouldPass_whenRequestHasBeenMadeOnchain() external { @@ -624,15 +651,15 @@ contract PWNSimpleLoanSimpleRequest_AcceptRefinanceRequest_Test is PWNSimpleLoan bytes32(uint256(1)) ); - requestContract.acceptRefinanceRequest(loanId, request, "", "", "", ""); + requestContract.acceptRefinanceRequest(loanId, request, "", permit, ""); } function test_shouldPass_withValidSignature_whenEOA_whenStandardSignature() external { - requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", "", ""); + requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), permit, ""); } function test_shouldPass_withValidSignature_whenEOA_whenCompactEIP2098Signature() external { - requestContract.acceptRefinanceRequest(loanId, request, _signRequestCompact(borrowerPK, request), "", "", ""); + requestContract.acceptRefinanceRequest(loanId, request, _signRequestCompact(borrowerPK, request), permit, ""); } function test_shouldPass_whenValidSignature_whenContractAccount() external { @@ -644,7 +671,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRefinanceRequest_Test is PWNSimpleLoan abi.encode(bytes4(0x1626ba7e)) ); - requestContract.acceptRefinanceRequest(loanId, request, "", "", "", ""); + requestContract.acceptRefinanceRequest(loanId, request, "", permit, ""); } function testFuzz_shouldFail_whenRequestIsExpired(uint256 timestamp) external { @@ -652,7 +679,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRefinanceRequest_Test is PWNSimpleLoan vm.warp(timestamp); vm.expectRevert(abi.encodeWithSelector(Expired.selector, timestamp, request.expiration)); - requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", "", ""); + requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), permit, ""); } function test_shouldFail_whenRequestNonceNotUsable() external { @@ -669,7 +696,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRefinanceRequest_Test is PWNSimpleLoan vm.expectRevert(abi.encodeWithSelector( NonceNotUsable.selector, request.borrower, request.nonceSpace, request.nonce )); - requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", "", ""); + requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), permit, ""); } function testFuzz_shouldFail_whenCallerIsNotAllowedLender(address caller) external { @@ -679,7 +706,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRefinanceRequest_Test is PWNSimpleLoan vm.expectRevert(abi.encodeWithSelector(CallerNotAllowedAcceptor.selector, caller, request.allowedLender)); vm.prank(caller); - requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", "", ""); + requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), permit, ""); } function testFuzz_shouldFail_whenLessThanMinDuration(uint256 duration) external { @@ -688,7 +715,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRefinanceRequest_Test is PWNSimpleLoan request.duration = uint32(duration); vm.expectRevert(abi.encodeWithSelector(InvalidDuration.selector, duration, requestContract.MIN_LOAN_DURATION())); - requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", "", ""); + requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), permit, ""); } function testFuzz_shouldFail_whenAccruingInterestAPROutOfBounds(uint256 interestAPR) external { @@ -697,7 +724,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRefinanceRequest_Test is PWNSimpleLoan request.accruingInterestAPR = uint40(interestAPR); vm.expectRevert(abi.encodeWithSelector(AccruingInterestAPROutOfBounds.selector, interestAPR, maxInterest)); - requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", "", ""); + requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), permit, ""); } function test_shouldRevokeRequest_whenAvailableCreditLimitEqualToZero() external { @@ -710,7 +737,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRefinanceRequest_Test is PWNSimpleLoan ) ); - requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", "", ""); + requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), permit, ""); } function testFuzz_shouldFail_whenUsedCreditExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { @@ -721,7 +748,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRefinanceRequest_Test is PWNSimpleLoan vm.store(address(requestContract), keccak256(abi.encode(_requestHash(request), CREDIT_USED_SLOT)), bytes32(used)); vm.expectRevert(abi.encodeWithSelector(AvailableCreditLimitExceeded.selector, used + request.creditAmount, limit)); - requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", "", ""); + requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), permit, ""); } function testFuzz_shouldIncreaseUsedCredit_whenUsedCreditNotExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { @@ -731,14 +758,43 @@ contract PWNSimpleLoanSimpleRequest_AcceptRefinanceRequest_Test is PWNSimpleLoan vm.store(address(requestContract), keccak256(abi.encode(_requestHash(request), CREDIT_USED_SLOT)), bytes32(used)); - requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", "", ""); + requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), permit, ""); assertEq(requestContract.creditUsed(_requestHash(request)), used + request.creditAmount); } + function testFuzz_shouldFail_whenPermitOwnerNotCaller(address owner) external { + vm.assume(owner != lender); + + permit.owner = owner; + permit.asset = request.creditAddress; + + vm.expectRevert(abi.encodeWithSelector(InvalidPermitOwner.selector, owner, lender)); + vm.prank(lender); + requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), permit, ""); + } + + function testFuzz_shouldFail_whenPermitAssetNotCreditAsset(address asset) external { + vm.assume(asset != request.creditAddress && asset != address(0)); + + permit.owner = lender; + permit.asset = asset; + + vm.expectRevert(abi.encodeWithSelector(InvalidPermitAsset.selector, asset, token)); + vm.prank(lender); + requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), permit, ""); + } + function test_shouldCallLoanContract() external { - bytes memory creditPermit = "creditPermit"; - bytes memory collateralPermit = "collateralPermit"; + permit = Permit({ + asset: token, + owner: lender, + 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({ @@ -765,19 +821,19 @@ contract PWNSimpleLoanSimpleRequest_AcceptRefinanceRequest_Test is PWNSimpleLoan activeLoanContract, abi.encodeWithSelector( PWNSimpleLoan.refinanceLOAN.selector, - loanId, _requestHash(request), loanTerms, creditPermit, collateralPermit, extra + loanId, _requestHash(request), loanTerms, permit, extra ) ); vm.prank(lender); requestContract.acceptRefinanceRequest( - loanId, request, _signRequest(borrowerPK, request), creditPermit, collateralPermit, extra + loanId, request, _signRequest(borrowerPK, request), permit, extra ); } function test_shouldReturnRefinancedLoanId() external { assertEq( - requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), "", "", ""), + requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), permit, ""), refinancedLoanId ); } @@ -810,8 +866,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRefinanceRequestAndRevokeCallersNonce_ loanId: loanId, request: request, signature: _signRequest(borrowerPK, request), - lenderCreditPermit: "", - borrowerCreditPermit: "", + permit: permit, extra: "", callersNonceSpace: nonceSpace, callersNonceToRevoke: nonce @@ -829,8 +884,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRefinanceRequestAndRevokeCallersNonce_ loanId: loanId, request: request, signature: _signRequest(borrowerPK, request), - lenderCreditPermit: "", - borrowerCreditPermit: "", + permit: permit, extra: "", callersNonceSpace: nonceSpace, callersNonceToRevoke: nonce @@ -843,8 +897,7 @@ contract PWNSimpleLoanSimpleRequest_AcceptRefinanceRequestAndRevokeCallersNonce_ loanId: loanId, request: request, signature: _signRequest(borrowerPK, request), - lenderCreditPermit: "", - borrowerCreditPermit: "", + permit: permit, extra: "", callersNonceSpace: 1, callersNonceToRevoke: 2 diff --git a/test/unit/PWNVault.t.sol b/test/unit/PWNVault.t.sol index db9bc3f..44e43da 100644 --- a/test/unit/PWNVault.t.sol +++ b/test/unit/PWNVault.t.sol @@ -3,21 +3,13 @@ pragma solidity 0.8.16; import "forge-std/Test.sol"; -import "MultiToken/MultiToken.sol"; - -import "openzeppelin-contracts/contracts/utils/introspection/IERC165.sol"; -import "openzeppelin-contracts/contracts/token/ERC721/IERC721Receiver.sol"; -import "openzeppelin-contracts/contracts/token/ERC1155/IERC1155Receiver.sol"; - -import "@pwn/loan/PWNVault.sol"; +import { PWNVault, IERC165, IERC721Receiver, IERC1155Receiver, Permit, MultiToken } from "@pwn/loan/vault/PWNVault.sol"; import "@pwn/PWNErrors.sol"; -import "@pwn-test/helper/token/T721.sol"; +import { T721 } from "@pwn-test/helper/token/T721.sol"; -// The only reason for this contract is to expose internal functions of PWNVault -// No additional logic is applied here -contract PWNVaultExposed is PWNVault { +contract PWNVaultHarness is PWNVault { function pull(MultiToken.Asset memory asset, address origin) external { _pull(asset, origin); @@ -31,15 +23,15 @@ contract PWNVaultExposed is PWNVault { _pushFrom(asset, origin, beneficiary); } - function permit(MultiToken.Asset memory asset, address origin, bytes memory permit_) external { - _permit(asset, origin, permit_); + function exposed_tryPermit(Permit calldata permit) external { + _tryPermit(permit); } } abstract contract PWNVaultTest is Test { - PWNVaultExposed vault; + PWNVaultHarness vault; address token = makeAddr("token"); address alice = makeAddr("alice"); address bob = makeAddr("bob"); @@ -55,8 +47,8 @@ abstract contract PWNVaultTest is Test { vm.etch(token, bytes("data")); } - function setUp() external { - vault = new PWNVaultExposed(); + function setUp() public virtual { + vault = new PWNVaultHarness(); t721 = new T721(); } @@ -204,33 +196,62 @@ contract PWNVault_PushFrom_Test is PWNVaultTest { /*----------------------------------------------------------*| -|* # PERMIT *| +|* # TRY PERMIT *| |*----------------------------------------------------------*/ -contract PWNVault_Permit_Test is PWNVaultTest { +contract PWNVault_TryPermit_Test is PWNVaultTest { - function test_shouldCallPermit_whenPermitNonZero() external { - vm.expectCall( + Permit permit; + string permitSignature = "permit(address,address,uint256,uint256,uint8,bytes32,bytes32)"; + + function setUp() public override { + super.setUp(); + + vm.mockCall( token, - abi.encodeWithSignature( - "permit(address,address,uint256,uint256,uint8,bytes32,bytes32)", - alice, address(vault), 100, 1, uint8(4), bytes32(uint256(2)), bytes32(uint256(3))) + abi.encodeWithSignature("permit(address,address,uint256,uint256,uint8,bytes32,bytes32)"), + abi.encode("") ); - MultiToken.Asset memory asset = MultiToken.Asset(MultiToken.Category.ERC20, token, 0, 100); - bytes memory permit = abi.encodePacked(uint256(1), bytes32(uint256(2)), bytes32(uint256(3)), uint8(4)); - vault.permit(asset, alice, permit); + permit = Permit({ + asset: token, + owner: alice, + amount: 100, + deadline: 1, + v: 4, + r: bytes32(uint256(2)), + s: bytes32(uint256(3)) + }); } - function testFail_shouldNotCallPermit_whenPermitIsZero() external { - // Should fail, because permit is not called + + function test_shouldCallPermit_whenPermitAssetNonZero() external { vm.expectCall( token, - abi.encodeWithSignature("permit(address,address,uint256,uint256,uint8,bytes32,bytes32)") + abi.encodeWithSignature( + permitSignature, + permit.owner, address(vault), permit.amount, permit.deadline, permit.v, permit.r, permit.s + ) ); - MultiToken.Asset memory asset = MultiToken.Asset(MultiToken.Category.ERC20, token, 0, 100); - vault.permit(asset, alice, ""); + vault.exposed_tryPermit(permit); + } + + function test_shouldNotCallPermit_whenPermitIsZero() external { + vm.expectCall({ + callee: token, + data: abi.encodeWithSignature(permitSignature), + count: 0 + }); + + permit.asset = address(0); + vault.exposed_tryPermit(permit); + } + + function test_shouldNotFail_whenPermitReverts() external { + vm.mockCallRevert(token, abi.encodeWithSignature(permitSignature), abi.encode("")); + + vault.exposed_tryPermit(permit); } } From 2c9b7a41dd20e689aa0e34c48418727777a5bf6a Mon Sep 17 00:00:00 2001 From: ashhanai Date: Tue, 19 Mar 2024 16:36:29 -0300 Subject: [PATCH 046/129] feat(on-chain-proposal): emit typed proposal object --- .../simple/proposal/PWNSimpleLoanProposal.sol | 9 +------ .../proposal/offer/PWNSimpleLoanListOffer.sol | 16 +++++++----- .../offer/PWNSimpleLoanSimpleOffer.sol | 16 +++++++----- .../request/PWNSimpleLoanSimpleRequest.sol | 16 +++++++----- test/unit/PWNSimpleLoanListOffer.t.sol | 26 ++++++------------- test/unit/PWNSimpleLoanSimpleOffer.t.sol | 24 +++++------------ test/unit/PWNSimpleLoanSimpleRequest.t.sol | 24 +++++------------ 7 files changed, 53 insertions(+), 78 deletions(-) diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol index cfff0ae..f0a1f54 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol @@ -37,11 +37,6 @@ abstract contract PWNSimpleLoanProposal { */ mapping (bytes32 => uint256) public creditUsed; - /** - * @dev Emitted when a proposal is made via an on-chain transaction. - */ - event ProposalMade(bytes32 indexed proposalHash, address indexed proposer, bytes proposal); - constructor( address _hub, address _revokedNonce, @@ -209,14 +204,12 @@ abstract contract PWNSimpleLoanProposal { * @param proposalHash Proposal hash. * @param proposer Address of a proposal proposer. */ - function _makeProposal(bytes32 proposalHash, address proposer, bytes memory proposal) internal { + function _makeProposal(bytes32 proposalHash, address proposer) internal { if (msg.sender != proposer) { revert CallerIsNotStatedProposer(proposer); } proposalsMade[proposalHash] = true; - - emit ProposalMade(proposalHash, proposer, proposal); } /** diff --git a/src/loan/terms/simple/proposal/offer/PWNSimpleLoanListOffer.sol b/src/loan/terms/simple/proposal/offer/PWNSimpleLoanListOffer.sol index f12fac1..1c233dc 100644 --- a/src/loan/terms/simple/proposal/offer/PWNSimpleLoanListOffer.sol +++ b/src/loan/terms/simple/proposal/offer/PWNSimpleLoanListOffer.sol @@ -84,6 +84,11 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanProposal { 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, @@ -105,9 +110,12 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanProposal { * @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 { - _makeProposal(getOfferHash(offer), offer.lender, abi.encode(offer)); + function makeOffer(Offer calldata offer) external returns (bytes32 proposalHash) { + proposalHash = getOfferHash(offer); + _makeProposal(proposalHash, offer.lender); + emit OfferMade(proposalHash, offer.lender, offer); } /** @@ -324,8 +332,4 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanProposal { }); } - function decodeProposal(bytes calldata proposal) external pure returns (Offer memory offer) { - return abi.decode(proposal, (Offer)); - } - } diff --git a/src/loan/terms/simple/proposal/offer/PWNSimpleLoanSimpleOffer.sol b/src/loan/terms/simple/proposal/offer/PWNSimpleLoanSimpleOffer.sol index a736977..ff3a1f2 100644 --- a/src/loan/terms/simple/proposal/offer/PWNSimpleLoanSimpleOffer.sol +++ b/src/loan/terms/simple/proposal/offer/PWNSimpleLoanSimpleOffer.sol @@ -69,6 +69,11 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanProposal { address loanContract; } + /** + * @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, @@ -90,9 +95,12 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanProposal { * @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 { - _makeProposal(getOfferHash(offer), offer.lender, abi.encode(offer)); + function makeOffer(Offer calldata offer) external returns (bytes32 proposalHash) { + proposalHash = getOfferHash(offer); + _makeProposal(proposalHash, offer.lender); + emit OfferMade(proposalHash, offer.lender, offer); } function acceptOffer( @@ -236,8 +244,4 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanProposal { }); } - function decodeProposal(bytes calldata proposal) external pure returns (Offer memory offer) { - return abi.decode(proposal, (Offer)); - } - } diff --git a/src/loan/terms/simple/proposal/request/PWNSimpleLoanSimpleRequest.sol b/src/loan/terms/simple/proposal/request/PWNSimpleLoanSimpleRequest.sol index e4c0884..1cead41 100644 --- a/src/loan/terms/simple/proposal/request/PWNSimpleLoanSimpleRequest.sol +++ b/src/loan/terms/simple/proposal/request/PWNSimpleLoanSimpleRequest.sol @@ -69,6 +69,11 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanProposal { address loanContract; } + /** + * @dev Emitted when a proposal is made via an on-chain transaction. + */ + event RequestMade(bytes32 indexed proposalHash, address indexed proposer, Request request); + constructor( address _hub, address _revokedNonce, @@ -90,9 +95,12 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanProposal { * @notice Make an on-chain request. * @dev Function will mark a request hash as proposed. Request will become acceptable by a lender without a request signature. * @param request Request struct containing all needed request data. + * @return proposalHash Request hash. */ - function makeRequest(Request calldata request) external { - _makeProposal(getRequestHash(request), request.borrower, abi.encode(request)); + function makeRequest(Request calldata request) external returns (bytes32 proposalHash){ + proposalHash = getRequestHash(request); + _makeProposal(proposalHash, request.borrower); + emit RequestMade(proposalHash, request.borrower, request); } @@ -237,8 +245,4 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanProposal { }); } - function decodeProposal(bytes calldata proposal) external pure returns (Request memory request) { - return abi.decode(proposal, (Request)); - } - } diff --git a/test/unit/PWNSimpleLoanListOffer.t.sol b/test/unit/PWNSimpleLoanListOffer.t.sol index d1ea01a..3c3a674 100644 --- a/test/unit/PWNSimpleLoanListOffer.t.sol +++ b/test/unit/PWNSimpleLoanListOffer.t.sol @@ -32,7 +32,7 @@ abstract contract PWNSimpleLoanListOfferTest is Test { uint256 refinancedLoanId = 123; Permit permit; - event ProposalMade(bytes32 indexed proposalHash, address indexed proposer, bytes proposal); + event OfferMade(bytes32 indexed proposalHash, address indexed proposer, PWNSimpleLoanListOffer.Offer offer); function setUp() virtual public { vm.etch(hub, bytes("data")); @@ -177,9 +177,9 @@ contract PWNSimpleLoanListOffer_MakeOffer_Test is PWNSimpleLoanListOfferTest { offerContract.makeOffer(offer); } - function test_shouldEmit_ProposalMade() external { + function test_shouldEmit_OfferMade() external { vm.expectEmit(); - emit ProposalMade(_offerHash(offer), offer.lender, abi.encode(offer)); + emit OfferMade(_offerHash(offer), offer.lender, offer); vm.prank(offer.lender); offerContract.makeOffer(offer); @@ -192,6 +192,11 @@ contract PWNSimpleLoanListOffer_MakeOffer_Test is PWNSimpleLoanListOfferTest { assertTrue(offerContract.proposalsMade(_offerHash(offer))); } + function test_shouldReturnOfferHash() external { + vm.prank(offer.lender); + assertEq(offerContract.makeOffer(offer), _offerHash(offer)); + } + } @@ -966,18 +971,3 @@ contract PWNSimpleLoanListOffer_AcceptRefinanceOfferAndRevokeCallersNonce_Test i } } - - -/*----------------------------------------------------------*| -|* # DECODE PROPOSAL *| -|*----------------------------------------------------------*/ - -contract PWNSimpleLoanListOffer_DecodeProposal_Test is PWNSimpleLoanListOfferTest { - - function test_shouldReturnDecodedOfferData() external { - PWNSimpleLoanListOffer.Offer memory decodedOffer = offerContract.decodeProposal(abi.encode(offer)); - - assertEq(_offerHash(decodedOffer), _offerHash(offer)); - } - -} diff --git a/test/unit/PWNSimpleLoanSimpleOffer.t.sol b/test/unit/PWNSimpleLoanSimpleOffer.t.sol index 49bcb09..9d5e52f 100644 --- a/test/unit/PWNSimpleLoanSimpleOffer.t.sol +++ b/test/unit/PWNSimpleLoanSimpleOffer.t.sol @@ -31,7 +31,7 @@ abstract contract PWNSimpleLoanSimpleOfferTest is Test { uint256 refinancedLoanId = 123; Permit permit; - event ProposalMade(bytes32 indexed proposalHash, address indexed proposer, bytes proposal); + event OfferMade(bytes32 indexed proposalHash, address indexed proposer, PWNSimpleLoanSimpleOffer.Offer offer); function setUp() virtual public { vm.etch(hub, bytes("data")); @@ -173,7 +173,7 @@ contract PWNSimpleLoanSimpleOffer_MakeOffer_Test is PWNSimpleLoanSimpleOfferTest function test_shouldEmit_OfferMade() external { vm.expectEmit(); - emit ProposalMade(_offerHash(offer), offer.lender, abi.encode(offer)); + emit OfferMade(_offerHash(offer), offer.lender, offer); vm.prank(offer.lender); offerContract.makeOffer(offer); @@ -186,6 +186,11 @@ contract PWNSimpleLoanSimpleOffer_MakeOffer_Test is PWNSimpleLoanSimpleOfferTest assertTrue(offerContract.proposalsMade(_offerHash(offer))); } + function test_shouldReturnOfferHash() external { + vm.prank(offer.lender); + assertEq(offerContract.makeOffer(offer), _offerHash(offer)); + } + } @@ -890,18 +895,3 @@ contract PWNSimpleLoanSimpleOffer_AcceptRefinanceOfferAndRevokeCallersNonce_Test } } - - -/*----------------------------------------------------------*| -|* # DECODE PROPOSAL *| -|*----------------------------------------------------------*/ - -contract PWNSimpleLoanSimpleOffer_DecodeProposal_Test is PWNSimpleLoanSimpleOfferTest { - - function test_shouldReturnDecodedOfferData() external { - PWNSimpleLoanSimpleOffer.Offer memory decodedOffer = offerContract.decodeProposal(abi.encode(offer)); - - assertEq(_offerHash(decodedOffer), _offerHash(offer)); - } - -} diff --git a/test/unit/PWNSimpleLoanSimpleRequest.t.sol b/test/unit/PWNSimpleLoanSimpleRequest.t.sol index 8f18283..12dbf1d 100644 --- a/test/unit/PWNSimpleLoanSimpleRequest.t.sol +++ b/test/unit/PWNSimpleLoanSimpleRequest.t.sol @@ -31,7 +31,7 @@ abstract contract PWNSimpleLoanSimpleRequestTest is Test { uint256 refinancedLoanId = 123; Permit permit; - event ProposalMade(bytes32 indexed proposalHash, address indexed proposer, bytes proposal); + event RequestMade(bytes32 indexed proposalHash, address indexed proposer, PWNSimpleLoanSimpleRequest.Request request); function setUp() virtual public { vm.etch(hub, bytes("data")); @@ -173,7 +173,7 @@ contract PWNSimpleLoanSimpleRequest_MakeRequest_Test is PWNSimpleLoanSimpleReque function test_shouldEmit_RequestMade() external { vm.expectEmit(); - emit ProposalMade(_requestHash(request), request.borrower, abi.encode(request)); + emit RequestMade(_requestHash(request), request.borrower, request); vm.prank(request.borrower); requestContract.makeRequest(request); @@ -186,6 +186,11 @@ contract PWNSimpleLoanSimpleRequest_MakeRequest_Test is PWNSimpleLoanSimpleReque assertTrue(requestContract.proposalsMade(_requestHash(request))); } + function test_shouldReturnRequestHash() external { + vm.prank(request.borrower); + assertEq(requestContract.makeRequest(request), _requestHash(request)); + } + } @@ -907,18 +912,3 @@ contract PWNSimpleLoanSimpleRequest_AcceptRefinanceRequestAndRevokeCallersNonce_ } } - - -/*----------------------------------------------------------*| -|* # DECODE PROPOSAL *| -|*----------------------------------------------------------*/ - -contract PWNSimpleLoanSimpleRequest_DecodeProposal_Test is PWNSimpleLoanSimpleRequestTest { - - function test_shouldReturnDecodedRequestData() external { - PWNSimpleLoanSimpleRequest.Request memory decodedRequest = requestContract.decodeProposal(abi.encode(request)); - - assertEq(_requestHash(decodedRequest), _requestHash(request)); - } - -} From 7ad0d044446e05670c72d17c739d182d7e475c08 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Wed, 20 Mar 2024 12:20:48 -0300 Subject: [PATCH 047/129] feat(fungible-offer): implement simple loan fungible offer --- src/PWNErrors.sol | 2 + .../offer/PWNSimpleLoanFungibleOffer.sol | 342 +++++++ .../proposal/offer/PWNSimpleLoanListOffer.sol | 8 +- .../offer/PWNSimpleLoanSimpleOffer.sol | 4 + .../request/PWNSimpleLoanSimpleRequest.sol | 4 + test/unit/PWNSimpleLoanFungibleOffer.t.sol | 964 ++++++++++++++++++ 6 files changed, 1321 insertions(+), 3 deletions(-) create mode 100644 src/loan/terms/simple/proposal/offer/PWNSimpleLoanFungibleOffer.sol create mode 100644 test/unit/PWNSimpleLoanFungibleOffer.t.sol diff --git a/src/PWNErrors.sol b/src/PWNErrors.sol index bddef09..19e5bea 100644 --- a/src/PWNErrors.sol +++ b/src/PWNErrors.sol @@ -43,6 +43,8 @@ error InvalidSignature(address signer, bytes32 digest); // Offer error CollateralIdNotWhitelisted(uint256 id); +error MinCollateralAmountNotSet(); +error InsufficientCollateralAmount(uint256 current, uint256 limit); // Proposal error CallerIsNotStatedProposer(address); diff --git a/src/loan/terms/simple/proposal/offer/PWNSimpleLoanFungibleOffer.sol b/src/loan/terms/simple/proposal/offer/PWNSimpleLoanFungibleOffer.sol new file mode 100644 index 0000000..9cb874d --- /dev/null +++ b/src/loan/terms/simple/proposal/offer/PWNSimpleLoanFungibleOffer.sol @@ -0,0 +1,342 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.16; + +import { MultiToken } from "MultiToken/MultiToken.sol"; + +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"; + +/** + * @title PWN Simple Loan Fungible Offer + * @notice Contract for creating and accepting fungible loan offers. + * Offers are fungible, which means that they are not tied to a specific collateral or credit amount. + * The amount of collateral and credit is specified during the offer acceptance. + */ +contract PWNSimpleLoanFungibleOffer is PWNSimpleLoanProposal { + + string public constant VERSION = "1.2"; + + /** + * @notice Credit per collateral unit denominator. It is used to calculate credit amount from collateral amount. + */ + uint256 public constant CREDIT_PER_COLLATERAL_UNIT_DENOMINATOR = 1e38; + + /** + * @dev EIP-712 simple offer struct type hash. + */ + bytes32 public constant OFFER_TYPEHASH = keccak256( + "Offer(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 minCollateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address creditAddress,uint256 creditPerCollateralUnit,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 fungible offer. + * @param collateralCategory Category of an asset used as a collateral (0 == ERC20, 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. + * @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 creditPerCollateralUnit Amount of tokens which are offered per collateral unit with 38 decimals. + * @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; + uint256 collateralId; + uint256 minCollateralAmount; + bool checkCollateralStateFingerprint; + bytes32 collateralStateFingerprint; + address creditAddress; + uint256 creditPerCollateralUnit; + 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 collateralAmount Amount of collateral to be used in the loan. + */ + struct OfferValues { + uint256 collateralAmount; + } + + /** + * @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, "PWNSimpleLoanFungibleOffer", 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 min collateral amount + if (offer.minCollateralAmount == 0) { + revert MinCollateralAmountNotSet(); + } + if (offerValues.collateralAmount < offer.minCollateralAmount) { + revert InsufficientCollateralAmount({ + current: offerValues.collateralAmount, + limit: offer.minCollateralAmount + }); + } + + // Check collateral state fingerprint if needed + if (offer.checkCollateralStateFingerprint) { + _checkCollateralState({ + addr: offer.collateralAddress, + id: offer.collateralId, + stateFingerprint: offer.collateralStateFingerprint + }); + } + + // Calculate credit amount + uint256 creditAmount = _creditAmount(offerValues.collateralAmount, offer.creditPerCollateralUnit); + + // Try to accept offer + offerHash = _tryAcceptOffer(offer, creditAmount, signature); + + // Create loan terms object + loanTerms = _createLoanTerms(offer, offerValues.collateralAmount, creditAmount); + } + + function _creditAmount(uint256 collateralAmount, uint256 creditPerCollateralUnit) private pure returns (uint256) { + return Math.mulDiv(collateralAmount, creditPerCollateralUnit, CREDIT_PER_COLLATERAL_UNIT_DENOMINATOR); + } + + function _tryAcceptOffer( + Offer calldata offer, + uint256 creditAmount, + bytes calldata signature + ) private returns (bytes32 offerHash) { + offerHash = getOfferHash(offer); + _tryAcceptProposal({ + proposalHash: offerHash, + creditAmount: 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, + uint256 collateralAmount, + uint256 creditAmount + ) 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: offer.collateralId, + amount: collateralAmount + }), + credit: MultiToken.ERC20({ + assetAddress: offer.creditAddress, + amount: creditAmount + }), + fixedInterestAmount: offer.fixedInterestAmount, + accruingInterestAPR: offer.accruingInterestAPR + }); + } + +} diff --git a/src/loan/terms/simple/proposal/offer/PWNSimpleLoanListOffer.sol b/src/loan/terms/simple/proposal/offer/PWNSimpleLoanListOffer.sol index 1c233dc..d33cc39 100644 --- a/src/loan/terms/simple/proposal/offer/PWNSimpleLoanListOffer.sol +++ b/src/loan/terms/simple/proposal/offer/PWNSimpleLoanListOffer.sol @@ -73,7 +73,7 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanProposal { } /** - * Construct defining an Offer concrete values + * @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. @@ -120,7 +120,6 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanProposal { /** * @notice Accept an offer. - * @dev Function will mark an offer hash 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. @@ -157,7 +156,6 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanProposal { /** * @notice Accept a refinancing offer. - * @dev Function will mark an offer hash 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. @@ -248,6 +246,10 @@ contract PWNSimpleLoanListOffer is PWNSimpleLoanProposal { } + /*----------------------------------------------------------*| + |* # INTERNALS *| + |*----------------------------------------------------------*/ + function _acceptOffer( Offer calldata offer, OfferValues calldata offerValues, diff --git a/src/loan/terms/simple/proposal/offer/PWNSimpleLoanSimpleOffer.sol b/src/loan/terms/simple/proposal/offer/PWNSimpleLoanSimpleOffer.sol index ff3a1f2..357e9d6 100644 --- a/src/loan/terms/simple/proposal/offer/PWNSimpleLoanSimpleOffer.sol +++ b/src/loan/terms/simple/proposal/offer/PWNSimpleLoanSimpleOffer.sol @@ -183,6 +183,10 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanProposal { } + /*----------------------------------------------------------*| + |* # INTERNALS *| + |*----------------------------------------------------------*/ + function _acceptOffer( Offer calldata offer, bytes calldata signature diff --git a/src/loan/terms/simple/proposal/request/PWNSimpleLoanSimpleRequest.sol b/src/loan/terms/simple/proposal/request/PWNSimpleLoanSimpleRequest.sol index 1cead41..665a567 100644 --- a/src/loan/terms/simple/proposal/request/PWNSimpleLoanSimpleRequest.sol +++ b/src/loan/terms/simple/proposal/request/PWNSimpleLoanSimpleRequest.sol @@ -184,6 +184,10 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanProposal { } + /*----------------------------------------------------------*| + |* # INTERNALS *| + |*----------------------------------------------------------*/ + function _acceptRequest( Request calldata request, bytes calldata signature diff --git a/test/unit/PWNSimpleLoanFungibleOffer.t.sol b/test/unit/PWNSimpleLoanFungibleOffer.t.sol new file mode 100644 index 0000000..303ef8c --- /dev/null +++ b/test/unit/PWNSimpleLoanFungibleOffer.t.sol @@ -0,0 +1,964 @@ +// 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 { PWNSimpleLoanFungibleOffer, PWNSimpleLoan, Permit } + from "@pwn/loan/terms/simple/proposal/offer/PWNSimpleLoanFungibleOffer.sol"; +import "@pwn/PWNErrors.sol"; + + +abstract contract PWNSimpleLoanFungibleOfferTest 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 + + PWNSimpleLoanFungibleOffer offerContract; + address hub = makeAddr("hub"); + address revokedNonce = makeAddr("revokedNonce"); + address stateFingerprintComputerRegistry = makeAddr("stateFingerprintComputerRegistry"); + address activeLoanContract = makeAddr("activeLoanContract"); + PWNSimpleLoanFungibleOffer.Offer offer; + PWNSimpleLoanFungibleOffer.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, PWNSimpleLoanFungibleOffer.Offer offer); + + function setUp() virtual public { + vm.etch(hub, bytes("data")); + vm.etch(revokedNonce, bytes("data")); + vm.etch(token, bytes("data")); + + offerContract = new PWNSimpleLoanFungibleOffer(hub, revokedNonce, stateFingerprintComputerRegistry); + + offer = PWNSimpleLoanFungibleOffer.Offer({ + collateralCategory: MultiToken.Category.ERC1155, + collateralAddress: token, + collateralId: 0, + minCollateralAmount: 100, + checkCollateralStateFingerprint: true, + collateralStateFingerprint: keccak256("some state fingerprint"), + creditAddress: token, + creditPerCollateralUnit: 10 * offerContract.CREDIT_PER_COLLATERAL_UNIT_DENOMINATOR(), + 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 = PWNSimpleLoanFungibleOffer.OfferValues({ + collateralAmount: 1000 + }); + + 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(PWNSimpleLoanFungibleOffer.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("PWNSimpleLoanFungibleOffer"), + keccak256("1.2"), + block.chainid, + address(offerContract) + )), + keccak256(abi.encodePacked( + keccak256("Offer(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 minCollateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address creditAddress,uint256 creditPerCollateralUnit,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, PWNSimpleLoanFungibleOffer.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, PWNSimpleLoanFungibleOffer.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); + } + + function _creditAmount(uint256 collateralAmount, uint256 creditPerCollateralUnit) internal view returns (uint256) { + return Math.mulDiv(collateralAmount, creditPerCollateralUnit, offerContract.CREDIT_PER_COLLATERAL_UNIT_DENOMINATOR()); + } + +} + + +/*----------------------------------------------------------*| +|* # CREDIT USED *| +|*----------------------------------------------------------*/ + +contract PWNSimpleLoanFungibleOffer_CreditUsed_Test is PWNSimpleLoanFungibleOfferTest { + + 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 PWNSimpleLoanFungibleOffer_GetOfferHash_Test is PWNSimpleLoanFungibleOfferTest { + + function test_shouldReturnOfferHash() external { + assertEq(_offerHash(offer), offerContract.getOfferHash(offer)); + } + +} + + +/*----------------------------------------------------------*| +|* # MAKE OFFER *| +|*----------------------------------------------------------*/ + +contract PWNSimpleLoanFungibleOffer_MakeOffer_Test is PWNSimpleLoanFungibleOfferTest { + + 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 PWNSimpleLoanFungibleOffer_RevokeNonce_Test is PWNSimpleLoanFungibleOfferTest { + + 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 PWNSimpleLoanFungibleOffer_AcceptOffer_Test is PWNSimpleLoanFungibleOfferTest { + + 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_shouldFail_whenZeroMinCollateralAmount() external { + offer.minCollateralAmount = 0; + + vm.expectRevert(abi.encodeWithSelector(MinCollateralAmountNotSet.selector)); + offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), permit, ""); + } + + function testFuzz_shouldFail_whenCollateralAmountLessThanMinCollateralAmount(uint256 collateralAmount) external { + collateralAmount = bound(collateralAmount, 0, offer.minCollateralAmount - 1); + offerValues.collateralAmount = collateralAmount; + + vm.expectRevert(abi.encodeWithSelector( + InsufficientCollateralAmount.selector, collateralAmount, offer.minCollateralAmount + )); + 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)", offer.collateralId), + abi.encode(stateFingerprint) + ); + + vm.expectCall( + stateFingerprintComputer, + abi.encodeWithSignature("getStateFingerprint(uint256)", offer.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 { + uint256 creditAmount = _creditAmount(offerValues.collateralAmount, offer.creditPerCollateralUnit); + used = bound(used, 1, type(uint256).max - creditAmount); + limit = bound(limit, used, used + 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 + creditAmount, limit)); + offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), permit, ""); + } + + function testFuzz_shouldIncreaseUsedCredit_whenUsedCreditNotExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { + uint256 creditAmount = _creditAmount(offerValues.collateralAmount, offer.creditPerCollateralUnit); + used = bound(used, 1, type(uint256).max - creditAmount); + limit = bound(limit, used + 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 + 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 testFuzz_shouldCallLoanContractWithLoanTerms( + uint256 collateralAmount, uint256 creditPerCollateralUnit + ) external { + offerValues.collateralAmount = bound(collateralAmount, offer.minCollateralAmount, 1e40); + offer.creditPerCollateralUnit = bound(creditPerCollateralUnit, 0, 1e40); + uint256 creditAmount = _creditAmount(offerValues.collateralAmount, offer.creditPerCollateralUnit); + + 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: offer.collateralId, + amount: offerValues.collateralAmount + }), + credit: MultiToken.Asset({ + category: MultiToken.Category.ERC20, + assetAddress: offer.creditAddress, + id: 0, + amount: 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 PWNSimpleLoanFungibleOffer_AcceptOfferAndRevokeCallersNonce_Test is PWNSimpleLoanFungibleOfferTest { + + 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 PWNSimpleLoanFungibleOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanFungibleOfferTest { + + 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_shouldFail_whenZeroMinCollateralAmount() external { + offer.minCollateralAmount = 0; + + vm.expectRevert(abi.encodeWithSelector(MinCollateralAmountNotSet.selector)); + offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), permit, ""); + } + + function testFuzz_shouldFail_whenCollateralAmountLessThanMinCollateralAmount(uint256 collateralAmount) external { + collateralAmount = bound(collateralAmount, 0, offer.minCollateralAmount - 1); + offerValues.collateralAmount = collateralAmount; + + vm.expectRevert(abi.encodeWithSelector( + InsufficientCollateralAmount.selector, collateralAmount, offer.minCollateralAmount + )); + 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)", offer.collateralId), + abi.encode(stateFingerprint) + ); + + vm.expectCall( + stateFingerprintComputer, + abi.encodeWithSignature("getStateFingerprint(uint256)", offer.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 { + uint256 creditAmount = _creditAmount(offerValues.collateralAmount, offer.creditPerCollateralUnit); + used = bound(used, 1, type(uint256).max - creditAmount); + limit = bound(limit, used, used + 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 + creditAmount, limit)); + offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), permit, ""); + } + + function testFuzz_shouldIncreaseUsedCredit_whenUsedCreditNotExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { + uint256 creditAmount = _creditAmount(offerValues.collateralAmount, offer.creditPerCollateralUnit); + used = bound(used, 1, type(uint256).max - creditAmount); + limit = bound(limit, used + 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 + 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 testFuzz_shouldCallLoanContractWithLoanTerms( + uint256 collateralAmount, uint256 creditPerCollateralUnit + ) external { + offerValues.collateralAmount = bound(collateralAmount, offer.minCollateralAmount, 1e40); + offer.creditPerCollateralUnit = bound(creditPerCollateralUnit, 0, 1e40); + uint256 creditAmount = _creditAmount(offerValues.collateralAmount, offer.creditPerCollateralUnit); + + 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: offer.collateralId, + amount: offerValues.collateralAmount + }), + credit: MultiToken.Asset({ + category: MultiToken.Category.ERC20, + assetAddress: offer.creditAddress, + id: 0, + amount: 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 PWNSimpleLoanFungibleOffer_AcceptRefinanceOfferAndRevokeCallersNonce_Test is PWNSimpleLoanFungibleOfferTest { + + 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); + } + +} From 9f79ed65f6794c926b3859bb9fe6026b2401c9d0 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Wed, 20 Mar 2024 12:25:49 -0300 Subject: [PATCH 048/129] docs: update accept offer / request docs --- .../offer/PWNSimpleLoanFungibleOffer.sol | 1 + .../offer/PWNSimpleLoanSimpleOffer.sol | 40 +++++++++++++++++++ .../request/PWNSimpleLoanSimpleRequest.sol | 39 +++++++++++++++++- 3 files changed, 79 insertions(+), 1 deletion(-) diff --git a/src/loan/terms/simple/proposal/offer/PWNSimpleLoanFungibleOffer.sol b/src/loan/terms/simple/proposal/offer/PWNSimpleLoanFungibleOffer.sol index 9cb874d..31f25a0 100644 --- a/src/loan/terms/simple/proposal/offer/PWNSimpleLoanFungibleOffer.sol +++ b/src/loan/terms/simple/proposal/offer/PWNSimpleLoanFungibleOffer.sol @@ -10,6 +10,7 @@ import { PWNSimpleLoanProposal } from "@pwn/loan/terms/simple/proposal/PWNSimple import { Permit } from "@pwn/loan/vault/Permit.sol"; import "@pwn/PWNErrors.sol"; + /** * @title PWN Simple Loan Fungible Offer * @notice Contract for creating and accepting fungible loan offers. diff --git a/src/loan/terms/simple/proposal/offer/PWNSimpleLoanSimpleOffer.sol b/src/loan/terms/simple/proposal/offer/PWNSimpleLoanSimpleOffer.sol index 357e9d6..d2fbf8f 100644 --- a/src/loan/terms/simple/proposal/offer/PWNSimpleLoanSimpleOffer.sol +++ b/src/loan/terms/simple/proposal/offer/PWNSimpleLoanSimpleOffer.sol @@ -103,6 +103,14 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanProposal { emit OfferMade(proposalHash, offer.lender, offer); } + /** + * @notice Accept an offer. + * @param offer Offer struct containing all offer data. + * @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, bytes calldata signature, @@ -129,6 +137,15 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanProposal { }); } + /** + * @notice Accept a refinancing offer. + * @param loanId Id of a loan to be refinanced. + * @param offer Offer struct containing all offer data. + * @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, @@ -157,6 +174,17 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanProposal { }); } + /** + * @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 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, bytes calldata signature, @@ -169,6 +197,18 @@ contract PWNSimpleLoanSimpleOffer is PWNSimpleLoanProposal { return acceptOffer(offer, 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 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, diff --git a/src/loan/terms/simple/proposal/request/PWNSimpleLoanSimpleRequest.sol b/src/loan/terms/simple/proposal/request/PWNSimpleLoanSimpleRequest.sol index 665a567..fdc7ee0 100644 --- a/src/loan/terms/simple/proposal/request/PWNSimpleLoanSimpleRequest.sol +++ b/src/loan/terms/simple/proposal/request/PWNSimpleLoanSimpleRequest.sol @@ -103,7 +103,14 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanProposal { emit RequestMade(proposalHash, request.borrower, request); } - + /** + * @notice Accept a request. + * @param request Request struct containing all needed request data. + * @param signature Borrower's signature of the request. + * @param permit Permit struct containing a credit token permit. + * @param extra Extra data to be passed to the loan contract. + * @return loanId Loan id. + */ function acceptRequest( Request calldata request, bytes calldata signature, @@ -130,6 +137,15 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanProposal { }); } + /** + * @notice Accept a refinancing request. + * @param loanId Id of a loan which is refinanced by the request. + * @param request Request struct containing all needed request data. + * @param signature Borrower's signature of the request. + * @param permit Permit struct containing a credit token permit. + * @param extra Extra data to be passed to the loan contract. + * @return refinancedLoanId Refinanced loan id. + */ function acceptRefinanceRequest( uint256 loanId, Request calldata request, @@ -158,6 +174,16 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanProposal { }); } + /** + * @notice Accept a request with a nonce revocation. + * @param request Request struct containing all needed request data. + * @param signature Borrower's signature of the request. + * @param permit Permit struct containing a credit token permit. + * @param extra Extra data to be passed to the loan contract. + * @param callersNonceSpace Nonce space of a caller's nonce to revoke. + * @param callersNonceToRevoke Nonce to revoke. + * @return loanId Loan id. + */ function acceptRequest( Request calldata request, bytes calldata signature, @@ -170,6 +196,17 @@ contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanProposal { return acceptRequest(request, signature, permit, extra); } + /** + * @notice Accept a refinancing request with a nonce revocation. + * @param loanId Id of a loan which is refinanced by the request. + * @param request Request struct containing all needed request data. + * @param signature Borrower's signature of the request. + * @param permit Permit struct containing a credit token permit. + * @param extra Extra data to be passed to the loan contract. + * @param callersNonceSpace Nonce space of a caller's nonce to revoke. + * @param callersNonceToRevoke Nonce to revoke. + * @return refinancedLoanId Refinanced loan id. + */ function acceptRefinanceRequest( uint256 loanId, Request calldata request, From fde8463e030c167e0e79e22bb7b73d8107dbedd0 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Thu, 21 Mar 2024 16:20:38 -0300 Subject: [PATCH 049/129] feat(fungible-proposal): update fungible offer to support borrower signed requests in the same contract --- .../PWNSimpleLoanFungibleProposal.sol | 349 +++++++++ test/unit/PWNSimpleLoanFungibleProposal.t.sol | 395 ++++++++++ test/unit/PWNSimpleLoanProposal.t.sol | 681 ++++++++++++++++++ 3 files changed, 1425 insertions(+) create mode 100644 src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol create mode 100644 test/unit/PWNSimpleLoanFungibleProposal.t.sol create mode 100644 test/unit/PWNSimpleLoanProposal.t.sol diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol new file mode 100644 index 0000000..adf269f --- /dev/null +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol @@ -0,0 +1,349 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.16; + +import { MultiToken } from "MultiToken/MultiToken.sol"; + +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"; + + +/** + * @title PWN Simple Loan Fungible Proposal + * @notice Contract for creating and accepting fungible loan proposals. + * Proposals are fungible, which means that they are not tied to a specific collateral or credit amount. + * The amount of collateral and credit is specified during the proposal acceptance. + */ +contract PWNSimpleLoanFungibleProposal is PWNSimpleLoanProposal { + + string public constant VERSION = "1.2"; + + /** + * @notice Credit per collateral unit denominator. It is used to calculate credit amount from collateral amount. + */ + uint256 public constant CREDIT_PER_COLLATERAL_UNIT_DENOMINATOR = 1e38; + + /** + * @dev EIP-712 simple proposal struct type hash. + */ + bytes32 public constant PROPOSAL_TYPEHASH = keccak256( + "Proposal(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 minCollateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address creditAddress,uint256 creditPerCollateralUnit,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 fungible proposal. + * @param collateralCategory Category of an asset used as a collateral (0 == ERC20, 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. + * @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 lended to a borrower. + * @param creditPerCollateralUnit Amount of tokens which are offered per collateral unit with 38 decimals. + * @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 with a collateral 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 will make others in the group invalid. + * @param loanContract Address of a loan contract that will create a loan from the proposal. + */ + struct Proposal { + MultiToken.Category collateralCategory; + address collateralAddress; + uint256 collateralId; + uint256 minCollateralAmount; + bool checkCollateralStateFingerprint; + bytes32 collateralStateFingerprint; + address creditAddress; + uint256 creditPerCollateralUnit; + 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 collateralAmount Amount of collateral to be used in the loan. + */ + struct ProposalValues { + uint256 collateralAmount; + } + + /** + * @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, "PWNSimpleLoanFungibleProposal", 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 min collateral amount + if (proposal.minCollateralAmount == 0) { + revert MinCollateralAmountNotSet(); + } + if (proposalValues.collateralAmount < proposal.minCollateralAmount) { + revert InsufficientCollateralAmount({ + current: proposalValues.collateralAmount, + limit: proposal.minCollateralAmount + }); + } + + // Check collateral state fingerprint if needed + if (proposal.checkCollateralStateFingerprint) { + _checkCollateralState({ + addr: proposal.collateralAddress, + id: proposal.collateralId, + stateFingerprint: proposal.collateralStateFingerprint + }); + } + + // Calculate credit amount + uint256 creditAmount = _creditAmount(proposalValues.collateralAmount, proposal.creditPerCollateralUnit); + + // Try to accept proposal + proposalHash = _tryAcceptProposal(proposal, creditAmount, signature); + + // Create loan terms object + loanTerms = _createLoanTerms(proposal, proposalValues.collateralAmount, creditAmount); + } + + function _creditAmount(uint256 collateralAmount, uint256 creditPerCollateralUnit) private pure returns (uint256) { + return Math.mulDiv(collateralAmount, creditPerCollateralUnit, CREDIT_PER_COLLATERAL_UNIT_DENOMINATOR); + } + + 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, + duration: proposal.duration, + collateral: MultiToken.Asset({ + category: proposal.collateralCategory, + assetAddress: proposal.collateralAddress, + id: proposal.collateralId, + amount: collateralAmount + }), + credit: MultiToken.ERC20({ + assetAddress: proposal.creditAddress, + amount: creditAmount + }), + fixedInterestAmount: proposal.fixedInterestAmount, + accruingInterestAPR: proposal.accruingInterestAPR + }); + } + +} diff --git a/test/unit/PWNSimpleLoanFungibleProposal.t.sol b/test/unit/PWNSimpleLoanFungibleProposal.t.sol new file mode 100644 index 0000000..6033d3c --- /dev/null +++ b/test/unit/PWNSimpleLoanFungibleProposal.t.sol @@ -0,0 +1,395 @@ +// 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 { PWNSimpleLoanFungibleProposal, PWNSimpleLoanProposal, PWNSimpleLoan, Permit } + from "@pwn/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.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 PWNSimpleLoanFungibleProposalTest is PWNSimpleLoanProposalTest { + + PWNSimpleLoanFungibleProposal proposalContract; + PWNSimpleLoanFungibleProposal.Proposal proposal; + PWNSimpleLoanFungibleProposal.ProposalValues proposalValues; + + event OfferMade(bytes32 indexed proposalHash, address indexed proposer, PWNSimpleLoanFungibleProposal.Proposal proposal); + + function setUp() virtual public override { + super.setUp(); + + proposalContract = new PWNSimpleLoanFungibleProposal(hub, revokedNonce, stateFingerprintComputerRegistry); + proposalContractAddr = PWNSimpleLoanProposal(proposalContract); + + proposal = PWNSimpleLoanFungibleProposal.Proposal({ + collateralCategory: MultiToken.Category.ERC1155, + collateralAddress: token, + collateralId: 0, + minCollateralAmount: 1, + checkCollateralStateFingerprint: true, + collateralStateFingerprint: keccak256("some state fingerprint"), + creditAddress: token, + creditPerCollateralUnit: 1 * proposalContract.CREDIT_PER_COLLATERAL_UNIT_DENOMINATOR(), + 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 = PWNSimpleLoanFungibleProposal.ProposalValues({ + collateralAmount: 1000 + }); + } + + + function _proposalHash(PWNSimpleLoanFungibleProposal.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("PWNSimpleLoanFungibleProposal"), + keccak256("1.2"), + block.chainid, + proposalContractAddr + )), + keccak256(abi.encodePacked( + keccak256("Proposal(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 minCollateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address creditAddress,uint256 creditPerCollateralUnit,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 _creditAmount(uint256 collateralAmount, uint256 creditPerCollateralUnit) internal view returns (uint256) { + return Math.mulDiv(collateralAmount, creditPerCollateralUnit, proposalContract.CREDIT_PER_COLLATERAL_UNIT_DENOMINATOR()); + } + + 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; + } + + 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); + } + +} + + +/*----------------------------------------------------------*| +|* # ACCEPT PROPOSAL *| +|*----------------------------------------------------------*/ + +contract PWNSimpleLoanFungibleProposal_AcceptProposal_Test is PWNSimpleLoanFungibleProposalTest, PWNSimpleLoanProposal_AcceptProposal_Test { + + function setUp() virtual public override(PWNSimpleLoanFungibleProposalTest, 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_shouldFail_whenZeroMinCollateralAmount() external { + proposal.minCollateralAmount = 0; + + vm.expectRevert(abi.encodeWithSelector(MinCollateralAmountNotSet.selector)); + proposalContract.acceptProposal( + proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, "" + ); + } + + function testFuzz_shouldFail_whenCollateralAmountLessThanMinCollateralAmount( + uint256 minCollateralAmount, uint256 collateralAmount + ) external { + proposal.minCollateralAmount = bound(minCollateralAmount, 1, type(uint256).max); + proposalValues.collateralAmount = bound(collateralAmount, 0, proposal.minCollateralAmount - 1); + + vm.expectRevert(abi.encodeWithSelector( + InsufficientCollateralAmount.selector, proposalValues.collateralAmount, proposal.minCollateralAmount + )); + proposalContract.acceptProposal( + proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, "" + ); + } + + function testFuzz_shouldCallLoanContractWithLoanTerms( + 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; + + 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: _creditAmount(proposalValues.collateralAmount, proposal.creditPerCollateralUnit) + }), + 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 PWNSimpleLoanFungibleProposal_AcceptProposalAndRevokeCallersNonce_Test is PWNSimpleLoanFungibleProposalTest, PWNSimpleLoanProposal_AcceptProposalAndRevokeCallersNonce_Test { + + function setUp() virtual public override(PWNSimpleLoanFungibleProposalTest, PWNSimpleLoanProposalTest) { + super.setUp(); + } + +} + + +/*----------------------------------------------------------*| +|* # ACCEPT REFINANCE PROPOSAL *| +|*----------------------------------------------------------*/ + +contract PWNSimpleLoanFungibleProposal_AcceptRefinanceProposal_Test is PWNSimpleLoanFungibleProposalTest, PWNSimpleLoanProposal_AcceptRefinanceProposal_Test { + + function setUp() virtual public override(PWNSimpleLoanFungibleProposalTest, 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_shouldFail_whenZeroMinCollateralAmount() external { + proposal.minCollateralAmount = 0; + + vm.expectRevert(abi.encodeWithSelector(MinCollateralAmountNotSet.selector)); + proposalContract.acceptRefinanceProposal( + loanId, proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, "" + ); + } + + function testFuzz_shouldFail_whenCollateralAmountLessThanMinCollateralAmount( + uint256 minCollateralAmount, uint256 collateralAmount + ) external { + proposal.minCollateralAmount = bound(minCollateralAmount, 1, type(uint256).max); + proposalValues.collateralAmount = bound(collateralAmount, 0, proposal.minCollateralAmount - 1); + + vm.expectRevert(abi.encodeWithSelector( + InsufficientCollateralAmount.selector, proposalValues.collateralAmount, proposal.minCollateralAmount + )); + proposalContract.acceptRefinanceProposal( + loanId, proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, "" + ); + } + + function testFuzz_shouldCallLoanContractWithLoanTerms( + 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; + + 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: _creditAmount(proposalValues.collateralAmount, proposal.creditPerCollateralUnit) + }), + 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 PWNSimpleLoanFungibleProposal_AcceptRefinanceProposalAndRevokeCallersNonce_Test is PWNSimpleLoanFungibleProposalTest, PWNSimpleLoanProposal_AcceptRefinanceProposalAndRevokeCallersNonce_Test { + + function setUp() virtual public override(PWNSimpleLoanFungibleProposalTest, PWNSimpleLoanProposalTest) { + super.setUp(); + } + +} diff --git a/test/unit/PWNSimpleLoanProposal.t.sol b/test/unit/PWNSimpleLoanProposal.t.sol new file mode 100644 index 0000000..1dfe231 --- /dev/null +++ b/test/unit/PWNSimpleLoanProposal.t.sol @@ -0,0 +1,681 @@ +// 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 { PWNSimpleLoan, Permit } from "@pwn/loan/terms/simple/loan/PWNSimpleLoan.sol"; +import { PWNSimpleLoanProposal } from "@pwn/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol"; +import "@pwn/PWNErrors.sol"; + + +abstract contract PWNSimpleLoanProposalTest is Test { + + bytes32 public constant PROPOSALS_MADE_SLOT = bytes32(uint256(0)); // `proposalsMade` mapping position + bytes32 public constant CREDIT_USED_SLOT = bytes32(uint256(1)); // `creditUsed` mapping position + + address public hub = makeAddr("hub"); + address public revokedNonce = makeAddr("revokedNonce"); + address public stateFingerprintComputerRegistry = makeAddr("stateFingerprintComputerRegistry"); + address public stateFingerprintComputer = makeAddr("stateFingerprintComputer"); + address public activeLoanContract = makeAddr("activeLoanContract"); + address public token = makeAddr("token"); + uint256 public proposerPK = 73661723; + address public proposer = vm.addr(proposerPK); + uint256 public acceptorPK = 32716637; + address public acceptor = vm.addr(acceptorPK); + uint256 public loanId = 421; + uint256 public refinancedLoanId = 123; + + 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; + uint256 signerPK; + bool compactSignature; + // cannot add anymore fields b/c of stack too deep error + } + + function setUp() virtual public { + vm.etch(hub, bytes("data")); + 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 + 1000); + params.proposer = proposer; + params.loanContract = activeLoanContract; + params.signerPK = proposerPK; + params.compactSignature = false; + + 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)"), + abi.encode(stateFingerprintComputer) + ); + 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(refinancedLoanId) + ); + } + + function _signProposalHash(uint256 pk, bytes32 proposalHash) internal pure returns (bytes memory) { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(pk, proposalHash); + return abi.encodePacked(r, s, v); + } + + function _signProposalHashCompact(uint256 pk, bytes32 proposalHash) internal pure returns (bytes memory) { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(pk, proposalHash); + return abi.encodePacked(r, bytes32(uint256(v) - 27) << 255 | s); + } + + + 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 _callAcceptRefinanceProposalWith(uint256 loanId, Params memory _params, Permit memory _permit) internal virtual returns (uint256); + function _callAcceptRefinanceProposalWith(uint256 loanId, Params memory _params, Permit memory _permit, uint256 nonceSpace, uint256 nonce) internal virtual returns (uint256); + function _getProposalHashWith(Params memory _params) internal virtual returns (bytes32); + + + function _callAcceptProposalWith() internal returns (uint256) { + return _callAcceptProposalWith(params, permit); + } + + function _callAcceptRefinanceProposalWith() internal returns (uint256) { + return _callAcceptRefinanceProposalWith(loanId, params, permit); + } + + function _getProposalHashWith() internal returns (bytes32) { + return _getProposalHashWith(params); + } + +} + + +/*----------------------------------------------------------*| +|* # 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: stateFingerprintComputerRegistry, + data: abi.encodeWithSignature("getStateFingerprintComputer(address)"), + count: 0 + }); + + _callAcceptProposalWith(); + } + + function test_shouldFail_whenComputerRegistryReturnsZeroAddress_whenShouldCheckStateFingerprint() external { + vm.mockCall( + stateFingerprintComputerRegistry, + abi.encodeWithSignature("getStateFingerprintComputer(address)", token), // test expects `token` being used as collateral asset + abi.encode(address(0)) + ); + + vm.expectRevert(abi.encodeWithSelector(MissingStateFingerprintComputer.selector)); + _callAcceptProposalWith(); + } + + function testFuzz_shouldFail_whenComputerReturnsDifferentStateFingerprint_whenShouldCheckStateFingerprint( + bytes32 stateFingerprint + ) external { + vm.assume(stateFingerprint != params.collateralStateFingerprint); + + vm.mockCall( + stateFingerprintComputer, + abi.encodeWithSignature("getStateFingerprint(uint256)"), + abi.encode(stateFingerprint) + ); + + vm.expectRevert(abi.encodeWithSelector( + InvalidCollateralStateFingerprint.selector, stateFingerprint, params.collateralStateFingerprint + )); + _callAcceptProposalWith(); + } + + function test_shouldFail_whenInvalidSignature_whenEOA() external { + params.signerPK = 1; + + vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, proposer, _getProposalHashWith(params))); + _callAcceptProposalWith(); + } + + function test_shouldFail_whenInvalidSignature_whenContractAccount() external { + vm.etch(proposer, bytes("data")); + params.signerPK = 0; + + vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, proposer, _getProposalHashWith(params))); + _callAcceptProposalWith(); + } + + function test_shouldPass_whenProposalMadeOnchain() external { + vm.store( + address(proposalContractAddr), + keccak256(abi.encode(_getProposalHashWith(params), PROPOSALS_MADE_SLOT)), + bytes32(uint256(1)) + ); + params.signerPK = 0; + + _callAcceptProposalWith(); + } + + function test_shouldPass_withValidSignature_whenEOA_whenStandardSignature() external { + params.compactSignature = false; + _callAcceptProposalWith(); + } + + function test_shouldPass_withValidSignature_whenEOA_whenCompactEIP2098Signature() external { + params.compactSignature = true; + _callAcceptProposalWith(); + } + + function test_shouldPass_whenValidSignature_whenContractAccount() external { + vm.etch(proposer, bytes("data")); + params.signerPK = 0; + + vm.mockCall( + proposer, + abi.encodeWithSignature("isValidSignature(bytes32,bytes)"), + abi.encode(bytes4(0x1626ba7e)) + ); + + _callAcceptProposalWith(); + } + + function testFuzz_shouldFail_whenProposalExpired(uint256 timestamp) external { + timestamp = bound(timestamp, params.expiration, type(uint256).max); + vm.warp(timestamp); + + vm.expectRevert(abi.encodeWithSelector(Expired.selector, timestamp, params.expiration)); + _callAcceptProposalWith(); + } + + function testFuzz_shouldFail_whenOfferNonceNotUsable(uint256 nonceSpace, uint256 nonce) external { + params.nonceSpace = nonceSpace; + params.nonce = nonce; + + vm.mockCall( + revokedNonce, + abi.encodeWithSignature("isNonceUsable(address,uint256,uint256)"), + abi.encode(false) + ); + vm.expectCall( + revokedNonce, + abi.encodeWithSignature("isNonceUsable(address,uint256,uint256)", proposer, nonceSpace, nonce) + ); + + vm.expectRevert(abi.encodeWithSelector( + NonceNotUsable.selector, proposer, nonceSpace, nonce + )); + _callAcceptProposalWith(); + } + + function testFuzz_shouldFail_whenCallerIsNotAllowedAcceptor(address caller) external { + address allowedAcceptor = makeAddr("allowedAcceptor"); + vm.assume(caller != allowedAcceptor); + params.allowedAcceptor = allowedAcceptor; + + 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)); + _callAcceptProposalWith(); + } + + function test_shouldRevokeOffer_whenAvailableCreditLimitEqualToZero(uint256 nonceSpace, uint256 nonce) external { + params.availableCreditLimit = 0; + params.nonceSpace = nonceSpace; + params.nonce = nonce; + + vm.expectCall( + revokedNonce, + abi.encodeWithSignature("revokeNonce(address,uint256,uint256)", proposer, nonceSpace, nonce) + ); + + _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); + + params.availableCreditLimit = limit; + + vm.store( + address(proposalContractAddr), + keccak256(abi.encode(_getProposalHashWith(params), CREDIT_USED_SLOT)), + bytes32(used) + ); + + vm.expectRevert(abi.encodeWithSelector(AvailableCreditLimitExceeded.selector, used + params.creditAmount, limit)); + _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); + + params.availableCreditLimit = limit; + + bytes32 proposalHash = _getProposalHashWith(params); + + vm.store( + address(proposalContractAddr), + keccak256(abi.encode(proposalHash, CREDIT_USED_SLOT)), + bytes32(used) + ); + + _callAcceptProposalWith(); + + assertEq(proposalContractAddr.creditUsed(proposalHash), used + params.creditAmount); + } + + function testFuzz_shouldFail_whenPermitOwnerNotCaller(address owner, address caller) external { + vm.assume(owner != caller && owner != address(0) && caller != address(0)); + + permit.owner = owner; + permit.asset = token; // test expects `token` being used as credit asset + + vm.expectRevert(abi.encodeWithSelector(InvalidPermitOwner.selector, owner, caller)); + vm.prank(caller); + _callAcceptProposalWith(); + } + + function testFuzz_shouldFail_whenPermitAssetNotCreditAsset(address asset, address caller) external { + vm.assume(asset != token && asset != address(0) && caller != address(0)); + + permit.owner = caller; + permit.asset = asset; // test expects `token` being used as credit asset + + vm.expectRevert(abi.encodeWithSelector(InvalidPermitAsset.selector, asset, token)); + vm.prank(caller); + _callAcceptProposalWith(); + } + + function test_shouldReturnNewLoanId() external { + assertEq(_callAcceptProposalWith(), loanId); + } + +} + + +/*----------------------------------------------------------*| +|* # 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 REFINANCE PROPOSAL *| +|*----------------------------------------------------------*/ + +abstract contract PWNSimpleLoanProposal_AcceptRefinanceProposal_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)); + _callAcceptRefinanceProposalWith(); + } + + function test_shouldNotCallComputerRegistry_whenShouldNotCheckStateFingerprint() external { + params.checkCollateralStateFingerprint = false; + + vm.expectCall({ + callee: stateFingerprintComputerRegistry, + data: abi.encodeWithSignature("getStateFingerprintComputer(address)"), + count: 0 + }); + + _callAcceptRefinanceProposalWith(); + } + + function test_shouldFail_whenComputerRegistryReturnsZeroAddress_whenShouldCheckStateFingerprint() external { + vm.mockCall( + stateFingerprintComputerRegistry, + abi.encodeWithSignature("getStateFingerprintComputer(address)", token), // test expects `token` being used as collateral asset + abi.encode(address(0)) + ); + + vm.expectRevert(abi.encodeWithSelector(MissingStateFingerprintComputer.selector)); + _callAcceptRefinanceProposalWith(); + } + + function testFuzz_shouldFail_whenComputerReturnsDifferentStateFingerprint_whenShouldCheckStateFingerprint( + bytes32 stateFingerprint + ) external { + vm.assume(stateFingerprint != params.collateralStateFingerprint); + + vm.mockCall( + stateFingerprintComputer, + abi.encodeWithSignature("getStateFingerprint(uint256)"), + abi.encode(stateFingerprint) + ); + + vm.expectRevert(abi.encodeWithSelector( + InvalidCollateralStateFingerprint.selector, stateFingerprint, params.collateralStateFingerprint + )); + _callAcceptRefinanceProposalWith(); + } + + function test_shouldFail_whenInvalidSignature_whenEOA() external { + params.signerPK = 1; + + vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, proposer, _getProposalHashWith(params))); + _callAcceptRefinanceProposalWith(); + } + + function test_shouldFail_whenInvalidSignature_whenContractAccount() external { + vm.etch(proposer, bytes("data")); + params.signerPK = 0; + + vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, proposer, _getProposalHashWith(params))); + _callAcceptRefinanceProposalWith(); + } + + function test_shouldPass_whenProposalMadeOnchain() external { + vm.store( + address(proposalContractAddr), + keccak256(abi.encode(_getProposalHashWith(params), PROPOSALS_MADE_SLOT)), + bytes32(uint256(1)) + ); + params.signerPK = 0; + + _callAcceptRefinanceProposalWith(); + } + + function test_shouldPass_withValidSignature_whenEOA_whenStandardSignature() external { + params.compactSignature = false; + _callAcceptRefinanceProposalWith(); + } + + function test_shouldPass_withValidSignature_whenEOA_whenCompactEIP2098Signature() external { + params.compactSignature = true; + _callAcceptRefinanceProposalWith(); + } + + function test_shouldPass_whenValidSignature_whenContractAccount() external { + vm.etch(proposer, bytes("data")); + params.signerPK = 0; + + vm.mockCall( + proposer, + abi.encodeWithSignature("isValidSignature(bytes32,bytes)"), + abi.encode(bytes4(0x1626ba7e)) + ); + + _callAcceptRefinanceProposalWith(); + } + + function testFuzz_shouldFail_whenProposalExpired(uint256 timestamp) external { + timestamp = bound(timestamp, params.expiration, type(uint256).max); + vm.warp(timestamp); + + vm.expectRevert(abi.encodeWithSelector(Expired.selector, timestamp, params.expiration)); + _callAcceptRefinanceProposalWith(); + } + + function testFuzz_shouldFail_whenOfferNonceNotUsable(uint256 nonceSpace, uint256 nonce) external { + params.nonceSpace = nonceSpace; + params.nonce = nonce; + + vm.mockCall( + revokedNonce, + abi.encodeWithSignature("isNonceUsable(address,uint256,uint256)"), + abi.encode(false) + ); + vm.expectCall( + revokedNonce, + abi.encodeWithSignature("isNonceUsable(address,uint256,uint256)", proposer, nonceSpace, nonce) + ); + + vm.expectRevert(abi.encodeWithSelector( + NonceNotUsable.selector, proposer, nonceSpace, nonce + )); + _callAcceptRefinanceProposalWith(); + } + + function testFuzz_shouldFail_whenCallerIsNotAllowedAcceptor(address caller) external { + address allowedAcceptor = makeAddr("allowedAcceptor"); + vm.assume(caller != allowedAcceptor); + params.allowedAcceptor = allowedAcceptor; + + vm.expectRevert(abi.encodeWithSelector(CallerNotAllowedAcceptor.selector, caller, allowedAcceptor)); + vm.prank(caller); + _callAcceptRefinanceProposalWith(); + } + + 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)); + _callAcceptRefinanceProposalWith(); + } + + 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)); + _callAcceptRefinanceProposalWith(); + } + + function test_shouldRevokeOffer_whenAvailableCreditLimitEqualToZero(uint256 nonceSpace, uint256 nonce) external { + params.availableCreditLimit = 0; + params.nonceSpace = nonceSpace; + params.nonce = nonce; + + vm.expectCall( + revokedNonce, + abi.encodeWithSignature("revokeNonce(address,uint256,uint256)", proposer, nonceSpace, nonce) + ); + + _callAcceptRefinanceProposalWith(); + } + + 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); + + params.availableCreditLimit = limit; + + vm.store( + address(proposalContractAddr), + keccak256(abi.encode(_getProposalHashWith(params), CREDIT_USED_SLOT)), + bytes32(used) + ); + + vm.expectRevert(abi.encodeWithSelector(AvailableCreditLimitExceeded.selector, used + params.creditAmount, limit)); + _callAcceptRefinanceProposalWith(); + } + + 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); + + params.availableCreditLimit = limit; + + bytes32 proposalHash = _getProposalHashWith(params); + + vm.store( + address(proposalContractAddr), + keccak256(abi.encode(proposalHash, CREDIT_USED_SLOT)), + bytes32(used) + ); + + _callAcceptRefinanceProposalWith(); + + assertEq(proposalContractAddr.creditUsed(proposalHash), used + params.creditAmount); + } + + function testFuzz_shouldFail_whenPermitOwnerNotCaller(address owner, address caller) external { + vm.assume(owner != caller && owner != address(0) && caller != address(0)); + + permit.owner = owner; + permit.asset = token; // test expects `token` being used as credit asset + + vm.expectRevert(abi.encodeWithSelector(InvalidPermitOwner.selector, owner, caller)); + vm.prank(caller); + _callAcceptRefinanceProposalWith(); + } + + function testFuzz_shouldFail_whenPermitAssetNotCreditAsset(address asset, address caller) external { + vm.assume(asset != token && asset != address(0) && caller != address(0)); + + permit.owner = caller; + permit.asset = asset; // test expects `token` being used as credit asset + + vm.expectRevert(abi.encodeWithSelector(InvalidPermitAsset.selector, asset, token)); + vm.prank(caller); + _callAcceptRefinanceProposalWith(); + } + + function test_shouldReturnRefinancedLoanId() external { + assertEq(_callAcceptRefinanceProposalWith(), refinancedLoanId); + } + +} + + +/*----------------------------------------------------------*| +|* # ACCEPT REFINANCE PROPOSAL AND REVOKE CALLERS NONCE *| +|*----------------------------------------------------------*/ + +abstract contract PWNSimpleLoanProposal_AcceptRefinanceProposalAndRevokeCallersNonce_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); + _callAcceptRefinanceProposalWith(loanId, 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); + _callAcceptRefinanceProposalWith(loanId, params, permit, nonceSpace, nonce); + } + + // function is calling `acceptRefinanceProposal`, no need to test it again + function test_shouldCallLoanContract() external { + assertEq(_callAcceptRefinanceProposalWith(loanId, params, permit, 1, 2), refinancedLoanId); + } + +} From f74263260303c78cda7de56aada2cb5f0917920b Mon Sep 17 00:00:00 2001 From: ashhanai Date: Thu, 21 Mar 2024 16:38:33 -0300 Subject: [PATCH 050/129] feat: remove unnecessary fungible offer files --- .../offer/PWNSimpleLoanFungibleOffer.sol | 343 ------- test/unit/PWNSimpleLoanFungibleOffer.t.sol | 964 ------------------ 2 files changed, 1307 deletions(-) delete mode 100644 src/loan/terms/simple/proposal/offer/PWNSimpleLoanFungibleOffer.sol delete mode 100644 test/unit/PWNSimpleLoanFungibleOffer.t.sol diff --git a/src/loan/terms/simple/proposal/offer/PWNSimpleLoanFungibleOffer.sol b/src/loan/terms/simple/proposal/offer/PWNSimpleLoanFungibleOffer.sol deleted file mode 100644 index 31f25a0..0000000 --- a/src/loan/terms/simple/proposal/offer/PWNSimpleLoanFungibleOffer.sol +++ /dev/null @@ -1,343 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity 0.8.16; - -import { MultiToken } from "MultiToken/MultiToken.sol"; - -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"; - - -/** - * @title PWN Simple Loan Fungible Offer - * @notice Contract for creating and accepting fungible loan offers. - * Offers are fungible, which means that they are not tied to a specific collateral or credit amount. - * The amount of collateral and credit is specified during the offer acceptance. - */ -contract PWNSimpleLoanFungibleOffer is PWNSimpleLoanProposal { - - string public constant VERSION = "1.2"; - - /** - * @notice Credit per collateral unit denominator. It is used to calculate credit amount from collateral amount. - */ - uint256 public constant CREDIT_PER_COLLATERAL_UNIT_DENOMINATOR = 1e38; - - /** - * @dev EIP-712 simple offer struct type hash. - */ - bytes32 public constant OFFER_TYPEHASH = keccak256( - "Offer(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 minCollateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address creditAddress,uint256 creditPerCollateralUnit,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 fungible offer. - * @param collateralCategory Category of an asset used as a collateral (0 == ERC20, 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. - * @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 creditPerCollateralUnit Amount of tokens which are offered per collateral unit with 38 decimals. - * @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; - uint256 collateralId; - uint256 minCollateralAmount; - bool checkCollateralStateFingerprint; - bytes32 collateralStateFingerprint; - address creditAddress; - uint256 creditPerCollateralUnit; - 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 collateralAmount Amount of collateral to be used in the loan. - */ - struct OfferValues { - uint256 collateralAmount; - } - - /** - * @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, "PWNSimpleLoanFungibleOffer", 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 min collateral amount - if (offer.minCollateralAmount == 0) { - revert MinCollateralAmountNotSet(); - } - if (offerValues.collateralAmount < offer.minCollateralAmount) { - revert InsufficientCollateralAmount({ - current: offerValues.collateralAmount, - limit: offer.minCollateralAmount - }); - } - - // Check collateral state fingerprint if needed - if (offer.checkCollateralStateFingerprint) { - _checkCollateralState({ - addr: offer.collateralAddress, - id: offer.collateralId, - stateFingerprint: offer.collateralStateFingerprint - }); - } - - // Calculate credit amount - uint256 creditAmount = _creditAmount(offerValues.collateralAmount, offer.creditPerCollateralUnit); - - // Try to accept offer - offerHash = _tryAcceptOffer(offer, creditAmount, signature); - - // Create loan terms object - loanTerms = _createLoanTerms(offer, offerValues.collateralAmount, creditAmount); - } - - function _creditAmount(uint256 collateralAmount, uint256 creditPerCollateralUnit) private pure returns (uint256) { - return Math.mulDiv(collateralAmount, creditPerCollateralUnit, CREDIT_PER_COLLATERAL_UNIT_DENOMINATOR); - } - - function _tryAcceptOffer( - Offer calldata offer, - uint256 creditAmount, - bytes calldata signature - ) private returns (bytes32 offerHash) { - offerHash = getOfferHash(offer); - _tryAcceptProposal({ - proposalHash: offerHash, - creditAmount: 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, - uint256 collateralAmount, - uint256 creditAmount - ) 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: offer.collateralId, - amount: collateralAmount - }), - credit: MultiToken.ERC20({ - assetAddress: offer.creditAddress, - amount: creditAmount - }), - fixedInterestAmount: offer.fixedInterestAmount, - accruingInterestAPR: offer.accruingInterestAPR - }); - } - -} diff --git a/test/unit/PWNSimpleLoanFungibleOffer.t.sol b/test/unit/PWNSimpleLoanFungibleOffer.t.sol deleted file mode 100644 index 303ef8c..0000000 --- a/test/unit/PWNSimpleLoanFungibleOffer.t.sol +++ /dev/null @@ -1,964 +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 { Math } from "openzeppelin-contracts/contracts/utils/math/Math.sol"; - -import { PWNHubTags } from "@pwn/hub/PWNHubTags.sol"; -import { PWNSimpleLoanFungibleOffer, PWNSimpleLoan, Permit } - from "@pwn/loan/terms/simple/proposal/offer/PWNSimpleLoanFungibleOffer.sol"; -import "@pwn/PWNErrors.sol"; - - -abstract contract PWNSimpleLoanFungibleOfferTest 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 - - PWNSimpleLoanFungibleOffer offerContract; - address hub = makeAddr("hub"); - address revokedNonce = makeAddr("revokedNonce"); - address stateFingerprintComputerRegistry = makeAddr("stateFingerprintComputerRegistry"); - address activeLoanContract = makeAddr("activeLoanContract"); - PWNSimpleLoanFungibleOffer.Offer offer; - PWNSimpleLoanFungibleOffer.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, PWNSimpleLoanFungibleOffer.Offer offer); - - function setUp() virtual public { - vm.etch(hub, bytes("data")); - vm.etch(revokedNonce, bytes("data")); - vm.etch(token, bytes("data")); - - offerContract = new PWNSimpleLoanFungibleOffer(hub, revokedNonce, stateFingerprintComputerRegistry); - - offer = PWNSimpleLoanFungibleOffer.Offer({ - collateralCategory: MultiToken.Category.ERC1155, - collateralAddress: token, - collateralId: 0, - minCollateralAmount: 100, - checkCollateralStateFingerprint: true, - collateralStateFingerprint: keccak256("some state fingerprint"), - creditAddress: token, - creditPerCollateralUnit: 10 * offerContract.CREDIT_PER_COLLATERAL_UNIT_DENOMINATOR(), - 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 = PWNSimpleLoanFungibleOffer.OfferValues({ - collateralAmount: 1000 - }); - - 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(PWNSimpleLoanFungibleOffer.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("PWNSimpleLoanFungibleOffer"), - keccak256("1.2"), - block.chainid, - address(offerContract) - )), - keccak256(abi.encodePacked( - keccak256("Offer(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 minCollateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address creditAddress,uint256 creditPerCollateralUnit,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, PWNSimpleLoanFungibleOffer.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, PWNSimpleLoanFungibleOffer.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); - } - - function _creditAmount(uint256 collateralAmount, uint256 creditPerCollateralUnit) internal view returns (uint256) { - return Math.mulDiv(collateralAmount, creditPerCollateralUnit, offerContract.CREDIT_PER_COLLATERAL_UNIT_DENOMINATOR()); - } - -} - - -/*----------------------------------------------------------*| -|* # CREDIT USED *| -|*----------------------------------------------------------*/ - -contract PWNSimpleLoanFungibleOffer_CreditUsed_Test is PWNSimpleLoanFungibleOfferTest { - - 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 PWNSimpleLoanFungibleOffer_GetOfferHash_Test is PWNSimpleLoanFungibleOfferTest { - - function test_shouldReturnOfferHash() external { - assertEq(_offerHash(offer), offerContract.getOfferHash(offer)); - } - -} - - -/*----------------------------------------------------------*| -|* # MAKE OFFER *| -|*----------------------------------------------------------*/ - -contract PWNSimpleLoanFungibleOffer_MakeOffer_Test is PWNSimpleLoanFungibleOfferTest { - - 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 PWNSimpleLoanFungibleOffer_RevokeNonce_Test is PWNSimpleLoanFungibleOfferTest { - - 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 PWNSimpleLoanFungibleOffer_AcceptOffer_Test is PWNSimpleLoanFungibleOfferTest { - - 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_shouldFail_whenZeroMinCollateralAmount() external { - offer.minCollateralAmount = 0; - - vm.expectRevert(abi.encodeWithSelector(MinCollateralAmountNotSet.selector)); - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), permit, ""); - } - - function testFuzz_shouldFail_whenCollateralAmountLessThanMinCollateralAmount(uint256 collateralAmount) external { - collateralAmount = bound(collateralAmount, 0, offer.minCollateralAmount - 1); - offerValues.collateralAmount = collateralAmount; - - vm.expectRevert(abi.encodeWithSelector( - InsufficientCollateralAmount.selector, collateralAmount, offer.minCollateralAmount - )); - 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)", offer.collateralId), - abi.encode(stateFingerprint) - ); - - vm.expectCall( - stateFingerprintComputer, - abi.encodeWithSignature("getStateFingerprint(uint256)", offer.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 { - uint256 creditAmount = _creditAmount(offerValues.collateralAmount, offer.creditPerCollateralUnit); - used = bound(used, 1, type(uint256).max - creditAmount); - limit = bound(limit, used, used + 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 + creditAmount, limit)); - offerContract.acceptOffer(offer, offerValues, _signOffer(lenderPK, offer), permit, ""); - } - - function testFuzz_shouldIncreaseUsedCredit_whenUsedCreditNotExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { - uint256 creditAmount = _creditAmount(offerValues.collateralAmount, offer.creditPerCollateralUnit); - used = bound(used, 1, type(uint256).max - creditAmount); - limit = bound(limit, used + 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 + 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 testFuzz_shouldCallLoanContractWithLoanTerms( - uint256 collateralAmount, uint256 creditPerCollateralUnit - ) external { - offerValues.collateralAmount = bound(collateralAmount, offer.minCollateralAmount, 1e40); - offer.creditPerCollateralUnit = bound(creditPerCollateralUnit, 0, 1e40); - uint256 creditAmount = _creditAmount(offerValues.collateralAmount, offer.creditPerCollateralUnit); - - 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: offer.collateralId, - amount: offerValues.collateralAmount - }), - credit: MultiToken.Asset({ - category: MultiToken.Category.ERC20, - assetAddress: offer.creditAddress, - id: 0, - amount: 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 PWNSimpleLoanFungibleOffer_AcceptOfferAndRevokeCallersNonce_Test is PWNSimpleLoanFungibleOfferTest { - - 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 PWNSimpleLoanFungibleOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanFungibleOfferTest { - - 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_shouldFail_whenZeroMinCollateralAmount() external { - offer.minCollateralAmount = 0; - - vm.expectRevert(abi.encodeWithSelector(MinCollateralAmountNotSet.selector)); - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), permit, ""); - } - - function testFuzz_shouldFail_whenCollateralAmountLessThanMinCollateralAmount(uint256 collateralAmount) external { - collateralAmount = bound(collateralAmount, 0, offer.minCollateralAmount - 1); - offerValues.collateralAmount = collateralAmount; - - vm.expectRevert(abi.encodeWithSelector( - InsufficientCollateralAmount.selector, collateralAmount, offer.minCollateralAmount - )); - 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)", offer.collateralId), - abi.encode(stateFingerprint) - ); - - vm.expectCall( - stateFingerprintComputer, - abi.encodeWithSignature("getStateFingerprint(uint256)", offer.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 { - uint256 creditAmount = _creditAmount(offerValues.collateralAmount, offer.creditPerCollateralUnit); - used = bound(used, 1, type(uint256).max - creditAmount); - limit = bound(limit, used, used + 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 + creditAmount, limit)); - offerContract.acceptRefinanceOffer(loanId, offer, offerValues, _signOffer(lenderPK, offer), permit, ""); - } - - function testFuzz_shouldIncreaseUsedCredit_whenUsedCreditNotExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { - uint256 creditAmount = _creditAmount(offerValues.collateralAmount, offer.creditPerCollateralUnit); - used = bound(used, 1, type(uint256).max - creditAmount); - limit = bound(limit, used + 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 + 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 testFuzz_shouldCallLoanContractWithLoanTerms( - uint256 collateralAmount, uint256 creditPerCollateralUnit - ) external { - offerValues.collateralAmount = bound(collateralAmount, offer.minCollateralAmount, 1e40); - offer.creditPerCollateralUnit = bound(creditPerCollateralUnit, 0, 1e40); - uint256 creditAmount = _creditAmount(offerValues.collateralAmount, offer.creditPerCollateralUnit); - - 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: offer.collateralId, - amount: offerValues.collateralAmount - }), - credit: MultiToken.Asset({ - category: MultiToken.Category.ERC20, - assetAddress: offer.creditAddress, - id: 0, - amount: 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 PWNSimpleLoanFungibleOffer_AcceptRefinanceOfferAndRevokeCallersNonce_Test is PWNSimpleLoanFungibleOfferTest { - - 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); - } - -} From f8d5403f4ae7d53b078a7b6eb5f51a15341c8efe Mon Sep 17 00:00:00 2001 From: ashhanai Date: Thu, 21 Mar 2024 16:38:44 -0300 Subject: [PATCH 051/129] test: add missing fungible proposal tests --- test/unit/PWNSimpleLoanFungibleProposal.t.sol | 86 ++++++++++++++++++- 1 file changed, 85 insertions(+), 1 deletion(-) diff --git a/test/unit/PWNSimpleLoanFungibleProposal.t.sol b/test/unit/PWNSimpleLoanFungibleProposal.t.sol index 6033d3c..b3ebc72 100644 --- a/test/unit/PWNSimpleLoanFungibleProposal.t.sol +++ b/test/unit/PWNSimpleLoanFungibleProposal.t.sol @@ -27,7 +27,7 @@ abstract contract PWNSimpleLoanFungibleProposalTest is PWNSimpleLoanProposalTest PWNSimpleLoanFungibleProposal.Proposal proposal; PWNSimpleLoanFungibleProposal.ProposalValues proposalValues; - event OfferMade(bytes32 indexed proposalHash, address indexed proposer, PWNSimpleLoanFungibleProposal.Proposal proposal); + event ProposalMade(bytes32 indexed proposalHash, address indexed proposer, PWNSimpleLoanFungibleProposal.Proposal proposal); function setUp() virtual public override { super.setUp(); @@ -140,6 +140,90 @@ abstract contract PWNSimpleLoanFungibleProposalTest is PWNSimpleLoanProposalTest } +/*----------------------------------------------------------*| +|* # CREDIT USED *| +|*----------------------------------------------------------*/ + +contract PWNSimpleLoanFungibleProposal_CreditUsed_Test is PWNSimpleLoanFungibleProposalTest { + + 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 PWNSimpleLoanFungibleProposal_RevokeNonce_Test is PWNSimpleLoanFungibleProposalTest { + + 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 PWNSimpleLoanFungibleProposal_GetProposalHash_Test is PWNSimpleLoanFungibleProposalTest { + + function test_shouldReturnOfferHash() external { + assertEq(_proposalHash(proposal), proposalContract.getProposalHash(proposal)); + } + +} + + +/*----------------------------------------------------------*| +|* # MAKE PROPOSAL *| +|*----------------------------------------------------------*/ + +contract PWNSimpleLoanFungibleProposal_MakeProposal_Test is PWNSimpleLoanFungibleProposalTest { + + 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_OfferMade() external { + vm.expectEmit(); + emit ProposalMade(_proposalHash(proposal), proposal.proposer, proposal); + + vm.prank(proposal.proposer); + proposalContract.makeProposal(proposal); + } + + function test_shouldMakeOffer() external { + vm.prank(proposal.proposer); + proposalContract.makeProposal(proposal); + + assertTrue(proposalContract.proposalsMade(_proposalHash(proposal))); + } + + function test_shouldReturnOfferHash() external { + vm.prank(proposal.proposer); + assertEq(proposalContract.makeProposal(proposal), _proposalHash(proposal)); + } + +} + + /*----------------------------------------------------------*| |* # ACCEPT PROPOSAL *| |*----------------------------------------------------------*/ From f5957b126576c7ae1d5e100765ff8b7ebc2281d1 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Thu, 21 Mar 2024 17:12:04 -0300 Subject: [PATCH 052/129] feat(simple-proposal): merge simple loan simple offer and request into a simple proposal --- script/PWN.s.sol | 47 +- src/Deployments.sol | 9 +- .../PWNSimpleLoanFungibleProposal.sol | 2 +- .../proposal/PWNSimpleLoanSimpleProposal.sol | 295 ++++++ .../offer/PWNSimpleLoanSimpleOffer.sol | 291 ------ .../request/PWNSimpleLoanSimpleRequest.sol | 289 ------ test/helper/DeploymentTest.t.sol | 20 +- test/unit/PWNSimpleLoanSimpleOffer.t.sol | 897 ----------------- test/unit/PWNSimpleLoanSimpleProposal.t.sol | 413 ++++++++ test/unit/PWNSimpleLoanSimpleRequest.t.sol | 914 ------------------ 10 files changed, 725 insertions(+), 2452 deletions(-) create mode 100644 src/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol delete mode 100644 src/loan/terms/simple/proposal/offer/PWNSimpleLoanSimpleOffer.sol delete mode 100644 src/loan/terms/simple/proposal/request/PWNSimpleLoanSimpleRequest.sol delete mode 100644 test/unit/PWNSimpleLoanSimpleOffer.t.sol create mode 100644 test/unit/PWNSimpleLoanSimpleProposal.t.sol delete mode 100644 test/unit/PWNSimpleLoanSimpleRequest.t.sol diff --git a/script/PWN.s.sol b/script/PWN.s.sol index 08480b4..953e2d6 100644 --- a/script/PWN.s.sol +++ b/script/PWN.s.sol @@ -14,8 +14,7 @@ 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 { PWNSimpleLoanSimpleOffer } from "@pwn/loan/terms/simple/proposal/offer/PWNSimpleLoanSimpleOffer.sol"; -import { PWNSimpleLoanSimpleRequest } from "@pwn/loan/terms/simple/proposal/request/PWNSimpleLoanSimpleRequest.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"; import { Deployments } from "@pwn/Deployments.sol"; @@ -39,13 +38,13 @@ library PWNContractDeployerSalt { // Loan types bytes32 internal constant SIMPLE_LOAN = keccak256("PWNSimpleLoan"); + // Proposal types + bytes32 internal constant SIMPLE_LOAN_SIMPLE_PROPOSAL = keccak256("PWNSimpleLoanSimpleProposal"); + bytes32 internal constant SIMPLE_LOAN_FUNGIBLE_PROPOSAL = keccak256("PWNSimpleLoanFungibleProposal"); + // Offer types - bytes32 internal constant SIMPLE_LOAN_SIMPLE_OFFER = keccak256("PWNSimpleLoanSimpleOffer"); bytes32 internal constant SIMPLE_LOAN_LIST_OFFER = keccak256("PWNSimpleLoanListOffer"); - // Request types - bytes32 internal constant SIMPLE_LOAN_SIMPLE_REQUEST = keccak256("PWNSimpleLoanSimpleRequest"); - } @@ -173,13 +172,6 @@ forge script script/PWN.s.sol:Deploy \ })); // - Offers - simpleLoanSimpleOffer = PWNSimpleLoanSimpleOffer(_deploy({ - salt: PWNContractDeployerSalt.SIMPLE_LOAN_SIMPLE_OFFER, - bytecode: abi.encodePacked( - type(PWNSimpleLoanSimpleOffer).creationCode, - abi.encode(address(hub), address(revokedNonce)) - ) - })); simpleLoanListOffer = PWNSimpleLoanListOffer(_deploy({ salt: PWNContractDeployerSalt.SIMPLE_LOAN_LIST_OFFER, bytecode: abi.encodePacked( @@ -188,24 +180,13 @@ forge script script/PWN.s.sol:Deploy \ ) })); - // - Requests - simpleLoanSimpleRequest = PWNSimpleLoanSimpleRequest(_deploy({ - salt: PWNContractDeployerSalt.SIMPLE_LOAN_SIMPLE_REQUEST, - bytecode: abi.encodePacked( - type(PWNSimpleLoanSimpleRequest).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("PWNSimpleLoanSimpleOffer:", address(simpleLoanSimpleOffer)); console2.log("PWNSimpleLoanListOffer:", address(simpleLoanListOffer)); - console2.log("PWNSimpleLoanSimpleRequest:", address(simpleLoanSimpleRequest)); vm.stopBroadcast(); } @@ -283,25 +264,17 @@ forge script script/PWN.s.sol:Setup \ } function _setTags() internal { - address[] memory addrs = new address[](8); + address[] memory addrs = new address[](4); addrs[0] = address(simpleLoan); addrs[1] = address(simpleLoan); - addrs[2] = address(simpleLoanSimpleOffer); - addrs[3] = address(simpleLoanSimpleOffer); - addrs[4] = address(simpleLoanListOffer); - addrs[5] = address(simpleLoanListOffer); - addrs[6] = address(simpleLoanSimpleRequest); - addrs[7] = address(simpleLoanSimpleRequest); - - bytes32[] memory tags = new bytes32[](8); + 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[4] = PWNHubTags.LOAN_PROPOSAL; - tags[5] = PWNHubTags.NONCE_MANAGER; - tags[6] = PWNHubTags.LOAN_PROPOSAL; - tags[7] = PWNHubTags.NONCE_MANAGER; bool success = GnosisSafeLike(protocolSafe).execTransaction({ to: address(hub), diff --git a/src/Deployments.sol b/src/Deployments.sol index 44ff143..10c86b7 100644 --- a/src/Deployments.sol +++ b/src/Deployments.sol @@ -14,8 +14,7 @@ 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 { PWNSimpleLoanSimpleOffer } from "@pwn/loan/terms/simple/proposal/offer/PWNSimpleLoanSimpleOffer.sol"; -import { PWNSimpleLoanSimpleRequest } from "@pwn/loan/terms/simple/proposal/request/PWNSimpleLoanSimpleRequest.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"; import { StateFingerprintComputerRegistry } from "@pwn/state-fingerprint/StateFingerprintComputerRegistry.sol"; @@ -46,8 +45,6 @@ abstract contract Deployments is CommonBase { PWNRevokedNonce revokedNonce; PWNSimpleLoan simpleLoan; PWNSimpleLoanListOffer simpleLoanListOffer; - PWNSimpleLoanSimpleOffer simpleLoanSimpleOffer; - PWNSimpleLoanSimpleRequest simpleLoanSimpleRequest; StateFingerprintComputerRegistry stateFingerprintComputerRegistry; } @@ -71,9 +68,7 @@ abstract contract Deployments is CommonBase { PWNLOAN loanToken; PWNSimpleLoan simpleLoan; PWNRevokedNonce revokedNonce; - PWNSimpleLoanSimpleOffer simpleLoanSimpleOffer; PWNSimpleLoanListOffer simpleLoanListOffer; - PWNSimpleLoanSimpleRequest simpleLoanSimpleRequest; function _loadDeployedAddresses() internal { @@ -101,9 +96,7 @@ abstract contract Deployments is CommonBase { loanToken = deployment.loanToken; simpleLoan = deployment.simpleLoan; revokedNonce = deployment.revokedNonce; - simpleLoanSimpleOffer = deployment.simpleLoanSimpleOffer; simpleLoanListOffer = deployment.simpleLoanListOffer; - simpleLoanSimpleRequest = deployment.simpleLoanSimpleRequest; 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 adf269f..4213092 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol @@ -48,7 +48,7 @@ contract PWNSimpleLoanFungibleProposal is PWNSimpleLoanProposal { * @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 with a collateral can accept the proposal. + * @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. diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol new file mode 100644 index 0000000..fe8c1e4 --- /dev/null +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol @@ -0,0 +1,295 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.16; + +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"; + + +/** + * @title PWN Simple Loan Simple Proposal + * @notice Contract for creating and accepting simple loan proposals. + */ +contract PWNSimpleLoanSimpleProposal 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,uint256 collateralId,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 simple 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 collateralId Token id of an asset used as a collateral, in case of ERC20 should be 0. + * @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 lended 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; + uint256 collateralId; + 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; + } + + /** + * @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, "PWNSimpleLoanSimpleProposal", 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 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, + 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, 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 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, + 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, 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 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, + 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, 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 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, + 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, signature, permit, extra); + } + + + /*----------------------------------------------------------*| + |* # INTERNALS *| + |*----------------------------------------------------------*/ + + function _acceptProposal( + Proposal calldata proposal, + 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 + }); + } + + // Try to accept proposal + proposalHash = _tryAcceptProposal(proposal, signature); + + // 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, + duration: proposal.duration, + collateral: MultiToken.Asset({ + category: proposal.collateralCategory, + assetAddress: proposal.collateralAddress, + id: proposal.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/PWNSimpleLoanSimpleOffer.sol b/src/loan/terms/simple/proposal/offer/PWNSimpleLoanSimpleOffer.sol deleted file mode 100644 index d2fbf8f..0000000 --- a/src/loan/terms/simple/proposal/offer/PWNSimpleLoanSimpleOffer.sol +++ /dev/null @@ -1,291 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity 0.8.16; - -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"; - - -/** - * @title PWN Simple Loan Simple Offer - * @notice Loan terms factory contract creating a simple loan terms from a simple offer. - */ -contract PWNSimpleLoanSimpleOffer 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,uint256 collateralId,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 simple 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 collateralId Token id of an asset used as a collateral, in case of ERC20 should be 0. - * @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; - uint256 collateralId; - 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; - } - - /** - * @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, "PWNSimpleLoanSimpleOffer", 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 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, - 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, 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 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, - 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, 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 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, - 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, 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 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, - 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, signature, permit, extra); - } - - - /*----------------------------------------------------------*| - |* # INTERNALS *| - |*----------------------------------------------------------*/ - - function _acceptOffer( - Offer calldata offer, - bytes calldata signature - ) private returns (bytes32 offerHash, PWNSimpleLoan.Terms memory loanTerms) { - // Check if the loan contract has a tag - _checkLoanContractTag(offer.loanContract); - - // Check collateral state fingerprint if needed - if (offer.checkCollateralStateFingerprint) { - _checkCollateralState({ - addr: offer.collateralAddress, - id: offer.collateralId, - stateFingerprint: offer.collateralStateFingerprint - }); - } - - // Try to accept offer - offerHash = _tryAcceptOffer(offer, signature); - - // Create loan terms object - loanTerms = _createLoanTerms(offer); - } - - 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) 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: offer.collateralId, - amount: offer.collateralAmount - }), - credit: MultiToken.ERC20({ - assetAddress: offer.creditAddress, - amount: offer.creditAmount - }), - fixedInterestAmount: offer.fixedInterestAmount, - accruingInterestAPR: offer.accruingInterestAPR - }); - } - -} diff --git a/src/loan/terms/simple/proposal/request/PWNSimpleLoanSimpleRequest.sol b/src/loan/terms/simple/proposal/request/PWNSimpleLoanSimpleRequest.sol deleted file mode 100644 index fdc7ee0..0000000 --- a/src/loan/terms/simple/proposal/request/PWNSimpleLoanSimpleRequest.sol +++ /dev/null @@ -1,289 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity 0.8.16; - -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"; - - -/** - * @title PWN Simple Loan Simple Request - * @notice Loan terms factory contract creating a simple loan terms from a simple request. - */ -contract PWNSimpleLoanSimpleRequest is PWNSimpleLoanProposal { - - string public constant VERSION = "1.2"; - - /** - * @dev EIP-712 simple request struct type hash. - */ - bytes32 public constant REQUEST_TYPEHASH = keccak256( - "Request(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address creditAddress,uint256 creditAmount,uint256 availableCreditLimit,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedLender,address borrower,uint256 refinancingLoanId,uint256 nonceSpace,uint256 nonce,address loanContract)" - ); - - /** - * @notice Construct defining a simple request. - * @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 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 requested as a loan to a borrower. - * @param availableCreditLimit Available credit limit for the request. It is the maximum amount of tokens which can be borrowed using the request. - * @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 Request expiration timestamp in seconds. - * @param allowedLender Address of an allowed lender. Only this address can accept a request. If the address is zero address, anybody with a credit asset can accept the request. - * @param borrower Address of a borrower. This address has to sign a request to be valid. - * @param refinancingLoanId Id of a loan which is refinanced by this request. If the id is 0, the request is not a refinancing request. - * @param nonceSpace Nonce space of a request nonce. All nonces in the same space can be revoked at once. - * @param nonce Additional value to enable identical requests in time. Without it, it would be impossible to make again request, which was once revoked. - * Can be used to create a group of requests, where accepting one request will make other requests in the group revoked. - * @param loanContract Address of a loan contract that will create a loan from the request. - */ - struct Request { - MultiToken.Category collateralCategory; - address collateralAddress; - uint256 collateralId; - uint256 collateralAmount; - bool checkCollateralStateFingerprint; - bytes32 collateralStateFingerprint; - address creditAddress; - uint256 creditAmount; - uint256 availableCreditLimit; - uint256 fixedInterestAmount; - uint40 accruingInterestAPR; - uint32 duration; - uint40 expiration; - address allowedLender; - address borrower; - uint256 refinancingLoanId; - uint256 nonceSpace; - uint256 nonce; - address loanContract; - } - - /** - * @dev Emitted when a proposal is made via an on-chain transaction. - */ - event RequestMade(bytes32 indexed proposalHash, address indexed proposer, Request request); - - constructor( - address _hub, - address _revokedNonce, - address _stateFingerprintComputerRegistry - ) PWNSimpleLoanProposal( - _hub, _revokedNonce, _stateFingerprintComputerRegistry, "PWNSimpleLoanSimpleRequest", VERSION - ) {} - - /** - * @notice Get a request hash according to EIP-712. - * @param request Request struct to be hashed. - * @return Request struct hash. - */ - function getRequestHash(Request calldata request) public view returns (bytes32) { - return _getProposalHash(REQUEST_TYPEHASH, abi.encode(request)); - } - - /** - * @notice Make an on-chain request. - * @dev Function will mark a request hash as proposed. Request will become acceptable by a lender without a request signature. - * @param request Request struct containing all needed request data. - * @return proposalHash Request hash. - */ - function makeRequest(Request calldata request) external returns (bytes32 proposalHash){ - proposalHash = getRequestHash(request); - _makeProposal(proposalHash, request.borrower); - emit RequestMade(proposalHash, request.borrower, request); - } - - /** - * @notice Accept a request. - * @param request Request struct containing all needed request data. - * @param signature Borrower's signature of the request. - * @param permit Permit struct containing a credit token permit. - * @param extra Extra data to be passed to the loan contract. - * @return loanId Loan id. - */ - function acceptRequest( - Request calldata request, - bytes calldata signature, - Permit calldata permit, - bytes calldata extra - ) public returns (uint256 loanId) { - // Check if the request is refinancing request - if (request.refinancingLoanId != 0) { - revert InvalidRefinancingLoanId({ refinancingLoanId: request.refinancingLoanId }); - } - - // Check permit - _checkPermit(msg.sender, request.creditAddress, permit); - - // Accept request - (bytes32 requestHash, PWNSimpleLoan.Terms memory loanTerms) = _acceptRequest(request, signature); - - // Create loan - return PWNSimpleLoan(request.loanContract).createLOAN({ - proposalHash: requestHash, - loanTerms: loanTerms, - permit: permit, - extra: extra - }); - } - - /** - * @notice Accept a refinancing request. - * @param loanId Id of a loan which is refinanced by the request. - * @param request Request struct containing all needed request data. - * @param signature Borrower's signature of the request. - * @param permit Permit struct containing a credit token permit. - * @param extra Extra data to be passed to the loan contract. - * @return refinancedLoanId Refinanced loan id. - */ - function acceptRefinanceRequest( - uint256 loanId, - Request calldata request, - bytes calldata signature, - Permit calldata permit, - bytes calldata extra - ) public returns (uint256 refinancedLoanId) { - // Check if the request is refinancing request - if (request.refinancingLoanId == 0 || request.refinancingLoanId != loanId) { - revert InvalidRefinancingLoanId({ refinancingLoanId: request.refinancingLoanId }); - } - - // Check permit - _checkPermit(msg.sender, request.creditAddress, permit); - - // Accept request - (bytes32 requestHash, PWNSimpleLoan.Terms memory loanTerms) = _acceptRequest(request, signature); - - // Refinance loan - return PWNSimpleLoan(request.loanContract).refinanceLOAN({ - loanId: loanId, - proposalHash: requestHash, - loanTerms: loanTerms, - permit: permit, - extra: extra - }); - } - - /** - * @notice Accept a request with a nonce revocation. - * @param request Request struct containing all needed request data. - * @param signature Borrower's signature of the request. - * @param permit Permit struct containing a credit token permit. - * @param extra Extra data to be passed to the loan contract. - * @param callersNonceSpace Nonce space of a caller's nonce to revoke. - * @param callersNonceToRevoke Nonce to revoke. - * @return loanId Loan id. - */ - function acceptRequest( - Request calldata request, - bytes calldata signature, - Permit calldata permit, - bytes calldata extra, - uint256 callersNonceSpace, - uint256 callersNonceToRevoke - ) external returns (uint256 loanId) { - _revokeCallersNonce(msg.sender, callersNonceSpace, callersNonceToRevoke); - return acceptRequest(request, signature, permit, extra); - } - - /** - * @notice Accept a refinancing request with a nonce revocation. - * @param loanId Id of a loan which is refinanced by the request. - * @param request Request struct containing all needed request data. - * @param signature Borrower's signature of the request. - * @param permit Permit struct containing a credit token permit. - * @param extra Extra data to be passed to the loan contract. - * @param callersNonceSpace Nonce space of a caller's nonce to revoke. - * @param callersNonceToRevoke Nonce to revoke. - * @return refinancedLoanId Refinanced loan id. - */ - function acceptRefinanceRequest( - uint256 loanId, - Request calldata request, - bytes calldata signature, - Permit calldata permit, - bytes calldata extra, - uint256 callersNonceSpace, - uint256 callersNonceToRevoke - ) external returns (uint256 refinancedLoanId) { - _revokeCallersNonce(msg.sender, callersNonceSpace, callersNonceToRevoke); - return acceptRefinanceRequest(loanId, request, signature, permit, extra); - } - - - /*----------------------------------------------------------*| - |* # INTERNALS *| - |*----------------------------------------------------------*/ - - function _acceptRequest( - Request calldata request, - bytes calldata signature - ) private returns (bytes32 requestHash, PWNSimpleLoan.Terms memory loanTerms) { - // Check if the loan contract has a tag - _checkLoanContractTag(request.loanContract); - - // Check collateral state fingerprint if needed - if (request.checkCollateralStateFingerprint) { - _checkCollateralState({ - addr: request.collateralAddress, - id: request.collateralId, - stateFingerprint: request.collateralStateFingerprint - }); - } - - // Try to accept request - requestHash = _tryAcceptRequest(request, signature); - - // Create loan terms object - loanTerms = _createLoanTerms(request); - } - - function _tryAcceptRequest(Request calldata request, bytes calldata signature) private returns (bytes32 requestHash) { - requestHash = getRequestHash(request); - _tryAcceptProposal({ - proposalHash: requestHash, - creditAmount: request.creditAmount, - availableCreditLimit: request.availableCreditLimit, - apr: request.accruingInterestAPR, - duration: request.duration, - expiration: request.expiration, - nonceSpace: request.nonceSpace, - nonce: request.nonce, - allowedAcceptor: request.allowedLender, - acceptor: msg.sender, - signer: request.borrower, - signature: signature - }); - } - - function _createLoanTerms(Request calldata request) private view returns (PWNSimpleLoan.Terms memory) { - return PWNSimpleLoan.Terms({ - lender: msg.sender, - borrower: request.borrower, - duration: request.duration, - collateral: MultiToken.Asset({ - category: request.collateralCategory, - assetAddress: request.collateralAddress, - id: request.collateralId, - amount: request.collateralAmount - }), - credit: MultiToken.ERC20({ - assetAddress: request.creditAddress, - amount: request.creditAmount - }), - fixedInterestAmount: request.fixedInterestAmount, - accruingInterestAPR: request.accruingInterestAPR - }); - } - -} diff --git a/test/helper/DeploymentTest.t.sol b/test/helper/DeploymentTest.t.sol index 6132d58..d2d3170 100644 --- a/test/helper/DeploymentTest.t.sol +++ b/test/helper/DeploymentTest.t.sol @@ -45,30 +45,20 @@ abstract contract DeploymentTest is Deployments, Test { address(hub), address(loanToken), address(config), address(revokedNonce), address(categoryRegistry) ); - simpleLoanSimpleOffer = new PWNSimpleLoanSimpleOffer(address(hub), address(revokedNonce), address(stateFingerprintComputerRegistry)); simpleLoanListOffer = new PWNSimpleLoanListOffer(address(hub), address(revokedNonce), address(stateFingerprintComputerRegistry)); - simpleLoanSimpleRequest = new PWNSimpleLoanSimpleRequest(address(hub), address(revokedNonce), address(stateFingerprintComputerRegistry)); // Set hub tags - address[] memory addrs = new address[](8); + address[] memory addrs = new address[](4); addrs[0] = address(simpleLoan); addrs[1] = address(simpleLoan); - addrs[2] = address(simpleLoanSimpleOffer); - addrs[3] = address(simpleLoanSimpleOffer); - addrs[4] = address(simpleLoanListOffer); - addrs[5] = address(simpleLoanListOffer); - addrs[6] = address(simpleLoanSimpleRequest); - addrs[7] = address(simpleLoanSimpleRequest); - - bytes32[] memory tags = new bytes32[](8); + 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[4] = PWNHubTags.LOAN_PROPOSAL; - tags[5] = PWNHubTags.NONCE_MANAGER; - tags[6] = PWNHubTags.LOAN_PROPOSAL; - tags[7] = PWNHubTags.NONCE_MANAGER; vm.prank(protocolSafe); hub.setTags(addrs, tags, true); diff --git a/test/unit/PWNSimpleLoanSimpleOffer.t.sol b/test/unit/PWNSimpleLoanSimpleOffer.t.sol deleted file mode 100644 index 9d5e52f..0000000 --- a/test/unit/PWNSimpleLoanSimpleOffer.t.sol +++ /dev/null @@ -1,897 +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 { PWNSimpleLoanSimpleOffer, PWNSimpleLoan, Permit } - from "@pwn/loan/terms/simple/proposal/offer/PWNSimpleLoanSimpleOffer.sol"; -import "@pwn/PWNErrors.sol"; - - -abstract contract PWNSimpleLoanSimpleOfferTest 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 - - PWNSimpleLoanSimpleOffer offerContract; - address hub = makeAddr("hub"); - address revokedNonce = makeAddr("revokedNonce"); - address stateFingerprintComputerRegistry = makeAddr("stateFingerprintComputerRegistry"); - address activeLoanContract = makeAddr("activeLoanContract"); - PWNSimpleLoanSimpleOffer.Offer offer; - 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, PWNSimpleLoanSimpleOffer.Offer offer); - - function setUp() virtual public { - vm.etch(hub, bytes("data")); - vm.etch(revokedNonce, bytes("data")); - vm.etch(token, bytes("data")); - - offerContract = new PWNSimpleLoanSimpleOffer(hub, revokedNonce, stateFingerprintComputerRegistry); - - offer = PWNSimpleLoanSimpleOffer.Offer({ - collateralCategory: MultiToken.Category.ERC721, - collateralAddress: token, - collateralId: 42, - 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 - }); - - 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)", offer.collateralId), - 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(PWNSimpleLoanSimpleOffer.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("PWNSimpleLoanSimpleOffer"), - keccak256("1.2"), - block.chainid, - address(offerContract) - )), - keccak256(abi.encodePacked( - keccak256("Offer(uint8 collateralCategory,address collateralAddress,uint256 collateralId,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, PWNSimpleLoanSimpleOffer.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, PWNSimpleLoanSimpleOffer.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 PWNSimpleLoanSimpleOffer_CreditUsed_Test is PWNSimpleLoanSimpleOfferTest { - - 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 PWNSimpleLoanSimpleOffer_GetOfferHash_Test is PWNSimpleLoanSimpleOfferTest { - - function test_shouldReturnOfferHash() external { - assertEq(_offerHash(offer), offerContract.getOfferHash(offer)); - } - -} - - -/*----------------------------------------------------------*| -|* # MAKE OFFER *| -|*----------------------------------------------------------*/ - -contract PWNSimpleLoanSimpleOffer_MakeOffer_Test is PWNSimpleLoanSimpleOfferTest { - - 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 PWNSimpleLoanSimpleOffer_RevokeNonce_Test is PWNSimpleLoanSimpleOfferTest { - - 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 PWNSimpleLoanSimpleOffer_AcceptOffer_Test is PWNSimpleLoanSimpleOfferTest { - - function testFuzz_shouldFail_whenRefinancingLoanIdNotZero(uint256 refinancingLoanId) external { - vm.assume(refinancingLoanId != 0); - offer.refinancingLoanId = refinancingLoanId; - - vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, refinancingLoanId)); - offerContract.acceptOffer(offer, _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, _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, _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, _signOffer(lenderPK, offer), permit, ""); - } - - function testFuzz_shouldFail_whenComputerReturnsDifferentStateFingerprint_whenShouldCheckStateFingerprint( - bytes32 stateFingerprint - ) external { - vm.assume(stateFingerprint != offer.collateralStateFingerprint); - - vm.mockCall( - stateFingerprintComputer, - abi.encodeWithSignature("getStateFingerprint(uint256)", offer.collateralId), - abi.encode(stateFingerprint) - ); - - vm.expectCall( - stateFingerprintComputer, - abi.encodeWithSignature("getStateFingerprint(uint256)", offer.collateralId) - ); - - vm.expectRevert(abi.encodeWithSelector( - InvalidCollateralStateFingerprint.selector, stateFingerprint, offer.collateralStateFingerprint - )); - offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), permit, ""); - } - - function test_shouldFail_whenInvalidSignature_whenEOA() external { - vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, offer.lender, _offerHash(offer))); - offerContract.acceptOffer(offer, _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, "", permit, ""); - } - - function test_shouldPass_whenOfferHasBeenMadeOnchain() external { - vm.store( - address(offerContract), - keccak256(abi.encode(_offerHash(offer), PROPOSALS_MADE_SLOT)), - bytes32(uint256(1)) - ); - - offerContract.acceptOffer(offer, "", permit, ""); - } - - function test_shouldPass_withValidSignature_whenEOA_whenStandardSignature() external { - offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), permit, ""); - } - - function test_shouldPass_withValidSignature_whenEOA_whenCompactEIP2098Signature() external { - offerContract.acceptOffer(offer, _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, "", 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, _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, _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, _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, _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, _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, _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, _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, _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, _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, _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: offer.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, _signOffer(lenderPK, offer), permit, extra); - } - - function test_shouldReturnNewLoanId() external { - assertEq( - offerContract.acceptOffer(offer, _signOffer(lenderPK, offer), permit, ""), - loanId - ); - } - -} - - -/*----------------------------------------------------------*| -|* # ACCEPT OFFER AND REVOKE CALLERS NONCE *| -|*----------------------------------------------------------*/ - -contract PWNSimpleLoanSimpleOffer_AcceptOfferAndRevokeCallersNonce_Test is PWNSimpleLoanSimpleOfferTest { - - 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, - 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, - 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, - signature: _signOffer(lenderPK, offer), - permit: permit, - extra: "", - callersNonceSpace: 1, - callersNonceToRevoke: 2 - }); - - assertEq(newLoanId, loanId); - } - -} - - -/*----------------------------------------------------------*| -|* # ACCEPT REFINANCE OFFER *| -|*----------------------------------------------------------*/ - -contract PWNSimpleLoanSimpleOffer_AcceptRefinanceOffer_Test is PWNSimpleLoanSimpleOfferTest { - - 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, _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, _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, _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, _signOffer(lenderPK, offer), permit, ""); - } - - function testFuzz_shouldFail_whenComputerReturnsDifferentStateFingerprint_whenShouldCheckStateFingerprint( - bytes32 stateFingerprint - ) external { - vm.assume(stateFingerprint != offer.collateralStateFingerprint); - - vm.mockCall( - stateFingerprintComputer, - abi.encodeWithSignature("getStateFingerprint(uint256)", offer.collateralId), - abi.encode(stateFingerprint) - ); - - vm.expectCall( - stateFingerprintComputer, - abi.encodeWithSignature("getStateFingerprint(uint256)", offer.collateralId) - ); - - vm.expectRevert(abi.encodeWithSelector( - InvalidCollateralStateFingerprint.selector, stateFingerprint, offer.collateralStateFingerprint - )); - offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), permit, ""); - } - - function test_shouldFail_whenInvalidSignature_whenEOA() external { - vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, offer.lender, _offerHash(offer))); - offerContract.acceptRefinanceOffer(loanId, offer, _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, "", 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, "", permit, ""); - } - - function test_shouldPass_withValidSignature_whenEOA_whenStandardSignature() external { - offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), permit, ""); - } - - function test_shouldPass_withValidSignature_whenEOA_whenCompactEIP2098Signature() external { - offerContract.acceptRefinanceOffer(loanId, offer, _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, "", 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, _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, _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, _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, _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, _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, _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, _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, _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, _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, _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: offer.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, _signOffer(lenderPK, offer), permit, extra - ); - } - - function test_shouldReturnRefinancedLoanId() external { - assertEq( - offerContract.acceptRefinanceOffer(loanId, offer, _signOffer(lenderPK, offer), permit, ""), - refinancedLoanId - ); - } - -} - - -/*----------------------------------------------------------*| -|* # ACCEPT REFINANCE OFFER AND REVOKE CALLERS NONCE *| -|*----------------------------------------------------------*/ - -contract PWNSimpleLoanSimpleOffer_AcceptRefinanceOfferAndRevokeCallersNonce_Test is PWNSimpleLoanSimpleOfferTest { - - 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, - 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, - 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, - signature: _signOffer(lenderPK, offer), - permit: permit, - extra: "", - callersNonceSpace: 1, - callersNonceToRevoke: 2 - }); - - assertEq(newLoanId, refinancedLoanId); - } - -} diff --git a/test/unit/PWNSimpleLoanSimpleProposal.t.sol b/test/unit/PWNSimpleLoanSimpleProposal.t.sol new file mode 100644 index 0000000..67fbc98 --- /dev/null +++ b/test/unit/PWNSimpleLoanSimpleProposal.t.sol @@ -0,0 +1,413 @@ +// 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 { PWNSimpleLoanSimpleProposal, PWNSimpleLoanProposal, PWNSimpleLoan, Permit } + from "@pwn/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.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 PWNSimpleLoanSimpleProposalTest is PWNSimpleLoanProposalTest { + + PWNSimpleLoanSimpleProposal proposalContract; + PWNSimpleLoanSimpleProposal.Proposal proposal; + + event ProposalMade(bytes32 indexed proposalHash, address indexed proposer, PWNSimpleLoanSimpleProposal.Proposal proposal); + + function setUp() virtual public override { + super.setUp(); + + proposalContract = new PWNSimpleLoanSimpleProposal(hub, revokedNonce, stateFingerprintComputerRegistry); + proposalContractAddr = PWNSimpleLoanProposal(proposalContract); + + proposal = PWNSimpleLoanSimpleProposal.Proposal({ + collateralCategory: MultiToken.Category.ERC1155, + collateralAddress: token, + collateralId: 0, + collateralAmount: 1, + checkCollateralStateFingerprint: true, + collateralStateFingerprint: keccak256("some state fingerprint"), + creditAddress: token, + creditAmount: 10000, + 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 + }); + } + + + function _proposalHash(PWNSimpleLoanSimpleProposal.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("PWNSimpleLoanSimpleProposal"), + keccak256("1.2"), + block.chainid, + proposalContractAddr + )), + keccak256(abi.encodePacked( + keccak256("Proposal(uint8 collateralCategory,address collateralAddress,uint256 collateralId,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, _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, _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, _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, _proposalSignature(params), _permit, "", nonceSpace, nonce); + } + + function _getProposalHashWith(Params memory _params) internal override returns (bytes32) { + _updateProposal(_params); + return _proposalHash(proposal); + } + +} + + +/*----------------------------------------------------------*| +|* # CREDIT USED *| +|*----------------------------------------------------------*/ + +contract PWNSimpleLoanSimpleProposal_CreditUsed_Test is PWNSimpleLoanSimpleProposalTest { + + 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 PWNSimpleLoanSimpleProposal_RevokeNonce_Test is PWNSimpleLoanSimpleProposalTest { + + 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 PWNSimpleLoanSimpleProposal_GetProposalHash_Test is PWNSimpleLoanSimpleProposalTest { + + function test_shouldReturnOfferHash() external { + assertEq(_proposalHash(proposal), proposalContract.getProposalHash(proposal)); + } + +} + + +/*----------------------------------------------------------*| +|* # MAKE PROPOSAL *| +|*----------------------------------------------------------*/ + +contract PWNSimpleLoanSimpleProposal_MakeProposal_Test is PWNSimpleLoanSimpleProposalTest { + + 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_OfferMade() external { + vm.expectEmit(); + emit ProposalMade(_proposalHash(proposal), proposal.proposer, proposal); + + vm.prank(proposal.proposer); + proposalContract.makeProposal(proposal); + } + + function test_shouldMakeOffer() external { + vm.prank(proposal.proposer); + proposalContract.makeProposal(proposal); + + assertTrue(proposalContract.proposalsMade(_proposalHash(proposal))); + } + + function test_shouldReturnOfferHash() external { + vm.prank(proposal.proposer); + assertEq(proposalContract.makeProposal(proposal), _proposalHash(proposal)); + } + +} + + +/*----------------------------------------------------------*| +|* # ACCEPT PROPOSAL *| +|*----------------------------------------------------------*/ + +contract PWNSimpleLoanSimpleProposal_AcceptProposal_Test is PWNSimpleLoanSimpleProposalTest, PWNSimpleLoanProposal_AcceptProposal_Test { + + function setUp() virtual public override(PWNSimpleLoanSimpleProposalTest, 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, _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: 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, + abi.encodeWithSelector( + PWNSimpleLoan.createLOAN.selector, + _proposalHash(proposal), loanTerms, permit, extra + ) + ); + + vm.prank(acceptor); + proposalContract.acceptProposal( + proposal, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, extra + ); + } + +} + + +/*----------------------------------------------------------*| +|* # ACCEPT PROPOSAL AND REVOKE CALLERS NONCE *| +|*----------------------------------------------------------*/ + +contract PWNSimpleLoanSimpleProposal_AcceptProposalAndRevokeCallersNonce_Test is PWNSimpleLoanSimpleProposalTest, PWNSimpleLoanProposal_AcceptProposalAndRevokeCallersNonce_Test { + + function setUp() virtual public override(PWNSimpleLoanSimpleProposalTest, PWNSimpleLoanProposalTest) { + super.setUp(); + } + +} + + +/*----------------------------------------------------------*| +|* # ACCEPT REFINANCE PROPOSAL *| +|*----------------------------------------------------------*/ + +contract PWNSimpleLoanSimpleProposal_AcceptRefinanceProposal_Test is PWNSimpleLoanSimpleProposalTest, PWNSimpleLoanProposal_AcceptRefinanceProposal_Test { + + function setUp() virtual public override(PWNSimpleLoanSimpleProposalTest, 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, _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, _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, _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: 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, + abi.encodeWithSelector( + PWNSimpleLoan.refinanceLOAN.selector, + loanId, _proposalHash(proposal), loanTerms, permit, extra + ) + ); + + vm.prank(acceptor); + proposalContract.acceptRefinanceProposal( + loanId, proposal, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, extra + ); + } + +} + + +/*----------------------------------------------------------*| +|* # ACCEPT REFINANCE PROPOSAL AND REVOKE CALLERS NONCE *| +|*----------------------------------------------------------*/ + +contract PWNSimpleLoanSimpleProposal_AcceptRefinanceProposalAndRevokeCallersNonce_Test is PWNSimpleLoanSimpleProposalTest, PWNSimpleLoanProposal_AcceptRefinanceProposalAndRevokeCallersNonce_Test { + + function setUp() virtual public override(PWNSimpleLoanSimpleProposalTest, PWNSimpleLoanProposalTest) { + super.setUp(); + } + +} diff --git a/test/unit/PWNSimpleLoanSimpleRequest.t.sol b/test/unit/PWNSimpleLoanSimpleRequest.t.sol deleted file mode 100644 index 12dbf1d..0000000 --- a/test/unit/PWNSimpleLoanSimpleRequest.t.sol +++ /dev/null @@ -1,914 +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 { PWNSimpleLoanSimpleRequest, PWNSimpleLoan, Permit } - from "@pwn/loan/terms/simple/proposal/request/PWNSimpleLoanSimpleRequest.sol"; -import "@pwn/PWNErrors.sol"; - - -abstract contract PWNSimpleLoanSimpleRequestTest 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 - - PWNSimpleLoanSimpleRequest requestContract; - address hub = makeAddr("hub"); - address revokedNonce = makeAddr("revokedNonce"); - address stateFingerprintComputerRegistry = makeAddr("stateFingerprintComputerRegistry"); - address activeLoanContract = makeAddr("activeLoanContract"); - PWNSimpleLoanSimpleRequest.Request request; - address token = makeAddr("token"); - uint256 borrowerPK = 73661723; - address borrower = vm.addr(borrowerPK); - address lender = makeAddr("lender"); - address stateFingerprintComputer = makeAddr("stateFingerprintComputer"); - uint256 loanId = 421; - uint256 refinancedLoanId = 123; - Permit permit; - - event RequestMade(bytes32 indexed proposalHash, address indexed proposer, PWNSimpleLoanSimpleRequest.Request request); - - function setUp() virtual public { - vm.etch(hub, bytes("data")); - vm.etch(revokedNonce, bytes("data")); - vm.etch(token, bytes("data")); - - requestContract = new PWNSimpleLoanSimpleRequest(hub, revokedNonce, stateFingerprintComputerRegistry); - - request = PWNSimpleLoanSimpleRequest.Request({ - collateralCategory: MultiToken.Category.ERC721, - collateralAddress: token, - collateralId: 42, - collateralAmount: 1032, - checkCollateralStateFingerprint: true, - collateralStateFingerprint: keccak256("some state fingerprint"), - creditAddress: token, - creditAmount: 1101001, - availableCreditLimit: 0, - fixedInterestAmount: 1, - accruingInterestAPR: 0, - duration: 1000, - expiration: 60303, - allowedLender: address(0), - borrower: borrower, - refinancingLoanId: 0, - nonceSpace: 1, - nonce: uint256(keccak256("nonce_1")), - loanContract: activeLoanContract - }); - - 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)", request.collateralAddress), - abi.encode(stateFingerprintComputer) - ); - vm.mockCall( - stateFingerprintComputer, - abi.encodeWithSignature("getStateFingerprint(uint256)", request.collateralId), - abi.encode(request.collateralStateFingerprint) - ); - - vm.mockCall( - activeLoanContract, abi.encodeWithSelector(PWNSimpleLoan.createLOAN.selector), abi.encode(loanId) - ); - vm.mockCall( - activeLoanContract, abi.encodeWithSelector(PWNSimpleLoan.refinanceLOAN.selector), abi.encode(refinancedLoanId) - ); - } - - - function _requestHash(PWNSimpleLoanSimpleRequest.Request memory _request) internal view returns (bytes32) { - return keccak256(abi.encodePacked( - "\x19\x01", - keccak256(abi.encode( - keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), - keccak256("PWNSimpleLoanSimpleRequest"), - keccak256("1.2"), - block.chainid, - address(requestContract) - )), - keccak256(abi.encodePacked( - keccak256("Request(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address creditAddress,uint256 creditAmount,uint256 availableCreditLimit,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedLender,address borrower,uint256 refinancingLoanId,uint256 nonceSpace,uint256 nonce,address loanContract)"), - abi.encode(_request) - )) - )); - } - - function _signRequest( - uint256 pk, PWNSimpleLoanSimpleRequest.Request memory _request - ) internal view returns (bytes memory) { - (uint8 v, bytes32 r, bytes32 s) = vm.sign(pk, _requestHash(_request)); - return abi.encodePacked(r, s, v); - } - - function _signRequestCompact( - uint256 pk, PWNSimpleLoanSimpleRequest.Request memory _request - ) internal view returns (bytes memory) { - (uint8 v, bytes32 r, bytes32 s) = vm.sign(pk, _requestHash(_request)); - return abi.encodePacked(r, bytes32(uint256(v) - 27) << 255 | s); - } - -} - - -/*----------------------------------------------------------*| -|* # CREDIT USED *| -|*----------------------------------------------------------*/ - -contract PWNSimpleLoanSimpleRequest_CreditUsed_Test is PWNSimpleLoanSimpleRequestTest { - - function testFuzz_shouldReturnUsedCredit(uint256 used) external { - vm.store(address(requestContract), keccak256(abi.encode(_requestHash(request), CREDIT_USED_SLOT)), bytes32(used)); - - assertEq(requestContract.creditUsed(_requestHash(request)), used); - } - -} - - -/*----------------------------------------------------------*| -|* # GET REQUEST HASH *| -|*----------------------------------------------------------*/ - -contract PWNSimpleLoanSimpleRequest_GetRequestHash_Test is PWNSimpleLoanSimpleRequestTest { - - function test_shouldReturnRequestHash() external { - assertEq(_requestHash(request), requestContract.getRequestHash(request)); - } - -} - - -/*----------------------------------------------------------*| -|* # MAKE REQUEST *| -|*----------------------------------------------------------*/ - -contract PWNSimpleLoanSimpleRequest_MakeRequest_Test is PWNSimpleLoanSimpleRequestTest { - - function testFuzz_shouldFail_whenCallerIsNotBorrower(address caller) external { - vm.assume(caller != request.borrower); - - vm.expectRevert(abi.encodeWithSelector(CallerIsNotStatedProposer.selector, borrower)); - vm.prank(caller); - requestContract.makeRequest(request); - } - - function test_shouldEmit_RequestMade() external { - vm.expectEmit(); - emit RequestMade(_requestHash(request), request.borrower, request); - - vm.prank(request.borrower); - requestContract.makeRequest(request); - } - - function test_shouldMakeRequest() external { - vm.prank(request.borrower); - requestContract.makeRequest(request); - - assertTrue(requestContract.proposalsMade(_requestHash(request))); - } - - function test_shouldReturnRequestHash() external { - vm.prank(request.borrower); - assertEq(requestContract.makeRequest(request), _requestHash(request)); - } - -} - - -/*----------------------------------------------------------*| -|* # REVOKE NONCE *| -|*----------------------------------------------------------*/ - -contract PWNSimpleLoanSimpleRequest_RevokeNonce_Test is PWNSimpleLoanSimpleRequestTest { - - 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); - requestContract.revokeNonce(nonceSpace, nonce); - } - -} - - -/*----------------------------------------------------------*| -|* # ACCEPT REQUEST *| -|*----------------------------------------------------------*/ - -contract PWNSimpleLoanSimpleRequest_AcceptRequest_Test is PWNSimpleLoanSimpleRequestTest { - - function testFuzz_shouldFail_whenRefinancingLoanIdNotZero(uint256 refinancingLoanId) external { - vm.assume(refinancingLoanId != 0); - request.refinancingLoanId = refinancingLoanId; - - vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, refinancingLoanId)); - requestContract.acceptRequest(request, _signRequest(borrowerPK, request), permit, ""); - } - - function testFuzz_shouldFail_whenLoanContractNotTagged_ACTIVE_LOAN(address loanContract) external { - vm.assume(loanContract != activeLoanContract); - request.loanContract = loanContract; - - vm.expectRevert(abi.encodeWithSelector(AddressMissingHubTag.selector, loanContract, PWNHubTags.ACTIVE_LOAN)); - requestContract.acceptRequest(request, _signRequest(borrowerPK, request), permit, ""); - } - - function test_shouldNotCallComputerRegistry_whenShouldNotCheckStateFingerprint() external { - request.checkCollateralStateFingerprint = false; - - vm.expectCall({ - callee: stateFingerprintComputerRegistry, - data: abi.encodeWithSignature("getStateFingerprintComputer(address)", request.collateralAddress), - count: 0 - }); - - requestContract.acceptRequest(request, _signRequest(borrowerPK, request), permit, ""); - } - - function test_shouldFail_whenComputerRegistryReturnsZeroAddress_whenShouldCheckStateFingerprint() external { - vm.mockCall( - stateFingerprintComputerRegistry, - abi.encodeWithSignature("getStateFingerprintComputer(address)", request.collateralAddress), - abi.encode(address(0)) - ); - - vm.expectCall( - stateFingerprintComputerRegistry, - abi.encodeWithSignature("getStateFingerprintComputer(address)", request.collateralAddress) - ); - - vm.expectRevert(abi.encodeWithSelector(MissingStateFingerprintComputer.selector)); - requestContract.acceptRequest(request, _signRequest(borrowerPK, request), permit, ""); - } - - function testFuzz_shouldFail_whenComputerReturnsDifferentStateFingerprint_whenShouldCheckStateFingerprint( - bytes32 stateFingerprint - ) external { - vm.assume(stateFingerprint != request.collateralStateFingerprint); - - vm.mockCall( - stateFingerprintComputer, - abi.encodeWithSignature("getStateFingerprint(uint256)", request.collateralId), - abi.encode(stateFingerprint) - ); - - vm.expectCall( - stateFingerprintComputer, - abi.encodeWithSignature("getStateFingerprint(uint256)", request.collateralId) - ); - - vm.expectRevert(abi.encodeWithSelector( - InvalidCollateralStateFingerprint.selector, stateFingerprint, request.collateralStateFingerprint - )); - requestContract.acceptRequest(request, _signRequest(borrowerPK, request), permit, ""); - } - - function test_shouldFail_whenInvalidSignature_whenEOA() external { - vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, request.borrower, _requestHash(request))); - requestContract.acceptRequest(request, _signRequest(1, request), permit, ""); - } - - function test_shouldFail_whenInvalidSignature_whenContractAccount() external { - vm.etch(borrower, bytes("data")); - - vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, request.borrower, _requestHash(request))); - requestContract.acceptRequest(request, "", permit, ""); - } - - function test_shouldPass_whenRequestHasBeenMadeOnchain() external { - vm.store( - address(requestContract), - keccak256(abi.encode(_requestHash(request), PROPOSALS_MADE_SLOT)), - bytes32(uint256(1)) - ); - - requestContract.acceptRequest(request, "", permit, ""); - } - - function test_shouldPass_withValidSignature_whenEOA_whenStandardSignature() external { - requestContract.acceptRequest(request, _signRequest(borrowerPK, request), permit, ""); - } - - function test_shouldPass_withValidSignature_whenEOA_whenCompactEIP2098Signature() external { - requestContract.acceptRequest(request, _signRequestCompact(borrowerPK, request), permit, ""); - } - - function test_shouldPass_whenValidSignature_whenContractAccount() external { - vm.etch(borrower, bytes("data")); - - vm.mockCall( - borrower, - abi.encodeWithSignature("isValidSignature(bytes32,bytes)"), - abi.encode(bytes4(0x1626ba7e)) - ); - - requestContract.acceptRequest(request, "", permit, ""); - } - - function testFuzz_shouldFail_whenRequestIsExpired(uint256 timestamp) external { - timestamp = bound(timestamp, request.expiration, type(uint256).max); - vm.warp(timestamp); - - vm.expectRevert(abi.encodeWithSelector(Expired.selector, timestamp, request.expiration)); - requestContract.acceptRequest(request, _signRequest(borrowerPK, request), permit, ""); - } - - function test_shouldFail_whenRequestNonceNotUsable() external { - vm.mockCall( - revokedNonce, - abi.encodeWithSignature("isNonceUsable(address,uint256,uint256)"), - abi.encode(false) - ); - vm.expectCall( - revokedNonce, - abi.encodeWithSignature("isNonceUsable(address,uint256,uint256)", request.borrower, request.nonceSpace, request.nonce) - ); - - vm.expectRevert(abi.encodeWithSelector( - NonceNotUsable.selector, request.borrower, request.nonceSpace, request.nonce - )); - requestContract.acceptRequest(request, _signRequest(borrowerPK, request), permit, ""); - } - - function testFuzz_shouldFail_whenCallerIsNotAllowedLender(address caller) external { - address allowedLender = makeAddr("allowedLender"); - vm.assume(caller != allowedLender); - request.allowedLender = allowedLender; - - vm.expectRevert(abi.encodeWithSelector(CallerNotAllowedAcceptor.selector, caller, request.allowedLender)); - vm.prank(caller); - requestContract.acceptRequest(request, _signRequest(borrowerPK, request), permit, ""); - } - - function testFuzz_shouldFail_whenLessThanMinDuration(uint256 duration) external { - vm.assume(duration < requestContract.MIN_LOAN_DURATION()); - duration = bound(duration, 0, requestContract.MIN_LOAN_DURATION() - 1); - request.duration = uint32(duration); - - vm.expectRevert(abi.encodeWithSelector(InvalidDuration.selector, duration, requestContract.MIN_LOAN_DURATION())); - requestContract.acceptRequest(request, _signRequest(borrowerPK, request), permit, ""); - } - - function testFuzz_shouldFail_whenAccruingInterestAPROutOfBounds(uint256 interestAPR) external { - uint256 maxInterest = requestContract.MAX_ACCRUING_INTEREST_APR(); - interestAPR = bound(interestAPR, maxInterest + 1, type(uint40).max); - request.accruingInterestAPR = uint40(interestAPR); - - vm.expectRevert(abi.encodeWithSelector(AccruingInterestAPROutOfBounds.selector, interestAPR, maxInterest)); - requestContract.acceptRequest(request, _signRequest(borrowerPK, request), permit, ""); - } - - function test_shouldRevokeRequest_whenAvailableCreditLimitEqualToZero() external { - request.availableCreditLimit = 0; - - vm.expectCall( - revokedNonce, - abi.encodeWithSignature( - "revokeNonce(address,uint256,uint256)", request.borrower, request.nonceSpace, request.nonce - ) - ); - - requestContract.acceptRequest(request, _signRequest(borrowerPK, request), permit, ""); - } - - function testFuzz_shouldFail_whenUsedCreditExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { - used = bound(used, 1, type(uint256).max - request.creditAmount); - limit = bound(limit, used, used + request.creditAmount - 1); - request.availableCreditLimit = limit; - - vm.store(address(requestContract), keccak256(abi.encode(_requestHash(request), CREDIT_USED_SLOT)), bytes32(used)); - - vm.expectRevert(abi.encodeWithSelector(AvailableCreditLimitExceeded.selector, used + request.creditAmount, limit)); - requestContract.acceptRequest(request, _signRequest(borrowerPK, request), permit, ""); - } - - function testFuzz_shouldIncreaseUsedCredit_whenUsedCreditNotExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { - used = bound(used, 1, type(uint256).max - request.creditAmount); - limit = bound(limit, used + request.creditAmount, type(uint256).max); - request.availableCreditLimit = limit; - - vm.store(address(requestContract), keccak256(abi.encode(_requestHash(request), CREDIT_USED_SLOT)), bytes32(used)); - - requestContract.acceptRequest(request, _signRequest(borrowerPK, request), permit, ""); - - assertEq(requestContract.creditUsed(_requestHash(request)), used + request.creditAmount); - } - - function testFuzz_shouldFail_whenPermitOwnerNotCaller(address owner) external { - vm.assume(owner != lender); - - permit.owner = owner; - permit.asset = request.creditAddress; - - vm.expectRevert(abi.encodeWithSelector(InvalidPermitOwner.selector, owner, lender)); - vm.prank(lender); - requestContract.acceptRequest(request, _signRequest(borrowerPK, request), permit, ""); - } - - function testFuzz_shouldFail_whenPermitAssetNotCreditAsset(address asset) external { - vm.assume(asset != request.creditAddress && asset != address(0)); - - permit.owner = lender; - permit.asset = asset; - - vm.expectRevert(abi.encodeWithSelector(InvalidPermitAsset.selector, asset, token)); - vm.prank(lender); - requestContract.acceptRequest(request, _signRequest(borrowerPK, request), permit, ""); - } - - function test_shouldCallLoanContractWithLoanTerms() external { - permit = Permit({ - asset: token, - owner: lender, - 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: lender, - borrower: request.borrower, - duration: request.duration, - collateral: MultiToken.Asset({ - category: request.collateralCategory, - assetAddress: request.collateralAddress, - id: request.collateralId, - amount: request.collateralAmount - }), - credit: MultiToken.Asset({ - category: MultiToken.Category.ERC20, - assetAddress: request.creditAddress, - id: 0, - amount: request.creditAmount - }), - fixedInterestAmount: request.fixedInterestAmount, - accruingInterestAPR: request.accruingInterestAPR - }); - - vm.expectCall( - activeLoanContract, - abi.encodeWithSelector( - PWNSimpleLoan.createLOAN.selector, - _requestHash(request), loanTerms, permit, extra - ) - ); - - vm.prank(lender); - requestContract.acceptRequest(request, _signRequest(borrowerPK, request), permit, extra); - } - - function test_shouldReturnNewLoanId() external { - assertEq( - requestContract.acceptRequest(request, _signRequest(borrowerPK, request), permit, ""), - loanId - ); - } - -} - - -/*----------------------------------------------------------*| -|* # ACCEPT REQUEST AND REVOKE CALLERS NONCE *| -|*----------------------------------------------------------*/ - -contract PWNSimpleLoanSimpleRequest_AcceptRequestAndRevokeCallersNonce_Test is PWNSimpleLoanSimpleRequestTest { - - 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); - requestContract.acceptRequest({ - request: request, - signature: _signRequest(borrowerPK, request), - 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); - requestContract.acceptRequest({ - request: request, - signature: _signRequest(borrowerPK, request), - permit: permit, - extra: "", - callersNonceSpace: nonceSpace, - callersNonceToRevoke: nonce - }); - } - - // function is calling `acceptRequest`, no need to test it again - function test_shouldCallLoanContract() external { - uint256 newLoanId = requestContract.acceptRequest({ - request: request, - signature: _signRequest(borrowerPK, request), - permit: permit, - extra: "", - callersNonceSpace: 1, - callersNonceToRevoke: 2 - }); - - assertEq(newLoanId, loanId); - } - -} - - -/*----------------------------------------------------------*| -|* # ACCEPT REFINANCE REQUEST *| -|*----------------------------------------------------------*/ - -contract PWNSimpleLoanSimpleRequest_AcceptRefinanceRequest_Test is PWNSimpleLoanSimpleRequestTest { - - function setUp() public override { - super.setUp(); - request.refinancingLoanId = loanId; - } - - - function test_shouldFail_whenRefinancingLoanIdZero() external { - request.refinancingLoanId = 0; - - vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, request.refinancingLoanId)); - requestContract.acceptRefinanceRequest(0, request, _signRequest(borrowerPK, request), permit, ""); - } - - function testFuzz_shouldFail_whenRefinancingLoanIdIsNotEqualToLoanId(uint256 _loanId, uint256 _refinancingLoanId) external { - vm.assume(_loanId != _refinancingLoanId); - vm.assume(_loanId != 0); - request.refinancingLoanId = _refinancingLoanId; - - vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, request.refinancingLoanId)); - requestContract.acceptRefinanceRequest(_loanId, request, _signRequest(borrowerPK, request), permit, ""); - } - - function testFuzz_shouldFail_whenLoanContractNotTagged_ACTIVE_LOAN(address loanContract) external { - vm.assume(loanContract != activeLoanContract); - request.loanContract = loanContract; - - vm.expectRevert(abi.encodeWithSelector(AddressMissingHubTag.selector, loanContract, PWNHubTags.ACTIVE_LOAN)); - requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), permit, ""); - } - - function test_shouldNotCallComputerRegistry_whenShouldNotCheckStateFingerprint() external { - request.checkCollateralStateFingerprint = false; - - vm.expectCall({ - callee: stateFingerprintComputerRegistry, - data: abi.encodeWithSignature("getStateFingerprintComputer(address)", request.collateralAddress), - count: 0 - }); - - requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), permit, ""); - } - - function test_shouldFail_whenComputerRegistryReturnsZeroAddress_whenShouldCheckStateFingerprint() external { - vm.mockCall( - stateFingerprintComputerRegistry, - abi.encodeWithSignature("getStateFingerprintComputer(address)", request.collateralAddress), - abi.encode(address(0)) - ); - - vm.expectCall( - stateFingerprintComputerRegistry, - abi.encodeWithSignature("getStateFingerprintComputer(address)", request.collateralAddress) - ); - - vm.expectRevert(abi.encodeWithSelector(MissingStateFingerprintComputer.selector)); - requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), permit, ""); - } - - function testFuzz_shouldFail_whenComputerReturnsDifferentStateFingerprint_whenShouldCheckStateFingerprint( - bytes32 stateFingerprint - ) external { - vm.assume(stateFingerprint != request.collateralStateFingerprint); - - vm.mockCall( - stateFingerprintComputer, - abi.encodeWithSignature("getStateFingerprint(uint256)", request.collateralId), - abi.encode(stateFingerprint) - ); - - vm.expectCall( - stateFingerprintComputer, - abi.encodeWithSignature("getStateFingerprint(uint256)", request.collateralId) - ); - - vm.expectRevert(abi.encodeWithSelector( - InvalidCollateralStateFingerprint.selector, stateFingerprint, request.collateralStateFingerprint - )); - requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), permit, ""); - } - - function test_shouldFail_whenInvalidSignature_whenEOA() external { - vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, request.borrower, _requestHash(request))); - requestContract.acceptRefinanceRequest(loanId, request, _signRequest(1, request), permit, ""); - } - - function test_shouldFail_whenInvalidSignature_whenContractAccount() external { - vm.etch(borrower, bytes("data")); - - vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, request.borrower, _requestHash(request))); - requestContract.acceptRefinanceRequest(loanId, request, "", permit, ""); - } - - function test_shouldPass_whenRequestHasBeenMadeOnchain() external { - vm.store( - address(requestContract), - keccak256(abi.encode(_requestHash(request), PROPOSALS_MADE_SLOT)), - bytes32(uint256(1)) - ); - - requestContract.acceptRefinanceRequest(loanId, request, "", permit, ""); - } - - function test_shouldPass_withValidSignature_whenEOA_whenStandardSignature() external { - requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), permit, ""); - } - - function test_shouldPass_withValidSignature_whenEOA_whenCompactEIP2098Signature() external { - requestContract.acceptRefinanceRequest(loanId, request, _signRequestCompact(borrowerPK, request), permit, ""); - } - - function test_shouldPass_whenValidSignature_whenContractAccount() external { - vm.etch(borrower, bytes("data")); - - vm.mockCall( - borrower, - abi.encodeWithSignature("isValidSignature(bytes32,bytes)"), - abi.encode(bytes4(0x1626ba7e)) - ); - - requestContract.acceptRefinanceRequest(loanId, request, "", permit, ""); - } - - function testFuzz_shouldFail_whenRequestIsExpired(uint256 timestamp) external { - timestamp = bound(timestamp, request.expiration, type(uint256).max); - vm.warp(timestamp); - - vm.expectRevert(abi.encodeWithSelector(Expired.selector, timestamp, request.expiration)); - requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), permit, ""); - } - - function test_shouldFail_whenRequestNonceNotUsable() external { - vm.mockCall( - revokedNonce, - abi.encodeWithSignature("isNonceUsable(address,uint256,uint256)"), - abi.encode(false) - ); - vm.expectCall( - revokedNonce, - abi.encodeWithSignature("isNonceUsable(address,uint256,uint256)", request.borrower, request.nonceSpace, request.nonce) - ); - - vm.expectRevert(abi.encodeWithSelector( - NonceNotUsable.selector, request.borrower, request.nonceSpace, request.nonce - )); - requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), permit, ""); - } - - function testFuzz_shouldFail_whenCallerIsNotAllowedLender(address caller) external { - address allowedLender = makeAddr("allowedLender"); - vm.assume(caller != allowedLender); - request.allowedLender = allowedLender; - - vm.expectRevert(abi.encodeWithSelector(CallerNotAllowedAcceptor.selector, caller, request.allowedLender)); - vm.prank(caller); - requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), permit, ""); - } - - function testFuzz_shouldFail_whenLessThanMinDuration(uint256 duration) external { - vm.assume(duration < requestContract.MIN_LOAN_DURATION()); - duration = bound(duration, 0, requestContract.MIN_LOAN_DURATION() - 1); - request.duration = uint32(duration); - - vm.expectRevert(abi.encodeWithSelector(InvalidDuration.selector, duration, requestContract.MIN_LOAN_DURATION())); - requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), permit, ""); - } - - function testFuzz_shouldFail_whenAccruingInterestAPROutOfBounds(uint256 interestAPR) external { - uint256 maxInterest = requestContract.MAX_ACCRUING_INTEREST_APR(); - interestAPR = bound(interestAPR, maxInterest + 1, type(uint40).max); - request.accruingInterestAPR = uint40(interestAPR); - - vm.expectRevert(abi.encodeWithSelector(AccruingInterestAPROutOfBounds.selector, interestAPR, maxInterest)); - requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), permit, ""); - } - - function test_shouldRevokeRequest_whenAvailableCreditLimitEqualToZero() external { - request.availableCreditLimit = 0; - - vm.expectCall( - revokedNonce, - abi.encodeWithSignature( - "revokeNonce(address,uint256,uint256)", request.borrower, request.nonceSpace, request.nonce - ) - ); - - requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), permit, ""); - } - - function testFuzz_shouldFail_whenUsedCreditExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { - used = bound(used, 1, type(uint256).max - request.creditAmount); - limit = bound(limit, used, used + request.creditAmount - 1); - request.availableCreditLimit = limit; - - vm.store(address(requestContract), keccak256(abi.encode(_requestHash(request), CREDIT_USED_SLOT)), bytes32(used)); - - vm.expectRevert(abi.encodeWithSelector(AvailableCreditLimitExceeded.selector, used + request.creditAmount, limit)); - requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), permit, ""); - } - - function testFuzz_shouldIncreaseUsedCredit_whenUsedCreditNotExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { - used = bound(used, 1, type(uint256).max - request.creditAmount); - limit = bound(limit, used + request.creditAmount, type(uint256).max); - request.availableCreditLimit = limit; - - vm.store(address(requestContract), keccak256(abi.encode(_requestHash(request), CREDIT_USED_SLOT)), bytes32(used)); - - requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), permit, ""); - - assertEq(requestContract.creditUsed(_requestHash(request)), used + request.creditAmount); - } - - function testFuzz_shouldFail_whenPermitOwnerNotCaller(address owner) external { - vm.assume(owner != lender); - - permit.owner = owner; - permit.asset = request.creditAddress; - - vm.expectRevert(abi.encodeWithSelector(InvalidPermitOwner.selector, owner, lender)); - vm.prank(lender); - requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), permit, ""); - } - - function testFuzz_shouldFail_whenPermitAssetNotCreditAsset(address asset) external { - vm.assume(asset != request.creditAddress && asset != address(0)); - - permit.owner = lender; - permit.asset = asset; - - vm.expectRevert(abi.encodeWithSelector(InvalidPermitAsset.selector, asset, token)); - vm.prank(lender); - requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), permit, ""); - } - - function test_shouldCallLoanContract() external { - permit = Permit({ - asset: token, - owner: lender, - 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: lender, - borrower: request.borrower, - duration: request.duration, - collateral: MultiToken.Asset({ - category: request.collateralCategory, - assetAddress: request.collateralAddress, - id: request.collateralId, - amount: request.collateralAmount - }), - credit: MultiToken.Asset({ - category: MultiToken.Category.ERC20, - assetAddress: request.creditAddress, - id: 0, - amount: request.creditAmount - }), - fixedInterestAmount: request.fixedInterestAmount, - accruingInterestAPR: request.accruingInterestAPR - }); - - vm.expectCall( - activeLoanContract, - abi.encodeWithSelector( - PWNSimpleLoan.refinanceLOAN.selector, - loanId, _requestHash(request), loanTerms, permit, extra - ) - ); - - vm.prank(lender); - requestContract.acceptRefinanceRequest( - loanId, request, _signRequest(borrowerPK, request), permit, extra - ); - } - - function test_shouldReturnRefinancedLoanId() external { - assertEq( - requestContract.acceptRefinanceRequest(loanId, request, _signRequest(borrowerPK, request), permit, ""), - refinancedLoanId - ); - } - -} - - -/*----------------------------------------------------------*| -|* # ACCEPT REFINANCE REQUEST AND REVOKE CALLERS NONCE *| -|*----------------------------------------------------------*/ - -contract PWNSimpleLoanSimpleRequest_AcceptRefinanceRequestAndRevokeCallersNonce_Test is PWNSimpleLoanSimpleRequestTest { - - function setUp() public override { - super.setUp(); - request.refinancingLoanId = loanId; - } - - - 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); - requestContract.acceptRefinanceRequest({ - loanId: loanId, - request: request, - signature: _signRequest(borrowerPK, request), - 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); - requestContract.acceptRefinanceRequest({ - loanId: loanId, - request: request, - signature: _signRequest(borrowerPK, request), - permit: permit, - extra: "", - callersNonceSpace: nonceSpace, - callersNonceToRevoke: nonce - }); - } - - // function is calling `acceptRefinanceRequest`, no need to test it again - function test_shouldCallLoanContract() external { - uint256 newLoanId = requestContract.acceptRefinanceRequest({ - loanId: loanId, - request: request, - signature: _signRequest(borrowerPK, request), - permit: permit, - extra: "", - callersNonceSpace: 1, - callersNonceToRevoke: 2 - }); - - assertEq(newLoanId, refinancedLoanId); - } - -} From ce0a4eb0d0e559a4f2fd85d96513abdc10c4c7b0 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Fri, 22 Mar 2024 10:39:02 -0300 Subject: [PATCH 053/129] 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)); } From c5457a39952dd20afb90f3139bf4bb64fc550d45 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Mon, 25 Mar 2024 11:30:39 -0400 Subject: [PATCH 054/129] feat(dutch-auction-proposal): implement dutch auction proposal --- src/PWNErrors.sol | 13 +- .../PWNSimpleLoanDutchAuctionProposal.sol | 421 +++++++++++ .../PWNSimpleLoanDutchAuctionProposal.t.sol | 697 ++++++++++++++++++ test/unit/PWNSimpleLoanProposal.t.sol | 2 +- 4 files changed, 1127 insertions(+), 6 deletions(-) create mode 100644 src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol create mode 100644 test/unit/PWNSimpleLoanDutchAuctionProposal.t.sol diff --git a/src/PWNErrors.sol b/src/PWNErrors.sol index 19e5bea..dd7180f 100644 --- a/src/PWNErrors.sol +++ b/src/PWNErrors.sol @@ -41,11 +41,6 @@ error NonceNotUsable(address addr, uint256 nonceSpace, uint256 nonce); error InvalidSignatureLength(uint256); error InvalidSignature(address signer, bytes32 digest); -// Offer -error CollateralIdNotWhitelisted(uint256 id); -error MinCollateralAmountNotSet(); -error InsufficientCollateralAmount(uint256 current, uint256 limit); - // Proposal error CallerIsNotStatedProposer(address); error InvalidDuration(uint256 current, uint256 limit); @@ -56,6 +51,14 @@ error Expired(uint256 current, uint256 expiration); error CallerNotAllowedAcceptor(address current, address allowed); error InvalidPermitOwner(address current, address expected); error InvalidPermitAsset(address current, address expected); +error CollateralIdNotWhitelisted(uint256 id); +error MinCollateralAmountNotSet(); +error InsufficientCollateralAmount(uint256 current, uint256 limit); +error InvalidAuctionDuration(uint256 current, uint256 limit); +error AuctionDurationNotInFullMinutes(uint256 current); +error InvalidCreditAmountRange(uint256 minCreditAmount, uint256 maxCreditAmount); +error InvalidCreditAmount(uint256 auctionCreditAmount, uint256 intendedCreditAmount, uint256 slippage); +error AuctionNotInProgress(uint256 currentTimestamp, uint256 auctionStart); // Input data error InvalidInputData(); diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol new file mode 100644 index 0000000..f95a447 --- /dev/null +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol @@ -0,0 +1,421 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.16; + +import { MultiToken } from "MultiToken/MultiToken.sol"; + +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"; + + +/** + * @title PWN Simple Loan Dutch Auction Proposal + * @notice Contract for creating and accepting auction loan proposals. + */ +contract PWNSimpleLoanDutchAuctionProposal 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,uint256 collateralId,uint256 collateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address creditAddress,uint256 minCreditAmount,uint256 maxCreditAmount,uint256 availableCreditLimit,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 auctionStart,uint40 auctionDuration,address allowedAcceptor,address proposer,bool isOffer,uint256 refinancingLoanId,uint256 nonceSpace,uint256 nonce,address loanContract)" + ); + + /** + * @notice Construct defining a simple 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 collateralId Token id of an asset used as a collateral, in case of ERC20 should be 0. + * @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 lended to a borrower. + * @param minCreditAmount Minimum amount of tokens which is proposed as a loan to a borrower. If `isOffer` is true, auction will start with this amount, otherwise it will end with this amount. + * @param maxCreditAmount Maximum amount of tokens which is proposed as a loan to a borrower. If `isOffer` is true, auction will end with this amount, otherwise it will start with this amount. + * @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 auctionStart Auction start timestamp in seconds. + * @param auctionDuration Auction duration 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; + uint256 collateralId; + uint256 collateralAmount; + bool checkCollateralStateFingerprint; + bytes32 collateralStateFingerprint; + address creditAddress; + uint256 minCreditAmount; + uint256 maxCreditAmount; + uint256 availableCreditLimit; + uint256 fixedInterestAmount; + uint40 accruingInterestAPR; + uint32 duration; + uint40 auctionStart; + uint40 auctionDuration; + address allowedAcceptor; + address proposer; + bool isOffer; + uint256 refinancingLoanId; + uint256 nonceSpace; + uint256 nonce; + address loanContract; + } + + /** + * @notice Construct defining proposal concrete values. + * @dev At the time of execution, current auction credit amount must be in the range of `creditAmount` and `creditAmount` + `slippage`. + * @param intendedCreditAmount Amount of tokens which acceptor intends to borrow. + * @param slippage Slippage value that is acceptor willing to accept from the intended `creditAmount`. + * If proposal is an offer, slippage is added to the `creditAmount`, otherwise it is subtracted. + */ + struct ProposalValues { + uint256 intendedCreditAmount; + uint256 slippage; + } + + /** + * @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, "PWNSimpleLoanDutchAuctionProposal", 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 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. + * @param proposal Proposal struct containing all proposal data. + * @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) { + // Check proposal + if (proposal.auctionDuration < 1 minutes) { + revert InvalidAuctionDuration({ + current: proposal.auctionDuration, + limit: 1 minutes + }); + } + if (proposal.auctionDuration % 1 minutes > 0) { + revert AuctionDurationNotInFullMinutes({ + current: proposal.auctionDuration + }); + } + if (proposal.maxCreditAmount <= proposal.minCreditAmount) { + revert InvalidCreditAmountRange({ + minCreditAmount: proposal.minCreditAmount, + maxCreditAmount: proposal.maxCreditAmount + }); + } + + // Check auction is in progress + if (timestamp < proposal.auctionStart) { + revert AuctionNotInProgress({ + currentTimestamp: timestamp, + auctionStart: proposal.auctionStart + }); + } + if (proposal.auctionStart + proposal.auctionDuration + 1 minutes <= timestamp) { + revert Expired({ + current: timestamp, + expiration: proposal.auctionStart + proposal.auctionDuration + 1 minutes + }); + } + + // Note: Auction duration is increased by 1 minute to have + // `maxCreditAmount` value in the last minutes of the auction. + + uint256 creditAmountDelta = Math.mulDiv( + proposal.maxCreditAmount - proposal.minCreditAmount, // Max credit amount difference + (timestamp - proposal.auctionStart) / 1 minutes, // Time passed since auction start + proposal.auctionDuration / 1 minutes // Auction duration + ); + + // Note: Request auction is decreasing credit amount (dutch auction). + // Offer auction is increasing credit amount (reverse dutch auction). + + // Return credit amount + return proposal.isOffer + ? proposal.minCreditAmount + creditAmountDelta + : proposal.maxCreditAmount - creditAmountDelta; + } + + /** + * @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 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 Proposal values struct containing concrete 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 Proposal values struct containing concrete 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 Proposal values struct containing concrete 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); + + // Calculate current credit amount + uint256 creditAmount = getCreditAmount(proposal, block.timestamp); + + // Invariant check + require(proposal.maxCreditAmount >= creditAmount && creditAmount >= proposal.minCreditAmount); + + // Check acceptor values + if (proposal.isOffer) { + if ( + creditAmount < proposalValues.intendedCreditAmount || + proposalValues.intendedCreditAmount + proposalValues.slippage < creditAmount + ) { + revert InvalidCreditAmount({ + auctionCreditAmount: creditAmount, + intendedCreditAmount: proposalValues.intendedCreditAmount, + slippage: proposalValues.slippage + }); + } + } else { + if ( + creditAmount > proposalValues.intendedCreditAmount || + proposalValues.intendedCreditAmount - proposalValues.slippage > creditAmount + ) { + revert InvalidCreditAmount({ + auctionCreditAmount: creditAmount, + intendedCreditAmount: proposalValues.intendedCreditAmount, + slippage: proposalValues.slippage + }); + } + } + + // 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); + + // 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, + duration: proposal.duration, + collateral: MultiToken.Asset({ + category: proposal.collateralCategory, + assetAddress: proposal.collateralAddress, + id: proposal.collateralId, + amount: proposal.collateralAmount + }), + credit: MultiToken.ERC20({ + assetAddress: proposal.creditAddress, + amount: creditAmount + }), + fixedInterestAmount: proposal.fixedInterestAmount, + accruingInterestAPR: proposal.accruingInterestAPR + }); + } + +} diff --git a/test/unit/PWNSimpleLoanDutchAuctionProposal.t.sol b/test/unit/PWNSimpleLoanDutchAuctionProposal.t.sol new file mode 100644 index 0000000..1b58f5e --- /dev/null +++ b/test/unit/PWNSimpleLoanDutchAuctionProposal.t.sol @@ -0,0 +1,697 @@ +// 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 { PWNSimpleLoanDutchAuctionProposal, PWNSimpleLoanProposal, PWNSimpleLoan, Permit } + from "@pwn/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.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 PWNSimpleLoanDutchAuctionProposalTest is PWNSimpleLoanProposalTest { + + PWNSimpleLoanDutchAuctionProposal proposalContract; + PWNSimpleLoanDutchAuctionProposal.Proposal proposal; + PWNSimpleLoanDutchAuctionProposal.ProposalValues proposalValues; + + event ProposalMade(bytes32 indexed proposalHash, address indexed proposer, PWNSimpleLoanDutchAuctionProposal.Proposal proposal); + + function setUp() virtual public override { + super.setUp(); + + proposalContract = new PWNSimpleLoanDutchAuctionProposal(hub, revokedNonce, stateFingerprintComputerRegistry); + proposalContractAddr = PWNSimpleLoanProposal(proposalContract); + + proposal = PWNSimpleLoanDutchAuctionProposal.Proposal({ + collateralCategory: MultiToken.Category.ERC1155, + collateralAddress: token, + collateralId: 0, + collateralAmount: 1, + checkCollateralStateFingerprint: true, + collateralStateFingerprint: keccak256("some state fingerprint"), + creditAddress: token, + minCreditAmount: 10000, + maxCreditAmount: 100000, + availableCreditLimit: 0, + fixedInterestAmount: 1, + accruingInterestAPR: 0, + duration: 1000, + auctionStart: 1, + auctionDuration: 1 minutes, + allowedAcceptor: address(0), + proposer: proposer, + isOffer: true, + refinancingLoanId: 0, + nonceSpace: 1, + nonce: uint256(keccak256("nonce_1")), + loanContract: activeLoanContract + }); + + proposalValues = PWNSimpleLoanDutchAuctionProposal.ProposalValues({ + intendedCreditAmount: 10000, + slippage: 0 + }); + } + + + function _proposalHash(PWNSimpleLoanDutchAuctionProposal.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("PWNSimpleLoanDutchAuctionProposal"), + keccak256("1.2"), + block.chainid, + proposalContractAddr + )), + keccak256(abi.encodePacked( + keccak256("Proposal(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address creditAddress,uint256 minCreditAmount,uint256 maxCreditAmount,uint256 availableCreditLimit,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 auctionStart,uint40 auctionDuration,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; + if (proposal.isOffer) { + proposal.minCreditAmount = _params.creditAmount; + proposal.maxCreditAmount = proposal.minCreditAmount + 1000; + proposalValues.intendedCreditAmount = proposal.minCreditAmount; + } else { + proposal.maxCreditAmount = _params.creditAmount; + proposal.minCreditAmount = proposal.maxCreditAmount - 1000; + 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; + } + + 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 PWNSimpleLoanDutchAuctionProposal_CreditUsed_Test is PWNSimpleLoanDutchAuctionProposalTest { + + 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 PWNSimpleLoanDutchAuctionProposal_RevokeNonce_Test is PWNSimpleLoanDutchAuctionProposalTest { + + 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 PWNSimpleLoanDutchAuctionProposal_GetProposalHash_Test is PWNSimpleLoanDutchAuctionProposalTest { + + function test_shouldReturnProposalHash() external { + assertEq(_proposalHash(proposal), proposalContract.getProposalHash(proposal)); + } + +} + + +/*----------------------------------------------------------*| +|* # MAKE PROPOSAL *| +|*----------------------------------------------------------*/ + +contract PWNSimpleLoanDutchAuctionProposal_MakeProposal_Test is PWNSimpleLoanDutchAuctionProposalTest { + + 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)); + } + +} + + +/*----------------------------------------------------------*| +|* # GET CREDIT AMOUNT *| +|*----------------------------------------------------------*/ + +contract PWNSimpleLoanDutchAuctionProposal_GetCreditAmount_Test is PWNSimpleLoanDutchAuctionProposalTest { + + function testFuzz_shouldFail_whenInvalidAuctionDuration(uint40 auctionDuration) external { + vm.assume(auctionDuration < 1 minutes); + proposal.auctionDuration = auctionDuration; + + vm.expectRevert(abi.encodeWithSelector(InvalidAuctionDuration.selector, auctionDuration, 1 minutes)); + proposalContract.acceptProposal( + proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, "" + ); + } + + function testFuzz_shouldFail_whenAuctionDurationNotInFullMinutes(uint40 auctionDuration) external { + vm.assume(auctionDuration > 1 minutes && auctionDuration % 1 minutes > 0); + proposal.auctionDuration = auctionDuration; + + vm.expectRevert(abi.encodeWithSelector(AuctionDurationNotInFullMinutes.selector, auctionDuration)); + proposalContract.acceptProposal( + proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, "" + ); + } + + function testFuzz_shouldFail_whenInvalidCreditAmountRange(uint256 minCreditAmount, uint256 maxCreditAmount) external { + vm.assume(minCreditAmount >= maxCreditAmount); + proposal.minCreditAmount = minCreditAmount; + proposal.maxCreditAmount = maxCreditAmount; + + vm.expectRevert(abi.encodeWithSelector(InvalidCreditAmountRange.selector, minCreditAmount, maxCreditAmount)); + proposalContract.acceptProposal( + proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, "" + ); + } + + function testFuzz_shouldFail_whenAuctionNotInProgress(uint40 auctionStart, uint256 time) external { + auctionStart = uint40(bound(auctionStart, 1, type(uint40).max)); + time = bound(time, 0, auctionStart - 1); + + proposal.auctionStart = auctionStart; + + vm.expectRevert(abi.encodeWithSelector(AuctionNotInProgress.selector, time, auctionStart)); + proposalContract.getCreditAmount(proposal, time); + } + + function testFuzz_shouldFail_whenProposalExpired(uint40 auctionDuration, uint256 time) external { + auctionDuration = uint40(bound(auctionDuration, 1, (type(uint40).max / 1 minutes) - 2)) * 1 minutes; + time = bound(time, auctionDuration + 1 minutes + 1, type(uint40).max); + + proposal.auctionStart = 0; + proposal.auctionDuration = auctionDuration; + + vm.expectRevert(abi.encodeWithSelector(Expired.selector, time, auctionDuration + 1 minutes)); + proposalContract.getCreditAmount(proposal, time); + } + + function testFuzz_shouldReturnCorrectEdgeValues(uint40 auctionDuration) external { + proposal.auctionStart = 0; + proposal.auctionDuration = uint40(bound(auctionDuration, 1, type(uint40).max / 1 minutes)) * 1 minutes; + + proposal.isOffer = true; + assertEq(proposalContract.getCreditAmount(proposal, proposal.auctionStart), proposal.minCreditAmount); + assertEq(proposalContract.getCreditAmount(proposal, proposal.auctionDuration), proposal.maxCreditAmount); + assertEq(proposalContract.getCreditAmount(proposal, proposal.auctionDuration + 59), proposal.maxCreditAmount); + + proposal.isOffer = false; + assertEq(proposalContract.getCreditAmount(proposal, proposal.auctionStart), proposal.maxCreditAmount); + assertEq(proposalContract.getCreditAmount(proposal, proposal.auctionDuration), proposal.minCreditAmount); + assertEq(proposalContract.getCreditAmount(proposal, proposal.auctionDuration + 59), proposal.minCreditAmount); + } + + function testFuzz_shouldReturnCorrectCreditAmount_whenOffer( + uint256 minCreditAmount, uint256 maxCreditAmount, uint256 timeInAuction, uint40 auctionDuration + ) external { + maxCreditAmount = bound(maxCreditAmount, 1, 1e40); + minCreditAmount = bound(minCreditAmount, 0, maxCreditAmount - 1); + auctionDuration = uint40(bound(auctionDuration, 1, 99999)) * 1 minutes; + timeInAuction = bound(timeInAuction, 0, auctionDuration); + + proposal.isOffer = true; + proposal.minCreditAmount = minCreditAmount; + proposal.maxCreditAmount = maxCreditAmount; + proposal.auctionStart = 0; + proposal.auctionDuration = auctionDuration; + + assertEq( + proposalContract.getCreditAmount(proposal, timeInAuction), + minCreditAmount + (maxCreditAmount - minCreditAmount) * (timeInAuction / 1 minutes * 1 minutes) / auctionDuration + ); + } + + function testFuzz_shouldReturnCorrectCreditAmount_whenRequest( + uint256 minCreditAmount, uint256 maxCreditAmount, uint256 timeInAuction, uint40 auctionDuration + ) external { + maxCreditAmount = bound(maxCreditAmount, 1, 1e40); + minCreditAmount = bound(minCreditAmount, 0, maxCreditAmount - 1); + auctionDuration = uint40(bound(auctionDuration, 1, 99999)) * 1 minutes; + timeInAuction = bound(timeInAuction, 0, auctionDuration); + + proposal.isOffer = false; + proposal.minCreditAmount = minCreditAmount; + proposal.maxCreditAmount = maxCreditAmount; + proposal.auctionStart = 0; + proposal.auctionDuration = auctionDuration; + + assertEq( + proposalContract.getCreditAmount(proposal, timeInAuction), + maxCreditAmount - (maxCreditAmount - minCreditAmount) * (timeInAuction / 1 minutes * 1 minutes) / auctionDuration + ); + } + +} + + +/*----------------------------------------------------------*| +|* # ACCEPT PROPOSAL *| +|*----------------------------------------------------------*/ + +contract PWNSimpleLoanDutchAuctionProposal_AcceptProposal_Test is PWNSimpleLoanDutchAuctionProposalTest, PWNSimpleLoanProposal_AcceptProposal_Test { + + function setUp() virtual public override(PWNSimpleLoanDutchAuctionProposalTest, 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 testFuzz_shouldFail_whenCurrentAuctionCreditAmountNotInIntendedCreditAmountRange_whenOffer( + uint256 intendedCreditAmount + ) external { + proposal.isOffer = true; + proposal.minCreditAmount = 0; + proposal.maxCreditAmount = 100000; + proposal.auctionStart = 1; + proposal.auctionDuration = 100 minutes; + + vm.warp(proposal.auctionStart + proposal.auctionDuration / 2); + + proposalValues.slippage = 500; + intendedCreditAmount = bound(intendedCreditAmount, 0, type(uint256).max - proposalValues.slippage); + proposalValues.intendedCreditAmount = intendedCreditAmount; + + uint256 auctionCreditAmount = proposalContract.getCreditAmount(proposal, block.timestamp); + + vm.assume( + intendedCreditAmount < auctionCreditAmount - proposalValues.slippage + || intendedCreditAmount > auctionCreditAmount + ); + + vm.expectRevert(abi.encodeWithSelector( + InvalidCreditAmount.selector, auctionCreditAmount, proposalValues.intendedCreditAmount, proposalValues.slippage + )); + proposalContract.acceptProposal( + proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, "" + ); + } + + function testFuzz_shouldFail_whenCurrentAuctionCreditAmountNotInIntendedCreditAmountRange_whenRequest( + uint256 intendedCreditAmount + ) external { + proposal.isOffer = false; + proposal.minCreditAmount = 0; + proposal.maxCreditAmount = 100000; + proposal.auctionStart = 1; + proposal.auctionDuration = 100 minutes; + + vm.warp(proposal.auctionStart + proposal.auctionDuration / 2); + + proposalValues.slippage = 500; + intendedCreditAmount = bound(intendedCreditAmount, proposalValues.slippage, type(uint256).max); + proposalValues.intendedCreditAmount = intendedCreditAmount; + + uint256 auctionCreditAmount = proposalContract.getCreditAmount(proposal, block.timestamp); + + vm.assume( + intendedCreditAmount < auctionCreditAmount + || intendedCreditAmount - proposalValues.slippage > auctionCreditAmount + ); + + vm.expectRevert(abi.encodeWithSelector( + InvalidCreditAmount.selector, auctionCreditAmount, proposalValues.intendedCreditAmount, proposalValues.slippage + )); + proposalContract.acceptProposal( + proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, "" + ); + } + + function testFuzz_shouldCallLoanContractWithLoanTerms( + uint256 minCreditAmount, uint256 maxCreditAmount, uint40 auctionDuration, uint256 timeInAuction, 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.minCreditAmount = minCreditAmount; + proposal.maxCreditAmount = maxCreditAmount; + proposal.auctionStart = 1; + proposal.auctionDuration = auctionDuration; + + vm.warp(proposal.auctionStart + timeInAuction); + + proposalValues.intendedCreditAmount = proposalContract.getCreditAmount(proposal, block.timestamp); + 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.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 PWNSimpleLoanDutchAuctionProposal_AcceptProposalAndRevokeCallersNonce_Test is PWNSimpleLoanDutchAuctionProposalTest, PWNSimpleLoanProposal_AcceptProposalAndRevokeCallersNonce_Test { + + function setUp() virtual public override(PWNSimpleLoanDutchAuctionProposalTest, PWNSimpleLoanProposalTest) { + super.setUp(); + } + +} + + +/*----------------------------------------------------------*| +|* # ACCEPT REFINANCE PROPOSAL *| +|*----------------------------------------------------------*/ + +contract PWNSimpleLoanDutchAuctionProposal_AcceptRefinanceProposal_Test is PWNSimpleLoanDutchAuctionProposalTest, PWNSimpleLoanProposal_AcceptRefinanceProposal_Test { + + function setUp() virtual public override(PWNSimpleLoanDutchAuctionProposalTest, 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 testFuzz_shouldFail_whenCurrentAuctionCreditAmountNotInIntendedCreditAmountRange_whenOffer( + uint256 intendedCreditAmount + ) external { + proposal.isOffer = true; + proposal.minCreditAmount = 0; + proposal.maxCreditAmount = 100000; + proposal.auctionStart = 1; + proposal.auctionDuration = 100 minutes; + + vm.warp(proposal.auctionStart + proposal.auctionDuration / 2); + + proposalValues.slippage = 500; + intendedCreditAmount = bound(intendedCreditAmount, 0, type(uint256).max - proposalValues.slippage); + proposalValues.intendedCreditAmount = intendedCreditAmount; + + uint256 auctionCreditAmount = proposalContract.getCreditAmount(proposal, block.timestamp); + + vm.assume( + intendedCreditAmount < auctionCreditAmount - proposalValues.slippage + || intendedCreditAmount > auctionCreditAmount + ); + + vm.expectRevert(abi.encodeWithSelector( + InvalidCreditAmount.selector, auctionCreditAmount, proposalValues.intendedCreditAmount, proposalValues.slippage + )); + proposalContract.acceptRefinanceProposal( + loanId, proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, extra + ); + } + + function testFuzz_shouldFail_whenCurrentAuctionCreditAmountNotInIntendedCreditAmountRange_whenRequest( + uint256 intendedCreditAmount + ) external { + proposal.isOffer = false; + proposal.minCreditAmount = 0; + proposal.maxCreditAmount = 100000; + proposal.auctionStart = 1; + proposal.auctionDuration = 100 minutes; + + vm.warp(proposal.auctionStart + proposal.auctionDuration / 2); + + proposalValues.slippage = 500; + intendedCreditAmount = bound(intendedCreditAmount, proposalValues.slippage, type(uint256).max); + proposalValues.intendedCreditAmount = intendedCreditAmount; + + uint256 auctionCreditAmount = proposalContract.getCreditAmount(proposal, block.timestamp); + + vm.assume( + intendedCreditAmount < auctionCreditAmount + || intendedCreditAmount - proposalValues.slippage > auctionCreditAmount + ); + + vm.expectRevert(abi.encodeWithSelector( + InvalidCreditAmount.selector, auctionCreditAmount, proposalValues.intendedCreditAmount, proposalValues.slippage + )); + proposalContract.acceptRefinanceProposal( + loanId, proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, extra + ); + } + + function testFuzz_shouldCallLoanContractWithLoanTerms( + uint256 minCreditAmount, uint256 maxCreditAmount, uint40 auctionDuration, uint256 timeInAuction, 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.minCreditAmount = minCreditAmount; + proposal.maxCreditAmount = maxCreditAmount; + proposal.auctionStart = 1; + proposal.auctionDuration = auctionDuration; + + vm.warp(proposal.auctionStart + timeInAuction); + + proposalValues.intendedCreditAmount = proposalContract.getCreditAmount(proposal, block.timestamp); + 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.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 PWNSimpleLoanDutchAuctionProposal_AcceptRefinanceProposalAndRevokeCallersNonce_Test is PWNSimpleLoanDutchAuctionProposalTest, PWNSimpleLoanProposal_AcceptRefinanceProposalAndRevokeCallersNonce_Test { + + function setUp() virtual public override(PWNSimpleLoanDutchAuctionProposalTest, PWNSimpleLoanProposalTest) { + super.setUp(); + } + +} diff --git a/test/unit/PWNSimpleLoanProposal.t.sol b/test/unit/PWNSimpleLoanProposal.t.sol index 1dfe231..c3e3e5e 100644 --- a/test/unit/PWNSimpleLoanProposal.t.sol +++ b/test/unit/PWNSimpleLoanProposal.t.sol @@ -64,7 +64,7 @@ abstract contract PWNSimpleLoanProposalTest is Test { params.checkCollateralStateFingerprint = true; params.collateralStateFingerprint = keccak256("some state fingerprint"); params.duration = 1 hours; - params.expiration = uint40(block.timestamp + 1000); + params.expiration = uint40(block.timestamp + 20 minutes); params.proposer = proposer; params.loanContract = activeLoanContract; params.signerPK = proposerPK; From 82fbed4ae218935a1ed3aaf094f36cfce84f26f5 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Mon, 25 Mar 2024 13:29:06 -0400 Subject: [PATCH 055/129] feat(dutch-auction-proposal): update proposal version --- .../simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol | 4 ++-- test/unit/PWNSimpleLoanDutchAuctionProposal.t.sol | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol index f95a447..55f2a4e 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol @@ -13,11 +13,11 @@ import "@pwn/PWNErrors.sol"; /** * @title PWN Simple Loan Dutch Auction Proposal - * @notice Contract for creating and accepting auction loan proposals. + * @notice Contract for creating and accepting dutch auction loan proposals. */ contract PWNSimpleLoanDutchAuctionProposal is PWNSimpleLoanProposal { - string public constant VERSION = "1.2"; + string public constant VERSION = "1.0"; /** * @dev EIP-712 simple proposal struct type hash. diff --git a/test/unit/PWNSimpleLoanDutchAuctionProposal.t.sol b/test/unit/PWNSimpleLoanDutchAuctionProposal.t.sol index 1b58f5e..f95eddb 100644 --- a/test/unit/PWNSimpleLoanDutchAuctionProposal.t.sol +++ b/test/unit/PWNSimpleLoanDutchAuctionProposal.t.sol @@ -73,7 +73,7 @@ abstract contract PWNSimpleLoanDutchAuctionProposalTest is PWNSimpleLoanProposal keccak256(abi.encode( keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), keccak256("PWNSimpleLoanDutchAuctionProposal"), - keccak256("1.2"), + keccak256("1.0"), block.chainid, proposalContractAddr )), From d6f76ba3512691311d79a6784593815264ec1c89 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Mon, 25 Mar 2024 13:29:58 -0400 Subject: [PATCH 056/129] feat(fungible-proposal): update proposal version --- .../terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol | 2 +- test/unit/PWNSimpleLoanFungibleProposal.t.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol index 676f7a5..914ec6f 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol @@ -19,7 +19,7 @@ import "@pwn/PWNErrors.sol"; */ contract PWNSimpleLoanFungibleProposal is PWNSimpleLoanProposal { - string public constant VERSION = "1.2"; + string public constant VERSION = "1.0"; /** * @notice Credit per collateral unit denominator. It is used to calculate credit amount from collateral amount. diff --git a/test/unit/PWNSimpleLoanFungibleProposal.t.sol b/test/unit/PWNSimpleLoanFungibleProposal.t.sol index dcb4987..654979f 100644 --- a/test/unit/PWNSimpleLoanFungibleProposal.t.sol +++ b/test/unit/PWNSimpleLoanFungibleProposal.t.sol @@ -70,7 +70,7 @@ abstract contract PWNSimpleLoanFungibleProposalTest is PWNSimpleLoanProposalTest keccak256(abi.encode( keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), keccak256("PWNSimpleLoanFungibleProposal"), - keccak256("1.2"), + keccak256("1.0"), block.chainid, proposalContractAddr )), From 3091405d792ba28351db4dd6a67d0811394cd034 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Mon, 25 Mar 2024 13:39:55 -0400 Subject: [PATCH 057/129] feat(dutch-auction-proposal): remove invariant check --- .../simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol index 55f2a4e..dcecda2 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol @@ -329,9 +329,6 @@ contract PWNSimpleLoanDutchAuctionProposal is PWNSimpleLoanProposal { // Calculate current credit amount uint256 creditAmount = getCreditAmount(proposal, block.timestamp); - // Invariant check - require(proposal.maxCreditAmount >= creditAmount && creditAmount >= proposal.minCreditAmount); - // Check acceptor values if (proposal.isOffer) { if ( From aa27f9aa7bfca15466d662f42c241f42ee104326 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Mon, 25 Mar 2024 14:23:46 -0400 Subject: [PATCH 058/129] feat(fungible-proposal): change credit amount computing function visibility to public --- .../PWNSimpleLoanFungibleProposal.sol | 16 ++++++---- test/unit/PWNSimpleLoanFungibleProposal.t.sol | 29 +++++++++++++++---- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol index 914ec6f..a02ab90 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol @@ -122,6 +122,16 @@ contract PWNSimpleLoanFungibleProposal is PWNSimpleLoanProposal { emit ProposalMade(proposalHash, proposal.proposer, proposal); } + /** + * @notice Compute credit amount from collateral amount and credit per collateral unit. + * @param collateralAmount Amount of collateral. + * @param creditPerCollateralUnit Amount of credit per collateral unit with 38 decimals. + * @return Amount of credit. + */ + function getCreditAmount(uint256 collateralAmount, uint256 creditPerCollateralUnit) public pure returns (uint256) { + return Math.mulDiv(collateralAmount, creditPerCollateralUnit, CREDIT_PER_COLLATERAL_UNIT_DENOMINATOR); + } + /** * @notice Accept a proposal. * @param proposal Proposal struct containing all proposal data. @@ -287,7 +297,7 @@ contract PWNSimpleLoanFungibleProposal is PWNSimpleLoanProposal { } // Calculate credit amount - uint256 creditAmount = _creditAmount(proposalValues.collateralAmount, proposal.creditPerCollateralUnit); + uint256 creditAmount = getCreditAmount(proposalValues.collateralAmount, proposal.creditPerCollateralUnit); // Try to accept proposal proposalHash = _tryAcceptProposal(proposal, creditAmount, signature); @@ -296,10 +306,6 @@ contract PWNSimpleLoanFungibleProposal is PWNSimpleLoanProposal { loanTerms = _createLoanTerms(proposal, proposalValues.collateralAmount, creditAmount); } - function _creditAmount(uint256 collateralAmount, uint256 creditPerCollateralUnit) private pure returns (uint256) { - return Math.mulDiv(collateralAmount, creditPerCollateralUnit, CREDIT_PER_COLLATERAL_UNIT_DENOMINATOR); - } - function _tryAcceptProposal( Proposal calldata proposal, uint256 creditAmount, diff --git a/test/unit/PWNSimpleLoanFungibleProposal.t.sol b/test/unit/PWNSimpleLoanFungibleProposal.t.sol index 654979f..ed0dfd5 100644 --- a/test/unit/PWNSimpleLoanFungibleProposal.t.sol +++ b/test/unit/PWNSimpleLoanFungibleProposal.t.sol @@ -81,10 +81,6 @@ abstract contract PWNSimpleLoanFungibleProposalTest is PWNSimpleLoanProposalTest )); } - function _creditAmount(uint256 collateralAmount, uint256 creditPerCollateralUnit) internal view returns (uint256) { - return Math.mulDiv(collateralAmount, creditPerCollateralUnit, proposalContract.CREDIT_PER_COLLATERAL_UNIT_DENOMINATOR()); - } - function _updateProposal(Params memory _params) internal { proposalValues.collateralAmount = _params.creditAmount; @@ -224,6 +220,27 @@ contract PWNSimpleLoanFungibleProposal_MakeProposal_Test is PWNSimpleLoanFungibl } +/*----------------------------------------------------------*| +|* # GET CREDIT AMOUNT *| +|*----------------------------------------------------------*/ + +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 + ); + + assertEq( + proposalContract.getCreditAmount(collateralAmount, creditPerCollateralUnit), + Math.mulDiv(collateralAmount, creditPerCollateralUnit, proposalContract.CREDIT_PER_COLLATERAL_UNIT_DENOMINATOR()) + ); + } + +} + + /*----------------------------------------------------------*| |* # ACCEPT PROPOSAL *| |*----------------------------------------------------------*/ @@ -300,7 +317,7 @@ contract PWNSimpleLoanFungibleProposal_AcceptProposal_Test is PWNSimpleLoanFungi category: MultiToken.Category.ERC20, assetAddress: proposal.creditAddress, id: 0, - amount: _creditAmount(proposalValues.collateralAmount, proposal.creditPerCollateralUnit) + amount: proposalContract.getCreditAmount(proposalValues.collateralAmount, proposal.creditPerCollateralUnit) }), fixedInterestAmount: proposal.fixedInterestAmount, accruingInterestAPR: proposal.accruingInterestAPR @@ -443,7 +460,7 @@ contract PWNSimpleLoanFungibleProposal_AcceptRefinanceProposal_Test is PWNSimple category: MultiToken.Category.ERC20, assetAddress: proposal.creditAddress, id: 0, - amount: _creditAmount(proposalValues.collateralAmount, proposal.creditPerCollateralUnit) + amount: proposalContract.getCreditAmount(proposalValues.collateralAmount, proposal.creditPerCollateralUnit) }), fixedInterestAmount: proposal.fixedInterestAmount, accruingInterestAPR: proposal.accruingInterestAPR From a2b12dc21b91fbf58b3bb98c08ee880594f6f3d8 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Tue, 26 Mar 2024 11:18:30 -0400 Subject: [PATCH 059/129] feat(state-fingerprint-computer-registry): move computer registry into pwn config --- src/config/PWNConfig.sol | 48 ++++++- .../PWNSimpleLoanDutchAuctionProposal.sol | 6 +- .../PWNSimpleLoanFungibleProposal.sol | 6 +- .../proposal/PWNSimpleLoanListProposal.sol | 6 +- .../simple/proposal/PWNSimpleLoanProposal.sol | 10 +- .../proposal/PWNSimpleLoanSimpleProposal.sol | 6 +- .../StateFingerprintComputerRegistry.sol | 57 --------- test/unit/PWNConfig.t.sol | 121 +++++++++++++++++- .../PWNSimpleLoanDutchAuctionProposal.t.sol | 2 +- test/unit/PWNSimpleLoanFungibleProposal.t.sol | 2 +- test/unit/PWNSimpleLoanListProposal.t.sol | 2 +- test/unit/PWNSimpleLoanProposal.t.sol | 12 +- test/unit/PWNSimpleLoanSimpleProposal.t.sol | 2 +- .../StateFingerprintComputerRegistry.t.sol | 121 ------------------ 14 files changed, 187 insertions(+), 214 deletions(-) delete mode 100644 src/state-fingerprint/StateFingerprintComputerRegistry.sol delete mode 100644 test/unit/StateFingerprintComputerRegistry.t.sol diff --git a/src/config/PWNConfig.sol b/src/config/PWNConfig.sol index 025e3cd..1730b9f 100644 --- a/src/config/PWNConfig.sol +++ b/src/config/PWNConfig.sol @@ -1,9 +1,11 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; -import "openzeppelin-contracts/contracts/access/Ownable2Step.sol"; -import "openzeppelin-contracts/contracts/proxy/utils/Initializable.sol"; +import { Ownable2Step } from "openzeppelin-contracts/contracts/access/Ownable2Step.sol"; +import { Initializable } from "openzeppelin-contracts/contracts/proxy/utils/Initializable.sol"; +import { ERC165Checker } from "openzeppelin-contracts/contracts/utils/introspection/ERC165Checker.sol"; +import { IERC5646 } from "@pwn/loan/token/IERC5646.sol"; import "@pwn/PWNErrors.sol"; @@ -40,6 +42,11 @@ contract PWNConfig is Ownable2Step, Initializable { */ mapping (address => string) private _loanMetadataUri; + /** + * @notice Mapping holding registered computer to an asset. + * @dev Only owner can update the mapping. + */ + mapping (address => address) private _computerRegistry; /*----------------------------------------------------------*| |* # EVENTS & ERRORS DEFINITIONS *| @@ -65,6 +72,10 @@ contract PWNConfig is Ownable2Step, Initializable { */ event DefaultLOANMetadataUriUpdated(string newUri); + /** + * @notice Error emitted when registering a computer which does not implement the IERC5646 interface. + */ + error InvalidComputerContract(); /*----------------------------------------------------------*| |* # CONSTRUCTOR *| @@ -167,4 +178,37 @@ contract PWNConfig is Ownable2Step, Initializable { uri = _loanMetadataUri[address(0)]; } + + /*----------------------------------------------------------*| + |* # STATE FINGERPRINT COMPUTER *| + |*----------------------------------------------------------*/ + + /** + * @notice Returns the ERC5646 computer for a given asset. + * @param asset The asset for which the computer is requested. + * @return The computer for the given asset. + */ + function getStateFingerprintComputer(address asset) external view returns (IERC5646) { + address computer = _computerRegistry[asset]; + if (computer == address(0)) + if (ERC165Checker.supportsInterface(asset, type(IERC5646).interfaceId)) + computer = asset; + + return IERC5646(computer); + } + + /** + * @notice Registers a state fingerprint computer for a given asset. + * @dev Only owner can register a computer. Computer can be set to address(0) to remove the computer. + * @param asset The asset for which the computer is registered. + * @param computer The computer to be registered. + */ + function registerStateFingerprintComputer(address asset, address computer) external onlyOwner { + if (computer != address(0)) + if (!ERC165Checker.supportsInterface(computer, type(IERC5646).interfaceId)) + revert InvalidComputerContract(); + + _computerRegistry[asset] = computer; + } + } diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol index dcecda2..80b5e53 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol @@ -97,10 +97,8 @@ contract PWNSimpleLoanDutchAuctionProposal is PWNSimpleLoanProposal { constructor( address _hub, address _revokedNonce, - address _stateFingerprintComputerRegistry - ) PWNSimpleLoanProposal( - _hub, _revokedNonce, _stateFingerprintComputerRegistry, "PWNSimpleLoanDutchAuctionProposal", VERSION - ) {} + address _config + ) PWNSimpleLoanProposal(_hub, _revokedNonce, _config, "PWNSimpleLoanDutchAuctionProposal", VERSION) {} /** * @notice Get an proposal hash according to EIP-712 diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol index a02ab90..58b11c0 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol @@ -96,10 +96,8 @@ contract PWNSimpleLoanFungibleProposal is PWNSimpleLoanProposal { constructor( address _hub, address _revokedNonce, - address _stateFingerprintComputerRegistry - ) PWNSimpleLoanProposal( - _hub, _revokedNonce, _stateFingerprintComputerRegistry, "PWNSimpleLoanFungibleProposal", VERSION - ) {} + address _config + ) PWNSimpleLoanProposal(_hub, _revokedNonce, _config, "PWNSimpleLoanFungibleProposal", VERSION) {} /** * @notice Get an proposal hash according to EIP-712 diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol index 6c7db90..7703922 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol @@ -94,10 +94,8 @@ contract PWNSimpleLoanListProposal is PWNSimpleLoanProposal { constructor( address _hub, address _revokedNonce, - address _stateFingerprintComputerRegistry - ) PWNSimpleLoanProposal( - _hub, _revokedNonce, _stateFingerprintComputerRegistry, "PWNSimpleLoanListProposal", VERSION - ) {} + address _config + ) PWNSimpleLoanProposal(_hub, _revokedNonce, _config, "PWNSimpleLoanListProposal", VERSION) {} /** * @notice Get an proposal hash according to EIP-712 diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol index f0a1f54..cbcefa9 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol @@ -1,12 +1,12 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; +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 { PWNRevokedNonce } from "@pwn/nonce/PWNRevokedNonce.sol"; -import { StateFingerprintComputerRegistry, IERC5646 } from "@pwn/state-fingerprint/StateFingerprintComputerRegistry.sol"; import "@pwn/PWNErrors.sol"; /** @@ -22,7 +22,7 @@ abstract contract PWNSimpleLoanProposal { PWNHub public immutable hub; PWNRevokedNonce public immutable revokedNonce; - StateFingerprintComputerRegistry public immutable stateFingerprintComputerRegistry; + PWNConfig public immutable config; /** * @dev Mapping of proposals made via on-chain transactions. @@ -40,13 +40,13 @@ abstract contract PWNSimpleLoanProposal { constructor( address _hub, address _revokedNonce, - address _stateFingerprintComputerRegistry, + address _config, string memory name, string memory version ) { hub = PWNHub(_hub); revokedNonce = PWNRevokedNonce(_revokedNonce); - stateFingerprintComputerRegistry = StateFingerprintComputerRegistry(_stateFingerprintComputerRegistry); + config = PWNConfig(_config); DOMAIN_SEPARATOR = keccak256(abi.encode( keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), @@ -155,7 +155,7 @@ abstract contract PWNSimpleLoanProposal { * @param stateFingerprint Proposed state fingerprint. */ function _checkCollateralState(address addr, uint256 id, bytes32 stateFingerprint) internal view { - IERC5646 computer = stateFingerprintComputerRegistry.getStateFingerprintComputer(addr); + IERC5646 computer = config.getStateFingerprintComputer(addr); if (address(computer) == address(0)) { // Asset is not implementing ERC5646 and no computer is registered revert MissingStateFingerprintComputer(); diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol index fe8c1e4..1415885 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol @@ -79,10 +79,8 @@ contract PWNSimpleLoanSimpleProposal is PWNSimpleLoanProposal { constructor( address _hub, address _revokedNonce, - address _stateFingerprintComputerRegistry - ) PWNSimpleLoanProposal( - _hub, _revokedNonce, _stateFingerprintComputerRegistry, "PWNSimpleLoanSimpleProposal", VERSION - ) {} + address _config + ) PWNSimpleLoanProposal(_hub, _revokedNonce, _config, "PWNSimpleLoanSimpleProposal", VERSION) {} /** * @notice Get an proposal hash according to EIP-712 diff --git a/src/state-fingerprint/StateFingerprintComputerRegistry.sol b/src/state-fingerprint/StateFingerprintComputerRegistry.sol deleted file mode 100644 index d037023..0000000 --- a/src/state-fingerprint/StateFingerprintComputerRegistry.sol +++ /dev/null @@ -1,57 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity 0.8.16; - -import { Ownable2Step } from "openzeppelin-contracts/contracts/access/Ownable2Step.sol"; -import { ERC165Checker } from "openzeppelin-contracts/contracts/utils/introspection/ERC165Checker.sol"; - -import { IERC5646 } from "@pwn/loan/token/IERC5646.sol"; - - -/** - * @title State Fingerprint Computer Registry - * @notice Registry for state fingerprint computers. - * @dev The computers are used to calculate the state fingerprint of an asset. - * It can be a dedicated contract or the asset itself if it implements the IERC5646 interface. - */ -contract StateFingerprintComputerRegistry is Ownable2Step { - - /** - * @notice Error emitted when registering a computer which does not implement the IERC5646 interface. - */ - error InvalidComputerContract(); - - /** - * @notice Mapping holding registered computer to an asset. - * @dev Only owner can update the mapping. - */ - mapping (address => address) private _computerRegistry; - - /** - * @notice Returns the ERC5646 computer for a given asset. - * @param asset The asset for which the computer is requested. - * @return The computer for the given asset. - */ - function getStateFingerprintComputer(address asset) external view returns (IERC5646) { - address computer = _computerRegistry[asset]; - if (computer == address(0)) - if (ERC165Checker.supportsInterface(asset, type(IERC5646).interfaceId)) - computer = asset; - - return IERC5646(computer); - } - - /** - * @notice Registers a state fingerprint computer for a given asset. - * @dev Only owner can register a computer. Computer can be set to address(0) to remove the computer. - * @param asset The asset for which the computer is registered. - * @param computer The computer to be registered. - */ - function registerStateFingerprintComputer(address asset, address computer) external onlyOwner { - if (computer != address(0)) - if (!ERC165Checker.supportsInterface(computer, type(IERC5646).interfaceId)) - revert InvalidComputerContract(); - - _computerRegistry[asset] = computer; - } - -} diff --git a/test/unit/PWNConfig.t.sol b/test/unit/PWNConfig.t.sol index 198bf1c..b935f28 100644 --- a/test/unit/PWNConfig.t.sol +++ b/test/unit/PWNConfig.t.sol @@ -3,7 +3,10 @@ pragma solidity 0.8.16; import "forge-std/Test.sol"; -import "@pwn/config/PWNConfig.sol"; +import { IERC165 } from "openzeppelin-contracts/contracts/utils/introspection/IERC165.sol"; + +import { PWNConfig, IERC5646 } from "@pwn/config/PWNConfig.sol"; +import "@pwn/PWNErrors.sol"; abstract contract PWNConfigTest is Test { @@ -14,10 +17,11 @@ abstract contract PWNConfigTest is Test { bytes32 internal constant FEE_SLOT = bytes32(uint256(1)); // `fee` property position bytes32 internal constant FEE_COLLECTOR_SLOT = bytes32(uint256(2)); // `feeCollector` property position bytes32 internal constant LOAN_METADATA_URI_SLOT = bytes32(uint256(3)); // `loanMetadataUri` mapping position + bytes32 internal constant REGISTRY_SLOT = bytes32(uint256(4)); // `_computerRegistry` mapping position PWNConfig config; - address owner = address(0x43); - address feeCollector = address(0xfeeC001ec704); + address owner = makeAddr("owner"); + address feeCollector = makeAddr("feeCollector"); event FeeUpdated(uint16 oldFee, uint16 newFee); event FeeCollectorUpdated(address oldFeeCollector, address newFeeCollector); @@ -34,6 +38,20 @@ abstract contract PWNConfigTest is Test { vm.store(address(config), FEE_COLLECTOR_SLOT, bytes32(uint256(uint160(feeCollector)))); } + function _mockERC5646Support(address asset, bool result) internal { + _mockERC165Call(asset, type(IERC165).interfaceId, true); + _mockERC165Call(asset, hex"ffffffff", false); + _mockERC165Call(asset, type(IERC5646).interfaceId, result); + } + + function _mockERC165Call(address asset, bytes4 interfaceId, bool result) internal { + vm.mockCall( + asset, + abi.encodeWithSignature("supportsInterface(bytes4)", interfaceId), + abi.encode(result) + ); + } + } @@ -341,3 +359,100 @@ contract PWNConfig_LoanMetadataUri_Test is PWNConfigTest { } } + + +/*----------------------------------------------------------*| +|* # GET STATE FINGERPRINT COMPUTER *| +|*----------------------------------------------------------*/ + +contract PWNConfig_GetStateFingerprintComputer_Test is PWNConfigTest { + + function setUp() override public { + super.setUp(); + + _initialize(); + } + + + function testFuzz_shouldReturnStoredComputer_whenIsRegistered(address asset, address computer) external { + bytes32 assetSlot = keccak256(abi.encode(asset, REGISTRY_SLOT)); + vm.store(address(config), assetSlot, bytes32(uint256(uint160(computer)))); + + assertEq(address(config.getStateFingerprintComputer(asset)), computer); + } + + function testFuzz_shouldReturnAsset_whenComputerIsNotRegistered_whenAssetImplementsERC5646(address asset) external { + assumeAddressIsNot(asset, AddressType.ForgeAddress, AddressType.Precompile); + + _mockERC5646Support(asset, true); + + assertEq(address(config.getStateFingerprintComputer(asset)), asset); + } + + function testFuzz_shouldReturnZeroAddress_whenComputerIsNotRegistered_whenAssetNotImplementsERC5646(address asset) external { + assertEq(address(config.getStateFingerprintComputer(asset)), address(0)); + } + +} + + +/*----------------------------------------------------------*| +|* # REGISTER STATE FINGERPRINT COMPUTER *| +|*----------------------------------------------------------*/ + +contract PWNConfig_RegisterStateFingerprintComputer_Test is PWNConfigTest { + + function setUp() override public { + super.setUp(); + + _initialize(); + } + + + function testFuzz_shouldFail_whenCallerIsNotOwner(address caller) external { + vm.assume(caller != owner); + + vm.expectRevert("Ownable: caller is not the owner"); + vm.prank(caller); + config.registerStateFingerprintComputer(address(0), address(0)); + } + + function testFuzz_shouldUnregisterComputer_whenComputerIsZeroAddress(address asset) external { + address computer = makeAddr("computer"); + bytes32 assetSlot = keccak256(abi.encode(asset, REGISTRY_SLOT)); + vm.store(address(config), assetSlot, bytes32(uint256(uint160(computer)))); + + vm.prank(owner); + config.registerStateFingerprintComputer(asset, address(0)); + + assertEq(address(config.getStateFingerprintComputer(asset)), address(0)); + } + + function testFuzz_shouldFail_whenComputerDoesNotImplementERC165(address asset, address computer) external { + assumeAddressIsNot(computer, AddressType.ForgeAddress, AddressType.Precompile, AddressType.ZeroAddress); + + vm.expectRevert(abi.encodeWithSelector(PWNConfig.InvalidComputerContract.selector)); + vm.prank(owner); + config.registerStateFingerprintComputer(asset, computer); + } + + function testFuzz_shouldFail_whenComputerDoesNotImplementERC5646(address asset, address computer) external { + assumeAddressIsNot(computer, AddressType.ForgeAddress, AddressType.Precompile, AddressType.ZeroAddress); + _mockERC5646Support(computer, false); + + vm.expectRevert(abi.encodeWithSelector(PWNConfig.InvalidComputerContract.selector)); + vm.prank(owner); + config.registerStateFingerprintComputer(asset, computer); + } + + function testFuzz_shouldRegisterComputer(address asset, address computer) external { + assumeAddressIsNot(computer, AddressType.ForgeAddress, AddressType.Precompile, AddressType.ZeroAddress); + _mockERC5646Support(computer, true); + + vm.prank(owner); + config.registerStateFingerprintComputer(asset, computer); + + assertEq(address(config.getStateFingerprintComputer(asset)), computer); + } + +} diff --git a/test/unit/PWNSimpleLoanDutchAuctionProposal.t.sol b/test/unit/PWNSimpleLoanDutchAuctionProposal.t.sol index f95eddb..e744f35 100644 --- a/test/unit/PWNSimpleLoanDutchAuctionProposal.t.sol +++ b/test/unit/PWNSimpleLoanDutchAuctionProposal.t.sol @@ -32,7 +32,7 @@ abstract contract PWNSimpleLoanDutchAuctionProposalTest is PWNSimpleLoanProposal function setUp() virtual public override { super.setUp(); - proposalContract = new PWNSimpleLoanDutchAuctionProposal(hub, revokedNonce, stateFingerprintComputerRegistry); + proposalContract = new PWNSimpleLoanDutchAuctionProposal(hub, revokedNonce, config); proposalContractAddr = PWNSimpleLoanProposal(proposalContract); proposal = PWNSimpleLoanDutchAuctionProposal.Proposal({ diff --git a/test/unit/PWNSimpleLoanFungibleProposal.t.sol b/test/unit/PWNSimpleLoanFungibleProposal.t.sol index ed0dfd5..b8b011e 100644 --- a/test/unit/PWNSimpleLoanFungibleProposal.t.sol +++ b/test/unit/PWNSimpleLoanFungibleProposal.t.sol @@ -32,7 +32,7 @@ abstract contract PWNSimpleLoanFungibleProposalTest is PWNSimpleLoanProposalTest function setUp() virtual public override { super.setUp(); - proposalContract = new PWNSimpleLoanFungibleProposal(hub, revokedNonce, stateFingerprintComputerRegistry); + proposalContract = new PWNSimpleLoanFungibleProposal(hub, revokedNonce, config); proposalContractAddr = PWNSimpleLoanProposal(proposalContract); proposal = PWNSimpleLoanFungibleProposal.Proposal({ diff --git a/test/unit/PWNSimpleLoanListProposal.t.sol b/test/unit/PWNSimpleLoanListProposal.t.sol index 136a65e..c2f97ba 100644 --- a/test/unit/PWNSimpleLoanListProposal.t.sol +++ b/test/unit/PWNSimpleLoanListProposal.t.sol @@ -32,7 +32,7 @@ abstract contract PWNSimpleLoanListProposalTest is PWNSimpleLoanProposalTest { function setUp() virtual public override { super.setUp(); - proposalContract = new PWNSimpleLoanListProposal(hub, revokedNonce, stateFingerprintComputerRegistry); + proposalContract = new PWNSimpleLoanListProposal(hub, revokedNonce, config); proposalContractAddr = PWNSimpleLoanProposal(proposalContract); proposal = PWNSimpleLoanListProposal.Proposal({ diff --git a/test/unit/PWNSimpleLoanProposal.t.sol b/test/unit/PWNSimpleLoanProposal.t.sol index c3e3e5e..e229a31 100644 --- a/test/unit/PWNSimpleLoanProposal.t.sol +++ b/test/unit/PWNSimpleLoanProposal.t.sol @@ -20,7 +20,7 @@ abstract contract PWNSimpleLoanProposalTest is Test { address public hub = makeAddr("hub"); address public revokedNonce = makeAddr("revokedNonce"); - address public stateFingerprintComputerRegistry = makeAddr("stateFingerprintComputerRegistry"); + address public config = makeAddr("config"); address public stateFingerprintComputer = makeAddr("stateFingerprintComputer"); address public activeLoanContract = makeAddr("activeLoanContract"); address public token = makeAddr("token"); @@ -84,7 +84,7 @@ abstract contract PWNSimpleLoanProposalTest is Test { ); vm.mockCall( - stateFingerprintComputerRegistry, + config, abi.encodeWithSignature("getStateFingerprintComputer(address)"), abi.encode(stateFingerprintComputer) ); @@ -153,7 +153,7 @@ abstract contract PWNSimpleLoanProposal_AcceptProposal_Test is PWNSimpleLoanProp params.checkCollateralStateFingerprint = false; vm.expectCall({ - callee: stateFingerprintComputerRegistry, + callee: config, data: abi.encodeWithSignature("getStateFingerprintComputer(address)"), count: 0 }); @@ -163,7 +163,7 @@ abstract contract PWNSimpleLoanProposal_AcceptProposal_Test is PWNSimpleLoanProp function test_shouldFail_whenComputerRegistryReturnsZeroAddress_whenShouldCheckStateFingerprint() external { vm.mockCall( - stateFingerprintComputerRegistry, + config, abi.encodeWithSignature("getStateFingerprintComputer(address)", token), // test expects `token` being used as collateral asset abi.encode(address(0)) ); @@ -426,7 +426,7 @@ abstract contract PWNSimpleLoanProposal_AcceptRefinanceProposal_Test is PWNSimpl params.checkCollateralStateFingerprint = false; vm.expectCall({ - callee: stateFingerprintComputerRegistry, + callee: config, data: abi.encodeWithSignature("getStateFingerprintComputer(address)"), count: 0 }); @@ -436,7 +436,7 @@ abstract contract PWNSimpleLoanProposal_AcceptRefinanceProposal_Test is PWNSimpl function test_shouldFail_whenComputerRegistryReturnsZeroAddress_whenShouldCheckStateFingerprint() external { vm.mockCall( - stateFingerprintComputerRegistry, + config, abi.encodeWithSignature("getStateFingerprintComputer(address)", token), // test expects `token` being used as collateral asset abi.encode(address(0)) ); diff --git a/test/unit/PWNSimpleLoanSimpleProposal.t.sol b/test/unit/PWNSimpleLoanSimpleProposal.t.sol index 8eb8c79..c7e6e86 100644 --- a/test/unit/PWNSimpleLoanSimpleProposal.t.sol +++ b/test/unit/PWNSimpleLoanSimpleProposal.t.sol @@ -29,7 +29,7 @@ abstract contract PWNSimpleLoanSimpleProposalTest is PWNSimpleLoanProposalTest { function setUp() virtual public override { super.setUp(); - proposalContract = new PWNSimpleLoanSimpleProposal(hub, revokedNonce, stateFingerprintComputerRegistry); + proposalContract = new PWNSimpleLoanSimpleProposal(hub, revokedNonce, config); proposalContractAddr = PWNSimpleLoanProposal(proposalContract); proposal = PWNSimpleLoanSimpleProposal.Proposal({ diff --git a/test/unit/StateFingerprintComputerRegistry.t.sol b/test/unit/StateFingerprintComputerRegistry.t.sol deleted file mode 100644 index 0e5d6b2..0000000 --- a/test/unit/StateFingerprintComputerRegistry.t.sol +++ /dev/null @@ -1,121 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity 0.8.16; - -import "forge-std/Test.sol"; - -import { IERC165 } from "openzeppelin-contracts/contracts/utils/introspection/IERC165.sol"; - -import { StateFingerprintComputerRegistry, IERC5646 } from "@pwn/state-fingerprint/StateFingerprintComputerRegistry.sol"; - - -abstract contract StateFingerprintComputerRegistryTest is Test { - - bytes32 internal constant OWNER_SLOT = bytes32(uint256(0)); - bytes32 internal constant REGISTRY_SLOT = bytes32(uint256(2)); - - address owner = makeAddr("owner"); - StateFingerprintComputerRegistry registry; - - function setUp() external { - vm.prank(owner); - registry = new StateFingerprintComputerRegistry(); - } - - function _mockERC5646Support(address asset, bool result) internal { - _mockERC165Call(asset, type(IERC165).interfaceId, true); - _mockERC165Call(asset, hex"ffffffff", false); - _mockERC165Call(asset, type(IERC5646).interfaceId, result); - } - - function _mockERC165Call(address asset, bytes4 interfaceId, bool result) internal { - vm.mockCall( - asset, - abi.encodeWithSignature("supportsInterface(bytes4)", interfaceId), - abi.encode(result) - ); - } - -} - - -/*----------------------------------------------------------*| -|* # GET STATE FINGERPRINT COMPUTER *| -|*----------------------------------------------------------*/ - -contract StateFingerprintComputerRegistry_GetStateFingerprintComputer_Test is StateFingerprintComputerRegistryTest { - - function testFuzz_shouldReturnStoredComputer_whenIsRegistered(address asset, address computer) external { - bytes32 assetSlot = keccak256(abi.encode(asset, REGISTRY_SLOT)); - vm.store(address(registry), assetSlot, bytes32(uint256(uint160(computer)))); - - assertEq(address(registry.getStateFingerprintComputer(asset)), computer); - } - - function testFuzz_shouldReturnAsset_whenComputerIsNotRegistered_whenAssetImplementsERC5646(address asset) external { - assumeAddressIsNot(asset, AddressType.ForgeAddress, AddressType.Precompile); - - _mockERC5646Support(asset, true); - - assertEq(address(registry.getStateFingerprintComputer(asset)), asset); - } - - function testFuzz_shouldReturnZeroAddress_whenComputerIsNotRegistered_whenAssetNotImplementsERC5646(address asset) external { - assertEq(address(registry.getStateFingerprintComputer(asset)), address(0)); - } - -} - - -/*----------------------------------------------------------*| -|* # REGISTER STATE FINGERPRINT COMPUTER *| -|*----------------------------------------------------------*/ - -contract StateFingerprintComputerRegistry_RegisterStateFingerprintComputer_Test is StateFingerprintComputerRegistryTest { - - function testFuzz_shouldFail_whenCallerIsNotOwner(address caller) external { - vm.assume(caller != owner); - - vm.expectRevert("Ownable: caller is not the owner"); - vm.prank(caller); - registry.registerStateFingerprintComputer(address(0), address(0)); - } - - function testFuzz_shouldUnregisterComputer_whenComputerIsZeroAddress(address asset) external { - address computer = makeAddr("computer"); - bytes32 assetSlot = keccak256(abi.encode(asset, REGISTRY_SLOT)); - vm.store(address(registry), assetSlot, bytes32(uint256(uint160(computer)))); - - vm.prank(owner); - registry.registerStateFingerprintComputer(asset, address(0)); - - assertEq(address(registry.getStateFingerprintComputer(asset)), address(0)); - } - - function testFuzz_shouldFail_whenComputerDoesNotImplementERC165(address asset, address computer) external { - assumeAddressIsNot(computer, AddressType.ForgeAddress, AddressType.Precompile, AddressType.ZeroAddress); - - vm.expectRevert(abi.encodeWithSelector(StateFingerprintComputerRegistry.InvalidComputerContract.selector)); - vm.prank(owner); - registry.registerStateFingerprintComputer(asset, computer); - } - - function testFuzz_shouldFail_whenComputerDoesNotImplementERC5646(address asset, address computer) external { - assumeAddressIsNot(computer, AddressType.ForgeAddress, AddressType.Precompile, AddressType.ZeroAddress); - _mockERC5646Support(computer, false); - - vm.expectRevert(abi.encodeWithSelector(StateFingerprintComputerRegistry.InvalidComputerContract.selector)); - vm.prank(owner); - registry.registerStateFingerprintComputer(asset, computer); - } - - function testFuzz_shouldRegisterComputer(address asset, address computer) external { - assumeAddressIsNot(computer, AddressType.ForgeAddress, AddressType.Precompile, AddressType.ZeroAddress); - _mockERC5646Support(computer, true); - - vm.prank(owner); - registry.registerStateFingerprintComputer(asset, computer); - - assertEq(address(registry.getStateFingerprintComputer(asset)), computer); - } - -} From afe2cb86ab4ef67c4d1172f60064394fa9fa7226 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Wed, 27 Mar 2024 09:33:28 -0400 Subject: [PATCH 060/129] script: update deployment scripts --- deployments/latest.json | 25 ++ deployments.json => deployments/v1.1.json | 223 ++++++------ foundry.toml | 2 +- script/PWN.s.sol | 422 +++++++++++++++++----- script/PWNTimelock.s.sol | 80 ++-- src/Deployments.sol | 49 +-- test/helper/DeploymentTest.t.sol | 96 +++-- 7 files changed, 588 insertions(+), 309 deletions(-) create mode 100644 deployments/latest.json rename deployments.json => deployments/v1.1.json (51%) diff --git a/deployments/latest.json b/deployments/latest.json new file mode 100644 index 0000000..19ccbbc --- /dev/null +++ b/deployments/latest.json @@ -0,0 +1,25 @@ +{ + "deployedChains": [1], + "chains": { + "1": { + "dao": "0x1B8383D2726E7e18189205337424a2631A2102F4", + "productTimelock": "0xd57e72A328AB1deC6b374c2babe2dc489819B5Ea", + "protocolTimelock": "0xd8dbdDf1c0FDdf9b5eCFA5C067C38DB66739FBAB", + "protocolSafe": "0x61a77B19b7F4dB82222625D7a969698894d77473", + "daoSafe": "0xd56635c0E91D31F88B89F195D3993a9e34516e59", + "deployerSafe": "0x42Cad20c964067f8e8b5c3E13fd0aa3C20a964C4", + "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", + "categoryRegistry": "0x0c93666Bf6359951Ade361D6E19f2DB240dA392f", + "configSingleton": "0x1E53eC63395576d23770b00E86053aBf0b9a3a21", + "config": "0x9C432a1A229ef87b138da29dE930B3d8EC2C67Fa", + "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", + "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", + "revokedNonce": "0x32353ecC81dE2d8c710a5295B96c0d315a0bb563", + "simpleLoan": "0xBE8A45A4d82A0D7181E6A37E13aE22143336D7DE", + "simpleLoanSimpleProposal": "0x430CC773C5C4931518CFeFf8d8563e01A8a1Da99", + "simpleLoanListProposal": "0xFFcC4E9D8CDefb34Fa80A3DD85712EfF6071a768", + "simpleLoanFungibleProposal": "0x036bcD053344Cf34ef33efa81cCA832AE998336d", + "simpleLoanDutchAuctionProposal": "0x3ecde871cB34231F946136EfF5010Bcc4800FA75" + } + } +} diff --git a/deployments.json b/deployments/v1.1.json similarity index 51% rename from deployments.json rename to deployments/v1.1.json index 184f569..f8c7469 100644 --- a/deployments.json +++ b/deployments/v1.1.json @@ -10,17 +10,16 @@ "feeCollector": "0xd56635c0E91D31F88B89F195D3993a9e34516e59", "deployerSafe": "0x42Cad20c964067f8e8b5c3E13fd0aa3C20a964C4", "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", - "configSingleton": "0x0000000000000000000000000000000000000000", - "config": "0x0000000000000000000000000000000000000000", + "configSingleton": "0x9926651f452ac52c851Ca91c4c79C2B3CdF4F7cD", + "config": "0x03DeAfC9678ab25F059df59Be3B20875018e1d46", "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", - "revokedNonce": "0x0000000000000000000000000000000000000000", - "simpleLoan": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", - "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", - "stateFingerprintComputerRegistry": "0x0000000000000000000000000000000000000000", - "categoryRegistry": "0x0000000000000000000000000000000000000000" + "revokedOfferNonce": "0xFFa73Eacce930BBd92a1Ef218400cBd1036c437e", + "revokedRequestNonce": "0x472361E75d28597b0a7F86146fbB4a86f173d10D", + "simpleLoan": "0x57c88D78f6D08b5c88b4A3b7BbB0C1AA34c3280A", + "simpleLoanSimpleOffer": "0x5E551f09b8d1353075A1FF3B484Ee688aCAc02F6", + "simpleLoanListOffer": "0xDA027058708961Be3676daEB68Fde1758B210065", + "simpleLoanSimpleRequest": "0x9Cb87eC6448299aBc326F32d60E191Ef32Ab225D" }, "5": { "dao": "0x0000000000000000000000000000000000000000", @@ -31,17 +30,16 @@ "feeCollector": "0xd56635c0E91D31F88B89F195D3993a9e34516e59", "deployerSafe": "0x42Cad20c964067f8e8b5c3E13fd0aa3C20a964C4", "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", - "configSingleton": "0x0000000000000000000000000000000000000000", - "config": "0x0000000000000000000000000000000000000000", + "configSingleton": "0x9926651f452ac52c851Ca91c4c79C2B3CdF4F7cD", + "config": "0x03DeAfC9678ab25F059df59Be3B20875018e1d46", "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", - "revokedNonce": "0x0000000000000000000000000000000000000000", - "simpleLoan": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", - "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", - "stateFingerprintComputerRegistry": "0x0000000000000000000000000000000000000000", - "categoryRegistry": "0x0000000000000000000000000000000000000000" + "revokedOfferNonce": "0xFFa73Eacce930BBd92a1Ef218400cBd1036c437e", + "revokedRequestNonce": "0x472361E75d28597b0a7F86146fbB4a86f173d10D", + "simpleLoan": "0x57c88D78f6D08b5c88b4A3b7BbB0C1AA34c3280A", + "simpleLoanSimpleOffer": "0x5E551f09b8d1353075A1FF3B484Ee688aCAc02F6", + "simpleLoanListOffer": "0xDA027058708961Be3676daEB68Fde1758B210065", + "simpleLoanSimpleRequest": "0x9Cb87eC6448299aBc326F32d60E191Ef32Ab225D" }, "10": { "dao": "0x0000000000000000000000000000000000000000", @@ -52,17 +50,16 @@ "feeCollector": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", "deployerSafe": "0x1B4B37738De3bb9E6a7a4f99aFe4C145734c071d", "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", - "configSingleton": "0x0000000000000000000000000000000000000000", - "config": "0x0000000000000000000000000000000000000000", + "configSingleton": "0x9926651f452ac52c851Ca91c4c79C2B3CdF4F7cD", + "config": "0x55e6A9F4183CFC01ecE8f2258FC13b93e1B6c140", "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", - "revokedNonce": "0x0000000000000000000000000000000000000000", - "simpleLoan": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", - "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", - "stateFingerprintComputerRegistry": "0x0000000000000000000000000000000000000000", - "categoryRegistry": "0x0000000000000000000000000000000000000000" + "revokedOfferNonce": "0xFFa73Eacce930BBd92a1Ef218400cBd1036c437e", + "revokedRequestNonce": "0x472361E75d28597b0a7F86146fbB4a86f173d10D", + "simpleLoan": "0x4188C513fd94B0458715287570c832d9560bc08a", + "simpleLoanSimpleOffer": "0x5E551f09b8d1353075A1FF3B484Ee688aCAc02F6", + "simpleLoanListOffer": "0xDA027058708961Be3676daEB68Fde1758B210065", + "simpleLoanSimpleRequest": "0x9Cb87eC6448299aBc326F32d60E191Ef32Ab225D" }, "25": { "dao": "0x0000000000000000000000000000000000000000", @@ -73,17 +70,16 @@ "feeCollector": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", "deployerSafe": "0x1B4B37738De3bb9E6a7a4f99aFe4C145734c071d", "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", - "configSingleton": "0x0000000000000000000000000000000000000000", - "config": "0x0000000000000000000000000000000000000000", + "configSingleton": "0x9926651f452ac52c851Ca91c4c79C2B3CdF4F7cD", + "config": "0x55e6A9F4183CFC01ecE8f2258FC13b93e1B6c140", "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", - "revokedNonce": "0x0000000000000000000000000000000000000000", - "simpleLoan": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", - "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", - "stateFingerprintComputerRegistry": "0x0000000000000000000000000000000000000000", - "categoryRegistry": "0x0000000000000000000000000000000000000000" + "revokedOfferNonce": "0xFFa73Eacce930BBd92a1Ef218400cBd1036c437e", + "revokedRequestNonce": "0x472361E75d28597b0a7F86146fbB4a86f173d10D", + "simpleLoan": "0x4188C513fd94B0458715287570c832d9560bc08a", + "simpleLoanSimpleOffer": "0x5E551f09b8d1353075A1FF3B484Ee688aCAc02F6", + "simpleLoanListOffer": "0xDA027058708961Be3676daEB68Fde1758B210065", + "simpleLoanSimpleRequest": "0x9Cb87eC6448299aBc326F32d60E191Ef32Ab225D" }, "56": { "dao": "0x0000000000000000000000000000000000000000", @@ -94,17 +90,16 @@ "feeCollector": "0xd56635c0E91D31F88B89F195D3993a9e34516e59", "deployerSafe": "0x42Cad20c964067f8e8b5c3E13fd0aa3C20a964C4", "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", - "configSingleton": "0x0000000000000000000000000000000000000000", - "config": "0x0000000000000000000000000000000000000000", + "configSingleton": "0x9926651f452ac52c851Ca91c4c79C2B3CdF4F7cD", + "config": "0x03DeAfC9678ab25F059df59Be3B20875018e1d46", "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", - "revokedNonce": "0x0000000000000000000000000000000000000000", - "simpleLoan": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", - "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", - "stateFingerprintComputerRegistry": "0x0000000000000000000000000000000000000000", - "categoryRegistry": "0x0000000000000000000000000000000000000000" + "revokedOfferNonce": "0xFFa73Eacce930BBd92a1Ef218400cBd1036c437e", + "revokedRequestNonce": "0x472361E75d28597b0a7F86146fbB4a86f173d10D", + "simpleLoan": "0x57c88D78f6D08b5c88b4A3b7BbB0C1AA34c3280A", + "simpleLoanSimpleOffer": "0x5E551f09b8d1353075A1FF3B484Ee688aCAc02F6", + "simpleLoanListOffer": "0xDA027058708961Be3676daEB68Fde1758B210065", + "simpleLoanSimpleRequest": "0x9Cb87eC6448299aBc326F32d60E191Ef32Ab225D" }, "137": { "dao": "0x0000000000000000000000000000000000000000", @@ -115,17 +110,16 @@ "feeCollector": "0xd56635c0E91D31F88B89F195D3993a9e34516e59", "deployerSafe": "0x42Cad20c964067f8e8b5c3E13fd0aa3C20a964C4", "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", - "configSingleton": "0x0000000000000000000000000000000000000000", - "config": "0x0000000000000000000000000000000000000000", + "configSingleton": "0x9926651f452ac52c851Ca91c4c79C2B3CdF4F7cD", + "config": "0x03DeAfC9678ab25F059df59Be3B20875018e1d46", "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", - "revokedNonce": "0x0000000000000000000000000000000000000000", - "simpleLoan": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", - "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", - "stateFingerprintComputerRegistry": "0x0000000000000000000000000000000000000000", - "categoryRegistry": "0x0000000000000000000000000000000000000000" + "revokedOfferNonce": "0xFFa73Eacce930BBd92a1Ef218400cBd1036c437e", + "revokedRequestNonce": "0x472361E75d28597b0a7F86146fbB4a86f173d10D", + "simpleLoan": "0x57c88D78f6D08b5c88b4A3b7BbB0C1AA34c3280A", + "simpleLoanSimpleOffer": "0x5E551f09b8d1353075A1FF3B484Ee688aCAc02F6", + "simpleLoanListOffer": "0xDA027058708961Be3676daEB68Fde1758B210065", + "simpleLoanSimpleRequest": "0x9Cb87eC6448299aBc326F32d60E191Ef32Ab225D" }, "338": { "dao": "0x0000000000000000000000000000000000000000", @@ -136,17 +130,16 @@ "feeCollector": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", "deployerSafe": "0x0000000000000000000000000000000000000000", "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", - "configSingleton": "0x0000000000000000000000000000000000000000", - "config": "0x0000000000000000000000000000000000000000", + "configSingleton": "0x9926651f452ac52c851Ca91c4c79C2B3CdF4F7cD", + "config": "0x55e6A9F4183CFC01ecE8f2258FC13b93e1B6c140", "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", - "revokedNonce": "0x0000000000000000000000000000000000000000", - "simpleLoan": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", - "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", - "stateFingerprintComputerRegistry": "0x0000000000000000000000000000000000000000", - "categoryRegistry": "0x0000000000000000000000000000000000000000" + "revokedOfferNonce": "0xFFa73Eacce930BBd92a1Ef218400cBd1036c437e", + "revokedRequestNonce": "0x472361E75d28597b0a7F86146fbB4a86f173d10D", + "simpleLoan": "0x4188C513fd94B0458715287570c832d9560bc08a", + "simpleLoanSimpleOffer": "0x5E551f09b8d1353075A1FF3B484Ee688aCAc02F6", + "simpleLoanListOffer": "0xDA027058708961Be3676daEB68Fde1758B210065", + "simpleLoanSimpleRequest": "0x9Cb87eC6448299aBc326F32d60E191Ef32Ab225D" }, "5000": { "dao": "0x0000000000000000000000000000000000000000", @@ -157,17 +150,16 @@ "feeCollector": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", "deployerSafe": "0x1B4B37738De3bb9E6a7a4f99aFe4C145734c071d", "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", - "configSingleton": "0x0000000000000000000000000000000000000000", - "config": "0x0000000000000000000000000000000000000000", + "configSingleton": "0x9926651f452ac52c851Ca91c4c79C2B3CdF4F7cD", + "config": "0x55e6A9F4183CFC01ecE8f2258FC13b93e1B6c140", "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", - "revokedNonce": "0x0000000000000000000000000000000000000000", - "simpleLoan": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", - "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", - "stateFingerprintComputerRegistry": "0x0000000000000000000000000000000000000000", - "categoryRegistry": "0x0000000000000000000000000000000000000000" + "revokedOfferNonce": "0xFFa73Eacce930BBd92a1Ef218400cBd1036c437e", + "revokedRequestNonce": "0x472361E75d28597b0a7F86146fbB4a86f173d10D", + "simpleLoan": "0x4188C513fd94B0458715287570c832d9560bc08a", + "simpleLoanSimpleOffer": "0x5E551f09b8d1353075A1FF3B484Ee688aCAc02F6", + "simpleLoanListOffer": "0xDA027058708961Be3676daEB68Fde1758B210065", + "simpleLoanSimpleRequest": "0x9Cb87eC6448299aBc326F32d60E191Ef32Ab225D" }, "5001": { "dao": "0x0000000000000000000000000000000000000000", @@ -178,17 +170,16 @@ "feeCollector": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", "deployerSafe": "0x0000000000000000000000000000000000000000", "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", - "configSingleton": "0x0000000000000000000000000000000000000000", - "config": "0x0000000000000000000000000000000000000000", + "configSingleton": "0x9926651f452ac52c851Ca91c4c79C2B3CdF4F7cD", + "config": "0x55e6A9F4183CFC01ecE8f2258FC13b93e1B6c140", "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", - "revokedNonce": "0x0000000000000000000000000000000000000000", - "simpleLoan": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", - "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", - "stateFingerprintComputerRegistry": "0x0000000000000000000000000000000000000000", - "categoryRegistry": "0x0000000000000000000000000000000000000000" + "revokedOfferNonce": "0xFFa73Eacce930BBd92a1Ef218400cBd1036c437e", + "revokedRequestNonce": "0x472361E75d28597b0a7F86146fbB4a86f173d10D", + "simpleLoan": "0x4188C513fd94B0458715287570c832d9560bc08a", + "simpleLoanSimpleOffer": "0x5E551f09b8d1353075A1FF3B484Ee688aCAc02F6", + "simpleLoanListOffer": "0xDA027058708961Be3676daEB68Fde1758B210065", + "simpleLoanSimpleRequest": "0x9Cb87eC6448299aBc326F32d60E191Ef32Ab225D" }, "8453": { "dao": "0x0000000000000000000000000000000000000000", @@ -199,17 +190,16 @@ "feeCollector": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", "deployerSafe": "0x1B4B37738De3bb9E6a7a4f99aFe4C145734c071d", "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", - "configSingleton": "0x0000000000000000000000000000000000000000", - "config": "0x0000000000000000000000000000000000000000", + "configSingleton": "0x9926651f452ac52c851Ca91c4c79C2B3CdF4F7cD", + "config": "0x55e6A9F4183CFC01ecE8f2258FC13b93e1B6c140", "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", - "revokedNonce": "0x0000000000000000000000000000000000000000", - "simpleLoan": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", - "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", - "stateFingerprintComputerRegistry": "0x0000000000000000000000000000000000000000", - "categoryRegistry": "0x0000000000000000000000000000000000000000" + "revokedOfferNonce": "0xFFa73Eacce930BBd92a1Ef218400cBd1036c437e", + "revokedRequestNonce": "0x472361E75d28597b0a7F86146fbB4a86f173d10D", + "simpleLoan": "0x4188C513fd94B0458715287570c832d9560bc08a", + "simpleLoanSimpleOffer": "0x5E551f09b8d1353075A1FF3B484Ee688aCAc02F6", + "simpleLoanListOffer": "0xDA027058708961Be3676daEB68Fde1758B210065", + "simpleLoanSimpleRequest": "0x9Cb87eC6448299aBc326F32d60E191Ef32Ab225D" }, "42161": { "dao": "0x0000000000000000000000000000000000000000", @@ -220,17 +210,16 @@ "feeCollector": "0xd56635c0E91D31F88B89F195D3993a9e34516e59", "deployerSafe": "0x42Cad20c964067f8e8b5c3E13fd0aa3C20a964C4", "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", - "configSingleton": "0x0000000000000000000000000000000000000000", - "config": "0x0000000000000000000000000000000000000000", + "configSingleton": "0x9926651f452ac52c851Ca91c4c79C2B3CdF4F7cD", + "config": "0x03DeAfC9678ab25F059df59Be3B20875018e1d46", "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", - "revokedNonce": "0x0000000000000000000000000000000000000000", - "simpleLoan": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", - "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", - "stateFingerprintComputerRegistry": "0x0000000000000000000000000000000000000000", - "categoryRegistry": "0x0000000000000000000000000000000000000000" + "revokedOfferNonce": "0xFFa73Eacce930BBd92a1Ef218400cBd1036c437e", + "revokedRequestNonce": "0x472361E75d28597b0a7F86146fbB4a86f173d10D", + "simpleLoan": "0x57c88D78f6D08b5c88b4A3b7BbB0C1AA34c3280A", + "simpleLoanSimpleOffer": "0x5E551f09b8d1353075A1FF3B484Ee688aCAc02F6", + "simpleLoanListOffer": "0xDA027058708961Be3676daEB68Fde1758B210065", + "simpleLoanSimpleRequest": "0x9Cb87eC6448299aBc326F32d60E191Ef32Ab225D" }, "84531": { "dao": "0x0000000000000000000000000000000000000000", @@ -241,17 +230,16 @@ "feeCollector": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", "deployerSafe": "0x0000000000000000000000000000000000000000", "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", - "configSingleton": "0x0000000000000000000000000000000000000000", - "config": "0x0000000000000000000000000000000000000000", + "configSingleton": "0x9926651f452ac52c851Ca91c4c79C2B3CdF4F7cD", + "config": "0x55e6A9F4183CFC01ecE8f2258FC13b93e1B6c140", "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", - "revokedNonce": "0x0000000000000000000000000000000000000000", - "simpleLoan": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", - "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", - "stateFingerprintComputerRegistry": "0x0000000000000000000000000000000000000000", - "categoryRegistry": "0x0000000000000000000000000000000000000000" + "revokedOfferNonce": "0xFFa73Eacce930BBd92a1Ef218400cBd1036c437e", + "revokedRequestNonce": "0x472361E75d28597b0a7F86146fbB4a86f173d10D", + "simpleLoan": "0x4188C513fd94B0458715287570c832d9560bc08a", + "simpleLoanSimpleOffer": "0x5E551f09b8d1353075A1FF3B484Ee688aCAc02F6", + "simpleLoanListOffer": "0xDA027058708961Be3676daEB68Fde1758B210065", + "simpleLoanSimpleRequest": "0x9Cb87eC6448299aBc326F32d60E191Ef32Ab225D" }, "11155111": { "dao": "0x0000000000000000000000000000000000000000", @@ -262,17 +250,16 @@ "feeCollector": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", "deployerSafe": "0x1B4B37738De3bb9E6a7a4f99aFe4C145734c071d", "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", - "configSingleton": "0x0000000000000000000000000000000000000000", - "config": "0x0000000000000000000000000000000000000000", + "configSingleton": "0x9926651f452ac52c851Ca91c4c79C2B3CdF4F7cD", + "config": "0x55e6A9F4183CFC01ecE8f2258FC13b93e1B6c140", "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", - "revokedNonce": "0x0000000000000000000000000000000000000000", - "simpleLoan": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", - "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", - "stateFingerprintComputerRegistry": "0x0000000000000000000000000000000000000000", - "categoryRegistry": "0x0000000000000000000000000000000000000000" + "revokedOfferNonce": "0xFFa73Eacce930BBd92a1Ef218400cBd1036c437e", + "revokedRequestNonce": "0x472361E75d28597b0a7F86146fbB4a86f173d10D", + "simpleLoan": "0x4188C513fd94B0458715287570c832d9560bc08a", + "simpleLoanSimpleOffer": "0x5E551f09b8d1353075A1FF3B484Ee688aCAc02F6", + "simpleLoanListOffer": "0xDA027058708961Be3676daEB68Fde1758B210065", + "simpleLoanSimpleRequest": "0x9Cb87eC6448299aBc326F32d60E191Ef32Ab225D" } } -} +} \ No newline at end of file diff --git a/foundry.toml b/foundry.toml index 6418914..f9f046a 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,6 +1,6 @@ [profile.default] solc_version = '0.8.16' -fs_permissions = [{ access = "read", path = "./deployments.json"}] +fs_permissions = [{ access = "read", path = "./deployments/latest.json"}] [rpc_endpoints] diff --git a/script/PWN.s.sol b/script/PWN.s.sol index d2dd958..32245a5 100644 --- a/script/PWN.s.sol +++ b/script/PWN.s.sol @@ -4,7 +4,9 @@ pragma solidity 0.8.16; import "forge-std/Script.sol"; import { TransparentUpgradeableProxy, ITransparentUpgradeableProxy } -from "openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + from "openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +import { MultiTokenCategoryRegistry } from "MultiToken/MultiTokenCategoryRegistry.sol"; import { GnosisSafeLike, GnosisSafeUtils } from "./lib/GnosisSafeUtils.sol"; @@ -13,8 +15,10 @@ 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 { PWNSimpleLoanListProposal } from "@pwn/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol"; import { PWNSimpleLoanSimpleProposal } from "@pwn/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol"; +import { PWNSimpleLoanListProposal } from "@pwn/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol"; +import { PWNSimpleLoanFungibleProposal } from "@pwn/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol"; +import { PWNSimpleLoanDutchAuctionProposal } from "@pwn/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol"; import { PWNLOAN } from "@pwn/loan/token/PWNLOAN.sol"; import { PWNRevokedNonce } from "@pwn/nonce/PWNRevokedNonce.sol"; import { Deployments } from "@pwn/Deployments.sol"; @@ -29,7 +33,7 @@ library PWNContractDeployerSalt { string internal constant VERSION = "1.2"; // Singletons - bytes32 internal constant CONFIG_V1 = keccak256("PWNConfigV1"); + bytes32 internal constant CONFIG = keccak256("PWNConfig"); bytes32 internal constant CONFIG_PROXY = keccak256("PWNConfigProxy"); bytes32 internal constant HUB = keccak256("PWNHub"); bytes32 internal constant LOAN = keccak256("PWNLOAN"); @@ -42,6 +46,7 @@ library PWNContractDeployerSalt { 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"); + bytes32 internal constant SIMPLE_LOAN_DUTCH_AUCTION_PROPOSAL = keccak256("PWNSimpleLoanDutchAuctionProposal"); } @@ -50,7 +55,7 @@ contract Deploy is Deployments, Script { using GnosisSafeUtils for GnosisSafeLike; function _protocolNotDeployedOnSelectedChain() internal pure override { - revert("PWN: selected chain is not set in deployments.json"); + revert("PWN: selected chain is not set in deployments/latest.json"); } function _deployAndTransferOwnership( @@ -58,28 +63,181 @@ contract Deploy is Deployments, Script { address owner, bytes memory bytecode ) internal returns (address) { - bool success = GnosisSafeLike(deployerSafe).execTransaction({ - to: address(deployer), + bool success = GnosisSafeLike(deployment.deployerSafe).execTransaction({ + to: address(deployment.deployer), data: abi.encodeWithSelector( IPWNDeployer.deployAndTransferOwnership.selector, salt, owner, bytecode ) }); require(success, "Deploy failed"); - return deployer.computeAddress(salt, keccak256(bytecode)); + return deployment.deployer.computeAddress(salt, keccak256(bytecode)); } function _deploy( bytes32 salt, bytes memory bytecode ) internal returns (address) { - bool success = GnosisSafeLike(deployerSafe).execTransaction({ - to: address(deployer), + bool success = GnosisSafeLike(deployment.deployerSafe).execTransaction({ + to: address(deployment.deployer), data: abi.encodeWithSelector( IPWNDeployer.deploy.selector, salt, bytecode ) }); require(success, "Deploy failed"); - return deployer.computeAddress(salt, keccak256(bytecode)); + return deployment.deployer.computeAddress(salt, keccak256(bytecode)); + } + +/* +forge script script/PWN.s.sol:Deploy \ +--sig "deployNewProtocolVersion()" \ +--rpc-url $RPCURL \ +--with-gas-price $(cast --to-wei 15 gwei) \ +--verify --etherscan-api-key $ETHERSCAN_API_KEY \ +--broadcast +*/ + /// @dev Expecting to have deployer, deployerSafe, protocolSafe, daoSafe, hub & LOAN token + /// addresses set in the `deployments/latest.json`. + function deployNewProtocolVersion() external { + _loadDeployedAddresses(); + + require(address(deployment.deployer) != address(0), "Deployer not set"); + require(deployment.deployerSafe != address(0), "Deployer safe not set"); + require(deployment.protocolSafe != address(0), "Protocol safe not set"); + require(deployment.daoSafe != address(0), "DAO safe not set"); + require(address(deployment.hub) != address(0), "Hub not set"); + require(address(deployment.loanToken) != address(0), "LOAN token not set"); + + uint256 initialConfigHelper = vmSafe.envUint("INITIAL_CONFIG_HELPER"); + + vm.startBroadcast(); + + // Deploy new protocol version + + // - Config + + // Note: To have the same config proxy address on new chains independently of the config implementation, + // the config proxy is deployed first with Deployer implementation that has the same address on all chains. + // Proxy implementation is then upgraded to the correct one in the next transaction. + + deployment.config = PWNConfig(_deploy({ + salt: PWNContractDeployerSalt.CONFIG_PROXY, + bytecode: abi.encodePacked( + type(TransparentUpgradeableProxy).creationCode, + abi.encode(deployment.deployer, vm.addr(initialConfigHelper), "") + ) + })); + address configSingleton = _deploy({ + salt: PWNContractDeployerSalt.CONFIG, + bytecode: type(PWNConfig).creationCode + }); + + vm.stopBroadcast(); + + + vm.startBroadcast(initialConfigHelper); + ITransparentUpgradeableProxy(address(deployment.config)).upgradeToAndCall( + configSingleton, + abi.encodeWithSelector(PWNConfig.initialize.selector, deployment.daoSafe, 0, deployment.daoSafe) + ); + ITransparentUpgradeableProxy(address(deployment.config)).changeAdmin(deployment.protocolSafe); + vm.stopBroadcast(); + + + vm.startBroadcast(); + + // - MultiToken category registry + deployment.categoryRegistry = MultiTokenCategoryRegistry(_deployAndTransferOwnership({ // Need ownership acceptance from the new owner + salt: PWNContractDeployerSalt.CONFIG, + owner: deployment.protocolSafe, + bytecode: type(MultiTokenCategoryRegistry).creationCode + })); + + // - Revoked nonces + deployment.revokedNonce = PWNRevokedNonce(_deploy({ + salt: PWNContractDeployerSalt.REVOKED_NONCE, + bytecode: abi.encodePacked( + type(PWNRevokedNonce).creationCode, + abi.encode(address(deployment.hub), PWNHubTags.NONCE_MANAGER) + ) + })); + + // - Loan types + deployment.simpleLoan = PWNSimpleLoan(_deploy({ + salt: PWNContractDeployerSalt.SIMPLE_LOAN, + bytecode: abi.encodePacked( + type(PWNSimpleLoan).creationCode, + abi.encode( + address(deployment.hub), + address(deployment.loanToken), + address(deployment.config), + address(deployment.revokedNonce), + address(deployment.categoryRegistry) + ) + ) + })); + + // - Proposals + deployment.simpleLoanSimpleProposal = PWNSimpleLoanSimpleProposal(_deploy({ + salt: PWNContractDeployerSalt.SIMPLE_LOAN_SIMPLE_PROPOSAL, + bytecode: abi.encodePacked( + type(PWNSimpleLoanSimpleProposal).creationCode, + abi.encode( + address(deployment.hub), + address(deployment.revokedNonce), + address(deployment.config) + ) + ) + })); + + deployment.simpleLoanListProposal = PWNSimpleLoanListProposal(_deploy({ + salt: PWNContractDeployerSalt.SIMPLE_LOAN_LIST_PROPOSAL, + bytecode: abi.encodePacked( + type(PWNSimpleLoanListProposal).creationCode, + abi.encode( + address(deployment.hub), + address(deployment.revokedNonce), + address(deployment.config) + ) + ) + })); + + deployment.simpleLoanFungibleProposal = PWNSimpleLoanFungibleProposal(_deploy({ + salt: PWNContractDeployerSalt.SIMPLE_LOAN_FUNGIBLE_PROPOSAL, + bytecode: abi.encodePacked( + type(PWNSimpleLoanFungibleProposal).creationCode, + abi.encode( + address(deployment.hub), + address(deployment.revokedNonce), + address(deployment.config) + ) + ) + })); + + deployment.simpleLoanDutchAuctionProposal = PWNSimpleLoanDutchAuctionProposal(_deploy({ + salt: PWNContractDeployerSalt.SIMPLE_LOAN_DUTCH_AUCTION_PROPOSAL, + bytecode: abi.encodePacked( + type(PWNSimpleLoanDutchAuctionProposal).creationCode, + abi.encode( + address(deployment.hub), + address(deployment.revokedNonce), + address(deployment.config) + ) + ) + })); + + console2.log("MultiToken Category Registry:", address(deployment.categoryRegistry)); + console2.log("PWNConfig - singleton:", configSingleton); + console2.log("PWNConfig - proxy:", address(deployment.config)); + console2.log("PWNHub:", address(deployment.hub)); + console2.log("PWNLOAN:", address(deployment.loanToken)); + console2.log("PWNRevokedNonce:", address(deployment.revokedNonce)); + console2.log("PWNSimpleLoan:", address(deployment.simpleLoan)); + console2.log("PWNSimpleLoanSimpleProposal:", address(deployment.simpleLoanSimpleProposal)); + console2.log("PWNSimpleLoanListProposal:", address(deployment.simpleLoanListProposal)); + console2.log("PWNSimpleLoanFungibleProposal:", address(deployment.simpleLoanFungibleProposal)); + console2.log("PWNSimpleLoanDutchAuctionProposal:", address(deployment.simpleLoanDutchAuctionProposal)); + + vm.stopBroadcast(); } /* @@ -91,16 +249,16 @@ forge script script/PWN.s.sol:Deploy \ --verify --etherscan-api-key $ETHERSCAN_API_KEY \ --broadcast */ - /// @dev Expecting to have deployer, deployerSafe, protocolSafe, daoSafe, feeCollector & categoryRegistry addresses set in the `deployments.json` + /// @dev Expecting to have deployer, deployerSafe, protocolSafe, daoSafe & categoryRegistry + /// addresses set in the `deployments/latest.json`. function deployProtocol() external { _loadDeployedAddresses(); - require(address(deployer) != address(0), "Deployer not set"); - require(deployerSafe != address(0), "Deployer safe not set"); - require(protocolSafe != address(0), "Protocol safe not set"); - require(daoSafe != address(0), "DAO safe not set"); - require(feeCollector != address(0), "Fee collector not set"); - require(address(categoryRegistry) != address(0), "Category registry not set"); + require(address(deployment.deployer) != address(0), "Deployer not set"); + require(deployment.deployerSafe != address(0), "Deployer safe not set"); + require(deployment.protocolSafe != address(0), "Protocol safe not set"); + require(deployment.daoSafe != address(0), "DAO safe not set"); + require(address(deployment.categoryRegistry) != address(0), "Category registry not set"); uint256 initialConfigHelper = vmSafe.envUint("INITIAL_CONFIG_HELPER"); @@ -109,72 +267,144 @@ forge script script/PWN.s.sol:Deploy \ // Deploy protocol // - Config - address configSingleton = _deploy({ - salt: PWNContractDeployerSalt.CONFIG_V1, - bytecode: type(PWNConfig).creationCode - }); - config = PWNConfig(_deploy({ + + // Note: To have the same config proxy address on new chains independently of the config implementation, + // the config proxy is deployed first with Deployer implementation that has the same address on all chains. + // Proxy implementation is then upgraded to the correct one in the next transaction. + + deployment.config = PWNConfig(_deploy({ salt: PWNContractDeployerSalt.CONFIG_PROXY, bytecode: abi.encodePacked( type(TransparentUpgradeableProxy).creationCode, - abi.encode(configSingleton, vm.addr(initialConfigHelper), "") + abi.encode(deployment.deployer, vm.addr(initialConfigHelper), "") ) })); - config.initialize(daoSafe, 0, feeCollector); + address configSingleton = _deploy({ + salt: PWNContractDeployerSalt.CONFIG, + bytecode: type(PWNConfig).creationCode + }); + + vm.stopBroadcast(); + + vm.startBroadcast(initialConfigHelper); + ITransparentUpgradeableProxy(address(deployment.config)).upgradeToAndCall( + configSingleton, + abi.encodeWithSelector(PWNConfig.initialize.selector, deployment.daoSafe, 0, deployment.daoSafe) + ); + ITransparentUpgradeableProxy(address(deployment.config)).changeAdmin(deployment.protocolSafe); vm.stopBroadcast(); - vm.broadcast(initialConfigHelper); - ITransparentUpgradeableProxy(address(config)).changeAdmin(protocolSafe); vm.startBroadcast(); + // - MultiToken category registry + deployment.categoryRegistry = MultiTokenCategoryRegistry(_deployAndTransferOwnership({ // Need ownership acceptance from the new owner + salt: PWNContractDeployerSalt.CONFIG, + owner: deployment.protocolSafe, + bytecode: type(MultiTokenCategoryRegistry).creationCode + })); + // - Hub - hub = PWNHub(_deployAndTransferOwnership({ + deployment.hub = PWNHub(_deployAndTransferOwnership({ // Need ownership acceptance from the new owner salt: PWNContractDeployerSalt.HUB, - owner: protocolSafe, + owner: deployment.protocolSafe, bytecode: type(PWNHub).creationCode })); // - LOAN token - loanToken = PWNLOAN(_deploy({ + deployment.loanToken = PWNLOAN(_deploy({ salt: PWNContractDeployerSalt.LOAN, bytecode: abi.encodePacked( type(PWNLOAN).creationCode, - abi.encode(address(hub)) + abi.encode(address(deployment.hub)) ) })); // - Revoked nonces - revokedNonce = PWNRevokedNonce(_deploy({ + deployment.revokedNonce = PWNRevokedNonce(_deploy({ salt: PWNContractDeployerSalt.REVOKED_NONCE, bytecode: abi.encodePacked( type(PWNRevokedNonce).creationCode, - abi.encode(address(hub), PWNHubTags.NONCE_MANAGER) + abi.encode(address(deployment.hub), PWNHubTags.NONCE_MANAGER) ) })); // - Loan types - simpleLoan = PWNSimpleLoan(_deploy({ + deployment.simpleLoan = PWNSimpleLoan(_deploy({ salt: PWNContractDeployerSalt.SIMPLE_LOAN, bytecode: abi.encodePacked( type(PWNSimpleLoan).creationCode, abi.encode( - address(hub), - address(loanToken), - address(config), - address(revokedNonce), - address(categoryRegistry) + address(deployment.hub), + address(deployment.loanToken), + address(deployment.config), + address(deployment.revokedNonce), + address(deployment.categoryRegistry) ) ) })); + // - Proposals + deployment.simpleLoanSimpleProposal = PWNSimpleLoanSimpleProposal(_deploy({ + salt: PWNContractDeployerSalt.SIMPLE_LOAN_SIMPLE_PROPOSAL, + bytecode: abi.encodePacked( + type(PWNSimpleLoanSimpleProposal).creationCode, + abi.encode( + address(deployment.hub), + address(deployment.revokedNonce), + address(deployment.config) + ) + ) + })); + + deployment.simpleLoanListProposal = PWNSimpleLoanListProposal(_deploy({ + salt: PWNContractDeployerSalt.SIMPLE_LOAN_LIST_PROPOSAL, + bytecode: abi.encodePacked( + type(PWNSimpleLoanListProposal).creationCode, + abi.encode( + address(deployment.hub), + address(deployment.revokedNonce), + address(deployment.config) + ) + ) + })); + + deployment.simpleLoanFungibleProposal = PWNSimpleLoanFungibleProposal(_deploy({ + salt: PWNContractDeployerSalt.SIMPLE_LOAN_FUNGIBLE_PROPOSAL, + bytecode: abi.encodePacked( + type(PWNSimpleLoanFungibleProposal).creationCode, + abi.encode( + address(deployment.hub), + address(deployment.revokedNonce), + address(deployment.config) + ) + ) + })); + + deployment.simpleLoanDutchAuctionProposal = PWNSimpleLoanDutchAuctionProposal(_deploy({ + salt: PWNContractDeployerSalt.SIMPLE_LOAN_DUTCH_AUCTION_PROPOSAL, + bytecode: abi.encodePacked( + type(PWNSimpleLoanDutchAuctionProposal).creationCode, + abi.encode( + address(deployment.hub), + address(deployment.revokedNonce), + address(deployment.config) + ) + ) + })); + + console2.log("MultiToken Category Registry:", address(deployment.categoryRegistry)); 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("PWNConfig - proxy:", address(deployment.config)); + console2.log("PWNHub:", address(deployment.hub)); + console2.log("PWNLOAN:", address(deployment.loanToken)); + console2.log("PWNRevokedNonce:", address(deployment.revokedNonce)); + console2.log("PWNSimpleLoan:", address(deployment.simpleLoan)); + console2.log("PWNSimpleLoanSimpleProposal:", address(deployment.simpleLoanSimpleProposal)); + console2.log("PWNSimpleLoanListProposal:", address(deployment.simpleLoanListProposal)); + console2.log("PWNSimpleLoanFungibleProposal:", address(deployment.simpleLoanFungibleProposal)); + console2.log("PWNSimpleLoanDutchAuctionProposal:", address(deployment.simpleLoanDutchAuctionProposal)); vm.stopBroadcast(); } @@ -186,24 +416,29 @@ contract Setup is Deployments, Script { using GnosisSafeUtils for GnosisSafeLike; function _protocolNotDeployedOnSelectedChain() internal pure override { - revert("PWN: selected chain is not set in deployments.json"); + revert("PWN: selected chain is not set in deployments/latest.json"); } /* forge script script/PWN.s.sol:Setup \ ---sig "setupProtocol()" \ +--sig "setupNewProtocolVersion()" \ --rpc-url $RPC_URL \ ---private-key $PRIVATE_KEY \ --with-gas-price $(cast --to-wei 15 gwei) \ --broadcast */ - /// @dev Expecting to have protocol addresses set in the `deployments.json` - function setupProtocol() external { + /// @dev Expecting to have protocol addresses set in the `deployments/latest.json` + /// Can be used only in fork tests, because protocol safe has threshold >1 and hub is owner by a timelock. + /// To set safes threshold to 1 use: cast rpc -r $TENDERLY_URL tenderly_setStorageAt {safe_address} 0x0000000000000000000000000000000000000000000000000000000000000004 0x0000000000000000000000000000000000000000000000000000000000000001 + /// To set hubs owner to protocol safe use: cast rpc -r $TENDERLY_URL tenderly_setStorageAt {hub_address} 0x0000000000000000000000000000000000000000000000000000000000000000 {protocol_safe_addr_to_32} + function setupNewProtocolVersion() external { _loadDeployedAddresses(); + require(address(deployment.protocolSafe) != address(0), "Protocol safe not set"); + require(address(deployment.categoryRegistry) != address(0), "Category registry not set"); + vm.startBroadcast(); - _acceptOwnership(protocolSafe, address(hub)); + _acceptOwnership(deployment.protocolSafe, address(deployment.categoryRegistry)); _setTags(); vm.stopBroadcast(); @@ -211,16 +446,25 @@ forge script script/PWN.s.sol:Setup \ /* forge script script/PWN.s.sol:Setup \ ---sig "acceptOwnership(address,address)" $SAFE $CONTRACT \ +--sig "setupProtocol()" \ --rpc-url $RPC_URL \ --private-key $PRIVATE_KEY \ --with-gas-price $(cast --to-wei 15 gwei) \ --broadcast */ - /// @dev Not expecting any addresses set in the `deployments.json` - function acceptOwnership(address safe, address contract_) external { + /// @dev Expecting to have protocol addresses set in the `deployments/latest.json` + function setupProtocol() external { + _loadDeployedAddresses(); + + require(address(deployment.protocolSafe) != address(0), "Protocol safe not set"); + require(address(deployment.hub) != address(0), "Hub not set"); + vm.startBroadcast(); - _acceptOwnership(safe, contract_); + + _acceptOwnership(deployment.protocolSafe, address(deployment.categoryRegistry)); + _acceptOwnership(deployment.protocolSafe, address(deployment.hub)); + _setTags(); + vm.stopBroadcast(); } @@ -234,38 +478,49 @@ forge script script/PWN.s.sol:Setup \ console2.log("Accept ownership tx succeeded"); } -/* -forge script script/PWN.s.sol:Setup \ ---sig "setTags()" \ ---rpc-url $RPC_URL \ ---private-key $PRIVATE_KEY \ ---with-gas-price $(cast --to-wei 15 gwei) \ ---broadcast -*/ - /// @dev Expecting to have protocol addresses set in the `deployments.json` - function setTags() external { - _loadDeployedAddresses(); + function _setTags() internal { + require(address(deployment.simpleLoan) != address(0), "Simple loan not set"); + require(address(deployment.simpleLoanSimpleProposal) != address(0), "Simple loan simple proposal not set"); + require(address(deployment.simpleLoanListProposal) != address(0), "Simple loan list proposal not set"); + require(address(deployment.simpleLoanFungibleProposal) != address(0), "Simple loan fungible proposal not set"); + require(address(deployment.simpleLoanDutchAuctionProposal) != address(0), "Simple loan dutch auctin proposal not set"); + require(address(deployment.protocolSafe) != address(0), "Protocol safe not set"); + require(address(deployment.hub) != address(0), "Hub not set"); - vm.startBroadcast(); - _setTags(); - vm.stopBroadcast(); - } + address[] memory addrs = new address[](10); + addrs[0] = address(deployment.simpleLoan); + addrs[1] = address(deployment.simpleLoan); - function _setTags() internal { - 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(deployment.simpleLoanSimpleProposal); + addrs[3] = address(deployment.simpleLoanSimpleProposal); + + addrs[4] = address(deployment.simpleLoanListProposal); + addrs[5] = address(deployment.simpleLoanListProposal); + + addrs[6] = address(deployment.simpleLoanFungibleProposal); + addrs[7] = address(deployment.simpleLoanFungibleProposal); - bytes32[] memory tags = new bytes32[](4); + addrs[8] = address(deployment.simpleLoanDutchAuctionProposal); + addrs[9] = address(deployment.simpleLoanDutchAuctionProposal); + + bytes32[] memory tags = new bytes32[](10); tags[0] = PWNHubTags.ACTIVE_LOAN; tags[1] = PWNHubTags.NONCE_MANAGER; - // tags[2] = PWNHubTags.LOAN_PROPOSAL; - // tags[3] = PWNHubTags.NONCE_MANAGER; - bool success = GnosisSafeLike(protocolSafe).execTransaction({ - to: address(hub), + tags[2] = PWNHubTags.LOAN_PROPOSAL; + tags[3] = PWNHubTags.NONCE_MANAGER; + + tags[4] = PWNHubTags.LOAN_PROPOSAL; + tags[5] = PWNHubTags.NONCE_MANAGER; + + tags[6] = PWNHubTags.LOAN_PROPOSAL; + tags[7] = PWNHubTags.NONCE_MANAGER; + + tags[8] = PWNHubTags.LOAN_PROPOSAL; + tags[9] = PWNHubTags.NONCE_MANAGER; + + bool success = GnosisSafeLike(deployment.protocolSafe).execTransaction({ + to: address(deployment.hub), data: abi.encodeWithSignature( "setTags(address[],bytes32[],bool)", addrs, tags, true ) @@ -283,14 +538,17 @@ forge script script/PWN.s.sol:Setup \ --with-gas-price $(cast --to-wei 15 gwei) \ --broadcast */ - /// @dev Expecting to have daoSafe & config addresses set in the `deployments.json` + /// @dev Expecting to have daoSafe & config addresses set in the `deployments/latest.json` function setMetadata(address address_, string memory metadata) external { _loadDeployedAddresses(); + require(address(deployment.daoSafe) != address(0), "DAO safe not set"); + require(address(deployment.config) != address(0), "Config not set"); + vm.startBroadcast(); - bool success = GnosisSafeLike(daoSafe).execTransaction({ - to: address(config), + bool success = GnosisSafeLike(deployment.daoSafe).execTransaction({ + to: address(deployment.config), data: abi.encodeWithSignature( "setLoanMetadataUri(address,string)", address_, metadata ) diff --git a/script/PWNTimelock.s.sol b/script/PWNTimelock.s.sol index f88e3bf..8453bdd 100644 --- a/script/PWNTimelock.s.sol +++ b/script/PWNTimelock.s.sol @@ -3,11 +3,11 @@ pragma solidity 0.8.16; import "forge-std/Script.sol"; -import "openzeppelin-contracts/contracts/governance/TimelockController.sol"; +import { TimelockController } from "openzeppelin-contracts/contracts/governance/TimelockController.sol"; import { GnosisSafeLike, GnosisSafeUtils } from "./lib/GnosisSafeUtils.sol"; -import "@pwn/deployer/IPWNDeployer.sol"; +import { IPWNDeployer } from "@pwn/deployer/IPWNDeployer.sol"; import "@pwn/Deployments.sol"; @@ -31,21 +31,21 @@ contract Deploy is Deployments, Script { using GnosisSafeUtils for GnosisSafeLike; function _protocolNotDeployedOnSelectedChain() internal pure override { - revert("PWNTimelock: selected chain is not set in deployments.json"); + revert("PWNTimelock: selected chain is not set in deployments/latest.json"); } function _deploy( bytes32 salt, bytes memory bytecode ) internal returns (address) { - bool success = GnosisSafeLike(deployerSafe).execTransaction({ - to: address(deployer), + bool success = GnosisSafeLike(deployment.deployerSafe).execTransaction({ + to: address(deployment.deployer), data: abi.encodeWithSelector( IPWNDeployer.deploy.selector, salt, bytecode ) }); require(success, "Deploy failed"); - return deployer.computeAddress(salt, keccak256(bytecode)); + return deployment.deployer.computeAddress(salt, keccak256(bytecode)); } /* @@ -109,7 +109,7 @@ contract Setup is Deployments, Script { using GnosisSafeUtils for GnosisSafeLike; function _protocolNotDeployedOnSelectedChain() internal pure override { - revert("PWN: selected chain is not set in deployments.json"); + revert("PWNTimelock: selected chain is not set in deployments/latest.json"); } /* @@ -122,8 +122,8 @@ forge script script/PWNTimelock.s.sol:Setup \ */ function updateProtocolTimelockProposer() external { _loadDeployedAddresses(); - console2.log("Updating protocol timelock proposer (%s)", protocolTimelock); - _updateProposer(TimelockController(payable(protocolTimelock)), protocolSafe); + console2.log("Updating protocol timelock proposer (%s)", deployment.protocolTimelock); + _updateProposer(TimelockController(payable(deployment.protocolTimelock)), deployment.protocolSafe); } /* @@ -136,8 +136,8 @@ forge script script/PWNTimelock.s.sol:Setup \ */ function updateProductTimelockProposer() external { _loadDeployedAddresses(); - console2.log("Updating product timelock proposer (%s)", productTimelock); - _updateProposer(TimelockController(payable(productTimelock)), daoSafe); + console2.log("Updating product timelock proposer (%s)", deployment.productTimelock); + _updateProposer(TimelockController(payable(deployment.productTimelock)), deployment.daoSafe); } /// @dev Will grant PROPOSER_ROLE & CANCELLOR_ROLE to the new address and revoke them from `0x0cfC...D6de`. @@ -215,31 +215,31 @@ forge script script/PWNTimelock.s.sol:Setup \ // set PWNConfig admin bool success; - success = GnosisSafeLike(protocolSafe).execTransaction({ - to: address(config), - data: abi.encodeWithSignature("changeAdmin(address)", protocolTimelock) + success = GnosisSafeLike(deployment.protocolSafe).execTransaction({ + to: address(deployment.config), + data: abi.encodeWithSignature("changeAdmin(address)", deployment.protocolTimelock) }); require(success, "PWN: change admin failed"); // transfer PWNHub owner - success = GnosisSafeLike(protocolSafe).execTransaction({ - to: address(hub), - data: abi.encodeWithSignature("transferOwnership(address)", protocolTimelock) + success = GnosisSafeLike(deployment.protocolSafe).execTransaction({ + to: address(deployment.hub), + data: abi.encodeWithSignature("transferOwnership(address)", deployment.protocolTimelock) }); require(success, "PWN: change owner failed"); // accept PWNHub owner - success = GnosisSafeLike(protocolSafe).execTransaction({ - to: address(protocolTimelock), + success = GnosisSafeLike(deployment.protocolSafe).execTransaction({ + to: address(deployment.protocolTimelock), data: abi.encodeWithSignature( "schedule(address,uint256,bytes,bytes32,bytes32,uint256)", - address(hub), 0, abi.encodeWithSignature("acceptOwnership()"), 0, 0, 0 + address(deployment.hub), 0, abi.encodeWithSignature("acceptOwnership()"), 0, 0, 0 ) }); require(success, "PWN: schedule accept ownership failed"); - TimelockController(payable(protocolTimelock)).execute({ - target: address(hub), + TimelockController(payable(deployment.protocolTimelock)).execute({ + target: address(deployment.hub), value: 0, payload: abi.encodeWithSignature("acceptOwnership()"), predecessor: 0, @@ -247,17 +247,17 @@ forge script script/PWNTimelock.s.sol:Setup \ }); // set min delay - success = GnosisSafeLike(protocolSafe).execTransaction({ - to: address(protocolTimelock), + success = GnosisSafeLike(deployment.protocolSafe).execTransaction({ + to: address(deployment.protocolTimelock), data: abi.encodeWithSignature( "schedule(address,uint256,bytes,bytes32,bytes32,uint256)", - address(protocolTimelock), 0, abi.encodeWithSignature("updateDelay(uint256)", protocolTimelockMinDelay), 0, 0, 0 + address(deployment.protocolTimelock), 0, abi.encodeWithSignature("updateDelay(uint256)", protocolTimelockMinDelay), 0, 0, 0 ) }); require(success, "PWN: schedule update delay failed"); - TimelockController(payable(protocolTimelock)).execute({ - target: protocolTimelock, + TimelockController(payable(deployment.protocolTimelock)).execute({ + target: deployment.protocolTimelock, value: 0, payload: abi.encodeWithSignature("updateDelay(uint256)", protocolTimelockMinDelay), predecessor: 0, @@ -288,24 +288,24 @@ forge script script/PWNTimelock.s.sol:Setup \ // transfer PWNConfig owner bool success; - success = GnosisSafeLike(daoSafe).execTransaction({ - to: address(config), - data: abi.encodeWithSignature("transferOwnership(address)", productTimelock) + success = GnosisSafeLike(deployment.daoSafe).execTransaction({ + to: address(deployment.config), + data: abi.encodeWithSignature("transferOwnership(address)", deployment.productTimelock) }); require(success, "PWN: change owner failed"); // accept PWNConfig owner - success = GnosisSafeLike(daoSafe).execTransaction({ - to: address(productTimelock), + success = GnosisSafeLike(deployment.daoSafe).execTransaction({ + to: address(deployment.productTimelock), data: abi.encodeWithSignature( "schedule(address,uint256,bytes,bytes32,bytes32,uint256)", - address(config), 0, abi.encodeWithSignature("acceptOwnership()"), 0, 0, 0 + address(deployment.config), 0, abi.encodeWithSignature("acceptOwnership()"), 0, 0, 0 ) }); require(success, "PWN: schedule failed"); - TimelockController(payable(productTimelock)).execute({ - target: address(config), + TimelockController(payable(deployment.productTimelock)).execute({ + target: address(deployment.config), value: 0, payload: abi.encodeWithSignature("acceptOwnership()"), predecessor: 0, @@ -313,17 +313,17 @@ forge script script/PWNTimelock.s.sol:Setup \ }); // set min delay - success = GnosisSafeLike(daoSafe).execTransaction({ - to: address(productTimelock), + success = GnosisSafeLike(deployment.daoSafe).execTransaction({ + to: address(deployment.productTimelock), data: abi.encodeWithSignature( "schedule(address,uint256,bytes,bytes32,bytes32,uint256)", - address(productTimelock), 0, abi.encodeWithSignature("updateDelay(uint256)", productTimelockMinDelay), 0, 0, 0 + address(deployment.productTimelock), 0, abi.encodeWithSignature("updateDelay(uint256)", productTimelockMinDelay), 0, 0, 0 ) }); require(success, "PWN: update delay failed"); - TimelockController(payable(productTimelock)).execute({ - target: productTimelock, + TimelockController(payable(deployment.productTimelock)).execute({ + target: deployment.productTimelock, value: 0, payload: abi.encodeWithSignature("updateDelay(uint256)", productTimelockMinDelay), predecessor: 0, diff --git a/src/Deployments.sol b/src/Deployments.sol index b6c1b0d..225f127 100644 --- a/src/Deployments.sol +++ b/src/Deployments.sol @@ -13,11 +13,12 @@ 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 { PWNSimpleLoanDutchAuctionProposal } from "@pwn/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol"; +import { PWNSimpleLoanFungibleProposal } from "@pwn/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.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"; -import { StateFingerprintComputerRegistry } from "@pwn/state-fingerprint/StateFingerprintComputerRegistry.sol"; abstract contract Deployments is CommonBase { @@ -36,7 +37,6 @@ abstract contract Deployments is CommonBase { address daoSafe; IPWNDeployer deployer; address deployerSafe; - address feeCollector; PWNHub hub; PWNLOAN loanToken; address productTimelock; @@ -44,34 +44,16 @@ abstract contract Deployments is CommonBase { address protocolTimelock; PWNRevokedNonce revokedNonce; PWNSimpleLoan simpleLoan; - StateFingerprintComputerRegistry stateFingerprintComputerRegistry; + PWNSimpleLoanDutchAuctionProposal simpleLoanDutchAuctionProposal; + PWNSimpleLoanFungibleProposal simpleLoanFungibleProposal; + PWNSimpleLoanListProposal simpleLoanListProposal; + PWNSimpleLoanSimpleProposal simpleLoanSimpleProposal; } - address dao; - - address productTimelock; - address protocolTimelock; - - address deployerSafe; - address protocolSafe; - address daoSafe; - address feeCollector; - - IMultiTokenCategoryRegistry categoryRegistry; - StateFingerprintComputerRegistry stateFingerprintComputerRegistry; - - IPWNDeployer deployer; - PWNHub hub; - PWNConfig configSingleton; - PWNConfig config; - PWNLOAN loanToken; - PWNSimpleLoan simpleLoan; - PWNRevokedNonce revokedNonce; - function _loadDeployedAddresses() internal { string memory root = vm.projectRoot(); - string memory path = string.concat(root, "/deployments.json"); + string memory path = string.concat(root, "/deployments/latest.json"); string memory json = vm.readFile(path); bytes memory rawDeployedChains = json.parseRaw(".deployedChains"); deployedChains = abi.decode(rawDeployedChains, (uint256[])); @@ -79,23 +61,6 @@ abstract contract Deployments is CommonBase { if (_contains(deployedChains, block.chainid)) { bytes memory rawDeployment = json.parseRaw(string.concat(".chains.", block.chainid.toString())); deployment = abi.decode(rawDeployment, (Deployment)); - - dao = deployment.dao; - productTimelock = deployment.productTimelock; - protocolTimelock = deployment.protocolTimelock; - deployerSafe = deployment.deployerSafe; - protocolSafe = deployment.protocolSafe; - daoSafe = deployment.daoSafe; - feeCollector = deployment.feeCollector; - deployer = deployment.deployer; - hub = deployment.hub; - configSingleton = deployment.configSingleton; - config = deployment.config; - loanToken = deployment.loanToken; - simpleLoan = deployment.simpleLoan; - revokedNonce = deployment.revokedNonce; - stateFingerprintComputerRegistry = deployment.stateFingerprintComputerRegistry; - categoryRegistry = deployment.categoryRegistry; } else { _protocolNotDeployedOnSelectedChain(); } diff --git a/test/helper/DeploymentTest.t.sol b/test/helper/DeploymentTest.t.sol index 030e6ad..24d7212 100644 --- a/test/helper/DeploymentTest.t.sol +++ b/test/helper/DeploymentTest.t.sol @@ -18,48 +18,92 @@ abstract contract DeploymentTest is Deployments, Test { } function _protocolNotDeployedOnSelectedChain() internal override { - protocolSafe = makeAddr("protocolSafe"); - daoSafe = makeAddr("daoSafe"); - feeCollector = makeAddr("feeCollector"); + deployment.protocolSafe = makeAddr("protocolSafe"); + deployment.daoSafe = makeAddr("daoSafe"); // Deploy category registry - vm.prank(protocolSafe); - categoryRegistry = IMultiTokenCategoryRegistry(new MultiTokenCategoryRegistry()); + vm.prank(deployment.protocolSafe); + deployment.categoryRegistry = IMultiTokenCategoryRegistry(new MultiTokenCategoryRegistry()); // Deploy protocol - configSingleton = new PWNConfig(); + deployment.configSingleton = new PWNConfig(); TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy( - address(configSingleton), - protocolSafe, - abi.encodeWithSignature("initialize(address,uint16,address)", address(this), 0, feeCollector) + address(deployment.configSingleton), + deployment.protocolSafe, + abi.encodeWithSignature("initialize(address,uint16,address)", address(this), 0, deployment.daoSafe) ); - config = PWNConfig(address(proxy)); + deployment.config = PWNConfig(address(proxy)); - vm.prank(protocolSafe); - hub = new PWNHub(); + vm.prank(deployment.protocolSafe); + deployment.hub = new PWNHub(); - revokedNonce = new PWNRevokedNonce(address(hub), PWNHubTags.NONCE_MANAGER); + deployment.revokedNonce = new PWNRevokedNonce(address(deployment.hub), PWNHubTags.NONCE_MANAGER); - loanToken = new PWNLOAN(address(hub)); - simpleLoan = new PWNSimpleLoan( - address(hub), address(loanToken), address(config), address(revokedNonce), address(categoryRegistry) + deployment.loanToken = new PWNLOAN(address(deployment.hub)); + deployment.simpleLoan = new PWNSimpleLoan( + address(deployment.hub), + address(deployment.loanToken), + address(deployment.config), + address(deployment.revokedNonce), + address(deployment.categoryRegistry) + ); + + deployment.simpleLoanSimpleProposal = new PWNSimpleLoanSimpleProposal( + address(deployment.hub), + address(deployment.revokedNonce), + address(deployment.config) + ); + deployment.simpleLoanListProposal = new PWNSimpleLoanListProposal( + address(deployment.hub), + address(deployment.revokedNonce), + address(deployment.config) + ); + deployment.simpleLoanFungibleProposal = new PWNSimpleLoanFungibleProposal( + address(deployment.hub), + address(deployment.revokedNonce), + address(deployment.config) + ); + deployment.simpleLoanDutchAuctionProposal = new PWNSimpleLoanDutchAuctionProposal( + address(deployment.hub), + address(deployment.revokedNonce), + address(deployment.config) ); // 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); + address[] memory addrs = new address[](10); + addrs[0] = address(deployment.simpleLoan); + addrs[1] = address(deployment.simpleLoan); + + addrs[2] = address(deployment.simpleLoanSimpleProposal); + addrs[3] = address(deployment.simpleLoanSimpleProposal); + + addrs[4] = address(deployment.simpleLoanListProposal); + addrs[5] = address(deployment.simpleLoanListProposal); - bytes32[] memory tags = new bytes32[](4); + addrs[6] = address(deployment.simpleLoanFungibleProposal); + addrs[7] = address(deployment.simpleLoanFungibleProposal); + + addrs[8] = address(deployment.simpleLoanDutchAuctionProposal); + addrs[9] = address(deployment.simpleLoanDutchAuctionProposal); + + bytes32[] memory tags = new bytes32[](10); tags[0] = PWNHubTags.ACTIVE_LOAN; tags[1] = PWNHubTags.NONCE_MANAGER; - // tags[2] = PWNHubTags.LOAN_PROPOSAL; - // tags[3] = PWNHubTags.NONCE_MANAGER; - vm.prank(protocolSafe); - hub.setTags(addrs, tags, true); + tags[2] = PWNHubTags.LOAN_PROPOSAL; + tags[3] = PWNHubTags.NONCE_MANAGER; + + tags[4] = PWNHubTags.LOAN_PROPOSAL; + tags[5] = PWNHubTags.NONCE_MANAGER; + + tags[6] = PWNHubTags.LOAN_PROPOSAL; + tags[7] = PWNHubTags.NONCE_MANAGER; + + tags[8] = PWNHubTags.LOAN_PROPOSAL; + tags[9] = PWNHubTags.NONCE_MANAGER; + + vm.prank(deployment.protocolSafe); + deployment.hub.setTags(addrs, tags, true); } } From 3fe1680807932d86e41dbbf5ea31cf068ff5267e Mon Sep 17 00:00:00 2001 From: ashhanai Date: Wed, 27 Mar 2024 12:17:27 -0400 Subject: [PATCH 061/129] refactor: merge accept new and refactoring proposal into one function --- .../PWNSimpleLoanDutchAuctionProposal.sol | 132 ++----- .../PWNSimpleLoanFungibleProposal.sol | 132 ++----- .../proposal/PWNSimpleLoanListProposal.sol | 132 ++----- .../simple/proposal/PWNSimpleLoanProposal.sol | 24 ++ .../proposal/PWNSimpleLoanSimpleProposal.sol | 124 ++----- .../PWNSimpleLoanDutchAuctionProposal.t.sol | 273 +++----------- test/unit/PWNSimpleLoanFungibleProposal.t.sol | 208 +++-------- test/unit/PWNSimpleLoanListProposal.t.sol | 223 +++--------- test/unit/PWNSimpleLoanProposal.t.sol | 339 ++---------------- test/unit/PWNSimpleLoanSimpleProposal.t.sol | 173 +++------ 10 files changed, 402 insertions(+), 1358 deletions(-) diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol index 80b5e53..e1cbb51 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol @@ -180,85 +180,6 @@ contract PWNSimpleLoanDutchAuctionProposal is PWNSimpleLoanProposal { : proposal.maxCreditAmount - creditAmountDelta; } - /** - * @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 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 Proposal values struct containing concrete 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. @@ -266,6 +187,7 @@ contract PWNSimpleLoanDutchAuctionProposal is PWNSimpleLoanProposal { * @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. @@ -275,40 +197,62 @@ contract PWNSimpleLoanDutchAuctionProposal is PWNSimpleLoanProposal { 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, permit, extra); + return acceptProposal(proposal, proposalValues, signature, refinancingLoanId, 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. + * @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. - * @param callersNonceSpace Nonce space of a callers nonce. - * @param callersNonceToRevoke Nonce to revoke. - * @return refinancedLoanId Id of a created refinanced loan. + * @return loanId Id of a created loan. */ - function acceptRefinanceProposal( - uint256 loanId, + 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 refinancedLoanId) { - _revokeCallersNonce(msg.sender, callersNonceSpace, callersNonceToRevoke); - return acceptRefinanceProposal(loanId, proposal, proposalValues, signature, permit, extra); + 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 + }); + } } diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol index 58b11c0..898a36e 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol @@ -130,91 +130,13 @@ contract PWNSimpleLoanFungibleProposal is PWNSimpleLoanProposal { return Math.mulDiv(collateralAmount, creditPerCollateralUnit, CREDIT_PER_COLLATERAL_UNIT_DENOMINATOR); } - /** - * @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 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. @@ -225,40 +147,62 @@ contract PWNSimpleLoanFungibleProposal is PWNSimpleLoanProposal { 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, permit, extra); + return acceptProposal(proposal, proposalValues, signature, refinancingLoanId, 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. + * @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. - * @param callersNonceSpace Nonce space of a callers nonce. - * @param callersNonceToRevoke Nonce to revoke. - * @return refinancedLoanId Id of a created refinanced loan. + * @return loanId Id of a created loan. */ - function acceptRefinanceProposal( - uint256 loanId, + 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 refinancedLoanId) { - _revokeCallersNonce(msg.sender, callersNonceSpace, callersNonceToRevoke); - return acceptRefinanceProposal(loanId, proposal, proposalValues, signature, permit, extra); + 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 + }); + } } diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol index 7703922..488f757 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol @@ -118,91 +118,13 @@ contract PWNSimpleLoanListProposal is PWNSimpleLoanProposal { 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 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. @@ -213,40 +135,62 @@ contract PWNSimpleLoanListProposal is PWNSimpleLoanProposal { 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, permit, extra); + return acceptProposal(proposal, proposalValues, signature, refinancingLoanId, 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. + * @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. - * @param callersNonceSpace Nonce space of a callers nonce. - * @param callersNonceToRevoke Nonce to revoke. - * @return refinancedLoanId Id of a created refinanced loan. + * @return loanId Id of a created loan. */ - function acceptRefinanceProposal( - uint256 loanId, + 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 refinancedLoanId) { - _revokeCallersNonce(msg.sender, callersNonceSpace, callersNonceToRevoke); - return acceptRefinanceProposal(loanId, proposal, proposalValues, signature, permit, extra); + 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 + }); + } } diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol index cbcefa9..0ab4bf2 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol @@ -198,6 +198,30 @@ abstract contract PWNSimpleLoanProposal { } } + /** + * @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 }); + } + } 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. diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol index 1415885..723af8a 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol @@ -103,84 +103,13 @@ contract PWNSimpleLoanSimpleProposal is PWNSimpleLoanProposal { emit ProposalMade(proposalHash, proposal.proposer, proposal); } - /** - * @notice Accept a proposal. - * @param proposal Proposal struct containing all proposal data. - * @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, - 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, 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 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, - 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, 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 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. @@ -190,38 +119,59 @@ contract PWNSimpleLoanSimpleProposal is PWNSimpleLoanProposal { 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, permit, extra); + return acceptProposal(proposal, signature, refinancingLoanId, 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. + * @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. - * @param callersNonceSpace Nonce space of a callers nonce. - * @param callersNonceToRevoke Nonce to revoke. - * @return refinancedLoanId Id of a created refinanced loan. + * @return loanId Id of a created loan. */ - function acceptRefinanceProposal( - uint256 loanId, + function acceptProposal( Proposal calldata proposal, bytes calldata signature, + uint256 refinancingLoanId, Permit calldata permit, - bytes calldata extra, - uint256 callersNonceSpace, - uint256 callersNonceToRevoke - ) external returns (uint256 refinancedLoanId) { - _revokeCallersNonce(msg.sender, callersNonceSpace, callersNonceToRevoke); - return acceptRefinanceProposal(loanId, proposal, signature, permit, extra); + 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 + }); + } } diff --git a/test/unit/PWNSimpleLoanDutchAuctionProposal.t.sol b/test/unit/PWNSimpleLoanDutchAuctionProposal.t.sol index e744f35..8e937c5 100644 --- a/test/unit/PWNSimpleLoanDutchAuctionProposal.t.sol +++ b/test/unit/PWNSimpleLoanDutchAuctionProposal.t.sol @@ -15,9 +15,7 @@ import "@pwn/PWNErrors.sol"; import { PWNSimpleLoanProposalTest, PWNSimpleLoanProposal_AcceptProposal_Test, - PWNSimpleLoanProposal_AcceptProposalAndRevokeCallersNonce_Test, - PWNSimpleLoanProposal_AcceptRefinanceProposal_Test, - PWNSimpleLoanProposal_AcceptRefinanceProposalAndRevokeCallersNonce_Test + PWNSimpleLoanProposal_AcceptProposalAndRevokeCallersNonce_Test } from "@pwn-test/unit/PWNSimpleLoanProposal.t.sol"; @@ -120,22 +118,12 @@ abstract contract PWNSimpleLoanDutchAuctionProposalTest is PWNSimpleLoanProposal function _callAcceptProposalWith(Params memory _params, Permit memory _permit) internal override returns (uint256) { _updateProposal(_params); - return proposalContract.acceptProposal(proposal, proposalValues, _proposalSignature(params), _permit, ""); + 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), _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); + return proposalContract.acceptProposal(proposal, proposalValues, _proposalSignature(params), 0, _permit, "", nonceSpace, nonce); } function _getProposalHashWith(Params memory _params) internal override returns (bytes32) { @@ -241,9 +229,7 @@ contract PWNSimpleLoanDutchAuctionProposal_GetCreditAmount_Test is PWNSimpleLoan proposal.auctionDuration = auctionDuration; vm.expectRevert(abi.encodeWithSelector(InvalidAuctionDuration.selector, auctionDuration, 1 minutes)); - proposalContract.acceptProposal( - proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, "" - ); + proposalContract.getCreditAmount(proposal, 0); } function testFuzz_shouldFail_whenAuctionDurationNotInFullMinutes(uint40 auctionDuration) external { @@ -251,9 +237,7 @@ contract PWNSimpleLoanDutchAuctionProposal_GetCreditAmount_Test is PWNSimpleLoan proposal.auctionDuration = auctionDuration; vm.expectRevert(abi.encodeWithSelector(AuctionDurationNotInFullMinutes.selector, auctionDuration)); - proposalContract.acceptProposal( - proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, "" - ); + proposalContract.getCreditAmount(proposal, 0); } function testFuzz_shouldFail_whenInvalidCreditAmountRange(uint256 minCreditAmount, uint256 maxCreditAmount) external { @@ -262,9 +246,7 @@ contract PWNSimpleLoanDutchAuctionProposal_GetCreditAmount_Test is PWNSimpleLoan proposal.maxCreditAmount = maxCreditAmount; vm.expectRevert(abi.encodeWithSelector(InvalidCreditAmountRange.selector, minCreditAmount, maxCreditAmount)); - proposalContract.acceptProposal( - proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, "" - ); + proposalContract.getCreditAmount(proposal, 0); } function testFuzz_shouldFail_whenAuctionNotInProgress(uint40 auctionStart, uint256 time) external { @@ -347,214 +329,75 @@ contract PWNSimpleLoanDutchAuctionProposal_GetCreditAmount_Test is PWNSimpleLoan /*----------------------------------------------------------*| -|* # ACCEPT PROPOSAL *| +|* # ACCEPT PROPOSAL AND REVOKE CALLERS NONCE *| |*----------------------------------------------------------*/ -contract PWNSimpleLoanDutchAuctionProposal_AcceptProposal_Test is PWNSimpleLoanDutchAuctionProposalTest, PWNSimpleLoanProposal_AcceptProposal_Test { +contract PWNSimpleLoanDutchAuctionProposal_AcceptProposalAndRevokeCallersNonce_Test is PWNSimpleLoanDutchAuctionProposalTest, PWNSimpleLoanProposal_AcceptProposalAndRevokeCallersNonce_Test { function setUp() virtual public override(PWNSimpleLoanDutchAuctionProposalTest, 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 testFuzz_shouldFail_whenCurrentAuctionCreditAmountNotInIntendedCreditAmountRange_whenOffer( - uint256 intendedCreditAmount - ) external { - proposal.isOffer = true; - proposal.minCreditAmount = 0; - proposal.maxCreditAmount = 100000; - proposal.auctionStart = 1; - proposal.auctionDuration = 100 minutes; - - vm.warp(proposal.auctionStart + proposal.auctionDuration / 2); - - proposalValues.slippage = 500; - intendedCreditAmount = bound(intendedCreditAmount, 0, type(uint256).max - proposalValues.slippage); - proposalValues.intendedCreditAmount = intendedCreditAmount; - - uint256 auctionCreditAmount = proposalContract.getCreditAmount(proposal, block.timestamp); - - vm.assume( - intendedCreditAmount < auctionCreditAmount - proposalValues.slippage - || intendedCreditAmount > auctionCreditAmount - ); - - vm.expectRevert(abi.encodeWithSelector( - InvalidCreditAmount.selector, auctionCreditAmount, proposalValues.intendedCreditAmount, proposalValues.slippage - )); - proposalContract.acceptProposal( - proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, "" - ); - } - - function testFuzz_shouldFail_whenCurrentAuctionCreditAmountNotInIntendedCreditAmountRange_whenRequest( - uint256 intendedCreditAmount - ) external { - proposal.isOffer = false; - proposal.minCreditAmount = 0; - proposal.maxCreditAmount = 100000; - proposal.auctionStart = 1; - proposal.auctionDuration = 100 minutes; - - vm.warp(proposal.auctionStart + proposal.auctionDuration / 2); - - proposalValues.slippage = 500; - intendedCreditAmount = bound(intendedCreditAmount, proposalValues.slippage, type(uint256).max); - proposalValues.intendedCreditAmount = intendedCreditAmount; - - uint256 auctionCreditAmount = proposalContract.getCreditAmount(proposal, block.timestamp); - - vm.assume( - intendedCreditAmount < auctionCreditAmount - || intendedCreditAmount - proposalValues.slippage > auctionCreditAmount - ); - - vm.expectRevert(abi.encodeWithSelector( - InvalidCreditAmount.selector, auctionCreditAmount, proposalValues.intendedCreditAmount, proposalValues.slippage - )); - proposalContract.acceptProposal( - proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, "" - ); - } - - function testFuzz_shouldCallLoanContractWithLoanTerms( - uint256 minCreditAmount, uint256 maxCreditAmount, uint40 auctionDuration, uint256 timeInAuction, 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.minCreditAmount = minCreditAmount; - proposal.maxCreditAmount = maxCreditAmount; - proposal.auctionStart = 1; - proposal.auctionDuration = auctionDuration; - - vm.warp(proposal.auctionStart + timeInAuction); - - proposalValues.intendedCreditAmount = proposalContract.getCreditAmount(proposal, block.timestamp); - 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.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 *| +|* # ACCEPT PROPOSAL *| |*----------------------------------------------------------*/ -contract PWNSimpleLoanDutchAuctionProposal_AcceptProposalAndRevokeCallersNonce_Test is PWNSimpleLoanDutchAuctionProposalTest, PWNSimpleLoanProposal_AcceptProposalAndRevokeCallersNonce_Test { +contract PWNSimpleLoanDutchAuctionProposal_AcceptProposal_Test is PWNSimpleLoanDutchAuctionProposalTest, PWNSimpleLoanProposal_AcceptProposal_Test { function setUp() virtual public override(PWNSimpleLoanDutchAuctionProposalTest, PWNSimpleLoanProposalTest) { super.setUp(); } -} - - -/*----------------------------------------------------------*| -|* # ACCEPT REFINANCE PROPOSAL *| -|*----------------------------------------------------------*/ -contract PWNSimpleLoanDutchAuctionProposal_AcceptRefinanceProposal_Test is PWNSimpleLoanDutchAuctionProposalTest, PWNSimpleLoanProposal_AcceptRefinanceProposal_Test { + function testFuzz_shouldFail_whenProposedRefinancingLoanIdNotZero_whenRefinancingLoanIdZero(uint256 proposedRefinancingLoanId) external { + vm.assume(proposedRefinancingLoanId != 0); + proposal.refinancingLoanId = proposedRefinancingLoanId; - function setUp() virtual public override(PWNSimpleLoanDutchAuctionProposalTest, PWNSimpleLoanProposalTest) { - super.setUp(); - - proposal.refinancingLoanId = loanId; + vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, proposedRefinancingLoanId)); + proposalContract.acceptProposal( + proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), 0, permit, "" + ); } - - function testFuzz_shouldFail_whenRefinancingLoanIdIsNotEqualToLoanId_whenRefinanceingLoanIdNotZero_whenOffer( - uint256 _loanId, uint256 _refinancingLoanId + function testFuzz_shouldFail_whenRefinancingLoanIdsIsNotEqual_whenProposedRefinanceingLoanIdNotZero_whenRefinancingLoanIdNotZero_whenOffer( + uint256 refinancingLoanId, uint256 proposedRefinancingLoanId ) external { - vm.assume(_refinancingLoanId != 0); - vm.assume(_loanId != _refinancingLoanId); - proposal.refinancingLoanId = _refinancingLoanId; + vm.assume(proposedRefinancingLoanId != 0); + vm.assume(refinancingLoanId != proposedRefinancingLoanId); + proposal.refinancingLoanId = proposedRefinancingLoanId; proposal.isOffer = true; - vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, _refinancingLoanId)); - proposalContract.acceptRefinanceProposal( - _loanId, proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, extra + vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, proposedRefinancingLoanId)); + proposalContract.acceptProposal( + proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), refinancingLoanId, permit, extra ); } - function testFuzz_shouldPass_whenRefinancingLoanIdIsNotEqualToLoanId_whenRefinanceingLoanIdZero_whenOffer( - uint256 _loanId + function testFuzz_shouldPass_whenRefinancingLoanIdsNotEqual_whenProposedRefinanceingLoanIdZero_whenRefinancingLoanIdNotZero_whenOffer( + uint256 refinancingLoanId ) external { - vm.assume(_loanId != 0); + vm.assume(refinancingLoanId != 0); proposal.refinancingLoanId = 0; proposal.isOffer = true; - proposalContract.acceptRefinanceProposal( - _loanId, proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, extra + proposalContract.acceptProposal( + proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), refinancingLoanId, permit, extra ); } - function testFuzz_shouldFail_whenRefinancingLoanIdIsNotEqualToLoanId_whenNotOffer( - uint256 _loanId, uint256 _refinancingLoanId + function testFuzz_shouldFail_whenRefinancingLoanIdsNotEqual_whenRefinancingLoanIdNotZero_whenRequest( + uint256 refinancingLoanId, uint256 proposedRefinancingLoanId ) external { - vm.assume(_loanId != _refinancingLoanId); - proposal.refinancingLoanId = _refinancingLoanId; + vm.assume(refinancingLoanId != proposedRefinancingLoanId); + proposal.refinancingLoanId = proposedRefinancingLoanId; proposal.isOffer = false; - vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, _refinancingLoanId)); - proposalContract.acceptRefinanceProposal( - _loanId, proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, extra + vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, proposedRefinancingLoanId)); + proposalContract.acceptProposal( + proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), refinancingLoanId, permit, extra ); } @@ -583,8 +426,8 @@ contract PWNSimpleLoanDutchAuctionProposal_AcceptRefinanceProposal_Test is PWNSi vm.expectRevert(abi.encodeWithSelector( InvalidCreditAmount.selector, auctionCreditAmount, proposalValues.intendedCreditAmount, proposalValues.slippage )); - proposalContract.acceptRefinanceProposal( - loanId, proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, extra + proposalContract.acceptProposal( + proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), 0, permit, "" ); } @@ -613,19 +456,25 @@ contract PWNSimpleLoanDutchAuctionProposal_AcceptRefinanceProposal_Test is PWNSi vm.expectRevert(abi.encodeWithSelector( InvalidCreditAmount.selector, auctionCreditAmount, proposalValues.intendedCreditAmount, proposalValues.slippage )); - proposalContract.acceptRefinanceProposal( - loanId, proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, extra + proposalContract.acceptProposal( + proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), 0, permit, "" ); } function testFuzz_shouldCallLoanContractWithLoanTerms( - uint256 minCreditAmount, uint256 maxCreditAmount, uint40 auctionDuration, uint256 timeInAuction, bool isOffer + uint256 minCreditAmount, + uint256 maxCreditAmount, + uint40 auctionDuration, + uint256 timeInAuction, + bool isOffer, + uint256 refinancingLoanId ) 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; @@ -669,29 +518,19 @@ contract PWNSimpleLoanDutchAuctionProposal_AcceptRefinanceProposal_Test is PWNSi vm.expectCall( activeLoanContract, - abi.encodeWithSelector( - PWNSimpleLoan.refinanceLOAN.selector, - loanId, _proposalHash(proposal), loanTerms, permit, extra + 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.acceptRefinanceProposal( - loanId, proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, extra + proposalContract.acceptProposal( + proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), refinancingLoanId, permit, extra ); } } - - -/*----------------------------------------------------------*| -|* # ACCEPT REFINANCE PROPOSAL AND REVOKE CALLERS NONCE *| -|*----------------------------------------------------------*/ - -contract PWNSimpleLoanDutchAuctionProposal_AcceptRefinanceProposalAndRevokeCallersNonce_Test is PWNSimpleLoanDutchAuctionProposalTest, PWNSimpleLoanProposal_AcceptRefinanceProposalAndRevokeCallersNonce_Test { - - function setUp() virtual public override(PWNSimpleLoanDutchAuctionProposalTest, PWNSimpleLoanProposalTest) { - super.setUp(); - } - -} diff --git a/test/unit/PWNSimpleLoanFungibleProposal.t.sol b/test/unit/PWNSimpleLoanFungibleProposal.t.sol index b8b011e..f95f163 100644 --- a/test/unit/PWNSimpleLoanFungibleProposal.t.sol +++ b/test/unit/PWNSimpleLoanFungibleProposal.t.sol @@ -15,9 +15,7 @@ import "@pwn/PWNErrors.sol"; import { PWNSimpleLoanProposalTest, PWNSimpleLoanProposal_AcceptProposal_Test, - PWNSimpleLoanProposal_AcceptProposalAndRevokeCallersNonce_Test, - PWNSimpleLoanProposal_AcceptRefinanceProposal_Test, - PWNSimpleLoanProposal_AcceptRefinanceProposalAndRevokeCallersNonce_Test + PWNSimpleLoanProposal_AcceptProposalAndRevokeCallersNonce_Test } from "@pwn-test/unit/PWNSimpleLoanProposal.t.sol"; @@ -110,22 +108,12 @@ abstract contract PWNSimpleLoanFungibleProposalTest is PWNSimpleLoanProposalTest function _callAcceptProposalWith(Params memory _params, Permit memory _permit) internal override returns (uint256) { _updateProposal(_params); - return proposalContract.acceptProposal(proposal, proposalValues, _proposalSignature(params), _permit, ""); + 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), _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); + return proposalContract.acceptProposal(proposal, proposalValues, _proposalSignature(params), 0, _permit, "", nonceSpace, nonce); } function _getProposalHashWith(Params memory _params) internal override returns (bytes32) { @@ -242,166 +230,75 @@ contract PWNSimpleLoanFungibleProposal_GetCreditAmount_Test is PWNSimpleLoanFung /*----------------------------------------------------------*| -|* # ACCEPT PROPOSAL *| +|* # ACCEPT PROPOSAL AND REVOKE CALLERS NONCE *| |*----------------------------------------------------------*/ -contract PWNSimpleLoanFungibleProposal_AcceptProposal_Test is PWNSimpleLoanFungibleProposalTest, PWNSimpleLoanProposal_AcceptProposal_Test { +contract PWNSimpleLoanFungibleProposal_AcceptProposalAndRevokeCallersNonce_Test is PWNSimpleLoanFungibleProposalTest, PWNSimpleLoanProposal_AcceptProposalAndRevokeCallersNonce_Test { function setUp() virtual public override(PWNSimpleLoanFungibleProposalTest, 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_shouldFail_whenZeroMinCollateralAmount() external { - proposal.minCollateralAmount = 0; - - vm.expectRevert(abi.encodeWithSelector(MinCollateralAmountNotSet.selector)); - proposalContract.acceptProposal( - proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, "" - ); - } - - function testFuzz_shouldFail_whenCollateralAmountLessThanMinCollateralAmount( - uint256 minCollateralAmount, uint256 collateralAmount - ) external { - proposal.minCollateralAmount = bound(minCollateralAmount, 1, type(uint256).max); - proposalValues.collateralAmount = bound(collateralAmount, 0, proposal.minCollateralAmount - 1); - - vm.expectRevert(abi.encodeWithSelector( - InsufficientCollateralAmount.selector, proposalValues.collateralAmount, proposal.minCollateralAmount - )); - proposalContract.acceptProposal( - proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, "" - ); - } - - function testFuzz_shouldCallLoanContractWithLoanTerms( - 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; - - 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, - 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 *| +|* # ACCEPT PROPOSAL *| |*----------------------------------------------------------*/ -contract PWNSimpleLoanFungibleProposal_AcceptProposalAndRevokeCallersNonce_Test is PWNSimpleLoanFungibleProposalTest, PWNSimpleLoanProposal_AcceptProposalAndRevokeCallersNonce_Test { +contract PWNSimpleLoanFungibleProposal_AcceptProposal_Test is PWNSimpleLoanFungibleProposalTest, PWNSimpleLoanProposal_AcceptProposal_Test { function setUp() virtual public override(PWNSimpleLoanFungibleProposalTest, PWNSimpleLoanProposalTest) { super.setUp(); } -} - - -/*----------------------------------------------------------*| -|* # ACCEPT REFINANCE PROPOSAL *| -|*----------------------------------------------------------*/ - -contract PWNSimpleLoanFungibleProposal_AcceptRefinanceProposal_Test is PWNSimpleLoanFungibleProposalTest, PWNSimpleLoanProposal_AcceptRefinanceProposal_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; - proposal.refinancingLoanId = loanId; + vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, proposedRefinancingLoanId)); + proposalContract.acceptProposal( + proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), 0, permit, "" + ); } - - function testFuzz_shouldFail_whenRefinancingLoanIdIsNotEqualToLoanId_whenRefinanceingLoanIdNotZero_whenOffer( - uint256 _loanId, uint256 _refinancingLoanId + function testFuzz_shouldFail_whenRefinancingLoanIdsIsNotEqual_whenProposedRefinanceingLoanIdNotZero_whenRefinancingLoanIdNotZero_whenOffer( + uint256 refinancingLoanId, uint256 proposedRefinancingLoanId ) external { - vm.assume(_refinancingLoanId != 0); - vm.assume(_loanId != _refinancingLoanId); - proposal.refinancingLoanId = _refinancingLoanId; + vm.assume(proposedRefinancingLoanId != 0); + vm.assume(refinancingLoanId != proposedRefinancingLoanId); + proposal.refinancingLoanId = proposedRefinancingLoanId; proposal.isOffer = true; - vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, _refinancingLoanId)); - proposalContract.acceptRefinanceProposal( - _loanId, proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, extra + vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, proposedRefinancingLoanId)); + proposalContract.acceptProposal( + proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), refinancingLoanId, permit, extra ); } - function testFuzz_shouldPass_whenRefinancingLoanIdIsNotEqualToLoanId_whenRefinanceingLoanIdZero_whenOffer( - uint256 _loanId + function testFuzz_shouldPass_whenRefinancingLoanIdsNotEqual_whenProposedRefinanceingLoanIdZero_whenRefinancingLoanIdNotZero_whenOffer( + uint256 refinancingLoanId ) external { - vm.assume(_loanId != 0); + vm.assume(refinancingLoanId != 0); proposal.refinancingLoanId = 0; proposal.isOffer = true; - proposalContract.acceptRefinanceProposal( - _loanId, proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, extra + proposalContract.acceptProposal( + proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), refinancingLoanId, permit, extra ); } - function testFuzz_shouldFail_whenRefinancingLoanIdIsNotEqualToLoanId_whenNotOffer( - uint256 _loanId, uint256 _refinancingLoanId + function testFuzz_shouldFail_whenRefinancingLoanIdsNotEqual_whenRefinancingLoanIdNotZero_whenRequest( + uint256 refinancingLoanId, uint256 proposedRefinancingLoanId ) external { - vm.assume(_loanId != _refinancingLoanId); - proposal.refinancingLoanId = _refinancingLoanId; + vm.assume(refinancingLoanId != proposedRefinancingLoanId); + proposal.refinancingLoanId = proposedRefinancingLoanId; proposal.isOffer = false; - vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, _refinancingLoanId)); - proposalContract.acceptRefinanceProposal( - _loanId, proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, extra + vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, proposedRefinancingLoanId)); + proposalContract.acceptProposal( + proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), refinancingLoanId, permit, extra ); } @@ -409,8 +306,8 @@ contract PWNSimpleLoanFungibleProposal_AcceptRefinanceProposal_Test is PWNSimple proposal.minCollateralAmount = 0; vm.expectRevert(abi.encodeWithSelector(MinCollateralAmountNotSet.selector)); - proposalContract.acceptRefinanceProposal( - loanId, proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, "" + proposalContract.acceptProposal( + proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), 0, permit, "" ); } @@ -423,17 +320,18 @@ contract PWNSimpleLoanFungibleProposal_AcceptRefinanceProposal_Test is PWNSimple vm.expectRevert(abi.encodeWithSelector( InsufficientCollateralAmount.selector, proposalValues.collateralAmount, proposal.minCollateralAmount )); - proposalContract.acceptRefinanceProposal( - loanId, proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, "" + proposalContract.acceptProposal( + proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), 0, permit, "" ); } function testFuzz_shouldCallLoanContractWithLoanTerms( - uint256 collateralAmount, uint256 creditPerCollateralUnit, bool isOffer + uint256 collateralAmount, uint256 creditPerCollateralUnit, bool isOffer, uint256 refinancingLoanId ) 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, @@ -468,29 +366,19 @@ contract PWNSimpleLoanFungibleProposal_AcceptRefinanceProposal_Test is PWNSimple vm.expectCall( activeLoanContract, - abi.encodeWithSelector( - PWNSimpleLoan.refinanceLOAN.selector, - loanId, _proposalHash(proposal), loanTerms, permit, extra + 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.acceptRefinanceProposal( - loanId, proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, extra + proposalContract.acceptProposal( + proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), refinancingLoanId, permit, extra ); } } - - -/*----------------------------------------------------------*| -|* # ACCEPT REFINANCE PROPOSAL AND REVOKE CALLERS NONCE *| -|*----------------------------------------------------------*/ - -contract PWNSimpleLoanFungibleProposal_AcceptRefinanceProposalAndRevokeCallersNonce_Test is PWNSimpleLoanFungibleProposalTest, PWNSimpleLoanProposal_AcceptRefinanceProposalAndRevokeCallersNonce_Test { - - function setUp() virtual public override(PWNSimpleLoanFungibleProposalTest, PWNSimpleLoanProposalTest) { - super.setUp(); - } - -} diff --git a/test/unit/PWNSimpleLoanListProposal.t.sol b/test/unit/PWNSimpleLoanListProposal.t.sol index c2f97ba..ea4bd0d 100644 --- a/test/unit/PWNSimpleLoanListProposal.t.sol +++ b/test/unit/PWNSimpleLoanListProposal.t.sol @@ -15,9 +15,7 @@ import "@pwn/PWNErrors.sol"; import { PWNSimpleLoanProposalTest, PWNSimpleLoanProposal_AcceptProposal_Test, - PWNSimpleLoanProposal_AcceptProposalAndRevokeCallersNonce_Test, - PWNSimpleLoanProposal_AcceptRefinanceProposal_Test, - PWNSimpleLoanProposal_AcceptRefinanceProposalAndRevokeCallersNonce_Test + PWNSimpleLoanProposal_AcceptProposalAndRevokeCallersNonce_Test } from "@pwn-test/unit/PWNSimpleLoanProposal.t.sol"; @@ -110,22 +108,12 @@ abstract contract PWNSimpleLoanListProposalTest is PWNSimpleLoanProposalTest { function _callAcceptProposalWith(Params memory _params, Permit memory _permit) internal override returns (uint256) { _updateProposal(_params); - return proposalContract.acceptProposal(proposal, proposalValues, _proposalSignature(params), _permit, ""); + 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), _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); + return proposalContract.acceptProposal(proposal, proposalValues, _proposalSignature(params), 0, _permit, "", nonceSpace, nonce); } function _getProposalHashWith(Params memory _params) internal override returns (bytes32) { @@ -221,177 +209,75 @@ contract PWNSimpleLoanListProposal_MakeProposal_Test is PWNSimpleLoanListProposa /*----------------------------------------------------------*| -|* # ACCEPT PROPOSAL *| +|* # ACCEPT PROPOSAL AND REVOKE CALLERS NONCE *| |*----------------------------------------------------------*/ -contract PWNSimpleLoanListProposal_AcceptProposal_Test is PWNSimpleLoanListProposalTest, PWNSimpleLoanProposal_AcceptProposal_Test { +contract PWNSimpleLoanListProposal_AcceptProposalAndRevokeCallersNonce_Test is PWNSimpleLoanListProposalTest, PWNSimpleLoanProposal_AcceptProposalAndRevokeCallersNonce_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 *| +|* # ACCEPT PROPOSAL *| |*----------------------------------------------------------*/ -contract PWNSimpleLoanListProposal_AcceptProposalAndRevokeCallersNonce_Test is PWNSimpleLoanListProposalTest, PWNSimpleLoanProposal_AcceptProposalAndRevokeCallersNonce_Test { +contract PWNSimpleLoanListProposal_AcceptProposal_Test is PWNSimpleLoanListProposalTest, PWNSimpleLoanProposal_AcceptProposal_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(); + function testFuzz_shouldFail_whenProposedRefinancingLoanIdNotZero_whenRefinancingLoanIdZero(uint256 proposedRefinancingLoanId) external { + vm.assume(proposedRefinancingLoanId != 0); + proposal.refinancingLoanId = proposedRefinancingLoanId; - proposal.refinancingLoanId = loanId; + vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, proposedRefinancingLoanId)); + proposalContract.acceptProposal( + proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), 0, permit, "" + ); } - - function testFuzz_shouldFail_whenRefinancingLoanIdIsNotEqualToLoanId_whenRefinanceingLoanIdNotZero_whenOffer( - uint256 _loanId, uint256 _refinancingLoanId + function testFuzz_shouldFail_whenRefinancingLoanIdsIsNotEqual_whenProposedRefinanceingLoanIdNotZero_whenRefinancingLoanIdNotZero_whenOffer( + uint256 refinancingLoanId, uint256 proposedRefinancingLoanId ) external { - vm.assume(_refinancingLoanId != 0); - vm.assume(_loanId != _refinancingLoanId); - proposal.refinancingLoanId = _refinancingLoanId; + vm.assume(proposedRefinancingLoanId != 0); + vm.assume(refinancingLoanId != proposedRefinancingLoanId); + proposal.refinancingLoanId = proposedRefinancingLoanId; proposal.isOffer = true; - vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, _refinancingLoanId)); - proposalContract.acceptRefinanceProposal( - _loanId, proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, extra + vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, proposedRefinancingLoanId)); + proposalContract.acceptProposal( + proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), refinancingLoanId, permit, extra ); } - function testFuzz_shouldPass_whenRefinancingLoanIdIsNotEqualToLoanId_whenRefinanceingLoanIdZero_whenOffer( - uint256 _loanId + function testFuzz_shouldPass_whenRefinancingLoanIdsNotEqual_whenProposedRefinanceingLoanIdZero_whenRefinancingLoanIdNotZero_whenOffer( + uint256 refinancingLoanId ) external { - vm.assume(_loanId != 0); + vm.assume(refinancingLoanId != 0); proposal.refinancingLoanId = 0; proposal.isOffer = true; - proposalContract.acceptRefinanceProposal( - _loanId, proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, extra + proposalContract.acceptProposal( + proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), refinancingLoanId, permit, extra ); } - function testFuzz_shouldFail_whenRefinancingLoanIdIsNotEqualToLoanId_whenNotOffer( - uint256 _loanId, uint256 _refinancingLoanId + function testFuzz_shouldFail_whenRefinancingLoanIdsNotEqual_whenRefinancingLoanIdNotZero_whenRequest( + uint256 refinancingLoanId, uint256 proposedRefinancingLoanId ) external { - vm.assume(_loanId != _refinancingLoanId); - proposal.refinancingLoanId = _refinancingLoanId; + vm.assume(refinancingLoanId != proposedRefinancingLoanId); + proposal.refinancingLoanId = proposedRefinancingLoanId; proposal.isOffer = false; - vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, _refinancingLoanId)); - proposalContract.acceptRefinanceProposal( - _loanId, proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, extra + vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, proposedRefinancingLoanId)); + proposalContract.acceptProposal( + proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), refinancingLoanId, permit, extra ); } @@ -399,8 +285,8 @@ contract PWNSimpleLoanListProposal_AcceptRefinanceProposal_Test is PWNSimpleLoan proposalValues.collateralId = 331; proposal.collateralIdsWhitelistMerkleRoot = bytes32(0); - proposalContract.acceptRefinanceProposal( - loanId, proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, extra + proposalContract.acceptProposal( + proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), 0, permit, "" ); } @@ -413,8 +299,8 @@ contract PWNSimpleLoanListProposal_AcceptRefinanceProposal_Test is PWNSimpleLoan proposalValues.merkleInclusionProof = new bytes32[](1); proposalValues.merkleInclusionProof[0] = id2Hash; - proposalContract.acceptRefinanceProposal( - loanId, proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, extra + proposalContract.acceptProposal( + proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), 0, permit, "" ); } @@ -428,13 +314,14 @@ contract PWNSimpleLoanListProposal_AcceptRefinanceProposal_Test is PWNSimpleLoan proposalValues.merkleInclusionProof[0] = id2Hash; vm.expectRevert(abi.encodeWithSelector(CollateralIdNotWhitelisted.selector, proposalValues.collateralId)); - proposalContract.acceptRefinanceProposal( - loanId, proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, extra + proposalContract.acceptProposal( + proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), 0, permit, "" ); } - function testFuzz_shouldCallLoanContractWithLoanTerms(bool isOffer) external { + function testFuzz_shouldCallLoanContractWithLoanTerms(bool isOffer, uint256 refinancingLoanId) external { proposal.isOffer = isOffer; + proposal.refinancingLoanId = refinancingLoanId; permit = Permit({ asset: token, @@ -469,29 +356,19 @@ contract PWNSimpleLoanListProposal_AcceptRefinanceProposal_Test is PWNSimpleLoan vm.expectCall( activeLoanContract, - abi.encodeWithSelector( - PWNSimpleLoan.refinanceLOAN.selector, - loanId, _proposalHash(proposal), loanTerms, permit, extra + 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.acceptRefinanceProposal( - loanId, proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, extra + proposalContract.acceptProposal( + proposal, proposalValues, _signProposalHash(proposerPK, _proposalHash(proposal)), refinancingLoanId, 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/PWNSimpleLoanProposal.t.sol b/test/unit/PWNSimpleLoanProposal.t.sol index e229a31..9d48ea2 100644 --- a/test/unit/PWNSimpleLoanProposal.t.sol +++ b/test/unit/PWNSimpleLoanProposal.t.sol @@ -29,7 +29,6 @@ abstract contract PWNSimpleLoanProposalTest is Test { uint256 public acceptorPK = 32716637; address public acceptor = vm.addr(acceptorPK); uint256 public loanId = 421; - uint256 public refinancedLoanId = 123; Params public params; Permit public permit; @@ -98,7 +97,7 @@ abstract contract PWNSimpleLoanProposalTest is Test { activeLoanContract, abi.encodeWithSelector(PWNSimpleLoan.createLOAN.selector), abi.encode(loanId) ); vm.mockCall( - activeLoanContract, abi.encodeWithSelector(PWNSimpleLoan.refinanceLOAN.selector), abi.encode(refinancedLoanId) + activeLoanContract, abi.encodeWithSelector(PWNSimpleLoan.refinanceLOAN.selector), abi.encode(loanId) ); } @@ -112,262 +111,18 @@ abstract contract PWNSimpleLoanProposalTest is Test { return abi.encodePacked(r, bytes32(uint256(v) - 27) << 255 | s); } - - 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 _callAcceptRefinanceProposalWith(uint256 loanId, Params memory _params, Permit memory _permit) internal virtual returns (uint256); - function _callAcceptRefinanceProposalWith(uint256 loanId, Params memory _params, Permit memory _permit, uint256 nonceSpace, uint256 nonce) internal virtual returns (uint256); - function _getProposalHashWith(Params memory _params) internal virtual returns (bytes32); - - function _callAcceptProposalWith() internal returns (uint256) { return _callAcceptProposalWith(params, permit); } - function _callAcceptRefinanceProposalWith() internal returns (uint256) { - return _callAcceptRefinanceProposalWith(loanId, params, permit); - } - function _getProposalHashWith() internal returns (bytes32) { return _getProposalHashWith(params); } -} - - -/*----------------------------------------------------------*| -|* # 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 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)); - _callAcceptProposalWith(); - } - - function testFuzz_shouldFail_whenComputerReturnsDifferentStateFingerprint_whenShouldCheckStateFingerprint( - bytes32 stateFingerprint - ) external { - vm.assume(stateFingerprint != params.collateralStateFingerprint); - - vm.mockCall( - stateFingerprintComputer, - abi.encodeWithSignature("getStateFingerprint(uint256)"), - abi.encode(stateFingerprint) - ); - - vm.expectRevert(abi.encodeWithSelector( - InvalidCollateralStateFingerprint.selector, stateFingerprint, params.collateralStateFingerprint - )); - _callAcceptProposalWith(); - } - - function test_shouldFail_whenInvalidSignature_whenEOA() external { - params.signerPK = 1; - - vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, proposer, _getProposalHashWith(params))); - _callAcceptProposalWith(); - } - - function test_shouldFail_whenInvalidSignature_whenContractAccount() external { - vm.etch(proposer, bytes("data")); - params.signerPK = 0; - - vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, proposer, _getProposalHashWith(params))); - _callAcceptProposalWith(); - } - - function test_shouldPass_whenProposalMadeOnchain() external { - vm.store( - address(proposalContractAddr), - keccak256(abi.encode(_getProposalHashWith(params), PROPOSALS_MADE_SLOT)), - bytes32(uint256(1)) - ); - params.signerPK = 0; - - _callAcceptProposalWith(); - } - - function test_shouldPass_withValidSignature_whenEOA_whenStandardSignature() external { - params.compactSignature = false; - _callAcceptProposalWith(); - } - - function test_shouldPass_withValidSignature_whenEOA_whenCompactEIP2098Signature() external { - params.compactSignature = true; - _callAcceptProposalWith(); - } - - function test_shouldPass_whenValidSignature_whenContractAccount() external { - vm.etch(proposer, bytes("data")); - params.signerPK = 0; - - vm.mockCall( - proposer, - abi.encodeWithSignature("isValidSignature(bytes32,bytes)"), - abi.encode(bytes4(0x1626ba7e)) - ); - - _callAcceptProposalWith(); - } - - function testFuzz_shouldFail_whenProposalExpired(uint256 timestamp) external { - timestamp = bound(timestamp, params.expiration, type(uint256).max); - vm.warp(timestamp); - - vm.expectRevert(abi.encodeWithSelector(Expired.selector, timestamp, params.expiration)); - _callAcceptProposalWith(); - } - - function testFuzz_shouldFail_whenOfferNonceNotUsable(uint256 nonceSpace, uint256 nonce) external { - params.nonceSpace = nonceSpace; - params.nonce = nonce; - - vm.mockCall( - revokedNonce, - abi.encodeWithSignature("isNonceUsable(address,uint256,uint256)"), - abi.encode(false) - ); - vm.expectCall( - revokedNonce, - abi.encodeWithSignature("isNonceUsable(address,uint256,uint256)", proposer, nonceSpace, nonce) - ); - - vm.expectRevert(abi.encodeWithSelector( - NonceNotUsable.selector, proposer, nonceSpace, nonce - )); - _callAcceptProposalWith(); - } - - function testFuzz_shouldFail_whenCallerIsNotAllowedAcceptor(address caller) external { - address allowedAcceptor = makeAddr("allowedAcceptor"); - vm.assume(caller != allowedAcceptor); - params.allowedAcceptor = allowedAcceptor; - - 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)); - _callAcceptProposalWith(); - } - - function test_shouldRevokeOffer_whenAvailableCreditLimitEqualToZero(uint256 nonceSpace, uint256 nonce) external { - params.availableCreditLimit = 0; - params.nonceSpace = nonceSpace; - params.nonce = nonce; - - vm.expectCall( - revokedNonce, - abi.encodeWithSignature("revokeNonce(address,uint256,uint256)", proposer, nonceSpace, nonce) - ); - - _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); - - params.availableCreditLimit = limit; - - vm.store( - address(proposalContractAddr), - keccak256(abi.encode(_getProposalHashWith(params), CREDIT_USED_SLOT)), - bytes32(used) - ); - - vm.expectRevert(abi.encodeWithSelector(AvailableCreditLimitExceeded.selector, used + params.creditAmount, limit)); - _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); - - params.availableCreditLimit = limit; - - bytes32 proposalHash = _getProposalHashWith(params); - - vm.store( - address(proposalContractAddr), - keccak256(abi.encode(proposalHash, CREDIT_USED_SLOT)), - bytes32(used) - ); - - _callAcceptProposalWith(); - - assertEq(proposalContractAddr.creditUsed(proposalHash), used + params.creditAmount); - } - - function testFuzz_shouldFail_whenPermitOwnerNotCaller(address owner, address caller) external { - vm.assume(owner != caller && owner != address(0) && caller != address(0)); - - permit.owner = owner; - permit.asset = token; // test expects `token` being used as credit asset - - vm.expectRevert(abi.encodeWithSelector(InvalidPermitOwner.selector, owner, caller)); - vm.prank(caller); - _callAcceptProposalWith(); - } - - function testFuzz_shouldFail_whenPermitAssetNotCreditAsset(address asset, address caller) external { - vm.assume(asset != token && asset != address(0) && caller != address(0)); - - permit.owner = caller; - permit.asset = asset; // test expects `token` being used as credit asset - - vm.expectRevert(abi.encodeWithSelector(InvalidPermitAsset.selector, asset, token)); - vm.prank(caller); - _callAcceptProposalWith(); - } - - function test_shouldReturnNewLoanId() external { - assertEq(_callAcceptProposalWith(), loanId); - } + // 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 _getProposalHashWith(Params memory _params) internal virtual returns (bytes32); } @@ -409,17 +164,17 @@ abstract contract PWNSimpleLoanProposal_AcceptProposalAndRevokeCallersNonce_Test /*----------------------------------------------------------*| -|* # ACCEPT REFINANCE PROPOSAL *| +|* # ACCEPT PROPOSAL *| |*----------------------------------------------------------*/ -abstract contract PWNSimpleLoanProposal_AcceptRefinanceProposal_Test is PWNSimpleLoanProposalTest { +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)); - _callAcceptRefinanceProposalWith(); + _callAcceptProposalWith(); } function test_shouldNotCallComputerRegistry_whenShouldNotCheckStateFingerprint() external { @@ -431,7 +186,7 @@ abstract contract PWNSimpleLoanProposal_AcceptRefinanceProposal_Test is PWNSimpl count: 0 }); - _callAcceptRefinanceProposalWith(); + _callAcceptProposalWith(); } function test_shouldFail_whenComputerRegistryReturnsZeroAddress_whenShouldCheckStateFingerprint() external { @@ -442,7 +197,7 @@ abstract contract PWNSimpleLoanProposal_AcceptRefinanceProposal_Test is PWNSimpl ); vm.expectRevert(abi.encodeWithSelector(MissingStateFingerprintComputer.selector)); - _callAcceptRefinanceProposalWith(); + _callAcceptProposalWith(); } function testFuzz_shouldFail_whenComputerReturnsDifferentStateFingerprint_whenShouldCheckStateFingerprint( @@ -459,14 +214,14 @@ abstract contract PWNSimpleLoanProposal_AcceptRefinanceProposal_Test is PWNSimpl vm.expectRevert(abi.encodeWithSelector( InvalidCollateralStateFingerprint.selector, stateFingerprint, params.collateralStateFingerprint )); - _callAcceptRefinanceProposalWith(); + _callAcceptProposalWith(); } function test_shouldFail_whenInvalidSignature_whenEOA() external { params.signerPK = 1; vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, proposer, _getProposalHashWith(params))); - _callAcceptRefinanceProposalWith(); + _callAcceptProposalWith(); } function test_shouldFail_whenInvalidSignature_whenContractAccount() external { @@ -474,7 +229,7 @@ abstract contract PWNSimpleLoanProposal_AcceptRefinanceProposal_Test is PWNSimpl params.signerPK = 0; vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, proposer, _getProposalHashWith(params))); - _callAcceptRefinanceProposalWith(); + _callAcceptProposalWith(); } function test_shouldPass_whenProposalMadeOnchain() external { @@ -485,17 +240,17 @@ abstract contract PWNSimpleLoanProposal_AcceptRefinanceProposal_Test is PWNSimpl ); params.signerPK = 0; - _callAcceptRefinanceProposalWith(); + _callAcceptProposalWith(); } function test_shouldPass_withValidSignature_whenEOA_whenStandardSignature() external { params.compactSignature = false; - _callAcceptRefinanceProposalWith(); + _callAcceptProposalWith(); } function test_shouldPass_withValidSignature_whenEOA_whenCompactEIP2098Signature() external { params.compactSignature = true; - _callAcceptRefinanceProposalWith(); + _callAcceptProposalWith(); } function test_shouldPass_whenValidSignature_whenContractAccount() external { @@ -508,7 +263,7 @@ abstract contract PWNSimpleLoanProposal_AcceptRefinanceProposal_Test is PWNSimpl abi.encode(bytes4(0x1626ba7e)) ); - _callAcceptRefinanceProposalWith(); + _callAcceptProposalWith(); } function testFuzz_shouldFail_whenProposalExpired(uint256 timestamp) external { @@ -516,7 +271,7 @@ abstract contract PWNSimpleLoanProposal_AcceptRefinanceProposal_Test is PWNSimpl vm.warp(timestamp); vm.expectRevert(abi.encodeWithSelector(Expired.selector, timestamp, params.expiration)); - _callAcceptRefinanceProposalWith(); + _callAcceptProposalWith(); } function testFuzz_shouldFail_whenOfferNonceNotUsable(uint256 nonceSpace, uint256 nonce) external { @@ -536,7 +291,7 @@ abstract contract PWNSimpleLoanProposal_AcceptRefinanceProposal_Test is PWNSimpl vm.expectRevert(abi.encodeWithSelector( NonceNotUsable.selector, proposer, nonceSpace, nonce )); - _callAcceptRefinanceProposalWith(); + _callAcceptProposalWith(); } function testFuzz_shouldFail_whenCallerIsNotAllowedAcceptor(address caller) external { @@ -546,7 +301,7 @@ abstract contract PWNSimpleLoanProposal_AcceptRefinanceProposal_Test is PWNSimpl vm.expectRevert(abi.encodeWithSelector(CallerNotAllowedAcceptor.selector, caller, allowedAcceptor)); vm.prank(caller); - _callAcceptRefinanceProposalWith(); + _callAcceptProposalWith(); } function testFuzz_shouldFail_whenLessThanMinDuration(uint256 duration) external { @@ -556,7 +311,7 @@ abstract contract PWNSimpleLoanProposal_AcceptRefinanceProposal_Test is PWNSimpl params.duration = uint32(duration); vm.expectRevert(abi.encodeWithSelector(InvalidDuration.selector, duration, minDuration)); - _callAcceptRefinanceProposalWith(); + _callAcceptProposalWith(); } function testFuzz_shouldFail_whenAccruingInterestAPROutOfBounds(uint256 interestAPR) external { @@ -565,7 +320,7 @@ abstract contract PWNSimpleLoanProposal_AcceptRefinanceProposal_Test is PWNSimpl params.accruingInterestAPR = uint40(interestAPR); vm.expectRevert(abi.encodeWithSelector(AccruingInterestAPROutOfBounds.selector, interestAPR, maxInterest)); - _callAcceptRefinanceProposalWith(); + _callAcceptProposalWith(); } function test_shouldRevokeOffer_whenAvailableCreditLimitEqualToZero(uint256 nonceSpace, uint256 nonce) external { @@ -578,7 +333,7 @@ abstract contract PWNSimpleLoanProposal_AcceptRefinanceProposal_Test is PWNSimpl abi.encodeWithSignature("revokeNonce(address,uint256,uint256)", proposer, nonceSpace, nonce) ); - _callAcceptRefinanceProposalWith(); + _callAcceptProposalWith(); } function testFuzz_shouldFail_whenUsedCreditExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { @@ -594,7 +349,7 @@ abstract contract PWNSimpleLoanProposal_AcceptRefinanceProposal_Test is PWNSimpl ); vm.expectRevert(abi.encodeWithSelector(AvailableCreditLimitExceeded.selector, used + params.creditAmount, limit)); - _callAcceptRefinanceProposalWith(); + _callAcceptProposalWith(); } function testFuzz_shouldIncreaseUsedCredit_whenUsedCreditNotExceedsAvailableCreditLimit(uint256 used, uint256 limit) external { @@ -611,7 +366,7 @@ abstract contract PWNSimpleLoanProposal_AcceptRefinanceProposal_Test is PWNSimpl bytes32(used) ); - _callAcceptRefinanceProposalWith(); + _callAcceptProposalWith(); assertEq(proposalContractAddr.creditUsed(proposalHash), used + params.creditAmount); } @@ -624,7 +379,7 @@ abstract contract PWNSimpleLoanProposal_AcceptRefinanceProposal_Test is PWNSimpl vm.expectRevert(abi.encodeWithSelector(InvalidPermitOwner.selector, owner, caller)); vm.prank(caller); - _callAcceptRefinanceProposalWith(); + _callAcceptProposalWith(); } function testFuzz_shouldFail_whenPermitAssetNotCreditAsset(address asset, address caller) external { @@ -635,47 +390,11 @@ abstract contract PWNSimpleLoanProposal_AcceptRefinanceProposal_Test is PWNSimpl vm.expectRevert(abi.encodeWithSelector(InvalidPermitAsset.selector, asset, token)); vm.prank(caller); - _callAcceptRefinanceProposalWith(); - } - - function test_shouldReturnRefinancedLoanId() external { - assertEq(_callAcceptRefinanceProposalWith(), refinancedLoanId); - } - -} - - -/*----------------------------------------------------------*| -|* # ACCEPT REFINANCE PROPOSAL AND REVOKE CALLERS NONCE *| -|*----------------------------------------------------------*/ - -abstract contract PWNSimpleLoanProposal_AcceptRefinanceProposalAndRevokeCallersNonce_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); - _callAcceptRefinanceProposalWith(loanId, 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); - _callAcceptRefinanceProposalWith(loanId, params, permit, nonceSpace, nonce); + _callAcceptProposalWith(); } - // function is calling `acceptRefinanceProposal`, no need to test it again - function test_shouldCallLoanContract() external { - assertEq(_callAcceptRefinanceProposalWith(loanId, params, permit, 1, 2), refinancedLoanId); + function test_shouldReturnNewLoanId() external { + assertEq(_callAcceptProposalWith(), loanId); } } diff --git a/test/unit/PWNSimpleLoanSimpleProposal.t.sol b/test/unit/PWNSimpleLoanSimpleProposal.t.sol index c7e6e86..ab6005a 100644 --- a/test/unit/PWNSimpleLoanSimpleProposal.t.sol +++ b/test/unit/PWNSimpleLoanSimpleProposal.t.sol @@ -13,9 +13,7 @@ import "@pwn/PWNErrors.sol"; import { PWNSimpleLoanProposalTest, PWNSimpleLoanProposal_AcceptProposal_Test, - PWNSimpleLoanProposal_AcceptProposalAndRevokeCallersNonce_Test, - PWNSimpleLoanProposal_AcceptRefinanceProposal_Test, - PWNSimpleLoanProposal_AcceptRefinanceProposalAndRevokeCallersNonce_Test + PWNSimpleLoanProposal_AcceptProposalAndRevokeCallersNonce_Test } from "@pwn-test/unit/PWNSimpleLoanProposal.t.sol"; @@ -102,22 +100,12 @@ abstract contract PWNSimpleLoanSimpleProposalTest is PWNSimpleLoanProposalTest { function _callAcceptProposalWith(Params memory _params, Permit memory _permit) internal override returns (uint256) { _updateProposal(_params); - return proposalContract.acceptProposal(proposal, _proposalSignature(params), _permit, ""); + 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), _permit, "", nonceSpace, nonce); - } - - function _callAcceptRefinanceProposalWith(uint256 loanId, Params memory _params, Permit memory _permit) internal override returns (uint256) { - _updateProposal(_params); - return proposalContract.acceptRefinanceProposal(loanId, proposal, _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, _proposalSignature(params), _permit, "", nonceSpace, nonce); + return proposalContract.acceptProposal(proposal, _proposalSignature(params), 0, _permit, "", nonceSpace, nonce); } function _getProposalHashWith(Params memory _params) internal override returns (bytes32) { @@ -213,144 +201,81 @@ contract PWNSimpleLoanSimpleProposal_MakeProposal_Test is PWNSimpleLoanSimplePro /*----------------------------------------------------------*| -|* # ACCEPT PROPOSAL *| +|* # ACCEPT PROPOSAL AND REVOKE CALLERS NONCE *| |*----------------------------------------------------------*/ -contract PWNSimpleLoanSimpleProposal_AcceptProposal_Test is PWNSimpleLoanSimpleProposalTest, PWNSimpleLoanProposal_AcceptProposal_Test { +contract PWNSimpleLoanSimpleProposal_AcceptProposalAndRevokeCallersNonce_Test is PWNSimpleLoanSimpleProposalTest, PWNSimpleLoanProposal_AcceptProposalAndRevokeCallersNonce_Test { function setUp() virtual public override(PWNSimpleLoanSimpleProposalTest, 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, _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: 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, - abi.encodeWithSelector( - PWNSimpleLoan.createLOAN.selector, - _proposalHash(proposal), loanTerms, permit, extra - ) - ); - - vm.prank(acceptor); - proposalContract.acceptProposal( - proposal, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, extra - ); - } - } /*----------------------------------------------------------*| -|* # ACCEPT PROPOSAL AND REVOKE CALLERS NONCE *| +|* # ACCEPT PROPOSAL *| |*----------------------------------------------------------*/ -contract PWNSimpleLoanSimpleProposal_AcceptProposalAndRevokeCallersNonce_Test is PWNSimpleLoanSimpleProposalTest, PWNSimpleLoanProposal_AcceptProposalAndRevokeCallersNonce_Test { +contract PWNSimpleLoanSimpleProposal_AcceptProposal_Test is PWNSimpleLoanSimpleProposalTest, PWNSimpleLoanProposal_AcceptProposal_Test { function setUp() virtual public override(PWNSimpleLoanSimpleProposalTest, PWNSimpleLoanProposalTest) { super.setUp(); } -} - - -/*----------------------------------------------------------*| -|* # ACCEPT REFINANCE PROPOSAL *| -|*----------------------------------------------------------*/ -contract PWNSimpleLoanSimpleProposal_AcceptRefinanceProposal_Test is PWNSimpleLoanSimpleProposalTest, PWNSimpleLoanProposal_AcceptRefinanceProposal_Test { + function testFuzz_shouldFail_whenProposedRefinancingLoanIdNotZero_whenRefinancingLoanIdZero(uint256 proposedRefinancingLoanId) external { + vm.assume(proposedRefinancingLoanId != 0); + proposal.refinancingLoanId = proposedRefinancingLoanId; - function setUp() virtual public override(PWNSimpleLoanSimpleProposalTest, PWNSimpleLoanProposalTest) { - super.setUp(); - - proposal.refinancingLoanId = loanId; + vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, proposedRefinancingLoanId)); + proposalContract.acceptProposal( + proposal, _signProposalHash(proposerPK, _proposalHash(proposal)), 0, permit, "" + ); } - - function testFuzz_shouldFail_whenRefinancingLoanIdIsNotEqualToLoanId_whenRefinanceingLoanIdNotZero_whenOffer( - uint256 _loanId, uint256 _refinancingLoanId + function testFuzz_shouldFail_whenRefinancingLoanIdsIsNotEqual_whenProposedRefinanceingLoanIdNotZero_whenRefinancingLoanIdNotZero_whenOffer( + uint256 refinancingLoanId, uint256 proposedRefinancingLoanId ) external { - vm.assume(_refinancingLoanId != 0); - vm.assume(_loanId != _refinancingLoanId); - proposal.refinancingLoanId = _refinancingLoanId; + vm.assume(proposedRefinancingLoanId != 0); + vm.assume(refinancingLoanId != proposedRefinancingLoanId); + proposal.refinancingLoanId = proposedRefinancingLoanId; proposal.isOffer = true; - vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, _refinancingLoanId)); - proposalContract.acceptRefinanceProposal( - _loanId, proposal, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, extra + vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, proposedRefinancingLoanId)); + proposalContract.acceptProposal( + proposal, _signProposalHash(proposerPK, _proposalHash(proposal)), refinancingLoanId, permit, extra ); } - function testFuzz_shouldPass_whenRefinancingLoanIdIsNotEqualToLoanId_whenRefinanceingLoanIdZero_whenOffer( - uint256 _loanId + function testFuzz_shouldPass_whenRefinancingLoanIdsNotEqual_whenProposedRefinanceingLoanIdZero_whenRefinancingLoanIdNotZero_whenOffer( + uint256 refinancingLoanId ) external { - vm.assume(_loanId != 0); + vm.assume(refinancingLoanId != 0); proposal.refinancingLoanId = 0; proposal.isOffer = true; - proposalContract.acceptRefinanceProposal( - _loanId, proposal, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, extra + proposalContract.acceptProposal( + proposal, _signProposalHash(proposerPK, _proposalHash(proposal)), refinancingLoanId, permit, extra ); } - function testFuzz_shouldFail_whenRefinancingLoanIdIsNotEqualToLoanId_whenNotOffer( - uint256 _loanId, uint256 _refinancingLoanId + function testFuzz_shouldFail_whenRefinancingLoanIdsNotEqual_whenRefinancingLoanIdNotZero_whenRequest( + uint256 refinancingLoanId, uint256 proposedRefinancingLoanId ) external { - vm.assume(_loanId != _refinancingLoanId); - proposal.refinancingLoanId = _refinancingLoanId; + vm.assume(refinancingLoanId != proposedRefinancingLoanId); + proposal.refinancingLoanId = proposedRefinancingLoanId; proposal.isOffer = false; - vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, _refinancingLoanId)); - proposalContract.acceptRefinanceProposal( - _loanId, proposal, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, extra + vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, proposedRefinancingLoanId)); + proposalContract.acceptProposal( + proposal, _signProposalHash(proposerPK, _proposalHash(proposal)), refinancingLoanId, permit, extra ); } - function testFuzz_shouldCallLoanContractWithLoanTerms(bool isOffer) external { + function testFuzz_shouldCallLoanContractWithLoanTerms(bool isOffer, uint256 refinancingLoanId) external { proposal.isOffer = isOffer; + proposal.refinancingLoanId = refinancingLoanId; permit = Permit({ asset: token, @@ -385,29 +310,19 @@ contract PWNSimpleLoanSimpleProposal_AcceptRefinanceProposal_Test is PWNSimpleLo vm.expectCall( activeLoanContract, - abi.encodeWithSelector( - PWNSimpleLoan.refinanceLOAN.selector, - loanId, _proposalHash(proposal), loanTerms, permit, extra + 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.acceptRefinanceProposal( - loanId, proposal, _signProposalHash(proposerPK, _proposalHash(proposal)), permit, extra + proposalContract.acceptProposal( + proposal, _signProposalHash(proposerPK, _proposalHash(proposal)), refinancingLoanId, permit, extra ); } } - - -/*----------------------------------------------------------*| -|* # ACCEPT REFINANCE PROPOSAL AND REVOKE CALLERS NONCE *| -|*----------------------------------------------------------*/ - -contract PWNSimpleLoanSimpleProposal_AcceptRefinanceProposalAndRevokeCallersNonce_Test is PWNSimpleLoanSimpleProposalTest, PWNSimpleLoanProposal_AcceptRefinanceProposalAndRevokeCallersNonce_Test { - - function setUp() virtual public override(PWNSimpleLoanSimpleProposalTest, PWNSimpleLoanProposalTest) { - super.setUp(); - } - -} From 8302cbafb2a88b21ed28f01337530d0266a3c499 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Wed, 27 Mar 2024 17:20:56 -0400 Subject: [PATCH 062/129] feat(revoked-nonce): enable tagged address revoke of users nonce in current nonce space --- src/nonce/PWNRevokedNonce.sol | 10 ++++++ test/unit/PWNRevokedNonce.t.sol | 54 +++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/src/nonce/PWNRevokedNonce.sol b/src/nonce/PWNRevokedNonce.sol index 486c04c..db68c50 100644 --- a/src/nonce/PWNRevokedNonce.sol +++ b/src/nonce/PWNRevokedNonce.sol @@ -80,6 +80,16 @@ contract PWNRevokedNonce is PWNHubAccessControl { _revokeNonce(msg.sender, nonceSpace, nonce); } + /** + * @notice Revoke a nonce in the current nonce space on behalf of an owner. + * @dev Only an address with associated access tag in PWN Hub can call this function. + * @param owner Owner address of a revoking nonce. + * @param nonce Nonce to be revoked. + */ + function revokeNonce(address owner, uint256 nonce) external onlyWithTag(accessTag) { + _revokeNonce(owner, _nonceSpace[owner], nonce); + } + /** * @notice Revoke a nonce in a nonce space on behalf of an owner. * @dev Only an address with associated access tag in PWN Hub can call this function. diff --git a/test/unit/PWNRevokedNonce.t.sol b/test/unit/PWNRevokedNonce.t.sol index 6af5834..d2d390c 100644 --- a/test/unit/PWNRevokedNonce.t.sol +++ b/test/unit/PWNRevokedNonce.t.sol @@ -120,6 +120,60 @@ contract PWNRevokedNonce_RevokeNonceWithOwner_Test is PWNRevokedNonceTest { } + function testFuzz_shouldFail_whenCallerIsDoesNotHaveAccessTag(address caller) external { + vm.assume(caller != accessEnabledAddress); + + vm.expectRevert(abi.encodeWithSelector(CallerMissingHubTag.selector, accessTag)); + vm.prank(caller); + revokedNonce.revokeNonce(caller, 1); + } + + function testFuzz_shouldStoreNonceAsRevoked(address owner, uint256 nonceSpace, uint256 nonce) external { + vm.store(address(revokedNonce), _nonceSpaceSlot(owner), bytes32(nonceSpace)); + + vm.prank(accessEnabledAddress); + revokedNonce.revokeNonce(owner, nonce); + + assertTrue(revokedNonce.isNonceRevoked(owner, nonceSpace, nonce)); + } + + function testFuzz_shouldEmit_NonceRevoked(address owner, uint256 nonceSpace, uint256 nonce) external { + vm.store(address(revokedNonce), _nonceSpaceSlot(owner), bytes32(nonceSpace)); + + vm.expectEmit(); + emit NonceRevoked(owner, nonceSpace, nonce); + + vm.prank(accessEnabledAddress); + revokedNonce.revokeNonce(owner, nonce); + } + +} + + +/*----------------------------------------------------------*| +|* # REVOKE NONCE WITH NONCE SPACE AND OWNER *| +|*----------------------------------------------------------*/ + +contract PWNRevokedNonce_RevokeNonceWithNonceSpaceAndOwner_Test is PWNRevokedNonceTest { + + address accessEnabledAddress = address(0x01); + + function setUp() override public { + super.setUp(); + + vm.mockCall( + hub, + abi.encodeWithSignature("hasTag(address,bytes32)"), + abi.encode(false) + ); + vm.mockCall( + hub, + abi.encodeWithSignature("hasTag(address,bytes32)", accessEnabledAddress, accessTag), + abi.encode(true) + ); + } + + function testFuzz_shouldFail_whenCallerIsDoesNotHaveAccessTag(address caller) external { vm.assume(caller != accessEnabledAddress); From adb606334e7115311cf6a75e6d2e818db4091c73 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Wed, 27 Mar 2024 17:22:18 -0400 Subject: [PATCH 063/129] remove forgotten file --- deployments.json | 278 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 278 insertions(+) create mode 100644 deployments.json diff --git a/deployments.json b/deployments.json new file mode 100644 index 0000000..184f569 --- /dev/null +++ b/deployments.json @@ -0,0 +1,278 @@ +{ + "deployedChains": [1, 5, 10, 25, 56, 137, 338, 5000, 5001, 8453, 42161, 84531, 11155111], + "chains": { + "1": { + "dao": "0x1B8383D2726E7e18189205337424a2631A2102F4", + "productTimelock": "0xd57e72A328AB1deC6b374c2babe2dc489819B5Ea", + "protocolTimelock": "0xd8dbdDf1c0FDdf9b5eCFA5C067C38DB66739FBAB", + "protocolSafe": "0x61a77B19b7F4dB82222625D7a969698894d77473", + "daoSafe": "0xd56635c0E91D31F88B89F195D3993a9e34516e59", + "feeCollector": "0xd56635c0E91D31F88B89F195D3993a9e34516e59", + "deployerSafe": "0x42Cad20c964067f8e8b5c3E13fd0aa3C20a964C4", + "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", + "configSingleton": "0x0000000000000000000000000000000000000000", + "config": "0x0000000000000000000000000000000000000000", + "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", + "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", + "revokedNonce": "0x0000000000000000000000000000000000000000", + "simpleLoan": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", + "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", + "stateFingerprintComputerRegistry": "0x0000000000000000000000000000000000000000", + "categoryRegistry": "0x0000000000000000000000000000000000000000" + }, + "5": { + "dao": "0x0000000000000000000000000000000000000000", + "productTimelock": "0x0000000000000000000000000000000000000000", + "protocolTimelock": "0x0000000000000000000000000000000000000000", + "protocolSafe": "0x61a77B19b7F4dB82222625D7a969698894d77473", + "daoSafe": "0xd56635c0E91D31F88B89F195D3993a9e34516e59", + "feeCollector": "0xd56635c0E91D31F88B89F195D3993a9e34516e59", + "deployerSafe": "0x42Cad20c964067f8e8b5c3E13fd0aa3C20a964C4", + "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", + "configSingleton": "0x0000000000000000000000000000000000000000", + "config": "0x0000000000000000000000000000000000000000", + "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", + "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", + "revokedNonce": "0x0000000000000000000000000000000000000000", + "simpleLoan": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", + "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", + "stateFingerprintComputerRegistry": "0x0000000000000000000000000000000000000000", + "categoryRegistry": "0x0000000000000000000000000000000000000000" + }, + "10": { + "dao": "0x0000000000000000000000000000000000000000", + "productTimelock": "0x60a0F7594793e3DC31DfE1cC930dF65B54e95B39", + "protocolTimelock": "0x744B83343a86F87Ed05a5f3A92939D6d81520F27", + "protocolSafe": "0xa7106a1C2498EaeF4AC1B594a6544c841623B327", + "daoSafe": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", + "feeCollector": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", + "deployerSafe": "0x1B4B37738De3bb9E6a7a4f99aFe4C145734c071d", + "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", + "configSingleton": "0x0000000000000000000000000000000000000000", + "config": "0x0000000000000000000000000000000000000000", + "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", + "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", + "revokedNonce": "0x0000000000000000000000000000000000000000", + "simpleLoan": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", + "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", + "stateFingerprintComputerRegistry": "0x0000000000000000000000000000000000000000", + "categoryRegistry": "0x0000000000000000000000000000000000000000" + }, + "25": { + "dao": "0x0000000000000000000000000000000000000000", + "productTimelock": "0x60a0F7594793e3DC31DfE1cC930dF65B54e95B39", + "protocolTimelock": "0x744B83343a86F87Ed05a5f3A92939D6d81520F27", + "protocolSafe": "0xa7106a1C2498EaeF4AC1B594a6544c841623B327", + "daoSafe": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", + "feeCollector": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", + "deployerSafe": "0x1B4B37738De3bb9E6a7a4f99aFe4C145734c071d", + "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", + "configSingleton": "0x0000000000000000000000000000000000000000", + "config": "0x0000000000000000000000000000000000000000", + "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", + "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", + "revokedNonce": "0x0000000000000000000000000000000000000000", + "simpleLoan": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", + "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", + "stateFingerprintComputerRegistry": "0x0000000000000000000000000000000000000000", + "categoryRegistry": "0x0000000000000000000000000000000000000000" + }, + "56": { + "dao": "0x0000000000000000000000000000000000000000", + "productTimelock": "0xd57e72A328AB1deC6b374c2babe2dc489819B5Ea", + "protocolTimelock": "0xd8dbdDf1c0FDdf9b5eCFA5C067C38DB66739FBAB", + "protocolSafe": "0x61a77B19b7F4dB82222625D7a969698894d77473", + "daoSafe": "0xd56635c0E91D31F88B89F195D3993a9e34516e59", + "feeCollector": "0xd56635c0E91D31F88B89F195D3993a9e34516e59", + "deployerSafe": "0x42Cad20c964067f8e8b5c3E13fd0aa3C20a964C4", + "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", + "configSingleton": "0x0000000000000000000000000000000000000000", + "config": "0x0000000000000000000000000000000000000000", + "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", + "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", + "revokedNonce": "0x0000000000000000000000000000000000000000", + "simpleLoan": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", + "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", + "stateFingerprintComputerRegistry": "0x0000000000000000000000000000000000000000", + "categoryRegistry": "0x0000000000000000000000000000000000000000" + }, + "137": { + "dao": "0x0000000000000000000000000000000000000000", + "productTimelock": "0x2cDf99aD1115Ea0E943E56dd26459E3e57788C12", + "protocolTimelock": "0x9b1ec4bc634db130ab7310d4e585338888030623", + "protocolSafe": "0x61a77B19b7F4dB82222625D7a969698894d77473", + "daoSafe": "0xd56635c0E91D31F88B89F195D3993a9e34516e59", + "feeCollector": "0xd56635c0E91D31F88B89F195D3993a9e34516e59", + "deployerSafe": "0x42Cad20c964067f8e8b5c3E13fd0aa3C20a964C4", + "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", + "configSingleton": "0x0000000000000000000000000000000000000000", + "config": "0x0000000000000000000000000000000000000000", + "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", + "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", + "revokedNonce": "0x0000000000000000000000000000000000000000", + "simpleLoan": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", + "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", + "stateFingerprintComputerRegistry": "0x0000000000000000000000000000000000000000", + "categoryRegistry": "0x0000000000000000000000000000000000000000" + }, + "338": { + "dao": "0x0000000000000000000000000000000000000000", + "productTimelock": "0x0000000000000000000000000000000000000000", + "protocolTimelock": "0x0000000000000000000000000000000000000000", + "protocolSafe": "0xa7106a1C2498EaeF4AC1B594a6544c841623B327", + "daoSafe": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", + "feeCollector": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", + "deployerSafe": "0x0000000000000000000000000000000000000000", + "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", + "configSingleton": "0x0000000000000000000000000000000000000000", + "config": "0x0000000000000000000000000000000000000000", + "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", + "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", + "revokedNonce": "0x0000000000000000000000000000000000000000", + "simpleLoan": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", + "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", + "stateFingerprintComputerRegistry": "0x0000000000000000000000000000000000000000", + "categoryRegistry": "0x0000000000000000000000000000000000000000" + }, + "5000": { + "dao": "0x0000000000000000000000000000000000000000", + "productTimelock": "0x60a0F7594793e3DC31DfE1cC930dF65B54e95B39", + "protocolTimelock": "0x744B83343a86F87Ed05a5f3A92939D6d81520F27", + "protocolSafe": "0xa7106a1C2498EaeF4AC1B594a6544c841623B327", + "daoSafe": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", + "feeCollector": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", + "deployerSafe": "0x1B4B37738De3bb9E6a7a4f99aFe4C145734c071d", + "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", + "configSingleton": "0x0000000000000000000000000000000000000000", + "config": "0x0000000000000000000000000000000000000000", + "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", + "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", + "revokedNonce": "0x0000000000000000000000000000000000000000", + "simpleLoan": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", + "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", + "stateFingerprintComputerRegistry": "0x0000000000000000000000000000000000000000", + "categoryRegistry": "0x0000000000000000000000000000000000000000" + }, + "5001": { + "dao": "0x0000000000000000000000000000000000000000", + "productTimelock": "0x0000000000000000000000000000000000000000", + "protocolTimelock": "0x0000000000000000000000000000000000000000", + "protocolSafe": "0xa7106a1C2498EaeF4AC1B594a6544c841623B327", + "daoSafe": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", + "feeCollector": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", + "deployerSafe": "0x0000000000000000000000000000000000000000", + "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", + "configSingleton": "0x0000000000000000000000000000000000000000", + "config": "0x0000000000000000000000000000000000000000", + "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", + "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", + "revokedNonce": "0x0000000000000000000000000000000000000000", + "simpleLoan": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", + "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", + "stateFingerprintComputerRegistry": "0x0000000000000000000000000000000000000000", + "categoryRegistry": "0x0000000000000000000000000000000000000000" + }, + "8453": { + "dao": "0x0000000000000000000000000000000000000000", + "productTimelock": "0x60a0F7594793e3DC31DfE1cC930dF65B54e95B39", + "protocolTimelock": "0x744B83343a86F87Ed05a5f3A92939D6d81520F27", + "protocolSafe": "0xa7106a1C2498EaeF4AC1B594a6544c841623B327", + "daoSafe": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", + "feeCollector": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", + "deployerSafe": "0x1B4B37738De3bb9E6a7a4f99aFe4C145734c071d", + "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", + "configSingleton": "0x0000000000000000000000000000000000000000", + "config": "0x0000000000000000000000000000000000000000", + "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", + "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", + "revokedNonce": "0x0000000000000000000000000000000000000000", + "simpleLoan": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", + "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", + "stateFingerprintComputerRegistry": "0x0000000000000000000000000000000000000000", + "categoryRegistry": "0x0000000000000000000000000000000000000000" + }, + "42161": { + "dao": "0x0000000000000000000000000000000000000000", + "productTimelock": "0x2cDf99aD1115Ea0E943E56dd26459E3e57788C12", + "protocolTimelock": "0x9b1ec4bc634db130ab7310d4e585338888030623", + "protocolSafe": "0x61a77B19b7F4dB82222625D7a969698894d77473", + "daoSafe": "0xd56635c0E91D31F88B89F195D3993a9e34516e59", + "feeCollector": "0xd56635c0E91D31F88B89F195D3993a9e34516e59", + "deployerSafe": "0x42Cad20c964067f8e8b5c3E13fd0aa3C20a964C4", + "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", + "configSingleton": "0x0000000000000000000000000000000000000000", + "config": "0x0000000000000000000000000000000000000000", + "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", + "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", + "revokedNonce": "0x0000000000000000000000000000000000000000", + "simpleLoan": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", + "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", + "stateFingerprintComputerRegistry": "0x0000000000000000000000000000000000000000", + "categoryRegistry": "0x0000000000000000000000000000000000000000" + }, + "84531": { + "dao": "0x0000000000000000000000000000000000000000", + "productTimelock": "0x0000000000000000000000000000000000000000", + "protocolTimelock": "0x0000000000000000000000000000000000000000", + "protocolSafe": "0xa7106a1C2498EaeF4AC1B594a6544c841623B327", + "daoSafe": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", + "feeCollector": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", + "deployerSafe": "0x0000000000000000000000000000000000000000", + "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", + "configSingleton": "0x0000000000000000000000000000000000000000", + "config": "0x0000000000000000000000000000000000000000", + "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", + "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", + "revokedNonce": "0x0000000000000000000000000000000000000000", + "simpleLoan": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", + "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", + "stateFingerprintComputerRegistry": "0x0000000000000000000000000000000000000000", + "categoryRegistry": "0x0000000000000000000000000000000000000000" + }, + "11155111": { + "dao": "0x0000000000000000000000000000000000000000", + "productTimelock": "0x0000000000000000000000000000000000000000", + "protocolTimelock": "0x0000000000000000000000000000000000000000", + "protocolSafe": "0xa7106a1C2498EaeF4AC1B594a6544c841623B327", + "daoSafe": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", + "feeCollector": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", + "deployerSafe": "0x1B4B37738De3bb9E6a7a4f99aFe4C145734c071d", + "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", + "configSingleton": "0x0000000000000000000000000000000000000000", + "config": "0x0000000000000000000000000000000000000000", + "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", + "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", + "revokedNonce": "0x0000000000000000000000000000000000000000", + "simpleLoan": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", + "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", + "stateFingerprintComputerRegistry": "0x0000000000000000000000000000000000000000", + "categoryRegistry": "0x0000000000000000000000000000000000000000" + } + } +} From 65001634683e05b6c05e97cb3372138d74f89161 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Fri, 29 Mar 2024 10:37:54 -0400 Subject: [PATCH 064/129] feat(revoked-nonce): revert when revoking already revoked nonce --- src/PWNErrors.sol | 1 + src/nonce/PWNRevokedNonce.sol | 3 +++ test/unit/PWNRevokedNonce.t.sol | 34 +++++++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/src/PWNErrors.sol b/src/PWNErrors.sol index dd7180f..eb2c026 100644 --- a/src/PWNErrors.sol +++ b/src/PWNErrors.sol @@ -35,6 +35,7 @@ error UnsupportedTransferFunction(); error IncompleteTransfer(); // Nonce +error NonceAlreadyRevoked(address addr, uint256 nonceSpace, uint256 nonce); error NonceNotUsable(address addr, uint256 nonceSpace, uint256 nonce); // Signature checks diff --git a/src/nonce/PWNRevokedNonce.sol b/src/nonce/PWNRevokedNonce.sol index db68c50..494487c 100644 --- a/src/nonce/PWNRevokedNonce.sol +++ b/src/nonce/PWNRevokedNonce.sol @@ -105,6 +105,9 @@ contract PWNRevokedNonce is PWNHubAccessControl { * @notice Internal function to revoke a nonce in a nonce space. */ function _revokeNonce(address owner, uint256 nonceSpace, uint256 nonce) private { + if (_revokedNonce[owner][nonceSpace][nonce]) { + revert NonceAlreadyRevoked({ addr: owner, nonceSpace: nonceSpace, nonce: nonce }); + } _revokedNonce[owner][nonceSpace][nonce] = true; emit NonceRevoked(owner, nonceSpace, nonce); } diff --git a/test/unit/PWNRevokedNonce.t.sol b/test/unit/PWNRevokedNonce.t.sol index d2d390c..d30de33 100644 --- a/test/unit/PWNRevokedNonce.t.sol +++ b/test/unit/PWNRevokedNonce.t.sol @@ -50,6 +50,15 @@ abstract contract PWNRevokedNonceTest is Test { contract PWNRevokedNonce_RevokeNonce_Test is PWNRevokedNonceTest { + function testFuzz_shouldFail_whenNonceAlreadyRevoked(uint256 nonceSpace, uint256 nonce) external { + vm.store(address(revokedNonce), _nonceSpaceSlot(alice), bytes32(nonceSpace)); + vm.store(address(revokedNonce), _revokedNonceSlot(alice, nonceSpace, nonce), bytes32(uint256(1))); + + vm.expectRevert(abi.encodeWithSelector(NonceAlreadyRevoked.selector, alice, nonceSpace, nonce)); + vm.prank(alice); + revokedNonce.revokeNonce(nonce); + } + function testFuzz_shouldStoreNonceAsRevoked(uint256 nonceSpace, uint256 nonce) external { vm.store(address(revokedNonce), _nonceSpaceSlot(alice), bytes32(nonceSpace)); @@ -78,6 +87,14 @@ contract PWNRevokedNonce_RevokeNonce_Test is PWNRevokedNonceTest { contract PWNRevokedNonce_RevokeNonceWithNonceSpace_Test is PWNRevokedNonceTest { + function testFuzz_shouldFail_whenNonceAlreadyRevoked(uint256 nonceSpace, uint256 nonce) external { + vm.store(address(revokedNonce), _revokedNonceSlot(alice, nonceSpace, nonce), bytes32(uint256(1))); + + vm.expectRevert(abi.encodeWithSelector(NonceAlreadyRevoked.selector, alice, nonceSpace, nonce)); + vm.prank(alice); + revokedNonce.revokeNonce(nonceSpace, nonce); + } + function testFuzz_shouldStoreNonceAsRevoked(uint256 nonceSpace, uint256 nonce) external { vm.prank(alice); revokedNonce.revokeNonce(nonceSpace, nonce); @@ -128,6 +145,15 @@ contract PWNRevokedNonce_RevokeNonceWithOwner_Test is PWNRevokedNonceTest { revokedNonce.revokeNonce(caller, 1); } + function testFuzz_shouldFail_whenNonceAlreadyRevoked(address owner, uint256 nonceSpace, uint256 nonce) external { + vm.store(address(revokedNonce), _nonceSpaceSlot(owner), bytes32(nonceSpace)); + vm.store(address(revokedNonce), _revokedNonceSlot(owner, nonceSpace, nonce), bytes32(uint256(1))); + + vm.expectRevert(abi.encodeWithSelector(NonceAlreadyRevoked.selector, owner, nonceSpace, nonce)); + vm.prank(accessEnabledAddress); + revokedNonce.revokeNonce(owner, nonce); + } + function testFuzz_shouldStoreNonceAsRevoked(address owner, uint256 nonceSpace, uint256 nonce) external { vm.store(address(revokedNonce), _nonceSpaceSlot(owner), bytes32(nonceSpace)); @@ -182,6 +208,14 @@ contract PWNRevokedNonce_RevokeNonceWithNonceSpaceAndOwner_Test is PWNRevokedNon revokedNonce.revokeNonce(caller, 1, 1); } + function testFuzz_shouldFail_whenNonceAlreadyRevoked(address owner, uint256 nonceSpace, uint256 nonce) external { + vm.store(address(revokedNonce), _revokedNonceSlot(owner, nonceSpace, nonce), bytes32(uint256(1))); + + vm.expectRevert(abi.encodeWithSelector(NonceAlreadyRevoked.selector, owner, nonceSpace, nonce)); + vm.prank(accessEnabledAddress); + revokedNonce.revokeNonce(owner, nonceSpace, nonce); + } + function testFuzz_shouldStoreNonceAsRevoked(address owner, uint256 nonceSpace, uint256 nonce) external { vm.prank(accessEnabledAddress); revokedNonce.revokeNonce(owner, nonceSpace, nonce); From 7f8250346fa216281c982260d99362d0722b13d3 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Fri, 29 Mar 2024 10:57:57 -0400 Subject: [PATCH 065/129] feat: refactor loan creation flow back to the one, where loan contract is starting point of the flow --- src/PWNErrors.sol | 1 + src/loan/terms/simple/loan/PWNSimpleLoan.sol | 315 ++++---- .../PWNSimpleLoanDutchAuctionProposal.sol | 184 ++--- .../PWNSimpleLoanFungibleProposal.sol | 187 ++--- .../proposal/PWNSimpleLoanListProposal.sol | 193 ++--- .../simple/proposal/PWNSimpleLoanProposal.sol | 288 ++++---- .../proposal/PWNSimpleLoanSimpleProposal.sol | 155 ++-- test/unit/PWNSimpleLoan.t.sol | 674 +++++++++--------- .../PWNSimpleLoanDutchAuctionProposal.t.sol | 269 ++++--- test/unit/PWNSimpleLoanFungibleProposal.t.sol | 251 +++---- test/unit/PWNSimpleLoanListProposal.t.sol | 282 ++++---- test/unit/PWNSimpleLoanProposal.t.sol | 305 ++++---- test/unit/PWNSimpleLoanSimpleProposal.t.sol | 203 +++--- 13 files changed, 1454 insertions(+), 1853 deletions(-) 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); } } From 5d0ad3a2c8e3ada64eea622fe56d991e79361f04 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Thu, 4 Apr 2024 14:38:07 -0400 Subject: [PATCH 066/129] feat(source-of-funds): implement lenders source of funds and allow to use Compound pools to fund loans --- src/PWNErrors.sol | 6 +- src/hub/PWNHubTags.sol | 3 + src/loan/terms/simple/loan/ICometLike.sol | 7 + src/loan/terms/simple/loan/PWNSimpleLoan.sol | 368 +++--- .../PWNSimpleLoanDutchAuctionProposal.sol | 8 +- .../PWNSimpleLoanFungibleProposal.sol | 8 +- .../proposal/PWNSimpleLoanListProposal.sol | 8 +- .../proposal/PWNSimpleLoanSimpleProposal.sol | 8 +- test/unit/PWNSimpleLoan.t.sol | 1112 ++++++++++++----- .../PWNSimpleLoanDutchAuctionProposal.t.sol | 5 +- test/unit/PWNSimpleLoanFungibleProposal.t.sol | 5 +- test/unit/PWNSimpleLoanListProposal.t.sol | 5 +- test/unit/PWNSimpleLoanSimpleProposal.t.sol | 5 +- 13 files changed, 1064 insertions(+), 484 deletions(-) create mode 100644 src/loan/terms/simple/loan/ICometLike.sol diff --git a/src/PWNErrors.sol b/src/PWNErrors.sol index 58a9391..bb90678 100644 --- a/src/PWNErrors.sol +++ b/src/PWNErrors.sol @@ -14,6 +14,10 @@ error CallerNotLOANTokenHolder(); error RefinanceBorrowerMismatch(address currentBorrower, address newBorrower); error RefinanceCreditMismatch(); error RefinanceCollateralMismatch(); +error InvalidLenderSpecHash(bytes32 current, bytes32 expected); +error InvalidDuration(uint256 current, uint256 limit); +error AccruingInterestAPROutOfBounds(uint256 current, uint256 limit); +error CallerNotVault(); // Loan extension error InvalidExtensionDuration(uint256 duration, uint256 limit); @@ -44,9 +48,7 @@ error InvalidSignature(address signer, bytes32 digest); // Proposal error CallerIsNotStatedProposer(address); -error InvalidDuration(uint256 current, uint256 limit); error InvalidRefinancingLoanId(uint256 refinancingLoanId); -error AccruingInterestAPROutOfBounds(uint256 current, uint256 limit); error AvailableCreditLimitExceeded(uint256 used, uint256 limit); error Expired(uint256 current, uint256 expiration); error CallerNotAllowedAcceptor(address current, address allowed); diff --git a/src/hub/PWNHubTags.sol b/src/hub/PWNHubTags.sol index 44dd32b..eeb3107 100644 --- a/src/hub/PWNHubTags.sol +++ b/src/hub/PWNHubTags.sol @@ -12,4 +12,7 @@ library PWNHubTags { /// @dev Address can revoke nonces on other addresses behalf. bytes32 internal constant NONCE_MANAGER = keccak256("PWN_NONCE_MANAGER"); + /// @dev Address is valid Compound pool and can be used as a source of funds. + bytes32 internal constant COMPOUND_V3_POOL = keccak256("COMPOUND_V3_POOL"); + } diff --git a/src/loan/terms/simple/loan/ICometLike.sol b/src/loan/terms/simple/loan/ICometLike.sol new file mode 100644 index 0000000..e6adedb --- /dev/null +++ b/src/loan/terms/simple/loan/ICometLike.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.16; + +interface ICometLike { + function supplyFrom(address from, address dst, address asset, uint amount) external; + function withdrawFrom(address src, address to, address asset, uint amount) external; +} diff --git a/src/loan/terms/simple/loan/PWNSimpleLoan.sol b/src/loan/terms/simple/loan/PWNSimpleLoan.sol index 5ac8d8e..8b312cf 100644 --- a/src/loan/terms/simple/loan/PWNSimpleLoan.sol +++ b/src/loan/terms/simple/loan/PWNSimpleLoan.sol @@ -11,6 +11,7 @@ import { PWNHub } from "@pwn/hub/PWNHub.sol"; import { PWNHubTags } from "@pwn/hub/PWNHubTags.sol"; import { PWNFeeCalculator } from "@pwn/loan/lib/PWNFeeCalculator.sol"; import { PWNSignatureChecker } from "@pwn/loan/lib/PWNSignatureChecker.sol"; +import { ICometLike } from "@pwn/loan/terms/simple/loan/ICometLike.sol"; import { PWNSimpleLoanProposal } from "@pwn/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol"; import { IERC5646 } from "@pwn/loan/token/IERC5646.sol"; import { IPWNLoanMetadataProvider } from "@pwn/loan/token/IPWNLoanMetadataProvider.sol"; @@ -27,6 +28,7 @@ import "@pwn/PWNErrors.sol"; * @dev Acts as a vault for every loan created by this contract. */ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { + using MultiToken for address; string public constant VERSION = "1.2"; @@ -74,6 +76,8 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { * @param credit Asset used as a loan credit. For a definition see { MultiToken dependency lib }. * @param fixedInterestAmount Fixed interest amount in credit asset tokens. It is the minimum amount of interest which has to be paid by a borrower. * @param accruingInterestAPR Accruing interest APR. + * @param lenderSpecHash Hash of a lender specification. + * @param borrowerSpecHash Hash of a borrower specification. */ struct Terms { address lender; @@ -83,6 +87,8 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { MultiToken.Asset credit; uint256 fixedInterestAmount; uint40 accruingInterestAPR; + bytes32 lenderSpecHash; + bytes32 borrowerSpecHash; } /** @@ -97,6 +103,15 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { bytes signature; } + /** + * @notice Lender specification during loan creation. + * @param sourceOfFunds Address of a source of funds. This address can be an address of a Compound v3 pool or + * lender address if lender is funding the loan directly. + */ + struct LenderSpec { + address sourceOfFunds; + } + /** * @notice Caller specification during loan creation. * @param refinancingLoanId Id of a loan to be refinanced. 0 if creating a new loan. @@ -115,6 +130,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { * @notice Struct defining a simple loan. * @param status 0 == none/dead || 2 == running/accepted offer/accepted request || 3 == paid back || 4 == expired. * @param creditAddress Address of an asset used as a loan credit. + * @param originalSourceOfFunds Address of a source of funds that was used to fund the loan. * @param startTimestamp Unix timestamp (in seconds) of a start date. * @param defaultTimestamp Unix timestamp (in seconds) of a default date. * @param borrower Address of a borrower. @@ -129,6 +145,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { struct LOAN { uint8 status; address creditAddress; + address originalSourceOfFunds; uint40 startTimestamp; uint40 defaultTimestamp; address borrower; @@ -226,6 +243,20 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { } + /*----------------------------------------------------------*| + |* # LENDER SPEC *| + |*----------------------------------------------------------*/ + + /** + * @notice Get hash of a lender specification. + * @param lenderSpec Lender specification struct. + * @return Hash of a lender specification. + */ + function getLenderSpecHash(LenderSpec calldata lenderSpec) public pure returns (bytes32) { + return keccak256(abi.encode(lenderSpec)); + } + + /*----------------------------------------------------------*| |* # CREATE LOAN *| |*----------------------------------------------------------*/ @@ -234,12 +265,14 @@ 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 proposalSpec Proposal specification struct. + * @param lenderSpec Lender 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( ProposalSpec calldata proposalSpec, + LenderSpec calldata lenderSpec, CallerSpec calldata callerSpec, bytes calldata extra ) external returns (uint256 loanId) { @@ -268,6 +301,17 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { signature: proposalSpec.signature }); + // Check that provided lender spec is correct + if (msg.sender != loanTerms.lender && loanTerms.lenderSpecHash != getLenderSpecHash(lenderSpec)) { + revert InvalidLenderSpecHash({ current: loanTerms.lenderSpecHash, expected: getLenderSpecHash(lenderSpec) }); + } + // Check that the lender is the source of funds or the source of funds is a Compound pool + if (lenderSpec.sourceOfFunds != loanTerms.lender) { + if (!hub.hasTag(lenderSpec.sourceOfFunds, PWNHubTags.COMPOUND_V3_POOL)) { + revert AddressMissingHubTag({ addr: lenderSpec.sourceOfFunds, tag: PWNHubTags.COMPOUND_V3_POOL }); + } + } + // Check minimum loan duration if (loanTerms.duration < MIN_LOAN_DURATION) { revert InvalidDuration({ current: loanTerms.duration, limit: MIN_LOAN_DURATION }); @@ -291,6 +335,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { proposalHash: proposalHash, proposalContract: proposalSpec.proposalContract, loanTerms: loanTerms, + lenderSpec: lenderSpec, extra: extra }); @@ -298,12 +343,20 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { _checkPermit(msg.sender, loanTerms.credit.assetAddress, callerSpec.permit); _tryPermit(callerSpec.permit); + // Settle the loan if (callerSpec.refinancingLoanId == 0) { // Transfer collateral to Vault and credit to borrower - _settleNewLoan(loanTerms); + _settleNewLoan(loanTerms, lenderSpec); } else { - // Refinance the original loan - _refinanceOriginalLoan(callerSpec.refinancingLoanId, loanTerms); + // Update loan to repaid state + _updateRepaidLoan(callerSpec.refinancingLoanId); + + // Repay the original loan and transfer the surplus to the borrower if any + _settleLoanRefinance({ + refinancingLoanId: callerSpec.refinancingLoanId, + loanTerms: loanTerms, + lenderSpec: lenderSpec + }); emit LOANRefinanced({ loanId: callerSpec.refinancingLoanId, refinancedLoanId: loanId }); } @@ -371,6 +424,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { bytes32 proposalHash, address proposalContract, Terms memory loanTerms, + LenderSpec calldata lenderSpec, bytes calldata extra ) private returns (uint256 loanId) { // Mint LOAN token for lender @@ -380,6 +434,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { LOAN storage loan = LOANs[loanId]; loan.status = 2; loan.creditAddress = loanTerms.credit.assetAddress; + loan.originalSourceOfFunds = lenderSpec.sourceOfFunds; loan.startTimestamp = uint40(block.timestamp); loan.defaultTimestamp = uint40(block.timestamp) + loanTerms.duration; loan.borrower = loanTerms.borrower; @@ -405,55 +460,42 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { * @dev The function assumes a prior token approval to a contract address or signed permits. * @param loanTerms Loan terms struct. */ - function _settleNewLoan(Terms memory loanTerms) private { + function _settleNewLoan( + Terms memory loanTerms, + LenderSpec calldata lenderSpec + ) private { // Transfer collateral to Vault _pull(loanTerms.collateral, loanTerms.borrower); - MultiToken.Asset memory creditHelper = loanTerms.credit; + // Decide credit provider + address creditProvider = loanTerms.lender; + if (lenderSpec.sourceOfFunds != loanTerms.lender) { + + // Note: Lender is not source of funds. + // Withdraw credit asset to the loan contract and use it as a credit provider. - // Collect fee if any and update credit asset amount + ICometLike(lenderSpec.sourceOfFunds).withdrawFrom( + loanTerms.lender, address(this), loanTerms.credit.assetAddress, loanTerms.credit.amount + ); + creditProvider = address(this); + } + + // Calculate fee amount and new loan amount (uint256 feeAmount, uint256 newLoanAmount) = PWNFeeCalculator.calculateFeeAmount(config.fee(), loanTerms.credit.amount); + + // Note: `creditHelper` must not be used before updating the amount. + MultiToken.Asset memory creditHelper = loanTerms.credit; + + // Collect fees if (feeAmount > 0) { - // Transfer fee amount to fee collector creditHelper.amount = feeAmount; - _pushFrom(creditHelper, loanTerms.lender, config.feeCollector()); - - // Set new loan amount value - creditHelper.amount = newLoanAmount; + _pushFrom(creditHelper, creditProvider, config.feeCollector()); } - // Note: If the fee amount is greater than zero, the credit amount is already updated to the new loan amount. - // Transfer credit to borrower - _pushFrom(creditHelper, loanTerms.lender, 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, - * 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 loanId Id of a loan that is being refinanced. - * @param loanTerms Loan terms struct. - */ - function _refinanceOriginalLoan( - uint256 loanId, - Terms memory loanTerms - ) private { - uint256 repaymentAmount = _loanRepaymentAmount(loanId); - - // Delete or update the original loan - (bool repayLoanDirectly, address loanOwner) = _deleteOrUpdateRepaidLoan(loanId); - - // Repay the original loan and transfer the surplus to the borrower if any - _settleLoanRefinance({ - repayLoanDirectly: repayLoanDirectly, - loanOwner: loanOwner, - repaymentAmount: repaymentAmount, - loanTerms: loanTerms - }); + creditHelper.amount = newLoanAmount; + _pushFrom(creditHelper, creditProvider, loanTerms.borrower); } /** @@ -461,57 +503,85 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { * 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 repayLoanDirectly If the loan can be repaid directly to the current LOAN owner. - * @param loanOwner Address of the current LOAN owner. - * @param repaymentAmount Amount of the original loan to be repaid. + * @param refinancingLoanId Id of a loan to be refinanced. * @param loanTerms Loan terms struct. + * @param lenderSpec Lender specification struct. */ function _settleLoanRefinance( - bool repayLoanDirectly, - address loanOwner, - uint256 repaymentAmount, - Terms memory loanTerms + uint256 refinancingLoanId, + Terms memory loanTerms, + LenderSpec calldata lenderSpec ) private { - MultiToken.Asset memory creditHelper = loanTerms.credit; + LOAN storage loan = LOANs[refinancingLoanId]; + address loanOwner = loanToken.ownerOf(refinancingLoanId); + uint256 repaymentAmount = _loanRepaymentAmount(refinancingLoanId); - // Collect fees + // Calculate fee amount and new loan amount (uint256 feeAmount, uint256 newLoanAmount) = PWNFeeCalculator.calculateFeeAmount(config.fee(), loanTerms.credit.amount); + + uint256 common = Math.min(repaymentAmount, newLoanAmount); + uint256 surplus = newLoanAmount > repaymentAmount ? newLoanAmount - repaymentAmount : 0; + uint256 shortage = surplus > 0 ? 0 : repaymentAmount - newLoanAmount; + + // Note: New lender will always transfer common loan amount to the Vault, except when: + // - the new lender is the current loan owner but not the original lender + // - the new lender is the current loan owner, is the original lender, and the new and original source of funds are equal + + bool shouldTransferCommon = + loanTerms.lender != loanOwner || + (loan.originalLender == loanOwner && loan.originalSourceOfFunds != lenderSpec.sourceOfFunds); + + // Decide credit provider + address creditProvider = loanTerms.lender; + if (lenderSpec.sourceOfFunds != loanTerms.lender) { + + // Note: Lender is not the source of funds. Withdraw credit asset to the Vault and use it + // as a credit provider to minimize the number of withdrawals. + + { + address creditAddr = loanTerms.credit.assetAddress; + uint256 withdrawAmount = feeAmount + (shouldTransferCommon ? common : 0) + surplus; + if (withdrawAmount > 0) { + ICometLike(lenderSpec.sourceOfFunds).withdrawFrom( + loanTerms.lender, address(this), creditAddr, withdrawAmount + ); + } + } + creditProvider = address(this); + } + + // Note: `creditHelper` must not be used before updating the amount. + MultiToken.Asset memory creditHelper = loanTerms.credit; + + // Collect fees if (feeAmount > 0) { creditHelper.amount = feeAmount; - _pushFrom(creditHelper, loanTerms.lender, config.feeCollector()); + _pushFrom(creditHelper, creditProvider, config.feeCollector()); } - // New lender repays the original loan - // Note: If the new lender is the LOAN token owner, don't execute the transfer at all, - // it would make transfer from the same address to the same address. - if (loanTerms.lender != loanOwner) { - creditHelper.amount = Math.min(repaymentAmount, newLoanAmount); - _transferLoanRepayment({ - repayLoanDirectly: repayLoanDirectly, - repaymentCredit: creditHelper, - repayingAddress: loanTerms.lender, - currentLoanOwner: loanOwner - }); + // Transfer common amount to the Vault if necessary + // Note: If the `creditProvider` is a vault, the common amount is already in the vault. + if (shouldTransferCommon && creditProvider != address(this)) { + creditHelper.amount = common; + _pull(creditHelper, creditProvider); } - // Handle the surplus or the missing amount - if (newLoanAmount >= repaymentAmount) { - // New loan covers the whole original loan, transfer surplus to the borrower if any - uint256 surplus = newLoanAmount - repaymentAmount; - if (surplus > 0) { - creditHelper.amount = surplus; - _pushFrom(creditHelper, loanTerms.lender, loanTerms.borrower); - } - } else { + // Handle the surplus or the shortage + if (surplus > 0) { + // New loan covers the whole original loan, transfer surplus to the borrower + creditHelper.amount = surplus; + _pushFrom(creditHelper, creditProvider, loanTerms.borrower); + } else if (shortage > 0) { // New loan covers only part of the original loan, borrower needs to contribute - creditHelper.amount = repaymentAmount - newLoanAmount; - _transferLoanRepayment({ - repayLoanDirectly: repayLoanDirectly || loanTerms.lender == loanOwner, - repaymentCredit: creditHelper, - repayingAddress: loanTerms.borrower, - currentLoanOwner: loanOwner - }); + creditHelper.amount = shortage; + _pull(creditHelper, loanTerms.borrower); + } + + try this.tryClaimRepaidLOANForLoanOwner(refinancingLoanId, loanOwner) {} catch { + // Note: Safe transfer or supply to a pool can fail. In that case the LOAN token stays in repaid state and + // waits for the LOAN token owner to claim the repaid credit. Otherwise lender would be able to prevent + // anybody from repaying the loan. } } @@ -538,21 +608,25 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { _checkLoanCanBeRepaid(loan.status, loan.defaultTimestamp); - address borrower = loan.borrower; - MultiToken.Asset memory collateral = loan.collateral; - MultiToken.Asset memory repaymentCredit = MultiToken.ERC20(loan.creditAddress, _loanRepaymentAmount(loanId)); - - (bool repayLoanDirectly, address loanOwner) = _deleteOrUpdateRepaidLoan(loanId); + // Update loan to repaid state + _updateRepaidLoan(loanId); // Execute permit for the caller - _checkPermit(msg.sender, repaymentCredit.assetAddress, permit); + _checkPermit(msg.sender, loan.creditAddress, permit); _tryPermit(permit); - // Transfer credit to the original lender or to the Vault - _transferLoanRepayment(repayLoanDirectly, repaymentCredit, msg.sender, loanOwner); + // Transfer the repaid credit to the Vault + _pull(loan.creditAddress.ERC20(_loanRepaymentAmount(loanId)), msg.sender); // Transfer collateral back to borrower - _push(collateral, borrower); + _push(loan.collateral, loan.borrower); + + // Try to repay directly + try this.tryClaimRepaidLOANForLoanOwner(loanId, loanToken.ownerOf(loanId)) {} catch { + // Note: Safe transfer or supply to a pool can fail. In that case leave the LOAN token in repaid state and + // wait for the LOAN token owner to claim the repaid credit. Otherwise lender would be able to prevent + // borrower from repaying the loan. + } } /** @@ -571,61 +645,23 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { } /** - * @notice Delete or update the original loan. - * @dev If the loan can be repaid directly to the current LOAN owner, - * the function will delete the loan and burn the LOAN token. - * If the loan cannot be repaid directly to the current LOAN owner, - * the function will move the loan to repaid state and wait for the lender to claim the repaid credit. + * @notice Update loan to repaid state. * @param loanId Id of a loan that is being repaid. - * @return repayLoanDirectly If the loan can be repaid directly to the current LOAN owner. - * @return loanOwner Address of the current LOAN owner. */ - function _deleteOrUpdateRepaidLoan(uint256 loanId) private returns (bool repayLoanDirectly, address loanOwner) { + function _updateRepaidLoan(uint256 loanId) private { LOAN storage loan = LOANs[loanId]; - emit LOANPaidBack({ loanId: loanId }); + // Move loan to repaid state and wait for the loan owner to claim the repaid credit + loan.status = 3; - // Note: Assuming that it is safe to transfer the credit asset to the original lender - // if the lender still owns the LOAN token because the lender was able to sign an offer - // or make a contract call, thus can handle incoming transfers. - loanOwner = loanToken.ownerOf(loanId); - repayLoanDirectly = loan.originalLender == loanOwner; - if (repayLoanDirectly) { - // Delete loan data & burn LOAN token before calling safe transfer - _deleteLoan(loanId); + // Update accrued interest amount + loan.fixedInterestAmount = _loanAccruedInterest(loan); + loan.accruingInterestDailyRate = 0; - emit LOANClaimed({ loanId: loanId, defaulted: false }); - } else { - // Move loan to repaid state and wait for the lender to claim the repaid credit - loan.status = 3; - // Update accrued interest amount - loan.fixedInterestAmount = _loanAccruedInterest(loan); - // Note: Reusing `fixedInterestAmount` to store accrued interest at the time of repayment - // to have the value at the time of claim and stop accruing new interest. - loan.accruingInterestDailyRate = 0; - } - } + // Note: Reusing `fixedInterestAmount` to store accrued interest at the time of repayment + // to have the value at the time of claim and stop accruing new interest. - /** - * @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. - * @param repaymentCredit Asset to be repaid. - * @param repayingAddress Address of the account repaying the loan. - * @param currentLoanOwner Address of the current LOAN owner. - */ - function _transferLoanRepayment( - bool repayLoanDirectly, - MultiToken.Asset memory repaymentCredit, - address repayingAddress, - address currentLoanOwner - ) private { - if (repayLoanDirectly) { - // Transfer the repaid credit to the LOAN token owner - _pushFrom(repaymentCredit, repayingAddress, currentLoanOwner); - } else { - // Transfer the repaid credit to the Vault - _pull(repaymentCredit, repayingAddress); - } + emit LOANPaidBack({ loanId: loanId }); } @@ -707,6 +743,57 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { revert InvalidLoanStatus(loan.status); } + /** + * @notice Try to claim a repaid loan for the loan owner. + * @dev The function is called by the vault to repay a loan directly to the original lender or its source of funds + * if the loan owner is the original lender. If the transfer fails, the LOAN token will remain in repaid state + * and the LOAN token owner will be able to claim the repaid credit. Otherwise lender would be able to prevent + * borrower from repaying the loan. + * @param loanId Id of a loan that is being claimed. + * @param loanOwner Address of the LOAN token holder. + */ + function tryClaimRepaidLOANForLoanOwner(uint256 loanId, address loanOwner) external { + if (msg.sender != address(this)) + revert CallerNotVault(); + + LOAN storage loan = LOANs[loanId]; + + if (loan.status != 3) + return; + + // If current loan owner is not original lender, the loan cannot be repaid directly, return without revert. + if (loan.originalLender != loanOwner) + return; + + // Note: The loan owner is the original lender at this point. + + address destinationOfFunds = loan.originalSourceOfFunds; + MultiToken.Asset memory repaymentCredit = loan.creditAddress.ERC20(_loanRepaymentAmount(loanId)); + + // Delete loan data & burn LOAN token before calling safe transfer + _deleteLoan(loanId); + + // Repay the original lender + if (destinationOfFunds == loanOwner) { + _push(repaymentCredit, loanOwner); + } else { + MultiToken.approveAsset(repaymentCredit, destinationOfFunds); + // Supply the repaid credit to the Compound pool + ICometLike(destinationOfFunds).supplyFrom({ + from: address(this), + dst: loanOwner, + asset: repaymentCredit.assetAddress, + amount: repaymentCredit.amount + }); + } + + // Note: If the transfer fails, the LOAN token will remain in repaid state and the LOAN token owner + // will be able to claim the repaid credit. Otherwise lender would be able to prevent borrower from + // repaying the loan. + + emit LOANClaimed({ loanId: loanId, defaulted: false }); + } + /** * @notice Settle the loan claim. * @param loanId Id of a loan that is being claimed. @@ -719,7 +806,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { // Store in memory before deleting the loan MultiToken.Asset memory asset = defaulted ? loan.collateral - : MultiToken.ERC20(loan.creditAddress, _loanRepaymentAmount(loanId)); + : loan.creditAddress.ERC20(_loanRepaymentAmount(loanId)); // Delete loan data & burn LOAN token before calling safe transfer _deleteLoan(loanId); @@ -845,9 +932,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { // Skip compensation transfer if it's not set if (extension.compensationAddress != address(0) && extension.compensationAmount > 0) { - MultiToken.Asset memory compensation = MultiToken.ERC20( - extension.compensationAddress, extension.compensationAmount - ); + MultiToken.Asset memory compensation = extension.compensationAddress.ERC20(extension.compensationAmount); // Check compensation asset validity _checkValidAsset(compensation); @@ -893,6 +978,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { * @return fixedInterestAmount Fixed interest amount in credit asset tokens. * @return credit Asset used as a loan credit. For a definition see { MultiToken dependency lib }. * @return collateral Asset used as a loan collateral. For a definition see { MultiToken dependency lib }. + * @return originalSourceOfFunds Address of a source of funds for the loan. Original lender address, if the loan was funded directly, or a pool address from witch credit funds were withdrawn / borrowred. * @return repaymentAmount Loan repayment amount in credit asset tokens. */ function getLOAN(uint256 loanId) external view returns ( @@ -906,6 +992,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { uint256 fixedInterestAmount, MultiToken.Asset memory credit, MultiToken.Asset memory collateral, + address originalSourceOfFunds, uint256 repaymentAmount ) { LOAN storage loan = LOANs[loanId]; @@ -918,8 +1005,9 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { loanOwner = loan.status != 0 ? loanToken.ownerOf(loanId) : address(0); accruingInterestDailyRate = loan.accruingInterestDailyRate; fixedInterestAmount = loan.fixedInterestAmount; - credit = MultiToken.ERC20(loan.creditAddress, loan.principalAmount); + credit = loan.creditAddress.ERC20(loan.principalAmount); collateral = loan.collateral; + originalSourceOfFunds = loan.originalSourceOfFunds; repaymentAmount = loanRepaymentAmount(loanId); } diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol index e1b2ae9..d175986 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol @@ -22,7 +22,7 @@ contract PWNSimpleLoanDutchAuctionProposal is PWNSimpleLoanProposal { * @dev EIP-712 simple proposal struct type hash. */ bytes32 public constant PROPOSAL_TYPEHASH = keccak256( - "Proposal(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address creditAddress,uint256 minCreditAmount,uint256 maxCreditAmount,uint256 availableCreditLimit,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 auctionStart,uint40 auctionDuration,address allowedAcceptor,address proposer,bool isOffer,uint256 refinancingLoanId,uint256 nonceSpace,uint256 nonce,address loanContract)" + "Proposal(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address creditAddress,uint256 minCreditAmount,uint256 maxCreditAmount,uint256 availableCreditLimit,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 auctionStart,uint40 auctionDuration,address allowedAcceptor,address proposer,bytes32 proposerSpecHash,bool isOffer,uint256 refinancingLoanId,uint256 nonceSpace,uint256 nonce,address loanContract)" ); /** @@ -44,6 +44,7 @@ contract PWNSimpleLoanDutchAuctionProposal is PWNSimpleLoanProposal { * @param auctionDuration Auction duration 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 proposerSpecHash Hash of a proposer specification. It is a hash of a proposer specific data, which must be provided during a loan creation. * @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. @@ -69,6 +70,7 @@ contract PWNSimpleLoanDutchAuctionProposal is PWNSimpleLoanProposal { uint40 auctionDuration; address allowedAcceptor; address proposer; + bytes32 proposerSpecHash; bool isOffer; uint256 refinancingLoanId; uint256 nonceSpace; @@ -285,7 +287,9 @@ contract PWNSimpleLoanDutchAuctionProposal is PWNSimpleLoanProposal { amount: creditAmount }), fixedInterestAmount: proposal.fixedInterestAmount, - accruingInterestAPR: proposal.accruingInterestAPR + accruingInterestAPR: proposal.accruingInterestAPR, + lenderSpecHash: proposal.isOffer ? proposal.proposerSpecHash : bytes32(0), + borrowerSpecHash: proposal.isOffer ? bytes32(0) : proposal.proposerSpecHash }); } diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol index d36776a..882bab0 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol @@ -29,7 +29,7 @@ contract PWNSimpleLoanFungibleProposal is PWNSimpleLoanProposal { * @dev EIP-712 simple proposal struct type hash. */ bytes32 public constant PROPOSAL_TYPEHASH = keccak256( - "Proposal(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 minCollateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address creditAddress,uint256 creditPerCollateralUnit,uint256 availableCreditLimit,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedAcceptor,address proposer,bool isOffer,uint256 refinancingLoanId,uint256 nonceSpace,uint256 nonce,address loanContract)" + "Proposal(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 minCollateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address creditAddress,uint256 creditPerCollateralUnit,uint256 availableCreditLimit,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedAcceptor,address proposer,bytes32 proposerSpecHash,bool isOffer,uint256 refinancingLoanId,uint256 nonceSpace,uint256 nonce,address loanContract)" ); /** @@ -49,6 +49,7 @@ contract PWNSimpleLoanFungibleProposal is PWNSimpleLoanProposal { * @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 proposerSpecHash Hash of a proposer specification. It is a hash of a proposer specific data, which must be provided during a loan creation. * @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. @@ -72,6 +73,7 @@ contract PWNSimpleLoanFungibleProposal is PWNSimpleLoanProposal { uint40 expiration; address allowedAcceptor; address proposer; + bytes32 proposerSpecHash; bool isOffer; uint256 refinancingLoanId; uint256 nonceSpace; @@ -221,7 +223,9 @@ contract PWNSimpleLoanFungibleProposal is PWNSimpleLoanProposal { amount: creditAmount }), fixedInterestAmount: proposal.fixedInterestAmount, - accruingInterestAPR: proposal.accruingInterestAPR + accruingInterestAPR: proposal.accruingInterestAPR, + lenderSpecHash: proposal.isOffer ? proposal.proposerSpecHash : bytes32(0), + borrowerSpecHash: proposal.isOffer ? bytes32(0) : proposal.proposerSpecHash }); } diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol index 80e776f..694aaf2 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol @@ -23,7 +23,7 @@ contract PWNSimpleLoanListProposal is PWNSimpleLoanProposal { * @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)" + "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,bytes32 proposerSpecHash,bool isOffer,uint256 refinancingLoanId,uint256 nonceSpace,uint256 nonce,address loanContract)" ); /** @@ -43,6 +43,7 @@ contract PWNSimpleLoanListProposal is PWNSimpleLoanProposal { * @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 proposerSpecHash Hash of a proposer specification. It is a hash of a proposer specific data, which must be provided during a loan creation. * @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. @@ -66,6 +67,7 @@ contract PWNSimpleLoanListProposal is PWNSimpleLoanProposal { uint40 expiration; address allowedAcceptor; address proposer; + bytes32 proposerSpecHash; bool isOffer; uint256 refinancingLoanId; uint256 nonceSpace; @@ -209,7 +211,9 @@ contract PWNSimpleLoanListProposal is PWNSimpleLoanProposal { amount: proposal.creditAmount }), fixedInterestAmount: proposal.fixedInterestAmount, - accruingInterestAPR: proposal.accruingInterestAPR + accruingInterestAPR: proposal.accruingInterestAPR, + lenderSpecHash: proposal.isOffer ? proposal.proposerSpecHash : bytes32(0), + borrowerSpecHash: proposal.isOffer ? bytes32(0) : proposal.proposerSpecHash }); } diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol index 01de7eb..14930a9 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol @@ -20,7 +20,7 @@ contract PWNSimpleLoanSimpleProposal is PWNSimpleLoanProposal { * @dev EIP-712 simple proposal struct type hash. */ bytes32 public constant PROPOSAL_TYPEHASH = keccak256( - "Proposal(uint8 collateralCategory,address collateralAddress,uint256 collateralId,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)" + "Proposal(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address creditAddress,uint256 creditAmount,uint256 availableCreditLimit,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedAcceptor,address proposer,bytes32 proposerSpecHash,bool isOffer,uint256 refinancingLoanId,uint256 nonceSpace,uint256 nonce,address loanContract)" ); /** @@ -40,6 +40,7 @@ contract PWNSimpleLoanSimpleProposal is PWNSimpleLoanProposal { * @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 proposerSpecHash Hash of a proposer specification. It is a hash of a proposer specific data, which must be provided during a loan creation. * @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. @@ -63,6 +64,7 @@ contract PWNSimpleLoanSimpleProposal is PWNSimpleLoanProposal { uint40 expiration; address allowedAcceptor; address proposer; + bytes32 proposerSpecHash; bool isOffer; uint256 refinancingLoanId; uint256 nonceSpace; @@ -175,7 +177,9 @@ contract PWNSimpleLoanSimpleProposal is PWNSimpleLoanProposal { amount: proposal.creditAmount }), fixedInterestAmount: proposal.fixedInterestAmount, - accruingInterestAPR: proposal.accruingInterestAPR + accruingInterestAPR: proposal.accruingInterestAPR, + lenderSpecHash: proposal.isOffer ? proposal.proposerSpecHash : bytes32(0), + borrowerSpecHash: proposal.isOffer ? bytes32(0) : proposal.proposerSpecHash }); } diff --git a/test/unit/PWNSimpleLoan.t.sol b/test/unit/PWNSimpleLoan.t.sol index f627e09..90ed8bc 100644 --- a/test/unit/PWNSimpleLoan.t.sol +++ b/test/unit/PWNSimpleLoan.t.sol @@ -29,7 +29,9 @@ abstract contract PWNSimpleLoanTest is Test { uint256 loanId = 42; address lender = makeAddr("lender"); address borrower = makeAddr("borrower"); + address sourceOfFunds = makeAddr("sourceOfFunds"); uint256 loanDurationInDays = 101; + PWNSimpleLoan.LenderSpec lenderSpec; PWNSimpleLoan.LOAN simpleLoan; PWNSimpleLoan.LOAN nonExistingLoan; PWNSimpleLoan.Terms simpleLoanTerms; @@ -77,9 +79,14 @@ abstract contract PWNSimpleLoanTest is Test { vm.prank(borrower); nonFungibleAsset.approve(address(loan), 2); + lenderSpec = PWNSimpleLoan.LenderSpec({ + sourceOfFunds: lender + }); + simpleLoan = PWNSimpleLoan.LOAN({ status: 2, creditAddress: address(fungibleAsset), + originalSourceOfFunds: lender, startTimestamp: uint40(block.timestamp), defaultTimestamp: uint40(block.timestamp + loanDurationInDays * 1 days), borrower: borrower, @@ -97,7 +104,9 @@ abstract contract PWNSimpleLoanTest is Test { collateral: MultiToken.ERC721(address(nonFungibleAsset), 2), credit: MultiToken.ERC20(address(fungibleAsset), 100), fixedInterestAmount: 6631, - accruingInterestAPR: 0 + accruingInterestAPR: 0, + lenderSpecHash: loan.getLenderSpecHash(lenderSpec), + borrowerSpecHash: bytes32(0) }); proposalSpec = PWNSimpleLoan.ProposalSpec({ @@ -109,6 +118,7 @@ abstract contract PWNSimpleLoanTest is Test { nonExistingLoan = PWNSimpleLoan.LOAN({ status: 0, creditAddress: address(0), + originalSourceOfFunds: address(0), startTimestamp: 0, defaultTimestamp: 0, borrower: address(0), @@ -151,6 +161,18 @@ abstract contract PWNSimpleLoanTest is Test { abi.encodeWithSignature("hasTag(address,bytes32)", proposalContract, PWNHubTags.LOAN_PROPOSAL), abi.encode(true) ); + vm.mockCall( + hub, + abi.encodeWithSignature("hasTag(address,bytes32)", sourceOfFunds, PWNHubTags.COMPOUND_V3_POOL), + abi.encode(true) + ); + + vm.mockCall( + sourceOfFunds, abi.encodeWithSignature("supplyFrom(address,address,address,uint256)"), abi.encode("") + ); + vm.mockCall( + sourceOfFunds, abi.encodeWithSignature("withdrawFrom(address,address,address,uint256)"), abi.encode("") + ); _mockLoanTerms(simpleLoanTerms); _mockLOANMint(loanId); @@ -165,6 +187,7 @@ abstract contract PWNSimpleLoanTest is Test { function _assertLOANEq(PWNSimpleLoan.LOAN memory _simpleLoan1, PWNSimpleLoan.LOAN memory _simpleLoan2) internal { assertEq(_simpleLoan1.status, _simpleLoan2.status); assertEq(_simpleLoan1.creditAddress, _simpleLoan2.creditAddress); + assertEq(_simpleLoan1.originalSourceOfFunds, _simpleLoan2.originalSourceOfFunds); assertEq(_simpleLoan1.startTimestamp, _simpleLoan2.startTimestamp); assertEq(_simpleLoan1.defaultTimestamp, _simpleLoan2.defaultTimestamp); assertEq(_simpleLoan1.borrower, _simpleLoan2.borrower); @@ -181,44 +204,48 @@ abstract contract PWNSimpleLoanTest is Test { function _assertLOANEq(uint256 _loanId, PWNSimpleLoan.LOAN memory _simpleLoan) internal { uint256 loanSlot = uint256(keccak256(abi.encode(_loanId, LOANS_SLOT))); - // Status, credit address, start timestamp, default timestamp - _assertLOANWord(loanSlot + 0, abi.encodePacked(uint8(0), _simpleLoan.defaultTimestamp, _simpleLoan.startTimestamp, _simpleLoan.creditAddress, _simpleLoan.status)); + // Status, credit address + _assertLOANWord(loanSlot + 0, abi.encodePacked(uint88(0), _simpleLoan.creditAddress, _simpleLoan.status)); + // Original source of funds, start timestamp, default timestamp + _assertLOANWord(loanSlot + 1, abi.encodePacked(uint16(0), _simpleLoan.defaultTimestamp, _simpleLoan.startTimestamp, _simpleLoan.originalSourceOfFunds)); // Borrower address - _assertLOANWord(loanSlot + 1, abi.encodePacked(uint96(0), _simpleLoan.borrower)); + _assertLOANWord(loanSlot + 2, abi.encodePacked(uint96(0), _simpleLoan.borrower)); // Original lender, accruing interest daily rate - _assertLOANWord(loanSlot + 2, abi.encodePacked(uint56(0), _simpleLoan.accruingInterestDailyRate, _simpleLoan.originalLender)); + _assertLOANWord(loanSlot + 3, abi.encodePacked(uint56(0), _simpleLoan.accruingInterestDailyRate, _simpleLoan.originalLender)); // Fixed interest amount - _assertLOANWord(loanSlot + 3, abi.encodePacked(_simpleLoan.fixedInterestAmount)); + _assertLOANWord(loanSlot + 4, abi.encodePacked(_simpleLoan.fixedInterestAmount)); // Principal amount - _assertLOANWord(loanSlot + 4, abi.encodePacked(_simpleLoan.principalAmount)); + _assertLOANWord(loanSlot + 5, abi.encodePacked(_simpleLoan.principalAmount)); // Collateral category, collateral asset address - _assertLOANWord(loanSlot + 5, abi.encodePacked(uint88(0), _simpleLoan.collateral.assetAddress, _simpleLoan.collateral.category)); + _assertLOANWord(loanSlot + 6, abi.encodePacked(uint88(0), _simpleLoan.collateral.assetAddress, _simpleLoan.collateral.category)); // Collateral id - _assertLOANWord(loanSlot + 6, abi.encodePacked(_simpleLoan.collateral.id)); + _assertLOANWord(loanSlot + 7, abi.encodePacked(_simpleLoan.collateral.id)); // Collateral amount - _assertLOANWord(loanSlot + 7, abi.encodePacked(_simpleLoan.collateral.amount)); + _assertLOANWord(loanSlot + 8, abi.encodePacked(_simpleLoan.collateral.amount)); } function _mockLOAN(uint256 _loanId, PWNSimpleLoan.LOAN memory _simpleLoan) internal { uint256 loanSlot = uint256(keccak256(abi.encode(_loanId, LOANS_SLOT))); - // Status, credit address, start timestamp, default timestamp - _storeLOANWord(loanSlot + 0, abi.encodePacked(uint8(0), _simpleLoan.defaultTimestamp, _simpleLoan.startTimestamp, _simpleLoan.creditAddress, _simpleLoan.status)); + // Status, credit address + _storeLOANWord(loanSlot + 0, abi.encodePacked(uint88(0), _simpleLoan.creditAddress, _simpleLoan.status)); + // Original source of funds, start timestamp, default timestamp + _storeLOANWord(loanSlot + 1, abi.encodePacked(uint16(0), _simpleLoan.defaultTimestamp, _simpleLoan.startTimestamp, _simpleLoan.originalSourceOfFunds)); // Borrower address - _storeLOANWord(loanSlot + 1, abi.encodePacked(uint96(0), _simpleLoan.borrower)); + _storeLOANWord(loanSlot + 2, abi.encodePacked(uint96(0), _simpleLoan.borrower)); // Original lender, accruing interest daily rate - _storeLOANWord(loanSlot + 2, abi.encodePacked(uint56(0), _simpleLoan.accruingInterestDailyRate, _simpleLoan.originalLender)); + _storeLOANWord(loanSlot + 3, abi.encodePacked(uint56(0), _simpleLoan.accruingInterestDailyRate, _simpleLoan.originalLender)); // Fixed interest amount - _storeLOANWord(loanSlot + 3, abi.encodePacked(_simpleLoan.fixedInterestAmount)); + _storeLOANWord(loanSlot + 4, abi.encodePacked(_simpleLoan.fixedInterestAmount)); // Principal amount - _storeLOANWord(loanSlot + 4, abi.encodePacked(_simpleLoan.principalAmount)); + _storeLOANWord(loanSlot + 5, abi.encodePacked(_simpleLoan.principalAmount)); // Collateral category, collateral asset address - _storeLOANWord(loanSlot + 5, abi.encodePacked(uint88(0), _simpleLoan.collateral.assetAddress, _simpleLoan.collateral.category)); + _storeLOANWord(loanSlot + 6, abi.encodePacked(uint88(0), _simpleLoan.collateral.assetAddress, _simpleLoan.collateral.category)); // Collateral id - _storeLOANWord(loanSlot + 6, abi.encodePacked(_simpleLoan.collateral.id)); + _storeLOANWord(loanSlot + 7, abi.encodePacked(_simpleLoan.collateral.id)); // Collateral amount - _storeLOANWord(loanSlot + 7, abi.encodePacked(_simpleLoan.collateral.amount)); + _storeLOANWord(loanSlot + 8, abi.encodePacked(_simpleLoan.collateral.amount)); } function _mockLoanTerms(PWNSimpleLoan.Terms memory _terms) internal { @@ -280,6 +307,19 @@ abstract contract PWNSimpleLoanTest is Test { } +/*----------------------------------------------------------*| +|* # GET LENDER SPEC HASH *| +|*----------------------------------------------------------*/ + +contract PWNSimpleLoan_GetLenderSpecHash_Test is PWNSimpleLoanTest { + + function test_shouldReturnLenderSpecHash() external { + assertEq(keccak256(abi.encode(lenderSpec)), loan.getLenderSpecHash(lenderSpec)); + } + +} + + /*----------------------------------------------------------*| |* # CREATE LOAN *| |*----------------------------------------------------------*/ @@ -294,6 +334,7 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { vm.expectRevert(abi.encodeWithSelector(AddressMissingHubTag.selector, _proposalContract, PWNHubTags.LOAN_PROPOSAL)); loan.createLOAN({ proposalSpec: proposalSpec, + lenderSpec: lenderSpec, callerSpec: callerSpec, extra: "" }); @@ -311,12 +352,13 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { vm.prank(caller); loan.createLOAN({ proposalSpec: proposalSpec, + lenderSpec: lenderSpec, callerSpec: callerSpec, extra: "" }); } - function testFuzz_shouldNotRevokeCallersNonce_whenFlagIsTrue(address caller, uint256 nonce) external { + function testFuzz_shouldNotRevokeCallersNonce_whenFlagIsFalse(address caller, uint256 nonce) external { callerSpec.revokeNonce = false; callerSpec.nonce = nonce; @@ -329,6 +371,7 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { vm.prank(caller); loan.createLOAN({ proposalSpec: proposalSpec, + lenderSpec: lenderSpec, callerSpec: callerSpec, extra: "" }); @@ -343,6 +386,51 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { vm.prank(caller); loan.createLOAN({ proposalSpec: proposalSpec, + lenderSpec: lenderSpec, + callerSpec: callerSpec, + extra: "" + }); + } + + function testFuzz_shouldFail_whenCallerNotLender_whenLenderSpecHashMismatch(bytes32 lenderSpecHash) external { + bytes32 correctLenderSpecHash = loan.getLenderSpecHash(lenderSpec); + vm.assume(lenderSpecHash != correctLenderSpecHash); + + simpleLoanTerms.lenderSpecHash = lenderSpecHash; + _mockLoanTerms(simpleLoanTerms); + + vm.expectRevert(abi.encodeWithSelector(InvalidLenderSpecHash.selector, lenderSpecHash, correctLenderSpecHash)); + loan.createLOAN({ + proposalSpec: proposalSpec, + lenderSpec: lenderSpec, + callerSpec: callerSpec, + extra: "" + }); + } + + function test_shouldNotFail_whenCallerLender_whenLenderSpecHashMismatch() external { + simpleLoanTerms.lenderSpecHash = bytes32(0); + _mockLoanTerms(simpleLoanTerms); + + vm.prank(lender); + loan.createLOAN({ + proposalSpec: proposalSpec, + lenderSpec: lenderSpec, + callerSpec: callerSpec, + extra: "" + }); + } + + function testFuzz_shouldFail_whenSourceOfFundsNotTaggedInHub(address _sourceOfFunds) external { + vm.assume(_sourceOfFunds != lender && _sourceOfFunds != sourceOfFunds); + lenderSpec.sourceOfFunds = _sourceOfFunds; + simpleLoanTerms.lenderSpecHash = loan.getLenderSpecHash(lenderSpec); + _mockLoanTerms(simpleLoanTerms); + + vm.expectRevert(abi.encodeWithSelector(AddressMissingHubTag.selector, _sourceOfFunds, PWNHubTags.COMPOUND_V3_POOL)); + loan.createLOAN({ + proposalSpec: proposalSpec, + lenderSpec: lenderSpec, callerSpec: callerSpec, extra: "" }); @@ -358,6 +446,7 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { vm.expectRevert(abi.encodeWithSelector(InvalidDuration.selector, duration, minDuration)); loan.createLOAN({ proposalSpec: proposalSpec, + lenderSpec: lenderSpec, callerSpec: callerSpec, extra: "" }); @@ -372,6 +461,7 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { vm.expectRevert(abi.encodeWithSelector(AccruingInterestAPROutOfBounds.selector, interestAPR, maxInterest)); loan.createLOAN({ proposalSpec: proposalSpec, + lenderSpec: lenderSpec, callerSpec: callerSpec, extra: "" }); @@ -395,6 +485,7 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { ); loan.createLOAN({ proposalSpec: proposalSpec, + lenderSpec: lenderSpec, callerSpec: callerSpec, extra: "" }); @@ -418,6 +509,7 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { ); loan.createLOAN({ proposalSpec: proposalSpec, + lenderSpec: lenderSpec, callerSpec: callerSpec, extra: "" }); @@ -428,6 +520,7 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { loan.createLOAN({ proposalSpec: proposalSpec, + lenderSpec: lenderSpec, callerSpec: callerSpec, extra: "" }); @@ -440,6 +533,7 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { loan.createLOAN({ proposalSpec: proposalSpec, + lenderSpec: lenderSpec, callerSpec: callerSpec, extra: "" }); @@ -459,6 +553,7 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { vm.prank(borrower); loan.createLOAN({ proposalSpec: proposalSpec, + lenderSpec: lenderSpec, callerSpec: callerSpec, extra: "" }); @@ -475,6 +570,7 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { vm.prank(borrower); loan.createLOAN({ proposalSpec: proposalSpec, + lenderSpec: lenderSpec, callerSpec: callerSpec, extra: "" }); @@ -502,6 +598,7 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { vm.prank(borrower); loan.createLOAN({ proposalSpec: proposalSpec, + lenderSpec: lenderSpec, callerSpec: callerSpec, extra: "" }); @@ -523,12 +620,13 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { loan.createLOAN({ proposalSpec: proposalSpec, + lenderSpec: lenderSpec, callerSpec: callerSpec, extra: "" }); } - function testFuzz_shouldTransferCredit_fromLender_toBorrowerAndFeeCollector( + function testFuzz_shouldTransferCredit_fromSourceOfFunds_toBorrowerAndFeeCollector_whenDirectSourceOfFunds( uint256 fee, uint256 loanAmount ) external { fee = bound(fee, 0, 9999); @@ -557,17 +655,86 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { loan.createLOAN({ proposalSpec: proposalSpec, + lenderSpec: lenderSpec, + callerSpec: callerSpec, + extra: "" + }); + } + + function testFuzz_shouldWithdrawCredit_fromSourceOfFunds_whenPoolSourceOfFunds( + uint256 fee, uint256 loanAmount + ) external { + fee = bound(fee, 0, 9999); + loanAmount = bound(loanAmount, 1, 1e40); + + lenderSpec.sourceOfFunds = sourceOfFunds; + simpleLoanTerms.lenderSpecHash = loan.getLenderSpecHash(lenderSpec); + simpleLoanTerms.credit.amount = loanAmount; + fungibleAsset.mint(address(loan), loanAmount); + + _mockLoanTerms(simpleLoanTerms); + vm.mockCall(config, abi.encodeWithSignature("fee()"), abi.encode(fee)); + + vm.expectCall( + sourceOfFunds, + abi.encodeWithSignature( + "withdrawFrom(address,address,address,uint256)", + lender, address(loan), simpleLoanTerms.credit.assetAddress, loanAmount + ) + ); + + loan.createLOAN({ + proposalSpec: proposalSpec, + lenderSpec: lenderSpec, + callerSpec: callerSpec, + extra: "" + }); + } + + function testFuzz_shouldTransferCredit_fromVault_toBorrowerAndFeeCollector_whenPoolSourceOfFunds( + uint256 fee, uint256 loanAmount + ) external { + fee = bound(fee, 0, 9999); + loanAmount = bound(loanAmount, 1, 1e40); + + lenderSpec.sourceOfFunds = sourceOfFunds; + simpleLoanTerms.lenderSpecHash = loan.getLenderSpecHash(lenderSpec); + simpleLoanTerms.credit.amount = loanAmount; + fungibleAsset.mint(address(loan), loanAmount); + + _mockLoanTerms(simpleLoanTerms); + vm.mockCall(config, abi.encodeWithSignature("fee()"), abi.encode(fee)); + + uint256 feeAmount = Math.mulDiv(loanAmount, fee, 1e4); + uint256 newAmount = loanAmount - feeAmount; + + // Fee transfer + vm.expectCall({ + callee: simpleLoanTerms.credit.assetAddress, + data: abi.encodeWithSignature("transfer(address,uint256)", feeCollector, feeAmount), + count: feeAmount > 0 ? 1 : 0 + }); + // Updated amount transfer + vm.expectCall( + simpleLoanTerms.credit.assetAddress, + abi.encodeWithSignature("transfer(address,uint256)", borrower, newAmount) + ); + + loan.createLOAN({ + proposalSpec: proposalSpec, + lenderSpec: lenderSpec, callerSpec: callerSpec, extra: "" }); } - function test_shouldEmitEvent_LOANCreated() external { + function test_shouldEmit_LOANCreated() external { vm.expectEmit(); emit LOANCreated(loanId, simpleLoanTerms, proposalHash, proposalContract, "lil extra"); loan.createLOAN({ proposalSpec: proposalSpec, + lenderSpec: lenderSpec, callerSpec: callerSpec, extra: "lil extra" }); @@ -578,6 +745,7 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { uint256 createdLoanId = loan.createLOAN({ proposalSpec: proposalSpec, + lenderSpec: lenderSpec, callerSpec: callerSpec, extra: "" }); @@ -592,7 +760,7 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { |* # REFINANCE LOAN *| |*----------------------------------------------------------*/ -/// @dev This contract tests only different behaviour of `createLOAN` with refinancingLoanId >0. +/// @dev This contract tests only different behaviour of `createLOAN` with refinancingLoanId > 0. contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { PWNSimpleLoan.LOAN refinancedLoan; @@ -610,13 +778,14 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { refinancedLoan = PWNSimpleLoan.LOAN({ status: 2, creditAddress: address(fungibleAsset), + originalSourceOfFunds: lender, startTimestamp: uint40(block.timestamp), defaultTimestamp: uint40(block.timestamp + 40039), borrower: borrower, originalLender: lender, accruingInterestDailyRate: 0, fixedInterestAmount: 6631, - principalAmount: 100, + principalAmount: 100e18, collateral: MultiToken.ERC721(address(nonFungibleAsset), 2) }); @@ -625,9 +794,11 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { borrower: borrower, duration: 40039, collateral: MultiToken.ERC721(address(nonFungibleAsset), 2), - credit: MultiToken.ERC20(address(fungibleAsset), 100), + credit: MultiToken.ERC20(address(fungibleAsset), 100e18), fixedInterestAmount: 6631, - accruingInterestAPR: 0 + accruingInterestAPR: 0, + lenderSpecHash: loan.getLenderSpecHash(lenderSpec), + borrowerSpecHash: bytes32(0) }); _mockLoanTerms(refinancedLoanTerms); @@ -637,6 +808,10 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { vm.prank(newLender); fungibleAsset.approve(address(loan), type(uint256).max); + + fungibleAsset.mint(newLender, 100e18); + fungibleAsset.mint(lender, 100e18); + fungibleAsset.mint(address(loan), 100e18); } @@ -647,6 +822,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { vm.expectRevert(abi.encodeWithSelector(NonExistingLoan.selector)); loan.createLOAN({ proposalSpec: proposalSpec, + lenderSpec: lenderSpec, callerSpec: callerSpec, extra: "" }); @@ -659,6 +835,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { vm.expectRevert(abi.encodeWithSelector(InvalidLoanStatus.selector, 3)); loan.createLOAN({ proposalSpec: proposalSpec, + lenderSpec: lenderSpec, callerSpec: callerSpec, extra: "" }); @@ -670,6 +847,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { vm.expectRevert(abi.encodeWithSelector(LoanDefaulted.selector, simpleLoan.defaultTimestamp)); loan.createLOAN({ proposalSpec: proposalSpec, + lenderSpec: lenderSpec, callerSpec: callerSpec, extra: "" }); @@ -683,6 +861,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { vm.expectRevert(abi.encodeWithSelector(RefinanceCreditMismatch.selector)); loan.createLOAN({ proposalSpec: proposalSpec, + lenderSpec: lenderSpec, callerSpec: callerSpec, extra: "" }); @@ -695,6 +874,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { vm.expectRevert(abi.encodeWithSelector(RefinanceCreditMismatch.selector)); loan.createLOAN({ proposalSpec: proposalSpec, + lenderSpec: lenderSpec, callerSpec: callerSpec, extra: "" }); @@ -709,6 +889,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { vm.expectRevert(abi.encodeWithSelector(RefinanceCollateralMismatch.selector)); loan.createLOAN({ proposalSpec: proposalSpec, + lenderSpec: lenderSpec, callerSpec: callerSpec, extra: "" }); @@ -722,6 +903,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { vm.expectRevert(abi.encodeWithSelector(RefinanceCollateralMismatch.selector)); loan.createLOAN({ proposalSpec: proposalSpec, + lenderSpec: lenderSpec, callerSpec: callerSpec, extra: "" }); @@ -735,6 +917,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { vm.expectRevert(abi.encodeWithSelector(RefinanceCollateralMismatch.selector)); loan.createLOAN({ proposalSpec: proposalSpec, + lenderSpec: lenderSpec, callerSpec: callerSpec, extra: "" }); @@ -748,6 +931,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { vm.expectRevert(abi.encodeWithSelector(RefinanceCollateralMismatch.selector)); loan.createLOAN({ proposalSpec: proposalSpec, + lenderSpec: lenderSpec, callerSpec: callerSpec, extra: "" }); @@ -761,6 +945,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { vm.expectRevert(abi.encodeWithSelector(RefinanceBorrowerMismatch.selector, simpleLoan.borrower, _borrower)); loan.createLOAN({ proposalSpec: proposalSpec, + lenderSpec: lenderSpec, callerSpec: callerSpec, extra: "" }); @@ -772,6 +957,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loan.createLOAN({ proposalSpec: proposalSpec, + lenderSpec: lenderSpec, callerSpec: callerSpec, extra: "" }); @@ -783,14 +969,18 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loan.createLOAN({ proposalSpec: proposalSpec, + lenderSpec: lenderSpec, callerSpec: callerSpec, extra: "" }); } - function test_shouldDeleteOldLoanData_whenLOANOwnerIsOriginalLender() external { + function test_shouldDeleteLoan_whenLOANOwnerIsOriginalLender() external { + vm.expectCall(loanToken, abi.encodeWithSignature("burn(uint256)", refinancingLoanId)); + loan.createLOAN({ proposalSpec: proposalSpec, + lenderSpec: lenderSpec, callerSpec: callerSpec, extra: "" }); @@ -804,352 +994,443 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loan.createLOAN({ proposalSpec: proposalSpec, + lenderSpec: lenderSpec, callerSpec: callerSpec, extra: "" }); } - function testFuzz_shouldUpdateLoanData_whenLOANOwnerIsNotOriginalLender( - uint256 _days, uint256 principal, uint256 fixedInterest, uint256 dailyInterest - ) external { + function test_shouldUpdateLoanData_whenLOANOwnerIsNotOriginalLender() external { _mockLOANTokenOwner(refinancingLoanId, makeAddr("notOriginalLender")); - _days = bound(_days, 0, loanDurationInDays - 1); - principal = bound(principal, 1, 1e40); - fixedInterest = bound(fixedInterest, 0, 1e40); - dailyInterest = bound(dailyInterest, 1, 274e8); + loan.createLOAN({ + proposalSpec: proposalSpec, + lenderSpec: lenderSpec, + callerSpec: callerSpec, + extra: "" + }); - simpleLoan.principalAmount = principal; - simpleLoan.fixedInterestAmount = fixedInterest; - simpleLoan.accruingInterestDailyRate = uint40(dailyInterest); - _mockLOAN(refinancingLoanId, simpleLoan); + // Update loan and compare + simpleLoan.status = 3; // move loan to repaid state + simpleLoan.fixedInterestAmount = loan.loanRepaymentAmount(refinancingLoanId) - simpleLoan.principalAmount; // stored accrued interest at the time of repayment + simpleLoan.accruingInterestDailyRate = 0; // stop accruing interest + _assertLOANEq(refinancingLoanId, simpleLoan); + } - vm.warp(simpleLoan.startTimestamp + _days * 1 days); + function test_shouldUpdateLoanData_whenLOANOwnerIsOriginalLender_whenDirectTransferFails() external { + _mockLOANTokenOwner(refinancingLoanId, lender); - uint256 loanRepaymentAmount = loan.loanRepaymentAmount(refinancingLoanId); - fungibleAsset.mint(borrower, loanRepaymentAmount); + vm.mockCallRevert(simpleLoan.creditAddress, abi.encodeWithSignature("transfer(address,uint256)"), ""); loan.createLOAN({ proposalSpec: proposalSpec, + lenderSpec: lenderSpec, callerSpec: callerSpec, extra: "" }); // Update loan and compare simpleLoan.status = 3; // move loan to repaid state - simpleLoan.fixedInterestAmount = loanRepaymentAmount - principal; // stored accrued interest at the time of repayment + simpleLoan.fixedInterestAmount = loan.loanRepaymentAmount(refinancingLoanId) - simpleLoan.principalAmount; // stored accrued interest at the time of repayment simpleLoan.accruingInterestDailyRate = 0; // stop accruing interest _assertLOANEq(refinancingLoanId, simpleLoan); } - function testFuzz_shouldTransferOriginalLoanRepaymentDirectly_andTransferSurplusToBorrower_whenLOANOwnerIsOriginalLender_whenRefinanceLoanMoreThanOrEqualToOriginalLoan( - uint256 refinanceAmount, uint256 fee - ) external { - uint256 loanRepaymentAmount = loan.loanRepaymentAmount(refinancingLoanId); - fee = bound(fee, 0, 9999); // 0 - 99.99% - uint256 minRefinanceAmount = Math.mulDiv(loanRepaymentAmount, 1e4, 1e4 - fee); - refinanceAmount = bound( - refinanceAmount, - minRefinanceAmount, - type(uint256).max - minRefinanceAmount - fungibleAsset.totalSupply() + // Pool withdraw + + function test_shouldWithdrawFullCreditAmountToVault_whenShouldTransferCommon_whenPoolSourceOfFunds() external { + lenderSpec.sourceOfFunds = sourceOfFunds; + refinancedLoanTerms.lenderSpecHash = loan.getLenderSpecHash(lenderSpec); + _mockLoanTerms(refinancedLoanTerms); + + vm.expectCall( + sourceOfFunds, + abi.encodeWithSignature( + "withdrawFrom(address,address,address,uint256)", + lender, address(loan), refinancedLoanTerms.credit.assetAddress, refinancedLoanTerms.credit.amount + ) ); - uint256 feeAmount = Math.mulDiv(refinanceAmount, fee, 1e4); - uint256 borrowerSurplus = refinanceAmount - feeAmount - loanRepaymentAmount; - refinancedLoanTerms.credit.amount = refinanceAmount; + loan.createLOAN({ + proposalSpec: proposalSpec, + lenderSpec: lenderSpec, + callerSpec: callerSpec, + extra: "" + }); + } + + function test_shouldWithdrawCreditWithoutCommonToVault_whenShouldNotTransferCommon_whenPoolSourceOfFunds() external { + lenderSpec.sourceOfFunds = sourceOfFunds; refinancedLoanTerms.lender = newLender; + refinancedLoanTerms.lenderSpecHash = loan.getLenderSpecHash(lenderSpec); _mockLoanTerms(refinancedLoanTerms); - _mockLOANTokenOwner(refinancingLoanId, simpleLoan.originalLender); - vm.mockCall(config, abi.encodeWithSignature("fee()"), abi.encode(fee)); + _mockLOANTokenOwner(refinancingLoanId, newLender); - fungibleAsset.mint(newLender, refinanceAmount); + vm.assume(refinancedLoanTerms.credit.amount > loan.loanRepaymentAmount(refinancingLoanId)); - vm.expectCall({ // fee transfer - callee: refinancedLoanTerms.credit.assetAddress, - data: abi.encodeWithSignature( - "transferFrom(address,address,uint256)", - newLender, feeCollector, feeAmount - ), - count: feeAmount > 0 ? 1 : 0 - }); - vm.expectCall( // lender repayment - refinancedLoanTerms.credit.assetAddress, + uint256 common = Math.min( + refinancedLoanTerms.credit.amount, // fee is zero, use whole amount + loan.loanRepaymentAmount(refinancingLoanId) + ); + + vm.expectCall( + sourceOfFunds, abi.encodeWithSignature( - "transferFrom(address,address,uint256)", - newLender, simpleLoan.originalLender, loanRepaymentAmount + "withdrawFrom(address,address,address,uint256)", + newLender, address(loan), refinancedLoanTerms.credit.assetAddress, refinancedLoanTerms.credit.amount - common ) ); - vm.expectCall({ // borrower surplus - callee: refinancedLoanTerms.credit.assetAddress, - data: abi.encodeWithSignature( - "transferFrom(address,address,uint256)", - newLender, borrower, borrowerSurplus - ), - count: borrowerSurplus > 0 ? 1 : 0 + + loan.createLOAN({ + proposalSpec: proposalSpec, + lenderSpec: lenderSpec, + callerSpec: callerSpec, + extra: "" + }); + } + + function test_shouldNotWithdrawCreditToVault_whenShouldNotTransferCommon_whenNoSurplus_whenNoFee_whenPoolSourceOfFunds() external { + lenderSpec.sourceOfFunds = sourceOfFunds; + refinancedLoanTerms.lender = newLender; + refinancedLoanTerms.lenderSpecHash = loan.getLenderSpecHash(lenderSpec); + refinancedLoanTerms.credit.amount = loan.loanRepaymentAmount(refinancingLoanId); + _mockLoanTerms(refinancedLoanTerms); + _mockLOANTokenOwner(refinancingLoanId, newLender); + + vm.expectCall({ + callee: sourceOfFunds, + data: abi.encodeWithSignature("withdrawFrom(address,address,address,uint256)"), + count: 0 }); loan.createLOAN({ proposalSpec: proposalSpec, + lenderSpec: lenderSpec, callerSpec: callerSpec, extra: "" }); } - function testFuzz_shouldTransferOriginalLoanRepaymentToVault_andTransferSurplusToBorrower_whenLOANOwnerIsNotOriginalLender_whenRefinanceLoanMoreThanOrEqualToOriginalLoan( - uint256 refinanceAmount, uint256 fee - ) external { - uint256 loanRepaymentAmount = loan.loanRepaymentAmount(refinancingLoanId); - fee = bound(fee, 0, 9999); // 0 - 99.99% - uint256 minRefinanceAmount = Math.mulDiv(loanRepaymentAmount, 1e4, 1e4 - fee); - refinanceAmount = bound( - refinanceAmount, - minRefinanceAmount, - type(uint256).max - minRefinanceAmount - fungibleAsset.totalSupply() + // Fee + + function testFuzz_shouldTransferFeeToCollector_fromLender_whenDirectSourceOfFunds(uint256 fee) external { + fee = bound(fee, 1, 9999); // 0.01 - 99.99% + + uint256 feeAmount = Math.mulDiv(refinancedLoanTerms.credit.amount, fee, 1e4); + + vm.mockCall(config, abi.encodeWithSignature("fee()"), abi.encode(fee)); + + vm.expectCall( + refinancedLoanTerms.credit.assetAddress, + abi.encodeWithSignature("transferFrom(address,address,uint256)", lender, feeCollector, feeAmount) ); - uint256 feeAmount = Math.mulDiv(refinanceAmount, fee, 1e4); - uint256 borrowerSurplus = refinanceAmount - feeAmount - loanRepaymentAmount; - refinancedLoanTerms.credit.amount = refinanceAmount; - refinancedLoanTerms.lender = newLender; + loan.createLOAN({ + proposalSpec: proposalSpec, + lenderSpec: lenderSpec, + callerSpec: callerSpec, + extra: "" + }); + } + + function testFuzz_shouldTransferFeeToCollector_fromVault_whenPoolSourceOfFunds(uint256 fee) external { + fee = bound(fee, 1, 9999); // 0.01 - 99.99% + + lenderSpec.sourceOfFunds = sourceOfFunds; + refinancedLoanTerms.lenderSpecHash = loan.getLenderSpecHash(lenderSpec); _mockLoanTerms(refinancedLoanTerms); - _mockLOANTokenOwner(refinancingLoanId, makeAddr("notOriginalLender")); + + uint256 feeAmount = Math.mulDiv(refinancedLoanTerms.credit.amount, fee, 1e4); + vm.mockCall(config, abi.encodeWithSignature("fee()"), abi.encode(fee)); - fungibleAsset.mint(newLender, refinanceAmount); + vm.expectCall( + refinancedLoanTerms.credit.assetAddress, + abi.encodeWithSignature("transfer(address,uint256)", feeCollector, feeAmount) + ); - vm.expectCall({ // fee transfer - callee: refinancedLoanTerms.credit.assetAddress, - data: abi.encodeWithSignature( - "transferFrom(address,address,uint256)", - newLender, feeCollector, feeAmount - ), - count: feeAmount > 0 ? 1 : 0 + loan.createLOAN({ + proposalSpec: proposalSpec, + lenderSpec: lenderSpec, + callerSpec: callerSpec, + extra: "" }); - vm.expectCall( // lender repayment + } + + // Transfer of common - should + + function test_shouldTransferCommonToVaul_whenLenderNotLoanOwner_whenDirectSourceOfFunds() external { + lenderSpec.sourceOfFunds = newLender; + refinancedLoanTerms.lender = newLender; + refinancedLoanTerms.lenderSpecHash = loan.getLenderSpecHash(lenderSpec); + _mockLoanTerms(refinancedLoanTerms); + _mockLOANTokenOwner(refinancingLoanId, makeAddr("loanOwner")); + + uint256 common = Math.min( + refinancedLoanTerms.credit.amount, + loan.loanRepaymentAmount(refinancingLoanId) + ); + + vm.expectCall( refinancedLoanTerms.credit.assetAddress, - abi.encodeWithSignature( - "transferFrom(address,address,uint256)", - newLender, address(loan), loanRepaymentAmount - ) + abi.encodeWithSignature("transferFrom(address,address,uint256)", newLender, address(loan), common) ); - vm.expectCall({ // borrower surplus + + loan.createLOAN({ + proposalSpec: proposalSpec, + lenderSpec: lenderSpec, + callerSpec: callerSpec, + extra: "" + }); + } + + function testFuzz_shouldTransferCommonToVaul_whenLenderOriginalLender_whenDifferentSourceOfFunds_whenDirectSourceOfFunds() external { + lenderSpec.sourceOfFunds = newLender; + refinancedLoanTerms.lender = newLender; + refinancedLoanTerms.lenderSpecHash = loan.getLenderSpecHash(lenderSpec); + _mockLoanTerms(refinancedLoanTerms); + simpleLoan.originalLender = newLender; + simpleLoan.originalSourceOfFunds = sourceOfFunds; + _mockLOAN(refinancingLoanId, simpleLoan); + _mockLOANTokenOwner(refinancingLoanId, newLender); + + uint256 common = Math.min( + refinancedLoanTerms.credit.amount, + loan.loanRepaymentAmount(refinancingLoanId) + ); + + vm.expectCall( + refinancedLoanTerms.credit.assetAddress, + abi.encodeWithSignature("transferFrom(address,address,uint256)", newLender, address(loan), common) + ); + + loan.createLOAN({ + proposalSpec: proposalSpec, + lenderSpec: lenderSpec, + callerSpec: callerSpec, + extra: "" + }); + } + + // Transfer of common - should not + + function test_shouldNotTransferCommonToVaul_whenLenderNotLoanOwner_whenPoolSourceOfFunds() external { + lenderSpec.sourceOfFunds = sourceOfFunds; + refinancedLoanTerms.lender = newLender; + refinancedLoanTerms.lenderSpecHash = loan.getLenderSpecHash(lenderSpec); + _mockLoanTerms(refinancedLoanTerms); + _mockLOANTokenOwner(refinancingLoanId, makeAddr("loanOwner")); + + vm.expectCall({ callee: refinancedLoanTerms.credit.assetAddress, - data: abi.encodeWithSignature( - "transferFrom(address,address,uint256)", - newLender, borrower, borrowerSurplus - ), - count: borrowerSurplus > 0 ? 1 : 0 + data: abi.encodeWithSignature("transferFrom(address,address,uint256)", address(loan), address(loan)), + count: 0 }); loan.createLOAN({ proposalSpec: proposalSpec, + lenderSpec: lenderSpec, callerSpec: callerSpec, extra: "" }); } - function testFuzz_shouldNotTransferOriginalLoanRepayment_andTransferSurplusToBorrower_whenLOANOwnerIsNewLender_whenRefinanceLoanMoreThanOrEqualOriginalLoan( - uint256 refinanceAmount, uint256 fee - ) external { - uint256 loanRepaymentAmount = loan.loanRepaymentAmount(refinancingLoanId); - fee = bound(fee, 0, 9999); // 0 - 99.99% - uint256 minRefinanceAmount = Math.mulDiv(loanRepaymentAmount, 1e4, 1e4 - fee); - refinanceAmount = bound( - refinanceAmount, - minRefinanceAmount, - type(uint256).max - minRefinanceAmount - fungibleAsset.totalSupply() + function test_shouldNotTransferCommonToVaul_whenLenderOriginalLender_whenDifferentSourceOfFunds_whenPoolSourceOfFunds() external { + lenderSpec.sourceOfFunds = sourceOfFunds; + refinancedLoanTerms.lender = newLender; + refinancedLoanTerms.lenderSpecHash = loan.getLenderSpecHash(lenderSpec); + _mockLoanTerms(refinancedLoanTerms); + simpleLoan.originalLender = newLender; + simpleLoan.originalSourceOfFunds = newLender; + _mockLOAN(refinancingLoanId, simpleLoan); + _mockLOANTokenOwner(refinancingLoanId, newLender); + + vm.expectCall({ + callee: refinancedLoanTerms.credit.assetAddress, + data: abi.encodeWithSignature("transferFrom(address,address,uint256)", address(loan), address(loan)), + count: 0 + }); + + loan.createLOAN({ + proposalSpec: proposalSpec, + lenderSpec: lenderSpec, + callerSpec: callerSpec, + extra: "" + }); + + } + + /// forge-config: default.fuzz.runs = 2 + function testFuzz_shouldNoTransferCommonToVaul_whenLenderLoanOwner_whenLenderOriginalLender_whenSameSourceOfFunds(bool flag) external { + lenderSpec.sourceOfFunds = flag ? newLender : sourceOfFunds; + refinancedLoanTerms.lender = newLender; + refinancedLoanTerms.lenderSpecHash = loan.getLenderSpecHash(lenderSpec); + _mockLoanTerms(refinancedLoanTerms); + simpleLoan.originalLender = newLender; + simpleLoan.originalSourceOfFunds = flag ? newLender : sourceOfFunds; + _mockLOAN(refinancingLoanId, simpleLoan); + _mockLOANTokenOwner(refinancingLoanId, newLender); + + vm.expectCall({ + callee: refinancedLoanTerms.credit.assetAddress, + data: abi.encodeWithSignature("transferFrom(address,address,uint256)", newLender, address(loan)), + count: 0 + }); + + loan.createLOAN({ + proposalSpec: proposalSpec, + lenderSpec: lenderSpec, + callerSpec: callerSpec, + extra: "" + }); + } + + // Surplus + + function test_shouldTransferSurplusToBorrower_fromNewLender_whenDirectSourceOfFunds() external { + lenderSpec.sourceOfFunds = newLender; + refinancedLoanTerms.lender = newLender; + refinancedLoanTerms.lenderSpecHash = loan.getLenderSpecHash(lenderSpec); + _mockLoanTerms(refinancedLoanTerms); + + uint256 surplus = refinancedLoanTerms.credit.amount - loan.loanRepaymentAmount(refinancingLoanId); + + vm.expectCall( + refinancedLoanTerms.credit.assetAddress, + abi.encodeWithSignature("transferFrom(address,address,uint256)", newLender, borrower, surplus) ); - uint256 feeAmount = Math.mulDiv(refinanceAmount, fee, 1e4); - uint256 borrowerSurplus = refinanceAmount - feeAmount - loanRepaymentAmount; - refinancedLoanTerms.credit.amount = refinanceAmount; + loan.createLOAN({ + proposalSpec: proposalSpec, + lenderSpec: lenderSpec, + callerSpec: callerSpec, + extra: "" + }); + } + + function test_shouldTransferSurplusToBorrower_fromVault_whenPoolSourceOfFunds() external { + lenderSpec.sourceOfFunds = sourceOfFunds; + refinancedLoanTerms.lender = newLender; + refinancedLoanTerms.lenderSpecHash = loan.getLenderSpecHash(lenderSpec); + _mockLoanTerms(refinancedLoanTerms); + + uint256 surplus = refinancedLoanTerms.credit.amount - loan.loanRepaymentAmount(refinancingLoanId); + + vm.expectCall( + refinancedLoanTerms.credit.assetAddress, + abi.encodeWithSignature("transfer(address,uint256)", borrower, surplus) + ); + + loan.createLOAN({ + proposalSpec: proposalSpec, + lenderSpec: lenderSpec, + callerSpec: callerSpec, + extra: "" + }); + } + + /// forge-config: default.fuzz.runs = 2 + function testFuzz_shouldNotTransferSurplusToBorrower_whenNoSurplus(bool flag) external { + lenderSpec.sourceOfFunds = flag ? newLender : sourceOfFunds; refinancedLoanTerms.lender = newLender; + refinancedLoanTerms.lenderSpecHash = loan.getLenderSpecHash(lenderSpec); _mockLoanTerms(refinancedLoanTerms); - _mockLOANTokenOwner(refinancingLoanId, newLender); - vm.mockCall(config, abi.encodeWithSignature("fee()"), abi.encode(fee)); - - fungibleAsset.mint(newLender, refinanceAmount); - vm.expectCall({ // fee transfer - callee: refinancedLoanTerms.credit.assetAddress, - data: abi.encodeWithSignature( - "transferFrom(address,address,uint256)", - newLender, feeCollector, feeAmount - ), - count: feeAmount > 0 ? 1 : 0 - }); - vm.expectCall({ // lender repayment + vm.expectCall({ callee: refinancedLoanTerms.credit.assetAddress, - data: abi.encodeWithSignature( - "transferFrom(address,address,uint256)", - newLender, newLender, loanRepaymentAmount - ), + data: flag + ? abi.encodeWithSignature("transferFrom(address,address,uint256)", newLender, borrower, 0) + : abi.encodeWithSignature("transfer(address,uint256)", borrower, 0), count: 0 }); - vm.expectCall({ // borrower surplus - callee: refinancedLoanTerms.credit.assetAddress, - data: abi.encodeWithSignature( - "transferFrom(address,address,uint256)", - newLender, borrower, borrowerSurplus - ), - count: borrowerSurplus > 0 ? 1 : 0 - }); loan.createLOAN({ proposalSpec: proposalSpec, + lenderSpec: lenderSpec, callerSpec: callerSpec, extra: "" }); } - function testFuzz_shouldTransferOriginalLoanRepaymentDirectly_andContributeFromBorrower_whenLOANOwnerIsOriginalLender_whenRefinanceLoanLessThanOriginalLoan( - uint256 refinanceAmount, uint256 fee - ) external { - 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); - uint256 feeAmount = Math.mulDiv(refinanceAmount, fee, 1e4); - uint256 borrowerContribution = loanRepaymentAmount - (refinanceAmount - feeAmount); + // Shortage - refinancedLoanTerms.credit.amount = refinanceAmount; - refinancedLoanTerms.lender = newLender; - _mockLoanTerms(refinancedLoanTerms); - _mockLOANTokenOwner(refinancingLoanId, simpleLoan.originalLender); - vm.mockCall(config, abi.encodeWithSignature("fee()"), abi.encode(fee)); + function test_shouldTransferShortageFromBorrowerToVaul() external { + simpleLoan.principalAmount = refinancedLoanTerms.credit.amount + 1; + _mockLOAN(refinancingLoanId, simpleLoan); - fungibleAsset.mint(newLender, refinanceAmount); + uint256 shortage = loan.loanRepaymentAmount(refinancingLoanId) - refinancedLoanTerms.credit.amount; - vm.expectCall({ // fee transfer - callee: refinancedLoanTerms.credit.assetAddress, - data: abi.encodeWithSignature( - "transferFrom(address,address,uint256)", - newLender, feeCollector, feeAmount - ), - count: feeAmount > 0 ? 1 : 0 - }); - vm.expectCall( // lender repayment + vm.expectCall( refinancedLoanTerms.credit.assetAddress, - abi.encodeWithSignature( - "transferFrom(address,address,uint256)", - newLender, simpleLoan.originalLender, refinanceAmount - feeAmount - ) + abi.encodeWithSignature("transferFrom(address,address,uint256)", borrower, address(loan), shortage) ); - vm.expectCall({ // borrower contribution - callee: refinancedLoanTerms.credit.assetAddress, - data: abi.encodeWithSignature( - "transferFrom(address,address,uint256)", - borrower, simpleLoan.originalLender, borrowerContribution - ), - count: borrowerContribution > 0 ? 1 : 0 - }); loan.createLOAN({ proposalSpec: proposalSpec, + lenderSpec: lenderSpec, callerSpec: callerSpec, extra: "" }); } - function testFuzz_shouldTransferOriginalLoanRepaymentToVault_andContributeFromBorrower_whenLOANOwnerIsNotOriginalLender_whenRefinanceLoanLessThanOriginalLoan( - uint256 refinanceAmount, uint256 fee - ) external { - 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); - uint256 feeAmount = Math.mulDiv(refinanceAmount, fee, 1e4); - uint256 borrowerContribution = loanRepaymentAmount - (refinanceAmount - feeAmount); - - refinancedLoanTerms.credit.amount = refinanceAmount; - refinancedLoanTerms.lender = newLender; + function test_shouldNotTransferShortageFromBorrowerToVaul_whenNoShortage() external { + refinancedLoanTerms.credit.amount = loan.loanRepaymentAmount(refinancingLoanId); _mockLoanTerms(refinancedLoanTerms); - _mockLOANTokenOwner(refinancingLoanId, makeAddr("notOriginalLender")); - vm.mockCall(config, abi.encodeWithSignature("fee()"), abi.encode(fee)); - - fungibleAsset.mint(newLender, refinanceAmount); - vm.expectCall({ // fee transfer - callee: refinancedLoanTerms.credit.assetAddress, - data: abi.encodeWithSignature( - "transferFrom(address,address,uint256)", - newLender, feeCollector, feeAmount - ), - count: feeAmount > 0 ? 1 : 0 - }); - vm.expectCall( // lender repayment - refinancedLoanTerms.credit.assetAddress, - abi.encodeWithSignature( - "transferFrom(address,address,uint256)", - newLender, address(loan), refinanceAmount - feeAmount - ) - ); - vm.expectCall({ // borrower contribution + vm.expectCall({ callee: refinancedLoanTerms.credit.assetAddress, - data: abi.encodeWithSignature( - "transferFrom(address,address,uint256)", - borrower, address(loan), borrowerContribution - ), - count: borrowerContribution > 0 ? 1 : 0 + data: abi.encodeWithSignature("transferFrom(address,address,uint256)", borrower, address(loan), 0), + count: 0 }); loan.createLOAN({ proposalSpec: proposalSpec, + lenderSpec: lenderSpec, callerSpec: callerSpec, extra: "" }); } - function testFuzz_shouldNotTransferOriginalLoanRepayment_andContributeFromBorrower_whenLOANOwnerIsNewLender_whenRefinanceLoanLessThanOriginalLoan( - uint256 refinanceAmount, uint256 fee - ) external { - 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); - uint256 feeAmount = Math.mulDiv(refinanceAmount, fee, 1e4); - uint256 borrowerContribution = loanRepaymentAmount - (refinanceAmount - feeAmount); - - refinancedLoanTerms.credit.amount = refinanceAmount; - refinancedLoanTerms.lender = newLender; - _mockLoanTerms(refinancedLoanTerms); - _mockLOANTokenOwner(refinancingLoanId, newLender); - vm.mockCall(config, abi.encodeWithSignature("fee()"), abi.encode(fee)); + function testFuzz_shouldCall_tryClaimRepaidLOANForLoanOwner(address loanOwner) external { + vm.assume(loanOwner != address(0)); + _mockLOANTokenOwner(refinancingLoanId, loanOwner); - fungibleAsset.mint(newLender, refinanceAmount); + vm.expectCall( + address(loan), + abi.encodeWithSignature("tryClaimRepaidLOANForLoanOwner(uint256,address)", refinancingLoanId, loanOwner) + ); - vm.expectCall({ // fee transfer - callee: refinancedLoanTerms.credit.assetAddress, - data: abi.encodeWithSignature( - "transferFrom(address,address,uint256)", - newLender, feeCollector, feeAmount - ), - count: feeAmount > 0 ? 1 : 0 - }); - vm.expectCall({ // lender repayment - callee: refinancedLoanTerms.credit.assetAddress, - data: abi.encodeWithSignature( - "transferFrom(address,address,uint256)", - newLender, newLender, refinanceAmount - feeAmount - ), - count: 0 - }); - vm.expectCall({ // borrower contribution - callee: refinancedLoanTerms.credit.assetAddress, - data: abi.encodeWithSignature( - "transferFrom(address,address,uint256)", - borrower, newLender, borrowerContribution - ), - count: borrowerContribution > 0 ? 1 : 0 + loan.createLOAN({ + proposalSpec: proposalSpec, + lenderSpec: lenderSpec, + callerSpec: callerSpec, + extra: "" }); + } + + function test_shouldNotFail_whenTryClaimRepaidLOANForLoanOwnerFails() external { + vm.mockCallRevert( + address(loan), + abi.encodeWithSignature("tryClaimRepaidLOANForLoanOwner(uint256,address)", refinancingLoanId, lender), + "" + ); loan.createLOAN({ proposalSpec: proposalSpec, + lenderSpec: lenderSpec, callerSpec: callerSpec, extra: "" }); + + simpleLoan.status = 3; + simpleLoan.fixedInterestAmount = loan.loanRepaymentAmount(refinancingLoanId) - simpleLoan.principalAmount; + simpleLoan.accruingInterestDailyRate = 0; + _assertLOANEq(refinancingLoanId, simpleLoan); + } + // More overall tests + function testFuzz_shouldRepayOriginalLoan( uint256 _days, uint256 principal, uint256 fixedInterest, uint256 dailyInterest, uint256 refinanceAmount ) external { @@ -1170,8 +1451,10 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { refinanceAmount, 1, type(uint256).max - loanRepaymentAmount - fungibleAsset.totalSupply() ); + lenderSpec.sourceOfFunds = newLender; refinancedLoanTerms.credit.amount = refinanceAmount; refinancedLoanTerms.lender = newLender; + refinancedLoanTerms.lenderSpecHash = loan.getLenderSpecHash(lenderSpec); _mockLoanTerms(refinancedLoanTerms); _mockLOANTokenOwner(refinancingLoanId, lender); @@ -1184,6 +1467,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loan.createLOAN({ proposalSpec: proposalSpec, + lenderSpec: lenderSpec, callerSpec: callerSpec, extra: "" }); @@ -1213,8 +1497,10 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { ); uint256 feeAmount = Math.mulDiv(refinanceAmount, fee, 1e4); + lenderSpec.sourceOfFunds = newLender; refinancedLoanTerms.credit.amount = refinanceAmount; refinancedLoanTerms.lender = newLender; + refinancedLoanTerms.lenderSpecHash = loan.getLenderSpecHash(lenderSpec); _mockLoanTerms(refinancedLoanTerms); _mockLOANTokenOwner(refinancingLoanId, lender); vm.mockCall(config, abi.encodeWithSignature("fee()"), abi.encode(fee)); @@ -1228,6 +1514,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loan.createLOAN({ proposalSpec: proposalSpec, + lenderSpec: lenderSpec, callerSpec: callerSpec, extra: "" }); @@ -1242,8 +1529,10 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { ); uint256 surplus = refinanceAmount - loanRepaymentAmount; + lenderSpec.sourceOfFunds = newLender; refinancedLoanTerms.credit.amount = refinanceAmount; refinancedLoanTerms.lender = newLender; + refinancedLoanTerms.lenderSpecHash = loan.getLenderSpecHash(lenderSpec); _mockLoanTerms(refinancedLoanTerms); _mockLOANTokenOwner(refinancingLoanId, lender); @@ -1252,6 +1541,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loan.createLOAN({ proposalSpec: proposalSpec, + lenderSpec: lenderSpec, callerSpec: callerSpec, extra: "" }); @@ -1259,13 +1549,15 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { assertEq(fungibleAsset.balanceOf(borrower), originalBalance + surplus); } - function testFuzz_shouldContributeFromBorrower(uint256 refinanceAmount) external { + function testFuzz_shouldTransferShortageFromBorrower(uint256 refinanceAmount) external { uint256 loanRepaymentAmount = loan.loanRepaymentAmount(refinancingLoanId); refinanceAmount = bound(refinanceAmount, 1, loanRepaymentAmount - 1); uint256 contribution = loanRepaymentAmount - refinanceAmount; + lenderSpec.sourceOfFunds = newLender; refinancedLoanTerms.credit.amount = refinanceAmount; refinancedLoanTerms.lender = newLender; + refinancedLoanTerms.lenderSpecHash = loan.getLenderSpecHash(lenderSpec); _mockLoanTerms(refinancedLoanTerms); _mockLOANTokenOwner(refinancingLoanId, lender); @@ -1274,6 +1566,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { loan.createLOAN({ proposalSpec: proposalSpec, + lenderSpec: lenderSpec, callerSpec: callerSpec, extra: "" }); @@ -1311,11 +1604,13 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { loan.repayLOAN(loanId, permit); } - function test_shouldFail_whenLoanIsNotRunning() external { - simpleLoan.status = 3; + function testFuzz_shouldFail_whenLoanIsNotRunning(uint8 status) external { + vm.assume(status != 0 && status != 2); + + simpleLoan.status = status; _mockLOAN(loanId, simpleLoan); - vm.expectRevert(abi.encodeWithSelector(InvalidLoanStatus.selector, 3)); + vm.expectRevert(abi.encodeWithSelector(InvalidLoanStatus.selector, status)); loan.repayLOAN(loanId, permit); } @@ -1326,7 +1621,7 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { loan.repayLOAN(loanId, permit); } - function testFuzz_shouldFail_whenInvalidPermitData_permitOwner(address permitOwner) external { + function testFuzz_shouldFail_whenInvalidPermitOwner_whenPermitProvided(address permitOwner) external { vm.assume(permitOwner != borrower && permitOwner != address(0)); permit.asset = simpleLoan.creditAddress; permit.owner = permitOwner; @@ -1338,7 +1633,7 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { loan.repayLOAN(loanId, permit); } - function testFuzz_shouldFail_whenInvalidPermitData_permitAsset(address permitAsset) external { + function testFuzz_shouldFail_whenInvalidPermitAsset_whenPermitProvided(address permitAsset) external { vm.assume(permitAsset != simpleLoan.creditAddress && permitAsset != address(0)); permit.asset = permitAsset; permit.owner = borrower; @@ -1350,7 +1645,7 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { loan.repayLOAN(loanId, permit); } - function test_shouldCallPermit_whenProvided() external { + function test_shouldCallPermit_whenPermitProvided() external { permit.asset = simpleLoan.creditAddress; permit.owner = borrower; permit.amount = 321; @@ -1371,47 +1666,6 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { loan.repayLOAN(loanId, permit); } - function test_shouldDeleteLoanData_whenLOANOwnerIsOriginalLender() external { - loan.repayLOAN(loanId, permit); - - _assertLOANEq(loanId, nonExistingLoan); - } - - function test_shouldBurnLOANToken_whenLOANOwnerIsOriginalLender() external { - vm.expectCall(loanToken, abi.encodeWithSignature("burn(uint256)", loanId)); - - loan.repayLOAN(loanId, permit); - } - - function testFuzz_shouldTransferRepaidAmountToLender_whenLOANOwnerIsOriginalLender( - uint256 _days, uint256 _principal, uint256 _fixedInterest, uint256 _dailyInterest - ) external { - _days = bound(_days, 0, loanDurationInDays - 1); - _principal = bound(_principal, 1, 1e40); - _fixedInterest = bound(_fixedInterest, 0, 1e40); - _dailyInterest = bound(_dailyInterest, 1, 274e8); - - simpleLoan.principalAmount = _principal; - simpleLoan.fixedInterestAmount = _fixedInterest; - simpleLoan.accruingInterestDailyRate = uint40(_dailyInterest); - _mockLOAN(loanId, simpleLoan); - - vm.warp(simpleLoan.startTimestamp + _days * 1 days); - - uint256 loanRepaymentAmount = loan.loanRepaymentAmount(loanId); - fungibleAsset.mint(borrower, loanRepaymentAmount); - - vm.expectCall( - simpleLoan.creditAddress, - abi.encodeWithSignature( - "transferFrom(address,address,uint256)", borrower, lender, loanRepaymentAmount - ) - ); - - vm.prank(borrower); - loan.repayLOAN(loanId, permit); - } - function testFuzz_shouldUpdateLoanData_whenLOANOwnerIsNotOriginalLender( uint256 _days, uint256 _principal, uint256 _fixedInterest, uint256 _dailyInterest ) external { @@ -1442,11 +1696,21 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { _assertLOANEq(loanId, simpleLoan); } - function testFuzz_shouldTransferRepaidAmountToVault_whenLOANOwnerIsNotOriginalLender( + function test_shouldDeleteLoanData_whenLOANOwnerIsOriginalLender() external { + loan.repayLOAN(loanId, permit); + + _assertLOANEq(loanId, nonExistingLoan); + } + + function test_shouldBurnLOANToken_whenLOANOwnerIsOriginalLender() external { + vm.expectCall(loanToken, abi.encodeWithSignature("burn(uint256)", loanId)); + + loan.repayLOAN(loanId, permit); + } + + function testFuzz_shouldTransferRepaidAmountToVault( uint256 _days, uint256 _principal, uint256 _fixedInterest, uint256 _dailyInterest ) external { - _mockLOANTokenOwner(loanId, notOriginalLender); - _days = bound(_days, 0, loanDurationInDays - 1); _principal = bound(_principal, 1, 1e40); _fixedInterest = bound(_fixedInterest, 0, 1e40); @@ -1485,14 +1749,41 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { loan.repayLOAN(loanId, permit); } - function test_shouldEmitEvent_LOANPaidBack() external { + function test_shouldEmit_LOANPaidBack() external { vm.expectEmit(); emit LOANPaidBack(loanId); loan.repayLOAN(loanId, permit); } - function test_shouldEmitEvent_LOANClaimed_whenLOANOwnerIsOriginalLender() external { + function testFuzz_shouldCall_tryClaimRepaidLOANForLoanOwner(address loanOwner) external { + vm.assume(loanOwner != address(0)); + _mockLOANTokenOwner(loanId, loanOwner); + + vm.expectCall( + address(loan), + abi.encodeWithSignature("tryClaimRepaidLOANForLoanOwner(uint256,address)", loanId, loanOwner) + ); + + loan.repayLOAN(loanId, permit); + } + + function test_shouldNotFail_whenTryClaimRepaidLOANForLoanOwnerFails() external { + vm.mockCallRevert( + address(loan), + abi.encodeWithSignature("tryClaimRepaidLOANForLoanOwner(uint256,address)", loanId, lender), + "" + ); + + loan.repayLOAN(loanId, permit); + + simpleLoan.status = 3; + simpleLoan.fixedInterestAmount = loan.loanRepaymentAmount(loanId) - simpleLoan.principalAmount; + simpleLoan.accruingInterestDailyRate = 0; + _assertLOANEq(loanId, simpleLoan); + } + + function test_shouldEmit_LOANClaimed_whenLOANOwnerIsOriginalLender() external { vm.expectEmit(); emit LOANClaimed(loanId, false); @@ -1672,7 +1963,7 @@ contract PWNSimpleLoan_ClaimLOAN_Test is PWNSimpleLoanTest { loan.claimLOAN(loanId); } - function test_shouldEmitEvent_LOANClaimed_whenRepaid() external { + function test_shouldEmit_LOANClaimed_whenRepaid() external { vm.expectEmit(); emit LOANClaimed(loanId, false); @@ -1680,7 +1971,7 @@ contract PWNSimpleLoan_ClaimLOAN_Test is PWNSimpleLoanTest { loan.claimLOAN(loanId); } - function test_shouldEmitEvent_LOANClaimed_whenDefaulted() external { + function test_shouldEmit_LOANClaimed_whenDefaulted() external { simpleLoan.status = 2; _mockLOAN(loanId, simpleLoan); @@ -1696,6 +1987,151 @@ contract PWNSimpleLoan_ClaimLOAN_Test is PWNSimpleLoanTest { } +/*----------------------------------------------------------*| +|* # TRY CLAIM REPAID LOAN FOR LOAN OWNER *| +|*----------------------------------------------------------*/ + +contract PWNSimpleLoan_TryClaimRepaidLOANForLoanOwner_Test is PWNSimpleLoanTest { + + function setUp() override public { + super.setUp(); + + simpleLoan.status = 3; + _mockLOAN(loanId, simpleLoan); + + // Move collateral to vault + vm.prank(borrower); + nonFungibleAsset.transferFrom(borrower, address(loan), 2); + } + + + function testFuzz_shouldFail_whenCallerIsNotVault(address caller) external { + vm.assume(caller != address(loan)); + + vm.expectRevert(abi.encodeWithSelector(CallerNotVault.selector)); + vm.prank(caller); + loan.tryClaimRepaidLOANForLoanOwner(loanId, lender); + } + + function testFuzz_shouldNotProceed_whenLoanNotInRepaidState(uint8 status) external { + vm.assume(status != 3); + + simpleLoan.status = status; + _mockLOAN(loanId, simpleLoan); + + vm.expectCall({ // Expect no call + callee: loanToken, + data: abi.encodeWithSignature("burn(uint256)", loanId), + count: 0 + }); + + vm.prank(address(loan)); + loan.tryClaimRepaidLOANForLoanOwner(loanId, lender); + + _assertLOANEq(loanId, simpleLoan); + } + + function testFuzz_shouldNotProceed_whenOriginalLenderNotEqualToLoanOwner(address loanOwner) external { + vm.assume(loanOwner != lender); + + vm.expectCall({ // Expect no call + callee: loanToken, + data: abi.encodeWithSignature("burn(uint256)", loanId), + count: 0 + }); + + vm.prank(address(loan)); + loan.tryClaimRepaidLOANForLoanOwner(loanId, loanOwner); + + _assertLOANEq(loanId, simpleLoan); + } + + function test_shouldBurnLOANToken() external { + vm.expectCall(loanToken, abi.encodeWithSignature("burn(uint256)", loanId)); + + vm.prank(address(loan)); + loan.tryClaimRepaidLOANForLoanOwner(loanId, lender); + } + + function test_shouldDeleteLOANData() external { + vm.prank(address(loan)); + loan.tryClaimRepaidLOANForLoanOwner(loanId, lender); + + _assertLOANEq(loanId, nonExistingLoan); + } + + function test_shouldTransferToOriginalLender_whenSourceOfFundsEqualToOriginalLender() external { + vm.expectCall( + simpleLoan.creditAddress, + abi.encodeWithSignature("transfer(address,uint256)", lender, loan.loanRepaymentAmount(loanId)) + ); + + vm.prank(address(loan)); + loan.tryClaimRepaidLOANForLoanOwner(loanId, lender); + } + + function test_shouldApproveSourceOfFundsToTransferAmount_whenSourceOfFundsNotEqualToOriginalLender() external { + simpleLoan.originalSourceOfFunds = sourceOfFunds; + _mockLOAN(loanId, simpleLoan); + + vm.expectCall( + simpleLoan.creditAddress, + abi.encodeWithSignature( + "approve(address,uint256)", + sourceOfFunds, loan.loanRepaymentAmount(loanId) + ) + ); + + vm.prank(address(loan)); + loan.tryClaimRepaidLOANForLoanOwner(loanId, lender); + } + + function test_shouldTransferToSourceOfFunds_whenSourceOfFundsNotEqualToOriginalLender() external { + simpleLoan.originalSourceOfFunds = sourceOfFunds; + _mockLOAN(loanId, simpleLoan); + + vm.expectCall( + sourceOfFunds, + abi.encodeWithSignature( + "supplyFrom(address,address,address,uint256)", + address(loan), lender, simpleLoan.creditAddress, loan.loanRepaymentAmount(loanId) + ) + ); + + vm.prank(address(loan)); + loan.tryClaimRepaidLOANForLoanOwner(loanId, lender); + } + + function test_shouldFail_whenTransferFails_whenSourceOfFundsEqualToOriginalLender() external { + vm.mockCallRevert(simpleLoan.creditAddress, abi.encodeWithSignature("transfer(address,uint256)"), ""); + + vm.expectRevert(); + vm.prank(address(loan)); + loan.tryClaimRepaidLOANForLoanOwner(loanId, lender); + } + + function test_shouldFail_whenTransferFails_whenSourceOfFundsNotEqualToOriginalLender() external { + simpleLoan.originalSourceOfFunds = sourceOfFunds; + _mockLOAN(loanId, simpleLoan); + + vm.mockCallRevert(sourceOfFunds, abi.encodeWithSignature("supplyFrom(address,address,address,uint256)"), ""); + + vm.expectRevert(); + vm.prank(address(loan)); + loan.tryClaimRepaidLOANForLoanOwner(loanId, lender); + } + + function test_shouldEmit_LOANClaimed() external { + vm.expectEmit(); + emit LOANClaimed(loanId, false); + + vm.prank(address(loan)); + loan.tryClaimRepaidLOANForLoanOwner(loanId, lender); + } + +} + + /*----------------------------------------------------------*| |* # MAKE EXTENSION PROPOSAL *| |*----------------------------------------------------------*/ @@ -2068,7 +2504,7 @@ contract PWNSimpleLoan_GetExtensionHash_Test is PWNSimpleLoanTest { contract PWNSimpleLoan_GetLOAN_Test is PWNSimpleLoanTest { - function testFuzz_shouldReturnStaticLOANData( + function testFuzz_shouldReturnStaticLOANData_FirstPart( uint40 _startTimestamp, uint40 _defaultTimestamp, address _borrower, @@ -2105,36 +2541,36 @@ contract PWNSimpleLoan_GetLOAN_Test is PWNSimpleLoanTest { // test every property separately to avoid stack too deep error { - (,uint40 startTimestamp,,,,,,,,,) = loan.getLOAN(loanId); + (, uint40 startTimestamp,,,,,,,,,,) = loan.getLOAN(loanId); assertEq(startTimestamp, _startTimestamp); } { - (,,uint40 defaultTimestamp,,,,,,,,) = loan.getLOAN(loanId); + (,, uint40 defaultTimestamp,,,,,,,,,) = loan.getLOAN(loanId); assertEq(defaultTimestamp, _defaultTimestamp); } { - (,,,address borrower,,,,,,,) = loan.getLOAN(loanId); + (,,, address borrower,,,,,,,,) = loan.getLOAN(loanId); assertEq(borrower, _borrower); } { - (,,,,address originalLender,,,,,,) = loan.getLOAN(loanId); + (,,,, address originalLender,,,,,,,) = loan.getLOAN(loanId); assertEq(originalLender, _originalLender); } { - (,,,,,,uint40 accruingInterestDailyRate,,,,) = loan.getLOAN(loanId); + (,,,,,, uint40 accruingInterestDailyRate,,,,,) = loan.getLOAN(loanId); assertEq(accruingInterestDailyRate, _accruingInterestDailyRate); } { - (,,,,,,,uint256 fixedInterestAmount,,,) = loan.getLOAN(loanId); + (,,,,,,, uint256 fixedInterestAmount,,,,) = loan.getLOAN(loanId); assertEq(fixedInterestAmount, _fixedInterestAmount); } { - (,,,,,,,,MultiToken.Asset memory credit,,) = loan.getLOAN(loanId); + (,,,,,,,, MultiToken.Asset memory credit,,,) = loan.getLOAN(loanId); assertEq(credit.assetAddress, _creditAddress); assertEq(credit.amount, _principalAmount); } { - (,,,,,,,,,MultiToken.Asset memory collateral,) = loan.getLOAN(loanId); + (,,,,,,,,, MultiToken.Asset memory collateral,,) = loan.getLOAN(loanId); assertEq(collateral.assetAddress, _collateralAssetAddress); assertEq(uint8(collateral.category), _collateralCategory % 4); assertEq(collateral.id, _collateralId); @@ -2142,21 +2578,35 @@ contract PWNSimpleLoan_GetLOAN_Test is PWNSimpleLoanTest { } } + function testFuzz_shouldReturnStaticLOANData_SecondPart( + address _originalSourceOfFunds + ) external { + simpleLoan.originalSourceOfFunds = _originalSourceOfFunds; + + _mockLOAN(loanId, simpleLoan); + + // test every property separately to avoid stack too deep error + { + (,,,,,,,,,, address originalSourceOfFunds,) = loan.getLOAN(loanId); + assertEq(originalSourceOfFunds, _originalSourceOfFunds); + } + } + function test_shouldReturnCorrectStatus() external { _mockLOAN(loanId, simpleLoan); - (uint8 status,,,,,,,,,,) = loan.getLOAN(loanId); + (uint8 status,,,,,,,,,,,) = loan.getLOAN(loanId); assertEq(status, 2); vm.warp(simpleLoan.defaultTimestamp); - (status,,,,,,,,,,) = loan.getLOAN(loanId); + (status,,,,,,,,,,,) = loan.getLOAN(loanId); assertEq(status, 4); simpleLoan.status = 3; _mockLOAN(loanId, simpleLoan); - (status,,,,,,,,,,) = loan.getLOAN(loanId); + (status,,,,,,,,,,,) = loan.getLOAN(loanId); assertEq(status, 3); } @@ -2164,7 +2614,7 @@ contract PWNSimpleLoan_GetLOAN_Test is PWNSimpleLoanTest { _mockLOAN(loanId, simpleLoan); _mockLOANTokenOwner(loanId, _loanOwner); - (,,,,, address loanOwner,,,,,) = loan.getLOAN(loanId); + (,,,,, address loanOwner,,,,,,) = loan.getLOAN(loanId); assertEq(loanOwner, _loanOwner); } @@ -2186,7 +2636,7 @@ contract PWNSimpleLoan_GetLOAN_Test is PWNSimpleLoanTest { vm.warp(simpleLoan.startTimestamp + _days * 1 days); - (,,,,,,,,,, uint256 repaymentAmount) = loan.getLOAN(loanId); + (,,,,,,,,,,, uint256 repaymentAmount) = loan.getLOAN(loanId); assertEq(repaymentAmount, loan.loanRepaymentAmount(loanId)); } @@ -2204,6 +2654,7 @@ contract PWNSimpleLoan_GetLOAN_Test is PWNSimpleLoanTest { uint256 fixedInterestAmount, MultiToken.Asset memory credit, MultiToken.Asset memory collateral, + address originalSourceOfFunds, uint256 repaymentAmount ) = loan.getLOAN(nonExistingLoanId); @@ -2221,6 +2672,7 @@ contract PWNSimpleLoan_GetLOAN_Test is PWNSimpleLoanTest { assertEq(uint8(collateral.category), 0); assertEq(collateral.id, 0); assertEq(collateral.amount, 0); + assertEq(originalSourceOfFunds, address(0)); assertEq(repaymentAmount, 0); } diff --git a/test/unit/PWNSimpleLoanDutchAuctionProposal.t.sol b/test/unit/PWNSimpleLoanDutchAuctionProposal.t.sol index 6951f9e..5e77c1d 100644 --- a/test/unit/PWNSimpleLoanDutchAuctionProposal.t.sol +++ b/test/unit/PWNSimpleLoanDutchAuctionProposal.t.sol @@ -50,6 +50,7 @@ abstract contract PWNSimpleLoanDutchAuctionProposalTest is PWNSimpleLoanProposal auctionDuration: 100 minutes, allowedAcceptor: address(0), proposer: proposer, + proposerSpecHash: keccak256("proposer spec"), isOffer: true, refinancingLoanId: 0, nonceSpace: 1, @@ -75,7 +76,7 @@ abstract contract PWNSimpleLoanDutchAuctionProposalTest is PWNSimpleLoanProposal proposalContractAddr )), keccak256(abi.encodePacked( - keccak256("Proposal(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address creditAddress,uint256 minCreditAmount,uint256 maxCreditAmount,uint256 availableCreditLimit,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 auctionStart,uint40 auctionDuration,address allowedAcceptor,address proposer,bool isOffer,uint256 refinancingLoanId,uint256 nonceSpace,uint256 nonce,address loanContract)"), + keccak256("Proposal(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address creditAddress,uint256 minCreditAmount,uint256 maxCreditAmount,uint256 availableCreditLimit,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 auctionStart,uint40 auctionDuration,address allowedAcceptor,address proposer,bytes32 proposerSpecHash,bool isOffer,uint256 refinancingLoanId,uint256 nonceSpace,uint256 nonce,address loanContract)"), abi.encode(_proposal) )) )); @@ -512,6 +513,8 @@ contract PWNSimpleLoanDutchAuctionProposal_AcceptProposal_Test is PWNSimpleLoanD assertEq(terms.credit.amount, creditAmount); assertEq(terms.fixedInterestAmount, proposal.fixedInterestAmount); assertEq(terms.accruingInterestAPR, proposal.accruingInterestAPR); + assertEq(terms.lenderSpecHash, isOffer ? proposal.proposerSpecHash : bytes32(0)); + assertEq(terms.borrowerSpecHash, isOffer ? bytes32(0) : proposal.proposerSpecHash); } } diff --git a/test/unit/PWNSimpleLoanFungibleProposal.t.sol b/test/unit/PWNSimpleLoanFungibleProposal.t.sol index 497a391..330462f 100644 --- a/test/unit/PWNSimpleLoanFungibleProposal.t.sol +++ b/test/unit/PWNSimpleLoanFungibleProposal.t.sol @@ -48,6 +48,7 @@ abstract contract PWNSimpleLoanFungibleProposalTest is PWNSimpleLoanProposalTest expiration: 60303, allowedAcceptor: address(0), proposer: proposer, + proposerSpecHash: keccak256("proposer spec"), isOffer: true, refinancingLoanId: 0, nonceSpace: 1, @@ -72,7 +73,7 @@ abstract contract PWNSimpleLoanFungibleProposalTest is PWNSimpleLoanProposalTest proposalContractAddr )), keccak256(abi.encodePacked( - keccak256("Proposal(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 minCollateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address creditAddress,uint256 creditPerCollateralUnit,uint256 availableCreditLimit,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedAcceptor,address proposer,bool isOffer,uint256 refinancingLoanId,uint256 nonceSpace,uint256 nonce,address loanContract)"), + keccak256("Proposal(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 minCollateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address creditAddress,uint256 creditPerCollateralUnit,uint256 availableCreditLimit,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedAcceptor,address proposer,bytes32 proposerSpecHash,bool isOffer,uint256 refinancingLoanId,uint256 nonceSpace,uint256 nonce,address loanContract)"), abi.encode(_proposal) )) )); @@ -356,6 +357,8 @@ contract PWNSimpleLoanFungibleProposal_AcceptProposal_Test is PWNSimpleLoanFungi assertEq(terms.credit.amount, proposalContract.getCreditAmount(proposalValues.collateralAmount, proposal.creditPerCollateralUnit)); assertEq(terms.fixedInterestAmount, proposal.fixedInterestAmount); assertEq(terms.accruingInterestAPR, proposal.accruingInterestAPR); + assertEq(terms.lenderSpecHash, isOffer ? proposal.proposerSpecHash : bytes32(0)); + assertEq(terms.borrowerSpecHash, isOffer ? bytes32(0) : proposal.proposerSpecHash); } } diff --git a/test/unit/PWNSimpleLoanListProposal.t.sol b/test/unit/PWNSimpleLoanListProposal.t.sol index 8ae1dc9..5f6e03e 100644 --- a/test/unit/PWNSimpleLoanListProposal.t.sol +++ b/test/unit/PWNSimpleLoanListProposal.t.sol @@ -48,6 +48,7 @@ abstract contract PWNSimpleLoanListProposalTest is PWNSimpleLoanProposalTest { expiration: 60303, allowedAcceptor: address(0), proposer: proposer, + proposerSpecHash: keccak256("proposer spec"), isOffer: true, refinancingLoanId: 0, nonceSpace: 1, @@ -73,7 +74,7 @@ abstract contract PWNSimpleLoanListProposalTest is PWNSimpleLoanProposalTest { 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)"), + 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,bytes32 proposerSpecHash,bool isOffer,uint256 refinancingLoanId,uint256 nonceSpace,uint256 nonce,address loanContract)"), abi.encode(_proposal) )) )); @@ -367,6 +368,8 @@ contract PWNSimpleLoanListProposal_AcceptProposal_Test is PWNSimpleLoanListPropo assertEq(terms.credit.amount, proposal.creditAmount); assertEq(terms.fixedInterestAmount, proposal.fixedInterestAmount); assertEq(terms.accruingInterestAPR, proposal.accruingInterestAPR); + assertEq(terms.lenderSpecHash, isOffer ? proposal.proposerSpecHash : bytes32(0)); + assertEq(terms.borrowerSpecHash, isOffer ? bytes32(0) : proposal.proposerSpecHash); } } diff --git a/test/unit/PWNSimpleLoanSimpleProposal.t.sol b/test/unit/PWNSimpleLoanSimpleProposal.t.sol index a41e462..4926043 100644 --- a/test/unit/PWNSimpleLoanSimpleProposal.t.sol +++ b/test/unit/PWNSimpleLoanSimpleProposal.t.sol @@ -45,6 +45,7 @@ abstract contract PWNSimpleLoanSimpleProposalTest is PWNSimpleLoanProposalTest { expiration: 60303, allowedAcceptor: address(0), proposer: proposer, + proposerSpecHash: keccak256("proposer spec"), isOffer: true, refinancingLoanId: 0, nonceSpace: 1, @@ -65,7 +66,7 @@ abstract contract PWNSimpleLoanSimpleProposalTest is PWNSimpleLoanProposalTest { proposalContractAddr )), keccak256(abi.encodePacked( - keccak256("Proposal(uint8 collateralCategory,address collateralAddress,uint256 collateralId,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)"), + keccak256("Proposal(uint8 collateralCategory,address collateralAddress,uint256 collateralId,uint256 collateralAmount,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address creditAddress,uint256 creditAmount,uint256 availableCreditLimit,uint256 fixedInterestAmount,uint40 accruingInterestAPR,uint32 duration,uint40 expiration,address allowedAcceptor,address proposer,bytes32 proposerSpecHash,bool isOffer,uint256 refinancingLoanId,uint256 nonceSpace,uint256 nonce,address loanContract)"), abi.encode(_proposal) )) )); @@ -284,6 +285,8 @@ contract PWNSimpleLoanSimpleProposal_AcceptProposal_Test is PWNSimpleLoanSimpleP assertEq(terms.credit.amount, proposal.creditAmount); assertEq(terms.fixedInterestAmount, proposal.fixedInterestAmount); assertEq(terms.accruingInterestAPR, proposal.accruingInterestAPR); + assertEq(terms.lenderSpecHash, isOffer ? proposal.proposerSpecHash : bytes32(0)); + assertEq(terms.borrowerSpecHash, isOffer ? bytes32(0) : proposal.proposerSpecHash); } } From 50ac87524fb4e41b6732c57f96caece73d7766d3 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Thu, 4 Apr 2024 14:51:19 -0400 Subject: [PATCH 067/129] feat: extend loan creation event by lender spec --- src/loan/terms/simple/loan/PWNSimpleLoan.sol | 5 +++-- test/unit/PWNSimpleLoan.t.sol | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/loan/terms/simple/loan/PWNSimpleLoan.sol b/src/loan/terms/simple/loan/PWNSimpleLoan.sol index 8b312cf..764d8d1 100644 --- a/src/loan/terms/simple/loan/PWNSimpleLoan.sol +++ b/src/loan/terms/simple/loan/PWNSimpleLoan.sol @@ -196,7 +196,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { /** * @dev Emitted when a new loan in created. */ - event LOANCreated(uint256 indexed loanId, Terms terms, bytes32 indexed proposalHash, address indexed proposalContract, bytes extra); + event LOANCreated(uint256 indexed loanId, bytes32 indexed proposalHash, address indexed proposalContract, Terms terms, LenderSpec lenderSpec, bytes extra); /** * @dev Emitted when a loan is refinanced. @@ -448,9 +448,10 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { emit LOANCreated({ loanId: loanId, - terms: loanTerms, proposalHash: proposalHash, proposalContract: proposalContract, + terms: loanTerms, + lenderSpec: lenderSpec, extra: extra }); } diff --git a/test/unit/PWNSimpleLoan.t.sol b/test/unit/PWNSimpleLoan.t.sol index 90ed8bc..a018a3d 100644 --- a/test/unit/PWNSimpleLoan.t.sol +++ b/test/unit/PWNSimpleLoan.t.sol @@ -44,7 +44,7 @@ abstract contract PWNSimpleLoanTest is Test { bytes32 proposalHash = keccak256("proposalHash"); - event LOANCreated(uint256 indexed loanId, PWNSimpleLoan.Terms terms, bytes32 indexed proposalHash, address indexed proposalContract, bytes extra); + event LOANCreated(uint256 indexed loanId, bytes32 indexed proposalHash, address indexed proposalContract, PWNSimpleLoan.Terms terms, PWNSimpleLoan.LenderSpec lenderSpec, bytes extra); event LOANPaidBack(uint256 indexed loanId); event LOANClaimed(uint256 indexed loanId, bool indexed defaulted); event LOANRefinanced(uint256 indexed loanId, uint256 indexed refinancedLoanId); @@ -730,7 +730,7 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { function test_shouldEmit_LOANCreated() external { vm.expectEmit(); - emit LOANCreated(loanId, simpleLoanTerms, proposalHash, proposalContract, "lil extra"); + emit LOANCreated(loanId, proposalHash, proposalContract, simpleLoanTerms, lenderSpec, "lil extra"); loan.createLOAN({ proposalSpec: proposalSpec, From d1291d6e51cf04d89bb9a1ef9aec87d0e9166a1a Mon Sep 17 00:00:00 2001 From: ashhanai Date: Thu, 4 Apr 2024 15:36:31 -0400 Subject: [PATCH 068/129] feat: check vault balance after compound withdraw and supply --- src/loan/terms/simple/loan/PWNSimpleLoan.sol | 31 ++++------- .../simple/loan => vault}/ICometLike.sol | 0 src/loan/vault/PWNVault.sol | 53 ++++++++++++++++--- test/helper/DummyCompoundPool.sol | 19 +++++++ test/unit/PWNSimpleLoan.t.sol | 15 ++---- 5 files changed, 80 insertions(+), 38 deletions(-) rename src/loan/{terms/simple/loan => vault}/ICometLike.sol (100%) create mode 100644 test/helper/DummyCompoundPool.sol diff --git a/src/loan/terms/simple/loan/PWNSimpleLoan.sol b/src/loan/terms/simple/loan/PWNSimpleLoan.sol index 764d8d1..ff4d173 100644 --- a/src/loan/terms/simple/loan/PWNSimpleLoan.sol +++ b/src/loan/terms/simple/loan/PWNSimpleLoan.sol @@ -11,7 +11,6 @@ import { PWNHub } from "@pwn/hub/PWNHub.sol"; import { PWNHubTags } from "@pwn/hub/PWNHubTags.sol"; import { PWNFeeCalculator } from "@pwn/loan/lib/PWNFeeCalculator.sol"; import { PWNSignatureChecker } from "@pwn/loan/lib/PWNSignatureChecker.sol"; -import { ICometLike } from "@pwn/loan/terms/simple/loan/ICometLike.sol"; import { PWNSimpleLoanProposal } from "@pwn/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol"; import { IERC5646 } from "@pwn/loan/token/IERC5646.sol"; import { IPWNLoanMetadataProvider } from "@pwn/loan/token/IPWNLoanMetadataProvider.sol"; @@ -475,9 +474,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { // Note: Lender is not source of funds. // Withdraw credit asset to the loan contract and use it as a credit provider. - ICometLike(lenderSpec.sourceOfFunds).withdrawFrom( - loanTerms.lender, address(this), loanTerms.credit.assetAddress, loanTerms.credit.amount - ); + _withdrawFromCompound(loanTerms.credit, lenderSpec.sourceOfFunds, loanTerms.lender); creditProvider = address(this); } @@ -533,6 +530,9 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { loanTerms.lender != loanOwner || (loan.originalLender == loanOwner && loan.originalSourceOfFunds != lenderSpec.sourceOfFunds); + // Note: `creditHelper` must not be used before updating the amount. + MultiToken.Asset memory creditHelper = loanTerms.credit; + // Decide credit provider address creditProvider = loanTerms.lender; if (lenderSpec.sourceOfFunds != loanTerms.lender) { @@ -540,21 +540,13 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { // Note: Lender is not the source of funds. Withdraw credit asset to the Vault and use it // as a credit provider to minimize the number of withdrawals. - { - address creditAddr = loanTerms.credit.assetAddress; - uint256 withdrawAmount = feeAmount + (shouldTransferCommon ? common : 0) + surplus; - if (withdrawAmount > 0) { - ICometLike(lenderSpec.sourceOfFunds).withdrawFrom( - loanTerms.lender, address(this), creditAddr, withdrawAmount - ); - } + creditHelper.amount = feeAmount + (shouldTransferCommon ? common : 0) + surplus; + if (creditHelper.amount > 0) { + _withdrawFromCompound(creditHelper, lenderSpec.sourceOfFunds, loanTerms.lender); } creditProvider = address(this); } - // Note: `creditHelper` must not be used before updating the amount. - MultiToken.Asset memory creditHelper = loanTerms.credit; - // Collect fees if (feeAmount > 0) { creditHelper.amount = feeAmount; @@ -778,14 +770,9 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { if (destinationOfFunds == loanOwner) { _push(repaymentCredit, loanOwner); } else { - MultiToken.approveAsset(repaymentCredit, destinationOfFunds); // Supply the repaid credit to the Compound pool - ICometLike(destinationOfFunds).supplyFrom({ - from: address(this), - dst: loanOwner, - asset: repaymentCredit.assetAddress, - amount: repaymentCredit.amount - }); + MultiToken.approveAsset(repaymentCredit, destinationOfFunds); + _supplyToCompound(repaymentCredit, destinationOfFunds, loanOwner); } // Note: If the transfer fails, the LOAN token will remain in repaid state and the LOAN token owner diff --git a/src/loan/terms/simple/loan/ICometLike.sol b/src/loan/vault/ICometLike.sol similarity index 100% rename from src/loan/terms/simple/loan/ICometLike.sol rename to src/loan/vault/ICometLike.sol diff --git a/src/loan/vault/PWNVault.sol b/src/loan/vault/PWNVault.sol index 55aa8bc..e4a5227 100644 --- a/src/loan/vault/PWNVault.sol +++ b/src/loan/vault/PWNVault.sol @@ -7,6 +7,7 @@ import { IERC20Permit } from "openzeppelin-contracts/contracts/token/ERC20/exten import { IERC721Receiver } from "openzeppelin-contracts/contracts/token/ERC721/IERC721Receiver.sol"; import { IERC1155Receiver, IERC165 } from "openzeppelin-contracts/contracts/token/ERC1155/IERC1155Receiver.sol"; +import { ICometLike } from "@pwn/loan/vault/ICometLike.sol"; import { Permit } from "@pwn/loan/vault/Permit.sol"; import "@pwn/PWNErrors.sol"; @@ -53,7 +54,7 @@ abstract contract PWNVault is IERC721Receiver, IERC1155Receiver { uint256 originalBalance = asset.balanceOf(address(this)); asset.transferAssetFrom(origin, address(this)); - _checkTransfer(asset, originalBalance, address(this)); + _checkTransfer(asset, originalBalance, address(this), true); emit VaultPull(asset, origin); } @@ -68,7 +69,7 @@ abstract contract PWNVault is IERC721Receiver, IERC1155Receiver { uint256 originalBalance = asset.balanceOf(beneficiary); asset.safeTransferAssetFrom(address(this), beneficiary); - _checkTransfer(asset, originalBalance, beneficiary); + _checkTransfer(asset, originalBalance, beneficiary, true); emit VaultPush(asset, beneficiary); } @@ -84,14 +85,54 @@ abstract contract PWNVault is IERC721Receiver, IERC1155Receiver { uint256 originalBalance = asset.balanceOf(beneficiary); asset.safeTransferAssetFrom(origin, beneficiary); - _checkTransfer(asset, originalBalance, beneficiary); + _checkTransfer(asset, originalBalance, beneficiary, true); emit VaultPushFrom(asset, origin, beneficiary); } - function _checkTransfer(MultiToken.Asset memory asset, uint256 originalBalance, address recipient) private view { - if (originalBalance + asset.getTransferAmount() != asset.balanceOf(recipient)) - revert IncompleteTransfer(); + /** + * @notice Function withdrawing an asset from a Compound pool to a vault. + * @dev The function assumes a prior token approval to a vault address and check for a valid pool address. + * @param asset An asset construct - for a definition see { MultiToken dependency lib }. + * @param pool An address of a Compound pool. + * @param from An address on which behalf the asset is withdrawn. + */ + function _withdrawFromCompound(MultiToken.Asset memory asset, address pool, address from) internal { + uint256 originalBalance = asset.balanceOf(address(this)); + + ICometLike(pool).withdrawFrom(from, address(this), asset.assetAddress, asset.amount); + _checkTransfer(asset, originalBalance, address(this), true); + } + + /** + * @notice Function supplying an asset to a Compound pool from a vault. + * @dev The function assumes a prior token approval to a vault address and check for a valid pool address. + * @param asset An asset construct - for a definition see { MultiToken dependency lib }. + * @param pool An address of a Compound pool. + * @param dst An address on which behalf the asset is supplied. + */ + function _supplyToCompound(MultiToken.Asset memory asset, address pool, address dst) internal { + uint256 originalBalance = asset.balanceOf(address(this)); + + ICometLike(pool).supplyFrom(address(this), dst, asset.assetAddress, asset.amount); + _checkTransfer(asset, originalBalance, address(this), false); + } + + function _checkTransfer( + MultiToken.Asset memory asset, + uint256 originalBalance, + address checkedAddress, + bool checkIncreasingBalance + ) private view { + if (checkIncreasingBalance) { + if (originalBalance + asset.getTransferAmount() != asset.balanceOf(checkedAddress)) { + revert IncompleteTransfer(); + } + } else { + if (originalBalance - asset.getTransferAmount() != asset.balanceOf(checkedAddress)) { + revert IncompleteTransfer(); + } + } } diff --git a/test/helper/DummyCompoundPool.sol b/test/helper/DummyCompoundPool.sol new file mode 100644 index 0000000..c77c2f5 --- /dev/null +++ b/test/helper/DummyCompoundPool.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.16; + +import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; + +import { ICometLike } from "@pwn/loan/vault/ICometLike.sol"; + + +contract DummyCompoundPool is ICometLike { + + function supplyFrom(address from, address, address asset, uint amount) external { + IERC20(asset).transferFrom(from, address(this), amount); + } + + function withdrawFrom(address, address to, address asset, uint amount) external { + IERC20(asset).transfer(to, amount); + } + +} diff --git a/test/unit/PWNSimpleLoan.t.sol b/test/unit/PWNSimpleLoan.t.sol index a018a3d..127ac49 100644 --- a/test/unit/PWNSimpleLoan.t.sol +++ b/test/unit/PWNSimpleLoan.t.sol @@ -8,6 +8,7 @@ import "@pwn/PWNErrors.sol"; import { T20 } from "@pwn-test/helper/token/T20.sol"; import { T721 } from "@pwn-test/helper/token/T721.sol"; +import { DummyCompoundPool } from "@pwn-test/helper/DummyCompoundPool.sol"; abstract contract PWNSimpleLoanTest is Test { @@ -29,7 +30,7 @@ abstract contract PWNSimpleLoanTest is Test { uint256 loanId = 42; address lender = makeAddr("lender"); address borrower = makeAddr("borrower"); - address sourceOfFunds = makeAddr("sourceOfFunds"); + address sourceOfFunds = address(new DummyCompoundPool()); uint256 loanDurationInDays = 101; PWNSimpleLoan.LenderSpec lenderSpec; PWNSimpleLoan.LOAN simpleLoan; @@ -65,6 +66,7 @@ abstract contract PWNSimpleLoanTest is Test { fungibleAsset.mint(borrower, 6831); fungibleAsset.mint(address(this), 6831); fungibleAsset.mint(address(loan), 6831); + fungibleAsset.mint(sourceOfFunds, 1e30); nonFungibleAsset.mint(borrower, 2); vm.prank(lender); @@ -167,13 +169,6 @@ abstract contract PWNSimpleLoanTest is Test { abi.encode(true) ); - vm.mockCall( - sourceOfFunds, abi.encodeWithSignature("supplyFrom(address,address,address,uint256)"), abi.encode("") - ); - vm.mockCall( - sourceOfFunds, abi.encodeWithSignature("withdrawFrom(address,address,address,uint256)"), abi.encode("") - ); - _mockLoanTerms(simpleLoanTerms); _mockLOANMint(loanId); _mockLOANTokenOwner(loanId, lender); @@ -670,7 +665,7 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { lenderSpec.sourceOfFunds = sourceOfFunds; simpleLoanTerms.lenderSpecHash = loan.getLenderSpecHash(lenderSpec); simpleLoanTerms.credit.amount = loanAmount; - fungibleAsset.mint(address(loan), loanAmount); + fungibleAsset.mint(sourceOfFunds, loanAmount); _mockLoanTerms(simpleLoanTerms); vm.mockCall(config, abi.encodeWithSignature("fee()"), abi.encode(fee)); @@ -700,7 +695,7 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { lenderSpec.sourceOfFunds = sourceOfFunds; simpleLoanTerms.lenderSpecHash = loan.getLenderSpecHash(lenderSpec); simpleLoanTerms.credit.amount = loanAmount; - fungibleAsset.mint(address(loan), loanAmount); + fungibleAsset.mint(sourceOfFunds, loanAmount); _mockLoanTerms(simpleLoanTerms); vm.mockCall(config, abi.encodeWithSignature("fee()"), abi.encode(fee)); From 21774f75420dba92566dfb30686f8a9dc4ab3a24 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Fri, 5 Apr 2024 16:03:24 -0400 Subject: [PATCH 069/129] feat(multiproposal): implement multiproposal --- src/loan/terms/simple/loan/PWNSimpleLoan.sol | 3 + .../PWNSimpleLoanDutchAuctionProposal.sol | 2 + .../PWNSimpleLoanFungibleProposal.sol | 2 + .../proposal/PWNSimpleLoanListProposal.sol | 2 + .../simple/proposal/PWNSimpleLoanProposal.sol | 57 +++++- .../proposal/PWNSimpleLoanSimpleProposal.sol | 2 + test/unit/PWNSimpleLoan.t.sol | 18 +- .../PWNSimpleLoanDutchAuctionProposal.t.sol | 60 +++--- test/unit/PWNSimpleLoanFungibleProposal.t.sol | 58 +++--- test/unit/PWNSimpleLoanListProposal.t.sol | 61 +++--- test/unit/PWNSimpleLoanProposal.t.sol | 180 ++++++++++++++++-- test/unit/PWNSimpleLoanSimpleProposal.t.sol | 50 ++--- 12 files changed, 340 insertions(+), 155 deletions(-) diff --git a/src/loan/terms/simple/loan/PWNSimpleLoan.sol b/src/loan/terms/simple/loan/PWNSimpleLoan.sol index ff4d173..8c62edf 100644 --- a/src/loan/terms/simple/loan/PWNSimpleLoan.sol +++ b/src/loan/terms/simple/loan/PWNSimpleLoan.sol @@ -94,11 +94,13 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { * @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 proposalInclusionProof Inclusion proof of the proposal in the proposal contract. * @param signature Signature of the proposal. */ struct ProposalSpec { address proposalContract; bytes proposalData; + bytes32[] proposalInclusionProof; bytes signature; } @@ -297,6 +299,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { acceptor: msg.sender, refinancingLoanId: callerSpec.refinancingLoanId, proposalData: proposalSpec.proposalData, + proposalInclusionProof: proposalSpec.proposalInclusionProof, signature: proposalSpec.signature }); diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol index d175986..887dc3a 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol @@ -211,6 +211,7 @@ contract PWNSimpleLoanDutchAuctionProposal is PWNSimpleLoanProposal { address acceptor, uint256 refinancingLoanId, bytes calldata proposalData, + bytes32[] calldata proposalInclusionProof, bytes calldata signature ) override external returns (bytes32 proposalHash, PWNSimpleLoan.Terms memory loanTerms) { // Decode proposal data @@ -252,6 +253,7 @@ contract PWNSimpleLoanDutchAuctionProposal is PWNSimpleLoanProposal { acceptor, refinancingLoanId, proposalHash, + proposalInclusionProof, signature, ProposalBase({ collateralAddress: proposal.collateralAddress, diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol index 882bab0..47e9ed5 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol @@ -161,6 +161,7 @@ contract PWNSimpleLoanFungibleProposal is PWNSimpleLoanProposal { address acceptor, uint256 refinancingLoanId, bytes calldata proposalData, + bytes32[] calldata proposalInclusionProof, bytes calldata signature ) override external returns (bytes32 proposalHash, PWNSimpleLoan.Terms memory loanTerms) { // Decode proposal data @@ -188,6 +189,7 @@ contract PWNSimpleLoanFungibleProposal is PWNSimpleLoanProposal { acceptor, refinancingLoanId, proposalHash, + proposalInclusionProof, signature, ProposalBase({ collateralAddress: proposal.collateralAddress, diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol index 694aaf2..4008999 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol @@ -149,6 +149,7 @@ contract PWNSimpleLoanListProposal is PWNSimpleLoanProposal { address acceptor, uint256 refinancingLoanId, bytes calldata proposalData, + bytes32[] calldata proposalInclusionProof, bytes calldata signature ) override external returns (bytes32 proposalHash, PWNSimpleLoan.Terms memory loanTerms) { // Decode proposal data @@ -176,6 +177,7 @@ contract PWNSimpleLoanListProposal is PWNSimpleLoanProposal { acceptor, refinancingLoanId, proposalHash, + proposalInclusionProof, signature, ProposalBase({ collateralAddress: proposal.collateralAddress, diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol index 1de93c5..99bbbb6 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; +import { MerkleProof } from "openzeppelin-contracts/contracts/utils/cryptography/MerkleProof.sol"; + import { PWNConfig, IERC5646 } from "@pwn/config/PWNConfig.sol"; import { PWNHub } from "@pwn/hub/PWNHub.sol"; import { PWNHubTags } from "@pwn/hub/PWNHubTags.sol"; @@ -16,11 +18,18 @@ import "@pwn/PWNErrors.sol"; abstract contract PWNSimpleLoanProposal { bytes32 public immutable DOMAIN_SEPARATOR; + bytes32 public immutable MULTIPROPOSAL_DOMAIN_SEPARATOR; PWNHub public immutable hub; PWNRevokedNonce public immutable revokedNonce; PWNConfig public immutable config; + bytes32 public constant MULTIPROPOSAL_TYPEHASH = keccak256("Multiproposal(bytes32 multiproposalMerkleRoot)"); + + struct Multiproposal { + bytes32 multiproposalMerkleRoot; + } + struct ProposalBase { address collateralAddress; uint256 collateralId; @@ -69,6 +78,11 @@ abstract contract PWNSimpleLoanProposal { block.chainid, address(this) )); + + MULTIPROPOSAL_DOMAIN_SEPARATOR = keccak256(abi.encode( + keccak256("EIP712Domain(string name)"), + keccak256("PWNMultiproposal") + )); } @@ -76,6 +90,19 @@ abstract contract PWNSimpleLoanProposal { |* # EXTERNALS *| |*----------------------------------------------------------*/ + /** + * @notice Get a multiproposal hash according to EIP-712. + * @param multiproposal Multiproposal struct. + * @return Multiproposal hash. + */ + function getMultiproposalHash(Multiproposal memory multiproposal) public view returns (bytes32) { + return keccak256(abi.encodePacked( + hex"1901", MULTIPROPOSAL_DOMAIN_SEPARATOR, keccak256(abi.encodePacked( + MULTIPROPOSAL_TYPEHASH, abi.encode(multiproposal) + )) + )); + } + /** * @notice Helper function for revoking a proposal nonce on behalf of a caller. * @param nonceSpace Nonce space of a proposal nonce to be revoked. @@ -91,6 +118,7 @@ abstract contract PWNSimpleLoanProposal { * @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. + * @param proposalInclusionProof Multiproposal inclusion proof. Empty if single proposal. * @return proposalHash Proposal hash. * @return loanTerms Loan terms. */ @@ -98,6 +126,7 @@ abstract contract PWNSimpleLoanProposal { address acceptor, uint256 refinancingLoanId, bytes calldata proposalData, + bytes32[] calldata proposalInclusionProof, bytes calldata signature ) virtual external returns (bytes32 proposalHash, PWNSimpleLoan.Terms memory loanTerms); @@ -141,6 +170,7 @@ abstract contract PWNSimpleLoanProposal { * @param acceptor Address of a proposal acceptor. * @param refinancingLoanId Refinancing loan ID. * @param proposalHash Proposal hash. + * @param proposalInclusionProof Multiproposal inclusion proof. Empty if single proposal. * @param signature Signature of a proposal. * @param proposal Proposal base struct. */ @@ -148,7 +178,8 @@ abstract contract PWNSimpleLoanProposal { address acceptor, uint256 refinancingLoanId, bytes32 proposalHash, - bytes memory signature, + bytes32[] calldata proposalInclusionProof, + bytes calldata signature, ProposalBase memory proposal ) internal { // Check loan contract @@ -159,10 +190,26 @@ abstract contract PWNSimpleLoanProposal { 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(proposal.proposer, proposalHash, signature)) { - revert InvalidSignature({ signer: proposal.proposer, digest: proposalHash }); + // Check proposal signature or that it was made on-chain + if (proposalInclusionProof.length == 0) { + // Single proposal signature + if (!proposalsMade[proposalHash]) { + if (!PWNSignatureChecker.isValidSignatureNow(proposal.proposer, proposalHash, signature)) { + revert InvalidSignature({ signer: proposal.proposer, digest: proposalHash }); + } + } + } else { + // Multiproposal signature + bytes32 multiproposalHash = getMultiproposalHash( + Multiproposal({ + multiproposalMerkleRoot: MerkleProof.processProofCalldata({ + proof: proposalInclusionProof, + leaf: proposalHash + }) + }) + ); + if (!PWNSignatureChecker.isValidSignatureNow(proposal.proposer, multiproposalHash, signature)) { + revert InvalidSignature({ signer: proposal.proposer, digest: multiproposalHash }); } } diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol index 14930a9..c1096ad 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol @@ -129,6 +129,7 @@ contract PWNSimpleLoanSimpleProposal is PWNSimpleLoanProposal { address acceptor, uint256 refinancingLoanId, bytes calldata proposalData, + bytes32[] calldata proposalInclusionProof, bytes calldata signature ) override external returns (bytes32 proposalHash, PWNSimpleLoan.Terms memory loanTerms) { // Decode proposal data @@ -142,6 +143,7 @@ contract PWNSimpleLoanSimpleProposal is PWNSimpleLoanProposal { acceptor, refinancingLoanId, proposalHash, + proposalInclusionProof, signature, ProposalBase({ collateralAddress: proposal.collateralAddress, diff --git a/test/unit/PWNSimpleLoan.t.sol b/test/unit/PWNSimpleLoan.t.sol index 127ac49..dc28d22 100644 --- a/test/unit/PWNSimpleLoan.t.sol +++ b/test/unit/PWNSimpleLoan.t.sol @@ -32,11 +32,11 @@ abstract contract PWNSimpleLoanTest is Test { address borrower = makeAddr("borrower"); address sourceOfFunds = address(new DummyCompoundPool()); uint256 loanDurationInDays = 101; - PWNSimpleLoan.LenderSpec lenderSpec; PWNSimpleLoan.LOAN simpleLoan; PWNSimpleLoan.LOAN nonExistingLoan; PWNSimpleLoan.Terms simpleLoanTerms; PWNSimpleLoan.ProposalSpec proposalSpec; + PWNSimpleLoan.LenderSpec lenderSpec; PWNSimpleLoan.CallerSpec callerSpec; PWNSimpleLoan.ExtensionProposal extension; T20 fungibleAsset; @@ -114,6 +114,7 @@ abstract contract PWNSimpleLoanTest is Test { proposalSpec = PWNSimpleLoan.ProposalSpec({ proposalContract: proposalContract, proposalData: proposalData, + proposalInclusionProof: new bytes32[](0), signature: signature }); @@ -246,7 +247,7 @@ abstract contract PWNSimpleLoanTest is Test { function _mockLoanTerms(PWNSimpleLoan.Terms memory _terms) internal { vm.mockCall( proposalContract, - abi.encodeWithSignature("acceptProposal(address,uint256,bytes,bytes)"), + abi.encodeWithSignature("acceptProposal(address,uint256,bytes,bytes32[],bytes)"), abi.encode(proposalHash, _terms) ); } @@ -372,10 +373,19 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { }); } - function testFuzz_shouldCallProposalContract(address caller) external { + function testFuzz_shouldCallProposalContract( + address caller, bytes memory _proposalData, bytes32[] memory _proposalInclusionProof, bytes memory _signature + ) external { + proposalSpec.proposalData = _proposalData; + proposalSpec.proposalInclusionProof = _proposalInclusionProof; + proposalSpec.signature = _signature; + vm.expectCall( proposalContract, - abi.encodeWithSignature("acceptProposal(address,uint256,bytes,bytes)", caller, 0, proposalData, signature) + abi.encodeWithSignature( + "acceptProposal(address,uint256,bytes,bytes32[],bytes)", + caller, 0, _proposalData, _proposalInclusionProof, _signature + ) ); vm.prank(caller); diff --git a/test/unit/PWNSimpleLoanDutchAuctionProposal.t.sol b/test/unit/PWNSimpleLoanDutchAuctionProposal.t.sol index 5e77c1d..8db63b7 100644 --- a/test/unit/PWNSimpleLoanDutchAuctionProposal.t.sol +++ b/test/unit/PWNSimpleLoanDutchAuctionProposal.t.sol @@ -82,55 +82,46 @@ abstract contract PWNSimpleLoanDutchAuctionProposalTest is PWNSimpleLoanProposal )); } - function _updateProposal(Params memory _params) internal { - if (_params.base.isOffer) { - proposal.minCreditAmount = _params.base.creditAmount; + function _updateProposal(PWNSimpleLoanProposal.ProposalBase memory _proposal) internal { + if (_proposal.isOffer) { + proposal.minCreditAmount = _proposal.creditAmount; proposal.maxCreditAmount = proposal.minCreditAmount * 10; proposalValues.intendedCreditAmount = proposal.minCreditAmount; } else { - proposal.maxCreditAmount = _params.base.creditAmount; + proposal.maxCreditAmount = _proposal.creditAmount; proposal.minCreditAmount = proposal.maxCreditAmount / 10; proposalValues.intendedCreditAmount = proposal.maxCreditAmount; } - 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) { - if (_params.signerPK != 0) { - if (_params.compactSignature) { - signature = _signProposalHashCompact(_params.signerPK, _proposalHash(proposal)); - } else { - signature = _signProposalHash(_params.signerPK, _proposalHash(proposal)); - } - } + proposal.collateralAddress = _proposal.collateralAddress; + proposal.collateralId = _proposal.collateralId; + proposal.checkCollateralStateFingerprint = _proposal.checkCollateralStateFingerprint; + proposal.collateralStateFingerprint = _proposal.collateralStateFingerprint; + proposal.availableCreditLimit = _proposal.availableCreditLimit; + proposal.auctionDuration = _proposal.expiration - proposal.auctionStart - 1 minutes; + proposal.allowedAcceptor = _proposal.allowedAcceptor; + proposal.proposer = _proposal.proposer; + proposal.isOffer = _proposal.isOffer; + proposal.refinancingLoanId = _proposal.refinancingLoanId; + proposal.nonceSpace = _proposal.nonceSpace; + proposal.nonce = _proposal.nonce; + proposal.loanContract = _proposal.loanContract; } function _callAcceptProposalWith(Params memory _params) internal override returns (bytes32, PWNSimpleLoan.Terms memory) { - _updateProposal(_params); + _updateProposal(_params.base); return proposalContract.acceptProposal({ acceptor: _params.acceptor, refinancingLoanId: _params.refinancingLoanId, proposalData: abi.encode(proposal, proposalValues), - signature: _proposalSignature(_params) + proposalInclusionProof: _params.proposalInclusionProof, + signature: _params.signature }); } function _getProposalHashWith(Params memory _params) internal override returns (bytes32) { - _updateProposal(_params); + _updateProposal(_params.base); return _proposalHash(proposal); } @@ -430,7 +421,8 @@ contract PWNSimpleLoanDutchAuctionProposal_AcceptProposal_Test is PWNSimpleLoanD acceptor: acceptor, refinancingLoanId: 0, proposalData: abi.encode(proposal, proposalValues), - signature: _signProposalHash(proposerPK, _proposalHash(proposal)) + proposalInclusionProof: new bytes32[](0), + signature: _sign(proposerPK, _proposalHash(proposal)) }); } @@ -464,7 +456,8 @@ contract PWNSimpleLoanDutchAuctionProposal_AcceptProposal_Test is PWNSimpleLoanD acceptor: acceptor, refinancingLoanId: 0, proposalData: abi.encode(proposal, proposalValues), - signature: _signProposalHash(proposerPK, _proposalHash(proposal)) + proposalInclusionProof: new bytes32[](0), + signature: _sign(proposerPK, _proposalHash(proposal)) }); } @@ -496,7 +489,8 @@ contract PWNSimpleLoanDutchAuctionProposal_AcceptProposal_Test is PWNSimpleLoanD acceptor: acceptor, refinancingLoanId: 0, proposalData: abi.encode(proposal, proposalValues), - signature: _signProposalHash(proposerPK, _proposalHash(proposal)) + proposalInclusionProof: new bytes32[](0), + signature: _sign(proposerPK, _proposalHash(proposal)) }); assertEq(proposalHash, _proposalHash(proposal)); diff --git a/test/unit/PWNSimpleLoanFungibleProposal.t.sol b/test/unit/PWNSimpleLoanFungibleProposal.t.sol index 330462f..5731fb8 100644 --- a/test/unit/PWNSimpleLoanFungibleProposal.t.sol +++ b/test/unit/PWNSimpleLoanFungibleProposal.t.sol @@ -79,47 +79,38 @@ abstract contract PWNSimpleLoanFungibleProposalTest is PWNSimpleLoanProposalTest )); } - function _updateProposal(Params memory _params) internal { - 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) { - if (_params.signerPK != 0) { - if (_params.compactSignature) { - signature = _signProposalHashCompact(_params.signerPK, _proposalHash(proposal)); - } else { - signature = _signProposalHash(_params.signerPK, _proposalHash(proposal)); - } - } + function _updateProposal(PWNSimpleLoanProposal.ProposalBase memory _proposal) internal { + proposal.collateralAddress = _proposal.collateralAddress; + proposal.collateralId = _proposal.collateralId; + proposal.checkCollateralStateFingerprint = _proposal.checkCollateralStateFingerprint; + proposal.collateralStateFingerprint = _proposal.collateralStateFingerprint; + proposal.availableCreditLimit = _proposal.availableCreditLimit; + proposal.expiration = _proposal.expiration; + proposal.allowedAcceptor = _proposal.allowedAcceptor; + proposal.proposer = _proposal.proposer; + proposal.isOffer = _proposal.isOffer; + proposal.refinancingLoanId = _proposal.refinancingLoanId; + proposal.nonceSpace = _proposal.nonceSpace; + proposal.nonce = _proposal.nonce; + proposal.loanContract = _proposal.loanContract; + + proposalValues.collateralAmount = _proposal.creditAmount; } function _callAcceptProposalWith(Params memory _params) internal override returns (bytes32, PWNSimpleLoan.Terms memory) { - _updateProposal(_params); + _updateProposal(_params.base); return proposalContract.acceptProposal({ acceptor: _params.acceptor, refinancingLoanId: _params.refinancingLoanId, proposalData: abi.encode(proposal, proposalValues), - signature: _proposalSignature(_params) + proposalInclusionProof: _params.proposalInclusionProof, + signature: _params.signature }); } function _getProposalHashWith(Params memory _params) internal override returns (bytes32) { - _updateProposal(_params); + _updateProposal(_params.base); return _proposalHash(proposal); } @@ -306,7 +297,8 @@ contract PWNSimpleLoanFungibleProposal_AcceptProposal_Test is PWNSimpleLoanFungi acceptor: acceptor, refinancingLoanId: 0, proposalData: abi.encode(proposal, proposalValues), - signature: _signProposalHash(proposerPK, _proposalHash(proposal)) + proposalInclusionProof: new bytes32[](0), + signature: _sign(proposerPK, _proposalHash(proposal)) }); } @@ -324,7 +316,8 @@ contract PWNSimpleLoanFungibleProposal_AcceptProposal_Test is PWNSimpleLoanFungi acceptor: acceptor, refinancingLoanId: 0, proposalData: abi.encode(proposal, proposalValues), - signature: _signProposalHash(proposerPK, _proposalHash(proposal)) + proposalInclusionProof: new bytes32[](0), + signature: _sign(proposerPK, _proposalHash(proposal)) }); } @@ -340,7 +333,8 @@ contract PWNSimpleLoanFungibleProposal_AcceptProposal_Test is PWNSimpleLoanFungi acceptor: acceptor, refinancingLoanId: 0, proposalData: abi.encode(proposal, proposalValues), - signature: _signProposalHash(proposerPK, _proposalHash(proposal)) + proposalInclusionProof: new bytes32[](0), + signature: _sign(proposerPK, _proposalHash(proposal)) }); assertEq(proposalHash, _proposalHash(proposal)); diff --git a/test/unit/PWNSimpleLoanListProposal.t.sol b/test/unit/PWNSimpleLoanListProposal.t.sol index 5f6e03e..052a199 100644 --- a/test/unit/PWNSimpleLoanListProposal.t.sol +++ b/test/unit/PWNSimpleLoanListProposal.t.sol @@ -80,47 +80,38 @@ abstract contract PWNSimpleLoanListProposalTest is PWNSimpleLoanProposalTest { )); } - function _updateProposal(Params memory _params) internal { - 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) { - if (_params.signerPK != 0) { - if (_params.compactSignature) { - signature = _signProposalHashCompact(_params.signerPK, _proposalHash(proposal)); - } else { - signature = _signProposalHash(_params.signerPK, _proposalHash(proposal)); - } - } + function _updateProposal(PWNSimpleLoanProposal.ProposalBase memory _proposal) internal { + proposal.collateralAddress = _proposal.collateralAddress; + proposal.checkCollateralStateFingerprint = _proposal.checkCollateralStateFingerprint; + proposal.collateralStateFingerprint = _proposal.collateralStateFingerprint; + proposal.creditAmount = _proposal.creditAmount; + proposal.availableCreditLimit = _proposal.availableCreditLimit; + proposal.expiration = _proposal.expiration; + proposal.allowedAcceptor = _proposal.allowedAcceptor; + proposal.proposer = _proposal.proposer; + proposal.isOffer = _proposal.isOffer; + proposal.refinancingLoanId = _proposal.refinancingLoanId; + proposal.nonceSpace = _proposal.nonceSpace; + proposal.nonce = _proposal.nonce; + proposal.loanContract = _proposal.loanContract; + + proposalValues.collateralId = _proposal.collateralId; } function _callAcceptProposalWith(Params memory _params) internal override returns (bytes32, PWNSimpleLoan.Terms memory) { - _updateProposal(_params); + _updateProposal(_params.base); return proposalContract.acceptProposal({ acceptor: _params.acceptor, refinancingLoanId: _params.refinancingLoanId, proposalData: abi.encode(proposal, proposalValues), - signature: _proposalSignature(_params) + proposalInclusionProof: _params.proposalInclusionProof, + signature: _params.signature }); } function _getProposalHashWith(Params memory _params) internal override returns (bytes32) { - _updateProposal(_params); + _updateProposal(_params.base); return _proposalHash(proposal); } @@ -290,7 +281,8 @@ contract PWNSimpleLoanListProposal_AcceptProposal_Test is PWNSimpleLoanListPropo acceptor: acceptor, refinancingLoanId: 0, proposalData: abi.encode(proposal, proposalValues), - signature: _signProposalHash(proposerPK, _proposalHash(proposal)) + proposalInclusionProof: new bytes32[](0), + signature: _sign(proposerPK, _proposalHash(proposal)) }); } @@ -312,7 +304,8 @@ contract PWNSimpleLoanListProposal_AcceptProposal_Test is PWNSimpleLoanListPropo acceptor: acceptor, refinancingLoanId: 0, proposalData: abi.encode(proposal, proposalValues), - signature: _signProposalHash(proposerPK, _proposalHash(proposal)) + proposalInclusionProof: new bytes32[](0), + signature: _sign(proposerPK, _proposalHash(proposal)) }); } @@ -339,7 +332,8 @@ contract PWNSimpleLoanListProposal_AcceptProposal_Test is PWNSimpleLoanListPropo acceptor: acceptor, refinancingLoanId: 0, proposalData: abi.encode(proposal, proposalValues), - signature: _signProposalHash(proposerPK, _proposalHash(proposal)) + proposalInclusionProof: new bytes32[](0), + signature: _sign(proposerPK, _proposalHash(proposal)) }); } @@ -351,7 +345,8 @@ contract PWNSimpleLoanListProposal_AcceptProposal_Test is PWNSimpleLoanListPropo acceptor: acceptor, refinancingLoanId: 0, proposalData: abi.encode(proposal, proposalValues), - signature: _signProposalHash(proposerPK, _proposalHash(proposal)) + proposalInclusionProof: new bytes32[](0), + signature: _sign(proposerPK, _proposalHash(proposal)) }); assertEq(proposalHash, _proposalHash(proposal)); diff --git a/test/unit/PWNSimpleLoanProposal.t.sol b/test/unit/PWNSimpleLoanProposal.t.sol index 19af484..7436af6 100644 --- a/test/unit/PWNSimpleLoanProposal.t.sol +++ b/test/unit/PWNSimpleLoanProposal.t.sol @@ -5,6 +5,7 @@ import "forge-std/Test.sol"; import { MultiToken } from "MultiToken/MultiToken.sol"; +import { MerkleProof } from "openzeppelin-contracts/contracts/utils/cryptography/MerkleProof.sol"; import { Math } from "openzeppelin-contracts/contracts/utils/math/Math.sol"; import { PWNHubTags } from "@pwn/hub/PWNHubTags.sol"; @@ -39,8 +40,8 @@ abstract contract PWNSimpleLoanProposalTest is Test { PWNSimpleLoanProposal.ProposalBase base; address acceptor; uint256 refinancingLoanId; - uint256 signerPK; - bool compactSignature; + bytes32[] proposalInclusionProof; + bytes signature; } function setUp() virtual public { @@ -56,8 +57,6 @@ abstract contract PWNSimpleLoanProposalTest is Test { params.base.loanContract = activeLoanContract; params.acceptor = acceptor; params.refinancingLoanId = 0; - params.signerPK = proposerPK; - params.compactSignature = false; vm.mockCall( revokedNonce, @@ -84,12 +83,12 @@ abstract contract PWNSimpleLoanProposalTest is Test { ); } - function _signProposalHash(uint256 pk, bytes32 proposalHash) internal pure returns (bytes memory) { + function _sign(uint256 pk, bytes32 proposalHash) internal pure returns (bytes memory) { (uint8 v, bytes32 r, bytes32 s) = vm.sign(pk, proposalHash); return abi.encodePacked(r, s, v); } - function _signProposalHashCompact(uint256 pk, bytes32 proposalHash) internal pure returns (bytes memory) { + function _signCompact(uint256 pk, bytes32 proposalHash) internal pure returns (bytes memory) { (uint8 v, bytes32 r, bytes32 s) = vm.sign(pk, proposalHash); return abi.encodePacked(r, bytes32(uint256(v) - 27) << 255 | s); } @@ -118,6 +117,7 @@ abstract contract PWNSimpleLoanProposal_AcceptProposal_Test is PWNSimpleLoanProp function testFuzz_shouldFail_whenCallerIsNotProposedLoanContract(address caller) external { vm.assume(caller != activeLoanContract); params.base.loanContract = activeLoanContract; + params.signature = _sign(proposerPK, _getProposalHashWith()); vm.expectRevert(abi.encodeWithSelector(CallerNotLoanContract.selector, caller, activeLoanContract)); vm.prank(caller); @@ -127,25 +127,93 @@ abstract contract PWNSimpleLoanProposal_AcceptProposal_Test is PWNSimpleLoanProp function testFuzz_shouldFail_whenCallerNotTagged_ACTIVE_LOAN(address caller) external { vm.assume(caller != activeLoanContract); params.base.loanContract = caller; + params.signature = _sign(proposerPK, _getProposalHashWith()); vm.expectRevert(abi.encodeWithSelector(AddressMissingHubTag.selector, caller, PWNHubTags.ACTIVE_LOAN)); vm.prank(caller); _callAcceptProposalWith(); } - function test_shouldFail_whenInvalidSignature_whenEOA() external { - params.signerPK = 1; + function testFuzz_shouldFail_whenInvalidSignature_whenEOA(uint256 randomPK) external { + randomPK = boundPrivateKey(randomPK); + vm.assume(randomPK != proposerPK); + params.signature = _sign(randomPK, _getProposalHashWith()); - vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, proposer, _getProposalHashWith(params))); + vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, proposer, _getProposalHashWith())); vm.prank(activeLoanContract); _callAcceptProposalWith(); } function test_shouldFail_whenInvalidSignature_whenContractAccount() external { vm.etch(proposer, bytes("data")); - params.signerPK = 0; + params.signature = ""; - vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, proposer, _getProposalHashWith(params))); + vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, proposer, _getProposalHashWith())); + vm.prank(activeLoanContract); + _callAcceptProposalWith(); + } + + function testFuzz_shouldFail_withInvalidSignature_whenEOA_whenMultiproposal(uint256 randomPK) external { + randomPK = boundPrivateKey(randomPK); + vm.assume(randomPK != proposerPK); + + bytes32 proposalHash = _getProposalHashWith(); + bytes32[] memory proposalInclusionProof = new bytes32[](1); + proposalInclusionProof[0] = keccak256("leaf1"); + bytes32 root = keccak256( + uint256(proposalHash) < uint256(proposalInclusionProof[0]) + ? abi.encode(proposalHash, proposalInclusionProof[0]) + : abi.encode(proposalInclusionProof[0], proposalHash) + ); + bytes32 multiproposalHash = proposalContractAddr.getMultiproposalHash(PWNSimpleLoanProposal.Multiproposal(root)); + params.signature = _sign(randomPK, multiproposalHash); + params.proposalInclusionProof = proposalInclusionProof; + + vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, proposer, multiproposalHash)); + vm.prank(activeLoanContract); + _callAcceptProposalWith(); + } + + function test_shouldFail_whenInvalidSignature_whenContractAccount_whenMultiproposal() external { + vm.etch(proposer, bytes("data")); + bytes32 proposalHash = _getProposalHashWith(); + bytes32[] memory proposalInclusionProof = new bytes32[](1); + proposalInclusionProof[0] = keccak256("leaf1"); + bytes32 root = keccak256( + uint256(proposalHash) < uint256(proposalInclusionProof[0]) + ? abi.encode(proposalHash, proposalInclusionProof[0]) + : abi.encode(proposalInclusionProof[0], proposalHash) + ); + bytes32 multiproposalHash = proposalContractAddr.getMultiproposalHash(PWNSimpleLoanProposal.Multiproposal(root)); + params.signature = ""; + params.proposalInclusionProof = proposalInclusionProof; + + vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, proposer, multiproposalHash)); + vm.prank(activeLoanContract); + _callAcceptProposalWith(); + } + + function test_shouldFail_withInvalidInclusionProof() external { + bytes32 proposalHash = _getProposalHashWith(); + bytes32[] memory proposalInclusionProof = new bytes32[](1); + proposalInclusionProof[0] = keccak256("other leaf1"); + bytes32 leaf = keccak256("leaf1"); + bytes32 root = keccak256( + uint256(proposalHash) < uint256(leaf) + ? abi.encode(proposalHash, leaf) + : abi.encode(leaf, proposalHash) + ); + bytes32 multiproposalHash = proposalContractAddr.getMultiproposalHash(PWNSimpleLoanProposal.Multiproposal(root)); + params.signature = _sign(proposerPK, multiproposalHash); + params.proposalInclusionProof = proposalInclusionProof; + + bytes32 actualRoot = keccak256( + uint256(proposalHash) < uint256(proposalInclusionProof[0]) + ? abi.encode(proposalHash, proposalInclusionProof[0]) + : abi.encode(proposalInclusionProof[0], proposalHash) + ); + bytes32 actualMultiproposalHash = proposalContractAddr.getMultiproposalHash(PWNSimpleLoanProposal.Multiproposal(actualRoot)); + vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, proposer, actualMultiproposalHash)); vm.prank(activeLoanContract); _callAcceptProposalWith(); } @@ -153,24 +221,24 @@ abstract contract PWNSimpleLoanProposal_AcceptProposal_Test is PWNSimpleLoanProp function test_shouldPass_whenProposalMadeOnchain() external { vm.store( address(proposalContractAddr), - keccak256(abi.encode(_getProposalHashWith(params), PROPOSALS_MADE_SLOT)), + keccak256(abi.encode(_getProposalHashWith(), PROPOSALS_MADE_SLOT)), bytes32(uint256(1)) ); - params.signerPK = 0; + params.signature = ""; vm.prank(activeLoanContract); _callAcceptProposalWith(); } function test_shouldPass_withValidSignature_whenEOA_whenStandardSignature() external { - params.compactSignature = false; + params.signature = _sign(proposerPK, _getProposalHashWith()); vm.prank(activeLoanContract); _callAcceptProposalWith(); } function test_shouldPass_withValidSignature_whenEOA_whenCompactEIP2098Signature() external { - params.compactSignature = true; + params.signature = _signCompact(proposerPK, _getProposalHashWith()); vm.prank(activeLoanContract); _callAcceptProposalWith(); @@ -178,11 +246,71 @@ abstract contract PWNSimpleLoanProposal_AcceptProposal_Test is PWNSimpleLoanProp function test_shouldPass_whenValidSignature_whenContractAccount() external { vm.etch(proposer, bytes("data")); - params.signerPK = 0; + params.signature = bytes("some signature"); + + bytes32 proposalHash = _getProposalHashWith(); vm.mockCall( proposer, - abi.encodeWithSignature("isValidSignature(bytes32,bytes)"), + abi.encodeWithSignature("isValidSignature(bytes32,bytes)", proposalHash, params.signature), + abi.encode(bytes4(0x1626ba7e)) + ); + + vm.prank(activeLoanContract); + _callAcceptProposalWith(); + } + + function test_shouldPass_withValidSignature_whenEOA_whenStandardSignature_whenMultiproposal() external { + bytes32 proposalHash = _getProposalHashWith(); + bytes32[] memory proposalInclusionProof = new bytes32[](1); + proposalInclusionProof[0] = keccak256("leaf1"); + bytes32 root = keccak256( + uint256(proposalHash) < uint256(proposalInclusionProof[0]) + ? abi.encode(proposalHash, proposalInclusionProof[0]) + : abi.encode(proposalInclusionProof[0], proposalHash) + ); + bytes32 multiproposalHash = proposalContractAddr.getMultiproposalHash(PWNSimpleLoanProposal.Multiproposal(root)); + params.signature = _sign(proposerPK, multiproposalHash); + params.proposalInclusionProof = proposalInclusionProof; + + vm.prank(activeLoanContract); + _callAcceptProposalWith(); + } + + function test_shouldPass_withValidSignature_whenEOA_whenCompactEIP2098Signature_whenMultiproposal() external { + bytes32 proposalHash = _getProposalHashWith(); + bytes32[] memory proposalInclusionProof = new bytes32[](1); + proposalInclusionProof[0] = keccak256("leaf1"); + bytes32 root = keccak256( + uint256(proposalHash) < uint256(proposalInclusionProof[0]) + ? abi.encode(proposalHash, proposalInclusionProof[0]) + : abi.encode(proposalInclusionProof[0], proposalHash) + ); + bytes32 multiproposalHash = proposalContractAddr.getMultiproposalHash(PWNSimpleLoanProposal.Multiproposal(root)); + params.signature = _signCompact(proposerPK, multiproposalHash); + params.proposalInclusionProof = proposalInclusionProof; + + vm.prank(activeLoanContract); + _callAcceptProposalWith(); + } + + function test_shouldPass_whenValidSignature_whenContractAccount_whenMultiproposal() external { + vm.etch(proposer, bytes("data")); + bytes32 proposalHash = _getProposalHashWith(); + bytes32[] memory proposalInclusionProof = new bytes32[](1); + proposalInclusionProof[0] = keccak256("leaf1"); + bytes32 root = keccak256( + uint256(proposalHash) < uint256(proposalInclusionProof[0]) + ? abi.encode(proposalHash, proposalInclusionProof[0]) + : abi.encode(proposalInclusionProof[0], proposalHash) + ); + bytes32 multiproposalHash = proposalContractAddr.getMultiproposalHash(PWNSimpleLoanProposal.Multiproposal(root)); + params.signature = bytes("some random string"); + params.proposalInclusionProof = proposalInclusionProof; + + vm.mockCall( + proposer, + abi.encodeWithSignature("isValidSignature(bytes32,bytes)", multiproposalHash, params.signature), abi.encode(bytes4(0x1626ba7e)) ); @@ -194,6 +322,7 @@ abstract contract PWNSimpleLoanProposal_AcceptProposal_Test is PWNSimpleLoanProp vm.assume(proposedRefinancingLoanId != 0); params.base.refinancingLoanId = proposedRefinancingLoanId; params.refinancingLoanId = 0; + params.signature = _sign(proposerPK, _getProposalHashWith()); vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, proposedRefinancingLoanId)); vm.prank(activeLoanContract); @@ -208,6 +337,7 @@ abstract contract PWNSimpleLoanProposal_AcceptProposal_Test is PWNSimpleLoanProp params.base.refinancingLoanId = proposedRefinancingLoanId; params.base.isOffer = true; params.refinancingLoanId = refinancingLoanId; + params.signature = _sign(proposerPK, _getProposalHashWith()); vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, proposedRefinancingLoanId)); vm.prank(activeLoanContract); @@ -221,6 +351,7 @@ abstract contract PWNSimpleLoanProposal_AcceptProposal_Test is PWNSimpleLoanProp params.base.refinancingLoanId = 0; params.base.isOffer = true; params.refinancingLoanId = refinancingLoanId; + params.signature = _sign(proposerPK, _getProposalHashWith()); vm.prank(activeLoanContract); _callAcceptProposalWith(); @@ -233,6 +364,7 @@ abstract contract PWNSimpleLoanProposal_AcceptProposal_Test is PWNSimpleLoanProp params.base.refinancingLoanId = proposedRefinancingLoanId; params.base.isOffer = false; params.refinancingLoanId = refinancingLoanId; + params.signature = _sign(proposerPK, _getProposalHashWith()); vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, proposedRefinancingLoanId)); vm.prank(activeLoanContract); @@ -243,6 +375,8 @@ abstract contract PWNSimpleLoanProposal_AcceptProposal_Test is PWNSimpleLoanProp timestamp = bound(timestamp, params.base.expiration, type(uint256).max); vm.warp(timestamp); + params.signature = _sign(proposerPK, _getProposalHashWith()); + vm.expectRevert(abi.encodeWithSelector(Expired.selector, timestamp, params.base.expiration)); vm.prank(activeLoanContract); _callAcceptProposalWith(); @@ -251,6 +385,7 @@ abstract contract PWNSimpleLoanProposal_AcceptProposal_Test is PWNSimpleLoanProp function testFuzz_shouldFail_whenOfferNonceNotUsable(uint256 nonceSpace, uint256 nonce) external { params.base.nonceSpace = nonceSpace; params.base.nonce = nonce; + params.signature = _sign(proposerPK, _getProposalHashWith()); vm.mockCall( revokedNonce, @@ -272,6 +407,7 @@ abstract contract PWNSimpleLoanProposal_AcceptProposal_Test is PWNSimpleLoanProp vm.assume(caller != allowedAcceptor); params.base.allowedAcceptor = allowedAcceptor; params.acceptor = caller; + params.signature = _sign(proposerPK, _getProposalHashWith()); vm.expectRevert(abi.encodeWithSelector(CallerNotAllowedAcceptor.selector, caller, allowedAcceptor)); vm.prank(activeLoanContract); @@ -282,6 +418,7 @@ abstract contract PWNSimpleLoanProposal_AcceptProposal_Test is PWNSimpleLoanProp params.base.availableCreditLimit = 0; params.base.nonceSpace = nonceSpace; params.base.nonce = nonce; + params.signature = _sign(proposerPK, _getProposalHashWith()); vm.expectCall( revokedNonce, @@ -297,10 +434,11 @@ abstract contract PWNSimpleLoanProposal_AcceptProposal_Test is PWNSimpleLoanProp limit = bound(limit, used, used + params.base.creditAmount - 1); params.base.availableCreditLimit = limit; + params.signature = _sign(proposerPK, _getProposalHashWith()); vm.store( address(proposalContractAddr), - keccak256(abi.encode(_getProposalHashWith(params), CREDIT_USED_SLOT)), + keccak256(abi.encode(_getProposalHashWith(), CREDIT_USED_SLOT)), bytes32(used) ); @@ -316,8 +454,9 @@ abstract contract PWNSimpleLoanProposal_AcceptProposal_Test is PWNSimpleLoanProp limit = bound(limit, used + params.base.creditAmount, type(uint256).max); params.base.availableCreditLimit = limit; + params.signature = _sign(proposerPK, _getProposalHashWith()); - bytes32 proposalHash = _getProposalHashWith(params); + bytes32 proposalHash = _getProposalHashWith(); vm.store( address(proposalContractAddr), @@ -333,6 +472,7 @@ abstract contract PWNSimpleLoanProposal_AcceptProposal_Test is PWNSimpleLoanProp function test_shouldNotCallComputerRegistry_whenShouldNotCheckStateFingerprint() external { params.base.checkCollateralStateFingerprint = false; + params.signature = _sign(proposerPK, _getProposalHashWith()); vm.expectCall({ callee: config, @@ -346,6 +486,7 @@ abstract contract PWNSimpleLoanProposal_AcceptProposal_Test is PWNSimpleLoanProp function test_shouldFail_whenComputerRegistryReturnsZeroAddress_whenShouldCheckStateFingerprint() external { params.base.collateralAddress = token; + params.signature = _sign(proposerPK, _getProposalHashWith()); vm.mockCall( config, @@ -362,6 +503,7 @@ abstract contract PWNSimpleLoanProposal_AcceptProposal_Test is PWNSimpleLoanProp bytes32 stateFingerprint ) external { vm.assume(stateFingerprint != params.base.collateralStateFingerprint); + params.signature = _sign(proposerPK, _getProposalHashWith()); vm.mockCall( stateFingerprintComputer, diff --git a/test/unit/PWNSimpleLoanSimpleProposal.t.sol b/test/unit/PWNSimpleLoanSimpleProposal.t.sol index 4926043..dd44c8e 100644 --- a/test/unit/PWNSimpleLoanSimpleProposal.t.sol +++ b/test/unit/PWNSimpleLoanSimpleProposal.t.sol @@ -72,46 +72,37 @@ abstract contract PWNSimpleLoanSimpleProposalTest is PWNSimpleLoanProposalTest { )); } - function _updateProposal(Params memory _params) internal { - 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) { - if (_params.signerPK != 0) { - if (_params.compactSignature) { - signature = _signProposalHashCompact(_params.signerPK, _proposalHash(proposal)); - } else { - signature = _signProposalHash(_params.signerPK, _proposalHash(proposal)); - } - } + function _updateProposal(PWNSimpleLoanProposal.ProposalBase memory _proposal) internal { + proposal.collateralAddress = _proposal.collateralAddress; + proposal.collateralId = _proposal.collateralId; + proposal.checkCollateralStateFingerprint = _proposal.checkCollateralStateFingerprint; + proposal.collateralStateFingerprint = _proposal.collateralStateFingerprint; + proposal.creditAmount = _proposal.creditAmount; + proposal.availableCreditLimit = _proposal.availableCreditLimit; + proposal.expiration = _proposal.expiration; + proposal.allowedAcceptor = _proposal.allowedAcceptor; + proposal.proposer = _proposal.proposer; + proposal.isOffer = _proposal.isOffer; + proposal.refinancingLoanId = _proposal.refinancingLoanId; + proposal.nonceSpace = _proposal.nonceSpace; + proposal.nonce = _proposal.nonce; + proposal.loanContract = _proposal.loanContract; } function _callAcceptProposalWith(Params memory _params) internal override returns (bytes32, PWNSimpleLoan.Terms memory) { - _updateProposal(_params); + _updateProposal(_params.base); return proposalContract.acceptProposal({ acceptor: _params.acceptor, refinancingLoanId: _params.refinancingLoanId, proposalData: abi.encode(proposal), - signature: _proposalSignature(_params) + proposalInclusionProof: _params.proposalInclusionProof, + signature: _params.signature }); } function _getProposalHashWith(Params memory _params) internal override returns (bytes32) { - _updateProposal(_params); + _updateProposal(_params.base); return _proposalHash(proposal); } @@ -268,7 +259,8 @@ contract PWNSimpleLoanSimpleProposal_AcceptProposal_Test is PWNSimpleLoanSimpleP acceptor: acceptor, refinancingLoanId: 0, proposalData: abi.encode(proposal), - signature: _signProposalHash(proposerPK, _proposalHash(proposal)) + proposalInclusionProof: new bytes32[](0), + signature: _sign(proposerPK, _proposalHash(proposal)) }); assertEq(proposalHash, _proposalHash(proposal)); From 31c12cad4b512259574bf64aafa2648f8f545fcb Mon Sep 17 00:00:00 2001 From: ashhanai Date: Mon, 8 Apr 2024 16:51:35 -0400 Subject: [PATCH 070/129] feat(state-fingerprint): allow computer to support more than one stateful token --- src/config/PWNConfig.sol | 21 +-- .../simple/proposal/PWNSimpleLoanProposal.sol | 18 ++- .../IStateFingerpringComputer.sol | 25 +++ test/unit/PWNConfig.t.sol | 42 +---- test/unit/PWNSimpleLoanProposal.t.sol | 143 +++++++++++++++++- 5 files changed, 190 insertions(+), 59 deletions(-) create mode 100644 src/state-fingerprint-computer/IStateFingerpringComputer.sol diff --git a/src/config/PWNConfig.sol b/src/config/PWNConfig.sol index 1730b9f..d0896d9 100644 --- a/src/config/PWNConfig.sol +++ b/src/config/PWNConfig.sol @@ -3,9 +3,8 @@ pragma solidity 0.8.16; import { Ownable2Step } from "openzeppelin-contracts/contracts/access/Ownable2Step.sol"; import { Initializable } from "openzeppelin-contracts/contracts/proxy/utils/Initializable.sol"; -import { ERC165Checker } from "openzeppelin-contracts/contracts/utils/introspection/ERC165Checker.sol"; -import { IERC5646 } from "@pwn/loan/token/IERC5646.sol"; +import { IStateFingerpringComputer } from "@pwn/state-fingerprint-computer/IStateFingerpringComputer.sol"; import "@pwn/PWNErrors.sol"; @@ -73,7 +72,7 @@ contract PWNConfig is Ownable2Step, Initializable { event DefaultLOANMetadataUriUpdated(string newUri); /** - * @notice Error emitted when registering a computer which does not implement the IERC5646 interface. + * @notice Error emitted when registering a computer which does not support the asset it is registered for. */ error InvalidComputerContract(); @@ -184,28 +183,22 @@ contract PWNConfig is Ownable2Step, Initializable { |*----------------------------------------------------------*/ /** - * @notice Returns the ERC5646 computer for a given asset. + * @notice Returns the state fingerprint computer for a given asset. * @param asset The asset for which the computer is requested. * @return The computer for the given asset. */ - function getStateFingerprintComputer(address asset) external view returns (IERC5646) { - address computer = _computerRegistry[asset]; - if (computer == address(0)) - if (ERC165Checker.supportsInterface(asset, type(IERC5646).interfaceId)) - computer = asset; - - return IERC5646(computer); + function getStateFingerprintComputer(address asset) external view returns (IStateFingerpringComputer) { + return IStateFingerpringComputer(_computerRegistry[asset]); } /** * @notice Registers a state fingerprint computer for a given asset. - * @dev Only owner can register a computer. Computer can be set to address(0) to remove the computer. * @param asset The asset for which the computer is registered. - * @param computer The computer to be registered. + * @param computer The computer to be registered. Use address(0) to remove a computer. */ function registerStateFingerprintComputer(address asset, address computer) external onlyOwner { if (computer != address(0)) - if (!ERC165Checker.supportsInterface(computer, type(IERC5646).interfaceId)) + if (!IStateFingerpringComputer(computer).supportsToken(asset)) revert InvalidComputerContract(); _computerRegistry[asset] = computer; diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol index 99bbbb6..c4cdfa3 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol @@ -2,12 +2,14 @@ pragma solidity 0.8.16; import { MerkleProof } from "openzeppelin-contracts/contracts/utils/cryptography/MerkleProof.sol"; +import { ERC165Checker } from "openzeppelin-contracts/contracts/utils/introspection/ERC165Checker.sol"; -import { PWNConfig, IERC5646 } from "@pwn/config/PWNConfig.sol"; +import { PWNConfig, IStateFingerpringComputer } 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 { PWNSimpleLoan } from "@pwn/loan/terms/simple/loan/PWNSimpleLoan.sol"; +import { IERC5646 } from "@pwn/loan/token/IERC5646.sol"; import { PWNRevokedNonce } from "@pwn/nonce/PWNRevokedNonce.sol"; import "@pwn/PWNErrors.sol"; @@ -257,13 +259,21 @@ abstract contract PWNSimpleLoanProposal { // Check collateral state fingerprint if needed if (proposal.checkCollateralStateFingerprint) { - IERC5646 computer = config.getStateFingerprintComputer(proposal.collateralAddress); - if (address(computer) == address(0)) { + bytes32 currentFingerprint; + IStateFingerpringComputer computer = config.getStateFingerprintComputer(proposal.collateralAddress); + if (address(computer) != address(0)) { + // Asset has registered computer + currentFingerprint = computer.computeStateFingerprint({ + token: proposal.collateralAddress, tokenId: proposal.collateralId + }); + } else if (ERC165Checker.supportsInterface(proposal.collateralAddress, type(IERC5646).interfaceId)) { + // Asset implements ERC5646 + currentFingerprint = IERC5646(proposal.collateralAddress).getStateFingerprint(proposal.collateralId); + } else { // Asset is not implementing ERC5646 and no computer is registered revert MissingStateFingerprintComputer(); } - bytes32 currentFingerprint = computer.getStateFingerprint(proposal.collateralId); if (proposal.collateralStateFingerprint != currentFingerprint) { // Fingerprint mismatch revert InvalidCollateralStateFingerprint({ diff --git a/src/state-fingerprint-computer/IStateFingerpringComputer.sol b/src/state-fingerprint-computer/IStateFingerpringComputer.sol new file mode 100644 index 0000000..3fb1a89 --- /dev/null +++ b/src/state-fingerprint-computer/IStateFingerpringComputer.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.16; + +/** + * @notice Interface of the state fingerprint computer. + * @dev Contract can compute state fingerprint of several tokens as long as they share the same state structure. + */ +interface IStateFingerpringComputer { + + /** + * @notice Compute current token state fingerprint for a given token. + * @param token Address of a token contract. + * @param tokenId Token id to compute state fingerprint for. + * @return Current token state fingerprint. + */ + function computeStateFingerprint(address token, uint256 tokenId) external view returns (bytes32); + + /** + * @notice Check if the computer supports a given token address. + * @param token Address of a token contract. + * @return True if the computer supports the token address, false otherwise. + */ + function supportsToken(address token) external view returns (bool); + +} diff --git a/test/unit/PWNConfig.t.sol b/test/unit/PWNConfig.t.sol index b935f28..68841cf 100644 --- a/test/unit/PWNConfig.t.sol +++ b/test/unit/PWNConfig.t.sol @@ -3,9 +3,7 @@ pragma solidity 0.8.16; import "forge-std/Test.sol"; -import { IERC165 } from "openzeppelin-contracts/contracts/utils/introspection/IERC165.sol"; - -import { PWNConfig, IERC5646 } from "@pwn/config/PWNConfig.sol"; +import { PWNConfig } from "@pwn/config/PWNConfig.sol"; import "@pwn/PWNErrors.sol"; @@ -38,16 +36,10 @@ abstract contract PWNConfigTest is Test { vm.store(address(config), FEE_COLLECTOR_SLOT, bytes32(uint256(uint160(feeCollector)))); } - function _mockERC5646Support(address asset, bool result) internal { - _mockERC165Call(asset, type(IERC165).interfaceId, true); - _mockERC165Call(asset, hex"ffffffff", false); - _mockERC165Call(asset, type(IERC5646).interfaceId, result); - } - - function _mockERC165Call(address asset, bytes4 interfaceId, bool result) internal { + function _mockSupportsToken(address computer, address token, bool result) internal { vm.mockCall( - asset, - abi.encodeWithSignature("supportsInterface(bytes4)", interfaceId), + computer, + abi.encodeWithSignature("supportsToken(address)", token), abi.encode(result) ); } @@ -381,18 +373,6 @@ contract PWNConfig_GetStateFingerprintComputer_Test is PWNConfigTest { assertEq(address(config.getStateFingerprintComputer(asset)), computer); } - function testFuzz_shouldReturnAsset_whenComputerIsNotRegistered_whenAssetImplementsERC5646(address asset) external { - assumeAddressIsNot(asset, AddressType.ForgeAddress, AddressType.Precompile); - - _mockERC5646Support(asset, true); - - assertEq(address(config.getStateFingerprintComputer(asset)), asset); - } - - function testFuzz_shouldReturnZeroAddress_whenComputerIsNotRegistered_whenAssetNotImplementsERC5646(address asset) external { - assertEq(address(config.getStateFingerprintComputer(asset)), address(0)); - } - } @@ -428,17 +408,9 @@ contract PWNConfig_RegisterStateFingerprintComputer_Test is PWNConfigTest { assertEq(address(config.getStateFingerprintComputer(asset)), address(0)); } - function testFuzz_shouldFail_whenComputerDoesNotImplementERC165(address asset, address computer) external { - assumeAddressIsNot(computer, AddressType.ForgeAddress, AddressType.Precompile, AddressType.ZeroAddress); - - vm.expectRevert(abi.encodeWithSelector(PWNConfig.InvalidComputerContract.selector)); - vm.prank(owner); - config.registerStateFingerprintComputer(asset, computer); - } - - function testFuzz_shouldFail_whenComputerDoesNotImplementERC5646(address asset, address computer) external { + function testFuzz_shouldFail_whenComputerDoesNotSupportToken(address asset, address computer) external { assumeAddressIsNot(computer, AddressType.ForgeAddress, AddressType.Precompile, AddressType.ZeroAddress); - _mockERC5646Support(computer, false); + _mockSupportsToken(computer, asset, false); vm.expectRevert(abi.encodeWithSelector(PWNConfig.InvalidComputerContract.selector)); vm.prank(owner); @@ -447,7 +419,7 @@ contract PWNConfig_RegisterStateFingerprintComputer_Test is PWNConfigTest { function testFuzz_shouldRegisterComputer(address asset, address computer) external { assumeAddressIsNot(computer, AddressType.ForgeAddress, AddressType.Precompile, AddressType.ZeroAddress); - _mockERC5646Support(computer, true); + _mockSupportsToken(computer, asset, true); vm.prank(owner); config.registerStateFingerprintComputer(asset, computer); diff --git a/test/unit/PWNSimpleLoanProposal.t.sol b/test/unit/PWNSimpleLoanProposal.t.sol index 7436af6..c044ae3 100644 --- a/test/unit/PWNSimpleLoanProposal.t.sol +++ b/test/unit/PWNSimpleLoanProposal.t.sol @@ -6,11 +6,15 @@ import "forge-std/Test.sol"; import { MultiToken } from "MultiToken/MultiToken.sol"; import { MerkleProof } from "openzeppelin-contracts/contracts/utils/cryptography/MerkleProof.sol"; +import { IERC165 } from "openzeppelin-contracts/contracts/utils/introspection/IERC165.sol"; import { Math } from "openzeppelin-contracts/contracts/utils/math/Math.sol"; import { PWNHubTags } from "@pwn/hub/PWNHubTags.sol"; import { PWNSimpleLoan } from "@pwn/loan/terms/simple/loan/PWNSimpleLoan.sol"; -import { PWNSimpleLoanProposal } from "@pwn/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol"; +import { + PWNSimpleLoanProposal, + IERC5646 +} from "@pwn/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol"; import "@pwn/PWNErrors.sol"; @@ -78,11 +82,25 @@ abstract contract PWNSimpleLoanProposalTest is Test { ); vm.mockCall( stateFingerprintComputer, - abi.encodeWithSignature("getStateFingerprint(uint256)"), + abi.encodeWithSignature("computeStateFingerprint(address,uint256)"), abi.encode(params.base.collateralStateFingerprint) ); } + function _mockERC5646Support(address asset, bool result) internal { + _mockERC165Call(asset, type(IERC165).interfaceId, true); + _mockERC165Call(asset, hex"ffffffff", false); + _mockERC165Call(asset, type(IERC5646).interfaceId, result); + } + + function _mockERC165Call(address asset, bytes4 interfaceId, bool result) internal { + vm.mockCall( + asset, + abi.encodeWithSignature("supportsInterface(bytes4)", interfaceId), + abi.encode(result) + ); + } + function _sign(uint256 pk, bytes32 proposalHash) internal pure returns (bytes memory) { (uint8 v, bytes32 r, bytes32 s) = vm.sign(pk, proposalHash); return abi.encodePacked(r, s, v); @@ -484,29 +502,105 @@ abstract contract PWNSimpleLoanProposal_AcceptProposal_Test is PWNSimpleLoanProp _callAcceptProposalWith(); } - function test_shouldFail_whenComputerRegistryReturnsZeroAddress_whenShouldCheckStateFingerprint() external { + function test_shouldCallComputerRegistry_whenShouldCheckStateFingerprint() external { + params.base.checkCollateralStateFingerprint = true; + params.signature = _sign(proposerPK, _getProposalHashWith()); + + vm.expectCall( + config, + abi.encodeWithSignature("getStateFingerprintComputer(address)", params.base.collateralAddress) + ); + + vm.prank(activeLoanContract); + _callAcceptProposalWith(); + } + + function test_shouldFail_whenComputerRegistryReturnsComputer_whenComputerFails() external { params.base.collateralAddress = token; params.signature = _sign(proposerPK, _getProposalHashWith()); + vm.mockCallRevert( + stateFingerprintComputer, + abi.encodeWithSignature("computeStateFingerprint(address,uint256)"), + "some error" + ); + + vm.expectRevert("some error"); + vm.prank(activeLoanContract); + _callAcceptProposalWith(); + } + + function testFuzz_shouldFail_whenComputerRegistryReturnsComputer_whenComputerReturnsDifferentStateFingerprint( + bytes32 stateFingerprint + ) external { + params.base.collateralAddress = token; + params.signature = _sign(proposerPK, _getProposalHashWith()); + + vm.mockCall( + stateFingerprintComputer, + abi.encodeWithSignature( + "computeStateFingerprint(address,uint256)", + params.base.collateralAddress, params.base.collateralId + ), + abi.encode(stateFingerprint) + ); + + vm.expectRevert(abi.encodeWithSelector( + InvalidCollateralStateFingerprint.selector, stateFingerprint, params.base.collateralStateFingerprint + )); + vm.prank(activeLoanContract); + _callAcceptProposalWith(); + } + + function test_shouldFail_whenNoComputerRegistered_whenAssetDoesNotImplementERC165() external { + params.base.collateralAddress = token; + params.signature = _sign(proposerPK, _getProposalHashWith()); + + vm.mockCall( + config, + abi.encodeWithSignature("getStateFingerprintComputer(address)"), + abi.encode(address(0)) + ); + vm.mockCallRevert( + params.base.collateralAddress, + abi.encodeWithSignature("supportsInterface(bytes4)"), + abi.encode("not implementing ERC165") + ); + + vm.expectRevert(abi.encodeWithSelector(MissingStateFingerprintComputer.selector)); + vm.prank(activeLoanContract); + _callAcceptProposalWith(); + } + + function test_shouldFail_whenNoComputerRegistered_whenAssetDoesNotImplementERC5646() external { + params.signature = _sign(proposerPK, _getProposalHashWith()); + vm.mockCall( config, - abi.encodeWithSignature("getStateFingerprintComputer(address)", token), + abi.encodeWithSignature("getStateFingerprintComputer(address)"), abi.encode(address(0)) ); + _mockERC5646Support(params.base.collateralAddress, false); vm.expectRevert(abi.encodeWithSelector(MissingStateFingerprintComputer.selector)); vm.prank(activeLoanContract); _callAcceptProposalWith(); } - function testFuzz_shouldFail_whenComputerReturnsDifferentStateFingerprint_whenShouldCheckStateFingerprint( + function testFuzz_shouldFail_whenAssetImplementsERC5646_whenComputerReturnsDifferentStateFingerprint( bytes32 stateFingerprint ) external { vm.assume(stateFingerprint != params.base.collateralStateFingerprint); params.signature = _sign(proposerPK, _getProposalHashWith()); vm.mockCall( - stateFingerprintComputer, + config, + abi.encodeWithSignature("getStateFingerprintComputer(address)"), + abi.encode(address(0)) + ); + _mockERC5646Support(params.base.collateralAddress, true); + vm.mockCall( + params.base.collateralAddress, abi.encodeWithSignature("getStateFingerprint(uint256)"), abi.encode(stateFingerprint) ); @@ -518,4 +612,41 @@ abstract contract PWNSimpleLoanProposal_AcceptProposal_Test is PWNSimpleLoanProp _callAcceptProposalWith(); } + function test_shouldPass_whenComputerReturnsMatchingFingerprint() external { + params.signature = _sign(proposerPK, _getProposalHashWith()); + + vm.mockCall( + config, + abi.encodeWithSignature("getStateFingerprintComputer(address)"), + abi.encode(stateFingerprintComputer) + ); + vm.mockCall( + stateFingerprintComputer, + abi.encodeWithSignature("computeStateFingerprint(address,uint256)"), + abi.encode(params.base.collateralStateFingerprint) + ); + + vm.prank(activeLoanContract); + _callAcceptProposalWith(); + } + + function test_shouldPass_whenAssetImplementsERC5646_whenReturnsMatchingFingerprint() external { + params.signature = _sign(proposerPK, _getProposalHashWith()); + + vm.mockCall( + config, + abi.encodeWithSignature("getStateFingerprintComputer(address)"), + abi.encode(address(0)) + ); + _mockERC5646Support(params.base.collateralAddress, true); + vm.mockCall( + params.base.collateralAddress, + abi.encodeWithSignature("getStateFingerprint(uint256)"), + abi.encode(params.base.collateralStateFingerprint) + ); + + vm.prank(activeLoanContract); + _callAcceptProposalWith(); + } + } From 2bea5226c67a9e7b8d2f2613f7c0afad1499a907 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Tue, 9 Apr 2024 12:29:36 -0400 Subject: [PATCH 071/129] feat(uni-v3-state-fingerprint): implement uni v3 position state fingerprint computer --- .../UniV3PosStateFingerpringComputer.sol | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 src/state-fingerprint-computer/UniV3PosStateFingerpringComputer.sol diff --git a/src/state-fingerprint-computer/UniV3PosStateFingerpringComputer.sol b/src/state-fingerprint-computer/UniV3PosStateFingerpringComputer.sol new file mode 100644 index 0000000..ccc166b --- /dev/null +++ b/src/state-fingerprint-computer/UniV3PosStateFingerpringComputer.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.16; + +import { IStateFingerpringComputer } from "@pwn/state-fingerprint-computer/IStateFingerpringComputer.sol"; + + +interface UniswapNonFungiblePositionManagerLike { + function positions(uint256 tokenId) external view returns ( + uint96 nonce, + address operator, + address token0, + address token1, + uint24 fee, + int24 tickLower, + int24 tickUpper, + uint128 liquidity, + uint256 feeGrowthInside0LastX128, + uint256 feeGrowthInside1LastX128, + uint128 tokensOwed0, + uint128 tokensOwed1 + ); +} + +/** + * @notice State fingerprint computer for Uniswap v3 positions. + */ +contract UniV3PosStateFingerpringComputer is IStateFingerpringComputer { + + address immutable public UNI_V3_POS; + + error UnsupportedToken(); + + constructor(address _uniV3Pos) { + UNI_V3_POS = _uniV3Pos; + } + + /** + * @inheritdoc IStateFingerpringComputer + */ + function computeStateFingerprint(address token, uint256 tokenId) external view returns (bytes32) { + if (token != UNI_V3_POS) { + revert UnsupportedToken(); + } + + return _computeStateFingerprint(tokenId); + } + + /** + * @inheritdoc IStateFingerpringComputer + */ + function supportsToken(address token) external view returns (bool) { + return token == UNI_V3_POS; + } + + /** + * @notice Compute current token state fingerprint of a Uniswap v3 position. + * @param tokenId Token id to compute state fingerprint for. + * @return Current token state fingerprint. + */ + function _computeStateFingerprint(uint256 tokenId) private view returns (bytes32) { + ( + uint96 nonce, + address operator, + address token0, + address token1, + uint24 fee, + int24 tickLower, + int24 tickUpper, + uint128 liquidity, + uint256 feeGrowthInside0LastX128, + uint256 feeGrowthInside1LastX128, + uint128 tokensOwed0, + uint128 tokensOwed1 + ) = UniswapNonFungiblePositionManagerLike(UNI_V3_POS).positions(tokenId); + + return keccak256(abi.encode( + nonce, + operator, + token0, + token1, + fee, + tickLower, + tickUpper, + liquidity, + feeGrowthInside0LastX128, + feeGrowthInside1LastX128, + tokensOwed0, + tokensOwed1 + )); + } + +} From 91a5d681bc8f5232ce95241c1d596634728e3597 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Tue, 9 Apr 2024 12:29:53 -0400 Subject: [PATCH 072/129] feat(chicken-bond-state-fingerprint): implement chicken bond state fingerprint computer --- .../ChickenBondStateFingerpringComputer.sol | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 src/state-fingerprint-computer/ChickenBondStateFingerpringComputer.sol diff --git a/src/state-fingerprint-computer/ChickenBondStateFingerpringComputer.sol b/src/state-fingerprint-computer/ChickenBondStateFingerpringComputer.sol new file mode 100644 index 0000000..78334b8 --- /dev/null +++ b/src/state-fingerprint-computer/ChickenBondStateFingerpringComputer.sol @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.16; + +import { IStateFingerpringComputer } from "@pwn/state-fingerprint-computer/IStateFingerpringComputer.sol"; + + +interface IChickenBondManagerLike { + function getBondData(uint256 _bondID) + external + view + returns ( + uint256 lusdAmount, + uint64 claimedBLUSD, + uint64 startTime, + uint64 endTime, + uint8 status + ); +} + +interface IChickenBondNFTLike { + function getBondExtraData(uint256 _tokenID) + external + view + returns ( + uint80 initialHalfDna, + uint80 finalHalfDna, + uint32 troveSize, + uint32 lqtyAmount, + uint32 curveGaugeSlopes + ); +} + +/** + * @notice State fingerprint computer for Chicken Bond positions. + * @dev Computer will get bond data from `CHICKEN_BOND_MANAGER` and `CHICKEN_BOND`. + */ +contract ChickenBondStateFingerpringComputer is IStateFingerpringComputer { + + address immutable public CHICKEN_BOND_MANAGER; + address immutable public CHICKEN_BOND; + + error UnsupportedToken(); + + constructor(address _chickenBondManager, address _chickenBond) { + CHICKEN_BOND_MANAGER = _chickenBondManager; + CHICKEN_BOND = _chickenBond; + } + + /** + * @inheritdoc IStateFingerpringComputer + */ + function computeStateFingerprint(address token, uint256 tokenId) external view returns (bytes32) { + if (token != CHICKEN_BOND) { + revert UnsupportedToken(); + } + + return _computeStateFingerprint(tokenId); + } + + /** + * @inheritdoc IStateFingerpringComputer + */ + function supportsToken(address token) external view returns (bool) { + return token == CHICKEN_BOND; + } + + /** + * @notice Compute current token state fingerprint of a Chicken Bond. + * @param tokenId Token id to compute state fingerprint for. + * @return Current token state fingerprint. + */ + function _computeStateFingerprint(uint256 tokenId) private view returns (bytes32) { + ( + uint256 lusdAmount, + uint64 claimedBLUSD, + uint64 startTime, + uint64 endTime, + uint8 status + ) = IChickenBondManagerLike(CHICKEN_BOND_MANAGER).getBondData(tokenId); + + ( + uint80 initialHalfDna, + uint80 finalHalfDna, + uint32 troveSize, + uint32 lqtyAmount, + uint32 curveGaugeSlopes + ) = IChickenBondNFTLike(CHICKEN_BOND).getBondExtraData(tokenId); + + return keccak256(abi.encode( + lusdAmount, + claimedBLUSD, + startTime, + endTime, + status, + initialHalfDna, + finalHalfDna, + troveSize, + lqtyAmount, + curveGaugeSlopes + )); + } + +} From b43752e69dae22e80feb122d6243fb5b50e8cc2a Mon Sep 17 00:00:00 2001 From: ashhanai Date: Tue, 9 Apr 2024 13:41:37 -0400 Subject: [PATCH 073/129] ci: redeploy to tenderly ethereum fork --- deployments.json | 278 ---------------------------------------- deployments/latest.json | 14 +- script/PWN.s.sol | 4 +- 3 files changed, 10 insertions(+), 286 deletions(-) delete mode 100644 deployments.json diff --git a/deployments.json b/deployments.json deleted file mode 100644 index 184f569..0000000 --- a/deployments.json +++ /dev/null @@ -1,278 +0,0 @@ -{ - "deployedChains": [1, 5, 10, 25, 56, 137, 338, 5000, 5001, 8453, 42161, 84531, 11155111], - "chains": { - "1": { - "dao": "0x1B8383D2726E7e18189205337424a2631A2102F4", - "productTimelock": "0xd57e72A328AB1deC6b374c2babe2dc489819B5Ea", - "protocolTimelock": "0xd8dbdDf1c0FDdf9b5eCFA5C067C38DB66739FBAB", - "protocolSafe": "0x61a77B19b7F4dB82222625D7a969698894d77473", - "daoSafe": "0xd56635c0E91D31F88B89F195D3993a9e34516e59", - "feeCollector": "0xd56635c0E91D31F88B89F195D3993a9e34516e59", - "deployerSafe": "0x42Cad20c964067f8e8b5c3E13fd0aa3C20a964C4", - "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", - "configSingleton": "0x0000000000000000000000000000000000000000", - "config": "0x0000000000000000000000000000000000000000", - "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", - "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", - "revokedNonce": "0x0000000000000000000000000000000000000000", - "simpleLoan": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", - "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", - "stateFingerprintComputerRegistry": "0x0000000000000000000000000000000000000000", - "categoryRegistry": "0x0000000000000000000000000000000000000000" - }, - "5": { - "dao": "0x0000000000000000000000000000000000000000", - "productTimelock": "0x0000000000000000000000000000000000000000", - "protocolTimelock": "0x0000000000000000000000000000000000000000", - "protocolSafe": "0x61a77B19b7F4dB82222625D7a969698894d77473", - "daoSafe": "0xd56635c0E91D31F88B89F195D3993a9e34516e59", - "feeCollector": "0xd56635c0E91D31F88B89F195D3993a9e34516e59", - "deployerSafe": "0x42Cad20c964067f8e8b5c3E13fd0aa3C20a964C4", - "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", - "configSingleton": "0x0000000000000000000000000000000000000000", - "config": "0x0000000000000000000000000000000000000000", - "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", - "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", - "revokedNonce": "0x0000000000000000000000000000000000000000", - "simpleLoan": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", - "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", - "stateFingerprintComputerRegistry": "0x0000000000000000000000000000000000000000", - "categoryRegistry": "0x0000000000000000000000000000000000000000" - }, - "10": { - "dao": "0x0000000000000000000000000000000000000000", - "productTimelock": "0x60a0F7594793e3DC31DfE1cC930dF65B54e95B39", - "protocolTimelock": "0x744B83343a86F87Ed05a5f3A92939D6d81520F27", - "protocolSafe": "0xa7106a1C2498EaeF4AC1B594a6544c841623B327", - "daoSafe": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", - "feeCollector": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", - "deployerSafe": "0x1B4B37738De3bb9E6a7a4f99aFe4C145734c071d", - "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", - "configSingleton": "0x0000000000000000000000000000000000000000", - "config": "0x0000000000000000000000000000000000000000", - "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", - "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", - "revokedNonce": "0x0000000000000000000000000000000000000000", - "simpleLoan": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", - "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", - "stateFingerprintComputerRegistry": "0x0000000000000000000000000000000000000000", - "categoryRegistry": "0x0000000000000000000000000000000000000000" - }, - "25": { - "dao": "0x0000000000000000000000000000000000000000", - "productTimelock": "0x60a0F7594793e3DC31DfE1cC930dF65B54e95B39", - "protocolTimelock": "0x744B83343a86F87Ed05a5f3A92939D6d81520F27", - "protocolSafe": "0xa7106a1C2498EaeF4AC1B594a6544c841623B327", - "daoSafe": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", - "feeCollector": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", - "deployerSafe": "0x1B4B37738De3bb9E6a7a4f99aFe4C145734c071d", - "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", - "configSingleton": "0x0000000000000000000000000000000000000000", - "config": "0x0000000000000000000000000000000000000000", - "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", - "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", - "revokedNonce": "0x0000000000000000000000000000000000000000", - "simpleLoan": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", - "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", - "stateFingerprintComputerRegistry": "0x0000000000000000000000000000000000000000", - "categoryRegistry": "0x0000000000000000000000000000000000000000" - }, - "56": { - "dao": "0x0000000000000000000000000000000000000000", - "productTimelock": "0xd57e72A328AB1deC6b374c2babe2dc489819B5Ea", - "protocolTimelock": "0xd8dbdDf1c0FDdf9b5eCFA5C067C38DB66739FBAB", - "protocolSafe": "0x61a77B19b7F4dB82222625D7a969698894d77473", - "daoSafe": "0xd56635c0E91D31F88B89F195D3993a9e34516e59", - "feeCollector": "0xd56635c0E91D31F88B89F195D3993a9e34516e59", - "deployerSafe": "0x42Cad20c964067f8e8b5c3E13fd0aa3C20a964C4", - "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", - "configSingleton": "0x0000000000000000000000000000000000000000", - "config": "0x0000000000000000000000000000000000000000", - "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", - "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", - "revokedNonce": "0x0000000000000000000000000000000000000000", - "simpleLoan": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", - "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", - "stateFingerprintComputerRegistry": "0x0000000000000000000000000000000000000000", - "categoryRegistry": "0x0000000000000000000000000000000000000000" - }, - "137": { - "dao": "0x0000000000000000000000000000000000000000", - "productTimelock": "0x2cDf99aD1115Ea0E943E56dd26459E3e57788C12", - "protocolTimelock": "0x9b1ec4bc634db130ab7310d4e585338888030623", - "protocolSafe": "0x61a77B19b7F4dB82222625D7a969698894d77473", - "daoSafe": "0xd56635c0E91D31F88B89F195D3993a9e34516e59", - "feeCollector": "0xd56635c0E91D31F88B89F195D3993a9e34516e59", - "deployerSafe": "0x42Cad20c964067f8e8b5c3E13fd0aa3C20a964C4", - "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", - "configSingleton": "0x0000000000000000000000000000000000000000", - "config": "0x0000000000000000000000000000000000000000", - "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", - "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", - "revokedNonce": "0x0000000000000000000000000000000000000000", - "simpleLoan": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", - "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", - "stateFingerprintComputerRegistry": "0x0000000000000000000000000000000000000000", - "categoryRegistry": "0x0000000000000000000000000000000000000000" - }, - "338": { - "dao": "0x0000000000000000000000000000000000000000", - "productTimelock": "0x0000000000000000000000000000000000000000", - "protocolTimelock": "0x0000000000000000000000000000000000000000", - "protocolSafe": "0xa7106a1C2498EaeF4AC1B594a6544c841623B327", - "daoSafe": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", - "feeCollector": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", - "deployerSafe": "0x0000000000000000000000000000000000000000", - "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", - "configSingleton": "0x0000000000000000000000000000000000000000", - "config": "0x0000000000000000000000000000000000000000", - "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", - "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", - "revokedNonce": "0x0000000000000000000000000000000000000000", - "simpleLoan": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", - "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", - "stateFingerprintComputerRegistry": "0x0000000000000000000000000000000000000000", - "categoryRegistry": "0x0000000000000000000000000000000000000000" - }, - "5000": { - "dao": "0x0000000000000000000000000000000000000000", - "productTimelock": "0x60a0F7594793e3DC31DfE1cC930dF65B54e95B39", - "protocolTimelock": "0x744B83343a86F87Ed05a5f3A92939D6d81520F27", - "protocolSafe": "0xa7106a1C2498EaeF4AC1B594a6544c841623B327", - "daoSafe": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", - "feeCollector": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", - "deployerSafe": "0x1B4B37738De3bb9E6a7a4f99aFe4C145734c071d", - "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", - "configSingleton": "0x0000000000000000000000000000000000000000", - "config": "0x0000000000000000000000000000000000000000", - "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", - "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", - "revokedNonce": "0x0000000000000000000000000000000000000000", - "simpleLoan": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", - "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", - "stateFingerprintComputerRegistry": "0x0000000000000000000000000000000000000000", - "categoryRegistry": "0x0000000000000000000000000000000000000000" - }, - "5001": { - "dao": "0x0000000000000000000000000000000000000000", - "productTimelock": "0x0000000000000000000000000000000000000000", - "protocolTimelock": "0x0000000000000000000000000000000000000000", - "protocolSafe": "0xa7106a1C2498EaeF4AC1B594a6544c841623B327", - "daoSafe": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", - "feeCollector": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", - "deployerSafe": "0x0000000000000000000000000000000000000000", - "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", - "configSingleton": "0x0000000000000000000000000000000000000000", - "config": "0x0000000000000000000000000000000000000000", - "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", - "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", - "revokedNonce": "0x0000000000000000000000000000000000000000", - "simpleLoan": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", - "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", - "stateFingerprintComputerRegistry": "0x0000000000000000000000000000000000000000", - "categoryRegistry": "0x0000000000000000000000000000000000000000" - }, - "8453": { - "dao": "0x0000000000000000000000000000000000000000", - "productTimelock": "0x60a0F7594793e3DC31DfE1cC930dF65B54e95B39", - "protocolTimelock": "0x744B83343a86F87Ed05a5f3A92939D6d81520F27", - "protocolSafe": "0xa7106a1C2498EaeF4AC1B594a6544c841623B327", - "daoSafe": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", - "feeCollector": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", - "deployerSafe": "0x1B4B37738De3bb9E6a7a4f99aFe4C145734c071d", - "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", - "configSingleton": "0x0000000000000000000000000000000000000000", - "config": "0x0000000000000000000000000000000000000000", - "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", - "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", - "revokedNonce": "0x0000000000000000000000000000000000000000", - "simpleLoan": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", - "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", - "stateFingerprintComputerRegistry": "0x0000000000000000000000000000000000000000", - "categoryRegistry": "0x0000000000000000000000000000000000000000" - }, - "42161": { - "dao": "0x0000000000000000000000000000000000000000", - "productTimelock": "0x2cDf99aD1115Ea0E943E56dd26459E3e57788C12", - "protocolTimelock": "0x9b1ec4bc634db130ab7310d4e585338888030623", - "protocolSafe": "0x61a77B19b7F4dB82222625D7a969698894d77473", - "daoSafe": "0xd56635c0E91D31F88B89F195D3993a9e34516e59", - "feeCollector": "0xd56635c0E91D31F88B89F195D3993a9e34516e59", - "deployerSafe": "0x42Cad20c964067f8e8b5c3E13fd0aa3C20a964C4", - "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", - "configSingleton": "0x0000000000000000000000000000000000000000", - "config": "0x0000000000000000000000000000000000000000", - "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", - "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", - "revokedNonce": "0x0000000000000000000000000000000000000000", - "simpleLoan": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", - "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", - "stateFingerprintComputerRegistry": "0x0000000000000000000000000000000000000000", - "categoryRegistry": "0x0000000000000000000000000000000000000000" - }, - "84531": { - "dao": "0x0000000000000000000000000000000000000000", - "productTimelock": "0x0000000000000000000000000000000000000000", - "protocolTimelock": "0x0000000000000000000000000000000000000000", - "protocolSafe": "0xa7106a1C2498EaeF4AC1B594a6544c841623B327", - "daoSafe": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", - "feeCollector": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", - "deployerSafe": "0x0000000000000000000000000000000000000000", - "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", - "configSingleton": "0x0000000000000000000000000000000000000000", - "config": "0x0000000000000000000000000000000000000000", - "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", - "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", - "revokedNonce": "0x0000000000000000000000000000000000000000", - "simpleLoan": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", - "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", - "stateFingerprintComputerRegistry": "0x0000000000000000000000000000000000000000", - "categoryRegistry": "0x0000000000000000000000000000000000000000" - }, - "11155111": { - "dao": "0x0000000000000000000000000000000000000000", - "productTimelock": "0x0000000000000000000000000000000000000000", - "protocolTimelock": "0x0000000000000000000000000000000000000000", - "protocolSafe": "0xa7106a1C2498EaeF4AC1B594a6544c841623B327", - "daoSafe": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", - "feeCollector": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", - "deployerSafe": "0x1B4B37738De3bb9E6a7a4f99aFe4C145734c071d", - "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", - "configSingleton": "0x0000000000000000000000000000000000000000", - "config": "0x0000000000000000000000000000000000000000", - "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", - "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", - "revokedNonce": "0x0000000000000000000000000000000000000000", - "simpleLoan": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleOffer": "0x0000000000000000000000000000000000000000", - "simpleLoanListOffer": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleRequest": "0x0000000000000000000000000000000000000000", - "stateFingerprintComputerRegistry": "0x0000000000000000000000000000000000000000", - "categoryRegistry": "0x0000000000000000000000000000000000000000" - } - } -} diff --git a/deployments/latest.json b/deployments/latest.json index 19ccbbc..27b02d1 100644 --- a/deployments/latest.json +++ b/deployments/latest.json @@ -10,16 +10,16 @@ "deployerSafe": "0x42Cad20c964067f8e8b5c3E13fd0aa3C20a964C4", "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", "categoryRegistry": "0x0c93666Bf6359951Ade361D6E19f2DB240dA392f", - "configSingleton": "0x1E53eC63395576d23770b00E86053aBf0b9a3a21", + "configSingleton": "0xD1f71974aAdc34FcC66C992E0A09b1E64D4C3E33", "config": "0x9C432a1A229ef87b138da29dE930B3d8EC2C67Fa", "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", - "revokedNonce": "0x32353ecC81dE2d8c710a5295B96c0d315a0bb563", - "simpleLoan": "0xBE8A45A4d82A0D7181E6A37E13aE22143336D7DE", - "simpleLoanSimpleProposal": "0x430CC773C5C4931518CFeFf8d8563e01A8a1Da99", - "simpleLoanListProposal": "0xFFcC4E9D8CDefb34Fa80A3DD85712EfF6071a768", - "simpleLoanFungibleProposal": "0x036bcD053344Cf34ef33efa81cCA832AE998336d", - "simpleLoanDutchAuctionProposal": "0x3ecde871cB34231F946136EfF5010Bcc4800FA75" + "revokedNonce": "0x870A65689f4AF5ecb05064D5A5C601274602d313", + "simpleLoan": "0x30f8D71f79B785A72b621DeD5665387CcE4f910C", + "simpleLoanSimpleProposal": "0x33F5b26ede842322E60433ec6e32F547cEA90A5C", + "simpleLoanListProposal": "0x94dA12676b3909BeeFbF01F66F3DFC824D67DBB5", + "simpleLoanFungibleProposal": "0x1C3911Ef03dEBeFc9b8567Ec4Dc1766F1C7Cf3f1", + "simpleLoanDutchAuctionProposal": "0x2850CB3D78389D1A5aa485EcD8D6472Fe1Dd6fa4" } } } diff --git a/script/PWN.s.sol b/script/PWN.s.sol index 32245a5..15ade38 100644 --- a/script/PWN.s.sol +++ b/script/PWN.s.sol @@ -90,7 +90,8 @@ contract Deploy is Deployments, Script { /* forge script script/PWN.s.sol:Deploy \ --sig "deployNewProtocolVersion()" \ ---rpc-url $RPCURL \ +--rpc-url $RPC_URL \ +--private-key $PRIVATE_KEY \ --with-gas-price $(cast --to-wei 15 gwei) \ --verify --etherscan-api-key $ETHERSCAN_API_KEY \ --broadcast @@ -423,6 +424,7 @@ contract Setup is Deployments, Script { forge script script/PWN.s.sol:Setup \ --sig "setupNewProtocolVersion()" \ --rpc-url $RPC_URL \ +--private-key $PRIVATE_KEY \ --with-gas-price $(cast --to-wei 15 gwei) \ --broadcast */ From 72b1e20e2dfb345e300d8b96c4961e28f452c575 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Wed, 10 Apr 2024 13:58:29 -0400 Subject: [PATCH 074/129] feat(pool-adapter): implement pool adapter registry in config --- src/config/PWNConfig.sol | 38 +++++++++++++++++-- src/pool-adapter/IPoolAdapter.sol | 31 ++++++++++++++++ test/unit/PWNConfig.t.sol | 61 +++++++++++++++++++++++++++++-- 3 files changed, 123 insertions(+), 7 deletions(-) create mode 100644 src/pool-adapter/IPoolAdapter.sol diff --git a/src/config/PWNConfig.sol b/src/config/PWNConfig.sol index d0896d9..5879e49 100644 --- a/src/config/PWNConfig.sol +++ b/src/config/PWNConfig.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.16; import { Ownable2Step } from "openzeppelin-contracts/contracts/access/Ownable2Step.sol"; import { Initializable } from "openzeppelin-contracts/contracts/proxy/utils/Initializable.sol"; +import { IPoolAdapter } from "@pwn/pool-adapter/IPoolAdapter.sol"; import { IStateFingerpringComputer } from "@pwn/state-fingerprint-computer/IStateFingerpringComputer.sol"; import "@pwn/PWNErrors.sol"; @@ -42,10 +43,16 @@ contract PWNConfig is Ownable2Step, Initializable { mapping (address => string) private _loanMetadataUri; /** - * @notice Mapping holding registered computer to an asset. + * @notice Mapping holding registered state fingerprint computer to an asset. * @dev Only owner can update the mapping. */ - mapping (address => address) private _computerRegistry; + mapping (address => address) private _sfComputerRegistry; + + /** + * @notice Mapping holding registered pool adapter to a pool address. + * @dev Only owner can update the mapping. + */ + mapping (address => address) private _poolAdapterRegistry; /*----------------------------------------------------------*| |* # EVENTS & ERRORS DEFINITIONS *| @@ -188,7 +195,7 @@ contract PWNConfig is Ownable2Step, Initializable { * @return The computer for the given asset. */ function getStateFingerprintComputer(address asset) external view returns (IStateFingerpringComputer) { - return IStateFingerpringComputer(_computerRegistry[asset]); + return IStateFingerpringComputer(_sfComputerRegistry[asset]); } /** @@ -201,7 +208,30 @@ contract PWNConfig is Ownable2Step, Initializable { if (!IStateFingerpringComputer(computer).supportsToken(asset)) revert InvalidComputerContract(); - _computerRegistry[asset] = computer; + _sfComputerRegistry[asset] = computer; + } + + + /*----------------------------------------------------------*| + |* # POOL ADAPTER *| + |*----------------------------------------------------------*/ + + /** + * @notice Returns the pool adapter for a given pool. + * @param pool The pool for which the adapter is requested. + * @return The adapter for the given pool. + */ + function getPoolAdapter(address pool) external view returns (IPoolAdapter) { + return IPoolAdapter(_poolAdapterRegistry[pool]); + } + + /** + * @notice Registers a pool adapter for a given pool. + * @param pool The pool for which the adapter is registered. + * @param adapter The adapter to be registered. + */ + function registerPoolAdapter(address pool, address adapter) external onlyOwner { + _poolAdapterRegistry[pool] = adapter; } } diff --git a/src/pool-adapter/IPoolAdapter.sol b/src/pool-adapter/IPoolAdapter.sol new file mode 100644 index 0000000..cb44ac2 --- /dev/null +++ b/src/pool-adapter/IPoolAdapter.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.16; + +/** + * @title IPoolAdapter + * @notice Interface for pool adapters used to withdraw and supply assets to the pool. + */ +interface IPoolAdapter { + + /** + * @notice Withdraw an asset from the pool on behalf of the owner. + * @dev Adapter will withdraw and transfer the asset to the caller. + * Caller must have the ACTIVE_LOAN tag in the hub. + * @param pool The address of the pool from which the asset is withdrawn. + * @param owner The address of the owner from whom the asset is withdrawn. + * @param asset The address of the asset to withdraw. + * @param amount The amount of the asset to withdraw. + */ + function withdraw(address pool, address owner, address asset, uint256 amount) external; + + /** + * @notice Supply an asset to the pool on behalf of the owner. + * @dev Need to transfer the asset to the adapter before calling this function. + * @param pool The address of the pool to which the asset is supplied. + * @param owner The address of the owner on whose behalf the asset is supplied. + * @param asset The address of the asset to supply. + * @param amount The amount of the asset to supply. + */ + function supply(address pool, address owner, address asset, uint256 amount) external; + +} diff --git a/test/unit/PWNConfig.t.sol b/test/unit/PWNConfig.t.sol index 68841cf..73039bf 100644 --- a/test/unit/PWNConfig.t.sol +++ b/test/unit/PWNConfig.t.sol @@ -15,7 +15,8 @@ abstract contract PWNConfigTest is Test { bytes32 internal constant FEE_SLOT = bytes32(uint256(1)); // `fee` property position bytes32 internal constant FEE_COLLECTOR_SLOT = bytes32(uint256(2)); // `feeCollector` property position bytes32 internal constant LOAN_METADATA_URI_SLOT = bytes32(uint256(3)); // `loanMetadataUri` mapping position - bytes32 internal constant REGISTRY_SLOT = bytes32(uint256(4)); // `_computerRegistry` mapping position + bytes32 internal constant SFC_REGISTRY_SLOT = bytes32(uint256(4)); // `_sfComputerRegistry` mapping position + bytes32 internal constant POOL_ADAPTER_REGISTRY_SLOT = bytes32(uint256(5)); // `_poolAdapterRegistry` mapping position PWNConfig config; address owner = makeAddr("owner"); @@ -367,7 +368,7 @@ contract PWNConfig_GetStateFingerprintComputer_Test is PWNConfigTest { function testFuzz_shouldReturnStoredComputer_whenIsRegistered(address asset, address computer) external { - bytes32 assetSlot = keccak256(abi.encode(asset, REGISTRY_SLOT)); + bytes32 assetSlot = keccak256(abi.encode(asset, SFC_REGISTRY_SLOT)); vm.store(address(config), assetSlot, bytes32(uint256(uint160(computer)))); assertEq(address(config.getStateFingerprintComputer(asset)), computer); @@ -399,7 +400,7 @@ contract PWNConfig_RegisterStateFingerprintComputer_Test is PWNConfigTest { function testFuzz_shouldUnregisterComputer_whenComputerIsZeroAddress(address asset) external { address computer = makeAddr("computer"); - bytes32 assetSlot = keccak256(abi.encode(asset, REGISTRY_SLOT)); + bytes32 assetSlot = keccak256(abi.encode(asset, SFC_REGISTRY_SLOT)); vm.store(address(config), assetSlot, bytes32(uint256(uint160(computer)))); vm.prank(owner); @@ -428,3 +429,57 @@ contract PWNConfig_RegisterStateFingerprintComputer_Test is PWNConfigTest { } } + + +/*----------------------------------------------------------*| +|* # GET POOL ADAPTER *| +|*----------------------------------------------------------*/ + +contract PWNConfig_GetPoolAdapter_Test is PWNConfigTest { + + function setUp() override public { + super.setUp(); + + _initialize(); + } + + + function testFuzz_shouldReturnStoredAdapter_whenIsRegistered(address pool, address adapter) external { + bytes32 poolSlot = keccak256(abi.encode(pool, POOL_ADAPTER_REGISTRY_SLOT)); + vm.store(address(config), poolSlot, bytes32(uint256(uint160(adapter)))); + + assertEq(address(config.getPoolAdapter(pool)), adapter); + } + +} + + +/*----------------------------------------------------------*| +|* # REGISTER POOL ADAPTER *| +|*----------------------------------------------------------*/ + +contract PWNConfig_RegisterPoolAdapter_Test is PWNConfigTest { + + function setUp() override public { + super.setUp(); + + _initialize(); + } + + + function testFuzz_shouldFail_whenCallerIsNotOwner(address caller) external { + vm.assume(caller != owner); + + vm.expectRevert("Ownable: caller is not the owner"); + vm.prank(caller); + config.registerPoolAdapter(address(0), address(0)); + } + + function testFuzz_shouldStoreAdapter(address pool, address adapter) external { + vm.prank(owner); + config.registerPoolAdapter(pool, adapter); + + assertEq(address(config.getPoolAdapter(pool)), adapter); + } + +} From 9e674d1650e059e274f048e2f7dba890a3688d00 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Wed, 10 Apr 2024 14:59:46 -0400 Subject: [PATCH 075/129] feat(pool-adapter): update simple loan to use pool adapter instead of direct pool integration --- src/PWNErrors.sol | 1 + src/hub/PWNHubTags.sol | 3 - src/loan/terms/simple/loan/PWNSimpleLoan.sol | 59 ++++++--- src/loan/vault/ICometLike.sol | 7 -- src/loan/vault/PWNVault.sol | 29 +++-- test/helper/DummyCompoundPool.sol | 19 --- test/helper/DummyPoolAdapter.sol | 19 +++ test/unit/PWNSimpleLoan.t.sol | 121 ++++++++++++------- 8 files changed, 156 insertions(+), 102 deletions(-) delete mode 100644 src/loan/vault/ICometLike.sol delete mode 100644 test/helper/DummyCompoundPool.sol create mode 100644 test/helper/DummyPoolAdapter.sol diff --git a/src/PWNErrors.sol b/src/PWNErrors.sol index bb90678..94e5a93 100644 --- a/src/PWNErrors.sol +++ b/src/PWNErrors.sol @@ -18,6 +18,7 @@ error InvalidLenderSpecHash(bytes32 current, bytes32 expected); error InvalidDuration(uint256 current, uint256 limit); error AccruingInterestAPROutOfBounds(uint256 current, uint256 limit); error CallerNotVault(); +error InvalidSourceOfFunds(address sourceOfFunds); // Loan extension error InvalidExtensionDuration(uint256 duration, uint256 limit); diff --git a/src/hub/PWNHubTags.sol b/src/hub/PWNHubTags.sol index eeb3107..44dd32b 100644 --- a/src/hub/PWNHubTags.sol +++ b/src/hub/PWNHubTags.sol @@ -12,7 +12,4 @@ library PWNHubTags { /// @dev Address can revoke nonces on other addresses behalf. bytes32 internal constant NONCE_MANAGER = keccak256("PWN_NONCE_MANAGER"); - /// @dev Address is valid Compound pool and can be used as a source of funds. - bytes32 internal constant COMPOUND_V3_POOL = keccak256("COMPOUND_V3_POOL"); - } diff --git a/src/loan/terms/simple/loan/PWNSimpleLoan.sol b/src/loan/terms/simple/loan/PWNSimpleLoan.sol index 8c62edf..dfd3a70 100644 --- a/src/loan/terms/simple/loan/PWNSimpleLoan.sol +++ b/src/loan/terms/simple/loan/PWNSimpleLoan.sol @@ -18,6 +18,7 @@ import { PWNLOAN } from "@pwn/loan/token/PWNLOAN.sol"; import { Permit } from "@pwn/loan/vault/Permit.sol"; import { PWNVault } from "@pwn/loan/vault/PWNVault.sol"; import { PWNRevokedNonce } from "@pwn/nonce/PWNRevokedNonce.sol"; +import { IPoolAdapter } from "@pwn/pool-adapter/IPoolAdapter.sol"; import "@pwn/PWNErrors.sol"; @@ -106,8 +107,8 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { /** * @notice Lender specification during loan creation. - * @param sourceOfFunds Address of a source of funds. This address can be an address of a Compound v3 pool or - * lender address if lender is funding the loan directly. + * @param sourceOfFunds Address of a source of funds. This can be the lenders address, if the loan is funded directly, + * or a pool address from with the funds are withdrawn on the lenders behalf. */ struct LenderSpec { address sourceOfFunds; @@ -307,17 +308,12 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { if (msg.sender != loanTerms.lender && loanTerms.lenderSpecHash != getLenderSpecHash(lenderSpec)) { revert InvalidLenderSpecHash({ current: loanTerms.lenderSpecHash, expected: getLenderSpecHash(lenderSpec) }); } - // Check that the lender is the source of funds or the source of funds is a Compound pool - if (lenderSpec.sourceOfFunds != loanTerms.lender) { - if (!hub.hasTag(lenderSpec.sourceOfFunds, PWNHubTags.COMPOUND_V3_POOL)) { - revert AddressMissingHubTag({ addr: lenderSpec.sourceOfFunds, tag: PWNHubTags.COMPOUND_V3_POOL }); - } - } // 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 }); @@ -474,10 +470,10 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { address creditProvider = loanTerms.lender; if (lenderSpec.sourceOfFunds != loanTerms.lender) { - // Note: Lender is not source of funds. - // Withdraw credit asset to the loan contract and use it as a credit provider. + // Note: Lender is not the source of funds. Withdraw credit asset to the Vault and use it + // as a credit provider to minimize the number of withdrawals. - _withdrawFromCompound(loanTerms.credit, lenderSpec.sourceOfFunds, loanTerms.lender); + _pullCreditFromPool(loanTerms.credit, loanTerms, lenderSpec); creditProvider = address(this); } @@ -544,9 +540,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { // as a credit provider to minimize the number of withdrawals. creditHelper.amount = feeAmount + (shouldTransferCommon ? common : 0) + surplus; - if (creditHelper.amount > 0) { - _withdrawFromCompound(creditHelper, lenderSpec.sourceOfFunds, loanTerms.lender); - } + _pullCreditFromPool(creditHelper, loanTerms, lenderSpec); creditProvider = address(this); } @@ -581,6 +575,28 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { } } + /** + * @notice Pull a credit asset from a pool to the Vault. + * @dev The function will revert if pool doesn't have registered pool adapter. + * @param credit Asset to be pulled from the pool. + * @param loanTerms Loan terms struct. + * @param lenderSpec Lender specification struct. + */ + function _pullCreditFromPool( + MultiToken.Asset memory credit, + Terms memory loanTerms, + LenderSpec calldata lenderSpec + ) private { + IPoolAdapter poolAdapter = config.getPoolAdapter(lenderSpec.sourceOfFunds); + if (address(poolAdapter) == address(0)) { + revert InvalidSourceOfFunds({ sourceOfFunds: lenderSpec.sourceOfFunds }); + } + + if (credit.amount > 0) { + _withdrawFromPool(credit, poolAdapter, lenderSpec.sourceOfFunds, loanTerms.lender); + } + } + /*----------------------------------------------------------*| |* # REPAY LOAN *| @@ -773,9 +789,18 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { if (destinationOfFunds == loanOwner) { _push(repaymentCredit, loanOwner); } else { - // Supply the repaid credit to the Compound pool - MultiToken.approveAsset(repaymentCredit, destinationOfFunds); - _supplyToCompound(repaymentCredit, destinationOfFunds, loanOwner); + IPoolAdapter poolAdapter = config.getPoolAdapter(destinationOfFunds); + // Check that pool has registered adapter + if (address(poolAdapter) == address(0)) { + + // Note: Adapter can be unregistered during the loan lifetime, so the pool might not have an adapter. + // In that case, the loan owner will be able to claim the repaid credit. + + revert InvalidSourceOfFunds({ sourceOfFunds: destinationOfFunds }); + } + + // Supply the repaid credit to the original pool + _supplyToPool(repaymentCredit, poolAdapter, destinationOfFunds, loanOwner); } // Note: If the transfer fails, the LOAN token will remain in repaid state and the LOAN token owner diff --git a/src/loan/vault/ICometLike.sol b/src/loan/vault/ICometLike.sol deleted file mode 100644 index e6adedb..0000000 --- a/src/loan/vault/ICometLike.sol +++ /dev/null @@ -1,7 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity 0.8.16; - -interface ICometLike { - function supplyFrom(address from, address dst, address asset, uint amount) external; - function withdrawFrom(address src, address to, address asset, uint amount) external; -} diff --git a/src/loan/vault/PWNVault.sol b/src/loan/vault/PWNVault.sol index e4a5227..80c32c6 100644 --- a/src/loan/vault/PWNVault.sol +++ b/src/loan/vault/PWNVault.sol @@ -7,8 +7,8 @@ import { IERC20Permit } from "openzeppelin-contracts/contracts/token/ERC20/exten import { IERC721Receiver } from "openzeppelin-contracts/contracts/token/ERC721/IERC721Receiver.sol"; import { IERC1155Receiver, IERC165 } from "openzeppelin-contracts/contracts/token/ERC1155/IERC1155Receiver.sol"; -import { ICometLike } from "@pwn/loan/vault/ICometLike.sol"; import { Permit } from "@pwn/loan/vault/Permit.sol"; +import { IPoolAdapter } from "@pwn/pool-adapter/IPoolAdapter.sol"; import "@pwn/PWNErrors.sol"; @@ -92,30 +92,35 @@ abstract contract PWNVault is IERC721Receiver, IERC1155Receiver { /** * @notice Function withdrawing an asset from a Compound pool to a vault. - * @dev The function assumes a prior token approval to a vault address and check for a valid pool address. + * @dev The function assumes a prior check for a valid pool address. * @param asset An asset construct - for a definition see { MultiToken dependency lib }. - * @param pool An address of a Compound pool. - * @param from An address on which behalf the asset is withdrawn. + * @param poolAdapter An address of a pool adapter. + * @param pool An address of a pool. + * @param owner An address on which behalf the asset is withdrawn. */ - function _withdrawFromCompound(MultiToken.Asset memory asset, address pool, address from) internal { + function _withdrawFromPool(MultiToken.Asset memory asset, IPoolAdapter poolAdapter, address pool, address owner) internal { uint256 originalBalance = asset.balanceOf(address(this)); - ICometLike(pool).withdrawFrom(from, address(this), asset.assetAddress, asset.amount); + poolAdapter.withdraw(pool, owner, asset.assetAddress, asset.amount); _checkTransfer(asset, originalBalance, address(this), true); } /** - * @notice Function supplying an asset to a Compound pool from a vault. - * @dev The function assumes a prior token approval to a vault address and check for a valid pool address. + * @notice Function supplying an asset to a pool from a vault via a pool adapter. + * @dev The function assumes a prior check for a valid pool address. * @param asset An asset construct - for a definition see { MultiToken dependency lib }. - * @param pool An address of a Compound pool. - * @param dst An address on which behalf the asset is supplied. + * @param poolAdapter An address of a pool adapter. + * @param pool An address of a pool. + * @param owner An address on which behalf the asset is supplied. */ - function _supplyToCompound(MultiToken.Asset memory asset, address pool, address dst) internal { + function _supplyToPool(MultiToken.Asset memory asset, IPoolAdapter poolAdapter, address pool, address owner) internal { uint256 originalBalance = asset.balanceOf(address(this)); - ICometLike(pool).supplyFrom(address(this), dst, asset.assetAddress, asset.amount); + asset.transferAssetFrom(address(this), address(poolAdapter)); + poolAdapter.supply(pool, owner, asset.assetAddress, asset.amount); _checkTransfer(asset, originalBalance, address(this), false); + + // Note: Assuming pool will revert supply transaction if it fails. } function _checkTransfer( diff --git a/test/helper/DummyCompoundPool.sol b/test/helper/DummyCompoundPool.sol deleted file mode 100644 index c77c2f5..0000000 --- a/test/helper/DummyCompoundPool.sol +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity 0.8.16; - -import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; - -import { ICometLike } from "@pwn/loan/vault/ICometLike.sol"; - - -contract DummyCompoundPool is ICometLike { - - function supplyFrom(address from, address, address asset, uint amount) external { - IERC20(asset).transferFrom(from, address(this), amount); - } - - function withdrawFrom(address, address to, address asset, uint amount) external { - IERC20(asset).transfer(to, amount); - } - -} diff --git a/test/helper/DummyPoolAdapter.sol b/test/helper/DummyPoolAdapter.sol new file mode 100644 index 0000000..cc32162 --- /dev/null +++ b/test/helper/DummyPoolAdapter.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.16; + +import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; + +import { IPoolAdapter } from "@pwn/pool-adapter/IPoolAdapter.sol"; + + +contract DummyPoolAdapter is IPoolAdapter { + + function withdraw(address pool, address /* owner */, address asset, uint256 amount) external { + IERC20(asset).transferFrom(pool, msg.sender, amount); + } + + function supply(address pool, address /* owner */, address asset, uint256 amount) external { + IERC20(asset).transfer(pool, amount); + } + +} diff --git a/test/unit/PWNSimpleLoan.t.sol b/test/unit/PWNSimpleLoan.t.sol index dc28d22..7715362 100644 --- a/test/unit/PWNSimpleLoan.t.sol +++ b/test/unit/PWNSimpleLoan.t.sol @@ -8,7 +8,7 @@ import "@pwn/PWNErrors.sol"; import { T20 } from "@pwn-test/helper/token/T20.sol"; import { T721 } from "@pwn-test/helper/token/T721.sol"; -import { DummyCompoundPool } from "@pwn-test/helper/DummyCompoundPool.sol"; +import { DummyPoolAdapter } from "@pwn-test/helper/DummyPoolAdapter.sol"; abstract contract PWNSimpleLoanTest is Test { @@ -30,7 +30,8 @@ abstract contract PWNSimpleLoanTest is Test { uint256 loanId = 42; address lender = makeAddr("lender"); address borrower = makeAddr("borrower"); - address sourceOfFunds = address(new DummyCompoundPool()); + address sourceOfFunds = makeAddr("sourceOfFunds"); + address poolAdapter = address(new DummyPoolAdapter()); uint256 loanDurationInDays = 101; PWNSimpleLoan.LOAN simpleLoan; PWNSimpleLoan.LOAN nonExistingLoan; @@ -78,6 +79,9 @@ abstract contract PWNSimpleLoanTest is Test { vm.prank(address(this)); fungibleAsset.approve(address(loan), type(uint256).max); + vm.prank(sourceOfFunds); + fungibleAsset.approve(poolAdapter, type(uint256).max); + vm.prank(borrower); nonFungibleAsset.approve(address(loan), 2); @@ -157,6 +161,7 @@ abstract contract PWNSimpleLoanTest is Test { vm.mockCall(config, abi.encodeWithSignature("fee()"), abi.encode(0)); vm.mockCall(config, abi.encodeWithSignature("feeCollector()"), abi.encode(feeCollector)); + vm.mockCall(config, abi.encodeWithSignature("getPoolAdapter(address)"), abi.encode(poolAdapter)); vm.mockCall(hub, abi.encodeWithSignature("hasTag(address,bytes32)"), abi.encode(false)); vm.mockCall( @@ -164,11 +169,6 @@ abstract contract PWNSimpleLoanTest is Test { abi.encodeWithSignature("hasTag(address,bytes32)", proposalContract, PWNHubTags.LOAN_PROPOSAL), abi.encode(true) ); - vm.mockCall( - hub, - abi.encodeWithSignature("hasTag(address,bytes32)", sourceOfFunds, PWNHubTags.COMPOUND_V3_POOL), - abi.encode(true) - ); _mockLoanTerms(simpleLoanTerms); _mockLOANMint(loanId); @@ -426,21 +426,6 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { }); } - function testFuzz_shouldFail_whenSourceOfFundsNotTaggedInHub(address _sourceOfFunds) external { - vm.assume(_sourceOfFunds != lender && _sourceOfFunds != sourceOfFunds); - lenderSpec.sourceOfFunds = _sourceOfFunds; - simpleLoanTerms.lenderSpecHash = loan.getLenderSpecHash(lenderSpec); - _mockLoanTerms(simpleLoanTerms); - - vm.expectRevert(abi.encodeWithSelector(AddressMissingHubTag.selector, _sourceOfFunds, PWNHubTags.COMPOUND_V3_POOL)); - loan.createLOAN({ - proposalSpec: proposalSpec, - lenderSpec: lenderSpec, - callerSpec: callerSpec, - extra: "" - }); - } - function testFuzz_shouldFail_whenLoanTermsDurationLessThanMin(uint256 duration) external { uint256 minDuration = loan.MIN_LOAN_DURATION(); vm.assume(duration < minDuration); @@ -666,7 +651,23 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { }); } - function testFuzz_shouldWithdrawCredit_fromSourceOfFunds_whenPoolSourceOfFunds( + function test_shouldFail_whenPoolAdapterNotRegistered_whenPoolSourceOfFunds() external { + lenderSpec.sourceOfFunds = sourceOfFunds; + simpleLoanTerms.lenderSpecHash = loan.getLenderSpecHash(lenderSpec); + _mockLoanTerms(simpleLoanTerms); + + vm.mockCall(config, abi.encodeWithSignature("getPoolAdapter(address)", sourceOfFunds), abi.encode(address(0))); + + vm.expectRevert(abi.encodeWithSelector(InvalidSourceOfFunds.selector, sourceOfFunds)); + loan.createLOAN({ + proposalSpec: proposalSpec, + lenderSpec: lenderSpec, + callerSpec: callerSpec, + extra: "" + }); + } + + function testFuzz_shouldWithdrawCredit_fromSourceOfFunds_toVault_whenPoolSourceOfFunds( uint256 fee, uint256 loanAmount ) external { fee = bound(fee, 0, 9999); @@ -681,10 +682,10 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { vm.mockCall(config, abi.encodeWithSignature("fee()"), abi.encode(fee)); vm.expectCall( - sourceOfFunds, + poolAdapter, abi.encodeWithSignature( - "withdrawFrom(address,address,address,uint256)", - lender, address(loan), simpleLoanTerms.credit.assetAddress, loanAmount + "withdraw(address,address,address,uint256)", + sourceOfFunds, lender, simpleLoanTerms.credit.assetAddress, loanAmount ) ); @@ -1043,16 +1044,32 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { // Pool withdraw + function test_shouldFail_whenPoolAdapterNotRegistered_whenPoolSourceOfFunds() external { + lenderSpec.sourceOfFunds = sourceOfFunds; + simpleLoanTerms.lenderSpecHash = loan.getLenderSpecHash(lenderSpec); + _mockLoanTerms(simpleLoanTerms); + + vm.mockCall(config, abi.encodeWithSignature("getPoolAdapter(address)", sourceOfFunds), abi.encode(address(0))); + + vm.expectRevert(abi.encodeWithSelector(InvalidSourceOfFunds.selector, sourceOfFunds)); + loan.createLOAN({ + proposalSpec: proposalSpec, + lenderSpec: lenderSpec, + callerSpec: callerSpec, + extra: "" + }); + } + function test_shouldWithdrawFullCreditAmountToVault_whenShouldTransferCommon_whenPoolSourceOfFunds() external { lenderSpec.sourceOfFunds = sourceOfFunds; refinancedLoanTerms.lenderSpecHash = loan.getLenderSpecHash(lenderSpec); _mockLoanTerms(refinancedLoanTerms); vm.expectCall( - sourceOfFunds, + poolAdapter, abi.encodeWithSignature( - "withdrawFrom(address,address,address,uint256)", - lender, address(loan), refinancedLoanTerms.credit.assetAddress, refinancedLoanTerms.credit.amount + "withdraw(address,address,address,uint256)", + sourceOfFunds, lender, refinancedLoanTerms.credit.assetAddress, refinancedLoanTerms.credit.amount ) ); @@ -1079,10 +1096,10 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { ); vm.expectCall( - sourceOfFunds, + poolAdapter, abi.encodeWithSignature( - "withdrawFrom(address,address,address,uint256)", - newLender, address(loan), refinancedLoanTerms.credit.assetAddress, refinancedLoanTerms.credit.amount - common + "withdraw(address,address,address,uint256)", + sourceOfFunds, newLender, refinancedLoanTerms.credit.assetAddress, refinancedLoanTerms.credit.amount - common ) ); @@ -1103,8 +1120,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { _mockLOANTokenOwner(refinancingLoanId, newLender); vm.expectCall({ - callee: sourceOfFunds, - data: abi.encodeWithSignature("withdrawFrom(address,address,address,uint256)"), + callee: poolAdapter, + data: abi.encodeWithSignature("withdraw(address,address,address,uint256)"), count: 0 }); @@ -2066,6 +2083,9 @@ contract PWNSimpleLoan_TryClaimRepaidLOANForLoanOwner_Test is PWNSimpleLoanTest } function test_shouldTransferToOriginalLender_whenSourceOfFundsEqualToOriginalLender() external { + simpleLoan.originalSourceOfFunds = lender; + _mockLOAN(loanId, simpleLoan); + vm.expectCall( simpleLoan.creditAddress, abi.encodeWithSignature("transfer(address,uint256)", lender, loan.loanRepaymentAmount(loanId)) @@ -2075,31 +2095,41 @@ contract PWNSimpleLoan_TryClaimRepaidLOANForLoanOwner_Test is PWNSimpleLoanTest loan.tryClaimRepaidLOANForLoanOwner(loanId, lender); } - function test_shouldApproveSourceOfFundsToTransferAmount_whenSourceOfFundsNotEqualToOriginalLender() external { + function test_shouldFail_whenPoolAdapterNotRegistered_whenSourceOfFundsNotEqualToOriginalLender() external { + simpleLoan.originalSourceOfFunds = sourceOfFunds; + _mockLOAN(loanId, simpleLoan); + + vm.mockCall( + config, abi.encodeWithSignature("getPoolAdapter(address)", sourceOfFunds), abi.encode(address(0)) + ); + + vm.expectRevert(abi.encodeWithSelector(InvalidSourceOfFunds.selector, sourceOfFunds)); + vm.prank(address(loan)); + loan.tryClaimRepaidLOANForLoanOwner(loanId, lender); + } + + function test_shouldTransferAmountToPoolAdapter_whenSourceOfFundsNotEqualToOriginalLender() external { simpleLoan.originalSourceOfFunds = sourceOfFunds; _mockLOAN(loanId, simpleLoan); vm.expectCall( simpleLoan.creditAddress, - abi.encodeWithSignature( - "approve(address,uint256)", - sourceOfFunds, loan.loanRepaymentAmount(loanId) - ) + abi.encodeWithSignature("transfer(address,uint256)", poolAdapter, loan.loanRepaymentAmount(loanId)) ); vm.prank(address(loan)); loan.tryClaimRepaidLOANForLoanOwner(loanId, lender); } - function test_shouldTransferToSourceOfFunds_whenSourceOfFundsNotEqualToOriginalLender() external { + function test_shouldCallSupplyOnPoolAdapter_whenSourceOfFundsNotEqualToOriginalLender() external { simpleLoan.originalSourceOfFunds = sourceOfFunds; _mockLOAN(loanId, simpleLoan); vm.expectCall( - sourceOfFunds, + poolAdapter, abi.encodeWithSignature( - "supplyFrom(address,address,address,uint256)", - address(loan), lender, simpleLoan.creditAddress, loan.loanRepaymentAmount(loanId) + "supply(address,address,address,uint256)", + sourceOfFunds, lender, simpleLoan.creditAddress, loan.loanRepaymentAmount(loanId) ) ); @@ -2108,6 +2138,9 @@ contract PWNSimpleLoan_TryClaimRepaidLOANForLoanOwner_Test is PWNSimpleLoanTest } function test_shouldFail_whenTransferFails_whenSourceOfFundsEqualToOriginalLender() external { + simpleLoan.originalSourceOfFunds = lender; + _mockLOAN(loanId, simpleLoan); + vm.mockCallRevert(simpleLoan.creditAddress, abi.encodeWithSignature("transfer(address,uint256)"), ""); vm.expectRevert(); @@ -2119,7 +2152,7 @@ contract PWNSimpleLoan_TryClaimRepaidLOANForLoanOwner_Test is PWNSimpleLoanTest simpleLoan.originalSourceOfFunds = sourceOfFunds; _mockLOAN(loanId, simpleLoan); - vm.mockCallRevert(sourceOfFunds, abi.encodeWithSignature("supplyFrom(address,address,address,uint256)"), ""); + vm.mockCallRevert(poolAdapter, abi.encodeWithSignature("supply(address,address,address,uint256)"), ""); vm.expectRevert(); vm.prank(address(loan)); From 0d6d1fe83e0231a193006106c3c9a26fb5d93492 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Thu, 11 Apr 2024 12:01:21 -0400 Subject: [PATCH 076/129] feat(revoked-nonce): remove dependency on PWNHubAccessControl --- src/nonce/PWNRevokedNonce.sol | 29 ++++++++++++++++++++++++----- test/unit/PWNRevokedNonce.t.sol | 4 ++-- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/nonce/PWNRevokedNonce.sol b/src/nonce/PWNRevokedNonce.sol index 494487c..0c4332a 100644 --- a/src/nonce/PWNRevokedNonce.sol +++ b/src/nonce/PWNRevokedNonce.sol @@ -1,7 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; -import { PWNHubAccessControl } from "@pwn/hub/PWNHubAccessControl.sol"; +import { PWNHub } from "@pwn/hub/PWNHub.sol"; +import { PWNHubTags } from "@pwn/hub/PWNHubTags.sol"; import "@pwn/PWNErrors.sol"; @@ -9,7 +10,7 @@ import "@pwn/PWNErrors.sol"; * @title PWN Revoked Nonce * @notice Contract holding revoked nonces. */ -contract PWNRevokedNonce is PWNHubAccessControl { +contract PWNRevokedNonce { /*----------------------------------------------------------*| |* # VARIABLES & CONSTANTS DEFINITIONS *| @@ -21,6 +22,12 @@ contract PWNRevokedNonce is PWNHubAccessControl { */ bytes32 public immutable accessTag; + /** + * @notice PWN Hub contract. + * @dev Addresses revoking nonces on behalf of an owner need to have an access tag in PWN Hub. + */ + PWNHub public immutable hub; + /** * @notice Mapping of revoked nonces by an address. Every address has its own nonce space. * (owner => nonce space => nonce => is revoked) @@ -48,12 +55,24 @@ contract PWNRevokedNonce is PWNHubAccessControl { event NonceSpaceRevoked(address indexed owner, uint256 indexed nonceSpace); + /*----------------------------------------------------------*| + |* # MODIFIERS *| + |*----------------------------------------------------------*/ + + modifier onlyWithHubTag() { + if (!hub.hasTag(msg.sender, accessTag)) + revert AddressMissingHubTag({ addr: msg.sender, tag: accessTag }); + _; + } + + /*----------------------------------------------------------*| |* # CONSTRUCTOR *| |*----------------------------------------------------------*/ - constructor(address hub, bytes32 _accessTag) PWNHubAccessControl(hub) { + constructor(address _hub, bytes32 _accessTag) { accessTag = _accessTag; + hub = PWNHub(_hub); } @@ -86,7 +105,7 @@ contract PWNRevokedNonce is PWNHubAccessControl { * @param owner Owner address of a revoking nonce. * @param nonce Nonce to be revoked. */ - function revokeNonce(address owner, uint256 nonce) external onlyWithTag(accessTag) { + function revokeNonce(address owner, uint256 nonce) external onlyWithHubTag { _revokeNonce(owner, _nonceSpace[owner], nonce); } @@ -97,7 +116,7 @@ contract PWNRevokedNonce is PWNHubAccessControl { * @param nonceSpace Nonce space where a nonce will be revoked. * @param nonce Nonce to be revoked. */ - function revokeNonce(address owner, uint256 nonceSpace, uint256 nonce) external onlyWithTag(accessTag) { + function revokeNonce(address owner, uint256 nonceSpace, uint256 nonce) external onlyWithHubTag { _revokeNonce(owner, nonceSpace, nonce); } diff --git a/test/unit/PWNRevokedNonce.t.sol b/test/unit/PWNRevokedNonce.t.sol index d30de33..fccfc60 100644 --- a/test/unit/PWNRevokedNonce.t.sol +++ b/test/unit/PWNRevokedNonce.t.sol @@ -140,7 +140,7 @@ contract PWNRevokedNonce_RevokeNonceWithOwner_Test is PWNRevokedNonceTest { function testFuzz_shouldFail_whenCallerIsDoesNotHaveAccessTag(address caller) external { vm.assume(caller != accessEnabledAddress); - vm.expectRevert(abi.encodeWithSelector(CallerMissingHubTag.selector, accessTag)); + vm.expectRevert(abi.encodeWithSelector(AddressMissingHubTag.selector, caller, accessTag)); vm.prank(caller); revokedNonce.revokeNonce(caller, 1); } @@ -203,7 +203,7 @@ contract PWNRevokedNonce_RevokeNonceWithNonceSpaceAndOwner_Test is PWNRevokedNon function testFuzz_shouldFail_whenCallerIsDoesNotHaveAccessTag(address caller) external { vm.assume(caller != accessEnabledAddress); - vm.expectRevert(abi.encodeWithSelector(CallerMissingHubTag.selector, accessTag)); + vm.expectRevert(abi.encodeWithSelector(AddressMissingHubTag.selector, caller, accessTag)); vm.prank(caller); revokedNonce.revokeNonce(caller, 1, 1); } From 3f0f9dbd2b9d7e5e1364bb21209dd37a0f19f566 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Thu, 11 Apr 2024 12:01:58 -0400 Subject: [PATCH 077/129] ci: add tenderly as rpc endpoint --- foundry.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/foundry.toml b/foundry.toml index f9f046a..c1de7e3 100644 --- a/foundry.toml +++ b/foundry.toml @@ -21,4 +21,5 @@ base_goerli = "${BASE_GOERLI_URL}" cronos_testnet = "${CRONOS_TESTNET_URL}" mantle_testnet = "${MANTLE_TESTNET_URL}" +tenderly = "${TENDERLY_URL}" local = "${LOCAL_URL}" From f432db2c9c7d75160d8fc9accc6d08ebc9b11a76 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Thu, 11 Apr 2024 12:02:49 -0400 Subject: [PATCH 078/129] test: move mock tokens --- script/PWN.s.sol | 6 +++--- test/helper/{token => }/T1155.sol | 2 +- test/helper/{token => }/T20.sol | 2 +- test/helper/{token => }/T721.sol | 2 +- test/unit/PWNSimpleLoan.t.sol | 4 ++-- test/unit/PWNVault.t.sol | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) rename test/helper/{token => }/T1155.sol (78%) rename test/helper/{token => }/T20.sol (77%) rename test/helper/{token => }/T721.sol (75%) diff --git a/script/PWN.s.sol b/script/PWN.s.sol index 15ade38..5b91f45 100644 --- a/script/PWN.s.sol +++ b/script/PWN.s.sol @@ -23,9 +23,9 @@ import { PWNLOAN } from "@pwn/loan/token/PWNLOAN.sol"; import { PWNRevokedNonce } from "@pwn/nonce/PWNRevokedNonce.sol"; import { Deployments } from "@pwn/Deployments.sol"; -import { T20 } from "@pwn-test/helper/token/T20.sol"; -import { T721 } from "@pwn-test/helper/token/T721.sol"; -import { T1155 } from "@pwn-test/helper/token/T1155.sol"; +import { T20 } from "@pwn-test/helper/T20.sol"; +import { T721 } from "@pwn-test/helper/T721.sol"; +import { T1155 } from "@pwn-test/helper/T1155.sol"; library PWNContractDeployerSalt { diff --git a/test/helper/token/T1155.sol b/test/helper/T1155.sol similarity index 78% rename from test/helper/token/T1155.sol rename to test/helper/T1155.sol index 1d6e33d..d7144d9 100644 --- a/test/helper/token/T1155.sol +++ b/test/helper/T1155.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.16; -import "openzeppelin-contracts/contracts/token/ERC1155/ERC1155.sol"; +import { ERC1155 } from "openzeppelin-contracts/contracts/token/ERC1155/ERC1155.sol"; contract T1155 is ERC1155("uri://") { diff --git a/test/helper/token/T20.sol b/test/helper/T20.sol similarity index 77% rename from test/helper/token/T20.sol rename to test/helper/T20.sol index eda1245..aeeb6ca 100644 --- a/test/helper/token/T20.sol +++ b/test/helper/T20.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.16; -import "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; +import { ERC20 } from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; contract T20 is ERC20("ERC20", "ERC20") { diff --git a/test/helper/token/T721.sol b/test/helper/T721.sol similarity index 75% rename from test/helper/token/T721.sol rename to test/helper/T721.sol index 215d16c..0039faf 100644 --- a/test/helper/token/T721.sol +++ b/test/helper/T721.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.16; -import "openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; +import { ERC721 } from "openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; contract T721 is ERC721("ERC721", "ERC721") { diff --git a/test/unit/PWNSimpleLoan.t.sol b/test/unit/PWNSimpleLoan.t.sol index 7715362..1c3ab44 100644 --- a/test/unit/PWNSimpleLoan.t.sol +++ b/test/unit/PWNSimpleLoan.t.sol @@ -6,8 +6,8 @@ import "forge-std/Test.sol"; import { PWNSimpleLoan, PWNHubTags, Math, MultiToken, Permit } from "@pwn/loan/terms/simple/loan/PWNSimpleLoan.sol"; import "@pwn/PWNErrors.sol"; -import { T20 } from "@pwn-test/helper/token/T20.sol"; -import { T721 } from "@pwn-test/helper/token/T721.sol"; +import { T20 } from "@pwn-test/helper/T20.sol"; +import { T721 } from "@pwn-test/helper/T721.sol"; import { DummyPoolAdapter } from "@pwn-test/helper/DummyPoolAdapter.sol"; diff --git a/test/unit/PWNVault.t.sol b/test/unit/PWNVault.t.sol index 44e43da..bac2607 100644 --- a/test/unit/PWNVault.t.sol +++ b/test/unit/PWNVault.t.sol @@ -6,7 +6,7 @@ import "forge-std/Test.sol"; import { PWNVault, IERC165, IERC721Receiver, IERC1155Receiver, Permit, MultiToken } from "@pwn/loan/vault/PWNVault.sol"; import "@pwn/PWNErrors.sol"; -import { T721 } from "@pwn-test/helper/token/T721.sol"; +import { T721 } from "@pwn-test/helper/T721.sol"; contract PWNVaultHarness is PWNVault { From 0c8ece85f05157cffd0c87934d8d38b9aeeac818 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Thu, 11 Apr 2024 12:03:33 -0400 Subject: [PATCH 079/129] test: move deployment test contract --- test/{helper => }/DeploymentTest.t.sol | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/{helper => }/DeploymentTest.t.sol (100%) diff --git a/test/helper/DeploymentTest.t.sol b/test/DeploymentTest.t.sol similarity index 100% rename from test/helper/DeploymentTest.t.sol rename to test/DeploymentTest.t.sol From 575eb0a8bfc01d2ee06b87d371a44109e439cb41 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Thu, 11 Apr 2024 12:04:20 -0400 Subject: [PATCH 080/129] test: update integration tests --- test/integration/BaseIntegrationTest.t.sol | 137 ++++--- .../PWNProtocolIntegrity.t.sol | 74 ++-- .../PWNSimpleLoanIntegration.t.sol | 360 +++++++++++++++++ .../contracts/PWNSimpleLoanIntegration.t.sol | 370 ------------------ .../PWNSimpleLoanSimpleOfferIntegration.t.sol | 73 ---- ...WNSimpleLoanSimpleRequestIntegration.t.sol | 73 ---- 6 files changed, 489 insertions(+), 598 deletions(-) rename test/integration/{contracts => }/PWNProtocolIntegrity.t.sol (53%) create mode 100644 test/integration/PWNSimpleLoanIntegration.t.sol delete mode 100644 test/integration/contracts/PWNSimpleLoanIntegration.t.sol delete mode 100644 test/integration/contracts/PWNSimpleLoanSimpleOfferIntegration.t.sol delete mode 100644 test/integration/contracts/PWNSimpleLoanSimpleRequestIntegration.t.sol diff --git a/test/integration/BaseIntegrationTest.t.sol b/test/integration/BaseIntegrationTest.t.sol index 685aa75..f07fa80 100644 --- a/test/integration/BaseIntegrationTest.t.sol +++ b/test/integration/BaseIntegrationTest.t.sol @@ -3,12 +3,14 @@ pragma solidity 0.8.16; import "forge-std/Test.sol"; -import "MultiToken/MultiToken.sol"; +import { MultiToken } from "MultiToken/MultiToken.sol"; -import "@pwn-test/helper/token/T20.sol"; -import "@pwn-test/helper/token/T721.sol"; -import "@pwn-test/helper/token/T1155.sol"; -import "@pwn-test/helper/DeploymentTest.t.sol"; +import { Permit } from "@pwn/loan/vault/Permit.sol"; + +import { T20 } from "@pwn-test/helper/T20.sol"; +import { T721 } from "@pwn-test/helper/T721.sol"; +import { T1155 } from "@pwn-test/helper/T1155.sol"; +import "@pwn-test/DeploymentTest.t.sol"; abstract contract BaseIntegrationTest is DeploymentTest { @@ -22,8 +24,7 @@ abstract contract BaseIntegrationTest is DeploymentTest { address lender = vm.addr(lenderPK); uint256 borrowerPK = uint256(888); address borrower = vm.addr(borrowerPK); - uint256 nonce = uint256(keccak256("nonce_1")); - PWNSimpleLoanSimpleOffer.Offer defaultOffer; + PWNSimpleLoanSimpleProposal.Proposal simpleProposal; function setUp() public override { super.setUp(); @@ -35,20 +36,28 @@ abstract contract BaseIntegrationTest is DeploymentTest { loanAsset = new T20(); // Default offer - defaultOffer = PWNSimpleLoanSimpleOffer.Offer({ + simpleProposal = PWNSimpleLoanSimpleProposal.Proposal({ collateralCategory: MultiToken.Category.ERC1155, collateralAddress: address(t1155), collateralId: 42, collateralAmount: 10e18, - loanAssetAddress: address(loanAsset), - loanAmount: 100e18, - loanYield: 10e18, + checkCollateralStateFingerprint: false, + collateralStateFingerprint: bytes32(0), + creditAddress: address(loanAsset), + creditAmount: 100e18, + availableCreditLimit: 0, + fixedInterestAmount: 10e18, + accruingInterestAPR: 0, duration: 3600, - expiration: 0, - allowedBorrower: borrower, - lender: lender, - isPersistent: false, - nonce: nonce + expiration: uint40(block.timestamp + 7 days), + allowedAcceptor: borrower, + proposer: lender, + proposerSpecHash: deployment.simpleLoan.getLenderSpecHash(PWNSimpleLoan.LenderSpec(lender)), + isOffer: true, + refinancingLoanId: 0, + nonceSpace: 0, + nonce: 0, + loanContract: address(deployment.simpleLoan) }); } @@ -58,42 +67,42 @@ abstract contract BaseIntegrationTest is DeploymentTest { return abi.encodePacked(r, s, v); } - // Create from offer + // Create from proposal function _createERC20Loan() internal returns (uint256) { - // Offer - defaultOffer.collateralCategory = MultiToken.Category.ERC20; - defaultOffer.collateralAddress = address(t20); - defaultOffer.collateralId = 0; - defaultOffer.collateralAmount = 10e18; + // Proposal + simpleProposal.collateralCategory = MultiToken.Category.ERC20; + simpleProposal.collateralAddress = address(t20); + simpleProposal.collateralId = 0; + simpleProposal.collateralAmount = 10e18; // Mint initial state t20.mint(borrower, 10e18); // Approve collateral vm.prank(borrower); - t20.approve(address(simpleLoan), 10e18); + t20.approve(address(deployment.simpleLoan), 10e18); // Create LOAN - return _createLoan(defaultOffer, ""); + return _createLoan(simpleProposal, ""); } function _createERC721Loan() internal returns (uint256) { - // Offer - defaultOffer.collateralCategory = MultiToken.Category.ERC721; - defaultOffer.collateralAddress = address(t721); - defaultOffer.collateralId = 42; - defaultOffer.collateralAmount = 0; + // Proposal + simpleProposal.collateralCategory = MultiToken.Category.ERC721; + simpleProposal.collateralAddress = address(t721); + simpleProposal.collateralId = 42; + simpleProposal.collateralAmount = 0; // Mint initial state t721.mint(borrower, 42); // Approve collateral vm.prank(borrower); - t721.approve(address(simpleLoan), 42); + t721.approve(address(deployment.simpleLoan), 42); // Create LOAN - return _createLoan(defaultOffer, ""); + return _createLoan(simpleProposal, ""); } function _createERC1155Loan() internal returns (uint256) { @@ -102,47 +111,64 @@ abstract contract BaseIntegrationTest is DeploymentTest { function _createERC1155LoanFailing(bytes memory revertData) internal returns (uint256) { // Offer - defaultOffer.collateralCategory = MultiToken.Category.ERC1155; - defaultOffer.collateralAddress = address(t1155); - defaultOffer.collateralId = 42; - defaultOffer.collateralAmount = 10e18; + simpleProposal.collateralCategory = MultiToken.Category.ERC1155; + simpleProposal.collateralAddress = address(t1155); + simpleProposal.collateralId = 42; + simpleProposal.collateralAmount = 10e18; // Mint initial state t1155.mint(borrower, 42, 10e18); // Approve collateral vm.prank(borrower); - t1155.setApprovalForAll(address(simpleLoan), true); + t1155.setApprovalForAll(address(deployment.simpleLoan), true); // Create LOAN - return _createLoan(defaultOffer, revertData); + return _createLoan(simpleProposal, revertData); } - function _createLoan(PWNSimpleLoanSimpleOffer.Offer memory _offer, bytes memory revertData) private returns (uint256) { - // Sign offer - bytes memory signature = _sign(lenderPK, simpleLoanSimpleOffer.getOfferHash(_offer)); + function _createLoan(PWNSimpleLoanSimpleProposal.Proposal memory _proposal, bytes memory revertData) private returns (uint256) { + // Sign proposal + bytes memory signature = _sign(lenderPK, deployment.simpleLoanSimpleProposal.getProposalHash(_proposal)); // Mint initial state loanAsset.mint(lender, 100e18); // Approve loan asset vm.prank(lender); - loanAsset.approve(address(simpleLoan), 100e18); + loanAsset.approve(address(deployment.simpleLoan), 100e18); - // Loan factory data (need for vm.prank to work properly when creating a loan) - bytes memory loanTermsFactoryData = simpleLoanSimpleOffer.encodeLoanTermsFactoryData(_offer); + // Proposal data (need for vm.prank to work properly when creating a loan) + bytes memory proposalData = deployment.simpleLoanSimpleProposal.encodeProposalData(_proposal); // Create LOAN if (keccak256(revertData) != keccak256("")) { vm.expectRevert(revertData); } vm.prank(borrower); - return simpleLoan.createLOAN({ - loanTermsFactoryContract: address(simpleLoanSimpleOffer), - loanTermsFactoryData: loanTermsFactoryData, - signature: signature, - loanAssetPermit: "", - collateralPermit: "" + return deployment.simpleLoan.createLOAN({ + proposalSpec: PWNSimpleLoan.ProposalSpec({ + proposalContract: address(deployment.simpleLoanSimpleProposal), + proposalData: proposalData, + proposalInclusionProof: new bytes32[](0), + signature: signature + }), + lenderSpec: PWNSimpleLoan.LenderSpec({ + sourceOfFunds: lender + }), + callerSpec: PWNSimpleLoan.CallerSpec({ + refinancingLoanId: 0, + revokeNonce: false, + nonce: 0, + permit: Permit({ + asset: address(0), + owner: address(0), + amount: 0, + deadline: 0, + v: 0, r: 0, s: 0 + }) + }), + extra: "" }); } @@ -158,14 +184,23 @@ abstract contract BaseIntegrationTest is DeploymentTest { // Approve loan asset vm.prank(borrower); - loanAsset.approve(address(simpleLoan), 110e18); + loanAsset.approve(address(deployment.simpleLoan), 110e18); // Repay loan if (keccak256(revertData) != keccak256("")) { vm.expectRevert(revertData); } vm.prank(borrower); - simpleLoan.repayLOAN(loanId, ""); + deployment.simpleLoan.repayLOAN({ + loanId: loanId, + permit: Permit({ + asset: address(0), + owner: address(0), + amount: 0, + deadline: 0, + v: 0, r: 0, s: 0 + }) + }); } } \ No newline at end of file diff --git a/test/integration/contracts/PWNProtocolIntegrity.t.sol b/test/integration/PWNProtocolIntegrity.t.sol similarity index 53% rename from test/integration/contracts/PWNProtocolIntegrity.t.sol rename to test/integration/PWNProtocolIntegrity.t.sol index 8067b04..5dc2bac 100644 --- a/test/integration/contracts/PWNProtocolIntegrity.t.sol +++ b/test/integration/PWNProtocolIntegrity.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.16; import "forge-std/Test.sol"; -import "@pwn/hub/PWNHubTags.sol"; +import { PWNHubTags } from "@pwn/hub/PWNHubTags.sol"; import "@pwn/PWNErrors.sol"; import "@pwn-test/integration/BaseIntegrationTest.t.sol"; @@ -13,12 +13,12 @@ contract PWNProtocolIntegrityTest is BaseIntegrationTest { function test_shouldFailToCreateLOAN_whenLoanContractNotActive() external { // Remove ACTIVE_LOAN tag - vm.prank(protocolSafe); - hub.setTag(address(simpleLoan), PWNHubTags.ACTIVE_LOAN, false); + vm.prank(deployment.protocolSafe); + deployment.hub.setTag(address(deployment.simpleLoan), PWNHubTags.ACTIVE_LOAN, false); // Try to create LOAN _createERC1155LoanFailing( - abi.encodeWithSelector(CallerMissingHubTag.selector, PWNHubTags.ACTIVE_LOAN) + abi.encodeWithSelector(AddressMissingHubTag.selector, address(deployment.simpleLoan), PWNHubTags.ACTIVE_LOAN) ); } @@ -27,23 +27,23 @@ contract PWNProtocolIntegrityTest is BaseIntegrationTest { uint256 loanId = _createERC1155Loan(); // Remove ACTIVE_LOAN tag - vm.prank(protocolSafe); - hub.setTag(address(simpleLoan), PWNHubTags.ACTIVE_LOAN, false); + vm.prank(deployment.protocolSafe); + deployment.hub.setTag(address(deployment.simpleLoan), PWNHubTags.ACTIVE_LOAN, false); // Repay loan directly to original lender _repayLoan(loanId); // Assert final state vm.expectRevert("ERC721: invalid token ID"); - loanToken.ownerOf(loanId); + deployment.loanToken.ownerOf(loanId); assertEq(loanAsset.balanceOf(lender), 110e18); assertEq(loanAsset.balanceOf(borrower), 0); - assertEq(loanAsset.balanceOf(address(simpleLoan)), 0); + assertEq(loanAsset.balanceOf(address(deployment.simpleLoan)), 0); assertEq(t1155.balanceOf(lender, 42), 0); assertEq(t1155.balanceOf(borrower, 42), 10e18); - assertEq(t1155.balanceOf(address(simpleLoan), 42), 0); + assertEq(t1155.balanceOf(address(deployment.simpleLoan), 42), 0); } function test_shouldRepayLOAN_whenLoanContractNotActive_whenOriginalLenderIsNotLOANOwner() external { @@ -54,27 +54,27 @@ contract PWNProtocolIntegrityTest is BaseIntegrationTest { // Transfer loan to another lender vm.prank(lender); - loanToken.transferFrom(lender, lender2, loanId); + deployment.loanToken.transferFrom(lender, lender2, loanId); // Remove ACTIVE_LOAN tag - vm.prank(protocolSafe); - hub.setTag(address(simpleLoan), PWNHubTags.ACTIVE_LOAN, false); + vm.prank(deployment.protocolSafe); + deployment.hub.setTag(address(deployment.simpleLoan), PWNHubTags.ACTIVE_LOAN, false); // Repay loan directly to original lender _repayLoan(loanId); // Assert final state - assertEq(loanToken.ownerOf(loanId), lender2); + assertEq(deployment.loanToken.ownerOf(loanId), lender2); assertEq(loanAsset.balanceOf(lender), 0); assertEq(loanAsset.balanceOf(lender2), 0); assertEq(loanAsset.balanceOf(borrower), 0); - assertEq(loanAsset.balanceOf(address(simpleLoan)), 110e18); + assertEq(loanAsset.balanceOf(address(deployment.simpleLoan)), 110e18); assertEq(t1155.balanceOf(lender, 42), 0); assertEq(t1155.balanceOf(lender2, 42), 0); assertEq(t1155.balanceOf(borrower, 42), 10e18); - assertEq(t1155.balanceOf(address(simpleLoan), 42), 0); + assertEq(t1155.balanceOf(address(deployment.simpleLoan), 42), 0); } function test_shouldClaimRepaidLOAN_whenLoanContractNotActive() external { @@ -85,54 +85,66 @@ contract PWNProtocolIntegrityTest is BaseIntegrationTest { // Transfer loan to another lender vm.prank(lender); - loanToken.transferFrom(lender, lender2, loanId); + deployment.loanToken.transferFrom(lender, lender2, loanId); // Repay loan _repayLoan(loanId); // Remove ACTIVE_LOAN tag - vm.prank(protocolSafe); - hub.setTag(address(simpleLoan), PWNHubTags.ACTIVE_LOAN, false); + vm.prank(deployment.protocolSafe); + deployment.hub.setTag(address(deployment.simpleLoan), PWNHubTags.ACTIVE_LOAN, false); // Claim loan vm.prank(lender2); - simpleLoan.claimLOAN(loanId); + deployment.simpleLoan.claimLOAN(loanId); // Assert final state vm.expectRevert("ERC721: invalid token ID"); - loanToken.ownerOf(loanId); + deployment.loanToken.ownerOf(loanId); assertEq(loanAsset.balanceOf(lender), 0); assertEq(loanAsset.balanceOf(lender2), 110e18); assertEq(loanAsset.balanceOf(borrower), 0); - assertEq(loanAsset.balanceOf(address(simpleLoan)), 0); + assertEq(loanAsset.balanceOf(address(deployment.simpleLoan)), 0); assertEq(t1155.balanceOf(lender, 42), 0); assertEq(t1155.balanceOf(lender2, 42), 0); assertEq(t1155.balanceOf(borrower, 42), 10e18); - assertEq(t1155.balanceOf(address(simpleLoan), 42), 0); + assertEq(t1155.balanceOf(address(deployment.simpleLoan), 42), 0); } function test_shouldFailToCreateLOANTerms_whenCallerIsNotActiveLoan() external { // Remove ACTIVE_LOAN tag - vm.prank(protocolSafe); - hub.setTag(address(simpleLoan), PWNHubTags.ACTIVE_LOAN, false); + vm.prank(deployment.protocolSafe); + deployment.hub.setTag(address(deployment.simpleLoan), PWNHubTags.ACTIVE_LOAN, false); + + bytes memory proposalData = deployment.simpleLoanSimpleProposal.encodeProposalData(simpleProposal); vm.expectRevert( - abi.encodeWithSelector(CallerMissingHubTag.selector, PWNHubTags.ACTIVE_LOAN) + abi.encodeWithSelector( + AddressMissingHubTag.selector, address(deployment.simpleLoan), PWNHubTags.ACTIVE_LOAN + ) ); - vm.prank(address(simpleLoan)); - simpleLoanSimpleOffer.createLOANTerms(borrower, "", ""); // Offer data are not important in this test + vm.prank(address(deployment.simpleLoan)); + deployment.simpleLoanSimpleProposal.acceptProposal({ + acceptor: borrower, + refinancingLoanId: 0, + proposalData: proposalData, + proposalInclusionProof: new bytes32[](0), + signature: "" + }); } function test_shouldFailToCreateLOAN_whenPassingInvalidTermsFactoryContract() external { - // Remove SIMPLE_LOAN_TERMS_FACTORY tag - vm.prank(protocolSafe); - hub.setTag(address(simpleLoanSimpleOffer), PWNHubTags.SIMPLE_LOAN_TERMS_FACTORY, false); + // Remove LOAN_PROPOSAL tag + vm.prank(deployment.protocolSafe); + deployment.hub.setTag(address(deployment.simpleLoanSimpleProposal), PWNHubTags.LOAN_PROPOSAL, false); // Try to create LOAN _createERC1155LoanFailing( - abi.encodeWithSelector(CallerMissingHubTag.selector, PWNHubTags.SIMPLE_LOAN_TERMS_FACTORY) + abi.encodeWithSelector( + AddressMissingHubTag.selector, address(deployment.simpleLoanSimpleProposal), PWNHubTags.LOAN_PROPOSAL + ) ); } diff --git a/test/integration/PWNSimpleLoanIntegration.t.sol b/test/integration/PWNSimpleLoanIntegration.t.sol new file mode 100644 index 0000000..7db72a4 --- /dev/null +++ b/test/integration/PWNSimpleLoanIntegration.t.sol @@ -0,0 +1,360 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.16; + +import "forge-std/Test.sol"; + +import "@pwn/PWNErrors.sol"; + +import "@pwn-test/integration/BaseIntegrationTest.t.sol"; + + +contract PWNSimpleLoanIntegrationTest is BaseIntegrationTest { + + // Create LOAN + + function test_shouldCreateLOAN_fromSimpleProposal() external { + PWNSimpleLoanSimpleProposal.Proposal memory proposal = PWNSimpleLoanSimpleProposal.Proposal({ + collateralCategory: MultiToken.Category.ERC1155, + collateralAddress: address(t1155), + collateralId: 42, + collateralAmount: 10e18, + checkCollateralStateFingerprint: false, + collateralStateFingerprint: bytes32(0), + creditAddress: address(loanAsset), + creditAmount: 100e18, + availableCreditLimit: 0, + fixedInterestAmount: 10e18, + accruingInterestAPR: 0, + duration: 3600, + expiration: uint40(block.timestamp + 7 days), + allowedAcceptor: borrower, + proposer: lender, + proposerSpecHash: deployment.simpleLoan.getLenderSpecHash(PWNSimpleLoan.LenderSpec(lender)), + isOffer: true, + refinancingLoanId: 0, + nonceSpace: 0, + nonce: 0, + loanContract: address(deployment.simpleLoan) + }); + + // Mint initial state + t1155.mint(borrower, 42, 10e18); + + // Approve collateral + vm.prank(borrower); + t1155.setApprovalForAll(address(deployment.simpleLoan), true); + + // Sign proposal + bytes memory signature = _sign(lenderPK, deployment.simpleLoanSimpleProposal.getProposalHash(proposal)); + + // Mint initial state + loanAsset.mint(lender, 100e18); + + // Approve loan asset + vm.prank(lender); + loanAsset.approve(address(deployment.simpleLoan), 100e18); + + // Proposal data (need for vm.prank to work properly when creating a loan) + bytes memory proposalData = deployment.simpleLoanSimpleProposal.encodeProposalData(proposal); + + // Create LOAN + vm.prank(borrower); + uint256 loanId = deployment.simpleLoan.createLOAN({ + proposalSpec: PWNSimpleLoan.ProposalSpec({ + proposalContract: address(deployment.simpleLoanSimpleProposal), + proposalData: proposalData, + proposalInclusionProof: new bytes32[](0), + signature: signature + }), + lenderSpec: PWNSimpleLoan.LenderSpec({ + sourceOfFunds: lender + }), + callerSpec: PWNSimpleLoan.CallerSpec({ + refinancingLoanId: 0, + revokeNonce: false, + nonce: 0, + permit: Permit({ + asset: address(0), + owner: address(0), + amount: 0, + deadline: 0, + v: 0, r: 0, s: 0 + }) + }), + extra: "" + }); + + // Assert final state + assertEq(deployment.loanToken.ownerOf(loanId), lender); + + assertEq(loanAsset.balanceOf(lender), 0); + assertEq(loanAsset.balanceOf(borrower), 100e18); + assertEq(loanAsset.balanceOf(address(deployment.simpleLoan)), 0); + + assertEq(t1155.balanceOf(lender, 42), 0); + assertEq(t1155.balanceOf(borrower, 42), 0); + assertEq(t1155.balanceOf(address(deployment.simpleLoan), 42), 10e18); + + assertEq(deployment.revokedNonce.isNonceRevoked(lender, proposal.nonceSpace, proposal.nonce), true); + assertEq(deployment.loanToken.loanContract(loanId), address(deployment.simpleLoan)); + } + + function test_shouldCreateLOAN_fromListProposal() external { + bytes32 id1Hash = keccak256(abi.encodePacked(uint256(52))); + bytes32 id2Hash = keccak256(abi.encodePacked(uint256(42))); + bytes32 collateralIdsWhitelistMerkleRoot = keccak256(abi.encodePacked(id1Hash, id2Hash)); + + PWNSimpleLoanListProposal.Proposal memory proposal = PWNSimpleLoanListProposal.Proposal({ + collateralCategory: MultiToken.Category.ERC1155, + collateralAddress: address(t1155), + collateralIdsWhitelistMerkleRoot: collateralIdsWhitelistMerkleRoot, + collateralAmount: 10e18, + checkCollateralStateFingerprint: false, + collateralStateFingerprint: bytes32(0), + creditAddress: address(loanAsset), + creditAmount: 100e18, + availableCreditLimit: 0, + fixedInterestAmount: 10e18, + accruingInterestAPR: 0, + duration: 3600, + expiration: uint40(block.timestamp + 7 days), + allowedAcceptor: borrower, + proposer: lender, + proposerSpecHash: deployment.simpleLoan.getLenderSpecHash(PWNSimpleLoan.LenderSpec(lender)), + isOffer: true, + refinancingLoanId: 0, + nonceSpace: 0, + nonce: 0, + loanContract: address(deployment.simpleLoan) + }); + + PWNSimpleLoanListProposal.ProposalValues memory proposalValues = PWNSimpleLoanListProposal.ProposalValues({ + collateralId: 42, + merkleInclusionProof: new bytes32[](1) + }); + proposalValues.merkleInclusionProof[0] = id1Hash; + + // Mint initial state + t1155.mint(borrower, 42, 10e18); + + // Approve collateral + vm.prank(borrower); + t1155.setApprovalForAll(address(deployment.simpleLoan), true); + + // Sign proposal + bytes memory signature = _sign(lenderPK, deployment.simpleLoanListProposal.getProposalHash(proposal)); + + // Mint initial state + loanAsset.mint(lender, 100e18); + + // Approve loan asset + vm.prank(lender); + loanAsset.approve(address(deployment.simpleLoan), 100e18); + + // Proposal data (need for vm.prank to work properly when creating a loan) + bytes memory proposalData = deployment.simpleLoanListProposal.encodeProposalData(proposal, proposalValues); + + // Create LOAN + vm.prank(borrower); + uint256 loanId = deployment.simpleLoan.createLOAN({ + proposalSpec: PWNSimpleLoan.ProposalSpec({ + proposalContract: address(deployment.simpleLoanListProposal), + proposalData: proposalData, + proposalInclusionProof: new bytes32[](0), + signature: signature + }), + lenderSpec: PWNSimpleLoan.LenderSpec({ + sourceOfFunds: lender + }), + callerSpec: PWNSimpleLoan.CallerSpec({ + refinancingLoanId: 0, + revokeNonce: false, + nonce: 0, + permit: Permit({ + asset: address(0), + owner: address(0), + amount: 0, + deadline: 0, + v: 0, r: 0, s: 0 + }) + }), + extra: "" + }); + + // Assert final state + assertEq(deployment.loanToken.ownerOf(loanId), lender); + + assertEq(loanAsset.balanceOf(lender), 0); + assertEq(loanAsset.balanceOf(borrower), 100e18); + assertEq(loanAsset.balanceOf(address(deployment.simpleLoan)), 0); + + assertEq(t1155.balanceOf(lender, 42), 0); + assertEq(t1155.balanceOf(borrower, 42), 0); + assertEq(t1155.balanceOf(address(deployment.simpleLoan), 42), 10e18); + + assertEq(deployment.revokedNonce.isNonceRevoked(lender, proposal.nonceSpace, proposal.nonce), true); + assertEq(deployment.loanToken.loanContract(loanId), address(deployment.simpleLoan)); + } + + // TODO: fungible, dutch auction + + + // Different collateral types + + function test_shouldCreateLOAN_withERC20Collateral() external { + // Create LOAN + uint256 loanId = _createERC20Loan(); + + // Assert final state + assertEq(deployment.loanToken.ownerOf(loanId), lender); + + assertEq(loanAsset.balanceOf(lender), 0); + assertEq(loanAsset.balanceOf(borrower), 100e18); + assertEq(loanAsset.balanceOf(address(deployment.simpleLoan)), 0); + + assertEq(t20.balanceOf(lender), 0); + assertEq(t20.balanceOf(borrower), 0); + assertEq(t20.balanceOf(address(deployment.simpleLoan)), 10e18); + + assertEq(deployment.revokedNonce.isNonceRevoked(lender, simpleProposal.nonceSpace, simpleProposal.nonce), true); + assertEq(deployment.loanToken.loanContract(loanId), address(deployment.simpleLoan)); + } + + function test_shouldCreateLOAN_withERC721Collateral() external { + // Create LOAN + uint256 loanId = _createERC721Loan(); + + // Assert final state + assertEq(deployment.loanToken.ownerOf(loanId), lender); + + assertEq(loanAsset.balanceOf(lender), 0); + assertEq(loanAsset.balanceOf(borrower), 100e18); + assertEq(loanAsset.balanceOf(address(deployment.simpleLoan)), 0); + + assertEq(t721.ownerOf(42), address(deployment.simpleLoan)); + + assertEq(deployment.revokedNonce.isNonceRevoked(lender, simpleProposal.nonceSpace, simpleProposal.nonce), true); + assertEq(deployment.loanToken.loanContract(loanId), address(deployment.simpleLoan)); + } + + function test_shouldCreateLOAN_withERC1155Collateral() external { + // Create LOAN + uint256 loanId = _createERC1155Loan(); + + // Assert final state + assertEq(deployment.loanToken.ownerOf(loanId), lender); + + assertEq(loanAsset.balanceOf(lender), 0); + assertEq(loanAsset.balanceOf(borrower), 100e18); + assertEq(loanAsset.balanceOf(address(deployment.simpleLoan)), 0); + + assertEq(t1155.balanceOf(lender, 42), 0); + assertEq(t1155.balanceOf(borrower, 42), 0); + assertEq(t1155.balanceOf(address(deployment.simpleLoan), 42), 10e18); + + assertEq(deployment.revokedNonce.isNonceRevoked(lender, simpleProposal.nonceSpace, simpleProposal.nonce), true); + assertEq(deployment.loanToken.loanContract(loanId), address(deployment.simpleLoan)); + } + + function test_shouldCreateLOAN_withCryptoKittiesCollateral() external { + // TODO: + } + + + // Repay LOAN + + function test_shouldRepayLoan_whenNotExpired_whenOriginalLenderIsLOANOwner() external { + // Create LOAN + uint256 loanId = _createERC1155Loan(); + + // Repay loan + _repayLoan(loanId); + + // Assert final state + vm.expectRevert("ERC721: invalid token ID"); + deployment.loanToken.ownerOf(loanId); + + assertEq(loanAsset.balanceOf(lender), 110e18); + assertEq(loanAsset.balanceOf(borrower), 0); + assertEq(loanAsset.balanceOf(address(deployment.simpleLoan)), 0); + + assertEq(t1155.balanceOf(lender, 42), 0); + assertEq(t1155.balanceOf(borrower, 42), 10e18); + assertEq(t1155.balanceOf(address(deployment.simpleLoan), 42), 0); + } + + function test_shouldFailToRepayLoan_whenLOANExpired() external { + // Create LOAN + uint256 loanId = _createERC1155Loan(); + + // Default on a loan + uint256 expiration = block.timestamp + uint256(simpleProposal.duration); + vm.warp(expiration); + + // Try to repay loan + _repayLoanFailing( + loanId, + abi.encodeWithSelector(LoanDefaulted.selector, uint40(expiration)) + ); + } + + + // Claim LOAN + + function test_shouldClaimRepaidLOAN_whenOriginalLenderIsNotLOANOwner() external { + address lender2 = makeAddr("lender2"); + + // Create LOAN + uint256 loanId = _createERC1155Loan(); + + // Transfer loan to another lender + vm.prank(lender); + deployment.loanToken.transferFrom(lender, lender2, loanId); + + // Repay loan + _repayLoan(loanId); + + // Claim loan + vm.prank(lender2); + deployment.simpleLoan.claimLOAN(loanId); + + // Assert final state + vm.expectRevert("ERC721: invalid token ID"); + deployment.loanToken.ownerOf(loanId); + + assertEq(loanAsset.balanceOf(lender), 0); + assertEq(loanAsset.balanceOf(lender2), 110e18); + assertEq(loanAsset.balanceOf(borrower), 0); + assertEq(loanAsset.balanceOf(address(deployment.simpleLoan)), 0); + + assertEq(t1155.balanceOf(lender, 42), 0); + assertEq(t1155.balanceOf(lender2, 42), 0); + assertEq(t1155.balanceOf(borrower, 42), 10e18); + assertEq(t1155.balanceOf(address(deployment.simpleLoan), 42), 0); + } + + function test_shouldClaimDefaultedLOAN() external { + // Create LOAN + uint256 loanId = _createERC1155Loan(); + + // Loan defaulted + vm.warp(block.timestamp + uint256(simpleProposal.duration)); + + // Claim defaulted loan + vm.prank(lender); + deployment.simpleLoan.claimLOAN(loanId); + + // Assert final state + vm.expectRevert("ERC721: invalid token ID"); + deployment.loanToken.ownerOf(loanId); + + assertEq(loanAsset.balanceOf(lender), 0); + assertEq(loanAsset.balanceOf(borrower), 100e18); + assertEq(loanAsset.balanceOf(address(deployment.simpleLoan)), 0); + + assertEq(t1155.balanceOf(lender, 42), 10e18); + assertEq(t1155.balanceOf(borrower, 42), 0); + assertEq(t1155.balanceOf(address(deployment.simpleLoan), 42), 0); + } + +} diff --git a/test/integration/contracts/PWNSimpleLoanIntegration.t.sol b/test/integration/contracts/PWNSimpleLoanIntegration.t.sol deleted file mode 100644 index 5985958..0000000 --- a/test/integration/contracts/PWNSimpleLoanIntegration.t.sol +++ /dev/null @@ -1,370 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity 0.8.16; - -import "forge-std/Test.sol"; - -import "@pwn/PWNErrors.sol"; - -import "@pwn-test/integration/BaseIntegrationTest.t.sol"; - - -contract PWNSimpleLoanIntegrationTest is BaseIntegrationTest { - - // Create LOAN - - function test_shouldCreateLOAN_fromSimpleOffer() external { - PWNSimpleLoanSimpleOffer.Offer memory offer = PWNSimpleLoanSimpleOffer.Offer({ - collateralCategory: MultiToken.Category.ERC1155, - collateralAddress: address(t1155), - collateralId: 42, - collateralAmount: 10e18, - loanAssetAddress: address(loanAsset), - loanAmount: 100e18, - loanYield: 10e18, - duration: 3600, - expiration: 0, - allowedBorrower: borrower, - lender: lender, - isPersistent: false, - nonce: nonce - }); - - // Mint initial state - t1155.mint(borrower, 42, 10e18); - - // Approve collateral - vm.prank(borrower); - t1155.setApprovalForAll(address(simpleLoan), true); - - // Sign offer - bytes memory signature = _sign(lenderPK, simpleLoanSimpleOffer.getOfferHash(offer)); - - // Mint initial state - loanAsset.mint(lender, 100e18); - - // Approve loan asset - vm.prank(lender); - loanAsset.approve(address(simpleLoan), 100e18); - - // Loan factory data (need for vm.prank to work properly when creating a loan) - bytes memory loanTermsFactoryData = simpleLoanSimpleOffer.encodeLoanTermsFactoryData(offer); - - // Create LOAN - vm.prank(borrower); - uint256 loanId = simpleLoan.createLOAN({ - loanTermsFactoryContract: address(simpleLoanSimpleOffer), - loanTermsFactoryData: loanTermsFactoryData, - signature: signature, - loanAssetPermit: "", - collateralPermit: "" - }); - - // Assert final state - assertEq(loanToken.ownerOf(loanId), lender); - - assertEq(loanAsset.balanceOf(lender), 0); - assertEq(loanAsset.balanceOf(borrower), 100e18); - assertEq(loanAsset.balanceOf(address(simpleLoan)), 0); - - assertEq(t1155.balanceOf(lender, 42), 0); - assertEq(t1155.balanceOf(borrower, 42), 0); - assertEq(t1155.balanceOf(address(simpleLoan), 42), 10e18); - - assertEq(revokedOfferNonce.isNonceRevoked(lender, nonce), true); - assertEq(loanToken.loanContract(loanId), address(simpleLoan)); - } - - function test_shouldCreateLOAN_fromListOffer() external { - bytes32 id1Hash = keccak256(abi.encodePacked(uint256(52))); - bytes32 id2Hash = keccak256(abi.encodePacked(uint256(42))); - bytes32 collateralIdsWhitelistMerkleRoot = keccak256(abi.encodePacked(id1Hash, id2Hash)); - - PWNSimpleLoanListOffer.Offer memory offer = PWNSimpleLoanListOffer.Offer({ - collateralCategory: MultiToken.Category.ERC1155, - collateralAddress: address(t1155), - collateralIdsWhitelistMerkleRoot: collateralIdsWhitelistMerkleRoot, - collateralAmount: 10e18, - loanAssetAddress: address(loanAsset), - loanAmount: 100e18, - loanYield: 10e18, - duration: 3600, - expiration: 0, - allowedBorrower: borrower, - lender: lender, - isPersistent: false, - nonce: nonce - }); - - PWNSimpleLoanListOffer.OfferValues memory offerValues = PWNSimpleLoanListOffer.OfferValues({ - collateralId: 42, - merkleInclusionProof: new bytes32[](1) - }); - offerValues.merkleInclusionProof[0] = id1Hash; - - // Mint initial state - t1155.mint(borrower, 42, 10e18); - - // Approve collateral - vm.prank(borrower); - t1155.setApprovalForAll(address(simpleLoan), true); - - // Sign offer - bytes memory signature = _sign(lenderPK, simpleLoanListOffer.getOfferHash(offer)); - - // Mint initial state - loanAsset.mint(lender, 100e18); - - // Approve loan asset - vm.prank(lender); - loanAsset.approve(address(simpleLoan), 100e18); - - // Loan factory data (need for vm.prank to work properly when creating a loan) - bytes memory loanTermsFactoryData = simpleLoanListOffer.encodeLoanTermsFactoryData(offer, offerValues); - - // Create LOAN - vm.prank(borrower); - uint256 loanId = simpleLoan.createLOAN({ - loanTermsFactoryContract: address(simpleLoanListOffer), - loanTermsFactoryData: loanTermsFactoryData, - signature: signature, - loanAssetPermit: "", - collateralPermit: "" - }); - - // Assert final state - assertEq(loanToken.ownerOf(loanId), lender); - - assertEq(loanAsset.balanceOf(lender), 0); - assertEq(loanAsset.balanceOf(borrower), 100e18); - assertEq(loanAsset.balanceOf(address(simpleLoan)), 0); - - assertEq(t1155.balanceOf(lender, 42), 0); - assertEq(t1155.balanceOf(borrower, 42), 0); - assertEq(t1155.balanceOf(address(simpleLoan), 42), 10e18); - - assertEq(revokedOfferNonce.isNonceRevoked(lender, nonce), true); - assertEq(loanToken.loanContract(loanId), address(simpleLoan)); - } - - function test_shouldCreateLOAN_fromSimpleRequest() external { - PWNSimpleLoanSimpleRequest.Request memory request = PWNSimpleLoanSimpleRequest.Request({ - collateralCategory: MultiToken.Category.ERC1155, - collateralAddress: address(t1155), - collateralId: 42, - collateralAmount: 10e18, - loanAssetAddress: address(loanAsset), - loanAmount: 100e18, - loanYield: 10e18, - duration: 3600, - expiration: 0, - allowedLender: lender, - borrower: borrower, - refinancingLoanId: 0, - nonce: nonce - }); - - // Mint initial state - t1155.mint(borrower, 42, 10e18); - - // Approve collateral - vm.prank(borrower); - t1155.setApprovalForAll(address(simpleLoan), true); - - // Sign request - bytes memory signature = _sign(borrowerPK, simpleLoanSimpleRequest.getRequestHash(request)); - - // Mint initial state - loanAsset.mint(lender, 100e18); - - // Approve loan asset - vm.prank(lender); - loanAsset.approve(address(simpleLoan), 100e18); - - // Loan factory data (need for vm.prank to work properly when creating a loan) - bytes memory loanTermsFactoryData = simpleLoanSimpleRequest.encodeLoanTermsFactoryData(request); - - // Create LOAN - vm.prank(lender); - uint256 loanId = simpleLoan.createLOAN({ - loanTermsFactoryContract: address(simpleLoanSimpleRequest), - loanTermsFactoryData: loanTermsFactoryData, - signature: signature, - loanAssetPermit: "", - collateralPermit: "" - }); - - // Assert final state - assertEq(loanToken.ownerOf(loanId), lender); - - assertEq(loanAsset.balanceOf(lender), 0); - assertEq(loanAsset.balanceOf(borrower), 100e18); - assertEq(loanAsset.balanceOf(address(simpleLoan)), 0); - - assertEq(t1155.balanceOf(lender, 42), 0); - assertEq(t1155.balanceOf(borrower, 42), 0); - assertEq(t1155.balanceOf(address(simpleLoan), 42), 10e18); - - assertEq(revokedRequestNonce.isNonceRevoked(borrower, request.nonce), true); - assertEq(loanToken.loanContract(loanId), address(simpleLoan)); - } - - - // Different collateral types - - function test_shouldCreateLOAN_withERC20Collateral() external { - // Create LOAN - uint256 loanId = _createERC20Loan(); - - // Assert final state - assertEq(loanToken.ownerOf(loanId), lender); - - assertEq(loanAsset.balanceOf(lender), 0); - assertEq(loanAsset.balanceOf(borrower), 100e18); - assertEq(loanAsset.balanceOf(address(simpleLoan)), 0); - - assertEq(t20.balanceOf(lender), 0); - assertEq(t20.balanceOf(borrower), 0); - assertEq(t20.balanceOf(address(simpleLoan)), 10e18); - - assertEq(revokedOfferNonce.isNonceRevoked(lender, defaultOffer.nonce), true); - assertEq(loanToken.loanContract(loanId), address(simpleLoan)); - } - - function test_shouldCreateLOAN_withERC721Collateral() external { - // Create LOAN - uint256 loanId = _createERC721Loan(); - - // Assert final state - assertEq(loanToken.ownerOf(loanId), lender); - - assertEq(loanAsset.balanceOf(lender), 0); - assertEq(loanAsset.balanceOf(borrower), 100e18); - assertEq(loanAsset.balanceOf(address(simpleLoan)), 0); - - assertEq(t721.ownerOf(42), address(simpleLoan)); - - assertEq(revokedOfferNonce.isNonceRevoked(lender, defaultOffer.nonce), true); - assertEq(loanToken.loanContract(loanId), address(simpleLoan)); - } - - function test_shouldCreateLOAN_withERC1155Collateral() external { - // Create LOAN - uint256 loanId = _createERC1155Loan(); - - // Assert final state - assertEq(loanToken.ownerOf(loanId), lender); - - assertEq(loanAsset.balanceOf(lender), 0); - assertEq(loanAsset.balanceOf(borrower), 100e18); - assertEq(loanAsset.balanceOf(address(simpleLoan)), 0); - - assertEq(t1155.balanceOf(lender, 42), 0); - assertEq(t1155.balanceOf(borrower, 42), 0); - assertEq(t1155.balanceOf(address(simpleLoan), 42), 10e18); - - assertEq(revokedOfferNonce.isNonceRevoked(lender, defaultOffer.nonce), true); - assertEq(loanToken.loanContract(loanId), address(simpleLoan)); - } - - function test_shouldCreateLOAN_withCryptoKittiesCollateral() external { - // TODO: - } - - - // Repay LOAN - - function test_shouldRepayLoan_whenNotExpired_whenOriginalLenderIsLOANOwner() external { - // Create LOAN - uint256 loanId = _createERC1155Loan(); - - // Repay loan - _repayLoan(loanId); - - // Assert final state - vm.expectRevert("ERC721: invalid token ID"); - loanToken.ownerOf(loanId); - - assertEq(loanAsset.balanceOf(lender), 110e18); - assertEq(loanAsset.balanceOf(borrower), 0); - assertEq(loanAsset.balanceOf(address(simpleLoan)), 0); - - assertEq(t1155.balanceOf(lender, 42), 0); - assertEq(t1155.balanceOf(borrower, 42), 10e18); - assertEq(t1155.balanceOf(address(simpleLoan), 42), 0); - } - - function test_shouldFailToRepayLoan_whenLOANExpired() external { - // Create LOAN - uint256 loanId = _createERC1155Loan(); - - // Default on a loan - uint256 expiration = block.timestamp + uint256(defaultOffer.duration); - vm.warp(expiration); - - // Try to repay loan - _repayLoanFailing( - loanId, - abi.encodeWithSelector(LoanDefaulted.selector, uint40(expiration)) - ); - } - - - // Claim LOAN - - function test_shouldClaimRepaidLOAN_whenOriginalLenderIsNotLOANOwner() external { - address lender2 = makeAddr("lender2"); - - // Create LOAN - uint256 loanId = _createERC1155Loan(); - - // Transfer loan to another lender - vm.prank(lender); - loanToken.transferFrom(lender, lender2, loanId); - - // Repay loan - _repayLoan(loanId); - - // Claim loan - vm.prank(lender2); - simpleLoan.claimLOAN(loanId); - - // Assert final state - vm.expectRevert("ERC721: invalid token ID"); - loanToken.ownerOf(loanId); - - assertEq(loanAsset.balanceOf(lender), 0); - assertEq(loanAsset.balanceOf(lender2), 110e18); - assertEq(loanAsset.balanceOf(borrower), 0); - assertEq(loanAsset.balanceOf(address(simpleLoan)), 0); - - assertEq(t1155.balanceOf(lender, 42), 0); - assertEq(t1155.balanceOf(lender2, 42), 0); - assertEq(t1155.balanceOf(borrower, 42), 10e18); - assertEq(t1155.balanceOf(address(simpleLoan), 42), 0); - } - - function test_shouldClaimDefaultedLOAN() external { - // Create LOAN - uint256 loanId = _createERC1155Loan(); - - // Loan defaulted - vm.warp(block.timestamp + uint256(defaultOffer.duration)); - - // Claim defaulted loan - vm.prank(lender); - simpleLoan.claimLOAN(loanId); - - // Assert final state - vm.expectRevert("ERC721: invalid token ID"); - loanToken.ownerOf(loanId); - - assertEq(loanAsset.balanceOf(lender), 0); - assertEq(loanAsset.balanceOf(borrower), 100e18); - assertEq(loanAsset.balanceOf(address(simpleLoan)), 0); - - assertEq(t1155.balanceOf(lender, 42), 10e18); - assertEq(t1155.balanceOf(borrower, 42), 0); - assertEq(t1155.balanceOf(address(simpleLoan), 42), 0); - } - -} diff --git a/test/integration/contracts/PWNSimpleLoanSimpleOfferIntegration.t.sol b/test/integration/contracts/PWNSimpleLoanSimpleOfferIntegration.t.sol deleted file mode 100644 index 65d784e..0000000 --- a/test/integration/contracts/PWNSimpleLoanSimpleOfferIntegration.t.sol +++ /dev/null @@ -1,73 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity 0.8.16; - -import "forge-std/Test.sol"; - -import "@pwn/PWNErrors.sol"; - -import "@pwn-test/integration/BaseIntegrationTest.t.sol"; - - -contract PWNSimpleLoanSimpleOfferIntegrationTest is BaseIntegrationTest { - - // Group of offers - - function test_shouldRevokeOffersInGroup_whenAcceptingOneFromGroup() external { - // Mint initial state - loanAsset.mint(lender, 100e18); - t1155.mint(borrower, 42, 10e18); - - // Sign offers - PWNSimpleLoanSimpleOffer.Offer memory offer = PWNSimpleLoanSimpleOffer.Offer({ - collateralCategory: MultiToken.Category.ERC1155, - collateralAddress: address(t1155), - collateralId: 42, - collateralAmount: 5e18, // 1/2 of borrower balance - loanAssetAddress: address(loanAsset), - loanAmount: 50e18, // 1/2 of lender balance - loanYield: 10e18, - duration: 3600, - expiration: 0, - allowedBorrower: borrower, - lender: lender, - isPersistent: false, - nonce: nonce - }); - bytes memory signature1 = _sign(lenderPK, simpleLoanSimpleOffer.getOfferHash(offer)); - bytes memory offerData1 = abi.encode(offer); - - offer.loanYield = 20e18; - bytes memory signature2 = _sign(lenderPK, simpleLoanSimpleOffer.getOfferHash(offer)); - bytes memory offerData2 = abi.encode(offer); - - // Approve loan asset - vm.prank(lender); - loanAsset.approve(address(simpleLoan), 100e18); - - // Approve collateral - vm.prank(borrower); - t1155.setApprovalForAll(address(simpleLoan), true); - - // Create LOAN with offer 2 - vm.prank(borrower); - simpleLoan.createLOAN({ - loanTermsFactoryContract: address(simpleLoanSimpleOffer), - loanTermsFactoryData: offerData2, - signature: signature2, - loanAssetPermit: "", - collateralPermit: "" - }); - - // Fail to accept other offers with same nonce - vm.expectRevert(abi.encodeWithSelector(NonceAlreadyRevoked.selector)); - vm.prank(borrower); - simpleLoan.createLOAN({ - loanTermsFactoryContract: address(simpleLoanSimpleOffer), - loanTermsFactoryData: offerData1, - signature: signature1, - loanAssetPermit: "", - collateralPermit: "" - }); - } - -} diff --git a/test/integration/contracts/PWNSimpleLoanSimpleRequestIntegration.t.sol b/test/integration/contracts/PWNSimpleLoanSimpleRequestIntegration.t.sol deleted file mode 100644 index 92a7736..0000000 --- a/test/integration/contracts/PWNSimpleLoanSimpleRequestIntegration.t.sol +++ /dev/null @@ -1,73 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity 0.8.16; - -import "forge-std/Test.sol"; - -import "@pwn/PWNErrors.sol"; - -import "@pwn-test/integration/BaseIntegrationTest.t.sol"; - - -contract PWNSimpleLoanSimpleRequestIntegrationTest is BaseIntegrationTest { - - // Group of requests - - function test_shouldRevokeRequestsInGroup_whenAcceptingOneFromGroup() external { - // Mint initial state - loanAsset.mint(lender, 100e18); - t1155.mint(borrower, 42, 10e18); - - // Sign requests - PWNSimpleLoanSimpleRequest.Request memory request = PWNSimpleLoanSimpleRequest.Request({ - collateralCategory: MultiToken.Category.ERC1155, - collateralAddress: address(t1155), - collateralId: 42, - collateralAmount: 5e18, // 1/2 of borrower balance - loanAssetAddress: address(loanAsset), - loanAmount: 50e18, // 1/2 of lender balance - loanYield: 10e18, - duration: 3600, - expiration: 0, - allowedLender: lender, - borrower: borrower, - refinancingLoanId: 0, - nonce: nonce - }); - bytes memory signature1 = _sign(borrowerPK, simpleLoanSimpleRequest.getRequestHash(request)); - bytes memory requestData1 = abi.encode(request); - - request.loanYield = 20e18; - bytes memory signature2 = _sign(borrowerPK, simpleLoanSimpleRequest.getRequestHash(request)); - bytes memory requestData2 = abi.encode(request); - - // Approve loan asset - vm.prank(lender); - loanAsset.approve(address(simpleLoan), 100e18); - - // Approve collateral - vm.prank(borrower); - t1155.setApprovalForAll(address(simpleLoan), true); - - // Create LOAN with request 2 - vm.prank(lender); - simpleLoan.createLOAN({ - loanTermsFactoryContract: address(simpleLoanSimpleRequest), - loanTermsFactoryData: requestData2, - signature: signature2, - loanAssetPermit: "", - collateralPermit: "" - }); - - // Fail to accept other requests with same nonce - vm.expectRevert(abi.encodeWithSelector(NonceAlreadyRevoked.selector)); - vm.prank(lender); - simpleLoan.createLOAN({ - loanTermsFactoryContract: address(simpleLoanSimpleRequest), - loanTermsFactoryData: requestData1, - signature: signature1, - loanAssetPermit: "", - collateralPermit: "" - }); - } - -} From a5783b08eec83b46904067d7f27b437cf535b1a6 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Thu, 11 Apr 2024 12:04:37 -0400 Subject: [PATCH 081/129] test: update fork tests --- test/fork/DeployedProtocol.fork.t.sol | 90 ++++++++ .../UseCases.fork.t.sol} | 218 ++++++++++-------- .../deployed/DeployedProtocol.t.sol | 111 --------- 3 files changed, 215 insertions(+), 204 deletions(-) create mode 100644 test/fork/DeployedProtocol.fork.t.sol rename test/{integration/use-cases/UseCases.t.sol => fork/UseCases.fork.t.sol} (52%) delete mode 100644 test/integration/deployed/DeployedProtocol.t.sol diff --git a/test/fork/DeployedProtocol.fork.t.sol b/test/fork/DeployedProtocol.fork.t.sol new file mode 100644 index 0000000..258d9c0 --- /dev/null +++ b/test/fork/DeployedProtocol.fork.t.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.16; + +import "forge-std/Test.sol"; + +import { TimelockController } from "openzeppelin-contracts/contracts/governance/TimelockController.sol"; + +import "@pwn-test/DeploymentTest.t.sol"; + + +contract DeployedProtocolTest is DeploymentTest { + + bytes32 internal constant PROXY_ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; + bytes32 internal constant PROXY_IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + bytes32 internal constant PROPOSER_ROLE = 0xb09aa5aeb3702cfd50b6b62bc4532604938f21248a27a1d5ca736082b6819cc1; + bytes32 internal constant EXECUTOR_ROLE = 0xd8aa0f3194971a2a116679f7c2090f6939c8d4e01a2a8d7e41d55e5351469e63; + bytes32 internal constant CANCELLER_ROLE = 0xfd643c72710c63c0180259aba6b2d05451e3591a24e58b62239378085726f783; + + function _test_deployedProtocol(string memory urlOrAlias) internal { + vm.createSelectFork(urlOrAlias); + super.setUp(); + + // DEPLOYER + // - owner is deployer safe + if (deployment.deployerSafe != address(0)) { + assertEq(deployment.deployer.owner(), deployment.deployerSafe); + } + + // TIMELOCK CONTROLLERS + address protocolTimelockOwner = deployment.dao == address(0) ? deployment.protocolSafe : deployment.dao; + TimelockController protocolTimelockController = TimelockController(payable(deployment.protocolTimelock)); + // - protocol timelock has min delay of 4 days + assertEq(protocolTimelockController.getMinDelay(), 4 days); + // - protocol safe has PROPOSER role in protocol timelock + assertTrue(protocolTimelockController.hasRole(PROPOSER_ROLE, protocolTimelockOwner)); + // - protocol safe has CANCELLER role in protocol timelock + assertTrue(protocolTimelockController.hasRole(CANCELLER_ROLE, protocolTimelockOwner)); + // - everybody has EXECUTOR role in protocol timelock + assertTrue(protocolTimelockController.hasRole(EXECUTOR_ROLE, address(0))); + + address productTimelockOwner = deployment.dao == address(0) ? deployment.daoSafe : deployment.dao; + TimelockController productTimelockController = TimelockController(payable(deployment.productTimelock)); + // - product timelock has min delay of 4 days + assertEq(productTimelockController.getMinDelay(), 4 days); + // - dao safe has PROPOSER role in product timelock + assertTrue(productTimelockController.hasRole(PROPOSER_ROLE, productTimelockOwner)); + // - dao safe has CANCELLER role in product timelock + assertTrue(productTimelockController.hasRole(CANCELLER_ROLE, productTimelockOwner)); + // - everybody has EXECUTOR role in product timelock + assertTrue(productTimelockController.hasRole(EXECUTOR_ROLE, address(0))); + + // CONFIG + // - admin is protocol timelock + assertEq(vm.load(address(deployment.config), PROXY_ADMIN_SLOT), bytes32(uint256(uint160(deployment.protocolTimelock)))); + // - owner is product timelock + assertEq(deployment.config.owner(), deployment.productTimelock); + // - feeCollector is feeCollector + assertEq(deployment.config.feeCollector(), deployment.daoSafe); + // - is initialized + assertEq(vm.load(address(deployment.config), bytes32(uint256(1))) << 88 >> 248, bytes32(uint256(1))); + // - implementation is initialized + address configImplementation = address(uint160(uint256(vm.load(address(deployment.config), PROXY_IMPLEMENTATION_SLOT)))); + assertEq(vm.load(configImplementation, bytes32(uint256(1))) << 88 >> 248, bytes32(uint256(1))); + + // HUB + // - owner is protocol timelock + assertEq(deployment.hub.owner(), deployment.protocolTimelock); + + // HUB TAGS + // - simple loan + assertTrue(deployment.hub.hasTag(address(deployment.simpleLoan), PWNHubTags.NONCE_MANAGER)); + assertTrue(deployment.hub.hasTag(address(deployment.simpleLoan), PWNHubTags.ACTIVE_LOAN)); + // - simple loan simple proposal + assertTrue(deployment.hub.hasTag(address(deployment.simpleLoanSimpleProposal), PWNHubTags.NONCE_MANAGER)); + assertTrue(deployment.hub.hasTag(address(deployment.simpleLoanSimpleProposal), PWNHubTags.LOAN_PROPOSAL)); + // - simple loan list proposal + assertTrue(deployment.hub.hasTag(address(deployment.simpleLoanListProposal), PWNHubTags.NONCE_MANAGER)); + assertTrue(deployment.hub.hasTag(address(deployment.simpleLoanListProposal), PWNHubTags.LOAN_PROPOSAL)); + // - simple loan fungible proposal + assertTrue(deployment.hub.hasTag(address(deployment.simpleLoanFungibleProposal), PWNHubTags.NONCE_MANAGER)); + assertTrue(deployment.hub.hasTag(address(deployment.simpleLoanFungibleProposal), PWNHubTags.LOAN_PROPOSAL)); + // - simple loan dutch auction proposal + assertTrue(deployment.hub.hasTag(address(deployment.simpleLoanDutchAuctionProposal), PWNHubTags.NONCE_MANAGER)); + assertTrue(deployment.hub.hasTag(address(deployment.simpleLoanDutchAuctionProposal), PWNHubTags.LOAN_PROPOSAL)); + } + + + // todo: function test_deployedProtocol_ethereum() external { _test_deployedProtocol("mainnet"); } + +} diff --git a/test/integration/use-cases/UseCases.t.sol b/test/fork/UseCases.fork.t.sol similarity index 52% rename from test/integration/use-cases/UseCases.t.sol rename to test/fork/UseCases.fork.t.sol index b1fca7c..669fa32 100644 --- a/test/integration/use-cases/UseCases.t.sol +++ b/test/fork/UseCases.fork.t.sol @@ -3,10 +3,13 @@ pragma solidity 0.8.16; import "forge-std/Test.sol"; -import "MultiToken/interfaces/ICryptoKitties.sol"; +import { MultiToken, ICryptoKitties, IERC721 } from "MultiToken/MultiToken.sol"; -import "@pwn-test/helper/token/T20.sol"; -import "@pwn-test/helper/DeploymentTest.t.sol"; +import { Permit } from "@pwn/loan/vault/Permit.sol"; +import "@pwn/PWNErrors.sol"; + +import { T20 } from "@pwn-test/helper/T20.sol"; +import "@pwn-test/DeploymentTest.t.sol"; abstract contract UseCasesTest is DeploymentTest { @@ -24,11 +27,11 @@ abstract contract UseCasesTest is DeploymentTest { address lender = makeAddr("lender"); address borrower = makeAddr("borrower"); - PWNSimpleLoanSimpleOffer.Offer offer; + PWNSimpleLoanSimpleProposal.Proposal proposal; function setUp() public override { - vm.createSelectFork("mainnet"); + vm.createSelectFork("tenderly"); super.setUp(); @@ -37,25 +40,33 @@ abstract contract UseCasesTest is DeploymentTest { loanAsset.mint(borrower, 100e18); vm.prank(lender); - loanAsset.approve(address(simpleLoan), type(uint256).max); + loanAsset.approve(address(deployment.simpleLoan), type(uint256).max); vm.prank(borrower); - loanAsset.approve(address(simpleLoan), type(uint256).max); + loanAsset.approve(address(deployment.simpleLoan), type(uint256).max); - offer = PWNSimpleLoanSimpleOffer.Offer({ + proposal = PWNSimpleLoanSimpleProposal.Proposal({ collateralCategory: MultiToken.Category.ERC20, collateralAddress: address(loanAsset), collateralId: 0, collateralAmount: 10e18, - loanAssetAddress: address(loanAsset), - loanAmount: 1e18, - loanYield: 0, - duration: 3600, - expiration: 0, - allowedBorrower: address(0), - lender: lender, - isPersistent: false, - nonce: 0 + checkCollateralStateFingerprint: false, + collateralStateFingerprint: bytes32(0), + creditAddress: address(loanAsset), + creditAmount: 1e18, + availableCreditLimit: 0, + fixedInterestAmount: 0, + accruingInterestAPR: 0, + duration: 1 days, + expiration: uint40(block.timestamp + 7 days), + allowedAcceptor: address(0), + proposer: lender, + proposerSpecHash: deployment.simpleLoan.getLenderSpecHash(PWNSimpleLoan.LenderSpec(lender)), + isOffer: true, + refinancingLoanId: 0, + nonceSpace: 0, + nonce: 0, + loanContract: address(deployment.simpleLoan) }); } @@ -65,23 +76,40 @@ abstract contract UseCasesTest is DeploymentTest { } function _createLoanRevertWith(bytes memory revertData) internal returns (uint256) { - // Make offer + // Make proposal vm.prank(lender); - simpleLoanSimpleOffer.makeOffer(offer); + deployment.simpleLoanSimpleProposal.makeProposal(proposal); - bytes memory factoryData = simpleLoanSimpleOffer.encodeLoanTermsFactoryData(offer); + bytes memory proposalData = deployment.simpleLoanSimpleProposal.encodeProposalData(proposal); // Create a loan if (revertData.length > 0) { vm.expectRevert(revertData); } vm.prank(borrower); - return simpleLoan.createLOAN({ - loanTermsFactoryContract: address(simpleLoanSimpleOffer), - loanTermsFactoryData: factoryData, - signature: "", - loanAssetPermit: "", - collateralPermit: "" + return deployment.simpleLoan.createLOAN({ + proposalSpec: PWNSimpleLoan.ProposalSpec({ + proposalContract: address(deployment.simpleLoanSimpleProposal), + proposalData: proposalData, + proposalInclusionProof: new bytes32[](0), + signature: "" + }), + lenderSpec: PWNSimpleLoan.LenderSpec({ + sourceOfFunds: lender + }), + callerSpec: PWNSimpleLoan.CallerSpec({ + refinancingLoanId: 0, + revokeNonce: false, + nonce: 0, + permit: Permit({ + asset: address(0), + owner: address(0), + amount: 0, + deadline: 0, + v: 0, r: 0, s: 0 + }) + }), + extra: "" }); } @@ -94,28 +122,28 @@ contract InvalidCollateralAssetCategoryTest is UseCasesTest { function testUseCase_shouldFail_when20CollateralPassedWith721Category() external { // Borrower has not ZRX tokens - // Define offer - offer.collateralCategory = MultiToken.Category.ERC721; - offer.collateralAddress = ZRX; - offer.collateralId = 10e18; - offer.collateralAmount = 0; + // Define proposal + proposal.collateralCategory = MultiToken.Category.ERC721; + proposal.collateralAddress = ZRX; + proposal.collateralId = 10e18; + proposal.collateralAmount = 0; // Create loan - _createLoanRevertWith(abi.encodeWithSelector(InvalidCollateralAsset.selector)); + _createLoanRevertWith(abi.encodeWithSelector(InvalidMultiTokenAsset.selector, 1, ZRX, 10e18, 0)); } // Borrower can steal lender’s assets by using WETH as collateral function testUseCase_shouldFail_when20CollateralPassedWith1155Category() external { // Borrower has not WETH tokens - // Define offer - offer.collateralCategory = MultiToken.Category.ERC1155; - offer.collateralAddress = WETH; - offer.collateralId = 0; - offer.collateralAmount = 10e18; + // Define proposal + proposal.collateralCategory = MultiToken.Category.ERC1155; + proposal.collateralAddress = WETH; + proposal.collateralId = 0; + proposal.collateralAmount = 10e18; // Create loan - _createLoanRevertWith(abi.encodeWithSelector(InvalidCollateralAsset.selector)); + _createLoanRevertWith(abi.encodeWithSelector(InvalidMultiTokenAsset.selector, 2, WETH, 0, 10e18)); } // CryptoKitties token is locked when using it as ERC721 type collateral @@ -128,16 +156,16 @@ contract InvalidCollateralAssetCategoryTest is UseCasesTest { ICryptoKitties(CK).transfer(borrower, ckId); vm.prank(borrower); - ICryptoKitties(CK).approve(address(simpleLoan), ckId); + ICryptoKitties(CK).approve(address(deployment.simpleLoan), ckId); - // Define offer - offer.collateralCategory = MultiToken.Category.ERC721; - offer.collateralAddress = CK; - offer.collateralId = ckId; - offer.collateralAmount = 0; + // Define proposal + proposal.collateralCategory = MultiToken.Category.ERC721; + proposal.collateralAddress = CK; + proposal.collateralId = ckId; + proposal.collateralAmount = 0; // Create loan - _createLoanRevertWith(abi.encodeWithSelector(InvalidCollateralAsset.selector)); + _createLoanRevertWith(abi.encodeWithSelector(InvalidMultiTokenAsset.selector, 1, CK, ckId, 0)); } } @@ -154,14 +182,14 @@ contract InvalidLoanAssetTest is UseCasesTest { IERC721(DOODLE).transferFrom(originalDoodleOwner, lender, doodleId); vm.prank(lender); - IERC721(DOODLE).approve(address(simpleLoan), doodleId); + IERC721(DOODLE).approve(address(deployment.simpleLoan), doodleId); - // Define offer - offer.loanAssetAddress = DOODLE; - offer.loanAmount = doodleId; + // Define proposal + proposal.creditAddress = DOODLE; + proposal.creditAmount = doodleId; // Create loan - _createLoanRevertWith(abi.encodeWithSelector(InvalidLoanAsset.selector)); + _createLoanRevertWith(abi.encodeWithSelector(InvalidMultiTokenAsset.selector, 0, DOODLE, 0, doodleId)); } function testUseCase_shouldFail_whenUsingCryptoKittiesAsLoanAsset() external { @@ -173,14 +201,14 @@ contract InvalidLoanAssetTest is UseCasesTest { ICryptoKitties(CK).transfer(lender, ckId); vm.prank(lender); - ICryptoKitties(CK).approve(address(simpleLoan), ckId); + ICryptoKitties(CK).approve(address(deployment.simpleLoan), ckId); - // Define offer - offer.loanAssetAddress = CK; - offer.loanAmount = ckId; + // Define proposal + proposal.creditAddress = CK; + proposal.creditAmount = ckId; // Create loan - _createLoanRevertWith(abi.encodeWithSelector(InvalidLoanAsset.selector)); + _createLoanRevertWith(abi.encodeWithSelector(InvalidMultiTokenAsset.selector, 0, CK, 0, ckId)); } } @@ -195,13 +223,13 @@ contract TaxTokensTest is UseCasesTest { T20(CULT).transfer(borrower, 20e18); vm.prank(borrower); - T20(CULT).approve(address(simpleLoan), type(uint256).max); + T20(CULT).approve(address(deployment.simpleLoan), type(uint256).max); - // Define offer - offer.collateralCategory = MultiToken.Category.ERC20; - offer.collateralAddress = CULT; - offer.collateralId = 0; - offer.collateralAmount = 10e18; + // Define proposal + proposal.collateralCategory = MultiToken.Category.ERC20; + proposal.collateralAddress = CULT; + proposal.collateralId = 0; + proposal.collateralAmount = 10e18; // Create loan _createLoanRevertWith(abi.encodeWithSelector(IncompleteTransfer.selector)); @@ -214,11 +242,11 @@ contract TaxTokensTest is UseCasesTest { T20(CULT).transfer(lender, 20e18); vm.prank(lender); - T20(CULT).approve(address(simpleLoan), type(uint256).max); + T20(CULT).approve(address(deployment.simpleLoan), type(uint256).max); - // Define offer - offer.loanAssetAddress = CULT; - offer.loanAmount = 10e18; + // Define proposal + proposal.creditAddress = CULT; + proposal.creditAmount = 10e18; // Create loan _createLoanRevertWith(abi.encodeWithSelector(IncompleteTransfer.selector)); @@ -239,36 +267,42 @@ contract IncompleteERC20TokensTest is UseCasesTest { require(success); vm.prank(borrower); - (success, ) = USDT.call(abi.encodeWithSignature("approve(address,uint256)", address(simpleLoan), type(uint256).max)); + (success, ) = USDT.call(abi.encodeWithSignature("approve(address,uint256)", address(deployment.simpleLoan), type(uint256).max)); require(success); - // Define offer - offer.collateralCategory = MultiToken.Category.ERC20; - offer.collateralAddress = USDT; - offer.collateralId = 0; - offer.collateralAmount = 10e6; // USDT has 6 decimals + // Define proposal + proposal.collateralCategory = MultiToken.Category.ERC20; + proposal.collateralAddress = USDT; + proposal.collateralId = 0; + proposal.collateralAmount = 10e6; // USDT has 6 decimals // Check balance assertEq(T20(USDT).balanceOf(borrower), 10e6); - assertEq(T20(USDT).balanceOf(address(simpleLoan)), 0); + assertEq(T20(USDT).balanceOf(address(deployment.simpleLoan)), 0); // Create loan uint256 loanId = _createLoan(); // Check balance assertEq(T20(USDT).balanceOf(borrower), 0); - assertEq(T20(USDT).balanceOf(address(simpleLoan)), 10e6); + assertEq(T20(USDT).balanceOf(address(deployment.simpleLoan)), 10e6); // Repay loan vm.prank(borrower); - simpleLoan.repayLOAN({ + deployment.simpleLoan.repayLOAN({ loanId: loanId, - loanAssetPermit: "" + permit: Permit({ + asset: address(0), + owner: address(0), + amount: 0, + deadline: 0, + v: 0, r: 0, s: 0 + }) }); // Check balance assertEq(T20(USDT).balanceOf(borrower), 10e6); - assertEq(T20(USDT).balanceOf(address(simpleLoan)), 0); + assertEq(T20(USDT).balanceOf(address(deployment.simpleLoan)), 0); } function testUseCase_shouldPass_when20TokenTransferNotReturnsBool_whenUsedAsCredit() external { @@ -281,16 +315,16 @@ contract IncompleteERC20TokensTest is UseCasesTest { require(success); vm.prank(lender); - (success, ) = USDT.call(abi.encodeWithSignature("approve(address,uint256)", address(simpleLoan), type(uint256).max)); + (success, ) = USDT.call(abi.encodeWithSignature("approve(address,uint256)", address(deployment.simpleLoan), type(uint256).max)); require(success); vm.prank(borrower); - (success, ) = USDT.call(abi.encodeWithSignature("approve(address,uint256)", address(simpleLoan), type(uint256).max)); + (success, ) = USDT.call(abi.encodeWithSignature("approve(address,uint256)", address(deployment.simpleLoan), type(uint256).max)); require(success); - // Define offer - offer.loanAssetAddress = USDT; - offer.loanAmount = 10e6; // USDT has 6 decimals + // Define proposal + proposal.creditAddress = USDT; + proposal.creditAmount = 10e6; // USDT has 6 decimals // Check balance assertEq(T20(USDT).balanceOf(lender), 10e6); @@ -305,22 +339,20 @@ contract IncompleteERC20TokensTest is UseCasesTest { // Repay loan vm.prank(borrower); - simpleLoan.repayLOAN({ + deployment.simpleLoan.repayLOAN({ loanId: loanId, - loanAssetPermit: "" + permit: Permit({ + asset: address(0), + owner: address(0), + amount: 0, + deadline: 0, + v: 0, r: 0, s: 0 + }) }); - // Check balance - assertEq(T20(USDT).balanceOf(lender), 0); - assertEq(T20(USDT).balanceOf(address(simpleLoan)), 10e6); - - // Claim repaid loan - vm.prank(lender); - simpleLoan.claimLOAN(loanId); - - // Check balance + // Check balance - repaid directly to lender assertEq(T20(USDT).balanceOf(lender), 10e6); - assertEq(T20(USDT).balanceOf(address(simpleLoan)), 0); + assertEq(T20(USDT).balanceOf(address(deployment.simpleLoan)), 0); } } diff --git a/test/integration/deployed/DeployedProtocol.t.sol b/test/integration/deployed/DeployedProtocol.t.sol deleted file mode 100644 index bfec2c2..0000000 --- a/test/integration/deployed/DeployedProtocol.t.sol +++ /dev/null @@ -1,111 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity 0.8.16; - -import "forge-std/Test.sol"; - -import "openzeppelin-contracts/contracts/governance/TimelockController.sol"; - -import "@pwn-test/helper/DeploymentTest.t.sol"; - - -contract DeployedProtocolTest is DeploymentTest { - - bytes32 internal constant PROXY_ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; - bytes32 internal constant PROXY_IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; - bytes32 internal constant PROPOSER_ROLE = 0xb09aa5aeb3702cfd50b6b62bc4532604938f21248a27a1d5ca736082b6819cc1; - bytes32 internal constant EXECUTOR_ROLE = 0xd8aa0f3194971a2a116679f7c2090f6939c8d4e01a2a8d7e41d55e5351469e63; - bytes32 internal constant CANCELLER_ROLE = 0xfd643c72710c63c0180259aba6b2d05451e3591a24e58b62239378085726f783; - - function _test_deployedProtocol(string memory urlOrAlias) internal { - vm.createSelectFork(urlOrAlias); - super.setUp(); - - // DEPLOYER - // - owner is deployer safe - if (deployerSafe != address(0)) { - assertEq(deployer.owner(), deployerSafe); - } - - // TIMELOCK CONTROLLERS - bool haveTimelocks = protocolTimelock != address(0) && productTimelock != address(0); - if (haveTimelocks) { - address protocolTimelockOwner = dao == address(0) ? protocolSafe : dao; - TimelockController protocolTimelockController = TimelockController(payable(protocolTimelock)); - // - protocol timelock has min delay of 14 days - assertEq(protocolTimelockController.getMinDelay(), 345_600); - // - protocol safe has PROPOSER role in protocol timelock - assertTrue(protocolTimelockController.hasRole(PROPOSER_ROLE, protocolTimelockOwner)); - // - protocol safe has CANCELLER role in protocol timelock - assertTrue(protocolTimelockController.hasRole(CANCELLER_ROLE, protocolTimelockOwner)); - // - everybody has EXECUTOR role in protocol timelock - assertTrue(protocolTimelockController.hasRole(EXECUTOR_ROLE, address(0))); - - address productTimelockOwner = dao == address(0) ? daoSafe : dao; - TimelockController productTimelockController = TimelockController(payable(productTimelock)); - // - product timelock has min delay of 4 days - assertEq(productTimelockController.getMinDelay(), 345_600); - // - dao safe has PROPOSER role in product timelock - assertTrue(productTimelockController.hasRole(PROPOSER_ROLE, productTimelockOwner)); - // - dao safe has CANCELLER role in product timelock - assertTrue(productTimelockController.hasRole(CANCELLER_ROLE, productTimelockOwner)); - // - everybody has EXECUTOR role in product timelock - assertTrue(productTimelockController.hasRole(EXECUTOR_ROLE, address(0))); - } - - // CONFIG - if (haveTimelocks) { - // - admin is protocol timelock - assertEq(vm.load(address(config), PROXY_ADMIN_SLOT), bytes32(uint256(uint160(protocolTimelock)))); - // - owner is product timelock - assertEq(config.owner(), productTimelock); - } else { - // - admin is protocol safe - assertEq(vm.load(address(config), PROXY_ADMIN_SLOT), bytes32(uint256(uint160(protocolSafe)))); - // - owner is dao safe - assertEq(config.owner(), daoSafe); - } - // - feeCollector is feeCollector - assertEq(config.feeCollector(), feeCollector); - // - is initialized - assertEq(vm.load(address(config), bytes32(uint256(1))) << 88 >> 248, bytes32(uint256(1))); - // - implementation is initialized - address configImplementation = address(uint160(uint256(vm.load(address(config), PROXY_IMPLEMENTATION_SLOT)))); - assertEq(vm.load(configImplementation, bytes32(uint256(1))) << 88 >> 248, bytes32(uint256(1))); - - // HUB - if (haveTimelocks) { - // - owner is protocol timelock - assertEq(hub.owner(), protocolTimelock); - } else { - // - owner is protocol safe - assertEq(hub.owner(), protocolSafe); - } - - // HUB TAGS - // - simple loan has active loan tag - assertTrue(hub.hasTag(address(simpleLoan), PWNHubTags.ACTIVE_LOAN)); - // - simple loan simple offer has simple loan terms factory & loan offer tags - assertTrue(hub.hasTag(address(simpleLoanSimpleOffer), PWNHubTags.SIMPLE_LOAN_TERMS_FACTORY)); - assertTrue(hub.hasTag(address(simpleLoanSimpleOffer), PWNHubTags.LOAN_OFFER)); - // - simple loan list offer has simple loan terms factory & loan offer tags - assertTrue(hub.hasTag(address(simpleLoanListOffer), PWNHubTags.SIMPLE_LOAN_TERMS_FACTORY)); - assertTrue(hub.hasTag(address(simpleLoanListOffer), PWNHubTags.LOAN_OFFER)); - // - simple loan simple request has simple loan terms factory & loan request tags - assertTrue(hub.hasTag(address(simpleLoanSimpleRequest), PWNHubTags.SIMPLE_LOAN_TERMS_FACTORY)); - assertTrue(hub.hasTag(address(simpleLoanSimpleRequest), PWNHubTags.LOAN_REQUEST)); - - } - - - function test_deployedProtocol_ethereum() external { _test_deployedProtocol("mainnet"); } - function test_deployedProtocol_polygon() external { _test_deployedProtocol("polygon"); } - function test_deployedProtocol_arbitrum() external { _test_deployedProtocol("arbitrum"); } - function test_deployedProtocol_optimism() external { _test_deployedProtocol("optimism"); } - function test_deployedProtocol_base() external { _test_deployedProtocol("base"); } - function test_deployedProtocol_cronos() external { _test_deployedProtocol("cronos"); } - function test_deployedProtocol_mantle() external { _test_deployedProtocol("mantle"); } - function test_deployedProtocol_bsc() external { _test_deployedProtocol("bsc"); } - - function test_deployedProtocol_sepolia() external { _test_deployedProtocol("sepolia"); } - -} From eb36edcb2e09dbc3f59868f45de7e35d565dcf95 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Fri, 12 Apr 2024 10:24:40 -0400 Subject: [PATCH 082/129] test: add integration tests for fungible and dutch auction proposals --- test/integration/BaseIntegrationTest.t.sol | 14 +- test/integration/PWNProtocolIntegrity.t.sol | 22 +- .../PWNSimpleLoanIntegration.t.sol | 261 +++++++++++++++--- 3 files changed, 243 insertions(+), 54 deletions(-) diff --git a/test/integration/BaseIntegrationTest.t.sol b/test/integration/BaseIntegrationTest.t.sol index f07fa80..d72b91f 100644 --- a/test/integration/BaseIntegrationTest.t.sol +++ b/test/integration/BaseIntegrationTest.t.sol @@ -18,7 +18,7 @@ abstract contract BaseIntegrationTest is DeploymentTest { T20 t20; T721 t721; T1155 t1155; - T20 loanAsset; + T20 credit; uint256 lenderPK = uint256(777); address lender = vm.addr(lenderPK); @@ -33,7 +33,7 @@ abstract contract BaseIntegrationTest is DeploymentTest { t20 = new T20(); t721 = new T721(); t1155 = new T1155(); - loanAsset = new T20(); + credit = new T20(); // Default offer simpleProposal = PWNSimpleLoanSimpleProposal.Proposal({ @@ -43,7 +43,7 @@ abstract contract BaseIntegrationTest is DeploymentTest { collateralAmount: 10e18, checkCollateralStateFingerprint: false, collateralStateFingerprint: bytes32(0), - creditAddress: address(loanAsset), + creditAddress: address(credit), creditAmount: 100e18, availableCreditLimit: 0, fixedInterestAmount: 10e18, @@ -132,11 +132,11 @@ abstract contract BaseIntegrationTest is DeploymentTest { bytes memory signature = _sign(lenderPK, deployment.simpleLoanSimpleProposal.getProposalHash(_proposal)); // Mint initial state - loanAsset.mint(lender, 100e18); + credit.mint(lender, 100e18); // Approve loan asset vm.prank(lender); - loanAsset.approve(address(deployment.simpleLoan), 100e18); + credit.approve(address(deployment.simpleLoan), 100e18); // Proposal data (need for vm.prank to work properly when creating a loan) bytes memory proposalData = deployment.simpleLoanSimpleProposal.encodeProposalData(_proposal); @@ -180,11 +180,11 @@ abstract contract BaseIntegrationTest is DeploymentTest { function _repayLoanFailing(uint256 loanId, bytes memory revertData) internal { // Get the yield by farming 100000% APR food tokens - loanAsset.mint(borrower, 10e18); + credit.mint(borrower, 10e18); // Approve loan asset vm.prank(borrower); - loanAsset.approve(address(deployment.simpleLoan), 110e18); + credit.approve(address(deployment.simpleLoan), 110e18); // Repay loan if (keccak256(revertData) != keccak256("")) { diff --git a/test/integration/PWNProtocolIntegrity.t.sol b/test/integration/PWNProtocolIntegrity.t.sol index 5dc2bac..43d8bd6 100644 --- a/test/integration/PWNProtocolIntegrity.t.sol +++ b/test/integration/PWNProtocolIntegrity.t.sol @@ -37,9 +37,9 @@ contract PWNProtocolIntegrityTest is BaseIntegrationTest { vm.expectRevert("ERC721: invalid token ID"); deployment.loanToken.ownerOf(loanId); - assertEq(loanAsset.balanceOf(lender), 110e18); - assertEq(loanAsset.balanceOf(borrower), 0); - assertEq(loanAsset.balanceOf(address(deployment.simpleLoan)), 0); + assertEq(credit.balanceOf(lender), 110e18); + assertEq(credit.balanceOf(borrower), 0); + assertEq(credit.balanceOf(address(deployment.simpleLoan)), 0); assertEq(t1155.balanceOf(lender, 42), 0); assertEq(t1155.balanceOf(borrower, 42), 10e18); @@ -66,10 +66,10 @@ contract PWNProtocolIntegrityTest is BaseIntegrationTest { // Assert final state assertEq(deployment.loanToken.ownerOf(loanId), lender2); - assertEq(loanAsset.balanceOf(lender), 0); - assertEq(loanAsset.balanceOf(lender2), 0); - assertEq(loanAsset.balanceOf(borrower), 0); - assertEq(loanAsset.balanceOf(address(deployment.simpleLoan)), 110e18); + assertEq(credit.balanceOf(lender), 0); + assertEq(credit.balanceOf(lender2), 0); + assertEq(credit.balanceOf(borrower), 0); + assertEq(credit.balanceOf(address(deployment.simpleLoan)), 110e18); assertEq(t1155.balanceOf(lender, 42), 0); assertEq(t1155.balanceOf(lender2, 42), 0); @@ -102,10 +102,10 @@ contract PWNProtocolIntegrityTest is BaseIntegrationTest { vm.expectRevert("ERC721: invalid token ID"); deployment.loanToken.ownerOf(loanId); - assertEq(loanAsset.balanceOf(lender), 0); - assertEq(loanAsset.balanceOf(lender2), 110e18); - assertEq(loanAsset.balanceOf(borrower), 0); - assertEq(loanAsset.balanceOf(address(deployment.simpleLoan)), 0); + assertEq(credit.balanceOf(lender), 0); + assertEq(credit.balanceOf(lender2), 110e18); + assertEq(credit.balanceOf(borrower), 0); + assertEq(credit.balanceOf(address(deployment.simpleLoan)), 0); assertEq(t1155.balanceOf(lender, 42), 0); assertEq(t1155.balanceOf(lender2, 42), 0); diff --git a/test/integration/PWNSimpleLoanIntegration.t.sol b/test/integration/PWNSimpleLoanIntegration.t.sol index 7db72a4..7f6f491 100644 --- a/test/integration/PWNSimpleLoanIntegration.t.sol +++ b/test/integration/PWNSimpleLoanIntegration.t.sol @@ -20,13 +20,13 @@ contract PWNSimpleLoanIntegrationTest is BaseIntegrationTest { collateralAmount: 10e18, checkCollateralStateFingerprint: false, collateralStateFingerprint: bytes32(0), - creditAddress: address(loanAsset), + creditAddress: address(credit), creditAmount: 100e18, availableCreditLimit: 0, fixedInterestAmount: 10e18, accruingInterestAPR: 0, - duration: 3600, - expiration: uint40(block.timestamp + 7 days), + duration: 7 days, + expiration: uint40(block.timestamp + 1 days), allowedAcceptor: borrower, proposer: lender, proposerSpecHash: deployment.simpleLoan.getLenderSpecHash(PWNSimpleLoan.LenderSpec(lender)), @@ -48,11 +48,11 @@ contract PWNSimpleLoanIntegrationTest is BaseIntegrationTest { bytes memory signature = _sign(lenderPK, deployment.simpleLoanSimpleProposal.getProposalHash(proposal)); // Mint initial state - loanAsset.mint(lender, 100e18); + credit.mint(lender, 100e18); // Approve loan asset vm.prank(lender); - loanAsset.approve(address(deployment.simpleLoan), 100e18); + credit.approve(address(deployment.simpleLoan), 100e18); // Proposal data (need for vm.prank to work properly when creating a loan) bytes memory proposalData = deployment.simpleLoanSimpleProposal.encodeProposalData(proposal); @@ -87,9 +87,9 @@ contract PWNSimpleLoanIntegrationTest is BaseIntegrationTest { // Assert final state assertEq(deployment.loanToken.ownerOf(loanId), lender); - assertEq(loanAsset.balanceOf(lender), 0); - assertEq(loanAsset.balanceOf(borrower), 100e18); - assertEq(loanAsset.balanceOf(address(deployment.simpleLoan)), 0); + assertEq(credit.balanceOf(lender), 0); + assertEq(credit.balanceOf(borrower), 100e18); + assertEq(credit.balanceOf(address(deployment.simpleLoan)), 0); assertEq(t1155.balanceOf(lender, 42), 0); assertEq(t1155.balanceOf(borrower, 42), 0); @@ -111,13 +111,13 @@ contract PWNSimpleLoanIntegrationTest is BaseIntegrationTest { collateralAmount: 10e18, checkCollateralStateFingerprint: false, collateralStateFingerprint: bytes32(0), - creditAddress: address(loanAsset), + creditAddress: address(credit), creditAmount: 100e18, availableCreditLimit: 0, fixedInterestAmount: 10e18, accruingInterestAPR: 0, - duration: 3600, - expiration: uint40(block.timestamp + 7 days), + duration: 7 days, + expiration: uint40(block.timestamp + 1 days), allowedAcceptor: borrower, proposer: lender, proposerSpecHash: deployment.simpleLoan.getLenderSpecHash(PWNSimpleLoan.LenderSpec(lender)), @@ -145,11 +145,11 @@ contract PWNSimpleLoanIntegrationTest is BaseIntegrationTest { bytes memory signature = _sign(lenderPK, deployment.simpleLoanListProposal.getProposalHash(proposal)); // Mint initial state - loanAsset.mint(lender, 100e18); + credit.mint(lender, 100e18); // Approve loan asset vm.prank(lender); - loanAsset.approve(address(deployment.simpleLoan), 100e18); + credit.approve(address(deployment.simpleLoan), 100e18); // Proposal data (need for vm.prank to work properly when creating a loan) bytes memory proposalData = deployment.simpleLoanListProposal.encodeProposalData(proposal, proposalValues); @@ -184,9 +184,9 @@ contract PWNSimpleLoanIntegrationTest is BaseIntegrationTest { // Assert final state assertEq(deployment.loanToken.ownerOf(loanId), lender); - assertEq(loanAsset.balanceOf(lender), 0); - assertEq(loanAsset.balanceOf(borrower), 100e18); - assertEq(loanAsset.balanceOf(address(deployment.simpleLoan)), 0); + assertEq(credit.balanceOf(lender), 0); + assertEq(credit.balanceOf(borrower), 100e18); + assertEq(credit.balanceOf(address(deployment.simpleLoan)), 0); assertEq(t1155.balanceOf(lender, 42), 0); assertEq(t1155.balanceOf(borrower, 42), 0); @@ -196,8 +196,197 @@ contract PWNSimpleLoanIntegrationTest is BaseIntegrationTest { assertEq(deployment.loanToken.loanContract(loanId), address(deployment.simpleLoan)); } - // TODO: fungible, dutch auction + function test_shouldCreateLOAN_fromFungibleProposal() external { + PWNSimpleLoanFungibleProposal.Proposal memory proposal = PWNSimpleLoanFungibleProposal.Proposal({ + collateralCategory: MultiToken.Category.ERC1155, + collateralAddress: address(t1155), + collateralId: 42, + minCollateralAmount: 1, + checkCollateralStateFingerprint: false, + collateralStateFingerprint: bytes32(0), + creditAddress: address(credit), + creditPerCollateralUnit: 10e18 * deployment.simpleLoanFungibleProposal.CREDIT_PER_COLLATERAL_UNIT_DENOMINATOR(), + availableCreditLimit: 100e18, + fixedInterestAmount: 10e18, + accruingInterestAPR: 0, + duration: 7 days, + expiration: uint40(block.timestamp + 1 days), + allowedAcceptor: borrower, + proposer: lender, + proposerSpecHash: deployment.simpleLoan.getLenderSpecHash(PWNSimpleLoan.LenderSpec(lender)), + isOffer: true, + refinancingLoanId: 0, + nonceSpace: 0, + nonce: 0, + loanContract: address(deployment.simpleLoan) + }); + + PWNSimpleLoanFungibleProposal.ProposalValues memory proposalValues = PWNSimpleLoanFungibleProposal.ProposalValues({ + collateralAmount: 7 + }); + + // Mint initial state + t1155.mint(borrower, 42, 10); + + // Approve collateral + vm.prank(borrower); + t1155.setApprovalForAll(address(deployment.simpleLoan), true); + + // Sign proposal + bytes32 proposalHash = deployment.simpleLoanFungibleProposal.getProposalHash(proposal); + bytes memory signature = _sign(lenderPK, proposalHash); + + // Mint initial state + credit.mint(lender, 100e18); + + // Approve loan asset + vm.prank(lender); + credit.approve(address(deployment.simpleLoan), 100e18); + + // Proposal data (need for vm.prank to work properly when creating a loan) + bytes memory proposalData = deployment.simpleLoanFungibleProposal.encodeProposalData(proposal, proposalValues); + + // Create LOAN + vm.prank(borrower); + uint256 loanId = deployment.simpleLoan.createLOAN({ + proposalSpec: PWNSimpleLoan.ProposalSpec({ + proposalContract: address(deployment.simpleLoanFungibleProposal), + proposalData: proposalData, + proposalInclusionProof: new bytes32[](0), + signature: signature + }), + lenderSpec: PWNSimpleLoan.LenderSpec({ + sourceOfFunds: lender + }), + callerSpec: PWNSimpleLoan.CallerSpec({ + refinancingLoanId: 0, + revokeNonce: false, + nonce: 0, + permit: Permit({ + asset: address(0), + owner: address(0), + amount: 0, + deadline: 0, + v: 0, r: 0, s: 0 + }) + }), + extra: "" + }); + + // Assert final state + assertEq(deployment.loanToken.ownerOf(loanId), lender); + + assertEq(credit.balanceOf(lender), 30e18); + assertEq(credit.balanceOf(borrower), 70e18); + assertEq(credit.balanceOf(address(deployment.simpleLoan)), 0); + + assertEq(t1155.balanceOf(lender, 42), 0); + assertEq(t1155.balanceOf(borrower, 42), 3); + assertEq(t1155.balanceOf(address(deployment.simpleLoan), 42), 7); + + assertEq(deployment.revokedNonce.isNonceRevoked(lender, proposal.nonceSpace, proposal.nonce), false); + assertEq(deployment.simpleLoanFungibleProposal.creditUsed(proposalHash), 70e18); + assertEq(deployment.loanToken.loanContract(loanId), address(deployment.simpleLoan)); + } + function test_shouldCreateLOAN_fromDutchAuctionProposal() external { + PWNSimpleLoanDutchAuctionProposal.Proposal memory proposal = PWNSimpleLoanDutchAuctionProposal.Proposal({ + collateralCategory: MultiToken.Category.ERC1155, + collateralAddress: address(t1155), + collateralId: 42, + collateralAmount: 10, + checkCollateralStateFingerprint: false, + collateralStateFingerprint: bytes32(0), + creditAddress: address(credit), + minCreditAmount: 10e18, + maxCreditAmount: 100e18, + availableCreditLimit: 0, + fixedInterestAmount: 10e18, + accruingInterestAPR: 0, + duration: 7 days, + auctionStart: uint40(block.timestamp), + auctionDuration: 30 hours, + allowedAcceptor: lender, + proposer: borrower, + proposerSpecHash: bytes32(0), + isOffer: false, + refinancingLoanId: 0, + nonceSpace: 0, + nonce: 0, + loanContract: address(deployment.simpleLoan) + }); + + PWNSimpleLoanDutchAuctionProposal.ProposalValues memory proposalValues = PWNSimpleLoanDutchAuctionProposal.ProposalValues({ + intendedCreditAmount: 90e18, + slippage: 10e18 + }); + + // Mint initial state + t1155.mint(borrower, 42, 10); + + // Approve collateral + vm.prank(borrower); + t1155.setApprovalForAll(address(deployment.simpleLoan), true); + + // Sign proposal + bytes32 proposalHash = deployment.simpleLoanDutchAuctionProposal.getProposalHash(proposal); + bytes memory signature = _sign(borrowerPK, proposalHash); + + // Mint initial state + credit.mint(lender, 100e18); + + // Approve loan asset + vm.prank(lender); + credit.approve(address(deployment.simpleLoan), 100e18); + + // Proposal data (need for vm.prank to work properly when creating a loan) + bytes memory proposalData = deployment.simpleLoanDutchAuctionProposal.encodeProposalData(proposal, proposalValues); + + vm.warp(proposal.auctionStart + 4 hours); + + uint256 creditAmount = deployment.simpleLoanDutchAuctionProposal.getCreditAmount(proposal, block.timestamp); + + // Create LOAN + vm.prank(lender); + uint256 loanId = deployment.simpleLoan.createLOAN({ + proposalSpec: PWNSimpleLoan.ProposalSpec({ + proposalContract: address(deployment.simpleLoanDutchAuctionProposal), + proposalData: proposalData, + proposalInclusionProof: new bytes32[](0), + signature: signature + }), + lenderSpec: PWNSimpleLoan.LenderSpec({ + sourceOfFunds: lender + }), + callerSpec: PWNSimpleLoan.CallerSpec({ + refinancingLoanId: 0, + revokeNonce: false, + nonce: 0, + permit: Permit({ + asset: address(0), + owner: address(0), + amount: 0, + deadline: 0, + v: 0, r: 0, s: 0 + }) + }), + extra: "" + }); + + // Assert final state + assertEq(deployment.loanToken.ownerOf(loanId), lender); + + assertEq(credit.balanceOf(lender), 100e18 - creditAmount); + assertEq(credit.balanceOf(borrower), creditAmount); + assertEq(credit.balanceOf(address(deployment.simpleLoan)), 0); + + assertEq(t1155.balanceOf(lender, 42), 0); + assertEq(t1155.balanceOf(borrower, 42), 0); + assertEq(t1155.balanceOf(address(deployment.simpleLoan), 42), 10); + + assertEq(deployment.revokedNonce.isNonceRevoked(borrower, proposal.nonceSpace, proposal.nonce), true); + assertEq(deployment.loanToken.loanContract(loanId), address(deployment.simpleLoan)); + } // Different collateral types @@ -208,9 +397,9 @@ contract PWNSimpleLoanIntegrationTest is BaseIntegrationTest { // Assert final state assertEq(deployment.loanToken.ownerOf(loanId), lender); - assertEq(loanAsset.balanceOf(lender), 0); - assertEq(loanAsset.balanceOf(borrower), 100e18); - assertEq(loanAsset.balanceOf(address(deployment.simpleLoan)), 0); + assertEq(credit.balanceOf(lender), 0); + assertEq(credit.balanceOf(borrower), 100e18); + assertEq(credit.balanceOf(address(deployment.simpleLoan)), 0); assertEq(t20.balanceOf(lender), 0); assertEq(t20.balanceOf(borrower), 0); @@ -227,9 +416,9 @@ contract PWNSimpleLoanIntegrationTest is BaseIntegrationTest { // Assert final state assertEq(deployment.loanToken.ownerOf(loanId), lender); - assertEq(loanAsset.balanceOf(lender), 0); - assertEq(loanAsset.balanceOf(borrower), 100e18); - assertEq(loanAsset.balanceOf(address(deployment.simpleLoan)), 0); + assertEq(credit.balanceOf(lender), 0); + assertEq(credit.balanceOf(borrower), 100e18); + assertEq(credit.balanceOf(address(deployment.simpleLoan)), 0); assertEq(t721.ownerOf(42), address(deployment.simpleLoan)); @@ -244,9 +433,9 @@ contract PWNSimpleLoanIntegrationTest is BaseIntegrationTest { // Assert final state assertEq(deployment.loanToken.ownerOf(loanId), lender); - assertEq(loanAsset.balanceOf(lender), 0); - assertEq(loanAsset.balanceOf(borrower), 100e18); - assertEq(loanAsset.balanceOf(address(deployment.simpleLoan)), 0); + assertEq(credit.balanceOf(lender), 0); + assertEq(credit.balanceOf(borrower), 100e18); + assertEq(credit.balanceOf(address(deployment.simpleLoan)), 0); assertEq(t1155.balanceOf(lender, 42), 0); assertEq(t1155.balanceOf(borrower, 42), 0); @@ -274,9 +463,9 @@ contract PWNSimpleLoanIntegrationTest is BaseIntegrationTest { vm.expectRevert("ERC721: invalid token ID"); deployment.loanToken.ownerOf(loanId); - assertEq(loanAsset.balanceOf(lender), 110e18); - assertEq(loanAsset.balanceOf(borrower), 0); - assertEq(loanAsset.balanceOf(address(deployment.simpleLoan)), 0); + assertEq(credit.balanceOf(lender), 110e18); + assertEq(credit.balanceOf(borrower), 0); + assertEq(credit.balanceOf(address(deployment.simpleLoan)), 0); assertEq(t1155.balanceOf(lender, 42), 0); assertEq(t1155.balanceOf(borrower, 42), 10e18); @@ -322,10 +511,10 @@ contract PWNSimpleLoanIntegrationTest is BaseIntegrationTest { vm.expectRevert("ERC721: invalid token ID"); deployment.loanToken.ownerOf(loanId); - assertEq(loanAsset.balanceOf(lender), 0); - assertEq(loanAsset.balanceOf(lender2), 110e18); - assertEq(loanAsset.balanceOf(borrower), 0); - assertEq(loanAsset.balanceOf(address(deployment.simpleLoan)), 0); + assertEq(credit.balanceOf(lender), 0); + assertEq(credit.balanceOf(lender2), 110e18); + assertEq(credit.balanceOf(borrower), 0); + assertEq(credit.balanceOf(address(deployment.simpleLoan)), 0); assertEq(t1155.balanceOf(lender, 42), 0); assertEq(t1155.balanceOf(lender2, 42), 0); @@ -348,9 +537,9 @@ contract PWNSimpleLoanIntegrationTest is BaseIntegrationTest { vm.expectRevert("ERC721: invalid token ID"); deployment.loanToken.ownerOf(loanId); - assertEq(loanAsset.balanceOf(lender), 0); - assertEq(loanAsset.balanceOf(borrower), 100e18); - assertEq(loanAsset.balanceOf(address(deployment.simpleLoan)), 0); + assertEq(credit.balanceOf(lender), 0); + assertEq(credit.balanceOf(borrower), 100e18); + assertEq(credit.balanceOf(address(deployment.simpleLoan)), 0); assertEq(t1155.balanceOf(lender, 42), 10e18); assertEq(t1155.balanceOf(borrower, 42), 0); From 989c48832eb8d4a4ef90552bdd1fbf89bb917542 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Fri, 12 Apr 2024 11:01:56 -0400 Subject: [PATCH 083/129] feat: add check for same proposer and acceptor address --- src/PWNErrors.sol | 1 + .../terms/simple/proposal/PWNSimpleLoanProposal.sol | 5 +++++ test/unit/PWNSimpleLoanProposal.t.sol | 10 ++++++++++ 3 files changed, 16 insertions(+) diff --git a/src/PWNErrors.sol b/src/PWNErrors.sol index 94e5a93..604d950 100644 --- a/src/PWNErrors.sol +++ b/src/PWNErrors.sol @@ -49,6 +49,7 @@ error InvalidSignature(address signer, bytes32 digest); // Proposal error CallerIsNotStatedProposer(address); +error AcceptorIsProposer(address addr); error InvalidRefinancingLoanId(uint256 refinancingLoanId); error AvailableCreditLimitExceeded(uint256 used, uint256 limit); error Expired(uint256 current, uint256 expiration); diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol index c4cdfa3..32f0bf4 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol @@ -215,6 +215,11 @@ abstract contract PWNSimpleLoanProposal { } } + // Check proposer is not acceptor + if (proposal.proposer == acceptor) { + revert AcceptorIsProposer({ addr: acceptor}); + } + // Check refinancing proposal if (refinancingLoanId == 0) { if (proposal.refinancingLoanId != 0) { diff --git a/test/unit/PWNSimpleLoanProposal.t.sol b/test/unit/PWNSimpleLoanProposal.t.sol index c044ae3..6e1d338 100644 --- a/test/unit/PWNSimpleLoanProposal.t.sol +++ b/test/unit/PWNSimpleLoanProposal.t.sol @@ -336,6 +336,15 @@ abstract contract PWNSimpleLoanProposal_AcceptProposal_Test is PWNSimpleLoanProp _callAcceptProposalWith(); } + function test_shouldFail_whenProposerIsSameAsAcceptor() external { + params.acceptor = proposer; + params.signature = _sign(proposerPK, _getProposalHashWith()); + + vm.expectRevert(abi.encodeWithSelector(AcceptorIsProposer.selector, proposer)); + vm.prank(activeLoanContract); + _callAcceptProposalWith(); + } + function testFuzz_shouldFail_whenProposedRefinancingLoanIdNotZero_whenRefinancingLoanIdZero(uint256 proposedRefinancingLoanId) external { vm.assume(proposedRefinancingLoanId != 0); params.base.refinancingLoanId = proposedRefinancingLoanId; @@ -533,6 +542,7 @@ abstract contract PWNSimpleLoanProposal_AcceptProposal_Test is PWNSimpleLoanProp function testFuzz_shouldFail_whenComputerRegistryReturnsComputer_whenComputerReturnsDifferentStateFingerprint( bytes32 stateFingerprint ) external { + vm.assume(stateFingerprint != params.base.collateralStateFingerprint); params.base.collateralAddress = token; params.signature = _sign(proposerPK, _getProposalHashWith()); From 78e1e11e076c88083388a8c421e8d945d763ea3b Mon Sep 17 00:00:00 2001 From: ashhanai Date: Fri, 12 Apr 2024 11:02:24 -0400 Subject: [PATCH 084/129] test: rename loan asset to credit in fork tests --- test/fork/UseCases.fork.t.sol | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/test/fork/UseCases.fork.t.sol b/test/fork/UseCases.fork.t.sol index 669fa32..b5fd9c1 100644 --- a/test/fork/UseCases.fork.t.sol +++ b/test/fork/UseCases.fork.t.sol @@ -23,7 +23,7 @@ abstract contract UseCasesTest is DeploymentTest { address BNB = 0xB8c77482e45F1F44dE1745F52C74426C631bDD52; // bool return only on transfer address DOODLE = 0x8a90CAb2b38dba80c64b7734e58Ee1dB38B8992e; - T20 loanAsset; + T20 credit; address lender = makeAddr("lender"); address borrower = makeAddr("borrower"); @@ -35,24 +35,24 @@ abstract contract UseCasesTest is DeploymentTest { super.setUp(); - loanAsset = new T20(); - loanAsset.mint(lender, 100e18); - loanAsset.mint(borrower, 100e18); + credit = new T20(); + credit.mint(lender, 100e18); + credit.mint(borrower, 100e18); vm.prank(lender); - loanAsset.approve(address(deployment.simpleLoan), type(uint256).max); + credit.approve(address(deployment.simpleLoan), type(uint256).max); vm.prank(borrower); - loanAsset.approve(address(deployment.simpleLoan), type(uint256).max); + credit.approve(address(deployment.simpleLoan), type(uint256).max); proposal = PWNSimpleLoanSimpleProposal.Proposal({ collateralCategory: MultiToken.Category.ERC20, - collateralAddress: address(loanAsset), + collateralAddress: address(credit), collateralId: 0, collateralAmount: 10e18, checkCollateralStateFingerprint: false, collateralStateFingerprint: bytes32(0), - creditAddress: address(loanAsset), + creditAddress: address(credit), creditAmount: 1e18, availableCreditLimit: 0, fixedInterestAmount: 0, @@ -171,9 +171,9 @@ contract InvalidCollateralAssetCategoryTest is UseCasesTest { } -contract InvalidLoanAssetTest is UseCasesTest { +contract InvalidCreditTest is UseCasesTest { - function testUseCase_shouldFail_whenUsingERC721AsLoanAsset() external { + function testUseCase_shouldFail_whenUsingERC721AsCredit() external { uint256 doodleId = 42; // Mock DOODLE @@ -192,7 +192,7 @@ contract InvalidLoanAssetTest is UseCasesTest { _createLoanRevertWith(abi.encodeWithSelector(InvalidMultiTokenAsset.selector, 0, DOODLE, 0, doodleId)); } - function testUseCase_shouldFail_whenUsingCryptoKittiesAsLoanAsset() external { + function testUseCase_shouldFail_whenUsingCryptoKittiesAsCredit() external { uint256 ckId = 42; // Mock CK From 6e25e552e0abb194d606dc9427e2f75a6408922e Mon Sep 17 00:00:00 2001 From: ashhanai Date: Fri, 12 Apr 2024 16:48:17 -0400 Subject: [PATCH 085/129] feat: use permit data as bytes string instead of Permit struct to not pass bunch of zero bytes when no permit --- deployments/latest.json | 4 +- foundry.toml | 3 +- src/loan/terms/simple/loan/PWNSimpleLoan.sol | 38 +++++--- src/loan/vault/PWNVault.sol | 2 +- test/fork/UseCases.fork.t.sol | 26 +----- test/integration/BaseIntegrationTest.t.sol | 16 +--- .../PWNSimpleLoanIntegration.t.sol | 32 +------ test/unit/PWNSimpleLoan.t.sol | 88 +++++++++---------- 8 files changed, 79 insertions(+), 130 deletions(-) diff --git a/deployments/latest.json b/deployments/latest.json index 27b02d1..4315558 100644 --- a/deployments/latest.json +++ b/deployments/latest.json @@ -1,7 +1,7 @@ { - "deployedChains": [1], + "deployedChains": [162314], "chains": { - "1": { + "162314": { "dao": "0x1B8383D2726E7e18189205337424a2631A2102F4", "productTimelock": "0xd57e72A328AB1deC6b374c2babe2dc489819B5Ea", "protocolTimelock": "0xd8dbdDf1c0FDdf9b5eCFA5C067C38DB66739FBAB", diff --git a/foundry.toml b/foundry.toml index c1de7e3..8219cfc 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,6 +1,7 @@ [profile.default] solc_version = '0.8.16' fs_permissions = [{ access = "read", path = "./deployments/latest.json"}] +gas_reports = ["PWNSimpleLoan"] [rpc_endpoints] @@ -21,5 +22,5 @@ base_goerli = "${BASE_GOERLI_URL}" cronos_testnet = "${CRONOS_TESTNET_URL}" mantle_testnet = "${MANTLE_TESTNET_URL}" -tenderly = "${TENDERLY_URL}" +tenderly = "${TENDERLY_URL}" # chain_id = 162314 local = "${LOCAL_URL}" diff --git a/src/loan/terms/simple/loan/PWNSimpleLoan.sol b/src/loan/terms/simple/loan/PWNSimpleLoan.sol index dfd3a70..ece7909 100644 --- a/src/loan/terms/simple/loan/PWNSimpleLoan.sol +++ b/src/loan/terms/simple/loan/PWNSimpleLoan.sol @@ -119,13 +119,13 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { * @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. + * @param permitData Callers permit data for a loans credit asset. */ struct CallerSpec { uint256 refinancingLoanId; bool revokeNonce; uint256 nonce; - Permit permit; + bytes permitData; } /** @@ -338,8 +338,11 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { }); // Execute permit for the caller - _checkPermit(msg.sender, loanTerms.credit.assetAddress, callerSpec.permit); - _tryPermit(callerSpec.permit); + if (callerSpec.permitData.length > 0) { + Permit memory permit = abi.decode(callerSpec.permitData, (Permit)); + _checkPermit(msg.sender, loanTerms.credit.assetAddress, permit); + _tryPermit(permit); + } // Settle the loan if (callerSpec.refinancingLoanId == 0) { @@ -366,10 +369,10 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { * @param creditAddress Address of a credit to be used. * @param permit Permit to be checked. */ - function _checkPermit(address caller, address creditAddress, Permit calldata permit) private pure { + function _checkPermit(address caller, address creditAddress, Permit memory permit) private pure { if (permit.asset != address(0)) { if (permit.owner != caller) { - revert InvalidPermitOwner({ current: permit.owner, expected: caller}); + revert InvalidPermitOwner({ current: permit.owner, expected: caller }); } if (permit.asset != creditAddress) { revert InvalidPermitAsset({ current: permit.asset, expected: creditAddress }); @@ -610,11 +613,12 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { * a vault, waiting on a LOAN token holder to claim it. The function assumes a prior token approval to a contract address * or a signed permit. * @param loanId Id of a loan that is being repaid. - * @param permit Callers credit permit data. + * @param permitData Callers credit permit data. */ function repayLOAN( uint256 loanId, - Permit calldata permit + bytes calldata permitData + // Permit calldata permit ) external { LOAN storage loan = LOANs[loanId]; @@ -624,8 +628,11 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { _updateRepaidLoan(loanId); // Execute permit for the caller - _checkPermit(msg.sender, loan.creditAddress, permit); - _tryPermit(permit); + if (permitData.length > 0) { + Permit memory permit = abi.decode(permitData, (Permit)); + _checkPermit(msg.sender, loan.creditAddress, permit); + _tryPermit(permit); + } // Transfer the repaid credit to the Vault _pull(loan.creditAddress.ERC20(_loanRepaymentAmount(loanId)), msg.sender); @@ -868,12 +875,12 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { * @dev The function assumes a prior token approval to a contract address or a signed permit. * @param extension Extension proposal struct. * @param signature Signature of the extension proposal. - * @param permit Callers permit. + * @param permitData Callers credit permit data. */ function extendLOAN( ExtensionProposal calldata extension, bytes calldata signature, - Permit calldata permit + bytes calldata permitData ) external { LOAN storage loan = LOANs[extension.loanId]; @@ -954,8 +961,11 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { _checkValidAsset(compensation); // Transfer compensation to the loan owner - _checkPermit(msg.sender, extension.compensationAddress, permit); - _tryPermit(permit); + if (permitData.length > 0) { + Permit memory permit = abi.decode(permitData, (Permit)); + _checkPermit(msg.sender, extension.compensationAddress, permit); + _tryPermit(permit); + } _pushFrom(compensation, loan.borrower, loanOwner); } } diff --git a/src/loan/vault/PWNVault.sol b/src/loan/vault/PWNVault.sol index 80c32c6..f1f988c 100644 --- a/src/loan/vault/PWNVault.sol +++ b/src/loan/vault/PWNVault.sol @@ -150,7 +150,7 @@ abstract contract PWNVault is IERC721Receiver, IERC1155Receiver { * @dev If the permit execution fails, the function will not revert. * @param permit The permit data. */ - function _tryPermit(Permit calldata permit) internal { + function _tryPermit(Permit memory permit) internal { if (permit.asset != address(0)) { try IERC20Permit(permit.asset).permit({ owner: permit.owner, diff --git a/test/fork/UseCases.fork.t.sol b/test/fork/UseCases.fork.t.sol index b5fd9c1..7ad91b5 100644 --- a/test/fork/UseCases.fork.t.sol +++ b/test/fork/UseCases.fork.t.sol @@ -31,7 +31,7 @@ abstract contract UseCasesTest is DeploymentTest { function setUp() public override { - vm.createSelectFork("tenderly"); + vm.createSelectFork("mainnet"); super.setUp(); @@ -101,13 +101,7 @@ abstract contract UseCasesTest is DeploymentTest { refinancingLoanId: 0, revokeNonce: false, nonce: 0, - permit: Permit({ - asset: address(0), - owner: address(0), - amount: 0, - deadline: 0, - v: 0, r: 0, s: 0 - }) + permitData: "" }), extra: "" }); @@ -291,13 +285,7 @@ contract IncompleteERC20TokensTest is UseCasesTest { vm.prank(borrower); deployment.simpleLoan.repayLOAN({ loanId: loanId, - permit: Permit({ - asset: address(0), - owner: address(0), - amount: 0, - deadline: 0, - v: 0, r: 0, s: 0 - }) + permitData: "" }); // Check balance @@ -341,13 +329,7 @@ contract IncompleteERC20TokensTest is UseCasesTest { vm.prank(borrower); deployment.simpleLoan.repayLOAN({ loanId: loanId, - permit: Permit({ - asset: address(0), - owner: address(0), - amount: 0, - deadline: 0, - v: 0, r: 0, s: 0 - }) + permitData: "" }); // Check balance - repaid directly to lender diff --git a/test/integration/BaseIntegrationTest.t.sol b/test/integration/BaseIntegrationTest.t.sol index d72b91f..e97a2a2 100644 --- a/test/integration/BaseIntegrationTest.t.sol +++ b/test/integration/BaseIntegrationTest.t.sol @@ -160,13 +160,7 @@ abstract contract BaseIntegrationTest is DeploymentTest { refinancingLoanId: 0, revokeNonce: false, nonce: 0, - permit: Permit({ - asset: address(0), - owner: address(0), - amount: 0, - deadline: 0, - v: 0, r: 0, s: 0 - }) + permitData: "" }), extra: "" }); @@ -193,13 +187,7 @@ abstract contract BaseIntegrationTest is DeploymentTest { vm.prank(borrower); deployment.simpleLoan.repayLOAN({ loanId: loanId, - permit: Permit({ - asset: address(0), - owner: address(0), - amount: 0, - deadline: 0, - v: 0, r: 0, s: 0 - }) + permitData: "" }); } diff --git a/test/integration/PWNSimpleLoanIntegration.t.sol b/test/integration/PWNSimpleLoanIntegration.t.sol index 7f6f491..ebec89a 100644 --- a/test/integration/PWNSimpleLoanIntegration.t.sol +++ b/test/integration/PWNSimpleLoanIntegration.t.sol @@ -73,13 +73,7 @@ contract PWNSimpleLoanIntegrationTest is BaseIntegrationTest { refinancingLoanId: 0, revokeNonce: false, nonce: 0, - permit: Permit({ - asset: address(0), - owner: address(0), - amount: 0, - deadline: 0, - v: 0, r: 0, s: 0 - }) + permitData: "" }), extra: "" }); @@ -170,13 +164,7 @@ contract PWNSimpleLoanIntegrationTest is BaseIntegrationTest { refinancingLoanId: 0, revokeNonce: false, nonce: 0, - permit: Permit({ - asset: address(0), - owner: address(0), - amount: 0, - deadline: 0, - v: 0, r: 0, s: 0 - }) + permitData: "" }), extra: "" }); @@ -262,13 +250,7 @@ contract PWNSimpleLoanIntegrationTest is BaseIntegrationTest { refinancingLoanId: 0, revokeNonce: false, nonce: 0, - permit: Permit({ - asset: address(0), - owner: address(0), - amount: 0, - deadline: 0, - v: 0, r: 0, s: 0 - }) + permitData: "" }), extra: "" }); @@ -362,13 +344,7 @@ contract PWNSimpleLoanIntegrationTest is BaseIntegrationTest { refinancingLoanId: 0, revokeNonce: false, nonce: 0, - permit: Permit({ - asset: address(0), - owner: address(0), - amount: 0, - deadline: 0, - v: 0, r: 0, s: 0 - }) + permitData: "" }), extra: "" }); diff --git a/test/unit/PWNSimpleLoan.t.sol b/test/unit/PWNSimpleLoan.t.sol index 1c3ab44..aff404f 100644 --- a/test/unit/PWNSimpleLoan.t.sol +++ b/test/unit/PWNSimpleLoan.t.sol @@ -537,7 +537,7 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { permit.asset = simpleLoan.creditAddress; permit.owner = permitOwner; - callerSpec.permit = permit; + callerSpec.permitData = abi.encode(permit); vm.expectRevert(abi.encodeWithSelector(InvalidPermitOwner.selector, permitOwner, borrower)); vm.prank(borrower); @@ -554,7 +554,7 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { permit.asset = permitAsset; permit.owner = borrower; - callerSpec.permit = permit; + callerSpec.permitData = abi.encode(permit); vm.expectRevert(abi.encodeWithSelector(InvalidPermitAsset.selector, permitAsset, simpleLoan.creditAddress)); vm.prank(borrower); @@ -575,7 +575,7 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { permit.r = bytes32(uint256(2)); permit.s = bytes32(uint256(3)); - callerSpec.permit = permit; + callerSpec.permitData = abi.encode(permit); vm.expectCall( permit.asset, @@ -1623,7 +1623,7 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { _mockLOAN(loanId, simpleLoan); vm.expectRevert(abi.encodeWithSelector(NonExistingLoan.selector)); - loan.repayLOAN(loanId, permit); + loan.repayLOAN(loanId, ""); } function testFuzz_shouldFail_whenLoanIsNotRunning(uint8 status) external { @@ -1633,14 +1633,14 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { _mockLOAN(loanId, simpleLoan); vm.expectRevert(abi.encodeWithSelector(InvalidLoanStatus.selector, status)); - loan.repayLOAN(loanId, permit); + loan.repayLOAN(loanId, ""); } function test_shouldFail_whenLoanIsDefaulted() external { vm.warp(simpleLoan.defaultTimestamp); vm.expectRevert(abi.encodeWithSelector(LoanDefaulted.selector, simpleLoan.defaultTimestamp)); - loan.repayLOAN(loanId, permit); + loan.repayLOAN(loanId, ""); } function testFuzz_shouldFail_whenInvalidPermitOwner_whenPermitProvided(address permitOwner) external { @@ -1648,11 +1648,9 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { 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); + loan.repayLOAN(loanId, abi.encode(permit)); } function testFuzz_shouldFail_whenInvalidPermitAsset_whenPermitProvided(address permitAsset) external { @@ -1660,11 +1658,9 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { 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); + loan.repayLOAN(loanId, abi.encode(permit)); } function test_shouldCallPermit_whenPermitProvided() external { @@ -1685,7 +1681,7 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { ); vm.prank(borrower); - loan.repayLOAN(loanId, permit); + loan.repayLOAN(loanId, abi.encode(permit)); } function testFuzz_shouldUpdateLoanData_whenLOANOwnerIsNotOriginalLender( @@ -1709,7 +1705,7 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { fungibleAsset.mint(borrower, loanRepaymentAmount); vm.prank(borrower); - loan.repayLOAN(loanId, permit); + loan.repayLOAN(loanId, ""); // Update loan and compare simpleLoan.status = 3; // move loan to repaid state @@ -1719,7 +1715,7 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { } function test_shouldDeleteLoanData_whenLOANOwnerIsOriginalLender() external { - loan.repayLOAN(loanId, permit); + loan.repayLOAN(loanId, ""); _assertLOANEq(loanId, nonExistingLoan); } @@ -1727,7 +1723,7 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { function test_shouldBurnLOANToken_whenLOANOwnerIsOriginalLender() external { vm.expectCall(loanToken, abi.encodeWithSignature("burn(uint256)", loanId)); - loan.repayLOAN(loanId, permit); + loan.repayLOAN(loanId, ""); } function testFuzz_shouldTransferRepaidAmountToVault( @@ -1756,7 +1752,7 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { ); vm.prank(borrower); - loan.repayLOAN(loanId, permit); + loan.repayLOAN(loanId, ""); } function test_shouldTransferCollateralToBorrower() external { @@ -1768,14 +1764,14 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { ) ); - loan.repayLOAN(loanId, permit); + loan.repayLOAN(loanId, ""); } function test_shouldEmit_LOANPaidBack() external { vm.expectEmit(); emit LOANPaidBack(loanId); - loan.repayLOAN(loanId, permit); + loan.repayLOAN(loanId, ""); } function testFuzz_shouldCall_tryClaimRepaidLOANForLoanOwner(address loanOwner) external { @@ -1787,7 +1783,7 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { abi.encodeWithSignature("tryClaimRepaidLOANForLoanOwner(uint256,address)", loanId, loanOwner) ); - loan.repayLOAN(loanId, permit); + loan.repayLOAN(loanId, ""); } function test_shouldNotFail_whenTryClaimRepaidLOANForLoanOwnerFails() external { @@ -1797,7 +1793,7 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { "" ); - loan.repayLOAN(loanId, permit); + loan.repayLOAN(loanId, ""); simpleLoan.status = 3; simpleLoan.fixedInterestAmount = loan.loanRepaymentAmount(loanId) - simpleLoan.principalAmount; @@ -1809,7 +1805,7 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { vm.expectEmit(); emit LOANClaimed(loanId, false); - loan.repayLOAN(loanId, permit); + loan.repayLOAN(loanId, ""); } } @@ -2243,7 +2239,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { vm.expectRevert(abi.encodeWithSelector(NonExistingLoan.selector)); vm.prank(lender); - loan.extendLOAN(extension, "", permit); + loan.extendLOAN(extension, "", ""); } function test_shouldFail_whenLoanIsRepaid() external { @@ -2252,7 +2248,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { vm.expectRevert(abi.encodeWithSelector(InvalidLoanStatus.selector, 3)); vm.prank(lender); - loan.extendLOAN(extension, "", permit); + loan.extendLOAN(extension, "", ""); } function testFuzz_shouldFail_whenInvalidSignature_whenEOA(uint256 pk) external { @@ -2261,7 +2257,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, extension.proposer, _extensionHash(extension))); vm.prank(lender); - loan.extendLOAN(extension, _signExtension(pk, extension), permit); + loan.extendLOAN(extension, _signExtension(pk, extension), ""); } function testFuzz_shouldFail_whenOfferExpirated(uint40 expiration) external { @@ -2273,7 +2269,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { vm.expectRevert(abi.encodeWithSelector(Expired.selector, block.timestamp, extension.expiration)); vm.prank(lender); - loan.extendLOAN(extension, "", permit); + loan.extendLOAN(extension, "", ""); } function test_shouldFail_whenOfferNonceNotUsable() external { @@ -2289,7 +2285,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { NonceNotUsable.selector, extension.proposer, extension.nonceSpace, extension.nonce )); vm.prank(lender); - loan.extendLOAN(extension, "", permit); + loan.extendLOAN(extension, "", ""); } function testFuzz_shouldFail_whenCallerIsNotBorrowerNorLoanOwner(address caller) external { @@ -2298,7 +2294,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { vm.expectRevert(abi.encodeWithSelector(InvalidExtensionCaller.selector)); vm.prank(caller); - loan.extendLOAN(extension, "", permit); + loan.extendLOAN(extension, "", ""); } function testFuzz_shouldFail_whenCallerIsBorrower_andProposerIsNotLoanOwner(address proposer) external { @@ -2309,7 +2305,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { vm.expectRevert(abi.encodeWithSelector(InvalidExtensionSigner.selector, lender, proposer)); vm.prank(borrower); - loan.extendLOAN(extension, "", permit); + loan.extendLOAN(extension, "", ""); } function testFuzz_shouldFail_whenCallerIsLoanOwner_andProposerIsNotBorrower(address proposer) external { @@ -2320,7 +2316,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { vm.expectRevert(abi.encodeWithSelector(InvalidExtensionSigner.selector, borrower, proposer)); vm.prank(lender); - loan.extendLOAN(extension, "", permit); + loan.extendLOAN(extension, "", ""); } function testFuzz_shouldFail_whenExtensionDurationLessThanMin(uint40 duration) external { @@ -2332,7 +2328,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { vm.expectRevert(abi.encodeWithSelector(InvalidExtensionDuration.selector, duration, minDuration)); vm.prank(lender); - loan.extendLOAN(extension, "", permit); + loan.extendLOAN(extension, "", ""); } function testFuzz_shouldFail_whenExtensionDurationMoreThanMax(uint40 duration) external { @@ -2344,7 +2340,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { vm.expectRevert(abi.encodeWithSelector(InvalidExtensionDuration.selector, duration, maxDuration)); vm.prank(lender); - loan.extendLOAN(extension, "", permit); + loan.extendLOAN(extension, "", ""); } function testFuzz_shouldRevokeExtensionNonce(uint256 nonceSpace, uint256 nonce) external { @@ -2358,7 +2354,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { ); vm.prank(lender); - loan.extendLOAN(extension, "", permit); + loan.extendLOAN(extension, "", ""); } function testFuzz_shouldUpdateLoanData(uint40 duration) external { @@ -2368,7 +2364,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { _mockExtensionProposalMade(extension); vm.prank(lender); - loan.extendLOAN(extension, "", permit); + loan.extendLOAN(extension, "", ""); simpleLoan.defaultTimestamp = simpleLoan.defaultTimestamp + duration; _assertLOANEq(loanId, simpleLoan); @@ -2384,7 +2380,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { emit LOANExtended(loanId, simpleLoan.defaultTimestamp, simpleLoan.defaultTimestamp + duration); vm.prank(lender); - loan.extendLOAN(extension, "", permit); + loan.extendLOAN(extension, "", ""); } function test_shouldNotTransferCredit_whenAmountZero() external { @@ -2399,7 +2395,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { }); vm.prank(lender); - loan.extendLOAN(extension, "", permit); + loan.extendLOAN(extension, "", ""); } function test_shouldNotTransferCredit_whenAddressZero() external { @@ -2414,7 +2410,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { }); vm.prank(lender); - loan.extendLOAN(extension, "", permit); + loan.extendLOAN(extension, "", ""); } function test_shouldFail_whenInvalidCompensationAsset() external { @@ -2430,7 +2426,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { vm.expectRevert(abi.encodeWithSelector(InvalidMultiTokenAsset.selector, 0, extension.compensationAddress, 0, extension.compensationAmount)); vm.prank(lender); - loan.extendLOAN(extension, "", permit); + loan.extendLOAN(extension, "", ""); } function testFuzz_shouldFail_whenInvalidPermitData_permitOwner(address permitOwner) external { @@ -2440,11 +2436,9 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { 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); + loan.extendLOAN(extension, "", abi.encode(permit)); } function testFuzz_shouldFail_whenInvalidPermitData_permitAsset(address permitAsset) external { @@ -2454,11 +2448,9 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { 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); + loan.extendLOAN(extension, "", abi.encode(permit)); } function test_shouldCallPermit_whenProvided() external { @@ -2481,7 +2473,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { ); vm.prank(lender); - loan.extendLOAN(extension, "", permit); + loan.extendLOAN(extension, "", abi.encode(permit)); } function testFuzz_shouldTransferCompensation_whenDefined(uint256 amount) external { @@ -2503,21 +2495,21 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { ); vm.prank(lender); - loan.extendLOAN(extension, "", permit); + loan.extendLOAN(extension, "", ""); } function test_shouldPass_whenBorrowerSignature_whenLenderAccepts() external { extension.proposer = borrower; vm.prank(lender); - loan.extendLOAN(extension, _signExtension(borrowerPk, extension), permit); + loan.extendLOAN(extension, _signExtension(borrowerPk, extension), ""); } function test_shouldPass_whenLenderSignature_whenBorrowerAccepts() external { extension.proposer = lender; vm.prank(borrower); - loan.extendLOAN(extension, _signExtension(lenderPk, extension), permit); + loan.extendLOAN(extension, _signExtension(lenderPk, extension), ""); } } From 9180d526bd8058a2cb0a13a43f2c0484aed65732 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Fri, 12 Apr 2024 17:20:35 -0400 Subject: [PATCH 086/129] refactor: update PWNHub and PWNLOAN imports --- src/hub/PWNHub.sol | 2 +- src/loan/token/PWNLOAN.sol | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/hub/PWNHub.sol b/src/hub/PWNHub.sol index 16bfc39..ff7c9ba 100644 --- a/src/hub/PWNHub.sol +++ b/src/hub/PWNHub.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; -import "openzeppelin-contracts/contracts/access/Ownable2Step.sol"; +import { Ownable2Step } from "openzeppelin-contracts/contracts/access/Ownable2Step.sol"; import "@pwn/PWNErrors.sol"; diff --git a/src/loan/token/PWNLOAN.sol b/src/loan/token/PWNLOAN.sol index 6701dca..7bfc999 100644 --- a/src/loan/token/PWNLOAN.sol +++ b/src/loan/token/PWNLOAN.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; -import "openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; +import { ERC721 } from "openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; -import "@pwn/hub/PWNHubAccessControl.sol"; -import "@pwn/loan/token/IERC5646.sol"; -import "@pwn/loan/token/IPWNLoanMetadataProvider.sol"; +import { PWNHubAccessControl } from "@pwn/hub/PWNHubAccessControl.sol"; +import { IERC5646 } from "@pwn/loan/token/IERC5646.sol"; +import { IPWNLoanMetadataProvider } from "@pwn/loan/token/IPWNLoanMetadataProvider.sol"; import "@pwn/PWNErrors.sol"; From 6dd9b4fc658b080ec99b2498b2df9547bf6b618a Mon Sep 17 00:00:00 2001 From: ashhanai Date: Sat, 13 Apr 2024 15:56:50 -0400 Subject: [PATCH 087/129] ci(timelocks): use product timelock as admin timelock --- deployments/latest.json | 2 +- script/PWN.s.sol | 21 ++++++------ script/PWNTimelock.s.sol | 47 ++++++++++++++------------- src/Deployments.sol | 2 +- test/fork/DeployedProtocol.fork.t.sol | 41 ++++++++++++----------- 5 files changed, 59 insertions(+), 54 deletions(-) diff --git a/deployments/latest.json b/deployments/latest.json index 4315558..83eed2c 100644 --- a/deployments/latest.json +++ b/deployments/latest.json @@ -3,7 +3,7 @@ "chains": { "162314": { "dao": "0x1B8383D2726E7e18189205337424a2631A2102F4", - "productTimelock": "0xd57e72A328AB1deC6b374c2babe2dc489819B5Ea", + "adminTimelock": "0xd57e72A328AB1deC6b374c2babe2dc489819B5Ea", "protocolTimelock": "0xd8dbdDf1c0FDdf9b5eCFA5C067C38DB66739FBAB", "protocolSafe": "0x61a77B19b7F4dB82222625D7a969698894d77473", "daoSafe": "0xd56635c0E91D31F88B89F195D3993a9e34516e59", diff --git a/script/PWN.s.sol b/script/PWN.s.sol index 5b91f45..ce6989d 100644 --- a/script/PWN.s.sol +++ b/script/PWN.s.sol @@ -103,7 +103,8 @@ forge script script/PWN.s.sol:Deploy \ require(address(deployment.deployer) != address(0), "Deployer not set"); require(deployment.deployerSafe != address(0), "Deployer safe not set"); - require(deployment.protocolSafe != address(0), "Protocol safe not set"); + require(deployment.adminTimelock != address(0), "Admin timelock not set"); + require(deployment.protocolTimelock != address(0), "Protocol timelock not set"); require(deployment.daoSafe != address(0), "DAO safe not set"); require(address(deployment.hub) != address(0), "Hub not set"); require(address(deployment.loanToken) != address(0), "LOAN token not set"); @@ -138,9 +139,9 @@ forge script script/PWN.s.sol:Deploy \ vm.startBroadcast(initialConfigHelper); ITransparentUpgradeableProxy(address(deployment.config)).upgradeToAndCall( configSingleton, - abi.encodeWithSelector(PWNConfig.initialize.selector, deployment.daoSafe, 0, deployment.daoSafe) + abi.encodeWithSelector(PWNConfig.initialize.selector, deployment.protocolTimelock, 0, deployment.daoSafe) ); - ITransparentUpgradeableProxy(address(deployment.config)).changeAdmin(deployment.protocolSafe); + ITransparentUpgradeableProxy(address(deployment.config)).changeAdmin(deployment.adminTimelock); vm.stopBroadcast(); @@ -149,7 +150,7 @@ forge script script/PWN.s.sol:Deploy \ // - MultiToken category registry deployment.categoryRegistry = MultiTokenCategoryRegistry(_deployAndTransferOwnership({ // Need ownership acceptance from the new owner salt: PWNContractDeployerSalt.CONFIG, - owner: deployment.protocolSafe, + owner: deployment.protocolTimelock, bytecode: type(MultiTokenCategoryRegistry).creationCode })); @@ -257,9 +258,9 @@ forge script script/PWN.s.sol:Deploy \ require(address(deployment.deployer) != address(0), "Deployer not set"); require(deployment.deployerSafe != address(0), "Deployer safe not set"); - require(deployment.protocolSafe != address(0), "Protocol safe not set"); + require(deployment.adminTimelock != address(0), "Admin timelock not set"); + require(deployment.protocolTimelock != address(0), "Protocol timelock not set"); require(deployment.daoSafe != address(0), "DAO safe not set"); - require(address(deployment.categoryRegistry) != address(0), "Category registry not set"); uint256 initialConfigHelper = vmSafe.envUint("INITIAL_CONFIG_HELPER"); @@ -291,9 +292,9 @@ forge script script/PWN.s.sol:Deploy \ vm.startBroadcast(initialConfigHelper); ITransparentUpgradeableProxy(address(deployment.config)).upgradeToAndCall( configSingleton, - abi.encodeWithSelector(PWNConfig.initialize.selector, deployment.daoSafe, 0, deployment.daoSafe) + abi.encodeWithSelector(PWNConfig.initialize.selector, deployment.protocolTimelock, 0, deployment.daoSafe) ); - ITransparentUpgradeableProxy(address(deployment.config)).changeAdmin(deployment.protocolSafe); + ITransparentUpgradeableProxy(address(deployment.config)).changeAdmin(deployment.adminTimelock); vm.stopBroadcast(); @@ -302,14 +303,14 @@ forge script script/PWN.s.sol:Deploy \ // - MultiToken category registry deployment.categoryRegistry = MultiTokenCategoryRegistry(_deployAndTransferOwnership({ // Need ownership acceptance from the new owner salt: PWNContractDeployerSalt.CONFIG, - owner: deployment.protocolSafe, + owner: deployment.protocolTimelock, bytecode: type(MultiTokenCategoryRegistry).creationCode })); // - Hub deployment.hub = PWNHub(_deployAndTransferOwnership({ // Need ownership acceptance from the new owner salt: PWNContractDeployerSalt.HUB, - owner: deployment.protocolSafe, + owner: deployment.protocolTimelock, bytecode: type(PWNHub).creationCode })); diff --git a/script/PWNTimelock.s.sol b/script/PWNTimelock.s.sol index 8453bdd..2f46c6a 100644 --- a/script/PWNTimelock.s.sol +++ b/script/PWNTimelock.s.sol @@ -21,8 +21,9 @@ library PWNDeployerSalt { // 0x608ebbaa27bfbe8dd5ce387b0590cab114c16a47f29d4df2aff471dff0da44cc bytes32 internal constant PROTOCOL_TIMELOCK = keccak256("PWNProtocolTimelock"); + // Note: Just renaming product timelock, thus using the same salt because it's already deployed on several chains. // 0xd7150558706b0331a55357de4d842961470f283908b8ca35618c3cdbb470da18 - bytes32 internal constant PRODUCT_TIMELOCK = keccak256("PWNProductTimelock"); + bytes32 internal constant ADMIN_TIMELOCK = keccak256("PWNProductTimelock"); } @@ -64,16 +65,16 @@ forge script script/PWNTimelock.s.sol:Deploy \ /* forge script script/PWNTimelock.s.sol:Deploy \ ---sig "deployProductTimelock()" \ +--sig "deployAdminTimelock()" \ --rpc-url $RPC_URL \ --private-key $PRIVATE_KEY \ --with-gas-price $(cast --to-wei 15 gwei) \ --verify --etherscan-api-key $ETHERSCAN_API_KEY \ --broadcast */ - function deployProductTimelock() external { - console2.log("Deploying product timelock"); - _deployTimelock(PWNDeployerSalt.PRODUCT_TIMELOCK); + function deployAdminTimelock() external { + console2.log("Deploying admin timelock"); + _deployTimelock(PWNDeployerSalt.ADMIN_TIMELOCK); } /// @dev Expecting to have deployer & deployerSafe addresses set in the `deployments.json` @@ -123,21 +124,21 @@ forge script script/PWNTimelock.s.sol:Setup \ function updateProtocolTimelockProposer() external { _loadDeployedAddresses(); console2.log("Updating protocol timelock proposer (%s)", deployment.protocolTimelock); - _updateProposer(TimelockController(payable(deployment.protocolTimelock)), deployment.protocolSafe); + _updateProposer(TimelockController(payable(deployment.protocolTimelock)), deployment.daoSafe); } /* forge script script/PWNTimelock.s.sol:Setup \ ---sig "updateProductTimelockProposer()" \ +--sig "updateAdminTimelockProposer()" \ --rpc-url $RPC_URL \ --private-key $PRIVATE_KEY \ --with-gas-price $(cast --to-wei 15 gwei) \ --broadcast */ - function updateProductTimelockProposer() external { + function updateAdminTimelockProposer() external { _loadDeployedAddresses(); - console2.log("Updating product timelock proposer (%s)", deployment.productTimelock); - _updateProposer(TimelockController(payable(deployment.productTimelock)), deployment.daoSafe); + console2.log("Updating product timelock proposer (%s)", deployment.adminTimelock); + _updateProposer(TimelockController(payable(deployment.adminTimelock)), deployment.daoSafe); } /// @dev Will grant PROPOSER_ROLE & CANCELLOR_ROLE to the new address and revoke them from `0x0cfC...D6de`. @@ -209,7 +210,7 @@ forge script script/PWNTimelock.s.sol:Setup \ function setupProtocolTimelock() external { _loadDeployedAddresses(); - uint256 protocolTimelockMinDelay = 345_600; // 4 days + uint256 protocolTimelockMinDelay = 4 days; vm.startBroadcast(); @@ -271,18 +272,18 @@ forge script script/PWNTimelock.s.sol:Setup \ /* forge script script/PWNTimelock.s.sol:Setup \ ---sig "setProductTimelock()" \ +--sig "setAdminTimelock()" \ --rpc-url $RPC_URL \ --private-key $PRIVATE_KEY \ --with-gas-price $(cast --to-wei 15 gwei) \ --broadcast */ - /// @dev Expecting to have protocol, daoSafe & productTimelock addresses set in the `deployments.json` + /// @dev Expecting to have protocol, daoSafe & adminTimelock addresses set in the `deployments.json` /// Expecting `0x0cfC...D6de` to be a proposer for the timelock - function setProductTimelock() external { + function setAdminTimelock() external { _loadDeployedAddresses(); - uint256 productTimelockMinDelay = 345_600; // 4 days + uint256 adminTimelockMinDelay = 4 days; vm.startBroadcast(); @@ -290,13 +291,13 @@ forge script script/PWNTimelock.s.sol:Setup \ bool success; success = GnosisSafeLike(deployment.daoSafe).execTransaction({ to: address(deployment.config), - data: abi.encodeWithSignature("transferOwnership(address)", deployment.productTimelock) + data: abi.encodeWithSignature("transferOwnership(address)", deployment.adminTimelock) }); require(success, "PWN: change owner failed"); // accept PWNConfig owner success = GnosisSafeLike(deployment.daoSafe).execTransaction({ - to: address(deployment.productTimelock), + to: address(deployment.adminTimelock), data: abi.encodeWithSignature( "schedule(address,uint256,bytes,bytes32,bytes32,uint256)", address(deployment.config), 0, abi.encodeWithSignature("acceptOwnership()"), 0, 0, 0 @@ -304,7 +305,7 @@ forge script script/PWNTimelock.s.sol:Setup \ }); require(success, "PWN: schedule failed"); - TimelockController(payable(deployment.productTimelock)).execute({ + TimelockController(payable(deployment.adminTimelock)).execute({ target: address(deployment.config), value: 0, payload: abi.encodeWithSignature("acceptOwnership()"), @@ -314,18 +315,18 @@ forge script script/PWNTimelock.s.sol:Setup \ // set min delay success = GnosisSafeLike(deployment.daoSafe).execTransaction({ - to: address(deployment.productTimelock), + to: address(deployment.adminTimelock), data: abi.encodeWithSignature( "schedule(address,uint256,bytes,bytes32,bytes32,uint256)", - address(deployment.productTimelock), 0, abi.encodeWithSignature("updateDelay(uint256)", productTimelockMinDelay), 0, 0, 0 + address(deployment.adminTimelock), 0, abi.encodeWithSignature("updateDelay(uint256)", adminTimelockMinDelay), 0, 0, 0 ) }); require(success, "PWN: update delay failed"); - TimelockController(payable(deployment.productTimelock)).execute({ - target: deployment.productTimelock, + TimelockController(payable(deployment.adminTimelock)).execute({ + target: deployment.adminTimelock, value: 0, - payload: abi.encodeWithSignature("updateDelay(uint256)", productTimelockMinDelay), + payload: abi.encodeWithSignature("updateDelay(uint256)", adminTimelockMinDelay), predecessor: 0, salt: 0 }); diff --git a/src/Deployments.sol b/src/Deployments.sol index 225f127..75820b6 100644 --- a/src/Deployments.sol +++ b/src/Deployments.sol @@ -30,6 +30,7 @@ abstract contract Deployments is CommonBase { // Properties need to be in alphabetical order struct Deployment { + address adminTimelock; IMultiTokenCategoryRegistry categoryRegistry; PWNConfig config; PWNConfig configSingleton; @@ -39,7 +40,6 @@ abstract contract Deployments is CommonBase { address deployerSafe; PWNHub hub; PWNLOAN loanToken; - address productTimelock; address protocolSafe; address protocolTimelock; PWNRevokedNonce revokedNonce; diff --git a/test/fork/DeployedProtocol.fork.t.sol b/test/fork/DeployedProtocol.fork.t.sol index 258d9c0..677c336 100644 --- a/test/fork/DeployedProtocol.fork.t.sol +++ b/test/fork/DeployedProtocol.fork.t.sol @@ -27,34 +27,33 @@ contract DeployedProtocolTest is DeploymentTest { } // TIMELOCK CONTROLLERS - address protocolTimelockOwner = deployment.dao == address(0) ? deployment.protocolSafe : deployment.dao; + address timelockOwner = deployment.dao == address(0) ? deployment.daoSafe : deployment.dao; TimelockController protocolTimelockController = TimelockController(payable(deployment.protocolTimelock)); // - protocol timelock has min delay of 4 days assertEq(protocolTimelockController.getMinDelay(), 4 days); - // - protocol safe has PROPOSER role in protocol timelock - assertTrue(protocolTimelockController.hasRole(PROPOSER_ROLE, protocolTimelockOwner)); - // - protocol safe has CANCELLER role in protocol timelock - assertTrue(protocolTimelockController.hasRole(CANCELLER_ROLE, protocolTimelockOwner)); + // - dao or dao safe has PROPOSER role in protocol timelock + assertTrue(protocolTimelockController.hasRole(PROPOSER_ROLE, timelockOwner)); + // - dao or dao safe has CANCELLER role in protocol timelock + assertTrue(protocolTimelockController.hasRole(CANCELLER_ROLE, timelockOwner)); // - everybody has EXECUTOR role in protocol timelock assertTrue(protocolTimelockController.hasRole(EXECUTOR_ROLE, address(0))); - address productTimelockOwner = deployment.dao == address(0) ? deployment.daoSafe : deployment.dao; - TimelockController productTimelockController = TimelockController(payable(deployment.productTimelock)); - // - product timelock has min delay of 4 days - assertEq(productTimelockController.getMinDelay(), 4 days); - // - dao safe has PROPOSER role in product timelock - assertTrue(productTimelockController.hasRole(PROPOSER_ROLE, productTimelockOwner)); - // - dao safe has CANCELLER role in product timelock - assertTrue(productTimelockController.hasRole(CANCELLER_ROLE, productTimelockOwner)); + TimelockController adminTimelockController = TimelockController(payable(deployment.adminTimelock)); + // - admin timelock has min delay of 4 days + assertEq(adminTimelockController.getMinDelay(), 4 days); + // - dao or dao safe has PROPOSER role in product timelock + assertTrue(adminTimelockController.hasRole(PROPOSER_ROLE, timelockOwner)); + // - dao or dao safe has CANCELLER role in product timelock + assertTrue(adminTimelockController.hasRole(CANCELLER_ROLE, timelockOwner)); // - everybody has EXECUTOR role in product timelock - assertTrue(productTimelockController.hasRole(EXECUTOR_ROLE, address(0))); + assertTrue(adminTimelockController.hasRole(EXECUTOR_ROLE, address(0))); // CONFIG - // - admin is protocol timelock - assertEq(vm.load(address(deployment.config), PROXY_ADMIN_SLOT), bytes32(uint256(uint160(deployment.protocolTimelock)))); - // - owner is product timelock - assertEq(deployment.config.owner(), deployment.productTimelock); - // - feeCollector is feeCollector + // - admin is admin timelock + assertEq(vm.load(address(deployment.config), PROXY_ADMIN_SLOT), bytes32(uint256(uint160(deployment.adminTimelock)))); + // - owner is protocol timelock + assertEq(deployment.config.owner(), deployment.protocolTimelock); + // - feeCollector is dao safe assertEq(deployment.config.feeCollector(), deployment.daoSafe); // - is initialized assertEq(vm.load(address(deployment.config), bytes32(uint256(1))) << 88 >> 248, bytes32(uint256(1))); @@ -62,6 +61,10 @@ contract DeployedProtocolTest is DeploymentTest { address configImplementation = address(uint160(uint256(vm.load(address(deployment.config), PROXY_IMPLEMENTATION_SLOT)))); assertEq(vm.load(configImplementation, bytes32(uint256(1))) << 88 >> 248, bytes32(uint256(1))); + // CATEGORY REGISTRY + // - owner is protocol timelock + // assertTrue(deployment.categoryRegistry.owner(), deployment.protocolTimelock); + // HUB // - owner is protocol timelock assertEq(deployment.hub.owner(), deployment.protocolTimelock); From e0547c202e39b12ae40d8ab98392219c580b3a0a Mon Sep 17 00:00:00 2001 From: ashhanai Date: Sat, 13 Apr 2024 16:59:30 -0400 Subject: [PATCH 088/129] refactor: remove unnecessary remappings --- remappings.txt | 5 --- script/PWN.s.sol | 32 +++++++++---------- script/PWNTimelock.s.sol | 4 +-- src/Deployments.sol | 22 ++++++------- src/config/PWNConfig.sol | 6 ++-- src/hub/PWNHub.sol | 2 +- src/hub/PWNHubAccessControl.sol | 6 ++-- src/loan/lib/PWNSignatureChecker.sol | 2 +- src/loan/terms/simple/loan/PWNSimpleLoan.sol | 28 ++++++++-------- .../PWNSimpleLoanDutchAuctionProposal.sol | 6 ++-- .../PWNSimpleLoanFungibleProposal.sol | 6 ++-- .../proposal/PWNSimpleLoanListProposal.sol | 6 ++-- .../simple/proposal/PWNSimpleLoanProposal.sol | 16 +++++----- .../proposal/PWNSimpleLoanSimpleProposal.sol | 6 ++-- src/loan/token/PWNLOAN.sol | 8 ++--- src/loan/vault/PWNVault.sol | 6 ++-- src/nonce/PWNRevokedNonce.sol | 6 ++-- .../ChickenBondStateFingerpringComputer.sol | 2 +- .../UniV3PosStateFingerpringComputer.sol | 2 +- test/DeploymentTest.t.sol | 2 +- test/fork/DeployedProtocol.fork.t.sol | 2 +- test/fork/UseCases.fork.t.sol | 4 +-- test/helper/DummyPoolAdapter.sol | 2 +- test/integration/BaseIntegrationTest.t.sol | 10 +++--- test/integration/PWNProtocolIntegrity.t.sol | 6 ++-- .../PWNSimpleLoanIntegration.t.sol | 4 +-- test/unit/PWNConfig.t.sol | 4 +-- test/unit/PWNFeeCalculator.t.sol | 2 +- test/unit/PWNHub.t.sol | 4 +-- test/unit/PWNLOAN.t.sol | 8 ++--- test/unit/PWNRevokedNonce.t.sol | 6 ++-- test/unit/PWNSignatureChecker.t.sol | 4 +-- test/unit/PWNSimpleLoan.t.sol | 10 +++--- .../PWNSimpleLoanDutchAuctionProposal.t.sol | 8 ++--- test/unit/PWNSimpleLoanFungibleProposal.t.sol | 8 ++--- test/unit/PWNSimpleLoanListProposal.t.sol | 8 ++--- test/unit/PWNSimpleLoanProposal.t.sol | 8 ++--- test/unit/PWNSimpleLoanSimpleProposal.t.sol | 8 ++--- test/unit/PWNVault.t.sol | 6 ++-- 39 files changed, 140 insertions(+), 145 deletions(-) diff --git a/remappings.txt b/remappings.txt index 9006274..5a6377f 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,8 +1,3 @@ -@MT/=lib/MultiToken/src/ -@openzeppelin/=lib/openzeppelin-contracts/contracts/ -@pwn-test/=test/ -@pwn/=src/ - MultiToken/=lib/MultiToken/src/ ds-test/=lib/forge-std/lib/ds-test/src/ forge-std/=lib/forge-std/src/ diff --git a/script/PWN.s.sol b/script/PWN.s.sol index ce6989d..fc1e108 100644 --- a/script/PWN.s.sol +++ b/script/PWN.s.sol @@ -10,22 +10,22 @@ import { MultiTokenCategoryRegistry } from "MultiToken/MultiTokenCategoryRegistr import { GnosisSafeLike, GnosisSafeUtils } from "./lib/GnosisSafeUtils.sol"; -import { PWNConfig } from "@pwn/config/PWNConfig.sol"; -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 { PWNSimpleLoanSimpleProposal } from "@pwn/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol"; -import { PWNSimpleLoanListProposal } from "@pwn/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol"; -import { PWNSimpleLoanFungibleProposal } from "@pwn/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol"; -import { PWNSimpleLoanDutchAuctionProposal } from "@pwn/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol"; -import { PWNLOAN } from "@pwn/loan/token/PWNLOAN.sol"; -import { PWNRevokedNonce } from "@pwn/nonce/PWNRevokedNonce.sol"; -import { Deployments } from "@pwn/Deployments.sol"; - -import { T20 } from "@pwn-test/helper/T20.sol"; -import { T721 } from "@pwn-test/helper/T721.sol"; -import { T1155 } from "@pwn-test/helper/T1155.sol"; +import { PWNConfig } from "src/config/PWNConfig.sol"; +import { IPWNDeployer } from "src/deployer/IPWNDeployer.sol"; +import { PWNHub } from "src/hub/PWNHub.sol"; +import { PWNHubTags } from "src/hub/PWNHubTags.sol"; +import { PWNSimpleLoan } from "src/loan/terms/simple/loan/PWNSimpleLoan.sol"; +import { PWNSimpleLoanSimpleProposal } from "src/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol"; +import { PWNSimpleLoanListProposal } from "src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol"; +import { PWNSimpleLoanFungibleProposal } from "src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol"; +import { PWNSimpleLoanDutchAuctionProposal } from "src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol"; +import { PWNLOAN } from "src/loan/token/PWNLOAN.sol"; +import { PWNRevokedNonce } from "src/nonce/PWNRevokedNonce.sol"; +import { Deployments } from "src/Deployments.sol"; + +import { T20 } from "test/helper/T20.sol"; +import { T721 } from "test/helper/T721.sol"; +import { T1155 } from "test/helper/T1155.sol"; library PWNContractDeployerSalt { diff --git a/script/PWNTimelock.s.sol b/script/PWNTimelock.s.sol index 2f46c6a..b7a5271 100644 --- a/script/PWNTimelock.s.sol +++ b/script/PWNTimelock.s.sol @@ -7,8 +7,8 @@ import { TimelockController } from "openzeppelin-contracts/contracts/governance/ import { GnosisSafeLike, GnosisSafeUtils } from "./lib/GnosisSafeUtils.sol"; -import { IPWNDeployer } from "@pwn/deployer/IPWNDeployer.sol"; -import "@pwn/Deployments.sol"; +import { IPWNDeployer } from "src/deployer/IPWNDeployer.sol"; +import "src/Deployments.sol"; library PWNDeployerSalt { diff --git a/src/Deployments.sol b/src/Deployments.sol index 75820b6..5879fba 100644 --- a/src/Deployments.sol +++ b/src/Deployments.sol @@ -8,17 +8,17 @@ import { IMultiTokenCategoryRegistry } from "MultiToken/interfaces/IMultiTokenCa import { Strings } from "openzeppelin-contracts/contracts/utils/Strings.sol"; -import { PWNConfig } from "@pwn/config/PWNConfig.sol"; -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 { PWNSimpleLoanDutchAuctionProposal } from "@pwn/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol"; -import { PWNSimpleLoanFungibleProposal } from "@pwn/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.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"; +import { PWNConfig } from "src/config/PWNConfig.sol"; +import { IPWNDeployer } from "src/deployer/IPWNDeployer.sol"; +import { PWNHub } from "src/hub/PWNHub.sol"; +import { PWNHubTags } from "src/hub/PWNHubTags.sol"; +import { PWNSimpleLoan } from "src/loan/terms/simple/loan/PWNSimpleLoan.sol"; +import { PWNSimpleLoanDutchAuctionProposal } from "src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol"; +import { PWNSimpleLoanFungibleProposal } from "src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol"; +import { PWNSimpleLoanListProposal } from "src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol"; +import { PWNSimpleLoanSimpleProposal } from "src/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol"; +import { PWNLOAN } from "src/loan/token/PWNLOAN.sol"; +import { PWNRevokedNonce } from "src/nonce/PWNRevokedNonce.sol"; abstract contract Deployments is CommonBase { diff --git a/src/config/PWNConfig.sol b/src/config/PWNConfig.sol index 5879e49..ff80c08 100644 --- a/src/config/PWNConfig.sol +++ b/src/config/PWNConfig.sol @@ -4,9 +4,9 @@ pragma solidity 0.8.16; import { Ownable2Step } from "openzeppelin-contracts/contracts/access/Ownable2Step.sol"; import { Initializable } from "openzeppelin-contracts/contracts/proxy/utils/Initializable.sol"; -import { IPoolAdapter } from "@pwn/pool-adapter/IPoolAdapter.sol"; -import { IStateFingerpringComputer } from "@pwn/state-fingerprint-computer/IStateFingerpringComputer.sol"; -import "@pwn/PWNErrors.sol"; +import { IPoolAdapter } from "src/pool-adapter/IPoolAdapter.sol"; +import { IStateFingerpringComputer } from "src/state-fingerprint-computer/IStateFingerpringComputer.sol"; +import "src/PWNErrors.sol"; /** diff --git a/src/hub/PWNHub.sol b/src/hub/PWNHub.sol index ff7c9ba..dbd4f69 100644 --- a/src/hub/PWNHub.sol +++ b/src/hub/PWNHub.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.16; import { Ownable2Step } from "openzeppelin-contracts/contracts/access/Ownable2Step.sol"; -import "@pwn/PWNErrors.sol"; +import "src/PWNErrors.sol"; /** diff --git a/src/hub/PWNHubAccessControl.sol b/src/hub/PWNHubAccessControl.sol index f96140e..1dda807 100644 --- a/src/hub/PWNHubAccessControl.sol +++ b/src/hub/PWNHubAccessControl.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; -import "@pwn/hub/PWNHub.sol"; -import "@pwn/hub/PWNHubTags.sol"; -import "@pwn/PWNErrors.sol"; +import "src/hub/PWNHub.sol"; +import "src/hub/PWNHubTags.sol"; +import "src/PWNErrors.sol"; /** diff --git a/src/loan/lib/PWNSignatureChecker.sol b/src/loan/lib/PWNSignatureChecker.sol index e204c44..651cbfc 100644 --- a/src/loan/lib/PWNSignatureChecker.sol +++ b/src/loan/lib/PWNSignatureChecker.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.16; import "openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol"; import "openzeppelin-contracts/contracts/interfaces/IERC1271.sol"; -import "@pwn/PWNErrors.sol"; +import "src/PWNErrors.sol"; /** diff --git a/src/loan/terms/simple/loan/PWNSimpleLoan.sol b/src/loan/terms/simple/loan/PWNSimpleLoan.sol index ece7909..6e0590b 100644 --- a/src/loan/terms/simple/loan/PWNSimpleLoan.sol +++ b/src/loan/terms/simple/loan/PWNSimpleLoan.sol @@ -6,20 +6,20 @@ import { MultiToken, IMultiTokenCategoryRegistry } from "MultiToken/MultiToken.s import { Math } from "openzeppelin-contracts/contracts/utils/math/Math.sol"; import { SafeCast } from "openzeppelin-contracts/contracts/utils/math/SafeCast.sol"; -import { PWNConfig } from "@pwn/config/PWNConfig.sol"; -import { PWNHub } from "@pwn/hub/PWNHub.sol"; -import { PWNHubTags } from "@pwn/hub/PWNHubTags.sol"; -import { PWNFeeCalculator } from "@pwn/loan/lib/PWNFeeCalculator.sol"; -import { PWNSignatureChecker } from "@pwn/loan/lib/PWNSignatureChecker.sol"; -import { PWNSimpleLoanProposal } from "@pwn/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol"; -import { IERC5646 } from "@pwn/loan/token/IERC5646.sol"; -import { IPWNLoanMetadataProvider } from "@pwn/loan/token/IPWNLoanMetadataProvider.sol"; -import { PWNLOAN } from "@pwn/loan/token/PWNLOAN.sol"; -import { Permit } from "@pwn/loan/vault/Permit.sol"; -import { PWNVault } from "@pwn/loan/vault/PWNVault.sol"; -import { PWNRevokedNonce } from "@pwn/nonce/PWNRevokedNonce.sol"; -import { IPoolAdapter } from "@pwn/pool-adapter/IPoolAdapter.sol"; -import "@pwn/PWNErrors.sol"; +import { PWNConfig } from "src/config/PWNConfig.sol"; +import { PWNHub } from "src/hub/PWNHub.sol"; +import { PWNHubTags } from "src/hub/PWNHubTags.sol"; +import { PWNFeeCalculator } from "src/loan/lib/PWNFeeCalculator.sol"; +import { PWNSignatureChecker } from "src/loan/lib/PWNSignatureChecker.sol"; +import { PWNSimpleLoanProposal } from "src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol"; +import { IERC5646 } from "src/loan/token/IERC5646.sol"; +import { IPWNLoanMetadataProvider } from "src/loan/token/IPWNLoanMetadataProvider.sol"; +import { PWNLOAN } from "src/loan/token/PWNLOAN.sol"; +import { Permit } from "src/loan/vault/Permit.sol"; +import { PWNVault } from "src/loan/vault/PWNVault.sol"; +import { PWNRevokedNonce } from "src/nonce/PWNRevokedNonce.sol"; +import { IPoolAdapter } from "src/pool-adapter/IPoolAdapter.sol"; +import "src/PWNErrors.sol"; /** diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol index 887dc3a..698f334 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol @@ -5,9 +5,9 @@ import { MultiToken } from "MultiToken/MultiToken.sol"; 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 "@pwn/PWNErrors.sol"; +import { PWNSimpleLoan } from "src/loan/terms/simple/loan/PWNSimpleLoan.sol"; +import { PWNSimpleLoanProposal } from "src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol"; +import "src/PWNErrors.sol"; /** diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol index 47e9ed5..7f40202 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol @@ -5,9 +5,9 @@ import { MultiToken } from "MultiToken/MultiToken.sol"; 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 "@pwn/PWNErrors.sol"; +import { PWNSimpleLoan } from "src/loan/terms/simple/loan/PWNSimpleLoan.sol"; +import { PWNSimpleLoanProposal } from "src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol"; +import "src/PWNErrors.sol"; /** diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol index 4008999..6513d06 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol @@ -5,9 +5,9 @@ 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 "@pwn/PWNErrors.sol"; +import { PWNSimpleLoan } from "src/loan/terms/simple/loan/PWNSimpleLoan.sol"; +import { PWNSimpleLoanProposal } from "src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol"; +import "src/PWNErrors.sol"; /** diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol index 32f0bf4..bb83bce 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol @@ -4,14 +4,14 @@ pragma solidity 0.8.16; import { MerkleProof } from "openzeppelin-contracts/contracts/utils/cryptography/MerkleProof.sol"; import { ERC165Checker } from "openzeppelin-contracts/contracts/utils/introspection/ERC165Checker.sol"; -import { PWNConfig, IStateFingerpringComputer } 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 { PWNSimpleLoan } from "@pwn/loan/terms/simple/loan/PWNSimpleLoan.sol"; -import { IERC5646 } from "@pwn/loan/token/IERC5646.sol"; -import { PWNRevokedNonce } from "@pwn/nonce/PWNRevokedNonce.sol"; -import "@pwn/PWNErrors.sol"; +import { PWNConfig, IStateFingerpringComputer } from "src/config/PWNConfig.sol"; +import { PWNHub } from "src/hub/PWNHub.sol"; +import { PWNHubTags } from "src/hub/PWNHubTags.sol"; +import { PWNSignatureChecker } from "src/loan/lib/PWNSignatureChecker.sol"; +import { PWNSimpleLoan } from "src/loan/terms/simple/loan/PWNSimpleLoan.sol"; +import { IERC5646 } from "src/loan/token/IERC5646.sol"; +import { PWNRevokedNonce } from "src/nonce/PWNRevokedNonce.sol"; +import "src/PWNErrors.sol"; /** * @title PWN Simple Loan Proposal Base Contract diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol index c1096ad..06dbb1f 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol @@ -3,9 +3,9 @@ pragma solidity 0.8.16; 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 "@pwn/PWNErrors.sol"; +import { PWNSimpleLoan } from "src/loan/terms/simple/loan/PWNSimpleLoan.sol"; +import { PWNSimpleLoanProposal } from "src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol"; +import "src/PWNErrors.sol"; /** diff --git a/src/loan/token/PWNLOAN.sol b/src/loan/token/PWNLOAN.sol index 7bfc999..c1fdf4d 100644 --- a/src/loan/token/PWNLOAN.sol +++ b/src/loan/token/PWNLOAN.sol @@ -3,10 +3,10 @@ pragma solidity 0.8.16; import { ERC721 } from "openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; -import { PWNHubAccessControl } from "@pwn/hub/PWNHubAccessControl.sol"; -import { IERC5646 } from "@pwn/loan/token/IERC5646.sol"; -import { IPWNLoanMetadataProvider } from "@pwn/loan/token/IPWNLoanMetadataProvider.sol"; -import "@pwn/PWNErrors.sol"; +import { PWNHubAccessControl } from "src/hub/PWNHubAccessControl.sol"; +import { IERC5646 } from "src/loan/token/IERC5646.sol"; +import { IPWNLoanMetadataProvider } from "src/loan/token/IPWNLoanMetadataProvider.sol"; +import "src/PWNErrors.sol"; /** diff --git a/src/loan/vault/PWNVault.sol b/src/loan/vault/PWNVault.sol index f1f988c..2c03d72 100644 --- a/src/loan/vault/PWNVault.sol +++ b/src/loan/vault/PWNVault.sol @@ -7,9 +7,9 @@ import { IERC20Permit } from "openzeppelin-contracts/contracts/token/ERC20/exten import { IERC721Receiver } from "openzeppelin-contracts/contracts/token/ERC721/IERC721Receiver.sol"; import { IERC1155Receiver, IERC165 } from "openzeppelin-contracts/contracts/token/ERC1155/IERC1155Receiver.sol"; -import { Permit } from "@pwn/loan/vault/Permit.sol"; -import { IPoolAdapter } from "@pwn/pool-adapter/IPoolAdapter.sol"; -import "@pwn/PWNErrors.sol"; +import { Permit } from "src/loan/vault/Permit.sol"; +import { IPoolAdapter } from "src/pool-adapter/IPoolAdapter.sol"; +import "src/PWNErrors.sol"; /** diff --git a/src/nonce/PWNRevokedNonce.sol b/src/nonce/PWNRevokedNonce.sol index 0c4332a..e532a7d 100644 --- a/src/nonce/PWNRevokedNonce.sol +++ b/src/nonce/PWNRevokedNonce.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; -import { PWNHub } from "@pwn/hub/PWNHub.sol"; -import { PWNHubTags } from "@pwn/hub/PWNHubTags.sol"; -import "@pwn/PWNErrors.sol"; +import { PWNHub } from "src/hub/PWNHub.sol"; +import { PWNHubTags } from "src/hub/PWNHubTags.sol"; +import "src/PWNErrors.sol"; /** diff --git a/src/state-fingerprint-computer/ChickenBondStateFingerpringComputer.sol b/src/state-fingerprint-computer/ChickenBondStateFingerpringComputer.sol index 78334b8..2fc0916 100644 --- a/src/state-fingerprint-computer/ChickenBondStateFingerpringComputer.sol +++ b/src/state-fingerprint-computer/ChickenBondStateFingerpringComputer.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; -import { IStateFingerpringComputer } from "@pwn/state-fingerprint-computer/IStateFingerpringComputer.sol"; +import { IStateFingerpringComputer } from "src/state-fingerprint-computer/IStateFingerpringComputer.sol"; interface IChickenBondManagerLike { diff --git a/src/state-fingerprint-computer/UniV3PosStateFingerpringComputer.sol b/src/state-fingerprint-computer/UniV3PosStateFingerpringComputer.sol index ccc166b..077ae0a 100644 --- a/src/state-fingerprint-computer/UniV3PosStateFingerpringComputer.sol +++ b/src/state-fingerprint-computer/UniV3PosStateFingerpringComputer.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; -import { IStateFingerpringComputer } from "@pwn/state-fingerprint-computer/IStateFingerpringComputer.sol"; +import { IStateFingerpringComputer } from "src/state-fingerprint-computer/IStateFingerpringComputer.sol"; interface UniswapNonFungiblePositionManagerLike { diff --git a/test/DeploymentTest.t.sol b/test/DeploymentTest.t.sol index 24d7212..e5e2595 100644 --- a/test/DeploymentTest.t.sol +++ b/test/DeploymentTest.t.sol @@ -8,7 +8,7 @@ import { MultiTokenCategoryRegistry, IMultiTokenCategoryRegistry } from "MultiTo import { TransparentUpgradeableProxy } from "openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; -import "@pwn/Deployments.sol"; +import "src/Deployments.sol"; abstract contract DeploymentTest is Deployments, Test { diff --git a/test/fork/DeployedProtocol.fork.t.sol b/test/fork/DeployedProtocol.fork.t.sol index 677c336..cdd7c9a 100644 --- a/test/fork/DeployedProtocol.fork.t.sol +++ b/test/fork/DeployedProtocol.fork.t.sol @@ -5,7 +5,7 @@ import "forge-std/Test.sol"; import { TimelockController } from "openzeppelin-contracts/contracts/governance/TimelockController.sol"; -import "@pwn-test/DeploymentTest.t.sol"; +import "test/DeploymentTest.t.sol"; contract DeployedProtocolTest is DeploymentTest { diff --git a/test/fork/UseCases.fork.t.sol b/test/fork/UseCases.fork.t.sol index 7ad91b5..74c28da 100644 --- a/test/fork/UseCases.fork.t.sol +++ b/test/fork/UseCases.fork.t.sol @@ -8,8 +8,8 @@ import { MultiToken, ICryptoKitties, IERC721 } from "MultiToken/MultiToken.sol"; import { Permit } from "@pwn/loan/vault/Permit.sol"; import "@pwn/PWNErrors.sol"; -import { T20 } from "@pwn-test/helper/T20.sol"; -import "@pwn-test/DeploymentTest.t.sol"; +import { T20 } from "test/helper/T20.sol"; +import "test/DeploymentTest.t.sol"; abstract contract UseCasesTest is DeploymentTest { diff --git a/test/helper/DummyPoolAdapter.sol b/test/helper/DummyPoolAdapter.sol index cc32162..76ad245 100644 --- a/test/helper/DummyPoolAdapter.sol +++ b/test/helper/DummyPoolAdapter.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.16; import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; -import { IPoolAdapter } from "@pwn/pool-adapter/IPoolAdapter.sol"; +import { IPoolAdapter } from "src/pool-adapter/IPoolAdapter.sol"; contract DummyPoolAdapter is IPoolAdapter { diff --git a/test/integration/BaseIntegrationTest.t.sol b/test/integration/BaseIntegrationTest.t.sol index e97a2a2..5162f9b 100644 --- a/test/integration/BaseIntegrationTest.t.sol +++ b/test/integration/BaseIntegrationTest.t.sol @@ -5,12 +5,12 @@ import "forge-std/Test.sol"; import { MultiToken } from "MultiToken/MultiToken.sol"; -import { Permit } from "@pwn/loan/vault/Permit.sol"; +import { Permit } from "src/loan/vault/Permit.sol"; -import { T20 } from "@pwn-test/helper/T20.sol"; -import { T721 } from "@pwn-test/helper/T721.sol"; -import { T1155 } from "@pwn-test/helper/T1155.sol"; -import "@pwn-test/DeploymentTest.t.sol"; +import { T20 } from "test/helper/T20.sol"; +import { T721 } from "test/helper/T721.sol"; +import { T1155 } from "test/helper/T1155.sol"; +import "test/DeploymentTest.t.sol"; abstract contract BaseIntegrationTest is DeploymentTest { diff --git a/test/integration/PWNProtocolIntegrity.t.sol b/test/integration/PWNProtocolIntegrity.t.sol index 43d8bd6..0942906 100644 --- a/test/integration/PWNProtocolIntegrity.t.sol +++ b/test/integration/PWNProtocolIntegrity.t.sol @@ -3,10 +3,10 @@ pragma solidity 0.8.16; import "forge-std/Test.sol"; -import { PWNHubTags } from "@pwn/hub/PWNHubTags.sol"; -import "@pwn/PWNErrors.sol"; +import { PWNHubTags } from "src/hub/PWNHubTags.sol"; +import "src/PWNErrors.sol"; -import "@pwn-test/integration/BaseIntegrationTest.t.sol"; +import "test/integration/BaseIntegrationTest.t.sol"; contract PWNProtocolIntegrityTest is BaseIntegrationTest { diff --git a/test/integration/PWNSimpleLoanIntegration.t.sol b/test/integration/PWNSimpleLoanIntegration.t.sol index ebec89a..18840ad 100644 --- a/test/integration/PWNSimpleLoanIntegration.t.sol +++ b/test/integration/PWNSimpleLoanIntegration.t.sol @@ -3,9 +3,9 @@ pragma solidity 0.8.16; import "forge-std/Test.sol"; -import "@pwn/PWNErrors.sol"; +import "src/PWNErrors.sol"; -import "@pwn-test/integration/BaseIntegrationTest.t.sol"; +import "test/integration/BaseIntegrationTest.t.sol"; contract PWNSimpleLoanIntegrationTest is BaseIntegrationTest { diff --git a/test/unit/PWNConfig.t.sol b/test/unit/PWNConfig.t.sol index 73039bf..aa98d4a 100644 --- a/test/unit/PWNConfig.t.sol +++ b/test/unit/PWNConfig.t.sol @@ -3,8 +3,8 @@ pragma solidity 0.8.16; import "forge-std/Test.sol"; -import { PWNConfig } from "@pwn/config/PWNConfig.sol"; -import "@pwn/PWNErrors.sol"; +import { PWNConfig } from "src/config/PWNConfig.sol"; +import "src/PWNErrors.sol"; abstract contract PWNConfigTest is Test { diff --git a/test/unit/PWNFeeCalculator.t.sol b/test/unit/PWNFeeCalculator.t.sol index 2986a32..28105d7 100644 --- a/test/unit/PWNFeeCalculator.t.sol +++ b/test/unit/PWNFeeCalculator.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.16; import "forge-std/Test.sol"; -import "@pwn/loan/lib/PWNFeeCalculator.sol"; +import "src/loan/lib/PWNFeeCalculator.sol"; contract PWNFeeCalculator_CalculateFeeAmount_Test is Test { diff --git a/test/unit/PWNHub.t.sol b/test/unit/PWNHub.t.sol index dc1c1fd..b3caed4 100644 --- a/test/unit/PWNHub.t.sol +++ b/test/unit/PWNHub.t.sol @@ -3,8 +3,8 @@ pragma solidity 0.8.16; import "forge-std/Test.sol"; -import "@pwn/hub/PWNHub.sol"; -import "@pwn/PWNErrors.sol"; +import "src/hub/PWNHub.sol"; +import "src/PWNErrors.sol"; abstract contract PWNHubTest is Test { diff --git a/test/unit/PWNLOAN.t.sol b/test/unit/PWNLOAN.t.sol index 5869b00..b2681a2 100644 --- a/test/unit/PWNLOAN.t.sol +++ b/test/unit/PWNLOAN.t.sol @@ -3,10 +3,10 @@ pragma solidity 0.8.16; import "forge-std/Test.sol"; -import "@pwn/hub/PWNHubTags.sol"; -import "@pwn/loan/token/IERC5646.sol"; -import "@pwn/loan/token/PWNLOAN.sol"; -import "@pwn/PWNErrors.sol"; +import "src/hub/PWNHubTags.sol"; +import "src/loan/token/IERC5646.sol"; +import "src/loan/token/PWNLOAN.sol"; +import "src/PWNErrors.sol"; abstract contract PWNLOANTest is Test { diff --git a/test/unit/PWNRevokedNonce.t.sol b/test/unit/PWNRevokedNonce.t.sol index fccfc60..a6b418f 100644 --- a/test/unit/PWNRevokedNonce.t.sol +++ b/test/unit/PWNRevokedNonce.t.sol @@ -3,9 +3,9 @@ pragma solidity 0.8.16; import "forge-std/Test.sol"; -import "@pwn/hub/PWNHubTags.sol"; -import "@pwn/nonce/PWNRevokedNonce.sol"; -import "@pwn/PWNErrors.sol"; +import "src/hub/PWNHubTags.sol"; +import "src/nonce/PWNRevokedNonce.sol"; +import "src/PWNErrors.sol"; abstract contract PWNRevokedNonceTest is Test { diff --git a/test/unit/PWNSignatureChecker.t.sol b/test/unit/PWNSignatureChecker.t.sol index 88f83b8..95af055 100644 --- a/test/unit/PWNSignatureChecker.t.sol +++ b/test/unit/PWNSignatureChecker.t.sol @@ -3,8 +3,8 @@ pragma solidity 0.8.16; import "forge-std/Test.sol"; -import "@pwn/loan/lib/PWNSignatureChecker.sol"; -import "@pwn/PWNErrors.sol"; +import "src/loan/lib/PWNSignatureChecker.sol"; +import "src/PWNErrors.sol"; abstract contract PWNSignatureCheckerTest is Test { diff --git a/test/unit/PWNSimpleLoan.t.sol b/test/unit/PWNSimpleLoan.t.sol index aff404f..1be6ab5 100644 --- a/test/unit/PWNSimpleLoan.t.sol +++ b/test/unit/PWNSimpleLoan.t.sol @@ -3,12 +3,12 @@ pragma solidity 0.8.16; import "forge-std/Test.sol"; -import { PWNSimpleLoan, PWNHubTags, Math, MultiToken, Permit } from "@pwn/loan/terms/simple/loan/PWNSimpleLoan.sol"; -import "@pwn/PWNErrors.sol"; +import { PWNSimpleLoan, PWNHubTags, Math, MultiToken, Permit } from "src/loan/terms/simple/loan/PWNSimpleLoan.sol"; +import "src/PWNErrors.sol"; -import { T20 } from "@pwn-test/helper/T20.sol"; -import { T721 } from "@pwn-test/helper/T721.sol"; -import { DummyPoolAdapter } from "@pwn-test/helper/DummyPoolAdapter.sol"; +import { T20 } from "test/helper/T20.sol"; +import { T721 } from "test/helper/T721.sol"; +import { DummyPoolAdapter } from "test/helper/DummyPoolAdapter.sol"; abstract contract PWNSimpleLoanTest is Test { diff --git a/test/unit/PWNSimpleLoanDutchAuctionProposal.t.sol b/test/unit/PWNSimpleLoanDutchAuctionProposal.t.sol index 8db63b7..91cd0fb 100644 --- a/test/unit/PWNSimpleLoanDutchAuctionProposal.t.sol +++ b/test/unit/PWNSimpleLoanDutchAuctionProposal.t.sol @@ -7,15 +7,15 @@ import { MultiToken } from "MultiToken/MultiToken.sol"; import { Math } from "openzeppelin-contracts/contracts/utils/math/Math.sol"; -import { PWNHubTags } from "@pwn/hub/PWNHubTags.sol"; +import { PWNHubTags } from "src/hub/PWNHubTags.sol"; import { PWNSimpleLoanDutchAuctionProposal, PWNSimpleLoanProposal, PWNSimpleLoan } - from "@pwn/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol"; -import "@pwn/PWNErrors.sol"; + from "src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol"; +import "src/PWNErrors.sol"; import { PWNSimpleLoanProposalTest, PWNSimpleLoanProposal_AcceptProposal_Test -} from "@pwn-test/unit/PWNSimpleLoanProposal.t.sol"; +} from "test/unit/PWNSimpleLoanProposal.t.sol"; abstract contract PWNSimpleLoanDutchAuctionProposalTest is PWNSimpleLoanProposalTest { diff --git a/test/unit/PWNSimpleLoanFungibleProposal.t.sol b/test/unit/PWNSimpleLoanFungibleProposal.t.sol index 5731fb8..411f9af 100644 --- a/test/unit/PWNSimpleLoanFungibleProposal.t.sol +++ b/test/unit/PWNSimpleLoanFungibleProposal.t.sol @@ -7,15 +7,15 @@ import { MultiToken } from "MultiToken/MultiToken.sol"; import { Math } from "openzeppelin-contracts/contracts/utils/math/Math.sol"; -import { PWNHubTags } from "@pwn/hub/PWNHubTags.sol"; +import { PWNHubTags } from "src/hub/PWNHubTags.sol"; import { PWNSimpleLoanFungibleProposal, PWNSimpleLoanProposal, PWNSimpleLoan } - from "@pwn/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol"; -import "@pwn/PWNErrors.sol"; + from "src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol"; +import "src/PWNErrors.sol"; import { PWNSimpleLoanProposalTest, PWNSimpleLoanProposal_AcceptProposal_Test -} from "@pwn-test/unit/PWNSimpleLoanProposal.t.sol"; +} from "test/unit/PWNSimpleLoanProposal.t.sol"; abstract contract PWNSimpleLoanFungibleProposalTest is PWNSimpleLoanProposalTest { diff --git a/test/unit/PWNSimpleLoanListProposal.t.sol b/test/unit/PWNSimpleLoanListProposal.t.sol index 052a199..53b6c5d 100644 --- a/test/unit/PWNSimpleLoanListProposal.t.sol +++ b/test/unit/PWNSimpleLoanListProposal.t.sol @@ -7,15 +7,15 @@ import { MultiToken } from "MultiToken/MultiToken.sol"; import { Math } from "openzeppelin-contracts/contracts/utils/math/Math.sol"; -import { PWNHubTags } from "@pwn/hub/PWNHubTags.sol"; +import { PWNHubTags } from "src/hub/PWNHubTags.sol"; import { PWNSimpleLoanListProposal, PWNSimpleLoanProposal, PWNSimpleLoan } - from "@pwn/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol"; -import "@pwn/PWNErrors.sol"; + from "src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol"; +import "src/PWNErrors.sol"; import { PWNSimpleLoanProposalTest, PWNSimpleLoanProposal_AcceptProposal_Test -} from "@pwn-test/unit/PWNSimpleLoanProposal.t.sol"; +} from "test/unit/PWNSimpleLoanProposal.t.sol"; abstract contract PWNSimpleLoanListProposalTest is PWNSimpleLoanProposalTest { diff --git a/test/unit/PWNSimpleLoanProposal.t.sol b/test/unit/PWNSimpleLoanProposal.t.sol index 6e1d338..8ed20f0 100644 --- a/test/unit/PWNSimpleLoanProposal.t.sol +++ b/test/unit/PWNSimpleLoanProposal.t.sol @@ -9,13 +9,13 @@ import { MerkleProof } from "openzeppelin-contracts/contracts/utils/cryptography import { IERC165 } from "openzeppelin-contracts/contracts/utils/introspection/IERC165.sol"; import { Math } from "openzeppelin-contracts/contracts/utils/math/Math.sol"; -import { PWNHubTags } from "@pwn/hub/PWNHubTags.sol"; -import { PWNSimpleLoan } from "@pwn/loan/terms/simple/loan/PWNSimpleLoan.sol"; +import { PWNHubTags } from "src/hub/PWNHubTags.sol"; +import { PWNSimpleLoan } from "src/loan/terms/simple/loan/PWNSimpleLoan.sol"; import { PWNSimpleLoanProposal, IERC5646 -} from "@pwn/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol"; -import "@pwn/PWNErrors.sol"; +} from "src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol"; +import "src/PWNErrors.sol"; abstract contract PWNSimpleLoanProposalTest is Test { diff --git a/test/unit/PWNSimpleLoanSimpleProposal.t.sol b/test/unit/PWNSimpleLoanSimpleProposal.t.sol index dd44c8e..aa6d47b 100644 --- a/test/unit/PWNSimpleLoanSimpleProposal.t.sol +++ b/test/unit/PWNSimpleLoanSimpleProposal.t.sol @@ -5,15 +5,15 @@ import "forge-std/Test.sol"; import { MultiToken } from "MultiToken/MultiToken.sol"; -import { PWNHubTags } from "@pwn/hub/PWNHubTags.sol"; +import { PWNHubTags } from "src/hub/PWNHubTags.sol"; import { PWNSimpleLoanSimpleProposal, PWNSimpleLoanProposal, PWNSimpleLoan } - from "@pwn/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol"; -import "@pwn/PWNErrors.sol"; + from "src/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol"; +import "src/PWNErrors.sol"; import { PWNSimpleLoanProposalTest, PWNSimpleLoanProposal_AcceptProposal_Test -} from "@pwn-test/unit/PWNSimpleLoanProposal.t.sol"; +} from "test/unit/PWNSimpleLoanProposal.t.sol"; abstract contract PWNSimpleLoanSimpleProposalTest is PWNSimpleLoanProposalTest { diff --git a/test/unit/PWNVault.t.sol b/test/unit/PWNVault.t.sol index bac2607..3d60e3c 100644 --- a/test/unit/PWNVault.t.sol +++ b/test/unit/PWNVault.t.sol @@ -3,10 +3,10 @@ pragma solidity 0.8.16; import "forge-std/Test.sol"; -import { PWNVault, IERC165, IERC721Receiver, IERC1155Receiver, Permit, MultiToken } from "@pwn/loan/vault/PWNVault.sol"; -import "@pwn/PWNErrors.sol"; +import { PWNVault, IERC165, IERC721Receiver, IERC1155Receiver, Permit, MultiToken } from "src/loan/vault/PWNVault.sol"; +import "src/PWNErrors.sol"; -import { T721 } from "@pwn-test/helper/T721.sol"; +import { T721 } from "test/helper/T721.sol"; contract PWNVaultHarness is PWNVault { From c99aa0c6ebf232657c824ec795d14b214ecb0aa8 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Sat, 13 Apr 2024 18:54:38 -0400 Subject: [PATCH 089/129] refactor: update remappings --- remappings.txt | 4 - script/PWN.s.sol | 33 +- script/PWNTimelock.s.sol | 13 +- src/Deployments.sol | 10 +- src/config/PWNConfig.sol | 4 +- src/hub/PWNHub.sol | 2 +- src/hub/PWNHubAccessControl.sol | 4 +- src/loan/lib/PWNFeeCalculator.sol | 2 +- src/loan/lib/PWNSignatureChecker.sol | 4 +- src/loan/terms/simple/loan/PWNSimpleLoan.sol | 4 +- .../PWNSimpleLoanDutchAuctionProposal.sol | 2 +- .../PWNSimpleLoanFungibleProposal.sol | 2 +- .../proposal/PWNSimpleLoanListProposal.sol | 2 +- .../simple/proposal/PWNSimpleLoanProposal.sol | 4 +- src/loan/token/PWNLOAN.sol | 2 +- src/loan/vault/PWNVault.sol | 6 +- test/DeploymentTest.t.sol | 29 +- test/fork/DeployedProtocol.fork.t.sol | 9 +- test/fork/UseCases.fork.t.sol | 316 +++++++++++++++++- test/helper/DummyPoolAdapter.sol | 2 +- test/helper/T1155.sol | 2 +- test/helper/T20.sol | 2 +- test/helper/T721.sol | 2 +- test/integration/BaseIntegrationTest.t.sol | 18 +- test/integration/PWNProtocolIntegrity.t.sol | 19 +- .../PWNSimpleLoanIntegration.t.sol | 19 +- test/unit/PWNConfig.t.sol | 2 +- test/unit/PWNFeeCalculator.t.sol | 4 +- test/unit/PWNHub.t.sol | 4 +- test/unit/PWNLOAN.t.sol | 8 +- test/unit/PWNRevokedNonce.t.sol | 6 +- test/unit/PWNSignatureChecker.t.sol | 4 +- test/unit/PWNSimpleLoan.t.sol | 2 +- .../PWNSimpleLoanDutchAuctionProposal.t.sol | 15 +- test/unit/PWNSimpleLoanFungibleProposal.t.sol | 15 +- test/unit/PWNSimpleLoanListProposal.t.sol | 15 +- test/unit/PWNSimpleLoanProposal.t.sol | 8 +- test/unit/PWNSimpleLoanSimpleProposal.t.sol | 12 +- test/unit/PWNVault.t.sol | 13 +- 39 files changed, 492 insertions(+), 132 deletions(-) delete mode 100644 remappings.txt diff --git a/remappings.txt b/remappings.txt deleted file mode 100644 index 5a6377f..0000000 --- a/remappings.txt +++ /dev/null @@ -1,4 +0,0 @@ -MultiToken/=lib/MultiToken/src/ -ds-test/=lib/forge-std/lib/ds-test/src/ -forge-std/=lib/forge-std/src/ -openzeppelin-contracts/=lib/openzeppelin-contracts/ diff --git a/script/PWN.s.sol b/script/PWN.s.sol index fc1e108..a5e3887 100644 --- a/script/PWN.s.sol +++ b/script/PWN.s.sol @@ -1,27 +1,28 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; -import "forge-std/Script.sol"; +import { Script, console2 } from "forge-std/Script.sol"; import { TransparentUpgradeableProxy, ITransparentUpgradeableProxy } - from "openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; - -import { MultiTokenCategoryRegistry } from "MultiToken/MultiTokenCategoryRegistry.sol"; + from "openzeppelin/proxy/transparent/TransparentUpgradeableProxy.sol"; import { GnosisSafeLike, GnosisSafeUtils } from "./lib/GnosisSafeUtils.sol"; -import { PWNConfig } from "src/config/PWNConfig.sol"; -import { IPWNDeployer } from "src/deployer/IPWNDeployer.sol"; -import { PWNHub } from "src/hub/PWNHub.sol"; -import { PWNHubTags } from "src/hub/PWNHubTags.sol"; -import { PWNSimpleLoan } from "src/loan/terms/simple/loan/PWNSimpleLoan.sol"; -import { PWNSimpleLoanSimpleProposal } from "src/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol"; -import { PWNSimpleLoanListProposal } from "src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol"; -import { PWNSimpleLoanFungibleProposal } from "src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol"; -import { PWNSimpleLoanDutchAuctionProposal } from "src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol"; -import { PWNLOAN } from "src/loan/token/PWNLOAN.sol"; -import { PWNRevokedNonce } from "src/nonce/PWNRevokedNonce.sol"; -import { Deployments } from "src/Deployments.sol"; +import { + Deployments, + PWNConfig, + IPWNDeployer, + PWNHub, + PWNHubTags, + PWNSimpleLoan, + PWNSimpleLoanDutchAuctionProposal, + PWNSimpleLoanFungibleProposal, + PWNSimpleLoanListProposal, + PWNSimpleLoanSimpleProposal, + PWNLOAN, + PWNRevokedNonce, + MultiTokenCategoryRegistry +} from "src/Deployments.sol"; import { T20 } from "test/helper/T20.sol"; import { T721 } from "test/helper/T721.sol"; diff --git a/script/PWNTimelock.s.sol b/script/PWNTimelock.s.sol index b7a5271..4d32344 100644 --- a/script/PWNTimelock.s.sol +++ b/script/PWNTimelock.s.sol @@ -1,14 +1,19 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; -import "forge-std/Script.sol"; +import { Script, console2 } from "forge-std/Script.sol"; -import { TimelockController } from "openzeppelin-contracts/contracts/governance/TimelockController.sol"; +import { TimelockController } from "openzeppelin/governance/TimelockController.sol"; import { GnosisSafeLike, GnosisSafeUtils } from "./lib/GnosisSafeUtils.sol"; -import { IPWNDeployer } from "src/deployer/IPWNDeployer.sol"; -import "src/Deployments.sol"; +import { + Deployments, + PWNConfig, + IPWNDeployer, + PWNHub +} from "src/Deployments.sol"; + library PWNDeployerSalt { diff --git a/src/Deployments.sol b/src/Deployments.sol index 5879fba..eba5082 100644 --- a/src/Deployments.sol +++ b/src/Deployments.sol @@ -1,12 +1,12 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; -import "forge-std/StdJson.sol"; -import "forge-std/Base.sol"; +import { stdJson } from "forge-std/StdJson.sol"; +import { CommonBase } from "forge-std/Base.sol"; -import { IMultiTokenCategoryRegistry } from "MultiToken/interfaces/IMultiTokenCategoryRegistry.sol"; +import { MultiTokenCategoryRegistry } from "MultiToken/MultiTokenCategoryRegistry.sol"; -import { Strings } from "openzeppelin-contracts/contracts/utils/Strings.sol"; +import { Strings } from "openzeppelin/utils/Strings.sol"; import { PWNConfig } from "src/config/PWNConfig.sol"; import { IPWNDeployer } from "src/deployer/IPWNDeployer.sol"; @@ -31,7 +31,7 @@ abstract contract Deployments is CommonBase { // Properties need to be in alphabetical order struct Deployment { address adminTimelock; - IMultiTokenCategoryRegistry categoryRegistry; + MultiTokenCategoryRegistry categoryRegistry; PWNConfig config; PWNConfig configSingleton; address dao; diff --git a/src/config/PWNConfig.sol b/src/config/PWNConfig.sol index ff80c08..7ff7b6c 100644 --- a/src/config/PWNConfig.sol +++ b/src/config/PWNConfig.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; -import { Ownable2Step } from "openzeppelin-contracts/contracts/access/Ownable2Step.sol"; -import { Initializable } from "openzeppelin-contracts/contracts/proxy/utils/Initializable.sol"; +import { Ownable2Step } from "openzeppelin/access/Ownable2Step.sol"; +import { Initializable } from "openzeppelin/proxy/utils/Initializable.sol"; import { IPoolAdapter } from "src/pool-adapter/IPoolAdapter.sol"; import { IStateFingerpringComputer } from "src/state-fingerprint-computer/IStateFingerpringComputer.sol"; diff --git a/src/hub/PWNHub.sol b/src/hub/PWNHub.sol index dbd4f69..8bc8e03 100644 --- a/src/hub/PWNHub.sol +++ b/src/hub/PWNHub.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; -import { Ownable2Step } from "openzeppelin-contracts/contracts/access/Ownable2Step.sol"; +import { Ownable2Step } from "openzeppelin/access/Ownable2Step.sol"; import "src/PWNErrors.sol"; diff --git a/src/hub/PWNHubAccessControl.sol b/src/hub/PWNHubAccessControl.sol index 1dda807..df9363c 100644 --- a/src/hub/PWNHubAccessControl.sol +++ b/src/hub/PWNHubAccessControl.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; -import "src/hub/PWNHub.sol"; -import "src/hub/PWNHubTags.sol"; +import { PWNHub } from "src/hub/PWNHub.sol"; +import { PWNHubTags } from "src/hub/PWNHubTags.sol"; import "src/PWNErrors.sol"; diff --git a/src/loan/lib/PWNFeeCalculator.sol b/src/loan/lib/PWNFeeCalculator.sol index 83d963c..cd17ea9 100644 --- a/src/loan/lib/PWNFeeCalculator.sol +++ b/src/loan/lib/PWNFeeCalculator.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; -import { Math } from "openzeppelin-contracts/contracts/utils/math/Math.sol"; +import { Math } from "openzeppelin/utils/math/Math.sol"; /** diff --git a/src/loan/lib/PWNSignatureChecker.sol b/src/loan/lib/PWNSignatureChecker.sol index 651cbfc..e21b6e7 100644 --- a/src/loan/lib/PWNSignatureChecker.sol +++ b/src/loan/lib/PWNSignatureChecker.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; -import "openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol"; -import "openzeppelin-contracts/contracts/interfaces/IERC1271.sol"; +import { ECDSA } from "openzeppelin/utils/cryptography/ECDSA.sol"; +import { IERC1271 } from "openzeppelin/interfaces/IERC1271.sol"; import "src/PWNErrors.sol"; diff --git a/src/loan/terms/simple/loan/PWNSimpleLoan.sol b/src/loan/terms/simple/loan/PWNSimpleLoan.sol index 6e0590b..e6109e5 100644 --- a/src/loan/terms/simple/loan/PWNSimpleLoan.sol +++ b/src/loan/terms/simple/loan/PWNSimpleLoan.sol @@ -3,8 +3,8 @@ pragma solidity 0.8.16; import { MultiToken, IMultiTokenCategoryRegistry } from "MultiToken/MultiToken.sol"; -import { Math } from "openzeppelin-contracts/contracts/utils/math/Math.sol"; -import { SafeCast } from "openzeppelin-contracts/contracts/utils/math/SafeCast.sol"; +import { Math } from "openzeppelin/utils/math/Math.sol"; +import { SafeCast } from "openzeppelin/utils/math/SafeCast.sol"; import { PWNConfig } from "src/config/PWNConfig.sol"; import { PWNHub } from "src/hub/PWNHub.sol"; diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol index 698f334..398d854 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.16; import { MultiToken } from "MultiToken/MultiToken.sol"; -import { Math } from "openzeppelin-contracts/contracts/utils/math/Math.sol"; +import { Math } from "openzeppelin/utils/math/Math.sol"; import { PWNSimpleLoan } from "src/loan/terms/simple/loan/PWNSimpleLoan.sol"; import { PWNSimpleLoanProposal } from "src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol"; diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol index 7f40202..7d57d92 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.16; import { MultiToken } from "MultiToken/MultiToken.sol"; -import { Math } from "openzeppelin-contracts/contracts/utils/math/Math.sol"; +import { Math } from "openzeppelin/utils/math/Math.sol"; import { PWNSimpleLoan } from "src/loan/terms/simple/loan/PWNSimpleLoan.sol"; import { PWNSimpleLoanProposal } from "src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol"; diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol index 6513d06..8b04ecb 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.16; import { MultiToken } from "MultiToken/MultiToken.sol"; -import { MerkleProof } from "openzeppelin-contracts/contracts/utils/cryptography/MerkleProof.sol"; +import { MerkleProof } from "openzeppelin/utils/cryptography/MerkleProof.sol"; import { PWNSimpleLoan } from "src/loan/terms/simple/loan/PWNSimpleLoan.sol"; import { PWNSimpleLoanProposal } from "src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol"; diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol index bb83bce..fee71cb 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; -import { MerkleProof } from "openzeppelin-contracts/contracts/utils/cryptography/MerkleProof.sol"; -import { ERC165Checker } from "openzeppelin-contracts/contracts/utils/introspection/ERC165Checker.sol"; +import { MerkleProof } from "openzeppelin/utils/cryptography/MerkleProof.sol"; +import { ERC165Checker } from "openzeppelin/utils/introspection/ERC165Checker.sol"; import { PWNConfig, IStateFingerpringComputer } from "src/config/PWNConfig.sol"; import { PWNHub } from "src/hub/PWNHub.sol"; diff --git a/src/loan/token/PWNLOAN.sol b/src/loan/token/PWNLOAN.sol index c1fdf4d..9678362 100644 --- a/src/loan/token/PWNLOAN.sol +++ b/src/loan/token/PWNLOAN.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; -import { ERC721 } from "openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; +import { ERC721 } from "openzeppelin/token/ERC721/ERC721.sol"; import { PWNHubAccessControl } from "src/hub/PWNHubAccessControl.sol"; import { IERC5646 } from "src/loan/token/IERC5646.sol"; diff --git a/src/loan/vault/PWNVault.sol b/src/loan/vault/PWNVault.sol index 2c03d72..218a559 100644 --- a/src/loan/vault/PWNVault.sol +++ b/src/loan/vault/PWNVault.sol @@ -3,9 +3,9 @@ pragma solidity 0.8.16; import { MultiToken } from "MultiToken/MultiToken.sol"; -import { IERC20Permit } from "openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Permit.sol"; -import { IERC721Receiver } from "openzeppelin-contracts/contracts/token/ERC721/IERC721Receiver.sol"; -import { IERC1155Receiver, IERC165 } from "openzeppelin-contracts/contracts/token/ERC1155/IERC1155Receiver.sol"; +import { IERC20Permit } from "openzeppelin/token/ERC20/extensions/IERC20Permit.sol"; +import { IERC721Receiver } from "openzeppelin/token/ERC721/IERC721Receiver.sol"; +import { IERC1155Receiver, IERC165 } from "openzeppelin/token/ERC1155/IERC1155Receiver.sol"; import { Permit } from "src/loan/vault/Permit.sol"; import { IPoolAdapter } from "src/pool-adapter/IPoolAdapter.sol"; diff --git a/test/DeploymentTest.t.sol b/test/DeploymentTest.t.sol index e5e2595..7c3960f 100644 --- a/test/DeploymentTest.t.sol +++ b/test/DeploymentTest.t.sol @@ -1,14 +1,25 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; -import "forge-std/Test.sol"; - -import { MultiTokenCategoryRegistry, IMultiTokenCategoryRegistry } from "MultiToken/MultiTokenCategoryRegistry.sol"; - -import { TransparentUpgradeableProxy } - from "openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; - -import "src/Deployments.sol"; +import { Test } from "forge-std/Test.sol"; + +import { TransparentUpgradeableProxy } from "openzeppelin/proxy/transparent/TransparentUpgradeableProxy.sol"; + +import { + Deployments, + PWNConfig, + IPWNDeployer, + PWNHub, + PWNHubTags, + PWNSimpleLoan, + PWNSimpleLoanDutchAuctionProposal, + PWNSimpleLoanFungibleProposal, + PWNSimpleLoanListProposal, + PWNSimpleLoanSimpleProposal, + PWNLOAN, + PWNRevokedNonce, + MultiTokenCategoryRegistry +} from "src/Deployments.sol"; abstract contract DeploymentTest is Deployments, Test { @@ -23,7 +34,7 @@ abstract contract DeploymentTest is Deployments, Test { // Deploy category registry vm.prank(deployment.protocolSafe); - deployment.categoryRegistry = IMultiTokenCategoryRegistry(new MultiTokenCategoryRegistry()); + deployment.categoryRegistry = new MultiTokenCategoryRegistry(); // Deploy protocol deployment.configSingleton = new PWNConfig(); diff --git a/test/fork/DeployedProtocol.fork.t.sol b/test/fork/DeployedProtocol.fork.t.sol index cdd7c9a..0728fc2 100644 --- a/test/fork/DeployedProtocol.fork.t.sol +++ b/test/fork/DeployedProtocol.fork.t.sol @@ -1,11 +1,12 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; -import "forge-std/Test.sol"; +import { TimelockController } from "openzeppelin/governance/TimelockController.sol"; -import { TimelockController } from "openzeppelin-contracts/contracts/governance/TimelockController.sol"; - -import "test/DeploymentTest.t.sol"; +import { + DeploymentTest, + PWNHubTags +} from "test/DeploymentTest.t.sol"; contract DeployedProtocolTest is DeploymentTest { diff --git a/test/fork/UseCases.fork.t.sol b/test/fork/UseCases.fork.t.sol index 74c28da..93ae7a4 100644 --- a/test/fork/UseCases.fork.t.sol +++ b/test/fork/UseCases.fork.t.sol @@ -1,15 +1,19 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; -import "forge-std/Test.sol"; +import { MultiToken, ICryptoKitties, IERC20, IERC721 } from "MultiToken/MultiToken.sol"; -import { MultiToken, ICryptoKitties, IERC721 } from "MultiToken/MultiToken.sol"; - -import { Permit } from "@pwn/loan/vault/Permit.sol"; -import "@pwn/PWNErrors.sol"; +import { Permit } from "src/loan/vault/Permit.sol"; +import { CompoundAdapter } from "src/pool-adapter/CompoundAdapter.sol"; +import { UniV3PosStateFingerpringComputer } from "src/state-fingerprint-computer/UniV3PosStateFingerpringComputer.sol"; +import "src/PWNErrors.sol"; import { T20 } from "test/helper/T20.sol"; -import "test/DeploymentTest.t.sol"; +import { + DeploymentTest, + PWNSimpleLoan, + PWNSimpleLoanSimpleProposal +} from "test/DeploymentTest.t.sol"; abstract contract UseCasesTest is DeploymentTest { @@ -338,3 +342,303 @@ contract IncompleteERC20TokensTest is UseCasesTest { } } + + +contract CategoryRegistryForIncompleteERCTokensTest is UseCasesTest { + + function test_shouldPass_whenInvalidERC165Support() external { + address catCoinBank = 0xdeDf88899D7c9025F19C6c9F188DEb98D49CD760; + + // Register category + vm.prank(deployment.protocolSafe); + deployment.categoryRegistry.registerCategoryValue(catCoinBank, uint8(MultiToken.Category.ERC721)); + + // Prepare collateral + uint256 collId = 2; + address originalOwner = IERC721(catCoinBank).ownerOf(collId); + vm.prank(originalOwner); + IERC721(catCoinBank).transferFrom(originalOwner, borrower, collId); + + vm.prank(borrower); + IERC721(catCoinBank).setApprovalForAll(address(deployment.simpleLoan), true); + + // Update proposal + proposal.collateralCategory = MultiToken.Category.ERC721; + proposal.collateralAddress = catCoinBank; + proposal.collateralId = collId; + proposal.collateralAmount = 0; + + // Create loan + _createLoan(); + + // Check balance + assertEq(IERC721(catCoinBank).ownerOf(collId), address(deployment.simpleLoan)); + } + +} + + +interface UniV3PostLike { + struct DecreaseLiquidityParams { + uint256 tokenId; + uint128 liquidity; + uint256 amount0Min; + uint256 amount1Min; + uint256 deadline; + } + + /// @notice Decreases the amount of liquidity in a position and accounts it to the position + /// @param params tokenId The ID of the token for which liquidity is being decreased, + /// amount The amount by which liquidity will be decreased, + /// amount0Min The minimum amount of token0 that should be accounted for the burned liquidity, + /// amount1Min The minimum amount of token1 that should be accounted for the burned liquidity, + /// deadline The time by which the transaction must be included to effect the change + /// @return amount0 The amount of token0 accounted to the position's tokens owed + /// @return amount1 The amount of token1 accounted to the position's tokens owed + function decreaseLiquidity(DecreaseLiquidityParams calldata params) + external + payable + returns (uint256 amount0, uint256 amount1); +} + +contract StateFingerprintTest is UseCasesTest { + + address constant UNI_V3_POS = 0xC36442b4a4522E871399CD717aBDD847Ab11FE88; + + function test_shouldFail_whenUniV3PosStateChanges() external { + UniV3PosStateFingerpringComputer computer = new UniV3PosStateFingerpringComputer(UNI_V3_POS); + deployment.config.registerStateFingerprintComputer(UNI_V3_POS, address(computer)); + + uint256 collId = 1; + address originalOwner = IERC721(UNI_V3_POS).ownerOf(collId); + vm.prank(originalOwner); + IERC721(UNI_V3_POS).transferFrom(originalOwner, borrower, collId); + + vm.prank(borrower); + IERC721(UNI_V3_POS).setApprovalForAll(address(deployment.simpleLoan), true); + + // Define proposal + proposal.collateralCategory = MultiToken.Category.ERC721; + proposal.collateralAddress = UNI_V3_POS; + proposal.collateralId = collId; + proposal.collateralAmount = 0; + proposal.checkCollateralStateFingerprint = true; + proposal.collateralStateFingerprint = computer.computeStateFingerprint(UNI_V3_POS, collId); + + vm.prank(borrower); + UniV3PostLike(UNI_V3_POS).decreaseLiquidity(UniV3PostLike.DecreaseLiquidityParams({ + tokenId: collId, + liquidity: 100, + amount0Min: 0, + amount1Min: 0, + deadline: block.timestamp + 1 days + })); + bytes32 currentFingerprint = computer.computeStateFingerprint(UNI_V3_POS, collId); + + assertNotEq(currentFingerprint, proposal.collateralStateFingerprint); + + // Create loan + _createLoanRevertWith( + abi.encodeWithSelector(InvalidCollateralStateFingerprint.selector, currentFingerprint, proposal.collateralStateFingerprint) + ); + } + + function test_shouldPass_whenUniV3PosStateDoesNotChange() external { + UniV3PosStateFingerpringComputer computer = new UniV3PosStateFingerpringComputer(UNI_V3_POS); + deployment.config.registerStateFingerprintComputer(UNI_V3_POS, address(computer)); + + uint256 collId = 1; + address originalOwner = IERC721(UNI_V3_POS).ownerOf(collId); + vm.prank(originalOwner); + IERC721(UNI_V3_POS).transferFrom(originalOwner, borrower, collId); + + vm.prank(borrower); + IERC721(UNI_V3_POS).setApprovalForAll(address(deployment.simpleLoan), true); + + // Define proposal + proposal.collateralCategory = MultiToken.Category.ERC721; + proposal.collateralAddress = UNI_V3_POS; + proposal.collateralId = collId; + proposal.collateralAmount = 0; + proposal.checkCollateralStateFingerprint = true; + proposal.collateralStateFingerprint = computer.computeStateFingerprint(UNI_V3_POS, collId); + + // Create loan + _createLoan(); + + // Check balance + assertEq(IERC721(UNI_V3_POS).ownerOf(collId), address(deployment.simpleLoan)); + } + +} + + +interface ICometLike { + function allow(address manager, bool isAllowed) external; + function supply(address asset, uint amount) external; + function withdraw(address asset, uint amount) external; +} + +contract PoolAdapterTest is UseCasesTest { + + address constant CMP_USDC = 0xc3d688B66703497DAA19211EEdff47f25384cdc3; + address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + + function test_shouldWithdrawAndRepayToPool() external { + CompoundAdapter adapter = new CompoundAdapter(address(deployment.hub)); + deployment.config.registerPoolAdapter(CMP_USDC, address(adapter)); + + vm.prank(0x47ac0Fb4F2D84898e4D9E7b4DaB3C24507a6D503); + IERC20(USDC).transfer(lender, 1000e6); + + // Supply to pool 1k USDC + vm.startPrank(lender); + IERC20(USDC).approve(CMP_USDC, type(uint256).max); + ICometLike(CMP_USDC).supply(USDC, 1000e6); + ICometLike(CMP_USDC).allow(address(adapter), true); + vm.stopPrank(); + + vm.prank(borrower); + IERC20(USDC).approve(address(deployment.simpleLoan), type(uint256).max); + + // Update lender spec + PWNSimpleLoan.LenderSpec memory lenderSpec = PWNSimpleLoan.LenderSpec({ + sourceOfFunds: CMP_USDC + }); + + // Update proposal + proposal.creditAddress = USDC; + proposal.creditAmount = 100e6; // 100 USDC + proposal.proposerSpecHash = deployment.simpleLoan.getLenderSpecHash(lenderSpec); + + assertEq(IERC20(USDC).balanceOf(lender), 0); + assertEq(IERC20(USDC).balanceOf(borrower), 0); + + // Make proposal + vm.prank(lender); + deployment.simpleLoanSimpleProposal.makeProposal(proposal); + + bytes memory proposalData = deployment.simpleLoanSimpleProposal.encodeProposalData(proposal); + + // Create loan + vm.prank(borrower); + uint256 loanId = deployment.simpleLoan.createLOAN({ + proposalSpec: PWNSimpleLoan.ProposalSpec({ + proposalContract: address(deployment.simpleLoanSimpleProposal), + proposalData: proposalData, + proposalInclusionProof: new bytes32[](0), + signature: "" + }), + lenderSpec: lenderSpec, + callerSpec: PWNSimpleLoan.CallerSpec({ + refinancingLoanId: 0, + revokeNonce: false, + nonce: 0, + permitData: "" + }), + extra: "" + }); + + // Check balance + assertEq(IERC20(USDC).balanceOf(lender), 0); + assertEq(IERC20(USDC).balanceOf(borrower), 100e6); + + // Move in time + vm.warp(block.timestamp + 20 hours); + + // Repay loan + vm.prank(borrower); + deployment.simpleLoan.repayLOAN({ + loanId: loanId, + permitData: "" + }); + + // LOAN token owner is original lender -> repay funds to the pool + assertEq(IERC20(USDC).balanceOf(lender), 0); + assertEq(IERC20(USDC).balanceOf(borrower), 0); + vm.expectRevert("ERC721: invalid token ID"); + deployment.loanToken.ownerOf(loanId); + } + + function test_shouldWithdrawFromPoolAndRepayToVault() external { + CompoundAdapter adapter = new CompoundAdapter(address(deployment.hub)); + deployment.config.registerPoolAdapter(CMP_USDC, address(adapter)); + + vm.prank(0x47ac0Fb4F2D84898e4D9E7b4DaB3C24507a6D503); + IERC20(USDC).transfer(lender, 1000e6); + + // Supply to pool 1k USDC + vm.startPrank(lender); + IERC20(USDC).approve(CMP_USDC, type(uint256).max); + ICometLike(CMP_USDC).supply(USDC, 1000e6); + ICometLike(CMP_USDC).allow(address(adapter), true); + vm.stopPrank(); + + vm.prank(borrower); + IERC20(USDC).approve(address(deployment.simpleLoan), type(uint256).max); + + // Update lender spec + PWNSimpleLoan.LenderSpec memory lenderSpec = PWNSimpleLoan.LenderSpec({ + sourceOfFunds: CMP_USDC + }); + + // Update proposal + proposal.creditAddress = USDC; + proposal.creditAmount = 100e6; // 100 USDC + proposal.proposerSpecHash = deployment.simpleLoan.getLenderSpecHash(lenderSpec); + + assertEq(IERC20(USDC).balanceOf(lender), 0); + assertEq(IERC20(USDC).balanceOf(borrower), 0); + + // Make proposal + vm.prank(lender); + deployment.simpleLoanSimpleProposal.makeProposal(proposal); + + bytes memory proposalData = deployment.simpleLoanSimpleProposal.encodeProposalData(proposal); + + // Create loan + vm.prank(borrower); + uint256 loanId = deployment.simpleLoan.createLOAN({ + proposalSpec: PWNSimpleLoan.ProposalSpec({ + proposalContract: address(deployment.simpleLoanSimpleProposal), + proposalData: proposalData, + proposalInclusionProof: new bytes32[](0), + signature: "" + }), + lenderSpec: lenderSpec, + callerSpec: PWNSimpleLoan.CallerSpec({ + refinancingLoanId: 0, + revokeNonce: false, + nonce: 0, + permitData: "" + }), + extra: "" + }); + + // Check balance + assertEq(IERC20(USDC).balanceOf(lender), 0); + assertEq(IERC20(USDC).balanceOf(borrower), 100e6); + + // Move in time + vm.warp(block.timestamp + 20 hours); + + address newLender = makeAddr("new lender"); + + vm.prank(lender); + deployment.loanToken.transferFrom(lender, newLender, loanId); + + uint256 originalBalance = IERC20(USDC).balanceOf(address(deployment.simpleLoan)); + + // Repay loan + vm.prank(borrower); + deployment.simpleLoan.repayLOAN({ + loanId: loanId, + permitData: "" + }); + + // LOAN token owner is not original lender -> repay funds to the Vault + assertEq(IERC20(USDC).balanceOf(address(deployment.simpleLoan)), originalBalance + 100e6); + assertEq(deployment.loanToken.ownerOf(loanId), newLender); + } + +} diff --git a/test/helper/DummyPoolAdapter.sol b/test/helper/DummyPoolAdapter.sol index 76ad245..28fea26 100644 --- a/test/helper/DummyPoolAdapter.sol +++ b/test/helper/DummyPoolAdapter.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; -import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import { IERC20 } from "openzeppelin/token/ERC20/IERC20.sol"; import { IPoolAdapter } from "src/pool-adapter/IPoolAdapter.sol"; diff --git a/test/helper/T1155.sol b/test/helper/T1155.sol index d7144d9..7218c32 100644 --- a/test/helper/T1155.sol +++ b/test/helper/T1155.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.16; -import { ERC1155 } from "openzeppelin-contracts/contracts/token/ERC1155/ERC1155.sol"; +import { ERC1155 } from "openzeppelin/token/ERC1155/ERC1155.sol"; contract T1155 is ERC1155("uri://") { diff --git a/test/helper/T20.sol b/test/helper/T20.sol index aeeb6ca..119cbd9 100644 --- a/test/helper/T20.sol +++ b/test/helper/T20.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.16; -import { ERC20 } from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; +import { ERC20 } from "openzeppelin/token/ERC20/ERC20.sol"; contract T20 is ERC20("ERC20", "ERC20") { diff --git a/test/helper/T721.sol b/test/helper/T721.sol index 0039faf..a8cbcdc 100644 --- a/test/helper/T721.sol +++ b/test/helper/T721.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.16; -import { ERC721 } from "openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; +import { ERC721 } from "openzeppelin/token/ERC721/ERC721.sol"; contract T721 is ERC721("ERC721", "ERC721") { diff --git a/test/integration/BaseIntegrationTest.t.sol b/test/integration/BaseIntegrationTest.t.sol index 5162f9b..9be5226 100644 --- a/test/integration/BaseIntegrationTest.t.sol +++ b/test/integration/BaseIntegrationTest.t.sol @@ -1,8 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; -import "forge-std/Test.sol"; - import { MultiToken } from "MultiToken/MultiToken.sol"; import { Permit } from "src/loan/vault/Permit.sol"; @@ -10,7 +8,21 @@ import { Permit } from "src/loan/vault/Permit.sol"; import { T20 } from "test/helper/T20.sol"; import { T721 } from "test/helper/T721.sol"; import { T1155 } from "test/helper/T1155.sol"; -import "test/DeploymentTest.t.sol"; +import { + DeploymentTest, + PWNConfig, + IPWNDeployer, + PWNHub, + PWNHubTags, + PWNSimpleLoan, + PWNSimpleLoanDutchAuctionProposal, + PWNSimpleLoanFungibleProposal, + PWNSimpleLoanListProposal, + PWNSimpleLoanSimpleProposal, + PWNLOAN, + PWNRevokedNonce, + MultiTokenCategoryRegistry +} from "test/DeploymentTest.t.sol"; abstract contract BaseIntegrationTest is DeploymentTest { diff --git a/test/integration/PWNProtocolIntegrity.t.sol b/test/integration/PWNProtocolIntegrity.t.sol index 0942906..09067dd 100644 --- a/test/integration/PWNProtocolIntegrity.t.sol +++ b/test/integration/PWNProtocolIntegrity.t.sol @@ -1,12 +1,25 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; -import "forge-std/Test.sol"; - import { PWNHubTags } from "src/hub/PWNHubTags.sol"; import "src/PWNErrors.sol"; -import "test/integration/BaseIntegrationTest.t.sol"; +import { + MultiToken, + MultiTokenCategoryRegistry, + BaseIntegrationTest, + PWNConfig, + IPWNDeployer, + PWNHub, + PWNHubTags, + PWNSimpleLoan, + PWNSimpleLoanDutchAuctionProposal, + PWNSimpleLoanFungibleProposal, + PWNSimpleLoanListProposal, + PWNSimpleLoanSimpleProposal, + PWNLOAN, + PWNRevokedNonce +} from "test/integration/BaseIntegrationTest.t.sol"; contract PWNProtocolIntegrityTest is BaseIntegrationTest { diff --git a/test/integration/PWNSimpleLoanIntegration.t.sol b/test/integration/PWNSimpleLoanIntegration.t.sol index 18840ad..7e6e500 100644 --- a/test/integration/PWNSimpleLoanIntegration.t.sol +++ b/test/integration/PWNSimpleLoanIntegration.t.sol @@ -1,11 +1,24 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; -import "forge-std/Test.sol"; - import "src/PWNErrors.sol"; -import "test/integration/BaseIntegrationTest.t.sol"; +import { + MultiToken, + MultiTokenCategoryRegistry, + BaseIntegrationTest, + PWNConfig, + IPWNDeployer, + PWNHub, + PWNHubTags, + PWNSimpleLoan, + PWNSimpleLoanDutchAuctionProposal, + PWNSimpleLoanFungibleProposal, + PWNSimpleLoanListProposal, + PWNSimpleLoanSimpleProposal, + PWNLOAN, + PWNRevokedNonce +} from "test/integration/BaseIntegrationTest.t.sol"; contract PWNSimpleLoanIntegrationTest is BaseIntegrationTest { diff --git a/test/unit/PWNConfig.t.sol b/test/unit/PWNConfig.t.sol index aa98d4a..d0eb519 100644 --- a/test/unit/PWNConfig.t.sol +++ b/test/unit/PWNConfig.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; -import "forge-std/Test.sol"; +import { Test } from "forge-std/Test.sol"; import { PWNConfig } from "src/config/PWNConfig.sol"; import "src/PWNErrors.sol"; diff --git a/test/unit/PWNFeeCalculator.t.sol b/test/unit/PWNFeeCalculator.t.sol index 28105d7..a198d28 100644 --- a/test/unit/PWNFeeCalculator.t.sol +++ b/test/unit/PWNFeeCalculator.t.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; -import "forge-std/Test.sol"; +import { Test } from "forge-std/Test.sol"; -import "src/loan/lib/PWNFeeCalculator.sol"; +import { PWNFeeCalculator } from "src/loan/lib/PWNFeeCalculator.sol"; contract PWNFeeCalculator_CalculateFeeAmount_Test is Test { diff --git a/test/unit/PWNHub.t.sol b/test/unit/PWNHub.t.sol index b3caed4..fa83802 100644 --- a/test/unit/PWNHub.t.sol +++ b/test/unit/PWNHub.t.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; -import "forge-std/Test.sol"; +import { Test } from "forge-std/Test.sol"; -import "src/hub/PWNHub.sol"; +import { PWNHub } from "src/hub/PWNHub.sol"; import "src/PWNErrors.sol"; diff --git a/test/unit/PWNLOAN.t.sol b/test/unit/PWNLOAN.t.sol index b2681a2..17fc926 100644 --- a/test/unit/PWNLOAN.t.sol +++ b/test/unit/PWNLOAN.t.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; -import "forge-std/Test.sol"; +import { Test } from "forge-std/Test.sol"; -import "src/hub/PWNHubTags.sol"; -import "src/loan/token/IERC5646.sol"; -import "src/loan/token/PWNLOAN.sol"; +import { PWNHubTags } from "src/hub/PWNHubTags.sol"; +import { IERC5646 } from "src/loan/token/IERC5646.sol"; +import { PWNLOAN } from "src/loan/token/PWNLOAN.sol"; import "src/PWNErrors.sol"; diff --git a/test/unit/PWNRevokedNonce.t.sol b/test/unit/PWNRevokedNonce.t.sol index a6b418f..283d342 100644 --- a/test/unit/PWNRevokedNonce.t.sol +++ b/test/unit/PWNRevokedNonce.t.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; -import "forge-std/Test.sol"; +import { Test } from "forge-std/Test.sol"; -import "src/hub/PWNHubTags.sol"; -import "src/nonce/PWNRevokedNonce.sol"; +import { PWNHubTags } from "src/hub/PWNHubTags.sol"; +import { PWNRevokedNonce } from "src/nonce/PWNRevokedNonce.sol"; import "src/PWNErrors.sol"; diff --git a/test/unit/PWNSignatureChecker.t.sol b/test/unit/PWNSignatureChecker.t.sol index 95af055..5c19906 100644 --- a/test/unit/PWNSignatureChecker.t.sol +++ b/test/unit/PWNSignatureChecker.t.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; -import "forge-std/Test.sol"; +import { Test } from "forge-std/Test.sol"; -import "src/loan/lib/PWNSignatureChecker.sol"; +import { PWNSignatureChecker } from "src/loan/lib/PWNSignatureChecker.sol"; import "src/PWNErrors.sol"; diff --git a/test/unit/PWNSimpleLoan.t.sol b/test/unit/PWNSimpleLoan.t.sol index 1be6ab5..9122c72 100644 --- a/test/unit/PWNSimpleLoan.t.sol +++ b/test/unit/PWNSimpleLoan.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; -import "forge-std/Test.sol"; +import { Test } from "forge-std/Test.sol"; import { PWNSimpleLoan, PWNHubTags, Math, MultiToken, Permit } from "src/loan/terms/simple/loan/PWNSimpleLoan.sol"; import "src/PWNErrors.sol"; diff --git a/test/unit/PWNSimpleLoanDutchAuctionProposal.t.sol b/test/unit/PWNSimpleLoanDutchAuctionProposal.t.sol index 91cd0fb..4677758 100644 --- a/test/unit/PWNSimpleLoanDutchAuctionProposal.t.sol +++ b/test/unit/PWNSimpleLoanDutchAuctionProposal.t.sol @@ -1,18 +1,17 @@ // 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 "src/hub/PWNHubTags.sol"; -import { PWNSimpleLoanDutchAuctionProposal, PWNSimpleLoanProposal, PWNSimpleLoan } - from "src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol"; +import { + PWNSimpleLoanDutchAuctionProposal, + PWNSimpleLoanProposal, + PWNSimpleLoan +} from "src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol"; import "src/PWNErrors.sol"; import { + MultiToken, + Math, PWNSimpleLoanProposalTest, PWNSimpleLoanProposal_AcceptProposal_Test } from "test/unit/PWNSimpleLoanProposal.t.sol"; diff --git a/test/unit/PWNSimpleLoanFungibleProposal.t.sol b/test/unit/PWNSimpleLoanFungibleProposal.t.sol index 411f9af..2148765 100644 --- a/test/unit/PWNSimpleLoanFungibleProposal.t.sol +++ b/test/unit/PWNSimpleLoanFungibleProposal.t.sol @@ -1,18 +1,17 @@ // 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 "src/hub/PWNHubTags.sol"; -import { PWNSimpleLoanFungibleProposal, PWNSimpleLoanProposal, PWNSimpleLoan } - from "src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol"; +import { + PWNSimpleLoanFungibleProposal, + PWNSimpleLoanProposal, + PWNSimpleLoan +} from "src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol"; import "src/PWNErrors.sol"; import { + MultiToken, + Math, PWNSimpleLoanProposalTest, PWNSimpleLoanProposal_AcceptProposal_Test } from "test/unit/PWNSimpleLoanProposal.t.sol"; diff --git a/test/unit/PWNSimpleLoanListProposal.t.sol b/test/unit/PWNSimpleLoanListProposal.t.sol index 53b6c5d..f2f35c6 100644 --- a/test/unit/PWNSimpleLoanListProposal.t.sol +++ b/test/unit/PWNSimpleLoanListProposal.t.sol @@ -1,18 +1,17 @@ // 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 "src/hub/PWNHubTags.sol"; -import { PWNSimpleLoanListProposal, PWNSimpleLoanProposal, PWNSimpleLoan } - from "src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol"; +import { + PWNSimpleLoanListProposal, + PWNSimpleLoanProposal, + PWNSimpleLoan +} from "src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol"; import "src/PWNErrors.sol"; import { + MultiToken, + Math, PWNSimpleLoanProposalTest, PWNSimpleLoanProposal_AcceptProposal_Test } from "test/unit/PWNSimpleLoanProposal.t.sol"; diff --git a/test/unit/PWNSimpleLoanProposal.t.sol b/test/unit/PWNSimpleLoanProposal.t.sol index 8ed20f0..8295483 100644 --- a/test/unit/PWNSimpleLoanProposal.t.sol +++ b/test/unit/PWNSimpleLoanProposal.t.sol @@ -1,13 +1,13 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; -import "forge-std/Test.sol"; +import { Test } from "forge-std/Test.sol"; import { MultiToken } from "MultiToken/MultiToken.sol"; -import { MerkleProof } from "openzeppelin-contracts/contracts/utils/cryptography/MerkleProof.sol"; -import { IERC165 } from "openzeppelin-contracts/contracts/utils/introspection/IERC165.sol"; -import { Math } from "openzeppelin-contracts/contracts/utils/math/Math.sol"; +import { MerkleProof } from "openzeppelin/utils/cryptography/MerkleProof.sol"; +import { IERC165 } from "openzeppelin/utils/introspection/IERC165.sol"; +import { Math } from "openzeppelin/utils/math/Math.sol"; import { PWNHubTags } from "src/hub/PWNHubTags.sol"; import { PWNSimpleLoan } from "src/loan/terms/simple/loan/PWNSimpleLoan.sol"; diff --git a/test/unit/PWNSimpleLoanSimpleProposal.t.sol b/test/unit/PWNSimpleLoanSimpleProposal.t.sol index aa6d47b..6fae1e4 100644 --- a/test/unit/PWNSimpleLoanSimpleProposal.t.sol +++ b/test/unit/PWNSimpleLoanSimpleProposal.t.sol @@ -1,16 +1,16 @@ // 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 "src/hub/PWNHubTags.sol"; -import { PWNSimpleLoanSimpleProposal, PWNSimpleLoanProposal, PWNSimpleLoan } - from "src/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol"; +import { + PWNSimpleLoanSimpleProposal, + PWNSimpleLoanProposal, + PWNSimpleLoan +} from "src/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol"; import "src/PWNErrors.sol"; import { + MultiToken, PWNSimpleLoanProposalTest, PWNSimpleLoanProposal_AcceptProposal_Test } from "test/unit/PWNSimpleLoanProposal.t.sol"; diff --git a/test/unit/PWNVault.t.sol b/test/unit/PWNVault.t.sol index 3d60e3c..e063a46 100644 --- a/test/unit/PWNVault.t.sol +++ b/test/unit/PWNVault.t.sol @@ -1,9 +1,16 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; -import "forge-std/Test.sol"; - -import { PWNVault, IERC165, IERC721Receiver, IERC1155Receiver, Permit, MultiToken } from "src/loan/vault/PWNVault.sol"; +import { Test } from "forge-std/Test.sol"; + +import { + MultiToken, + PWNVault, + IERC165, + IERC721Receiver, + IERC1155Receiver, + Permit +} from "src/loan/vault/PWNVault.sol"; import "src/PWNErrors.sol"; import { T721 } from "test/helper/T721.sol"; From 8545d52ebdab241662d4171460015f5f5bbb2e14 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Sat, 13 Apr 2024 19:46:18 -0400 Subject: [PATCH 090/129] refactor: move interfaces into its own directory --- src/Deployments.sol | 2 +- src/config/PWNConfig.sol | 4 ++-- src/{loan/token => interfaces}/IERC5646.sol | 0 src/{deployer => interfaces}/IPWNDeployer.sol | 0 src/{loan/token => interfaces}/IPWNLoanMetadataProvider.sol | 0 src/{pool-adapter => interfaces}/IPoolAdapter.sol | 0 .../IStateFingerpringComputer.sol | 0 src/loan/terms/simple/loan/PWNSimpleLoan.sol | 6 +++--- src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol | 2 +- src/loan/token/PWNLOAN.sol | 4 ++-- src/loan/vault/PWNVault.sol | 2 +- test/helper/DummyPoolAdapter.sol | 2 +- test/unit/PWNLOAN.t.sol | 2 +- 13 files changed, 12 insertions(+), 12 deletions(-) rename src/{loan/token => interfaces}/IERC5646.sol (100%) rename src/{deployer => interfaces}/IPWNDeployer.sol (100%) rename src/{loan/token => interfaces}/IPWNLoanMetadataProvider.sol (100%) rename src/{pool-adapter => interfaces}/IPoolAdapter.sol (100%) rename src/{state-fingerprint-computer => interfaces}/IStateFingerpringComputer.sol (100%) diff --git a/src/Deployments.sol b/src/Deployments.sol index eba5082..d459669 100644 --- a/src/Deployments.sol +++ b/src/Deployments.sol @@ -9,9 +9,9 @@ import { MultiTokenCategoryRegistry } from "MultiToken/MultiTokenCategoryRegistr import { Strings } from "openzeppelin/utils/Strings.sol"; import { PWNConfig } from "src/config/PWNConfig.sol"; -import { IPWNDeployer } from "src/deployer/IPWNDeployer.sol"; import { PWNHub } from "src/hub/PWNHub.sol"; import { PWNHubTags } from "src/hub/PWNHubTags.sol"; +import { IPWNDeployer } from "src/interfaces/IPWNDeployer.sol"; import { PWNSimpleLoan } from "src/loan/terms/simple/loan/PWNSimpleLoan.sol"; import { PWNSimpleLoanDutchAuctionProposal } from "src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol"; import { PWNSimpleLoanFungibleProposal } from "src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol"; diff --git a/src/config/PWNConfig.sol b/src/config/PWNConfig.sol index 7ff7b6c..d4799d4 100644 --- a/src/config/PWNConfig.sol +++ b/src/config/PWNConfig.sol @@ -4,8 +4,8 @@ pragma solidity 0.8.16; import { Ownable2Step } from "openzeppelin/access/Ownable2Step.sol"; import { Initializable } from "openzeppelin/proxy/utils/Initializable.sol"; -import { IPoolAdapter } from "src/pool-adapter/IPoolAdapter.sol"; -import { IStateFingerpringComputer } from "src/state-fingerprint-computer/IStateFingerpringComputer.sol"; +import { IPoolAdapter } from "src/interfaces/IPoolAdapter.sol"; +import { IStateFingerpringComputer } from "src/interfaces/IStateFingerpringComputer.sol"; import "src/PWNErrors.sol"; diff --git a/src/loan/token/IERC5646.sol b/src/interfaces/IERC5646.sol similarity index 100% rename from src/loan/token/IERC5646.sol rename to src/interfaces/IERC5646.sol diff --git a/src/deployer/IPWNDeployer.sol b/src/interfaces/IPWNDeployer.sol similarity index 100% rename from src/deployer/IPWNDeployer.sol rename to src/interfaces/IPWNDeployer.sol diff --git a/src/loan/token/IPWNLoanMetadataProvider.sol b/src/interfaces/IPWNLoanMetadataProvider.sol similarity index 100% rename from src/loan/token/IPWNLoanMetadataProvider.sol rename to src/interfaces/IPWNLoanMetadataProvider.sol diff --git a/src/pool-adapter/IPoolAdapter.sol b/src/interfaces/IPoolAdapter.sol similarity index 100% rename from src/pool-adapter/IPoolAdapter.sol rename to src/interfaces/IPoolAdapter.sol diff --git a/src/state-fingerprint-computer/IStateFingerpringComputer.sol b/src/interfaces/IStateFingerpringComputer.sol similarity index 100% rename from src/state-fingerprint-computer/IStateFingerpringComputer.sol rename to src/interfaces/IStateFingerpringComputer.sol diff --git a/src/loan/terms/simple/loan/PWNSimpleLoan.sol b/src/loan/terms/simple/loan/PWNSimpleLoan.sol index e6109e5..989668d 100644 --- a/src/loan/terms/simple/loan/PWNSimpleLoan.sol +++ b/src/loan/terms/simple/loan/PWNSimpleLoan.sol @@ -9,16 +9,16 @@ import { SafeCast } from "openzeppelin/utils/math/SafeCast.sol"; import { PWNConfig } from "src/config/PWNConfig.sol"; import { PWNHub } from "src/hub/PWNHub.sol"; import { PWNHubTags } from "src/hub/PWNHubTags.sol"; +import { IERC5646 } from "src/interfaces/IERC5646.sol"; +import { IPoolAdapter } from "src/interfaces/IPoolAdapter.sol"; +import { IPWNLoanMetadataProvider } from "src/interfaces/IPWNLoanMetadataProvider.sol"; import { PWNFeeCalculator } from "src/loan/lib/PWNFeeCalculator.sol"; import { PWNSignatureChecker } from "src/loan/lib/PWNSignatureChecker.sol"; import { PWNSimpleLoanProposal } from "src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol"; -import { IERC5646 } from "src/loan/token/IERC5646.sol"; -import { IPWNLoanMetadataProvider } from "src/loan/token/IPWNLoanMetadataProvider.sol"; import { PWNLOAN } from "src/loan/token/PWNLOAN.sol"; import { Permit } from "src/loan/vault/Permit.sol"; import { PWNVault } from "src/loan/vault/PWNVault.sol"; import { PWNRevokedNonce } from "src/nonce/PWNRevokedNonce.sol"; -import { IPoolAdapter } from "src/pool-adapter/IPoolAdapter.sol"; import "src/PWNErrors.sol"; diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol index fee71cb..f0400e8 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol @@ -7,9 +7,9 @@ import { ERC165Checker } from "openzeppelin/utils/introspection/ERC165Checker.so import { PWNConfig, IStateFingerpringComputer } from "src/config/PWNConfig.sol"; import { PWNHub } from "src/hub/PWNHub.sol"; import { PWNHubTags } from "src/hub/PWNHubTags.sol"; +import { IERC5646 } from "src/interfaces/IERC5646.sol"; import { PWNSignatureChecker } from "src/loan/lib/PWNSignatureChecker.sol"; import { PWNSimpleLoan } from "src/loan/terms/simple/loan/PWNSimpleLoan.sol"; -import { IERC5646 } from "src/loan/token/IERC5646.sol"; import { PWNRevokedNonce } from "src/nonce/PWNRevokedNonce.sol"; import "src/PWNErrors.sol"; diff --git a/src/loan/token/PWNLOAN.sol b/src/loan/token/PWNLOAN.sol index 9678362..4eee3fd 100644 --- a/src/loan/token/PWNLOAN.sol +++ b/src/loan/token/PWNLOAN.sol @@ -4,8 +4,8 @@ pragma solidity 0.8.16; import { ERC721 } from "openzeppelin/token/ERC721/ERC721.sol"; import { PWNHubAccessControl } from "src/hub/PWNHubAccessControl.sol"; -import { IERC5646 } from "src/loan/token/IERC5646.sol"; -import { IPWNLoanMetadataProvider } from "src/loan/token/IPWNLoanMetadataProvider.sol"; +import { IERC5646 } from "src/interfaces/IERC5646.sol"; +import { IPWNLoanMetadataProvider } from "src/interfaces/IPWNLoanMetadataProvider.sol"; import "src/PWNErrors.sol"; diff --git a/src/loan/vault/PWNVault.sol b/src/loan/vault/PWNVault.sol index 218a559..3f2e201 100644 --- a/src/loan/vault/PWNVault.sol +++ b/src/loan/vault/PWNVault.sol @@ -7,8 +7,8 @@ import { IERC20Permit } from "openzeppelin/token/ERC20/extensions/IERC20Permit.s import { IERC721Receiver } from "openzeppelin/token/ERC721/IERC721Receiver.sol"; import { IERC1155Receiver, IERC165 } from "openzeppelin/token/ERC1155/IERC1155Receiver.sol"; +import { IPoolAdapter } from "src/interfaces/IPoolAdapter.sol"; import { Permit } from "src/loan/vault/Permit.sol"; -import { IPoolAdapter } from "src/pool-adapter/IPoolAdapter.sol"; import "src/PWNErrors.sol"; diff --git a/test/helper/DummyPoolAdapter.sol b/test/helper/DummyPoolAdapter.sol index 28fea26..900ebb7 100644 --- a/test/helper/DummyPoolAdapter.sol +++ b/test/helper/DummyPoolAdapter.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.16; import { IERC20 } from "openzeppelin/token/ERC20/IERC20.sol"; -import { IPoolAdapter } from "src/pool-adapter/IPoolAdapter.sol"; +import { IPoolAdapter } from "src/interfaces/IPoolAdapter.sol"; contract DummyPoolAdapter is IPoolAdapter { diff --git a/test/unit/PWNLOAN.t.sol b/test/unit/PWNLOAN.t.sol index 17fc926..f9810b5 100644 --- a/test/unit/PWNLOAN.t.sol +++ b/test/unit/PWNLOAN.t.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.16; import { Test } from "forge-std/Test.sol"; import { PWNHubTags } from "src/hub/PWNHubTags.sol"; -import { IERC5646 } from "src/loan/token/IERC5646.sol"; +import { IERC5646 } from "src/interfaces/IERC5646.sol"; import { PWNLOAN } from "src/loan/token/PWNLOAN.sol"; import "src/PWNErrors.sol"; From b5d1944c66516aa5ba9394cfab16457838651ed1 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Sat, 13 Apr 2024 19:46:52 -0400 Subject: [PATCH 091/129] refactor: move state fingerprint computers into standalone repo --- .../ChickenBondStateFingerpringComputer.sol | 103 ------------------ .../UniV3PosStateFingerpringComputer.sol | 92 ---------------- 2 files changed, 195 deletions(-) delete mode 100644 src/state-fingerprint-computer/ChickenBondStateFingerpringComputer.sol delete mode 100644 src/state-fingerprint-computer/UniV3PosStateFingerpringComputer.sol diff --git a/src/state-fingerprint-computer/ChickenBondStateFingerpringComputer.sol b/src/state-fingerprint-computer/ChickenBondStateFingerpringComputer.sol deleted file mode 100644 index 2fc0916..0000000 --- a/src/state-fingerprint-computer/ChickenBondStateFingerpringComputer.sol +++ /dev/null @@ -1,103 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity 0.8.16; - -import { IStateFingerpringComputer } from "src/state-fingerprint-computer/IStateFingerpringComputer.sol"; - - -interface IChickenBondManagerLike { - function getBondData(uint256 _bondID) - external - view - returns ( - uint256 lusdAmount, - uint64 claimedBLUSD, - uint64 startTime, - uint64 endTime, - uint8 status - ); -} - -interface IChickenBondNFTLike { - function getBondExtraData(uint256 _tokenID) - external - view - returns ( - uint80 initialHalfDna, - uint80 finalHalfDna, - uint32 troveSize, - uint32 lqtyAmount, - uint32 curveGaugeSlopes - ); -} - -/** - * @notice State fingerprint computer for Chicken Bond positions. - * @dev Computer will get bond data from `CHICKEN_BOND_MANAGER` and `CHICKEN_BOND`. - */ -contract ChickenBondStateFingerpringComputer is IStateFingerpringComputer { - - address immutable public CHICKEN_BOND_MANAGER; - address immutable public CHICKEN_BOND; - - error UnsupportedToken(); - - constructor(address _chickenBondManager, address _chickenBond) { - CHICKEN_BOND_MANAGER = _chickenBondManager; - CHICKEN_BOND = _chickenBond; - } - - /** - * @inheritdoc IStateFingerpringComputer - */ - function computeStateFingerprint(address token, uint256 tokenId) external view returns (bytes32) { - if (token != CHICKEN_BOND) { - revert UnsupportedToken(); - } - - return _computeStateFingerprint(tokenId); - } - - /** - * @inheritdoc IStateFingerpringComputer - */ - function supportsToken(address token) external view returns (bool) { - return token == CHICKEN_BOND; - } - - /** - * @notice Compute current token state fingerprint of a Chicken Bond. - * @param tokenId Token id to compute state fingerprint for. - * @return Current token state fingerprint. - */ - function _computeStateFingerprint(uint256 tokenId) private view returns (bytes32) { - ( - uint256 lusdAmount, - uint64 claimedBLUSD, - uint64 startTime, - uint64 endTime, - uint8 status - ) = IChickenBondManagerLike(CHICKEN_BOND_MANAGER).getBondData(tokenId); - - ( - uint80 initialHalfDna, - uint80 finalHalfDna, - uint32 troveSize, - uint32 lqtyAmount, - uint32 curveGaugeSlopes - ) = IChickenBondNFTLike(CHICKEN_BOND).getBondExtraData(tokenId); - - return keccak256(abi.encode( - lusdAmount, - claimedBLUSD, - startTime, - endTime, - status, - initialHalfDna, - finalHalfDna, - troveSize, - lqtyAmount, - curveGaugeSlopes - )); - } - -} diff --git a/src/state-fingerprint-computer/UniV3PosStateFingerpringComputer.sol b/src/state-fingerprint-computer/UniV3PosStateFingerpringComputer.sol deleted file mode 100644 index 077ae0a..0000000 --- a/src/state-fingerprint-computer/UniV3PosStateFingerpringComputer.sol +++ /dev/null @@ -1,92 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity 0.8.16; - -import { IStateFingerpringComputer } from "src/state-fingerprint-computer/IStateFingerpringComputer.sol"; - - -interface UniswapNonFungiblePositionManagerLike { - function positions(uint256 tokenId) external view returns ( - uint96 nonce, - address operator, - address token0, - address token1, - uint24 fee, - int24 tickLower, - int24 tickUpper, - uint128 liquidity, - uint256 feeGrowthInside0LastX128, - uint256 feeGrowthInside1LastX128, - uint128 tokensOwed0, - uint128 tokensOwed1 - ); -} - -/** - * @notice State fingerprint computer for Uniswap v3 positions. - */ -contract UniV3PosStateFingerpringComputer is IStateFingerpringComputer { - - address immutable public UNI_V3_POS; - - error UnsupportedToken(); - - constructor(address _uniV3Pos) { - UNI_V3_POS = _uniV3Pos; - } - - /** - * @inheritdoc IStateFingerpringComputer - */ - function computeStateFingerprint(address token, uint256 tokenId) external view returns (bytes32) { - if (token != UNI_V3_POS) { - revert UnsupportedToken(); - } - - return _computeStateFingerprint(tokenId); - } - - /** - * @inheritdoc IStateFingerpringComputer - */ - function supportsToken(address token) external view returns (bool) { - return token == UNI_V3_POS; - } - - /** - * @notice Compute current token state fingerprint of a Uniswap v3 position. - * @param tokenId Token id to compute state fingerprint for. - * @return Current token state fingerprint. - */ - function _computeStateFingerprint(uint256 tokenId) private view returns (bytes32) { - ( - uint96 nonce, - address operator, - address token0, - address token1, - uint24 fee, - int24 tickLower, - int24 tickUpper, - uint128 liquidity, - uint256 feeGrowthInside0LastX128, - uint256 feeGrowthInside1LastX128, - uint128 tokensOwed0, - uint128 tokensOwed1 - ) = UniswapNonFungiblePositionManagerLike(UNI_V3_POS).positions(tokenId); - - return keccak256(abi.encode( - nonce, - operator, - token0, - token1, - fee, - tickLower, - tickUpper, - liquidity, - feeGrowthInside0LastX128, - feeGrowthInside1LastX128, - tokensOwed0, - tokensOwed1 - )); - } - -} From 360aa79bb121c98346d580275a8e65b30032884d Mon Sep 17 00:00:00 2001 From: ashhanai Date: Sun, 14 Apr 2024 11:41:29 -0400 Subject: [PATCH 092/129] build(multi-token): increase multi token lib to v3.0.0 --- lib/MultiToken | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/MultiToken b/lib/MultiToken index 9f5d642..d4f604a 160000 --- a/lib/MultiToken +++ b/lib/MultiToken @@ -1 +1 @@ -Subproject commit 9f5d642f682e13d67c0266b4250f8e54f484be8b +Subproject commit d4f604a27caa5f22610d1a86d4d00973c819ca97 From 0d0188229c1aa5f190a15cdf30b97a7c1d644a44 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Sun, 14 Apr 2024 11:50:59 -0400 Subject: [PATCH 093/129] test: remove periphery contracts fork tests --- test/fork/UseCases.fork.t.sol | 268 ---------------------------------- 1 file changed, 268 deletions(-) diff --git a/test/fork/UseCases.fork.t.sol b/test/fork/UseCases.fork.t.sol index 93ae7a4..84c8823 100644 --- a/test/fork/UseCases.fork.t.sol +++ b/test/fork/UseCases.fork.t.sol @@ -4,8 +4,6 @@ pragma solidity 0.8.16; import { MultiToken, ICryptoKitties, IERC20, IERC721 } from "MultiToken/MultiToken.sol"; import { Permit } from "src/loan/vault/Permit.sol"; -import { CompoundAdapter } from "src/pool-adapter/CompoundAdapter.sol"; -import { UniV3PosStateFingerpringComputer } from "src/state-fingerprint-computer/UniV3PosStateFingerpringComputer.sol"; import "src/PWNErrors.sol"; import { T20 } from "test/helper/T20.sol"; @@ -376,269 +374,3 @@ contract CategoryRegistryForIncompleteERCTokensTest is UseCasesTest { } } - - -interface UniV3PostLike { - struct DecreaseLiquidityParams { - uint256 tokenId; - uint128 liquidity; - uint256 amount0Min; - uint256 amount1Min; - uint256 deadline; - } - - /// @notice Decreases the amount of liquidity in a position and accounts it to the position - /// @param params tokenId The ID of the token for which liquidity is being decreased, - /// amount The amount by which liquidity will be decreased, - /// amount0Min The minimum amount of token0 that should be accounted for the burned liquidity, - /// amount1Min The minimum amount of token1 that should be accounted for the burned liquidity, - /// deadline The time by which the transaction must be included to effect the change - /// @return amount0 The amount of token0 accounted to the position's tokens owed - /// @return amount1 The amount of token1 accounted to the position's tokens owed - function decreaseLiquidity(DecreaseLiquidityParams calldata params) - external - payable - returns (uint256 amount0, uint256 amount1); -} - -contract StateFingerprintTest is UseCasesTest { - - address constant UNI_V3_POS = 0xC36442b4a4522E871399CD717aBDD847Ab11FE88; - - function test_shouldFail_whenUniV3PosStateChanges() external { - UniV3PosStateFingerpringComputer computer = new UniV3PosStateFingerpringComputer(UNI_V3_POS); - deployment.config.registerStateFingerprintComputer(UNI_V3_POS, address(computer)); - - uint256 collId = 1; - address originalOwner = IERC721(UNI_V3_POS).ownerOf(collId); - vm.prank(originalOwner); - IERC721(UNI_V3_POS).transferFrom(originalOwner, borrower, collId); - - vm.prank(borrower); - IERC721(UNI_V3_POS).setApprovalForAll(address(deployment.simpleLoan), true); - - // Define proposal - proposal.collateralCategory = MultiToken.Category.ERC721; - proposal.collateralAddress = UNI_V3_POS; - proposal.collateralId = collId; - proposal.collateralAmount = 0; - proposal.checkCollateralStateFingerprint = true; - proposal.collateralStateFingerprint = computer.computeStateFingerprint(UNI_V3_POS, collId); - - vm.prank(borrower); - UniV3PostLike(UNI_V3_POS).decreaseLiquidity(UniV3PostLike.DecreaseLiquidityParams({ - tokenId: collId, - liquidity: 100, - amount0Min: 0, - amount1Min: 0, - deadline: block.timestamp + 1 days - })); - bytes32 currentFingerprint = computer.computeStateFingerprint(UNI_V3_POS, collId); - - assertNotEq(currentFingerprint, proposal.collateralStateFingerprint); - - // Create loan - _createLoanRevertWith( - abi.encodeWithSelector(InvalidCollateralStateFingerprint.selector, currentFingerprint, proposal.collateralStateFingerprint) - ); - } - - function test_shouldPass_whenUniV3PosStateDoesNotChange() external { - UniV3PosStateFingerpringComputer computer = new UniV3PosStateFingerpringComputer(UNI_V3_POS); - deployment.config.registerStateFingerprintComputer(UNI_V3_POS, address(computer)); - - uint256 collId = 1; - address originalOwner = IERC721(UNI_V3_POS).ownerOf(collId); - vm.prank(originalOwner); - IERC721(UNI_V3_POS).transferFrom(originalOwner, borrower, collId); - - vm.prank(borrower); - IERC721(UNI_V3_POS).setApprovalForAll(address(deployment.simpleLoan), true); - - // Define proposal - proposal.collateralCategory = MultiToken.Category.ERC721; - proposal.collateralAddress = UNI_V3_POS; - proposal.collateralId = collId; - proposal.collateralAmount = 0; - proposal.checkCollateralStateFingerprint = true; - proposal.collateralStateFingerprint = computer.computeStateFingerprint(UNI_V3_POS, collId); - - // Create loan - _createLoan(); - - // Check balance - assertEq(IERC721(UNI_V3_POS).ownerOf(collId), address(deployment.simpleLoan)); - } - -} - - -interface ICometLike { - function allow(address manager, bool isAllowed) external; - function supply(address asset, uint amount) external; - function withdraw(address asset, uint amount) external; -} - -contract PoolAdapterTest is UseCasesTest { - - address constant CMP_USDC = 0xc3d688B66703497DAA19211EEdff47f25384cdc3; - address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; - - function test_shouldWithdrawAndRepayToPool() external { - CompoundAdapter adapter = new CompoundAdapter(address(deployment.hub)); - deployment.config.registerPoolAdapter(CMP_USDC, address(adapter)); - - vm.prank(0x47ac0Fb4F2D84898e4D9E7b4DaB3C24507a6D503); - IERC20(USDC).transfer(lender, 1000e6); - - // Supply to pool 1k USDC - vm.startPrank(lender); - IERC20(USDC).approve(CMP_USDC, type(uint256).max); - ICometLike(CMP_USDC).supply(USDC, 1000e6); - ICometLike(CMP_USDC).allow(address(adapter), true); - vm.stopPrank(); - - vm.prank(borrower); - IERC20(USDC).approve(address(deployment.simpleLoan), type(uint256).max); - - // Update lender spec - PWNSimpleLoan.LenderSpec memory lenderSpec = PWNSimpleLoan.LenderSpec({ - sourceOfFunds: CMP_USDC - }); - - // Update proposal - proposal.creditAddress = USDC; - proposal.creditAmount = 100e6; // 100 USDC - proposal.proposerSpecHash = deployment.simpleLoan.getLenderSpecHash(lenderSpec); - - assertEq(IERC20(USDC).balanceOf(lender), 0); - assertEq(IERC20(USDC).balanceOf(borrower), 0); - - // Make proposal - vm.prank(lender); - deployment.simpleLoanSimpleProposal.makeProposal(proposal); - - bytes memory proposalData = deployment.simpleLoanSimpleProposal.encodeProposalData(proposal); - - // Create loan - vm.prank(borrower); - uint256 loanId = deployment.simpleLoan.createLOAN({ - proposalSpec: PWNSimpleLoan.ProposalSpec({ - proposalContract: address(deployment.simpleLoanSimpleProposal), - proposalData: proposalData, - proposalInclusionProof: new bytes32[](0), - signature: "" - }), - lenderSpec: lenderSpec, - callerSpec: PWNSimpleLoan.CallerSpec({ - refinancingLoanId: 0, - revokeNonce: false, - nonce: 0, - permitData: "" - }), - extra: "" - }); - - // Check balance - assertEq(IERC20(USDC).balanceOf(lender), 0); - assertEq(IERC20(USDC).balanceOf(borrower), 100e6); - - // Move in time - vm.warp(block.timestamp + 20 hours); - - // Repay loan - vm.prank(borrower); - deployment.simpleLoan.repayLOAN({ - loanId: loanId, - permitData: "" - }); - - // LOAN token owner is original lender -> repay funds to the pool - assertEq(IERC20(USDC).balanceOf(lender), 0); - assertEq(IERC20(USDC).balanceOf(borrower), 0); - vm.expectRevert("ERC721: invalid token ID"); - deployment.loanToken.ownerOf(loanId); - } - - function test_shouldWithdrawFromPoolAndRepayToVault() external { - CompoundAdapter adapter = new CompoundAdapter(address(deployment.hub)); - deployment.config.registerPoolAdapter(CMP_USDC, address(adapter)); - - vm.prank(0x47ac0Fb4F2D84898e4D9E7b4DaB3C24507a6D503); - IERC20(USDC).transfer(lender, 1000e6); - - // Supply to pool 1k USDC - vm.startPrank(lender); - IERC20(USDC).approve(CMP_USDC, type(uint256).max); - ICometLike(CMP_USDC).supply(USDC, 1000e6); - ICometLike(CMP_USDC).allow(address(adapter), true); - vm.stopPrank(); - - vm.prank(borrower); - IERC20(USDC).approve(address(deployment.simpleLoan), type(uint256).max); - - // Update lender spec - PWNSimpleLoan.LenderSpec memory lenderSpec = PWNSimpleLoan.LenderSpec({ - sourceOfFunds: CMP_USDC - }); - - // Update proposal - proposal.creditAddress = USDC; - proposal.creditAmount = 100e6; // 100 USDC - proposal.proposerSpecHash = deployment.simpleLoan.getLenderSpecHash(lenderSpec); - - assertEq(IERC20(USDC).balanceOf(lender), 0); - assertEq(IERC20(USDC).balanceOf(borrower), 0); - - // Make proposal - vm.prank(lender); - deployment.simpleLoanSimpleProposal.makeProposal(proposal); - - bytes memory proposalData = deployment.simpleLoanSimpleProposal.encodeProposalData(proposal); - - // Create loan - vm.prank(borrower); - uint256 loanId = deployment.simpleLoan.createLOAN({ - proposalSpec: PWNSimpleLoan.ProposalSpec({ - proposalContract: address(deployment.simpleLoanSimpleProposal), - proposalData: proposalData, - proposalInclusionProof: new bytes32[](0), - signature: "" - }), - lenderSpec: lenderSpec, - callerSpec: PWNSimpleLoan.CallerSpec({ - refinancingLoanId: 0, - revokeNonce: false, - nonce: 0, - permitData: "" - }), - extra: "" - }); - - // Check balance - assertEq(IERC20(USDC).balanceOf(lender), 0); - assertEq(IERC20(USDC).balanceOf(borrower), 100e6); - - // Move in time - vm.warp(block.timestamp + 20 hours); - - address newLender = makeAddr("new lender"); - - vm.prank(lender); - deployment.loanToken.transferFrom(lender, newLender, loanId); - - uint256 originalBalance = IERC20(USDC).balanceOf(address(deployment.simpleLoan)); - - // Repay loan - vm.prank(borrower); - deployment.simpleLoan.repayLOAN({ - loanId: loanId, - permitData: "" - }); - - // LOAN token owner is not original lender -> repay funds to the Vault - assertEq(IERC20(USDC).balanceOf(address(deployment.simpleLoan)), originalBalance + 100e6); - assertEq(deployment.loanToken.ownerOf(loanId), newLender); - } - -} From 9baabd614f5bd192e11696df7406b33170653ad5 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Sun, 14 Apr 2024 12:19:14 -0400 Subject: [PATCH 094/129] test: fix proposal fuzz test --- test/unit/PWNSimpleLoanProposal.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/PWNSimpleLoanProposal.t.sol b/test/unit/PWNSimpleLoanProposal.t.sol index 8295483..2f6a9bc 100644 --- a/test/unit/PWNSimpleLoanProposal.t.sol +++ b/test/unit/PWNSimpleLoanProposal.t.sol @@ -431,7 +431,7 @@ abstract contract PWNSimpleLoanProposal_AcceptProposal_Test is PWNSimpleLoanProp function testFuzz_shouldFail_whenCallerIsNotAllowedAcceptor(address caller) external { address allowedAcceptor = makeAddr("allowedAcceptor"); - vm.assume(caller != allowedAcceptor); + vm.assume(caller != allowedAcceptor && caller != proposer); params.base.allowedAcceptor = allowedAcceptor; params.acceptor = caller; params.signature = _sign(proposerPK, _getProposalHashWith()); From 72c09bec5e6c20ae05f4eaa583dcf104cfc78643 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Mon, 15 Apr 2024 15:57:58 -0400 Subject: [PATCH 095/129] feat: withdraw funds to lender address when pool source of funds --- src/interfaces/IPoolAdapter.sol | 3 +- src/loan/terms/simple/loan/PWNSimpleLoan.sol | 56 ++-- src/loan/vault/PWNVault.sol | 37 ++- test/fork/UseCases.fork.t.sol | 76 ++++++ test/helper/DummyPoolAdapter.sol | 4 +- test/unit/PWNSimpleLoan.t.sol | 270 ++++++------------- test/unit/PWNVault.t.sol | 129 ++++++++- 7 files changed, 342 insertions(+), 233 deletions(-) diff --git a/src/interfaces/IPoolAdapter.sol b/src/interfaces/IPoolAdapter.sol index cb44ac2..34989b9 100644 --- a/src/interfaces/IPoolAdapter.sol +++ b/src/interfaces/IPoolAdapter.sol @@ -9,8 +9,7 @@ interface IPoolAdapter { /** * @notice Withdraw an asset from the pool on behalf of the owner. - * @dev Adapter will withdraw and transfer the asset to the caller. - * Caller must have the ACTIVE_LOAN tag in the hub. + * @dev Withdrawn asset remains in the owner. Caller must have the ACTIVE_LOAN tag in the hub. * @param pool The address of the pool from which the asset is withdrawn. * @param owner The address of the owner from whom the asset is withdrawn. * @param asset The address of the asset to withdraw. diff --git a/src/loan/terms/simple/loan/PWNSimpleLoan.sol b/src/loan/terms/simple/loan/PWNSimpleLoan.sol index 989668d..8ddbb5e 100644 --- a/src/loan/terms/simple/loan/PWNSimpleLoan.sol +++ b/src/loan/terms/simple/loan/PWNSimpleLoan.sol @@ -469,15 +469,10 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { // Transfer collateral to Vault _pull(loanTerms.collateral, loanTerms.borrower); - // Decide credit provider - address creditProvider = loanTerms.lender; + // Lender is not the source of funds if (lenderSpec.sourceOfFunds != loanTerms.lender) { - - // Note: Lender is not the source of funds. Withdraw credit asset to the Vault and use it - // as a credit provider to minimize the number of withdrawals. - - _pullCreditFromPool(loanTerms.credit, loanTerms, lenderSpec); - creditProvider = address(this); + // Withdraw credit asset to the lender first + _withdrawCreditFromPool(loanTerms.credit, loanTerms, lenderSpec); } // Calculate fee amount and new loan amount @@ -490,12 +485,12 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { // Collect fees if (feeAmount > 0) { creditHelper.amount = feeAmount; - _pushFrom(creditHelper, creditProvider, config.feeCollector()); + _pushFrom(creditHelper, loanTerms.lender, config.feeCollector()); } // Transfer credit to borrower creditHelper.amount = newLoanAmount; - _pushFrom(creditHelper, creditProvider, loanTerms.borrower); + _pushFrom(creditHelper, loanTerms.lender, loanTerms.borrower); } /** @@ -535,43 +530,42 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { // Note: `creditHelper` must not be used before updating the amount. MultiToken.Asset memory creditHelper = loanTerms.credit; - // Decide credit provider - address creditProvider = loanTerms.lender; + // Lender is not the source of funds if (lenderSpec.sourceOfFunds != loanTerms.lender) { - - // Note: Lender is not the source of funds. Withdraw credit asset to the Vault and use it - // as a credit provider to minimize the number of withdrawals. - + // Withdraw credit asset to the lender first creditHelper.amount = feeAmount + (shouldTransferCommon ? common : 0) + surplus; - _pullCreditFromPool(creditHelper, loanTerms, lenderSpec); - creditProvider = address(this); + _withdrawCreditFromPool(creditHelper, loanTerms, lenderSpec); } // Collect fees if (feeAmount > 0) { creditHelper.amount = feeAmount; - _pushFrom(creditHelper, creditProvider, config.feeCollector()); + _pushFrom(creditHelper, loanTerms.lender, config.feeCollector()); } // Transfer common amount to the Vault if necessary - // Note: If the `creditProvider` is a vault, the common amount is already in the vault. - if (shouldTransferCommon && creditProvider != address(this)) { + if (shouldTransferCommon) { creditHelper.amount = common; - _pull(creditHelper, creditProvider); + _pull(creditHelper, loanTerms.lender); } // Handle the surplus or the shortage if (surplus > 0) { // New loan covers the whole original loan, transfer surplus to the borrower creditHelper.amount = surplus; - _pushFrom(creditHelper, creditProvider, loanTerms.borrower); + _pushFrom(creditHelper, loanTerms.lender, loanTerms.borrower); } else if (shortage > 0) { // New loan covers only part of the original loan, borrower needs to contribute creditHelper.amount = shortage; _pull(creditHelper, loanTerms.borrower); } - try this.tryClaimRepaidLOANForLoanOwner(refinancingLoanId, loanOwner) {} catch { + // Try to repay directly + try this.tryClaimRepaidLOAN({ + loanId: refinancingLoanId, + creditAmount: (shouldTransferCommon ? common : 0) + shortage, + loanOwner: loanOwner + }) {} catch { // Note: Safe transfer or supply to a pool can fail. In that case the LOAN token stays in repaid state and // waits for the LOAN token owner to claim the repaid credit. Otherwise lender would be able to prevent // anybody from repaying the loan. @@ -579,13 +573,13 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { } /** - * @notice Pull a credit asset from a pool to the Vault. + * @notice Withdraw a credit asset from a pool to the Vault. * @dev The function will revert if pool doesn't have registered pool adapter. * @param credit Asset to be pulled from the pool. * @param loanTerms Loan terms struct. * @param lenderSpec Lender specification struct. */ - function _pullCreditFromPool( + function _withdrawCreditFromPool( MultiToken.Asset memory credit, Terms memory loanTerms, LenderSpec calldata lenderSpec @@ -635,13 +629,14 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { } // Transfer the repaid credit to the Vault - _pull(loan.creditAddress.ERC20(_loanRepaymentAmount(loanId)), msg.sender); + uint256 repaymentAmount = _loanRepaymentAmount(loanId); + _pull(loan.creditAddress.ERC20(repaymentAmount), msg.sender); // Transfer collateral back to borrower _push(loan.collateral, loan.borrower); // Try to repay directly - try this.tryClaimRepaidLOANForLoanOwner(loanId, loanToken.ownerOf(loanId)) {} catch { + try this.tryClaimRepaidLOAN(loanId, repaymentAmount, loanToken.ownerOf(loanId)) {} catch { // Note: Safe transfer or supply to a pool can fail. In that case leave the LOAN token in repaid state and // wait for the LOAN token owner to claim the repaid credit. Otherwise lender would be able to prevent // borrower from repaying the loan. @@ -769,9 +764,10 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { * and the LOAN token owner will be able to claim the repaid credit. Otherwise lender would be able to prevent * borrower from repaying the loan. * @param loanId Id of a loan that is being claimed. + * @param creditAmount Amount of a credit to be claimed. * @param loanOwner Address of the LOAN token holder. */ - function tryClaimRepaidLOANForLoanOwner(uint256 loanId, address loanOwner) external { + function tryClaimRepaidLOAN(uint256 loanId, uint256 creditAmount, address loanOwner) external { if (msg.sender != address(this)) revert CallerNotVault(); @@ -787,7 +783,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { // Note: The loan owner is the original lender at this point. address destinationOfFunds = loan.originalSourceOfFunds; - MultiToken.Asset memory repaymentCredit = loan.creditAddress.ERC20(_loanRepaymentAmount(loanId)); + MultiToken.Asset memory repaymentCredit = loan.creditAddress.ERC20(creditAmount); // Delete loan data & burn LOAN token before calling safe transfer _deleteLoan(loanId); diff --git a/src/loan/vault/PWNVault.sol b/src/loan/vault/PWNVault.sol index 3f2e201..1c5a67f 100644 --- a/src/loan/vault/PWNVault.sol +++ b/src/loan/vault/PWNVault.sol @@ -39,6 +39,16 @@ abstract contract PWNVault is IERC721Receiver, IERC1155Receiver { */ event VaultPushFrom(MultiToken.Asset asset, address indexed origin, address indexed beneficiary); + /** + * @dev Emitted when asset is withdrawn from a pool to an `owner` address. + */ + event PoolWithdraw(MultiToken.Asset asset, address indexed poolAdapter, address indexed pool, address indexed owner); + + /** + * @dev Emitted when asset is supplied to a pool from a vault. + */ + event PoolSupply(MultiToken.Asset asset, address indexed poolAdapter, address indexed pool, address indexed owner); + /*----------------------------------------------------------*| |* # TRANSFER FUNCTIONS *| @@ -91,23 +101,26 @@ abstract contract PWNVault is IERC721Receiver, IERC1155Receiver { } /** - * @notice Function withdrawing an asset from a Compound pool to a vault. + * @notice Function withdrawing an asset from a Compound pool to the owner. * @dev The function assumes a prior check for a valid pool address. * @param asset An asset construct - for a definition see { MultiToken dependency lib }. * @param poolAdapter An address of a pool adapter. * @param pool An address of a pool. - * @param owner An address on which behalf the asset is withdrawn. + * @param owner An address on which behalf the assets are withdrawn. */ function _withdrawFromPool(MultiToken.Asset memory asset, IPoolAdapter poolAdapter, address pool, address owner) internal { - uint256 originalBalance = asset.balanceOf(address(this)); + uint256 originalBalance = asset.balanceOf(owner); poolAdapter.withdraw(pool, owner, asset.assetAddress, asset.amount); - _checkTransfer(asset, originalBalance, address(this), true); + _checkTransfer(asset, originalBalance, owner, true); + + emit PoolWithdraw(asset, address(poolAdapter), pool, owner); } /** * @notice Function supplying an asset to a pool from a vault via a pool adapter. * @dev The function assumes a prior check for a valid pool address. + * Assuming pool will revert supply transaction if it fails. * @param asset An asset construct - for a definition see { MultiToken dependency lib }. * @param poolAdapter An address of a pool adapter. * @param pool An address of a pool. @@ -121,6 +134,8 @@ abstract contract PWNVault is IERC721Receiver, IERC1155Receiver { _checkTransfer(asset, originalBalance, address(this), false); // Note: Assuming pool will revert supply transaction if it fails. + + emit PoolSupply(asset, address(poolAdapter), pool, owner); } function _checkTransfer( @@ -129,14 +144,12 @@ abstract contract PWNVault is IERC721Receiver, IERC1155Receiver { address checkedAddress, bool checkIncreasingBalance ) private view { - if (checkIncreasingBalance) { - if (originalBalance + asset.getTransferAmount() != asset.balanceOf(checkedAddress)) { - revert IncompleteTransfer(); - } - } else { - if (originalBalance - asset.getTransferAmount() != asset.balanceOf(checkedAddress)) { - revert IncompleteTransfer(); - } + uint256 expectedBalance = checkIncreasingBalance + ? originalBalance + asset.getTransferAmount() + : originalBalance - asset.getTransferAmount(); + + if (expectedBalance != asset.balanceOf(checkedAddress)) { + revert IncompleteTransfer(); } } diff --git a/test/fork/UseCases.fork.t.sol b/test/fork/UseCases.fork.t.sol index 84c8823..b3c8513 100644 --- a/test/fork/UseCases.fork.t.sol +++ b/test/fork/UseCases.fork.t.sol @@ -374,3 +374,79 @@ contract CategoryRegistryForIncompleteERCTokensTest is UseCasesTest { } } + + +contract RefinacningTest is UseCasesTest { + + function testUseCase_shouldRefinanceRunningLoan() external { + proposal.creditAmount = 10 ether; + proposal.fixedInterestAmount = 1 ether; + proposal.availableCreditLimit = 20 ether; + proposal.duration = 5 days; + + // Make proposal + vm.prank(lender); + deployment.simpleLoanSimpleProposal.makeProposal(proposal); + + bytes memory proposalData = deployment.simpleLoanSimpleProposal.encodeProposalData(proposal); + + // Create a loan + vm.prank(borrower); + uint256 loanId = deployment.simpleLoan.createLOAN({ + proposalSpec: PWNSimpleLoan.ProposalSpec({ + proposalContract: address(deployment.simpleLoanSimpleProposal), + proposalData: proposalData, + proposalInclusionProof: new bytes32[](0), + signature: "" + }), + lenderSpec: PWNSimpleLoan.LenderSpec({ + sourceOfFunds: lender + }), + callerSpec: PWNSimpleLoan.CallerSpec({ + refinancingLoanId: 0, + revokeNonce: false, + nonce: 0, + permitData: "" + }), + extra: "" + }); + + // Check balance + assertEq(credit.balanceOf(lender), 90 ether); // -10 credit + assertEq(credit.balanceOf(borrower), 100 ether); // -10 coll, +10 credit + assertEq(credit.balanceOf(address(deployment.simpleLoan)), 10 ether); // +10 coll + + vm.warp(block.timestamp + 4 days); + + vm.expectCall( + address(credit), + abi.encodeWithSelector(credit.transferFrom.selector, borrower, address(deployment.simpleLoan), 1 ether) + ); + + vm.prank(borrower); + deployment.simpleLoan.createLOAN({ + proposalSpec: PWNSimpleLoan.ProposalSpec({ + proposalContract: address(deployment.simpleLoanSimpleProposal), + proposalData: proposalData, + proposalInclusionProof: new bytes32[](0), + signature: "" + }), + lenderSpec: PWNSimpleLoan.LenderSpec({ + sourceOfFunds: lender + }), + callerSpec: PWNSimpleLoan.CallerSpec({ + refinancingLoanId: loanId, + revokeNonce: false, + nonce: 0, + permitData: "" + }), + extra: "" + }); + + // Check balance + assertEq(credit.balanceOf(lender), 91 ether); // -10 credit, +1 refinance + assertEq(credit.balanceOf(borrower), 99 ether); // -10 coll, +10 credit, -1 refinance + assertEq(credit.balanceOf(address(deployment.simpleLoan)), 10 ether); // +10 coll + } + +} diff --git a/test/helper/DummyPoolAdapter.sol b/test/helper/DummyPoolAdapter.sol index 900ebb7..d8c72e7 100644 --- a/test/helper/DummyPoolAdapter.sol +++ b/test/helper/DummyPoolAdapter.sol @@ -8,8 +8,8 @@ import { IPoolAdapter } from "src/interfaces/IPoolAdapter.sol"; contract DummyPoolAdapter is IPoolAdapter { - function withdraw(address pool, address /* owner */, address asset, uint256 amount) external { - IERC20(asset).transferFrom(pool, msg.sender, amount); + function withdraw(address pool, address owner, address asset, uint256 amount) external { + IERC20(asset).transferFrom(pool, owner, amount); } function supply(address pool, address /* owner */, address asset, uint256 amount) external { diff --git a/test/unit/PWNSimpleLoan.t.sol b/test/unit/PWNSimpleLoan.t.sol index 9122c72..023e152 100644 --- a/test/unit/PWNSimpleLoan.t.sol +++ b/test/unit/PWNSimpleLoan.t.sol @@ -616,41 +616,6 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { }); } - function testFuzz_shouldTransferCredit_fromSourceOfFunds_toBorrowerAndFeeCollector_whenDirectSourceOfFunds( - uint256 fee, uint256 loanAmount - ) external { - fee = bound(fee, 0, 9999); - loanAmount = bound(loanAmount, 1, 1e40); - - 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); - uint256 newAmount = loanAmount - feeAmount; - - // Fee transfer - vm.expectCall({ - callee: simpleLoanTerms.credit.assetAddress, - data: abi.encodeWithSignature("transferFrom(address,address,uint256)", lender, feeCollector, feeAmount), - count: feeAmount > 0 ? 1 : 0 - }); - // Updated amount transfer - vm.expectCall( - simpleLoanTerms.credit.assetAddress, - abi.encodeWithSignature("transferFrom(address,address,uint256)", lender, borrower, newAmount) - ); - - loan.createLOAN({ - proposalSpec: proposalSpec, - lenderSpec: lenderSpec, - callerSpec: callerSpec, - extra: "" - }); - } - function test_shouldFail_whenPoolAdapterNotRegistered_whenPoolSourceOfFunds() external { lenderSpec.sourceOfFunds = sourceOfFunds; simpleLoanTerms.lenderSpecHash = loan.getLenderSpecHash(lenderSpec); @@ -667,19 +632,15 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { }); } - function testFuzz_shouldWithdrawCredit_fromSourceOfFunds_toVault_whenPoolSourceOfFunds( - uint256 fee, uint256 loanAmount - ) external { - fee = bound(fee, 0, 9999); + function testFuzz_shouldCallWithdraw_whenPoolSourceOfFunds(uint256 loanAmount) external { loanAmount = bound(loanAmount, 1, 1e40); lenderSpec.sourceOfFunds = sourceOfFunds; simpleLoanTerms.lenderSpecHash = loan.getLenderSpecHash(lenderSpec); simpleLoanTerms.credit.amount = loanAmount; - fungibleAsset.mint(sourceOfFunds, loanAmount); - _mockLoanTerms(simpleLoanTerms); - vm.mockCall(config, abi.encodeWithSignature("fee()"), abi.encode(fee)); + + fungibleAsset.mint(sourceOfFunds, loanAmount); vm.expectCall( poolAdapter, @@ -697,16 +658,14 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { }); } - function testFuzz_shouldTransferCredit_fromVault_toBorrowerAndFeeCollector_whenPoolSourceOfFunds( + function testFuzz_shouldTransferCredit_toBorrowerAndFeeCollector( uint256 fee, uint256 loanAmount ) external { fee = bound(fee, 0, 9999); loanAmount = bound(loanAmount, 1, 1e40); - lenderSpec.sourceOfFunds = sourceOfFunds; - simpleLoanTerms.lenderSpecHash = loan.getLenderSpecHash(lenderSpec); simpleLoanTerms.credit.amount = loanAmount; - fungibleAsset.mint(sourceOfFunds, loanAmount); + fungibleAsset.mint(lender, loanAmount); _mockLoanTerms(simpleLoanTerms); vm.mockCall(config, abi.encodeWithSignature("fee()"), abi.encode(fee)); @@ -717,13 +676,13 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { // Fee transfer vm.expectCall({ callee: simpleLoanTerms.credit.assetAddress, - data: abi.encodeWithSignature("transfer(address,uint256)", feeCollector, feeAmount), + data: abi.encodeWithSignature("transferFrom(address,address,uint256)", lender, feeCollector, feeAmount), count: feeAmount > 0 ? 1 : 0 }); // Updated amount transfer vm.expectCall( simpleLoanTerms.credit.assetAddress, - abi.encodeWithSignature("transfer(address,uint256)", borrower, newAmount) + abi.encodeWithSignature("transferFrom(address,address,uint256)", lender, borrower, newAmount) ); loan.createLOAN({ @@ -1023,7 +982,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { _assertLOANEq(refinancingLoanId, simpleLoan); } - function test_shouldUpdateLoanData_whenLOANOwnerIsOriginalLender_whenDirectTransferFails() external { + function test_shouldUpdateLoanData_whenLOANOwnerIsOriginalLender_whenDirectRepaymentFails() external { _mockLOANTokenOwner(refinancingLoanId, lender); vm.mockCallRevert(simpleLoan.creditAddress, abi.encodeWithSignature("transfer(address,uint256)"), ""); @@ -1060,7 +1019,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { }); } - function test_shouldWithdrawFullCreditAmountToVault_whenShouldTransferCommon_whenPoolSourceOfFunds() external { + function test_shouldWithdrawFullCreditAmount_whenShouldTransferCommon_whenPoolSourceOfFunds() external { lenderSpec.sourceOfFunds = sourceOfFunds; refinancedLoanTerms.lenderSpecHash = loan.getLenderSpecHash(lenderSpec); _mockLoanTerms(refinancedLoanTerms); @@ -1081,7 +1040,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { }); } - function test_shouldWithdrawCreditWithoutCommonToVault_whenShouldNotTransferCommon_whenPoolSourceOfFunds() external { + function test_shouldWithdrawCreditWithoutCommon_whenShouldNotTransferCommon_whenPoolSourceOfFunds() external { lenderSpec.sourceOfFunds = sourceOfFunds; refinancedLoanTerms.lender = newLender; refinancedLoanTerms.lenderSpecHash = loan.getLenderSpecHash(lenderSpec); @@ -1111,7 +1070,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { }); } - function test_shouldNotWithdrawCreditToVault_whenShouldNotTransferCommon_whenNoSurplus_whenNoFee_whenPoolSourceOfFunds() external { + function test_shouldNotWithdrawCredit_whenShouldNotTransferCommon_whenNoSurplus_whenNoFee_whenPoolSourceOfFunds() external { lenderSpec.sourceOfFunds = sourceOfFunds; refinancedLoanTerms.lender = newLender; refinancedLoanTerms.lenderSpecHash = loan.getLenderSpecHash(lenderSpec); @@ -1135,7 +1094,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { // Fee - function testFuzz_shouldTransferFeeToCollector_fromLender_whenDirectSourceOfFunds(uint256 fee) external { + function testFuzz_shouldTransferFeeToCollector(uint256 fee) external { fee = bound(fee, 1, 9999); // 0.01 - 99.99% uint256 feeAmount = Math.mulDiv(refinancedLoanTerms.credit.amount, fee, 1e4); @@ -1155,33 +1114,9 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { }); } - function testFuzz_shouldTransferFeeToCollector_fromVault_whenPoolSourceOfFunds(uint256 fee) external { - fee = bound(fee, 1, 9999); // 0.01 - 99.99% - - lenderSpec.sourceOfFunds = sourceOfFunds; - refinancedLoanTerms.lenderSpecHash = loan.getLenderSpecHash(lenderSpec); - _mockLoanTerms(refinancedLoanTerms); - - uint256 feeAmount = Math.mulDiv(refinancedLoanTerms.credit.amount, fee, 1e4); - - vm.mockCall(config, abi.encodeWithSignature("fee()"), abi.encode(fee)); - - vm.expectCall( - refinancedLoanTerms.credit.assetAddress, - abi.encodeWithSignature("transfer(address,uint256)", feeCollector, feeAmount) - ); - - loan.createLOAN({ - proposalSpec: proposalSpec, - lenderSpec: lenderSpec, - callerSpec: callerSpec, - extra: "" - }); - } - - // Transfer of common - should + // Transfer of common - function test_shouldTransferCommonToVaul_whenLenderNotLoanOwner_whenDirectSourceOfFunds() external { + function test_shouldTransferCommonToVaul_whenLenderNotLoanOwner() external { lenderSpec.sourceOfFunds = newLender; refinancedLoanTerms.lender = newLender; refinancedLoanTerms.lenderSpecHash = loan.getLenderSpecHash(lenderSpec); @@ -1206,7 +1141,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { }); } - function testFuzz_shouldTransferCommonToVaul_whenLenderOriginalLender_whenDifferentSourceOfFunds_whenDirectSourceOfFunds() external { + function testFuzz_shouldTransferCommonToVaul_whenLenderOriginalLender_whenDifferentSourceOfFunds() external { lenderSpec.sourceOfFunds = newLender; refinancedLoanTerms.lender = newLender; refinancedLoanTerms.lenderSpecHash = loan.getLenderSpecHash(lenderSpec); @@ -1234,62 +1169,14 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { }); } - // Transfer of common - should not - - function test_shouldNotTransferCommonToVaul_whenLenderNotLoanOwner_whenPoolSourceOfFunds() external { - lenderSpec.sourceOfFunds = sourceOfFunds; - refinancedLoanTerms.lender = newLender; - refinancedLoanTerms.lenderSpecHash = loan.getLenderSpecHash(lenderSpec); - _mockLoanTerms(refinancedLoanTerms); - _mockLOANTokenOwner(refinancingLoanId, makeAddr("loanOwner")); - - vm.expectCall({ - callee: refinancedLoanTerms.credit.assetAddress, - data: abi.encodeWithSignature("transferFrom(address,address,uint256)", address(loan), address(loan)), - count: 0 - }); - - loan.createLOAN({ - proposalSpec: proposalSpec, - lenderSpec: lenderSpec, - callerSpec: callerSpec, - extra: "" - }); - } - - function test_shouldNotTransferCommonToVaul_whenLenderOriginalLender_whenDifferentSourceOfFunds_whenPoolSourceOfFunds() external { - lenderSpec.sourceOfFunds = sourceOfFunds; - refinancedLoanTerms.lender = newLender; - refinancedLoanTerms.lenderSpecHash = loan.getLenderSpecHash(lenderSpec); - _mockLoanTerms(refinancedLoanTerms); - simpleLoan.originalLender = newLender; - simpleLoan.originalSourceOfFunds = newLender; - _mockLOAN(refinancingLoanId, simpleLoan); - _mockLOANTokenOwner(refinancingLoanId, newLender); - - vm.expectCall({ - callee: refinancedLoanTerms.credit.assetAddress, - data: abi.encodeWithSignature("transferFrom(address,address,uint256)", address(loan), address(loan)), - count: 0 - }); - - loan.createLOAN({ - proposalSpec: proposalSpec, - lenderSpec: lenderSpec, - callerSpec: callerSpec, - extra: "" - }); - - } - /// forge-config: default.fuzz.runs = 2 - function testFuzz_shouldNoTransferCommonToVaul_whenLenderLoanOwner_whenLenderOriginalLender_whenSameSourceOfFunds(bool flag) external { - lenderSpec.sourceOfFunds = flag ? newLender : sourceOfFunds; + function testFuzz_shouldNotTransferCommonToVaul_whenLenderLoanOwner_whenLenderOriginalLender_whenSameSourceOfFunds(bool sourceOfFundsflag) external { + lenderSpec.sourceOfFunds = sourceOfFundsflag ? newLender : sourceOfFunds; refinancedLoanTerms.lender = newLender; refinancedLoanTerms.lenderSpecHash = loan.getLenderSpecHash(lenderSpec); _mockLoanTerms(refinancedLoanTerms); simpleLoan.originalLender = newLender; - simpleLoan.originalSourceOfFunds = flag ? newLender : sourceOfFunds; + simpleLoan.originalSourceOfFunds = sourceOfFundsflag ? newLender : sourceOfFunds; _mockLOAN(refinancingLoanId, simpleLoan); _mockLOANTokenOwner(refinancingLoanId, newLender); @@ -1309,7 +1196,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { // Surplus - function test_shouldTransferSurplusToBorrower_fromNewLender_whenDirectSourceOfFunds() external { + function test_shouldTransferSurplusToBorrower() external { lenderSpec.sourceOfFunds = newLender; refinancedLoanTerms.lender = newLender; refinancedLoanTerms.lenderSpecHash = loan.getLenderSpecHash(lenderSpec); @@ -1330,39 +1217,15 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { }); } - function test_shouldTransferSurplusToBorrower_fromVault_whenPoolSourceOfFunds() external { - lenderSpec.sourceOfFunds = sourceOfFunds; - refinancedLoanTerms.lender = newLender; - refinancedLoanTerms.lenderSpecHash = loan.getLenderSpecHash(lenderSpec); - _mockLoanTerms(refinancedLoanTerms); - - uint256 surplus = refinancedLoanTerms.credit.amount - loan.loanRepaymentAmount(refinancingLoanId); - - vm.expectCall( - refinancedLoanTerms.credit.assetAddress, - abi.encodeWithSignature("transfer(address,uint256)", borrower, surplus) - ); - - loan.createLOAN({ - proposalSpec: proposalSpec, - lenderSpec: lenderSpec, - callerSpec: callerSpec, - extra: "" - }); - } - - /// forge-config: default.fuzz.runs = 2 - function testFuzz_shouldNotTransferSurplusToBorrower_whenNoSurplus(bool flag) external { - lenderSpec.sourceOfFunds = flag ? newLender : sourceOfFunds; + function test_shouldNotTransferSurplusToBorrower_whenNoSurplus() external { + lenderSpec.sourceOfFunds = newLender; refinancedLoanTerms.lender = newLender; refinancedLoanTerms.lenderSpecHash = loan.getLenderSpecHash(lenderSpec); _mockLoanTerms(refinancedLoanTerms); vm.expectCall({ callee: refinancedLoanTerms.credit.assetAddress, - data: flag - ? abi.encodeWithSignature("transferFrom(address,address,uint256)", newLender, borrower, 0) - : abi.encodeWithSignature("transfer(address,uint256)", borrower, 0), + data: abi.encodeWithSignature("transferFrom(address,address,uint256)", newLender, borrower, 0), count: 0 }); @@ -1413,13 +1276,45 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { }); } - function testFuzz_shouldCall_tryClaimRepaidLOANForLoanOwner(address loanOwner) external { + // Try claim repaid LOAN + + function testFuzz_shouldTryClaimRepaidLOAN_fullAmount_whenShouldTransferCommon(address loanOwner) external { vm.assume(loanOwner != address(0)); _mockLOANTokenOwner(refinancingLoanId, loanOwner); vm.expectCall( address(loan), - abi.encodeWithSignature("tryClaimRepaidLOANForLoanOwner(uint256,address)", refinancingLoanId, loanOwner) + abi.encodeWithSignature( + "tryClaimRepaidLOAN(uint256,uint256,address)", + refinancingLoanId, loan.loanRepaymentAmount(refinancingLoanId), loanOwner + ) + ); + + loan.createLOAN({ + proposalSpec: proposalSpec, + lenderSpec: lenderSpec, + callerSpec: callerSpec, + extra: "" + }); + } + + function testFuzz_shouldTryClaimRepaidLOAN_shortageAmount_whenShouldNotTransferCommon(uint256 shortage) external { + simpleLoan.principalAmount = refinancedLoanTerms.credit.amount + 1; + _mockLOAN(refinancingLoanId, simpleLoan); + + uint256 repaymentAmount = loan.loanRepaymentAmount(refinancingLoanId); + shortage = bound(shortage, 1, repaymentAmount - 1); + + fungibleAsset.mint(borrower, shortage); + + refinancedLoanTerms.credit.amount = repaymentAmount - shortage; + _mockLoanTerms(refinancedLoanTerms); + + vm.expectCall( + address(loan), + abi.encodeWithSignature( + "tryClaimRepaidLOAN(uint256,uint256,address)", refinancingLoanId, shortage, lender + ) ); loan.createLOAN({ @@ -1430,10 +1325,10 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { }); } - function test_shouldNotFail_whenTryClaimRepaidLOANForLoanOwnerFails() external { + function test_shouldNotFail_whenTryClaimRepaidLOANFails() external { vm.mockCallRevert( address(loan), - abi.encodeWithSignature("tryClaimRepaidLOANForLoanOwner(uint256,address)", refinancingLoanId, lender), + abi.encodeWithSignature("tryClaimRepaidLOAN(uint256,uint256,address)"), "" ); @@ -1774,22 +1669,24 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { loan.repayLOAN(loanId, ""); } - function testFuzz_shouldCall_tryClaimRepaidLOANForLoanOwner(address loanOwner) external { + function testFuzz_shouldCall_tryClaimRepaidLOAN(address loanOwner) external { vm.assume(loanOwner != address(0)); _mockLOANTokenOwner(loanId, loanOwner); vm.expectCall( address(loan), - abi.encodeWithSignature("tryClaimRepaidLOANForLoanOwner(uint256,address)", loanId, loanOwner) + abi.encodeWithSignature( + "tryClaimRepaidLOAN(uint256,uint256,address)", loanId, loan.loanRepaymentAmount(loanId), loanOwner + ) ); loan.repayLOAN(loanId, ""); } - function test_shouldNotFail_whenTryClaimRepaidLOANForLoanOwnerFails() external { + function test_shouldNotFail_whenTryClaimRepaidLOANFails() external { vm.mockCallRevert( address(loan), - abi.encodeWithSignature("tryClaimRepaidLOANForLoanOwner(uint256,address)", loanId, lender), + abi.encodeWithSignature("tryClaimRepaidLOAN(uint256,uint256,address)"), "" ); @@ -2006,10 +1903,12 @@ contract PWNSimpleLoan_ClaimLOAN_Test is PWNSimpleLoanTest { /*----------------------------------------------------------*| -|* # TRY CLAIM REPAID LOAN FOR LOAN OWNER *| +|* # TRY CLAIM REPAID LOAN *| |*----------------------------------------------------------*/ -contract PWNSimpleLoan_TryClaimRepaidLOANForLoanOwner_Test is PWNSimpleLoanTest { +contract PWNSimpleLoan_TryClaimRepaidLOAN_Test is PWNSimpleLoanTest { + + uint256 public creditAmount; function setUp() override public { super.setUp(); @@ -2020,6 +1919,8 @@ contract PWNSimpleLoan_TryClaimRepaidLOANForLoanOwner_Test is PWNSimpleLoanTest // Move collateral to vault vm.prank(borrower); nonFungibleAsset.transferFrom(borrower, address(loan), 2); + + creditAmount = 100; } @@ -2028,7 +1929,7 @@ contract PWNSimpleLoan_TryClaimRepaidLOANForLoanOwner_Test is PWNSimpleLoanTest vm.expectRevert(abi.encodeWithSelector(CallerNotVault.selector)); vm.prank(caller); - loan.tryClaimRepaidLOANForLoanOwner(loanId, lender); + loan.tryClaimRepaidLOAN(loanId, creditAmount, lender); } function testFuzz_shouldNotProceed_whenLoanNotInRepaidState(uint8 status) external { @@ -2044,7 +1945,7 @@ contract PWNSimpleLoan_TryClaimRepaidLOANForLoanOwner_Test is PWNSimpleLoanTest }); vm.prank(address(loan)); - loan.tryClaimRepaidLOANForLoanOwner(loanId, lender); + loan.tryClaimRepaidLOAN(loanId, creditAmount, lender); _assertLOANEq(loanId, simpleLoan); } @@ -2059,7 +1960,7 @@ contract PWNSimpleLoan_TryClaimRepaidLOANForLoanOwner_Test is PWNSimpleLoanTest }); vm.prank(address(loan)); - loan.tryClaimRepaidLOANForLoanOwner(loanId, loanOwner); + loan.tryClaimRepaidLOAN(loanId, creditAmount, loanOwner); _assertLOANEq(loanId, simpleLoan); } @@ -2068,12 +1969,12 @@ contract PWNSimpleLoan_TryClaimRepaidLOANForLoanOwner_Test is PWNSimpleLoanTest vm.expectCall(loanToken, abi.encodeWithSignature("burn(uint256)", loanId)); vm.prank(address(loan)); - loan.tryClaimRepaidLOANForLoanOwner(loanId, lender); + loan.tryClaimRepaidLOAN(loanId, creditAmount, lender); } function test_shouldDeleteLOANData() external { vm.prank(address(loan)); - loan.tryClaimRepaidLOANForLoanOwner(loanId, lender); + loan.tryClaimRepaidLOAN(loanId, creditAmount, lender); _assertLOANEq(loanId, nonExistingLoan); } @@ -2084,11 +1985,11 @@ contract PWNSimpleLoan_TryClaimRepaidLOANForLoanOwner_Test is PWNSimpleLoanTest vm.expectCall( simpleLoan.creditAddress, - abi.encodeWithSignature("transfer(address,uint256)", lender, loan.loanRepaymentAmount(loanId)) + abi.encodeWithSignature("transfer(address,uint256)", lender, creditAmount) ); vm.prank(address(loan)); - loan.tryClaimRepaidLOANForLoanOwner(loanId, lender); + loan.tryClaimRepaidLOAN(loanId, creditAmount, lender); } function test_shouldFail_whenPoolAdapterNotRegistered_whenSourceOfFundsNotEqualToOriginalLender() external { @@ -2101,7 +2002,7 @@ contract PWNSimpleLoan_TryClaimRepaidLOANForLoanOwner_Test is PWNSimpleLoanTest vm.expectRevert(abi.encodeWithSelector(InvalidSourceOfFunds.selector, sourceOfFunds)); vm.prank(address(loan)); - loan.tryClaimRepaidLOANForLoanOwner(loanId, lender); + loan.tryClaimRepaidLOAN(loanId, creditAmount, lender); } function test_shouldTransferAmountToPoolAdapter_whenSourceOfFundsNotEqualToOriginalLender() external { @@ -2110,11 +2011,11 @@ contract PWNSimpleLoan_TryClaimRepaidLOANForLoanOwner_Test is PWNSimpleLoanTest vm.expectCall( simpleLoan.creditAddress, - abi.encodeWithSignature("transfer(address,uint256)", poolAdapter, loan.loanRepaymentAmount(loanId)) + abi.encodeWithSignature("transfer(address,uint256)", poolAdapter, creditAmount) ); vm.prank(address(loan)); - loan.tryClaimRepaidLOANForLoanOwner(loanId, lender); + loan.tryClaimRepaidLOAN(loanId, creditAmount, lender); } function test_shouldCallSupplyOnPoolAdapter_whenSourceOfFundsNotEqualToOriginalLender() external { @@ -2124,13 +2025,12 @@ contract PWNSimpleLoan_TryClaimRepaidLOANForLoanOwner_Test is PWNSimpleLoanTest vm.expectCall( poolAdapter, abi.encodeWithSignature( - "supply(address,address,address,uint256)", - sourceOfFunds, lender, simpleLoan.creditAddress, loan.loanRepaymentAmount(loanId) + "supply(address,address,address,uint256)", sourceOfFunds, lender, simpleLoan.creditAddress, creditAmount ) ); vm.prank(address(loan)); - loan.tryClaimRepaidLOANForLoanOwner(loanId, lender); + loan.tryClaimRepaidLOAN(loanId, creditAmount, lender); } function test_shouldFail_whenTransferFails_whenSourceOfFundsEqualToOriginalLender() external { @@ -2141,7 +2041,7 @@ contract PWNSimpleLoan_TryClaimRepaidLOANForLoanOwner_Test is PWNSimpleLoanTest vm.expectRevert(); vm.prank(address(loan)); - loan.tryClaimRepaidLOANForLoanOwner(loanId, lender); + loan.tryClaimRepaidLOAN(loanId, creditAmount, lender); } function test_shouldFail_whenTransferFails_whenSourceOfFundsNotEqualToOriginalLender() external { @@ -2152,7 +2052,7 @@ contract PWNSimpleLoan_TryClaimRepaidLOANForLoanOwner_Test is PWNSimpleLoanTest vm.expectRevert(); vm.prank(address(loan)); - loan.tryClaimRepaidLOANForLoanOwner(loanId, lender); + loan.tryClaimRepaidLOAN(loanId, creditAmount, lender); } function test_shouldEmit_LOANClaimed() external { @@ -2160,7 +2060,7 @@ contract PWNSimpleLoan_TryClaimRepaidLOANForLoanOwner_Test is PWNSimpleLoanTest emit LOANClaimed(loanId, false); vm.prank(address(loan)); - loan.tryClaimRepaidLOANForLoanOwner(loanId, lender); + loan.tryClaimRepaidLOAN(loanId, creditAmount, lender); } } diff --git a/test/unit/PWNVault.t.sol b/test/unit/PWNVault.t.sol index e063a46..c553ac8 100644 --- a/test/unit/PWNVault.t.sol +++ b/test/unit/PWNVault.t.sol @@ -5,14 +5,17 @@ import { Test } from "forge-std/Test.sol"; import { MultiToken, - PWNVault, IERC165, IERC721Receiver, IERC1155Receiver, + PWNVault, + IPoolAdapter, Permit } from "src/loan/vault/PWNVault.sol"; import "src/PWNErrors.sol"; +import { DummyPoolAdapter } from "test/helper/DummyPoolAdapter.sol"; +import { T20 } from "test/helper/T20.sol"; import { T721 } from "test/helper/T721.sol"; @@ -30,6 +33,14 @@ contract PWNVaultHarness is PWNVault { _pushFrom(asset, origin, beneficiary); } + function withdrawFromPool(MultiToken.Asset memory asset, IPoolAdapter poolAdapter, address pool, address owner) external { + _withdrawFromPool(asset, poolAdapter, pool, owner); + } + + function supplyToPool(MultiToken.Asset memory asset, IPoolAdapter poolAdapter, address pool, address owner) external { + _supplyToPool(asset, poolAdapter, pool, owner); + } + function exposed_tryPermit(Permit calldata permit) external { _tryPermit(permit); } @@ -43,12 +54,14 @@ abstract contract PWNVaultTest is Test { address alice = makeAddr("alice"); address bob = makeAddr("bob"); + T20 t20; T721 t721; event VaultPull(MultiToken.Asset asset, address indexed origin); event VaultPush(MultiToken.Asset asset, address indexed beneficiary); event VaultPushFrom(MultiToken.Asset asset, address indexed origin, address indexed beneficiary); - + event PoolWithdraw(MultiToken.Asset asset, address indexed poolAdapter, address indexed pool, address indexed owner); + event PoolSupply(MultiToken.Asset asset, address indexed poolAdapter, address indexed pool, address indexed owner); constructor() { vm.etch(token, bytes("data")); @@ -56,6 +69,7 @@ abstract contract PWNVaultTest is Test { function setUp() public virtual { vault = new PWNVaultHarness(); + t20 = new T20(); t721 = new T721(); } @@ -202,6 +216,117 @@ contract PWNVault_PushFrom_Test is PWNVaultTest { } +/*----------------------------------------------------------*| +|* # WITHDRAW FROM POOL *| +|*----------------------------------------------------------*/ + +contract PWNVault_WithdrawFromPool_Test is PWNVaultTest { + using MultiToken for address; + + IPoolAdapter poolAdapter = IPoolAdapter(new DummyPoolAdapter()); + address pool = makeAddr("pool"); + MultiToken.Asset asset; + + function setUp() override public { + super.setUp(); + + asset = address(t20).ERC20(42e18); + + t20.mint(pool, asset.amount); + vm.prank(pool); + t20.approve(address(poolAdapter), asset.amount); + } + + + function test_shouldCallWithdrawOnPoolAdapter() external { + vm.expectCall( + address(poolAdapter), + abi.encodeWithSelector(IPoolAdapter.withdraw.selector, pool, alice, asset.assetAddress, asset.amount) + ); + + vault.withdrawFromPool(asset, poolAdapter, pool, alice); + } + + function test_shouldFail_whenIncompleteTransaction() external { + vm.mockCall( + asset.assetAddress, + abi.encodeWithSignature("balanceOf(address)", alice), + abi.encode(asset.amount) + ); + + vm.expectRevert(abi.encodeWithSelector(IncompleteTransfer.selector)); + vault.withdrawFromPool(asset, poolAdapter, pool, alice); + } + + function test_shouldEmitEvent_PoolWithdraw() external { + vm.expectEmit(); + emit PoolWithdraw(asset, address(poolAdapter), pool, alice); + + vault.withdrawFromPool(asset, poolAdapter, pool, alice); + } + +} + + +/*----------------------------------------------------------*| +|* # SUPPLY TO POOL *| +|*----------------------------------------------------------*/ + +contract PWNVault_SupplyToPool_Test is PWNVaultTest { + using MultiToken for address; + + IPoolAdapter poolAdapter = IPoolAdapter(new DummyPoolAdapter()); + address pool = makeAddr("pool"); + MultiToken.Asset asset; + + function setUp() override public { + super.setUp(); + + asset = address(t20).ERC20(42e18); + + t20.mint(address(vault), asset.amount); + } + + + function test_shouldTransferAssetToPoolAdapter() external { + vm.expectCall( + asset.assetAddress, + abi.encodeWithSignature("transfer(address,uint256)", address(poolAdapter), asset.amount) + ); + + vault.supplyToPool(asset, poolAdapter, pool, alice); + } + + function test_shouldCallSupplyOnPoolAdapter() external { + vm.expectCall( + address(poolAdapter), + abi.encodeWithSelector(IPoolAdapter.supply.selector, pool, alice, asset.assetAddress, asset.amount) + ); + + vault.supplyToPool(asset, poolAdapter, pool, alice); + } + + function test_shouldFail_whenIncompleteTransaction() external { + vm.mockCall( + asset.assetAddress, + abi.encodeWithSignature("balanceOf(address)", address(vault)), + abi.encode(asset.amount) + ); + + vm.expectRevert(abi.encodeWithSelector(IncompleteTransfer.selector)); + vault.supplyToPool(asset, poolAdapter, pool, alice); + } + + function test_shouldEmitEvent_PoolSupply() external { + vm.expectEmit(); + emit PoolSupply(asset, address(poolAdapter), pool, alice); + + vault.supplyToPool(asset, poolAdapter, pool, alice); + } + +} + + /*----------------------------------------------------------*| |* # TRY PERMIT *| |*----------------------------------------------------------*/ From 16666a0a07514e3af1df235b876f69f35d4590b7 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Thu, 18 Apr 2024 14:11:47 -0400 Subject: [PATCH 096/129] style: remove unnecessary comment --- src/loan/terms/simple/loan/PWNSimpleLoan.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/src/loan/terms/simple/loan/PWNSimpleLoan.sol b/src/loan/terms/simple/loan/PWNSimpleLoan.sol index 8ddbb5e..4afccd5 100644 --- a/src/loan/terms/simple/loan/PWNSimpleLoan.sol +++ b/src/loan/terms/simple/loan/PWNSimpleLoan.sol @@ -612,7 +612,6 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { function repayLOAN( uint256 loanId, bytes calldata permitData - // Permit calldata permit ) external { LOAN storage loan = LOANs[loanId]; From be49bd01aa29e69f69bb2b0364df57e3d90d5095 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Thu, 18 Apr 2024 14:15:55 -0400 Subject: [PATCH 097/129] docs: remove deployed addresses from readme --- README.md | 59 +------------------------------------------------------ 1 file changed, 1 insertion(+), 58 deletions(-) diff --git a/README.md b/README.md index 9e50063..81f6c8f 100644 --- a/README.md +++ b/README.md @@ -5,64 +5,7 @@ Smart contracts enabling P2P loans using arbitrary collateral (supporting ERC20, For in-depth documentation about PWN contracts see [PWN Developer Docs](https://dev-docs.pwn.xyz/). ## Deployed addresses - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameAddressChain
PWNConfig0x03DeAfC9678ab25F059df59Be3B20875018e1d46Ethereum Polygon Arbitrum BSC Goerli
0x55e6A9F4183CFC01ecE8f2258FC13b93e1B6c140Optimism Base Cronos Mantle Sepolia
PWNHub0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5Ethereum Polygon Arbitrum Optimism Base Cronos Mantle BSC Goerli Sepolia
PWNLOAN0x4440C069272cC34b80C7B11bEE657D0349Ba9C23Ethereum Polygon Arbitrum Optimism Base Cronos Mantle BSC Goerli Sepolia
PWNRevokedNonce (offer)0xFFa73Eacce930BBd92a1Ef218400cBd1036c437eEthereum Polygon Arbitrum Optimism Base Cronos Mantle BSC Goerli Sepolia
PWNRevokedNonce (request)0x472361E75d28597b0a7F86146fbB4a86f173d10DEthereum Polygon Arbitrum Optimism Base Cronos Mantle BSC Goerli Sepolia
PWNSimpleLoan0x57c88D78f6D08b5c88b4A3b7BbB0C1AA34c3280AEthereum Polygon Arbitrum BSC Goerli
0x4188C513fd94B0458715287570c832d9560bc08aOptimism Base Cronos Mantle Sepolia
PWNSimpleLoanSimpleOffer0x5E551f09b8d1353075A1FF3B484Ee688aCAc02F6Ethereum Polygon Arbitrum Optimism Base Cronos Mantle BSC Goerli Sepolia
PWNSimpleLoanListOffer0xDA027058708961Be3676daEB68Fde1758B210065Ethereum Polygon Arbitrum Optimism Base Cronos Mantle BSC Goerli Sepolia
PWNSimpleLoanSimpleRequest0x9Cb87eC6448299aBc326F32d60E191Ef32Ab225DEthereum Polygon Arbitrum Optimism Base Cronos Mantle BSC Goerli Sepolia
Product TimelockController0x2cDf99aD1115Ea0E943E56dd26459E3e57788C12Ethereum Polygon Arbitrum
0x60a0F7594793e3DC31DfE1cC930dF65B54e95B39Optimism Base Cronos Mantle
0xd57e72A328AB1deC6b374c2babe2dc489819B5EaBSC
Protocol TimelockController0x9b1ec4bc634db130ab7310d4e585338888030623Ethereum Polygon Arbitrum
0x744B83343a86F87Ed05a5f3A92939D6d81520F27Optimism Base Cronos Mantle
0xd8dbdDf1c0FDdf9b5eCFA5C067C38DB66739FBABBSC
+TBD # PWN is hiring! https://www.notion.so/PWN-is-hiring-f5a49899369045e39f41fc7e4c7b5633 From 969361c7ff635c0c785869618b54b6c8b49fd3bd Mon Sep 17 00:00:00 2001 From: ashhanai Date: Thu, 18 Apr 2024 16:42:15 -0400 Subject: [PATCH 098/129] refactor: update error docs and move them to contract files when possible --- src/PWNErrors.sol | 81 +++---------- src/config/PWNConfig.sol | 55 ++++++--- src/hub/PWNHub.sol | 2 +- src/interfaces/IERC5646.sol | 3 +- src/interfaces/IPWNDeployer.sol | 32 ++++- src/interfaces/IPWNLoanMetadataProvider.sol | 2 +- src/interfaces/IStateFingerpringComputer.sol | 3 +- src/loan/lib/PWNSignatureChecker.sol | 14 ++- src/loan/terms/simple/loan/PWNSimpleLoan.sol | 112 ++++++++++++++++-- .../PWNSimpleLoanDutchAuctionProposal.sol | 30 ++++- .../PWNSimpleLoanFungibleProposal.sol | 13 +- .../proposal/PWNSimpleLoanListProposal.sol | 8 +- .../simple/proposal/PWNSimpleLoanProposal.sol | 68 ++++++++++- .../proposal/PWNSimpleLoanSimpleProposal.sol | 3 +- src/loan/token/PWNLOAN.sol | 4 +- src/loan/vault/PWNVault.sol | 26 +++- src/loan/vault/Permit.sol | 10 ++ src/nonce/PWNRevokedNonce.sol | 22 +++- test/fork/UseCases.fork.t.sol | 16 +-- test/integration/PWNProtocolIntegrity.t.sol | 2 +- .../PWNSimpleLoanIntegration.t.sol | 4 +- test/unit/PWNConfig.t.sol | 14 +-- test/unit/PWNHub.t.sol | 2 +- test/unit/PWNLOAN.t.sol | 2 +- test/unit/PWNRevokedNonce.t.sol | 16 +-- test/unit/PWNSignatureChecker.t.sol | 3 +- test/unit/PWNSimpleLoan.t.sol | 101 ++++++++++------ .../PWNSimpleLoanDutchAuctionProposal.t.sol | 50 ++++++-- test/unit/PWNSimpleLoanFungibleProposal.t.sol | 15 ++- test/unit/PWNSimpleLoanListProposal.t.sol | 9 +- test/unit/PWNSimpleLoanProposal.t.sol | 81 +++++++++---- test/unit/PWNSimpleLoanSimpleProposal.t.sol | 3 +- test/unit/PWNVault.t.sol | 17 ++- 33 files changed, 573 insertions(+), 250 deletions(-) diff --git a/src/PWNErrors.sol b/src/PWNErrors.sol index 604d950..97cd1dc 100644 --- a/src/PWNErrors.sol +++ b/src/PWNErrors.sol @@ -2,74 +2,27 @@ pragma solidity 0.8.16; -// Access control +/** + * @notice Thrown when caller is missing a PWN Hub tag. + */ error CallerMissingHubTag(bytes32); -error AddressMissingHubTag(address addr, bytes32 tag); - -// Loan contract -error LoanDefaulted(uint40); -error InvalidLoanStatus(uint256); -error NonExistingLoan(); -error CallerNotLOANTokenHolder(); -error RefinanceBorrowerMismatch(address currentBorrower, address newBorrower); -error RefinanceCreditMismatch(); -error RefinanceCollateralMismatch(); -error InvalidLenderSpecHash(bytes32 current, bytes32 expected); -error InvalidDuration(uint256 current, uint256 limit); -error AccruingInterestAPROutOfBounds(uint256 current, uint256 limit); -error CallerNotVault(); -error InvalidSourceOfFunds(address sourceOfFunds); - -// Loan extension -error InvalidExtensionDuration(uint256 duration, uint256 limit); -error InvalidExtensionSigner(address allowed, address current); -error InvalidExtensionCaller(); -// Invalid asset -error InvalidMultiTokenAsset(uint8 category, address addr, uint256 id, uint256 amount); -error InvalidCollateralStateFingerprint(bytes32 current, bytes32 proposed); - -// State fingerprint computer registry -error MissingStateFingerprintComputer(); +/** + * @notice Thrown when an address is missing a PWN Hub tag. + */ +error AddressMissingHubTag(address addr, bytes32 tag); -// LOAN token +/** + * @notice Thrown when `PWNLOAN.burn` caller is not a loan contract that minted the LOAN token. + */ error InvalidLoanContractCaller(); -// Vault -error UnsupportedTransferFunction(); -error IncompleteTransfer(); - -// Nonce -error NonceAlreadyRevoked(address addr, uint256 nonceSpace, uint256 nonce); -error NonceNotUsable(address addr, uint256 nonceSpace, uint256 nonce); - -// Signature checks -error InvalidSignatureLength(uint256); -error InvalidSignature(address signer, bytes32 digest); - -// Proposal -error CallerIsNotStatedProposer(address); -error AcceptorIsProposer(address addr); -error InvalidRefinancingLoanId(uint256 refinancingLoanId); -error AvailableCreditLimitExceeded(uint256 used, uint256 limit); -error Expired(uint256 current, uint256 expiration); -error CallerNotAllowedAcceptor(address current, address allowed); -error InvalidPermitOwner(address current, address expected); -error InvalidPermitAsset(address current, address expected); -error CollateralIdNotWhitelisted(uint256 id); -error MinCollateralAmountNotSet(); -error InsufficientCollateralAmount(uint256 current, uint256 limit); -error InvalidAuctionDuration(uint256 current, uint256 limit); -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 +/** + * @notice Thrown when `PWNHub.setTags` inputs lengths are not equal. + */ error InvalidInputData(); -// Config -error InvalidFeeValue(); -error InvalidFeeCollector(); -error ZeroLoanContract(); +/** + * @notice Thrown when a proposal is expired. + */ +error Expired(uint256 current, uint256 expiration); diff --git a/src/config/PWNConfig.sol b/src/config/PWNConfig.sol index d4799d4..0e9a357 100644 --- a/src/config/PWNConfig.sol +++ b/src/config/PWNConfig.sol @@ -6,7 +6,6 @@ import { Initializable } from "openzeppelin/proxy/utils/Initializable.sol"; import { IPoolAdapter } from "src/interfaces/IPoolAdapter.sol"; import { IStateFingerpringComputer } from "src/interfaces/IStateFingerpringComputer.sol"; -import "src/PWNErrors.sol"; /** @@ -44,44 +43,64 @@ contract PWNConfig is Ownable2Step, Initializable { /** * @notice Mapping holding registered state fingerprint computer to an asset. - * @dev Only owner can update the mapping. */ mapping (address => address) private _sfComputerRegistry; /** * @notice Mapping holding registered pool adapter to a pool address. - * @dev Only owner can update the mapping. */ mapping (address => address) private _poolAdapterRegistry; + /*----------------------------------------------------------*| - |* # EVENTS & ERRORS DEFINITIONS *| + |* # EVENTS DEFINITIONS *| |*----------------------------------------------------------*/ /** - * @dev Emitted when new fee value is set. + * @notice Emitted when new fee value is set. */ event FeeUpdated(uint16 oldFee, uint16 newFee); /** - * @dev Emitted when new fee collector address is set. + * @notice Emitted when new fee collector address is set. */ event FeeCollectorUpdated(address oldFeeCollector, address newFeeCollector); /** - * @dev Emitted when new LOAN token metadata uri is set. + * @notice Emitted when new LOAN token metadata uri is set. */ event LOANMetadataUriUpdated(address indexed loanContract, string newUri); /** - * @dev Emitted when new default LOAN token metadata uri is set. + * @notice Emitted when new default LOAN token metadata uri is set. */ event DefaultLOANMetadataUriUpdated(string newUri); + + /*----------------------------------------------------------*| + |* # ERRORS DEFINITIONS *| + |*----------------------------------------------------------*/ + + /** + * @notice Thrown when registering a computer which does not support the asset it is registered for. + */ + error InvalidComputerContract(address computer, address asset); + + /** + * @notice Thrown when trying to set a fee value higher than `MAX_FEE`. + */ + error InvalidFeeValue(uint256 fee, uint256 limit); + /** - * @notice Error emitted when registering a computer which does not support the asset it is registered for. + * @notice Thrown when trying to set a fee collector to zero address. */ - error InvalidComputerContract(); + error ZeroFeeCollector(); + + /** + * @notice Thrown when trying to set a LOAN token metadata uri for zero address loan contract. + */ + error ZeroLoanContract(); + /*----------------------------------------------------------*| |* # CONSTRUCTOR *| @@ -110,16 +129,19 @@ contract PWNConfig is Ownable2Step, Initializable { /** * @notice Set new protocol fee value. - * @dev Only contract owner can call this function. * @param _fee New fee value in basis points. Value of 100 is 1% fee. */ function setFee(uint16 _fee) external onlyOwner { _setFee(_fee); } + /** + * @notice Internal implementation of setting new protocol fee value. + * @param _fee New fee value in basis points. Value of 100 is 1% fee. + */ function _setFee(uint16 _fee) private { if (_fee > MAX_FEE) - revert InvalidFeeValue(); + revert InvalidFeeValue({ fee: _fee, limit: MAX_FEE }); uint16 oldFee = fee; fee = _fee; @@ -128,16 +150,19 @@ contract PWNConfig is Ownable2Step, Initializable { /** * @notice Set new fee collector address. - * @dev Only contract owner can call this function. * @param _feeCollector New fee collector address. */ function setFeeCollector(address _feeCollector) external onlyOwner { _setFeeCollector(_feeCollector); } + /** + * @notice Internal implementation of setting new fee collector address. + * @param _feeCollector New fee collector address. + */ function _setFeeCollector(address _feeCollector) private { if (_feeCollector == address(0)) - revert InvalidFeeCollector(); + revert ZeroFeeCollector(); address oldFeeCollector = feeCollector; feeCollector = _feeCollector; @@ -206,7 +231,7 @@ contract PWNConfig is Ownable2Step, Initializable { function registerStateFingerprintComputer(address asset, address computer) external onlyOwner { if (computer != address(0)) if (!IStateFingerpringComputer(computer).supportsToken(asset)) - revert InvalidComputerContract(); + revert InvalidComputerContract({ computer: computer, asset: asset }); _sfComputerRegistry[asset] = computer; } diff --git a/src/hub/PWNHub.sol b/src/hub/PWNHub.sol index 8bc8e03..5e58aa2 100644 --- a/src/hub/PWNHub.sol +++ b/src/hub/PWNHub.sol @@ -27,7 +27,7 @@ contract PWNHub is Ownable2Step { |*----------------------------------------------------------*/ /** - * @dev Emitted when tag is set for an address. + * @notice Emitted when tag is set for an address. */ event TagSet(address indexed _address, bytes32 indexed tag, bool hasTag); diff --git a/src/interfaces/IERC5646.sol b/src/interfaces/IERC5646.sol index f300eef..dc9984a 100644 --- a/src/interfaces/IERC5646.sol +++ b/src/interfaces/IERC5646.sol @@ -2,7 +2,8 @@ pragma solidity 0.8.16; /** - * @dev Interface of the ERC5646 standard, as defined in the https://eips.ethereum.org/EIPS/eip-5646. + * @title IERC5646 + * @notice Interface of the ERC5646 standard, as defined in the https://eips.ethereum.org/EIPS/eip-5646. */ interface IERC5646 { diff --git a/src/interfaces/IPWNDeployer.sol b/src/interfaces/IPWNDeployer.sol index 89406c0..bf7d0a2 100644 --- a/src/interfaces/IPWNDeployer.sol +++ b/src/interfaces/IPWNDeployer.sol @@ -1,11 +1,41 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; - +/** + * @title IPWNDeployer + * @notice Interface of the PWN deployer contract. + */ interface IPWNDeployer { + + /** + * @notice Function to return the owner of the deployer contract. + * @return Owner of the deployer contract. + */ function owner() external returns (address); + /** + * @notice Function to deploy a contract with a given salt and bytecode. + * @param salt Salt to be used for deployment. + * @param bytecode Bytecode of the contract to be deployed. + * @return Address of the deployed contract. + */ function deploy(bytes32 salt, bytes memory bytecode) external returns (address); + + /** + * @notice Function to deploy a contract and transfer ownership with a given salt, owner and bytecode. + * @param salt Salt to be used for deployment. + * @param owner Address to which ownership of the deployed contract is transferred. + * @param bytecode Bytecode of the contract to be deployed. + * @return Address of the deployed contract. + */ function deployAndTransferOwnership(bytes32 salt, address owner, bytes memory bytecode) external returns (address); + + /** + * @notice Function to compute the address of a contract with a given salt and bytecode hash. + * @param salt Salt to be used for deployment. + * @param bytecodeHash Hash of the bytecode of the contract to be deployed. + * @return Address of the deployed contract. + */ function computeAddress(bytes32 salt, bytes32 bytecodeHash) external view returns (address); + } diff --git a/src/interfaces/IPWNLoanMetadataProvider.sol b/src/interfaces/IPWNLoanMetadataProvider.sol index ed5dfbf..ea0be12 100644 --- a/src/interfaces/IPWNLoanMetadataProvider.sol +++ b/src/interfaces/IPWNLoanMetadataProvider.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.16; /** - * @title PWN Loan Metadata Provider + * @title IPWNLoanMetadataProvider * @notice Interface for a provider of a LOAN token metadata. * @dev Loan contracts should implement this interface. */ diff --git a/src/interfaces/IStateFingerpringComputer.sol b/src/interfaces/IStateFingerpringComputer.sol index 3fb1a89..da90178 100644 --- a/src/interfaces/IStateFingerpringComputer.sol +++ b/src/interfaces/IStateFingerpringComputer.sol @@ -2,7 +2,8 @@ pragma solidity 0.8.16; /** - * @notice Interface of the state fingerprint computer. + * @title IStateFingerpringComputer + * @notice State Fingerprint Computer Interface. * @dev Contract can compute state fingerprint of several tokens as long as they share the same state structure. */ interface IStateFingerpringComputer { diff --git a/src/loan/lib/PWNSignatureChecker.sol b/src/loan/lib/PWNSignatureChecker.sol index e21b6e7..e86bcf2 100644 --- a/src/loan/lib/PWNSignatureChecker.sol +++ b/src/loan/lib/PWNSignatureChecker.sol @@ -4,8 +4,6 @@ pragma solidity 0.8.16; import { ECDSA } from "openzeppelin/utils/cryptography/ECDSA.sol"; import { IERC1271 } from "openzeppelin/interfaces/IERC1271.sol"; -import "src/PWNErrors.sol"; - /** * @title PWN Signature Checker @@ -16,6 +14,16 @@ library PWNSignatureChecker { string internal constant VERSION = "1.0"; + /** + * @dev Thrown when signature length is not 64 nor 65 bytes. + */ + error InvalidSignatureLength(uint256 length); + + /** + * @dev Thrown when signature is invalid. + */ + error InvalidSignature(address signer, bytes32 digest); + /** * @dev Function will try to recover a signer of a given signature and check if is the same as given signer address. * For a contract account signer address, function will check signature validity by calling `isValidSignature` function defined by EIP-1271. @@ -66,7 +74,7 @@ library PWNSignatureChecker { s = vs & bytes32(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff); v = uint8((uint256(vs) >> 255) + 27); } else { - revert InvalidSignatureLength(signature.length); + revert InvalidSignatureLength({ length: signature.length }); } return signer == ECDSA.recover(hash, v, r, s); diff --git a/src/loan/terms/simple/loan/PWNSimpleLoan.sol b/src/loan/terms/simple/loan/PWNSimpleLoan.sol index 4afccd5..299de37 100644 --- a/src/loan/terms/simple/loan/PWNSimpleLoan.sol +++ b/src/loan/terms/simple/loan/PWNSimpleLoan.sol @@ -16,10 +16,10 @@ import { PWNFeeCalculator } from "src/loan/lib/PWNFeeCalculator.sol"; import { PWNSignatureChecker } from "src/loan/lib/PWNSignatureChecker.sol"; import { PWNSimpleLoanProposal } from "src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol"; import { PWNLOAN } from "src/loan/token/PWNLOAN.sol"; -import { Permit } from "src/loan/vault/Permit.sol"; +import { Permit, InvalidPermitOwner, InvalidPermitAsset } from "src/loan/vault/Permit.sol"; import { PWNVault } from "src/loan/vault/PWNVault.sol"; import { PWNRevokedNonce } from "src/nonce/PWNRevokedNonce.sol"; -import "src/PWNErrors.sol"; +import { Expired, AddressMissingHubTag } from "src/PWNErrors.sol"; /** @@ -192,40 +192,126 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { /*----------------------------------------------------------*| - |* # EVENTS & ERRORS DEFINITIONS *| + |* # EVENTS DEFINITIONS *| |*----------------------------------------------------------*/ /** - * @dev Emitted when a new loan in created. + * @notice Emitted when a new loan in created. */ event LOANCreated(uint256 indexed loanId, bytes32 indexed proposalHash, address indexed proposalContract, Terms terms, LenderSpec lenderSpec, bytes extra); /** - * @dev Emitted when a loan is refinanced. + * @notice Emitted when a loan is refinanced. */ event LOANRefinanced(uint256 indexed loanId, uint256 indexed refinancedLoanId); /** - * @dev Emitted when a loan is paid back. + * @notice Emitted when a loan is paid back. */ event LOANPaidBack(uint256 indexed loanId); /** - * @dev Emitted when a repaid or defaulted loan is claimed. + * @notice Emitted when a repaid or defaulted loan is claimed. */ event LOANClaimed(uint256 indexed loanId, bool indexed defaulted); /** - * @dev Emitted when a LOAN token holder extends a loan. + * @notice Emitted when a LOAN token holder extends a loan. */ event LOANExtended(uint256 indexed loanId, uint40 originalDefaultTimestamp, uint40 extendedDefaultTimestamp); /** - * @dev Emitted when a loan extension proposal is made. + * @notice Emitted when a loan extension proposal is made. */ event ExtensionProposalMade(bytes32 indexed extensionHash, address indexed proposer, ExtensionProposal proposal); + /*----------------------------------------------------------*| + |* # ERRORS DEFINITIONS *| + |*----------------------------------------------------------*/ + + /** + * @notice Thrown when managed loan is defaulted. + */ + error LoanDefaulted(uint40); + + /** + * @notice Thrown when manged loan is in incorrect state. + */ + error InvalidLoanStatus(uint256); + + /** + * @notice Thrown when loan doesn't exist. + */ + error NonExistingLoan(); + + /** + * @notice Thrown when caller is not a LOAN token holder. + */ + error CallerNotLOANTokenHolder(); + + /** + * @notice Thrown when refinancing loan terms have different borrower than the original loan. + */ + error RefinanceBorrowerMismatch(address currentBorrower, address newBorrower); + + /** + * @notice Thrown when refinancing loan terms have different credit asset than the original loan. + */ + error RefinanceCreditMismatch(); + + /** + * @notice Thrown when refinancing loan terms have different collateral asset than the original loan. + */ + error RefinanceCollateralMismatch(); + + /** + * @notice Thrown when hash of provided lender spec doesn't match the one in loan terms. + */ + error InvalidLenderSpecHash(bytes32 current, bytes32 expected); + + /** + * @notice Thrown when loan duration is below the minimum. + */ + error InvalidDuration(uint256 current, uint256 limit); + + /** + * @notice Thrown when accruing interest APR is above the maximum. + */ + error AccruingInterestAPROutOfBounds(uint256 current, uint256 limit); + + /** + * @notice Thrown when caller is not a vault. + */ + error CallerNotVault(); + + /** + * @notice Thrown when pool based source of funds doesn't have a registered adapter. + */ + error InvalidSourceOfFunds(address sourceOfFunds); + + /** + * @notice Thrown when caller is not a loan borrower or lender. + */ + error InvalidExtensionCaller(); + + /** + * @notice Thrown when signer is not a loan extension proposer. + */ + error InvalidExtensionSigner(address allowed, address current); + + /** + * @notice Thrown when loan extension duration is out of bounds. + */ + error InvalidExtensionDuration(uint256 duration, uint256 limit); + + /** + * @notice Thrown when MultiToken.Asset is invalid. + * @dev Could be because of invalid category, address, id or amount. + */ + error InvalidMultiTokenAsset(uint8 category, address addr, uint256 id, uint256 amount); + + /*----------------------------------------------------------*| |* # CONSTRUCTOR *| |*----------------------------------------------------------*/ @@ -889,7 +975,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { bytes32 extensionHash = getExtensionHash(extension); if (!extensionProposalsMade[extensionHash]) if (!PWNSignatureChecker.isValidSignatureNow(extension.proposer, extensionHash, signature)) - revert InvalidSignature({ signer: extension.proposer, digest: extensionHash }); + revert PWNSignatureChecker.InvalidSignature({ signer: extension.proposer, digest: extensionHash }); // Check extension expiration if (block.timestamp >= extension.expiration) @@ -897,7 +983,11 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { // Check extension nonce if (!revokedNonce.isNonceUsable(extension.proposer, extension.nonceSpace, extension.nonce)) - revert NonceNotUsable({ addr: extension.proposer, nonceSpace: extension.nonceSpace, nonce: extension.nonce }); + revert PWNRevokedNonce.NonceNotUsable({ + addr: extension.proposer, + nonceSpace: extension.nonceSpace, + nonce: extension.nonce + }); // Check caller and signer address loanOwner = loanToken.ownerOf(extension.loanId); diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol index 398d854..743f99d 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol @@ -6,8 +6,7 @@ import { MultiToken } from "MultiToken/MultiToken.sol"; import { Math } from "openzeppelin/utils/math/Math.sol"; import { PWNSimpleLoan } from "src/loan/terms/simple/loan/PWNSimpleLoan.sol"; -import { PWNSimpleLoanProposal } from "src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol"; -import "src/PWNErrors.sol"; +import { PWNSimpleLoanProposal, Expired } from "src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol"; /** @@ -91,10 +90,35 @@ contract PWNSimpleLoanDutchAuctionProposal is PWNSimpleLoanProposal { } /** - * @dev Emitted when a proposal is made via an on-chain transaction. + * @notice Emitted when a proposal is made via an on-chain transaction. */ event ProposalMade(bytes32 indexed proposalHash, address indexed proposer, Proposal proposal); + /** + * @notice Thrown when auction duration is less than min auction duration. + */ + error InvalidAuctionDuration(uint256 current, uint256 limit); + + /** + * @notice Thrown when auction duration is not in full minutes. + */ + error AuctionDurationNotInFullMinutes(uint256 current); + + /** + * @notice Thrown when min credit amount is greater than max credit amount. + */ + error InvalidCreditAmountRange(uint256 minCreditAmount, uint256 maxCreditAmount); + + /** + * @notice Thrown when current auction credit amount is not in the range of intended credit amount and slippage. + */ + error InvalidCreditAmount(uint256 auctionCreditAmount, uint256 intendedCreditAmount, uint256 slippage); + + /** + * @notice Thrown when auction has not started yet or has already ended. + */ + error AuctionNotInProgress(uint256 currentTimestamp, uint256 auctionStart); + constructor( address _hub, address _revokedNonce, diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol index 7d57d92..c087bbb 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/utils/math/Math.sol"; import { PWNSimpleLoan } from "src/loan/terms/simple/loan/PWNSimpleLoan.sol"; import { PWNSimpleLoanProposal } from "src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol"; -import "src/PWNErrors.sol"; /** @@ -90,10 +89,20 @@ contract PWNSimpleLoanFungibleProposal is PWNSimpleLoanProposal { } /** - * @dev Emitted when a proposal is made via an on-chain transaction. + * @notice Emitted when a proposal is made via an on-chain transaction. */ event ProposalMade(bytes32 indexed proposalHash, address indexed proposer, Proposal proposal); + /** + * @notice Thrown when proposal has no minimal collateral amount set. + */ + error MinCollateralAmountNotSet(); + + /** + * @notice Thrown when acceptor provides insufficient collateral amount. + */ + error InsufficientCollateralAmount(uint256 current, uint256 limit); + constructor( address _hub, address _revokedNonce, diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol index 8b04ecb..183e641 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/utils/cryptography/MerkleProof.sol"; import { PWNSimpleLoan } from "src/loan/terms/simple/loan/PWNSimpleLoan.sol"; import { PWNSimpleLoanProposal } from "src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol"; -import "src/PWNErrors.sol"; /** @@ -88,10 +87,15 @@ contract PWNSimpleLoanListProposal is PWNSimpleLoanProposal { } /** - * @dev Emitted when a proposal is made via an on-chain transaction. + * @notice Emitted when a proposal is made via an on-chain transaction. */ event ProposalMade(bytes32 indexed proposalHash, address indexed proposer, Proposal proposal); + /** + * @notice Thrown when a collateral id is not whitelisted. + */ + error CollateralIdNotWhitelisted(uint256 id); + constructor( address _hub, address _revokedNonce, diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol index f0400e8..547ee04 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol @@ -11,7 +11,7 @@ import { IERC5646 } from "src/interfaces/IERC5646.sol"; import { PWNSignatureChecker } from "src/loan/lib/PWNSignatureChecker.sol"; import { PWNSimpleLoan } from "src/loan/terms/simple/loan/PWNSimpleLoan.sol"; import { PWNRevokedNonce } from "src/nonce/PWNRevokedNonce.sol"; -import "src/PWNErrors.sol"; +import { Expired, AddressMissingHubTag } from "src/PWNErrors.sol"; /** * @title PWN Simple Loan Proposal Base Contract @@ -19,6 +19,10 @@ import "src/PWNErrors.sol"; */ abstract contract PWNSimpleLoanProposal { + /*----------------------------------------------------------*| + |* # VARIABLES & CONSTANTS DEFINITIONS *| + |*----------------------------------------------------------*/ + bytes32 public immutable DOMAIN_SEPARATOR; bytes32 public immutable MULTIPROPOSAL_DOMAIN_SEPARATOR; @@ -62,6 +66,56 @@ abstract contract PWNSimpleLoanProposal { */ mapping (bytes32 => uint256) public creditUsed; + + /*----------------------------------------------------------*| + |* # ERRORS DEFINITIONS *| + |*----------------------------------------------------------*/ + + /** + * @notice Thrown when a caller is missing a required hub tag. + */ + error CallerNotLoanContract(address caller, address loanContract); + + /** + * @notice Thrown when a state fingerprint computer is not registered. + */ + error MissingStateFingerprintComputer(); + + /** + * @notice Thrown when a proposed collateral state fingerprint doesn't match the current state. + */ + error InvalidCollateralStateFingerprint(bytes32 current, bytes32 proposed); + + /** + * @notice Thrown when a caller is not a stated proposer. + */ + error CallerIsNotStatedProposer(address addr); + + /** + * @notice Thrown when proposal acceptor and proposer are the same. + */ + error AcceptorIsProposer(address addr); + + /** + * @notice Thrown when provided refinance loan id cannot be used. + */ + error InvalidRefinancingLoanId(uint256 refinancingLoanId); + + /** + * @notice Thrown when a proposal would exceed the available credit limit. + */ + error AvailableCreditLimitExceeded(uint256 used, uint256 limit); + + /** + * @notice Thrown when caller is not allowed to accept a proposal. + */ + error CallerNotAllowedAcceptor(address current, address allowed); + + + /*----------------------------------------------------------*| + |* # CONSTRUCTOR *| + |*----------------------------------------------------------*/ + constructor( address _hub, address _revokedNonce, @@ -161,7 +215,7 @@ abstract contract PWNSimpleLoanProposal { */ function _makeProposal(bytes32 proposalHash, address proposer) internal { if (msg.sender != proposer) { - revert CallerIsNotStatedProposer(proposer); + revert CallerIsNotStatedProposer({ addr: proposer }); } proposalsMade[proposalHash] = true; @@ -197,7 +251,7 @@ abstract contract PWNSimpleLoanProposal { // Single proposal signature if (!proposalsMade[proposalHash]) { if (!PWNSignatureChecker.isValidSignatureNow(proposal.proposer, proposalHash, signature)) { - revert InvalidSignature({ signer: proposal.proposer, digest: proposalHash }); + revert PWNSignatureChecker.InvalidSignature({ signer: proposal.proposer, digest: proposalHash }); } } } else { @@ -211,7 +265,7 @@ abstract contract PWNSimpleLoanProposal { }) ); if (!PWNSignatureChecker.isValidSignatureNow(proposal.proposer, multiproposalHash, signature)) { - revert InvalidSignature({ signer: proposal.proposer, digest: multiproposalHash }); + revert PWNSignatureChecker.InvalidSignature({ signer: proposal.proposer, digest: multiproposalHash }); } } @@ -240,7 +294,11 @@ abstract contract PWNSimpleLoanProposal { // Check proposal is not revoked if (!revokedNonce.isNonceUsable(proposal.proposer, proposal.nonceSpace, proposal.nonce)) { - revert NonceNotUsable({ addr: proposal.proposer, nonceSpace: proposal.nonceSpace, nonce: proposal.nonce }); + revert PWNRevokedNonce.NonceNotUsable({ + addr: proposal.proposer, + nonceSpace: proposal.nonceSpace, + nonce: proposal.nonce + }); } // Check propsal is accepted by an allowed address diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol index 06dbb1f..7749792 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 "src/loan/terms/simple/loan/PWNSimpleLoan.sol"; import { PWNSimpleLoanProposal } from "src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol"; -import "src/PWNErrors.sol"; /** @@ -73,7 +72,7 @@ contract PWNSimpleLoanSimpleProposal is PWNSimpleLoanProposal { } /** - * @dev Emitted when a proposal is made via an on-chain transaction. + * @notice Emitted when a proposal is made via an on-chain transaction. */ event ProposalMade(bytes32 indexed proposalHash, address indexed proposer, Proposal proposal); diff --git a/src/loan/token/PWNLOAN.sol b/src/loan/token/PWNLOAN.sol index 4eee3fd..da2f390 100644 --- a/src/loan/token/PWNLOAN.sol +++ b/src/loan/token/PWNLOAN.sol @@ -37,12 +37,12 @@ contract PWNLOAN is PWNHubAccessControl, IERC5646, ERC721 { |*----------------------------------------------------------*/ /** - * @dev Emitted when a new LOAN token is minted. + * @notice Emitted when a new LOAN token is minted. */ event LOANMinted(uint256 indexed loanId, address indexed loanContract, address indexed owner); /** - * @dev Emitted when a LOAN token is burned. + * @notice Emitted when a LOAN token is burned. */ event LOANBurned(uint256 indexed loanId); diff --git a/src/loan/vault/PWNVault.sol b/src/loan/vault/PWNVault.sol index 1c5a67f..f486be9 100644 --- a/src/loan/vault/PWNVault.sol +++ b/src/loan/vault/PWNVault.sol @@ -9,7 +9,6 @@ import { IERC1155Receiver, IERC165 } from "openzeppelin/token/ERC1155/IERC1155Re import { IPoolAdapter } from "src/interfaces/IPoolAdapter.sol"; import { Permit } from "src/loan/vault/Permit.sol"; -import "src/PWNErrors.sol"; /** @@ -25,31 +24,46 @@ abstract contract PWNVault is IERC721Receiver, IERC1155Receiver { |*----------------------------------------------------------*/ /** - * @dev Emitted when asset transfer happens from an `origin` address to a vault. + * @notice Emitted when asset transfer happens from an `origin` address to a vault. */ event VaultPull(MultiToken.Asset asset, address indexed origin); /** - * @dev Emitted when asset transfer happens from a vault to a `beneficiary` address. + * @notice Emitted when asset transfer happens from a vault to a `beneficiary` address. */ event VaultPush(MultiToken.Asset asset, address indexed beneficiary); /** - * @dev Emitted when asset transfer happens from an `origin` address to a `beneficiary` address. + * @notice Emitted when asset transfer happens from an `origin` address to a `beneficiary` address. */ event VaultPushFrom(MultiToken.Asset asset, address indexed origin, address indexed beneficiary); /** - * @dev Emitted when asset is withdrawn from a pool to an `owner` address. + * @notice Emitted when asset is withdrawn from a pool to an `owner` address. */ event PoolWithdraw(MultiToken.Asset asset, address indexed poolAdapter, address indexed pool, address indexed owner); /** - * @dev Emitted when asset is supplied to a pool from a vault. + * @notice Emitted when asset is supplied to a pool from a vault. */ event PoolSupply(MultiToken.Asset asset, address indexed poolAdapter, address indexed pool, address indexed owner); + /*----------------------------------------------------------*| + |* # ERRORS DEFINITIONS *| + |*----------------------------------------------------------*/ + + /** + * @notice Thrown when the Vault receives an asset that is not transferred by the Vault itself. + */ + error UnsupportedTransferFunction(); + + /** + * @notice Thrown when an asset transfer is incomplete. + */ + error IncompleteTransfer(); + + /*----------------------------------------------------------*| |* # TRANSFER FUNCTIONS *| |*----------------------------------------------------------*/ diff --git a/src/loan/vault/Permit.sol b/src/loan/vault/Permit.sol index de430d1..4d91ced 100644 --- a/src/loan/vault/Permit.sol +++ b/src/loan/vault/Permit.sol @@ -20,3 +20,13 @@ struct Permit { bytes32 r; bytes32 s; } + +/** + * @notice Thrown when the permit owner is not matching. + */ +error InvalidPermitOwner(address current, address expected); + +/** + * @notice Thrown when the permit asset is not matching. + */ +error InvalidPermitAsset(address current, address expected); diff --git a/src/nonce/PWNRevokedNonce.sol b/src/nonce/PWNRevokedNonce.sol index e532a7d..d4a2726 100644 --- a/src/nonce/PWNRevokedNonce.sol +++ b/src/nonce/PWNRevokedNonce.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.16; import { PWNHub } from "src/hub/PWNHub.sol"; import { PWNHubTags } from "src/hub/PWNHubTags.sol"; -import "src/PWNErrors.sol"; +import { AddressMissingHubTag } from "src/PWNErrors.sol"; /** @@ -45,16 +45,32 @@ contract PWNRevokedNonce { |*----------------------------------------------------------*/ /** - * @dev Emitted when a nonce is revoked. + * @notice Emitted when a nonce is revoked. */ event NonceRevoked(address indexed owner, uint256 indexed nonceSpace, uint256 indexed nonce); /** - * @dev Emitted when a nonce is revoked. + * @notice Emitted when a nonce is revoked. */ event NonceSpaceRevoked(address indexed owner, uint256 indexed nonceSpace); + /*----------------------------------------------------------*| + |* # ERRORS DEFINITIONS *| + |*----------------------------------------------------------*/ + + /** + * @notice Thrown when trying to revoke a nonce that is already revoked. + */ + error NonceAlreadyRevoked(address addr, uint256 nonceSpace, uint256 nonce); + + /** + * @notice Thrown when nonce is currently not usable. + * @dev Maybe nonce is revoked or not in the current nonce space. + */ + error NonceNotUsable(address addr, uint256 nonceSpace, uint256 nonce); + + /*----------------------------------------------------------*| |* # MODIFIERS *| |*----------------------------------------------------------*/ diff --git a/test/fork/UseCases.fork.t.sol b/test/fork/UseCases.fork.t.sol index b3c8513..2512560 100644 --- a/test/fork/UseCases.fork.t.sol +++ b/test/fork/UseCases.fork.t.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.16; import { MultiToken, ICryptoKitties, IERC20, IERC721 } from "MultiToken/MultiToken.sol"; import { Permit } from "src/loan/vault/Permit.sol"; -import "src/PWNErrors.sol"; +import { PWNVault } from "src/loan/vault/PWNVault.sol"; import { T20 } from "test/helper/T20.sol"; import { @@ -125,7 +125,7 @@ contract InvalidCollateralAssetCategoryTest is UseCasesTest { proposal.collateralAmount = 0; // Create loan - _createLoanRevertWith(abi.encodeWithSelector(InvalidMultiTokenAsset.selector, 1, ZRX, 10e18, 0)); + _createLoanRevertWith(abi.encodeWithSelector(PWNSimpleLoan.InvalidMultiTokenAsset.selector, 1, ZRX, 10e18, 0)); } // Borrower can steal lender’s assets by using WETH as collateral @@ -139,7 +139,7 @@ contract InvalidCollateralAssetCategoryTest is UseCasesTest { proposal.collateralAmount = 10e18; // Create loan - _createLoanRevertWith(abi.encodeWithSelector(InvalidMultiTokenAsset.selector, 2, WETH, 0, 10e18)); + _createLoanRevertWith(abi.encodeWithSelector(PWNSimpleLoan.InvalidMultiTokenAsset.selector, 2, WETH, 0, 10e18)); } // CryptoKitties token is locked when using it as ERC721 type collateral @@ -161,7 +161,7 @@ contract InvalidCollateralAssetCategoryTest is UseCasesTest { proposal.collateralAmount = 0; // Create loan - _createLoanRevertWith(abi.encodeWithSelector(InvalidMultiTokenAsset.selector, 1, CK, ckId, 0)); + _createLoanRevertWith(abi.encodeWithSelector(PWNSimpleLoan.InvalidMultiTokenAsset.selector, 1, CK, ckId, 0)); } } @@ -185,7 +185,7 @@ contract InvalidCreditTest is UseCasesTest { proposal.creditAmount = doodleId; // Create loan - _createLoanRevertWith(abi.encodeWithSelector(InvalidMultiTokenAsset.selector, 0, DOODLE, 0, doodleId)); + _createLoanRevertWith(abi.encodeWithSelector(PWNSimpleLoan.InvalidMultiTokenAsset.selector, 0, DOODLE, 0, doodleId)); } function testUseCase_shouldFail_whenUsingCryptoKittiesAsCredit() external { @@ -204,7 +204,7 @@ contract InvalidCreditTest is UseCasesTest { proposal.creditAmount = ckId; // Create loan - _createLoanRevertWith(abi.encodeWithSelector(InvalidMultiTokenAsset.selector, 0, CK, 0, ckId)); + _createLoanRevertWith(abi.encodeWithSelector(PWNSimpleLoan.InvalidMultiTokenAsset.selector, 0, CK, 0, ckId)); } } @@ -228,7 +228,7 @@ contract TaxTokensTest is UseCasesTest { proposal.collateralAmount = 10e18; // Create loan - _createLoanRevertWith(abi.encodeWithSelector(IncompleteTransfer.selector)); + _createLoanRevertWith(abi.encodeWithSelector(PWNVault.IncompleteTransfer.selector)); } // Fee-on-transfer tokens can be locked in the vault @@ -245,7 +245,7 @@ contract TaxTokensTest is UseCasesTest { proposal.creditAmount = 10e18; // Create loan - _createLoanRevertWith(abi.encodeWithSelector(IncompleteTransfer.selector)); + _createLoanRevertWith(abi.encodeWithSelector(PWNVault.IncompleteTransfer.selector)); } } diff --git a/test/integration/PWNProtocolIntegrity.t.sol b/test/integration/PWNProtocolIntegrity.t.sol index 09067dd..a430d2b 100644 --- a/test/integration/PWNProtocolIntegrity.t.sol +++ b/test/integration/PWNProtocolIntegrity.t.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.16; import { PWNHubTags } from "src/hub/PWNHubTags.sol"; -import "src/PWNErrors.sol"; +import { AddressMissingHubTag } from "src/PWNErrors.sol"; import { MultiToken, diff --git a/test/integration/PWNSimpleLoanIntegration.t.sol b/test/integration/PWNSimpleLoanIntegration.t.sol index 7e6e500..2f7e107 100644 --- a/test/integration/PWNSimpleLoanIntegration.t.sol +++ b/test/integration/PWNSimpleLoanIntegration.t.sol @@ -1,8 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; -import "src/PWNErrors.sol"; - import { MultiToken, MultiTokenCategoryRegistry, @@ -472,7 +470,7 @@ contract PWNSimpleLoanIntegrationTest is BaseIntegrationTest { // Try to repay loan _repayLoanFailing( loanId, - abi.encodeWithSelector(LoanDefaulted.selector, uint40(expiration)) + abi.encodeWithSelector(PWNSimpleLoan.LoanDefaulted.selector, uint40(expiration)) ); } diff --git a/test/unit/PWNConfig.t.sol b/test/unit/PWNConfig.t.sol index d0eb519..0f07ff6 100644 --- a/test/unit/PWNConfig.t.sol +++ b/test/unit/PWNConfig.t.sol @@ -4,7 +4,6 @@ pragma solidity 0.8.16; import { Test } from "forge-std/Test.sol"; import { PWNConfig } from "src/config/PWNConfig.sol"; -import "src/PWNErrors.sol"; abstract contract PWNConfigTest is Test { @@ -137,12 +136,13 @@ contract PWNConfig_SetFee_Test is PWNConfigTest { config.setFee(9); } - function test_shouldFaile_whenNewValueBiggerThanMaxFee() external { + function testFuzz_shouldFail_whenNewValueBiggerThanMaxFee(uint16 fee) external { uint16 maxFee = config.MAX_FEE(); + fee = uint16(bound(fee, maxFee + 1, type(uint16).max)); - vm.expectRevert(abi.encodeWithSelector(InvalidFeeValue.selector)); + vm.expectRevert(abi.encodeWithSelector(PWNConfig.InvalidFeeValue.selector, fee, maxFee)); vm.prank(owner); - config.setFee(maxFee + 1); + config.setFee(fee); } function test_shouldSetFeeValue() external { @@ -187,7 +187,7 @@ contract PWNConfig_SetFeeCollector_Test is PWNConfigTest { } function test_shouldFail_whenSettingZeroAddress() external { - vm.expectRevert(abi.encodeWithSelector(InvalidFeeCollector.selector)); + vm.expectRevert(abi.encodeWithSelector(PWNConfig.ZeroFeeCollector.selector)); vm.prank(owner); config.setFeeCollector(address(0)); } @@ -233,7 +233,7 @@ contract PWNConfig_SetLOANMetadataUri_Test is PWNConfigTest { } function test_shouldFail_whenZeroLoanContract() external { - vm.expectRevert(abi.encodeWithSelector(ZeroLoanContract.selector)); + vm.expectRevert(abi.encodeWithSelector(PWNConfig.ZeroLoanContract.selector)); vm.prank(owner); config.setLOANMetadataUri(address(0), tokenUri); } @@ -413,7 +413,7 @@ contract PWNConfig_RegisterStateFingerprintComputer_Test is PWNConfigTest { assumeAddressIsNot(computer, AddressType.ForgeAddress, AddressType.Precompile, AddressType.ZeroAddress); _mockSupportsToken(computer, asset, false); - vm.expectRevert(abi.encodeWithSelector(PWNConfig.InvalidComputerContract.selector)); + vm.expectRevert(abi.encodeWithSelector(PWNConfig.InvalidComputerContract.selector, computer, asset)); vm.prank(owner); config.registerStateFingerprintComputer(asset, computer); } diff --git a/test/unit/PWNHub.t.sol b/test/unit/PWNHub.t.sol index fa83802..9187d05 100644 --- a/test/unit/PWNHub.t.sol +++ b/test/unit/PWNHub.t.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.16; import { Test } from "forge-std/Test.sol"; import { PWNHub } from "src/hub/PWNHub.sol"; -import "src/PWNErrors.sol"; +import { InvalidInputData } from "src/PWNErrors.sol"; abstract contract PWNHubTest is Test { diff --git a/test/unit/PWNLOAN.t.sol b/test/unit/PWNLOAN.t.sol index f9810b5..26a57ec 100644 --- a/test/unit/PWNLOAN.t.sol +++ b/test/unit/PWNLOAN.t.sol @@ -6,7 +6,7 @@ import { Test } from "forge-std/Test.sol"; import { PWNHubTags } from "src/hub/PWNHubTags.sol"; import { IERC5646 } from "src/interfaces/IERC5646.sol"; import { PWNLOAN } from "src/loan/token/PWNLOAN.sol"; -import "src/PWNErrors.sol"; +import { CallerMissingHubTag, InvalidLoanContractCaller } from "src/PWNErrors.sol"; abstract contract PWNLOANTest is Test { diff --git a/test/unit/PWNRevokedNonce.t.sol b/test/unit/PWNRevokedNonce.t.sol index 283d342..505aeab 100644 --- a/test/unit/PWNRevokedNonce.t.sol +++ b/test/unit/PWNRevokedNonce.t.sol @@ -3,9 +3,11 @@ pragma solidity 0.8.16; import { Test } from "forge-std/Test.sol"; -import { PWNHubTags } from "src/hub/PWNHubTags.sol"; -import { PWNRevokedNonce } from "src/nonce/PWNRevokedNonce.sol"; -import "src/PWNErrors.sol"; +import { + PWNRevokedNonce, + PWNHubTags, + AddressMissingHubTag +} from "src/nonce/PWNRevokedNonce.sol"; abstract contract PWNRevokedNonceTest is Test { @@ -54,7 +56,7 @@ contract PWNRevokedNonce_RevokeNonce_Test is PWNRevokedNonceTest { vm.store(address(revokedNonce), _nonceSpaceSlot(alice), bytes32(nonceSpace)); vm.store(address(revokedNonce), _revokedNonceSlot(alice, nonceSpace, nonce), bytes32(uint256(1))); - vm.expectRevert(abi.encodeWithSelector(NonceAlreadyRevoked.selector, alice, nonceSpace, nonce)); + vm.expectRevert(abi.encodeWithSelector(PWNRevokedNonce.NonceAlreadyRevoked.selector, alice, nonceSpace, nonce)); vm.prank(alice); revokedNonce.revokeNonce(nonce); } @@ -90,7 +92,7 @@ contract PWNRevokedNonce_RevokeNonceWithNonceSpace_Test is PWNRevokedNonceTest { function testFuzz_shouldFail_whenNonceAlreadyRevoked(uint256 nonceSpace, uint256 nonce) external { vm.store(address(revokedNonce), _revokedNonceSlot(alice, nonceSpace, nonce), bytes32(uint256(1))); - vm.expectRevert(abi.encodeWithSelector(NonceAlreadyRevoked.selector, alice, nonceSpace, nonce)); + vm.expectRevert(abi.encodeWithSelector(PWNRevokedNonce.NonceAlreadyRevoked.selector, alice, nonceSpace, nonce)); vm.prank(alice); revokedNonce.revokeNonce(nonceSpace, nonce); } @@ -149,7 +151,7 @@ contract PWNRevokedNonce_RevokeNonceWithOwner_Test is PWNRevokedNonceTest { vm.store(address(revokedNonce), _nonceSpaceSlot(owner), bytes32(nonceSpace)); vm.store(address(revokedNonce), _revokedNonceSlot(owner, nonceSpace, nonce), bytes32(uint256(1))); - vm.expectRevert(abi.encodeWithSelector(NonceAlreadyRevoked.selector, owner, nonceSpace, nonce)); + vm.expectRevert(abi.encodeWithSelector(PWNRevokedNonce.NonceAlreadyRevoked.selector, owner, nonceSpace, nonce)); vm.prank(accessEnabledAddress); revokedNonce.revokeNonce(owner, nonce); } @@ -211,7 +213,7 @@ contract PWNRevokedNonce_RevokeNonceWithNonceSpaceAndOwner_Test is PWNRevokedNon function testFuzz_shouldFail_whenNonceAlreadyRevoked(address owner, uint256 nonceSpace, uint256 nonce) external { vm.store(address(revokedNonce), _revokedNonceSlot(owner, nonceSpace, nonce), bytes32(uint256(1))); - vm.expectRevert(abi.encodeWithSelector(NonceAlreadyRevoked.selector, owner, nonceSpace, nonce)); + vm.expectRevert(abi.encodeWithSelector(PWNRevokedNonce.NonceAlreadyRevoked.selector, owner, nonceSpace, nonce)); vm.prank(accessEnabledAddress); revokedNonce.revokeNonce(owner, nonceSpace, nonce); } diff --git a/test/unit/PWNSignatureChecker.t.sol b/test/unit/PWNSignatureChecker.t.sol index 5c19906..1938e64 100644 --- a/test/unit/PWNSignatureChecker.t.sol +++ b/test/unit/PWNSignatureChecker.t.sol @@ -4,7 +4,6 @@ pragma solidity 0.8.16; import { Test } from "forge-std/Test.sol"; import { PWNSignatureChecker } from "src/loan/lib/PWNSignatureChecker.sol"; -import "src/PWNErrors.sol"; abstract contract PWNSignatureCheckerTest is Test { @@ -86,7 +85,7 @@ contract PWNSignatureChecker_isValidSignatureNow_Test is PWNSignatureCheckerTest function test_shouldFail_whenSignerIsEOA_whenSignatureHasWrongLength() external { signature = abi.encodePacked(uint256(1), uint256(2), uint256(3)); - vm.expectRevert(abi.encodeWithSelector(InvalidSignatureLength.selector, 96)); + vm.expectRevert(abi.encodeWithSelector(PWNSignatureChecker.InvalidSignatureLength.selector, 96)); PWNSignatureChecker.isValidSignatureNow(signer, digest, signature); } diff --git a/test/unit/PWNSimpleLoan.t.sol b/test/unit/PWNSimpleLoan.t.sol index 023e152..b2705b3 100644 --- a/test/unit/PWNSimpleLoan.t.sol +++ b/test/unit/PWNSimpleLoan.t.sol @@ -3,8 +3,19 @@ pragma solidity 0.8.16; import { Test } from "forge-std/Test.sol"; -import { PWNSimpleLoan, PWNHubTags, Math, MultiToken, Permit } from "src/loan/terms/simple/loan/PWNSimpleLoan.sol"; -import "src/PWNErrors.sol"; +import { + PWNSimpleLoan, + PWNHubTags, + Math, + MultiToken, + PWNSignatureChecker, + PWNRevokedNonce, + Permit, + InvalidPermitOwner, + InvalidPermitAsset, + Expired, + AddressMissingHubTag +} from "src/loan/terms/simple/loan/PWNSimpleLoan.sol"; import { T20 } from "test/helper/T20.sol"; import { T721 } from "test/helper/T721.sol"; @@ -404,7 +415,9 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { simpleLoanTerms.lenderSpecHash = lenderSpecHash; _mockLoanTerms(simpleLoanTerms); - vm.expectRevert(abi.encodeWithSelector(InvalidLenderSpecHash.selector, lenderSpecHash, correctLenderSpecHash)); + vm.expectRevert( + abi.encodeWithSelector(PWNSimpleLoan.InvalidLenderSpecHash.selector, lenderSpecHash, correctLenderSpecHash) + ); loan.createLOAN({ proposalSpec: proposalSpec, lenderSpec: lenderSpec, @@ -433,7 +446,7 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { simpleLoanTerms.duration = uint32(duration); _mockLoanTerms(simpleLoanTerms); - vm.expectRevert(abi.encodeWithSelector(InvalidDuration.selector, duration, minDuration)); + vm.expectRevert(abi.encodeWithSelector(PWNSimpleLoan.InvalidDuration.selector, duration, minDuration)); loan.createLOAN({ proposalSpec: proposalSpec, lenderSpec: lenderSpec, @@ -448,7 +461,9 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { simpleLoanTerms.accruingInterestAPR = uint40(interestAPR); _mockLoanTerms(simpleLoanTerms); - vm.expectRevert(abi.encodeWithSelector(AccruingInterestAPROutOfBounds.selector, interestAPR, maxInterest)); + vm.expectRevert( + abi.encodeWithSelector(PWNSimpleLoan.AccruingInterestAPROutOfBounds.selector, interestAPR, maxInterest) + ); loan.createLOAN({ proposalSpec: proposalSpec, lenderSpec: lenderSpec, @@ -466,7 +481,7 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { vm.expectRevert( abi.encodeWithSelector( - InvalidMultiTokenAsset.selector, + PWNSimpleLoan.InvalidMultiTokenAsset.selector, uint8(simpleLoanTerms.credit.category), simpleLoanTerms.credit.assetAddress, simpleLoanTerms.credit.id, @@ -490,7 +505,7 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { vm.expectRevert( abi.encodeWithSelector( - InvalidMultiTokenAsset.selector, + PWNSimpleLoan.InvalidMultiTokenAsset.selector, uint8(simpleLoanTerms.collateral.category), simpleLoanTerms.collateral.assetAddress, simpleLoanTerms.collateral.id, @@ -623,7 +638,7 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { vm.mockCall(config, abi.encodeWithSignature("getPoolAdapter(address)", sourceOfFunds), abi.encode(address(0))); - vm.expectRevert(abi.encodeWithSelector(InvalidSourceOfFunds.selector, sourceOfFunds)); + vm.expectRevert(abi.encodeWithSelector(PWNSimpleLoan.InvalidSourceOfFunds.selector, sourceOfFunds)); loan.createLOAN({ proposalSpec: proposalSpec, lenderSpec: lenderSpec, @@ -784,7 +799,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { simpleLoan.status = 0; _mockLOAN(refinancingLoanId, simpleLoan); - vm.expectRevert(abi.encodeWithSelector(NonExistingLoan.selector)); + vm.expectRevert(abi.encodeWithSelector(PWNSimpleLoan.NonExistingLoan.selector)); loan.createLOAN({ proposalSpec: proposalSpec, lenderSpec: lenderSpec, @@ -797,7 +812,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { simpleLoan.status = 3; _mockLOAN(refinancingLoanId, simpleLoan); - vm.expectRevert(abi.encodeWithSelector(InvalidLoanStatus.selector, 3)); + vm.expectRevert(abi.encodeWithSelector(PWNSimpleLoan.InvalidLoanStatus.selector, 3)); loan.createLOAN({ proposalSpec: proposalSpec, lenderSpec: lenderSpec, @@ -809,7 +824,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { function test_shouldFail_whenLoanIsDefaulted() external { vm.warp(simpleLoan.defaultTimestamp); - vm.expectRevert(abi.encodeWithSelector(LoanDefaulted.selector, simpleLoan.defaultTimestamp)); + vm.expectRevert(abi.encodeWithSelector(PWNSimpleLoan.LoanDefaulted.selector, simpleLoan.defaultTimestamp)); loan.createLOAN({ proposalSpec: proposalSpec, lenderSpec: lenderSpec, @@ -823,7 +838,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { refinancedLoanTerms.credit.assetAddress = _assetAddress; _mockLoanTerms(refinancedLoanTerms); - vm.expectRevert(abi.encodeWithSelector(RefinanceCreditMismatch.selector)); + vm.expectRevert(abi.encodeWithSelector(PWNSimpleLoan.RefinanceCreditMismatch.selector)); loan.createLOAN({ proposalSpec: proposalSpec, lenderSpec: lenderSpec, @@ -836,7 +851,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { refinancedLoanTerms.credit.amount = 0; _mockLoanTerms(refinancedLoanTerms); - vm.expectRevert(abi.encodeWithSelector(RefinanceCreditMismatch.selector)); + vm.expectRevert(abi.encodeWithSelector(PWNSimpleLoan.RefinanceCreditMismatch.selector)); loan.createLOAN({ proposalSpec: proposalSpec, lenderSpec: lenderSpec, @@ -851,7 +866,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { refinancedLoanTerms.collateral.category = MultiToken.Category(_category); _mockLoanTerms(refinancedLoanTerms); - vm.expectRevert(abi.encodeWithSelector(RefinanceCollateralMismatch.selector)); + vm.expectRevert(abi.encodeWithSelector(PWNSimpleLoan.RefinanceCollateralMismatch.selector)); loan.createLOAN({ proposalSpec: proposalSpec, lenderSpec: lenderSpec, @@ -865,7 +880,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { refinancedLoanTerms.collateral.assetAddress = _assetAddress; _mockLoanTerms(refinancedLoanTerms); - vm.expectRevert(abi.encodeWithSelector(RefinanceCollateralMismatch.selector)); + vm.expectRevert(abi.encodeWithSelector(PWNSimpleLoan.RefinanceCollateralMismatch.selector)); loan.createLOAN({ proposalSpec: proposalSpec, lenderSpec: lenderSpec, @@ -879,7 +894,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { refinancedLoanTerms.collateral.id = _id; _mockLoanTerms(refinancedLoanTerms); - vm.expectRevert(abi.encodeWithSelector(RefinanceCollateralMismatch.selector)); + vm.expectRevert(abi.encodeWithSelector(PWNSimpleLoan.RefinanceCollateralMismatch.selector)); loan.createLOAN({ proposalSpec: proposalSpec, lenderSpec: lenderSpec, @@ -893,7 +908,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { refinancedLoanTerms.collateral.amount = _amount; _mockLoanTerms(refinancedLoanTerms); - vm.expectRevert(abi.encodeWithSelector(RefinanceCollateralMismatch.selector)); + vm.expectRevert(abi.encodeWithSelector(PWNSimpleLoan.RefinanceCollateralMismatch.selector)); loan.createLOAN({ proposalSpec: proposalSpec, lenderSpec: lenderSpec, @@ -907,7 +922,9 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { refinancedLoanTerms.borrower = _borrower; _mockLoanTerms(refinancedLoanTerms); - vm.expectRevert(abi.encodeWithSelector(RefinanceBorrowerMismatch.selector, simpleLoan.borrower, _borrower)); + vm.expectRevert( + abi.encodeWithSelector(PWNSimpleLoan.RefinanceBorrowerMismatch.selector, simpleLoan.borrower, _borrower) + ); loan.createLOAN({ proposalSpec: proposalSpec, lenderSpec: lenderSpec, @@ -1010,7 +1027,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { vm.mockCall(config, abi.encodeWithSignature("getPoolAdapter(address)", sourceOfFunds), abi.encode(address(0))); - vm.expectRevert(abi.encodeWithSelector(InvalidSourceOfFunds.selector, sourceOfFunds)); + vm.expectRevert(abi.encodeWithSelector(PWNSimpleLoan.InvalidSourceOfFunds.selector, sourceOfFunds)); loan.createLOAN({ proposalSpec: proposalSpec, lenderSpec: lenderSpec, @@ -1517,7 +1534,7 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { simpleLoan.status = 0; _mockLOAN(loanId, simpleLoan); - vm.expectRevert(abi.encodeWithSelector(NonExistingLoan.selector)); + vm.expectRevert(abi.encodeWithSelector(PWNSimpleLoan.NonExistingLoan.selector)); loan.repayLOAN(loanId, ""); } @@ -1527,14 +1544,14 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { simpleLoan.status = status; _mockLOAN(loanId, simpleLoan); - vm.expectRevert(abi.encodeWithSelector(InvalidLoanStatus.selector, status)); + vm.expectRevert(abi.encodeWithSelector(PWNSimpleLoan.InvalidLoanStatus.selector, status)); loan.repayLOAN(loanId, ""); } function test_shouldFail_whenLoanIsDefaulted() external { vm.warp(simpleLoan.defaultTimestamp); - vm.expectRevert(abi.encodeWithSelector(LoanDefaulted.selector, simpleLoan.defaultTimestamp)); + vm.expectRevert(abi.encodeWithSelector(PWNSimpleLoan.LoanDefaulted.selector, simpleLoan.defaultTimestamp)); loan.repayLOAN(loanId, ""); } @@ -1781,7 +1798,7 @@ contract PWNSimpleLoan_ClaimLOAN_Test is PWNSimpleLoanTest { function testFuzz_shouldFail_whenCallerIsNotLOANTokenHolder(address caller) external { vm.assume(caller != lender); - vm.expectRevert(abi.encodeWithSelector(CallerNotLOANTokenHolder.selector)); + vm.expectRevert(abi.encodeWithSelector(PWNSimpleLoan.CallerNotLOANTokenHolder.selector)); vm.prank(caller); loan.claimLOAN(loanId); } @@ -1790,7 +1807,7 @@ contract PWNSimpleLoan_ClaimLOAN_Test is PWNSimpleLoanTest { simpleLoan.status = 0; _mockLOAN(loanId, simpleLoan); - vm.expectRevert(abi.encodeWithSelector(NonExistingLoan.selector)); + vm.expectRevert(abi.encodeWithSelector(PWNSimpleLoan.NonExistingLoan.selector)); vm.prank(lender); loan.claimLOAN(loanId); } @@ -1799,7 +1816,7 @@ contract PWNSimpleLoan_ClaimLOAN_Test is PWNSimpleLoanTest { simpleLoan.status = 2; _mockLOAN(loanId, simpleLoan); - vm.expectRevert(abi.encodeWithSelector(InvalidLoanStatus.selector, 2)); + vm.expectRevert(abi.encodeWithSelector(PWNSimpleLoan.InvalidLoanStatus.selector, 2)); vm.prank(lender); loan.claimLOAN(loanId); } @@ -1927,7 +1944,7 @@ contract PWNSimpleLoan_TryClaimRepaidLOAN_Test is PWNSimpleLoanTest { function testFuzz_shouldFail_whenCallerIsNotVault(address caller) external { vm.assume(caller != address(loan)); - vm.expectRevert(abi.encodeWithSelector(CallerNotVault.selector)); + vm.expectRevert(abi.encodeWithSelector(PWNSimpleLoan.CallerNotVault.selector)); vm.prank(caller); loan.tryClaimRepaidLOAN(loanId, creditAmount, lender); } @@ -2000,7 +2017,7 @@ contract PWNSimpleLoan_TryClaimRepaidLOAN_Test is PWNSimpleLoanTest { config, abi.encodeWithSignature("getPoolAdapter(address)", sourceOfFunds), abi.encode(address(0)) ); - vm.expectRevert(abi.encodeWithSelector(InvalidSourceOfFunds.selector, sourceOfFunds)); + vm.expectRevert(abi.encodeWithSelector(PWNSimpleLoan.InvalidSourceOfFunds.selector, sourceOfFunds)); vm.prank(address(loan)); loan.tryClaimRepaidLOAN(loanId, creditAmount, lender); } @@ -2075,7 +2092,7 @@ contract PWNSimpleLoan_MakeExtensionProposal_Test is PWNSimpleLoanTest { function testFuzz_shouldFail_whenCallerNotProposer(address caller) external { vm.assume(caller != extension.proposer); - vm.expectRevert(abi.encodeWithSelector(InvalidExtensionSigner.selector, extension.proposer, caller)); + vm.expectRevert(abi.encodeWithSelector(PWNSimpleLoan.InvalidExtensionSigner.selector, extension.proposer, caller)); vm.prank(caller); loan.makeExtensionProposal(extension); } @@ -2137,7 +2154,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { simpleLoan.status = 0; _mockLOAN(loanId, simpleLoan); - vm.expectRevert(abi.encodeWithSelector(NonExistingLoan.selector)); + vm.expectRevert(abi.encodeWithSelector(PWNSimpleLoan.NonExistingLoan.selector)); vm.prank(lender); loan.extendLOAN(extension, "", ""); } @@ -2146,7 +2163,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { simpleLoan.status = 3; _mockLOAN(loanId, simpleLoan); - vm.expectRevert(abi.encodeWithSelector(InvalidLoanStatus.selector, 3)); + vm.expectRevert(abi.encodeWithSelector(PWNSimpleLoan.InvalidLoanStatus.selector, 3)); vm.prank(lender); loan.extendLOAN(extension, "", ""); } @@ -2155,7 +2172,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { pk = boundPrivateKey(pk); vm.assume(pk != borrowerPk); - vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, extension.proposer, _extensionHash(extension))); + vm.expectRevert(abi.encodeWithSelector(PWNSignatureChecker.InvalidSignature.selector, extension.proposer, _extensionHash(extension))); vm.prank(lender); loan.extendLOAN(extension, _signExtension(pk, extension), ""); } @@ -2182,7 +2199,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { ); vm.expectRevert(abi.encodeWithSelector( - NonceNotUsable.selector, extension.proposer, extension.nonceSpace, extension.nonce + PWNRevokedNonce.NonceNotUsable.selector, extension.proposer, extension.nonceSpace, extension.nonce )); vm.prank(lender); loan.extendLOAN(extension, "", ""); @@ -2192,7 +2209,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { vm.assume(caller != borrower && caller != lender); _mockExtensionProposalMade(extension); - vm.expectRevert(abi.encodeWithSelector(InvalidExtensionCaller.selector)); + vm.expectRevert(abi.encodeWithSelector(PWNSimpleLoan.InvalidExtensionCaller.selector)); vm.prank(caller); loan.extendLOAN(extension, "", ""); } @@ -2203,7 +2220,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { extension.proposer = proposer; _mockExtensionProposalMade(extension); - vm.expectRevert(abi.encodeWithSelector(InvalidExtensionSigner.selector, lender, proposer)); + vm.expectRevert(abi.encodeWithSelector(PWNSimpleLoan.InvalidExtensionSigner.selector, lender, proposer)); vm.prank(borrower); loan.extendLOAN(extension, "", ""); } @@ -2214,7 +2231,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { extension.proposer = proposer; _mockExtensionProposalMade(extension); - vm.expectRevert(abi.encodeWithSelector(InvalidExtensionSigner.selector, borrower, proposer)); + vm.expectRevert(abi.encodeWithSelector(PWNSimpleLoan.InvalidExtensionSigner.selector, borrower, proposer)); vm.prank(lender); loan.extendLOAN(extension, "", ""); } @@ -2226,7 +2243,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { extension.duration = duration; _mockExtensionProposalMade(extension); - vm.expectRevert(abi.encodeWithSelector(InvalidExtensionDuration.selector, duration, minDuration)); + vm.expectRevert(abi.encodeWithSelector(PWNSimpleLoan.InvalidExtensionDuration.selector, duration, minDuration)); vm.prank(lender); loan.extendLOAN(extension, "", ""); } @@ -2238,7 +2255,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { extension.duration = duration; _mockExtensionProposalMade(extension); - vm.expectRevert(abi.encodeWithSelector(InvalidExtensionDuration.selector, duration, maxDuration)); + vm.expectRevert(abi.encodeWithSelector(PWNSimpleLoan.InvalidExtensionDuration.selector, duration, maxDuration)); vm.prank(lender); loan.extendLOAN(extension, "", ""); } @@ -2324,7 +2341,15 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { abi.encode(1) // ERC721 ); - vm.expectRevert(abi.encodeWithSelector(InvalidMultiTokenAsset.selector, 0, extension.compensationAddress, 0, extension.compensationAmount)); + vm.expectRevert( + abi.encodeWithSelector( + PWNSimpleLoan.InvalidMultiTokenAsset.selector, + 0, + extension.compensationAddress, + 0, + extension.compensationAmount + ) + ); vm.prank(lender); loan.extendLOAN(extension, "", ""); } diff --git a/test/unit/PWNSimpleLoanDutchAuctionProposal.t.sol b/test/unit/PWNSimpleLoanDutchAuctionProposal.t.sol index 4677758..ead103e 100644 --- a/test/unit/PWNSimpleLoanDutchAuctionProposal.t.sol +++ b/test/unit/PWNSimpleLoanDutchAuctionProposal.t.sol @@ -7,13 +7,13 @@ import { PWNSimpleLoanProposal, PWNSimpleLoan } from "src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol"; -import "src/PWNErrors.sol"; import { MultiToken, Math, PWNSimpleLoanProposalTest, - PWNSimpleLoanProposal_AcceptProposal_Test + PWNSimpleLoanProposal_AcceptProposal_Test, + Expired } from "test/unit/PWNSimpleLoanProposal.t.sol"; @@ -183,7 +183,7 @@ contract PWNSimpleLoanDutchAuctionProposal_MakeProposal_Test is PWNSimpleLoanDut function testFuzz_shouldFail_whenCallerIsNotProposer(address caller) external { vm.assume(caller != proposal.proposer); - vm.expectRevert(abi.encodeWithSelector(CallerIsNotStatedProposer.selector, proposal.proposer)); + vm.expectRevert(abi.encodeWithSelector(PWNSimpleLoanProposal.CallerIsNotStatedProposer.selector, proposal.proposer)); vm.prank(caller); proposalContract.makeProposal(proposal); } @@ -279,7 +279,11 @@ contract PWNSimpleLoanDutchAuctionProposal_GetCreditAmount_Test is PWNSimpleLoan vm.assume(auctionDuration < 1 minutes); proposal.auctionDuration = auctionDuration; - vm.expectRevert(abi.encodeWithSelector(InvalidAuctionDuration.selector, auctionDuration, 1 minutes)); + vm.expectRevert( + abi.encodeWithSelector( + PWNSimpleLoanDutchAuctionProposal.InvalidAuctionDuration.selector, auctionDuration, 1 minutes + ) + ); proposalContract.getCreditAmount(proposal, 0); } @@ -287,7 +291,11 @@ contract PWNSimpleLoanDutchAuctionProposal_GetCreditAmount_Test is PWNSimpleLoan vm.assume(auctionDuration > 1 minutes && auctionDuration % 1 minutes > 0); proposal.auctionDuration = auctionDuration; - vm.expectRevert(abi.encodeWithSelector(AuctionDurationNotInFullMinutes.selector, auctionDuration)); + vm.expectRevert( + abi.encodeWithSelector( + PWNSimpleLoanDutchAuctionProposal.AuctionDurationNotInFullMinutes.selector, auctionDuration + ) + ); proposalContract.getCreditAmount(proposal, 0); } @@ -296,7 +304,11 @@ contract PWNSimpleLoanDutchAuctionProposal_GetCreditAmount_Test is PWNSimpleLoan proposal.minCreditAmount = minCreditAmount; proposal.maxCreditAmount = maxCreditAmount; - vm.expectRevert(abi.encodeWithSelector(InvalidCreditAmountRange.selector, minCreditAmount, maxCreditAmount)); + vm.expectRevert( + abi.encodeWithSelector( + PWNSimpleLoanDutchAuctionProposal.InvalidCreditAmountRange.selector, minCreditAmount, maxCreditAmount + ) + ); proposalContract.getCreditAmount(proposal, 0); } @@ -306,7 +318,9 @@ contract PWNSimpleLoanDutchAuctionProposal_GetCreditAmount_Test is PWNSimpleLoan proposal.auctionStart = auctionStart; - vm.expectRevert(abi.encodeWithSelector(AuctionNotInProgress.selector, time, auctionStart)); + vm.expectRevert( + abi.encodeWithSelector(PWNSimpleLoanDutchAuctionProposal.AuctionNotInProgress.selector, time, auctionStart) + ); proposalContract.getCreditAmount(proposal, time); } @@ -412,9 +426,14 @@ contract PWNSimpleLoanDutchAuctionProposal_AcceptProposal_Test is PWNSimpleLoanD || intendedCreditAmount > auctionCreditAmount ); - vm.expectRevert(abi.encodeWithSelector( - InvalidCreditAmount.selector, auctionCreditAmount, proposalValues.intendedCreditAmount, proposalValues.slippage - )); + vm.expectRevert( + abi.encodeWithSelector( + PWNSimpleLoanDutchAuctionProposal.InvalidCreditAmount.selector, + auctionCreditAmount, + proposalValues.intendedCreditAmount, + proposalValues.slippage + ) + ); vm.prank(activeLoanContract); proposalContract.acceptProposal({ acceptor: acceptor, @@ -447,9 +466,14 @@ contract PWNSimpleLoanDutchAuctionProposal_AcceptProposal_Test is PWNSimpleLoanD || intendedCreditAmount - proposalValues.slippage > auctionCreditAmount ); - vm.expectRevert(abi.encodeWithSelector( - InvalidCreditAmount.selector, auctionCreditAmount, proposalValues.intendedCreditAmount, proposalValues.slippage - )); + vm.expectRevert( + abi.encodeWithSelector( + PWNSimpleLoanDutchAuctionProposal.InvalidCreditAmount.selector, + auctionCreditAmount, + proposalValues.intendedCreditAmount, + proposalValues.slippage + ) + ); vm.prank(activeLoanContract); proposalContract.acceptProposal({ acceptor: acceptor, diff --git a/test/unit/PWNSimpleLoanFungibleProposal.t.sol b/test/unit/PWNSimpleLoanFungibleProposal.t.sol index 2148765..16c0b32 100644 --- a/test/unit/PWNSimpleLoanFungibleProposal.t.sol +++ b/test/unit/PWNSimpleLoanFungibleProposal.t.sol @@ -7,7 +7,6 @@ import { PWNSimpleLoanProposal, PWNSimpleLoan } from "src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol"; -import "src/PWNErrors.sol"; import { MultiToken, @@ -172,7 +171,7 @@ contract PWNSimpleLoanFungibleProposal_MakeProposal_Test is PWNSimpleLoanFungibl function testFuzz_shouldFail_whenCallerIsNotProposer(address caller) external { vm.assume(caller != proposal.proposer); - vm.expectRevert(abi.encodeWithSelector(CallerIsNotStatedProposer.selector, proposal.proposer)); + vm.expectRevert(abi.encodeWithSelector(PWNSimpleLoanProposal.CallerIsNotStatedProposer.selector, proposal.proposer)); vm.prank(caller); proposalContract.makeProposal(proposal); } @@ -290,7 +289,7 @@ contract PWNSimpleLoanFungibleProposal_AcceptProposal_Test is PWNSimpleLoanFungi function test_shouldFail_whenZeroMinCollateralAmount() external { proposal.minCollateralAmount = 0; - vm.expectRevert(abi.encodeWithSelector(MinCollateralAmountNotSet.selector)); + vm.expectRevert(abi.encodeWithSelector(PWNSimpleLoanFungibleProposal.MinCollateralAmountNotSet.selector)); vm.prank(activeLoanContract); proposalContract.acceptProposal({ acceptor: acceptor, @@ -307,9 +306,13 @@ contract PWNSimpleLoanFungibleProposal_AcceptProposal_Test is PWNSimpleLoanFungi proposal.minCollateralAmount = bound(minCollateralAmount, 1, type(uint256).max); proposalValues.collateralAmount = bound(collateralAmount, 0, proposal.minCollateralAmount - 1); - vm.expectRevert(abi.encodeWithSelector( - InsufficientCollateralAmount.selector, proposalValues.collateralAmount, proposal.minCollateralAmount - )); + vm.expectRevert( + abi.encodeWithSelector( + PWNSimpleLoanFungibleProposal.InsufficientCollateralAmount.selector, + proposalValues.collateralAmount, + proposal.minCollateralAmount + ) + ); vm.prank(activeLoanContract); proposalContract.acceptProposal({ acceptor: acceptor, diff --git a/test/unit/PWNSimpleLoanListProposal.t.sol b/test/unit/PWNSimpleLoanListProposal.t.sol index f2f35c6..bc5da91 100644 --- a/test/unit/PWNSimpleLoanListProposal.t.sol +++ b/test/unit/PWNSimpleLoanListProposal.t.sol @@ -7,7 +7,6 @@ import { PWNSimpleLoanProposal, PWNSimpleLoan } from "src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol"; -import "src/PWNErrors.sol"; import { MultiToken, @@ -173,7 +172,7 @@ contract PWNSimpleLoanListProposal_MakeProposal_Test is PWNSimpleLoanListProposa function testFuzz_shouldFail_whenCallerIsNotProposer(address caller) external { vm.assume(caller != proposal.proposer); - vm.expectRevert(abi.encodeWithSelector(CallerIsNotStatedProposer.selector, proposal.proposer)); + vm.expectRevert(abi.encodeWithSelector(PWNSimpleLoanProposal.CallerIsNotStatedProposer.selector, proposal.proposer)); vm.prank(caller); proposalContract.makeProposal(proposal); } @@ -325,7 +324,11 @@ contract PWNSimpleLoanListProposal_AcceptProposal_Test is PWNSimpleLoanListPropo proposalValues.merkleInclusionProof = new bytes32[](1); proposalValues.merkleInclusionProof[0] = id2Hash; - vm.expectRevert(abi.encodeWithSelector(CollateralIdNotWhitelisted.selector, proposalValues.collateralId)); + vm.expectRevert( + abi.encodeWithSelector( + PWNSimpleLoanListProposal.CollateralIdNotWhitelisted.selector, proposalValues.collateralId + ) + ); vm.prank(activeLoanContract); proposalContract.acceptProposal({ acceptor: acceptor, diff --git a/test/unit/PWNSimpleLoanProposal.t.sol b/test/unit/PWNSimpleLoanProposal.t.sol index 2f6a9bc..68428ba 100644 --- a/test/unit/PWNSimpleLoanProposal.t.sol +++ b/test/unit/PWNSimpleLoanProposal.t.sol @@ -9,13 +9,16 @@ import { MerkleProof } from "openzeppelin/utils/cryptography/MerkleProof.sol"; import { IERC165 } from "openzeppelin/utils/introspection/IERC165.sol"; import { Math } from "openzeppelin/utils/math/Math.sol"; -import { PWNHubTags } from "src/hub/PWNHubTags.sol"; -import { PWNSimpleLoan } from "src/loan/terms/simple/loan/PWNSimpleLoan.sol"; import { PWNSimpleLoanProposal, + PWNHubTags, + PWNSimpleLoan, + PWNSignatureChecker, + PWNRevokedNonce, + AddressMissingHubTag, + Expired, IERC5646 } from "src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol"; -import "src/PWNErrors.sol"; abstract contract PWNSimpleLoanProposalTest is Test { @@ -137,7 +140,9 @@ abstract contract PWNSimpleLoanProposal_AcceptProposal_Test is PWNSimpleLoanProp params.base.loanContract = activeLoanContract; params.signature = _sign(proposerPK, _getProposalHashWith()); - vm.expectRevert(abi.encodeWithSelector(CallerNotLoanContract.selector, caller, activeLoanContract)); + vm.expectRevert( + abi.encodeWithSelector(PWNSimpleLoanProposal.CallerNotLoanContract.selector, caller, activeLoanContract) + ); vm.prank(caller); _callAcceptProposalWith(); } @@ -157,7 +162,9 @@ abstract contract PWNSimpleLoanProposal_AcceptProposal_Test is PWNSimpleLoanProp vm.assume(randomPK != proposerPK); params.signature = _sign(randomPK, _getProposalHashWith()); - vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, proposer, _getProposalHashWith())); + vm.expectRevert( + abi.encodeWithSelector(PWNSignatureChecker.InvalidSignature.selector, proposer, _getProposalHashWith()) + ); vm.prank(activeLoanContract); _callAcceptProposalWith(); } @@ -166,7 +173,9 @@ abstract contract PWNSimpleLoanProposal_AcceptProposal_Test is PWNSimpleLoanProp vm.etch(proposer, bytes("data")); params.signature = ""; - vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, proposer, _getProposalHashWith())); + vm.expectRevert( + abi.encodeWithSelector(PWNSignatureChecker.InvalidSignature.selector, proposer, _getProposalHashWith()) + ); vm.prank(activeLoanContract); _callAcceptProposalWith(); } @@ -187,7 +196,7 @@ abstract contract PWNSimpleLoanProposal_AcceptProposal_Test is PWNSimpleLoanProp params.signature = _sign(randomPK, multiproposalHash); params.proposalInclusionProof = proposalInclusionProof; - vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, proposer, multiproposalHash)); + vm.expectRevert(abi.encodeWithSelector(PWNSignatureChecker.InvalidSignature.selector, proposer, multiproposalHash)); vm.prank(activeLoanContract); _callAcceptProposalWith(); } @@ -206,7 +215,7 @@ abstract contract PWNSimpleLoanProposal_AcceptProposal_Test is PWNSimpleLoanProp params.signature = ""; params.proposalInclusionProof = proposalInclusionProof; - vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, proposer, multiproposalHash)); + vm.expectRevert(abi.encodeWithSelector(PWNSignatureChecker.InvalidSignature.selector, proposer, multiproposalHash)); vm.prank(activeLoanContract); _callAcceptProposalWith(); } @@ -231,7 +240,9 @@ abstract contract PWNSimpleLoanProposal_AcceptProposal_Test is PWNSimpleLoanProp : abi.encode(proposalInclusionProof[0], proposalHash) ); bytes32 actualMultiproposalHash = proposalContractAddr.getMultiproposalHash(PWNSimpleLoanProposal.Multiproposal(actualRoot)); - vm.expectRevert(abi.encodeWithSelector(InvalidSignature.selector, proposer, actualMultiproposalHash)); + vm.expectRevert( + abi.encodeWithSelector(PWNSignatureChecker.InvalidSignature.selector, proposer, actualMultiproposalHash) + ); vm.prank(activeLoanContract); _callAcceptProposalWith(); } @@ -340,7 +351,7 @@ abstract contract PWNSimpleLoanProposal_AcceptProposal_Test is PWNSimpleLoanProp params.acceptor = proposer; params.signature = _sign(proposerPK, _getProposalHashWith()); - vm.expectRevert(abi.encodeWithSelector(AcceptorIsProposer.selector, proposer)); + vm.expectRevert(abi.encodeWithSelector(PWNSimpleLoanProposal.AcceptorIsProposer.selector, proposer)); vm.prank(activeLoanContract); _callAcceptProposalWith(); } @@ -351,7 +362,9 @@ abstract contract PWNSimpleLoanProposal_AcceptProposal_Test is PWNSimpleLoanProp params.refinancingLoanId = 0; params.signature = _sign(proposerPK, _getProposalHashWith()); - vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, proposedRefinancingLoanId)); + vm.expectRevert( + abi.encodeWithSelector(PWNSimpleLoanProposal.InvalidRefinancingLoanId.selector, proposedRefinancingLoanId) + ); vm.prank(activeLoanContract); _callAcceptProposalWith(); } @@ -366,7 +379,9 @@ abstract contract PWNSimpleLoanProposal_AcceptProposal_Test is PWNSimpleLoanProp params.refinancingLoanId = refinancingLoanId; params.signature = _sign(proposerPK, _getProposalHashWith()); - vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, proposedRefinancingLoanId)); + vm.expectRevert( + abi.encodeWithSelector(PWNSimpleLoanProposal.InvalidRefinancingLoanId.selector, proposedRefinancingLoanId) + ); vm.prank(activeLoanContract); _callAcceptProposalWith(); } @@ -393,7 +408,9 @@ abstract contract PWNSimpleLoanProposal_AcceptProposal_Test is PWNSimpleLoanProp params.refinancingLoanId = refinancingLoanId; params.signature = _sign(proposerPK, _getProposalHashWith()); - vm.expectRevert(abi.encodeWithSelector(InvalidRefinancingLoanId.selector, proposedRefinancingLoanId)); + vm.expectRevert( + abi.encodeWithSelector(PWNSimpleLoanProposal.InvalidRefinancingLoanId.selector, proposedRefinancingLoanId) + ); vm.prank(activeLoanContract); _callAcceptProposalWith(); } @@ -424,7 +441,7 @@ 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(PWNRevokedNonce.NonceNotUsable.selector, proposer, nonceSpace, nonce)); vm.prank(activeLoanContract); _callAcceptProposalWith(); } @@ -436,7 +453,9 @@ abstract contract PWNSimpleLoanProposal_AcceptProposal_Test is PWNSimpleLoanProp params.acceptor = caller; params.signature = _sign(proposerPK, _getProposalHashWith()); - vm.expectRevert(abi.encodeWithSelector(CallerNotAllowedAcceptor.selector, caller, allowedAcceptor)); + vm.expectRevert( + abi.encodeWithSelector(PWNSimpleLoanProposal.CallerNotAllowedAcceptor.selector, caller, allowedAcceptor) + ); vm.prank(activeLoanContract); _callAcceptProposalWith(); } @@ -469,9 +488,11 @@ abstract contract PWNSimpleLoanProposal_AcceptProposal_Test is PWNSimpleLoanProp bytes32(used) ); - vm.expectRevert(abi.encodeWithSelector( - AvailableCreditLimitExceeded.selector, used + params.base.creditAmount, limit - )); + vm.expectRevert( + abi.encodeWithSelector( + PWNSimpleLoanProposal.AvailableCreditLimitExceeded.selector, used + params.base.creditAmount, limit + ) + ); vm.prank(activeLoanContract); _callAcceptProposalWith(); } @@ -555,9 +576,13 @@ abstract contract PWNSimpleLoanProposal_AcceptProposal_Test is PWNSimpleLoanProp abi.encode(stateFingerprint) ); - vm.expectRevert(abi.encodeWithSelector( - InvalidCollateralStateFingerprint.selector, stateFingerprint, params.base.collateralStateFingerprint - )); + vm.expectRevert( + abi.encodeWithSelector( + PWNSimpleLoanProposal.InvalidCollateralStateFingerprint.selector, + stateFingerprint, + params.base.collateralStateFingerprint + ) + ); vm.prank(activeLoanContract); _callAcceptProposalWith(); } @@ -577,7 +602,7 @@ abstract contract PWNSimpleLoanProposal_AcceptProposal_Test is PWNSimpleLoanProp abi.encode("not implementing ERC165") ); - vm.expectRevert(abi.encodeWithSelector(MissingStateFingerprintComputer.selector)); + vm.expectRevert(abi.encodeWithSelector(PWNSimpleLoanProposal.MissingStateFingerprintComputer.selector)); vm.prank(activeLoanContract); _callAcceptProposalWith(); } @@ -592,7 +617,7 @@ abstract contract PWNSimpleLoanProposal_AcceptProposal_Test is PWNSimpleLoanProp ); _mockERC5646Support(params.base.collateralAddress, false); - vm.expectRevert(abi.encodeWithSelector(MissingStateFingerprintComputer.selector)); + vm.expectRevert(abi.encodeWithSelector(PWNSimpleLoanProposal.MissingStateFingerprintComputer.selector)); vm.prank(activeLoanContract); _callAcceptProposalWith(); } @@ -615,9 +640,13 @@ abstract contract PWNSimpleLoanProposal_AcceptProposal_Test is PWNSimpleLoanProp abi.encode(stateFingerprint) ); - vm.expectRevert(abi.encodeWithSelector( - InvalidCollateralStateFingerprint.selector, stateFingerprint, params.base.collateralStateFingerprint - )); + vm.expectRevert( + abi.encodeWithSelector( + PWNSimpleLoanProposal.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 6fae1e4..bf03f6d 100644 --- a/test/unit/PWNSimpleLoanSimpleProposal.t.sol +++ b/test/unit/PWNSimpleLoanSimpleProposal.t.sol @@ -7,7 +7,6 @@ import { PWNSimpleLoanProposal, PWNSimpleLoan } from "src/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol"; -import "src/PWNErrors.sol"; import { MultiToken, @@ -165,7 +164,7 @@ contract PWNSimpleLoanSimpleProposal_MakeProposal_Test is PWNSimpleLoanSimplePro function testFuzz_shouldFail_whenCallerIsNotProposer(address caller) external { vm.assume(caller != proposal.proposer); - vm.expectRevert(abi.encodeWithSelector(CallerIsNotStatedProposer.selector, proposal.proposer)); + vm.expectRevert(abi.encodeWithSelector(PWNSimpleLoanProposal.CallerIsNotStatedProposer.selector, proposal.proposer)); vm.prank(caller); proposalContract.makeProposal(proposal); } diff --git a/test/unit/PWNVault.t.sol b/test/unit/PWNVault.t.sol index c553ac8..32f7450 100644 --- a/test/unit/PWNVault.t.sol +++ b/test/unit/PWNVault.t.sol @@ -12,7 +12,6 @@ import { IPoolAdapter, Permit } from "src/loan/vault/PWNVault.sol"; -import "src/PWNErrors.sol"; import { DummyPoolAdapter } from "test/helper/DummyPoolAdapter.sol"; import { T20 } from "test/helper/T20.sol"; @@ -103,7 +102,7 @@ contract PWNVault_Pull_Test is PWNVaultTest { abi.encode(alice) ); - vm.expectRevert(abi.encodeWithSelector(IncompleteTransfer.selector)); + vm.expectRevert(abi.encodeWithSelector(PWNVault.IncompleteTransfer.selector)); MultiToken.Asset memory asset = MultiToken.Asset(MultiToken.Category.ERC721, token, 42, 0); vault.pull(asset, alice); } @@ -149,7 +148,7 @@ contract PWNVault_Push_Test is PWNVaultTest { abi.encode(address(vault)) ); - vm.expectRevert(abi.encodeWithSelector(IncompleteTransfer.selector)); + vm.expectRevert(abi.encodeWithSelector(PWNVault.IncompleteTransfer.selector)); MultiToken.Asset memory asset = MultiToken.Asset(MultiToken.Category.ERC721, token, 42, 0); vault.push(asset, alice); } @@ -195,7 +194,7 @@ contract PWNVault_PushFrom_Test is PWNVaultTest { abi.encode(alice) ); - vm.expectRevert(abi.encodeWithSelector(IncompleteTransfer.selector)); + vm.expectRevert(abi.encodeWithSelector(PWNVault.IncompleteTransfer.selector)); MultiToken.Asset memory asset = MultiToken.Asset(MultiToken.Category.ERC721, token, 42, 0); vault.pushFrom(asset, alice, bob); } @@ -254,7 +253,7 @@ contract PWNVault_WithdrawFromPool_Test is PWNVaultTest { abi.encode(asset.amount) ); - vm.expectRevert(abi.encodeWithSelector(IncompleteTransfer.selector)); + vm.expectRevert(abi.encodeWithSelector(PWNVault.IncompleteTransfer.selector)); vault.withdrawFromPool(asset, poolAdapter, pool, alice); } @@ -313,7 +312,7 @@ contract PWNVault_SupplyToPool_Test is PWNVaultTest { abi.encode(asset.amount) ); - vm.expectRevert(abi.encodeWithSelector(IncompleteTransfer.selector)); + vm.expectRevert(abi.encodeWithSelector(PWNVault.IncompleteTransfer.selector)); vault.supplyToPool(asset, poolAdapter, pool, alice); } @@ -402,7 +401,7 @@ contract PWNVault_ReceivedHooks_Test is PWNVaultTest { } function test_shouldFail_whenOperatorIsNotVault_onERC721Received() external { - vm.expectRevert(abi.encodeWithSelector(UnsupportedTransferFunction.selector)); + vm.expectRevert(abi.encodeWithSelector(PWNVault.UnsupportedTransferFunction.selector)); vault.onERC721Received(address(0), address(0), 0, ""); } @@ -413,7 +412,7 @@ contract PWNVault_ReceivedHooks_Test is PWNVaultTest { } function test_shouldFail_whenOperatorIsNotVault_onERC1155Received() external { - vm.expectRevert(abi.encodeWithSelector(UnsupportedTransferFunction.selector)); + vm.expectRevert(abi.encodeWithSelector(PWNVault.UnsupportedTransferFunction.selector)); vault.onERC1155Received(address(0), address(0), 0, 0, ""); } @@ -421,7 +420,7 @@ contract PWNVault_ReceivedHooks_Test is PWNVaultTest { uint256[] memory ids; uint256[] memory values; - vm.expectRevert(abi.encodeWithSelector(UnsupportedTransferFunction.selector)); + vm.expectRevert(abi.encodeWithSelector(PWNVault.UnsupportedTransferFunction.selector)); vault.onERC1155BatchReceived(address(0), address(0), ids, values, ""); } From 555bb71d73072402ad53d89980a0c5d3bf11dd16 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Fri, 19 Apr 2024 14:56:43 -0400 Subject: [PATCH 099/129] refactor: define project remapping --- remappings.txt | 1 + script/PWN.s.sol | 2 +- script/PWNTimelock.s.sol | 3 +- src/Deployments.sol | 22 +++++++-------- src/config/PWNConfig.sol | 4 +-- src/hub/PWNHub.sol | 2 +- src/hub/PWNHubAccessControl.sol | 6 ++-- src/loan/terms/simple/loan/PWNSimpleLoan.sol | 28 +++++++++---------- .../PWNSimpleLoanDutchAuctionProposal.sol | 4 +-- .../PWNSimpleLoanFungibleProposal.sol | 4 +-- .../proposal/PWNSimpleLoanListProposal.sol | 4 +-- .../simple/proposal/PWNSimpleLoanProposal.sol | 16 +++++------ .../proposal/PWNSimpleLoanSimpleProposal.sol | 4 +-- src/loan/token/PWNLOAN.sol | 8 +++--- src/loan/vault/PWNVault.sol | 4 +-- src/nonce/PWNRevokedNonce.sol | 6 ++-- test/DeploymentTest.t.sol | 2 +- test/fork/UseCases.fork.t.sol | 4 +-- test/helper/DummyPoolAdapter.sol | 2 +- test/integration/BaseIntegrationTest.t.sol | 2 +- test/integration/PWNProtocolIntegrity.t.sol | 4 +-- test/unit/PWNConfig.t.sol | 2 +- test/unit/PWNFeeCalculator.t.sol | 2 +- test/unit/PWNHub.t.sol | 4 +-- test/unit/PWNLOAN.t.sol | 8 +++--- test/unit/PWNRevokedNonce.t.sol | 2 +- test/unit/PWNSignatureChecker.t.sol | 2 +- test/unit/PWNSimpleLoan.t.sol | 2 +- .../PWNSimpleLoanDutchAuctionProposal.t.sol | 4 +-- test/unit/PWNSimpleLoanFungibleProposal.t.sol | 4 +-- test/unit/PWNSimpleLoanListProposal.t.sol | 4 +-- test/unit/PWNSimpleLoanProposal.t.sol | 2 +- test/unit/PWNSimpleLoanSimpleProposal.t.sol | 4 +-- test/unit/PWNVault.t.sol | 2 +- 34 files changed, 87 insertions(+), 87 deletions(-) create mode 100644 remappings.txt diff --git a/remappings.txt b/remappings.txt new file mode 100644 index 0000000..741d58c --- /dev/null +++ b/remappings.txt @@ -0,0 +1 @@ +pwn/=src/ \ No newline at end of file diff --git a/script/PWN.s.sol b/script/PWN.s.sol index a5e3887..52447b1 100644 --- a/script/PWN.s.sol +++ b/script/PWN.s.sol @@ -22,7 +22,7 @@ import { PWNLOAN, PWNRevokedNonce, MultiTokenCategoryRegistry -} from "src/Deployments.sol"; +} from "pwn/Deployments.sol"; import { T20 } from "test/helper/T20.sol"; import { T721 } from "test/helper/T721.sol"; diff --git a/script/PWNTimelock.s.sol b/script/PWNTimelock.s.sol index 4d32344..216ca0b 100644 --- a/script/PWNTimelock.s.sol +++ b/script/PWNTimelock.s.sol @@ -12,8 +12,7 @@ import { PWNConfig, IPWNDeployer, PWNHub -} from "src/Deployments.sol"; - +} from "pwn/Deployments.sol"; library PWNDeployerSalt { diff --git a/src/Deployments.sol b/src/Deployments.sol index d459669..b11c368 100644 --- a/src/Deployments.sol +++ b/src/Deployments.sol @@ -8,17 +8,17 @@ import { MultiTokenCategoryRegistry } from "MultiToken/MultiTokenCategoryRegistr import { Strings } from "openzeppelin/utils/Strings.sol"; -import { PWNConfig } from "src/config/PWNConfig.sol"; -import { PWNHub } from "src/hub/PWNHub.sol"; -import { PWNHubTags } from "src/hub/PWNHubTags.sol"; -import { IPWNDeployer } from "src/interfaces/IPWNDeployer.sol"; -import { PWNSimpleLoan } from "src/loan/terms/simple/loan/PWNSimpleLoan.sol"; -import { PWNSimpleLoanDutchAuctionProposal } from "src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol"; -import { PWNSimpleLoanFungibleProposal } from "src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol"; -import { PWNSimpleLoanListProposal } from "src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol"; -import { PWNSimpleLoanSimpleProposal } from "src/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol"; -import { PWNLOAN } from "src/loan/token/PWNLOAN.sol"; -import { PWNRevokedNonce } from "src/nonce/PWNRevokedNonce.sol"; +import { PWNConfig } from "pwn/config/PWNConfig.sol"; +import { PWNHub } from "pwn/hub/PWNHub.sol"; +import { PWNHubTags } from "pwn/hub/PWNHubTags.sol"; +import { IPWNDeployer } from "pwn/interfaces/IPWNDeployer.sol"; +import { PWNSimpleLoan } from "pwn/loan/terms/simple/loan/PWNSimpleLoan.sol"; +import { PWNSimpleLoanDutchAuctionProposal } from "pwn/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol"; +import { PWNSimpleLoanFungibleProposal } from "pwn/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.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"; abstract contract Deployments is CommonBase { diff --git a/src/config/PWNConfig.sol b/src/config/PWNConfig.sol index 0e9a357..2ebc7fd 100644 --- a/src/config/PWNConfig.sol +++ b/src/config/PWNConfig.sol @@ -4,8 +4,8 @@ pragma solidity 0.8.16; import { Ownable2Step } from "openzeppelin/access/Ownable2Step.sol"; import { Initializable } from "openzeppelin/proxy/utils/Initializable.sol"; -import { IPoolAdapter } from "src/interfaces/IPoolAdapter.sol"; -import { IStateFingerpringComputer } from "src/interfaces/IStateFingerpringComputer.sol"; +import { IPoolAdapter } from "pwn/interfaces/IPoolAdapter.sol"; +import { IStateFingerpringComputer } from "pwn/interfaces/IStateFingerpringComputer.sol"; /** diff --git a/src/hub/PWNHub.sol b/src/hub/PWNHub.sol index 5e58aa2..d46e8f1 100644 --- a/src/hub/PWNHub.sol +++ b/src/hub/PWNHub.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.16; import { Ownable2Step } from "openzeppelin/access/Ownable2Step.sol"; -import "src/PWNErrors.sol"; +import { InvalidInputData } from "pwn/PWNErrors.sol"; /** diff --git a/src/hub/PWNHubAccessControl.sol b/src/hub/PWNHubAccessControl.sol index df9363c..5eaf564 100644 --- a/src/hub/PWNHubAccessControl.sol +++ b/src/hub/PWNHubAccessControl.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; -import { PWNHub } from "src/hub/PWNHub.sol"; -import { PWNHubTags } from "src/hub/PWNHubTags.sol"; -import "src/PWNErrors.sol"; +import { PWNHub } from "pwn/hub/PWNHub.sol"; +import { PWNHubTags } from "pwn/hub/PWNHubTags.sol"; +import { CallerMissingHubTag } from "pwn/PWNErrors.sol"; /** diff --git a/src/loan/terms/simple/loan/PWNSimpleLoan.sol b/src/loan/terms/simple/loan/PWNSimpleLoan.sol index 299de37..820ac5b 100644 --- a/src/loan/terms/simple/loan/PWNSimpleLoan.sol +++ b/src/loan/terms/simple/loan/PWNSimpleLoan.sol @@ -6,20 +6,20 @@ import { MultiToken, IMultiTokenCategoryRegistry } from "MultiToken/MultiToken.s import { Math } from "openzeppelin/utils/math/Math.sol"; import { SafeCast } from "openzeppelin/utils/math/SafeCast.sol"; -import { PWNConfig } from "src/config/PWNConfig.sol"; -import { PWNHub } from "src/hub/PWNHub.sol"; -import { PWNHubTags } from "src/hub/PWNHubTags.sol"; -import { IERC5646 } from "src/interfaces/IERC5646.sol"; -import { IPoolAdapter } from "src/interfaces/IPoolAdapter.sol"; -import { IPWNLoanMetadataProvider } from "src/interfaces/IPWNLoanMetadataProvider.sol"; -import { PWNFeeCalculator } from "src/loan/lib/PWNFeeCalculator.sol"; -import { PWNSignatureChecker } from "src/loan/lib/PWNSignatureChecker.sol"; -import { PWNSimpleLoanProposal } from "src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol"; -import { PWNLOAN } from "src/loan/token/PWNLOAN.sol"; -import { Permit, InvalidPermitOwner, InvalidPermitAsset } from "src/loan/vault/Permit.sol"; -import { PWNVault } from "src/loan/vault/PWNVault.sol"; -import { PWNRevokedNonce } from "src/nonce/PWNRevokedNonce.sol"; -import { Expired, AddressMissingHubTag } from "src/PWNErrors.sol"; +import { PWNConfig } from "pwn/config/PWNConfig.sol"; +import { PWNHub } from "pwn/hub/PWNHub.sol"; +import { PWNHubTags } from "pwn/hub/PWNHubTags.sol"; +import { IERC5646 } from "pwn/interfaces/IERC5646.sol"; +import { IPoolAdapter } from "pwn/interfaces/IPoolAdapter.sol"; +import { IPWNLoanMetadataProvider } from "pwn/interfaces/IPWNLoanMetadataProvider.sol"; +import { PWNFeeCalculator } from "pwn/loan/lib/PWNFeeCalculator.sol"; +import { PWNSignatureChecker } from "pwn/loan/lib/PWNSignatureChecker.sol"; +import { PWNSimpleLoanProposal } from "pwn/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol"; +import { PWNLOAN } from "pwn/loan/token/PWNLOAN.sol"; +import { Permit, InvalidPermitOwner, InvalidPermitAsset } from "pwn/loan/vault/Permit.sol"; +import { PWNVault } from "pwn/loan/vault/PWNVault.sol"; +import { PWNRevokedNonce } from "pwn/nonce/PWNRevokedNonce.sol"; +import { Expired, AddressMissingHubTag } from "pwn/PWNErrors.sol"; /** diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol index 743f99d..f40f765 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol @@ -5,8 +5,8 @@ import { MultiToken } from "MultiToken/MultiToken.sol"; import { Math } from "openzeppelin/utils/math/Math.sol"; -import { PWNSimpleLoan } from "src/loan/terms/simple/loan/PWNSimpleLoan.sol"; -import { PWNSimpleLoanProposal, Expired } from "src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol"; +import { PWNSimpleLoan } from "pwn/loan/terms/simple/loan/PWNSimpleLoan.sol"; +import { PWNSimpleLoanProposal, Expired } from "pwn/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol"; /** diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol index c087bbb..7d1d3d1 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol @@ -5,8 +5,8 @@ import { MultiToken } from "MultiToken/MultiToken.sol"; import { Math } from "openzeppelin/utils/math/Math.sol"; -import { PWNSimpleLoan } from "src/loan/terms/simple/loan/PWNSimpleLoan.sol"; -import { PWNSimpleLoanProposal } from "src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol"; +import { PWNSimpleLoan } from "pwn/loan/terms/simple/loan/PWNSimpleLoan.sol"; +import { PWNSimpleLoanProposal } from "pwn/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol"; /** diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol index 183e641..60387f0 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol @@ -5,8 +5,8 @@ import { MultiToken } from "MultiToken/MultiToken.sol"; import { MerkleProof } from "openzeppelin/utils/cryptography/MerkleProof.sol"; -import { PWNSimpleLoan } from "src/loan/terms/simple/loan/PWNSimpleLoan.sol"; -import { PWNSimpleLoanProposal } from "src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol"; +import { PWNSimpleLoan } from "pwn/loan/terms/simple/loan/PWNSimpleLoan.sol"; +import { PWNSimpleLoanProposal } from "pwn/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol"; /** diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol index 547ee04..8968a9a 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol @@ -4,14 +4,14 @@ pragma solidity 0.8.16; import { MerkleProof } from "openzeppelin/utils/cryptography/MerkleProof.sol"; import { ERC165Checker } from "openzeppelin/utils/introspection/ERC165Checker.sol"; -import { PWNConfig, IStateFingerpringComputer } from "src/config/PWNConfig.sol"; -import { PWNHub } from "src/hub/PWNHub.sol"; -import { PWNHubTags } from "src/hub/PWNHubTags.sol"; -import { IERC5646 } from "src/interfaces/IERC5646.sol"; -import { PWNSignatureChecker } from "src/loan/lib/PWNSignatureChecker.sol"; -import { PWNSimpleLoan } from "src/loan/terms/simple/loan/PWNSimpleLoan.sol"; -import { PWNRevokedNonce } from "src/nonce/PWNRevokedNonce.sol"; -import { Expired, AddressMissingHubTag } from "src/PWNErrors.sol"; +import { PWNConfig, IStateFingerpringComputer } from "pwn/config/PWNConfig.sol"; +import { PWNHub } from "pwn/hub/PWNHub.sol"; +import { PWNHubTags } from "pwn/hub/PWNHubTags.sol"; +import { IERC5646 } from "pwn/interfaces/IERC5646.sol"; +import { PWNSignatureChecker } from "pwn/loan/lib/PWNSignatureChecker.sol"; +import { PWNSimpleLoan } from "pwn/loan/terms/simple/loan/PWNSimpleLoan.sol"; +import { PWNRevokedNonce } from "pwn/nonce/PWNRevokedNonce.sol"; +import { Expired, AddressMissingHubTag } from "pwn/PWNErrors.sol"; /** * @title PWN Simple Loan Proposal Base Contract diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol index 7749792..4e89fd3 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol @@ -3,8 +3,8 @@ pragma solidity 0.8.16; import { MultiToken } from "MultiToken/MultiToken.sol"; -import { PWNSimpleLoan } from "src/loan/terms/simple/loan/PWNSimpleLoan.sol"; -import { PWNSimpleLoanProposal } from "src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol"; +import { PWNSimpleLoan } from "pwn/loan/terms/simple/loan/PWNSimpleLoan.sol"; +import { PWNSimpleLoanProposal } from "pwn/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol"; /** diff --git a/src/loan/token/PWNLOAN.sol b/src/loan/token/PWNLOAN.sol index da2f390..3f02cec 100644 --- a/src/loan/token/PWNLOAN.sol +++ b/src/loan/token/PWNLOAN.sol @@ -3,10 +3,10 @@ pragma solidity 0.8.16; import { ERC721 } from "openzeppelin/token/ERC721/ERC721.sol"; -import { PWNHubAccessControl } from "src/hub/PWNHubAccessControl.sol"; -import { IERC5646 } from "src/interfaces/IERC5646.sol"; -import { IPWNLoanMetadataProvider } from "src/interfaces/IPWNLoanMetadataProvider.sol"; -import "src/PWNErrors.sol"; +import { PWNHubAccessControl } from "pwn/hub/PWNHubAccessControl.sol"; +import { IERC5646 } from "pwn/interfaces/IERC5646.sol"; +import { IPWNLoanMetadataProvider } from "pwn/interfaces/IPWNLoanMetadataProvider.sol"; +import { InvalidLoanContractCaller } from "pwn/PWNErrors.sol"; /** diff --git a/src/loan/vault/PWNVault.sol b/src/loan/vault/PWNVault.sol index f486be9..f8e22ee 100644 --- a/src/loan/vault/PWNVault.sol +++ b/src/loan/vault/PWNVault.sol @@ -7,8 +7,8 @@ import { IERC20Permit } from "openzeppelin/token/ERC20/extensions/IERC20Permit.s import { IERC721Receiver } from "openzeppelin/token/ERC721/IERC721Receiver.sol"; import { IERC1155Receiver, IERC165 } from "openzeppelin/token/ERC1155/IERC1155Receiver.sol"; -import { IPoolAdapter } from "src/interfaces/IPoolAdapter.sol"; -import { Permit } from "src/loan/vault/Permit.sol"; +import { IPoolAdapter } from "pwn/interfaces/IPoolAdapter.sol"; +import { Permit } from "pwn/loan/vault/Permit.sol"; /** diff --git a/src/nonce/PWNRevokedNonce.sol b/src/nonce/PWNRevokedNonce.sol index d4a2726..69a90c9 100644 --- a/src/nonce/PWNRevokedNonce.sol +++ b/src/nonce/PWNRevokedNonce.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; -import { PWNHub } from "src/hub/PWNHub.sol"; -import { PWNHubTags } from "src/hub/PWNHubTags.sol"; -import { AddressMissingHubTag } from "src/PWNErrors.sol"; +import { PWNHub } from "pwn/hub/PWNHub.sol"; +import { PWNHubTags } from "pwn/hub/PWNHubTags.sol"; +import { AddressMissingHubTag } from "pwn/PWNErrors.sol"; /** diff --git a/test/DeploymentTest.t.sol b/test/DeploymentTest.t.sol index 7c3960f..45cd228 100644 --- a/test/DeploymentTest.t.sol +++ b/test/DeploymentTest.t.sol @@ -19,7 +19,7 @@ import { PWNLOAN, PWNRevokedNonce, MultiTokenCategoryRegistry -} from "src/Deployments.sol"; +} from "pwn/Deployments.sol"; abstract contract DeploymentTest is Deployments, Test { diff --git a/test/fork/UseCases.fork.t.sol b/test/fork/UseCases.fork.t.sol index 2512560..e019dfb 100644 --- a/test/fork/UseCases.fork.t.sol +++ b/test/fork/UseCases.fork.t.sol @@ -3,8 +3,8 @@ pragma solidity 0.8.16; import { MultiToken, ICryptoKitties, IERC20, IERC721 } from "MultiToken/MultiToken.sol"; -import { Permit } from "src/loan/vault/Permit.sol"; -import { PWNVault } from "src/loan/vault/PWNVault.sol"; +import { Permit } from "pwn/loan/vault/Permit.sol"; +import { PWNVault } from "pwn/loan/vault/PWNVault.sol"; import { T20 } from "test/helper/T20.sol"; import { diff --git a/test/helper/DummyPoolAdapter.sol b/test/helper/DummyPoolAdapter.sol index d8c72e7..656039e 100644 --- a/test/helper/DummyPoolAdapter.sol +++ b/test/helper/DummyPoolAdapter.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.16; import { IERC20 } from "openzeppelin/token/ERC20/IERC20.sol"; -import { IPoolAdapter } from "src/interfaces/IPoolAdapter.sol"; +import { IPoolAdapter } from "pwn/interfaces/IPoolAdapter.sol"; contract DummyPoolAdapter is IPoolAdapter { diff --git a/test/integration/BaseIntegrationTest.t.sol b/test/integration/BaseIntegrationTest.t.sol index 9be5226..b752cf1 100644 --- a/test/integration/BaseIntegrationTest.t.sol +++ b/test/integration/BaseIntegrationTest.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.16; import { MultiToken } from "MultiToken/MultiToken.sol"; -import { Permit } from "src/loan/vault/Permit.sol"; +import { Permit } from "pwn/loan/vault/Permit.sol"; import { T20 } from "test/helper/T20.sol"; import { T721 } from "test/helper/T721.sol"; diff --git a/test/integration/PWNProtocolIntegrity.t.sol b/test/integration/PWNProtocolIntegrity.t.sol index a430d2b..32df91c 100644 --- a/test/integration/PWNProtocolIntegrity.t.sol +++ b/test/integration/PWNProtocolIntegrity.t.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; -import { PWNHubTags } from "src/hub/PWNHubTags.sol"; -import { AddressMissingHubTag } from "src/PWNErrors.sol"; +import { PWNHubTags } from "pwn/hub/PWNHubTags.sol"; +import { AddressMissingHubTag } from "pwn/PWNErrors.sol"; import { MultiToken, diff --git a/test/unit/PWNConfig.t.sol b/test/unit/PWNConfig.t.sol index 0f07ff6..57093c2 100644 --- a/test/unit/PWNConfig.t.sol +++ b/test/unit/PWNConfig.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.16; import { Test } from "forge-std/Test.sol"; -import { PWNConfig } from "src/config/PWNConfig.sol"; +import { PWNConfig } from "pwn/config/PWNConfig.sol"; abstract contract PWNConfigTest is Test { diff --git a/test/unit/PWNFeeCalculator.t.sol b/test/unit/PWNFeeCalculator.t.sol index a198d28..759d3c3 100644 --- a/test/unit/PWNFeeCalculator.t.sol +++ b/test/unit/PWNFeeCalculator.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.16; import { Test } from "forge-std/Test.sol"; -import { PWNFeeCalculator } from "src/loan/lib/PWNFeeCalculator.sol"; +import { PWNFeeCalculator } from "pwn/loan/lib/PWNFeeCalculator.sol"; contract PWNFeeCalculator_CalculateFeeAmount_Test is Test { diff --git a/test/unit/PWNHub.t.sol b/test/unit/PWNHub.t.sol index 9187d05..d9f6cc9 100644 --- a/test/unit/PWNHub.t.sol +++ b/test/unit/PWNHub.t.sol @@ -3,8 +3,8 @@ pragma solidity 0.8.16; import { Test } from "forge-std/Test.sol"; -import { PWNHub } from "src/hub/PWNHub.sol"; -import { InvalidInputData } from "src/PWNErrors.sol"; +import { PWNHub } from "pwn/hub/PWNHub.sol"; +import { InvalidInputData } from "pwn/PWNErrors.sol"; abstract contract PWNHubTest is Test { diff --git a/test/unit/PWNLOAN.t.sol b/test/unit/PWNLOAN.t.sol index 26a57ec..ae0df96 100644 --- a/test/unit/PWNLOAN.t.sol +++ b/test/unit/PWNLOAN.t.sol @@ -3,10 +3,10 @@ pragma solidity 0.8.16; import { Test } from "forge-std/Test.sol"; -import { PWNHubTags } from "src/hub/PWNHubTags.sol"; -import { IERC5646 } from "src/interfaces/IERC5646.sol"; -import { PWNLOAN } from "src/loan/token/PWNLOAN.sol"; -import { CallerMissingHubTag, InvalidLoanContractCaller } from "src/PWNErrors.sol"; +import { PWNHubTags } from "pwn/hub/PWNHubTags.sol"; +import { IERC5646 } from "pwn/interfaces/IERC5646.sol"; +import { PWNLOAN } from "pwn/loan/token/PWNLOAN.sol"; +import { CallerMissingHubTag, InvalidLoanContractCaller } from "pwn/PWNErrors.sol"; abstract contract PWNLOANTest is Test { diff --git a/test/unit/PWNRevokedNonce.t.sol b/test/unit/PWNRevokedNonce.t.sol index 505aeab..0fa687e 100644 --- a/test/unit/PWNRevokedNonce.t.sol +++ b/test/unit/PWNRevokedNonce.t.sol @@ -7,7 +7,7 @@ import { PWNRevokedNonce, PWNHubTags, AddressMissingHubTag -} from "src/nonce/PWNRevokedNonce.sol"; +} from "pwn/nonce/PWNRevokedNonce.sol"; abstract contract PWNRevokedNonceTest is Test { diff --git a/test/unit/PWNSignatureChecker.t.sol b/test/unit/PWNSignatureChecker.t.sol index 1938e64..dadc33b 100644 --- a/test/unit/PWNSignatureChecker.t.sol +++ b/test/unit/PWNSignatureChecker.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.16; import { Test } from "forge-std/Test.sol"; -import { PWNSignatureChecker } from "src/loan/lib/PWNSignatureChecker.sol"; +import { PWNSignatureChecker } from "pwn/loan/lib/PWNSignatureChecker.sol"; abstract contract PWNSignatureCheckerTest is Test { diff --git a/test/unit/PWNSimpleLoan.t.sol b/test/unit/PWNSimpleLoan.t.sol index b2705b3..75ff8c3 100644 --- a/test/unit/PWNSimpleLoan.t.sol +++ b/test/unit/PWNSimpleLoan.t.sol @@ -15,7 +15,7 @@ import { InvalidPermitAsset, Expired, AddressMissingHubTag -} from "src/loan/terms/simple/loan/PWNSimpleLoan.sol"; +} from "pwn/loan/terms/simple/loan/PWNSimpleLoan.sol"; import { T20 } from "test/helper/T20.sol"; import { T721 } from "test/helper/T721.sol"; diff --git a/test/unit/PWNSimpleLoanDutchAuctionProposal.t.sol b/test/unit/PWNSimpleLoanDutchAuctionProposal.t.sol index ead103e..53a9ecf 100644 --- a/test/unit/PWNSimpleLoanDutchAuctionProposal.t.sol +++ b/test/unit/PWNSimpleLoanDutchAuctionProposal.t.sol @@ -1,12 +1,12 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; -import { PWNHubTags } from "src/hub/PWNHubTags.sol"; +import { PWNHubTags } from "pwn/hub/PWNHubTags.sol"; import { PWNSimpleLoanDutchAuctionProposal, PWNSimpleLoanProposal, PWNSimpleLoan -} from "src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol"; +} from "pwn/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol"; import { MultiToken, diff --git a/test/unit/PWNSimpleLoanFungibleProposal.t.sol b/test/unit/PWNSimpleLoanFungibleProposal.t.sol index 16c0b32..ce90c84 100644 --- a/test/unit/PWNSimpleLoanFungibleProposal.t.sol +++ b/test/unit/PWNSimpleLoanFungibleProposal.t.sol @@ -1,12 +1,12 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; -import { PWNHubTags } from "src/hub/PWNHubTags.sol"; +import { PWNHubTags } from "pwn/hub/PWNHubTags.sol"; import { PWNSimpleLoanFungibleProposal, PWNSimpleLoanProposal, PWNSimpleLoan -} from "src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol"; +} from "pwn/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol"; import { MultiToken, diff --git a/test/unit/PWNSimpleLoanListProposal.t.sol b/test/unit/PWNSimpleLoanListProposal.t.sol index bc5da91..27628c9 100644 --- a/test/unit/PWNSimpleLoanListProposal.t.sol +++ b/test/unit/PWNSimpleLoanListProposal.t.sol @@ -1,12 +1,12 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; -import { PWNHubTags } from "src/hub/PWNHubTags.sol"; +import { PWNHubTags } from "pwn/hub/PWNHubTags.sol"; import { PWNSimpleLoanListProposal, PWNSimpleLoanProposal, PWNSimpleLoan -} from "src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol"; +} from "pwn/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol"; import { MultiToken, diff --git a/test/unit/PWNSimpleLoanProposal.t.sol b/test/unit/PWNSimpleLoanProposal.t.sol index 68428ba..b242801 100644 --- a/test/unit/PWNSimpleLoanProposal.t.sol +++ b/test/unit/PWNSimpleLoanProposal.t.sol @@ -18,7 +18,7 @@ import { AddressMissingHubTag, Expired, IERC5646 -} from "src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol"; +} from "pwn/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol"; abstract contract PWNSimpleLoanProposalTest is Test { diff --git a/test/unit/PWNSimpleLoanSimpleProposal.t.sol b/test/unit/PWNSimpleLoanSimpleProposal.t.sol index bf03f6d..d15ed75 100644 --- a/test/unit/PWNSimpleLoanSimpleProposal.t.sol +++ b/test/unit/PWNSimpleLoanSimpleProposal.t.sol @@ -1,12 +1,12 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.16; -import { PWNHubTags } from "src/hub/PWNHubTags.sol"; +import { PWNHubTags } from "pwn/hub/PWNHubTags.sol"; import { PWNSimpleLoanSimpleProposal, PWNSimpleLoanProposal, PWNSimpleLoan -} from "src/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol"; +} from "pwn/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol"; import { MultiToken, diff --git a/test/unit/PWNVault.t.sol b/test/unit/PWNVault.t.sol index 32f7450..91ce9fe 100644 --- a/test/unit/PWNVault.t.sol +++ b/test/unit/PWNVault.t.sol @@ -11,7 +11,7 @@ import { PWNVault, IPoolAdapter, Permit -} from "src/loan/vault/PWNVault.sol"; +} from "pwn/loan/vault/PWNVault.sol"; import { DummyPoolAdapter } from "test/helper/DummyPoolAdapter.sol"; import { T20 } from "test/helper/T20.sol"; From 9d8086ecb6973fe89acda9c85ba9f85998510757 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Fri, 19 Apr 2024 15:34:56 -0400 Subject: [PATCH 100/129] feat: add additional subpath to deployments contracts to allow having project as submodule --- src/Deployments.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Deployments.sol b/src/Deployments.sol index b11c368..6e11e06 100644 --- a/src/Deployments.sol +++ b/src/Deployments.sol @@ -25,6 +25,8 @@ abstract contract Deployments is CommonBase { using stdJson for string; using Strings for uint256; + string public deploymentsSubpath; + uint256[] deployedChains; Deployment deployment; @@ -53,7 +55,7 @@ abstract contract Deployments is CommonBase { function _loadDeployedAddresses() internal { string memory root = vm.projectRoot(); - string memory path = string.concat(root, "/deployments/latest.json"); + string memory path = string.concat(root, deploymentsSubpath, "/deployments/latest.json"); string memory json = vm.readFile(path); bytes memory rawDeployedChains = json.parseRaw(".deployedChains"); deployedChains = abi.decode(rawDeployedChains, (uint256[])); From 127f8b0b2e002e8de2043b1c4582209ca6fa011b Mon Sep 17 00:00:00 2001 From: ashhanai Date: Wed, 24 Apr 2024 15:23:01 +0100 Subject: [PATCH 101/129] refactor: make loan errors more descriptive --- src/loan/terms/simple/loan/PWNSimpleLoan.sol | 43 +++++++++++++------- test/unit/PWNSimpleLoan.t.sol | 12 +++--- 2 files changed, 34 insertions(+), 21 deletions(-) diff --git a/src/loan/terms/simple/loan/PWNSimpleLoan.sol b/src/loan/terms/simple/loan/PWNSimpleLoan.sol index 820ac5b..00b606e 100644 --- a/src/loan/terms/simple/loan/PWNSimpleLoan.sol +++ b/src/loan/terms/simple/loan/PWNSimpleLoan.sol @@ -231,14 +231,24 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { |*----------------------------------------------------------*/ /** - * @notice Thrown when managed loan is defaulted. + * @notice Thrown when managed loan is running. */ - error LoanDefaulted(uint40); + error LoanNotRunning(); + + /** + * @notice Thrown when manged loan is still running. + */ + error LoanRunning(); /** - * @notice Thrown when manged loan is in incorrect state. + * @notice Thrown when managed loan is repaid. */ - error InvalidLoanStatus(uint256); + error LoanRepaid(); + + /** + * @notice Thrown when managed loan is defaulted. + */ + error LoanDefaulted(uint40); /** * @notice Thrown when loan doesn't exist. @@ -278,7 +288,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { /** * @notice Thrown when accruing interest APR is above the maximum. */ - error AccruingInterestAPROutOfBounds(uint256 current, uint256 limit); + error InterestAPROutOfBounds(uint256 current, uint256 limit); /** * @notice Thrown when caller is not a vault. @@ -402,7 +412,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { // Check maximum accruing interest APR if (loanTerms.accruingInterestAPR > MAX_ACCRUING_INTEREST_APR) { - revert AccruingInterestAPROutOfBounds({ current: loanTerms.accruingInterestAPR, limit: MAX_ACCRUING_INTEREST_APR }); + revert InterestAPROutOfBounds({ current: loanTerms.accruingInterestAPR, limit: MAX_ACCRUING_INTEREST_APR }); } if (callerSpec.refinancingLoanId == 0) { @@ -736,11 +746,14 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { */ function _checkLoanCanBeRepaid(uint8 status, uint40 defaultTimestamp) private view { // Check that loan exists and is not from a different loan contract - if (status == 0) revert NonExistingLoan(); + if (status == 0) + revert NonExistingLoan(); // Check that loan is running - if (status != 2) revert InvalidLoanStatus(status); + if (status != 2) + revert LoanNotRunning(); // Check that loan is not defaulted - if (defaultTimestamp <= block.timestamp) revert LoanDefaulted(defaultTimestamp); + if (defaultTimestamp <= block.timestamp) + revert LoanDefaulted(defaultTimestamp); } /** @@ -828,18 +841,18 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { if (loanToken.ownerOf(loanId) != msg.sender) revert CallerNotLOANTokenHolder(); - // Loan is not existing or from a different loan contract if (loan.status == 0) + // Loan is not existing or from a different loan contract revert NonExistingLoan(); - // Loan has been paid back else if (loan.status == 3) + // Loan has been paid back _settleLoanClaim({ loanId: loanId, loanOwner: msg.sender, defaulted: false }); - // Loan is running but expired else if (loan.status == 2 && loan.defaultTimestamp <= block.timestamp) + // Loan is running but expired _settleLoanClaim({ loanId: loanId, loanOwner: msg.sender, defaulted: true }); - // Loan is in wrong state else - revert InvalidLoanStatus(loan.status); + // Loan is in wrong state + revert LoanRunning(); } /** @@ -969,7 +982,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { if (loan.status == 0) revert NonExistingLoan(); if (loan.status == 3) // cannot extend repaid loan - revert InvalidLoanStatus(loan.status); + revert LoanRepaid(); // Check extension validity bytes32 extensionHash = getExtensionHash(extension); diff --git a/test/unit/PWNSimpleLoan.t.sol b/test/unit/PWNSimpleLoan.t.sol index 75ff8c3..fdd990b 100644 --- a/test/unit/PWNSimpleLoan.t.sol +++ b/test/unit/PWNSimpleLoan.t.sol @@ -455,14 +455,14 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { }); } - function testFuzz_shouldFail_whenLoanTermsAccruingInterestAPROutOfBounds(uint256 interestAPR) external { + function testFuzz_shouldFail_whenLoanTermsInterestAPROutOfBounds(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(PWNSimpleLoan.AccruingInterestAPROutOfBounds.selector, interestAPR, maxInterest) + abi.encodeWithSelector(PWNSimpleLoan.InterestAPROutOfBounds.selector, interestAPR, maxInterest) ); loan.createLOAN({ proposalSpec: proposalSpec, @@ -812,7 +812,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { simpleLoan.status = 3; _mockLOAN(refinancingLoanId, simpleLoan); - vm.expectRevert(abi.encodeWithSelector(PWNSimpleLoan.InvalidLoanStatus.selector, 3)); + vm.expectRevert(abi.encodeWithSelector(PWNSimpleLoan.LoanNotRunning.selector)); loan.createLOAN({ proposalSpec: proposalSpec, lenderSpec: lenderSpec, @@ -1544,7 +1544,7 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { simpleLoan.status = status; _mockLOAN(loanId, simpleLoan); - vm.expectRevert(abi.encodeWithSelector(PWNSimpleLoan.InvalidLoanStatus.selector, status)); + vm.expectRevert(abi.encodeWithSelector(PWNSimpleLoan.LoanNotRunning.selector)); loan.repayLOAN(loanId, ""); } @@ -1816,7 +1816,7 @@ contract PWNSimpleLoan_ClaimLOAN_Test is PWNSimpleLoanTest { simpleLoan.status = 2; _mockLOAN(loanId, simpleLoan); - vm.expectRevert(abi.encodeWithSelector(PWNSimpleLoan.InvalidLoanStatus.selector, 2)); + vm.expectRevert(abi.encodeWithSelector(PWNSimpleLoan.LoanRunning.selector)); vm.prank(lender); loan.claimLOAN(loanId); } @@ -2163,7 +2163,7 @@ contract PWNSimpleLoan_ExtendLOAN_Test is PWNSimpleLoanTest { simpleLoan.status = 3; _mockLOAN(loanId, simpleLoan); - vm.expectRevert(abi.encodeWithSelector(PWNSimpleLoan.InvalidLoanStatus.selector, 3)); + vm.expectRevert(abi.encodeWithSelector(PWNSimpleLoan.LoanRepaid.selector)); vm.prank(lender); loan.extendLOAN(extension, "", ""); } From a70c0156dc208479e75e33b82c534c20275ad231 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Wed, 24 Apr 2024 15:23:20 +0100 Subject: [PATCH 102/129] docs: update proposal docs --- .../proposal/PWNSimpleLoanDutchAuctionProposal.sol | 11 +++++------ .../simple/proposal/PWNSimpleLoanFungibleProposal.sol | 11 +++++------ .../simple/proposal/PWNSimpleLoanListProposal.sol | 11 +++++------ .../simple/proposal/PWNSimpleLoanSimpleProposal.sol | 11 +++++------ 4 files changed, 20 insertions(+), 24 deletions(-) diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol index f40f765..3f5e0b5 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol @@ -30,25 +30,24 @@ contract PWNSimpleLoanDutchAuctionProposal is PWNSimpleLoanProposal { * @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 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 checkCollateralStateFingerprint If true, the collateral state fingerprint will be checked during proposal acceptance. * @param collateralStateFingerprint Fingerprint of a collateral state defined by ERC5646. * @param creditAddress Address of an asset which is lended to a borrower. * @param minCreditAmount Minimum amount of tokens which is proposed as a loan to a borrower. If `isOffer` is true, auction will start with this amount, otherwise it will end with this amount. * @param maxCreditAmount Maximum amount of tokens which is proposed as a loan to a borrower. If `isOffer` is true, auction will end with this amount, otherwise it will start with this amount. - * @param availableCreditLimit Available credit limit for the proposal. It is the maximum amount of tokens which can be borrowed using the proposal. + * @param availableCreditLimit Available credit limit for the proposal. It is the maximum amount of tokens which can be borrowed using the proposal. If non-zero, proposal can be accepted more than once, until the credit limit is reached. * @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 accruingInterestAPR Accruing interest APR with 5 decimals. * @param duration Loan duration in seconds. * @param auctionStart Auction start timestamp in seconds. * @param auctionDuration Auction duration 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 proposerSpecHash Hash of a proposer specification. It is a hash of a proposer specific data, which must be provided during a loan creation. + * @param proposerSpecHash Hash of a proposer specific data, which must be provided during a loan creation. * @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 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 { diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol index 7d1d3d1..831ab71 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol @@ -37,23 +37,22 @@ contract PWNSimpleLoanFungibleProposal is PWNSimpleLoanProposal { * @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. - * @param checkCollateralStateFingerprint If true, collateral state fingerprint will be checked on loan terms creation. + * @param checkCollateralStateFingerprint If true, the collateral state fingerprint will be checked during proposal acceptance. * @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 lended to a borrower. * @param creditPerCollateralUnit Amount of tokens which are offered per collateral unit with 38 decimals. - * @param availableCreditLimit Available credit limit for the proposal. It is the maximum amount of tokens which can be borrowed using the proposal. + * @param availableCreditLimit Available credit limit for the proposal. It is the maximum amount of tokens which can be borrowed using the proposal. If non-zero, proposal can be accepted more than once, until the credit limit is reached. * @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 accruingInterestAPR Accruing interest APR with 5 decimals. * @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 proposerSpecHash Hash of a proposer specification. It is a hash of a proposer specific data, which must be provided during a loan creation. + * @param proposerSpecHash Hash of a proposer specific data, which must be provided during a loan creation. * @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 will make others in the group invalid. + * @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 will make others in the group invalid. * @param loanContract Address of a loan contract that will create a loan from the proposal. */ struct Proposal { diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol index 60387f0..7e4567d 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol @@ -31,23 +31,22 @@ contract PWNSimpleLoanListProposal is PWNSimpleLoanProposal { * @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 checkCollateralStateFingerprint If true, the collateral state fingerprint will be checked during proposal acceptance. * @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 availableCreditLimit Available credit limit for the proposal. It is the maximum amount of tokens which can be borrowed using the proposal. If non-zero, proposal can be accepted more than once, until the credit limit is reached. * @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 accruingInterestAPR Accruing interest APR with 5 decimals. * @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 proposerSpecHash Hash of a proposer specification. It is a hash of a proposer specific data, which must be provided during a loan creation. + * @param proposerSpecHash Hash of a proposer specific data, which must be provided during a loan creation. * @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 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 { diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol index 4e89fd3..c2ecda1 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol @@ -28,23 +28,22 @@ contract PWNSimpleLoanSimpleProposal is PWNSimpleLoanProposal { * @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 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 checkCollateralStateFingerprint If true, the collateral state fingerprint will be checked during proposal acceptance. * @param collateralStateFingerprint Fingerprint of a collateral state defined by ERC5646. * @param creditAddress Address of an asset which is lended 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 availableCreditLimit Available credit limit for the proposal. It is the maximum amount of tokens which can be borrowed using the proposal. If non-zero, proposal can be accepted more than once, until the credit limit is reached. * @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 accruingInterestAPR Accruing interest APR with 5 decimals. * @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 proposerSpecHash Hash of a proposer specification. It is a hash of a proposer specific data, which must be provided during a loan creation. + * @param proposerSpecHash Hash of a proposer specific data, which must be provided during a loan creation. * @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 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 { From 4263b766f7bbafd6a3220be467ca9ba1befb783d Mon Sep 17 00:00:00 2001 From: ashhanai Date: Thu, 25 Apr 2024 17:32:29 +0100 Subject: [PATCH 103/129] refactor: stop using protocol safe --- src/Deployments.sol | 1 - test/DeploymentTest.t.sol | 13 +++++++------ test/fork/UseCases.fork.t.sol | 2 +- test/integration/PWNProtocolIntegrity.t.sol | 12 ++++++------ 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Deployments.sol b/src/Deployments.sol index 6e11e06..fe2d65b 100644 --- a/src/Deployments.sol +++ b/src/Deployments.sol @@ -42,7 +42,6 @@ abstract contract Deployments is CommonBase { address deployerSafe; PWNHub hub; PWNLOAN loanToken; - address protocolSafe; address protocolTimelock; PWNRevokedNonce revokedNonce; PWNSimpleLoan simpleLoan; diff --git a/test/DeploymentTest.t.sol b/test/DeploymentTest.t.sol index 45cd228..12856d5 100644 --- a/test/DeploymentTest.t.sol +++ b/test/DeploymentTest.t.sol @@ -29,23 +29,24 @@ abstract contract DeploymentTest is Deployments, Test { } function _protocolNotDeployedOnSelectedChain() internal override { - deployment.protocolSafe = makeAddr("protocolSafe"); + deployment.protocolTimelock = makeAddr("protocolTimelock"); + deployment.adminTimelock = makeAddr("adminTimelock"); deployment.daoSafe = makeAddr("daoSafe"); // Deploy category registry - vm.prank(deployment.protocolSafe); + vm.prank(deployment.protocolTimelock); deployment.categoryRegistry = new MultiTokenCategoryRegistry(); // Deploy protocol deployment.configSingleton = new PWNConfig(); TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy( address(deployment.configSingleton), - deployment.protocolSafe, - abi.encodeWithSignature("initialize(address,uint16,address)", address(this), 0, deployment.daoSafe) + deployment.adminTimelock, + abi.encodeWithSignature("initialize(address,uint16,address)", deployment.protocolTimelock, 0, deployment.daoSafe) ); deployment.config = PWNConfig(address(proxy)); - vm.prank(deployment.protocolSafe); + vm.prank(deployment.protocolTimelock); deployment.hub = new PWNHub(); deployment.revokedNonce = new PWNRevokedNonce(address(deployment.hub), PWNHubTags.NONCE_MANAGER); @@ -113,7 +114,7 @@ abstract contract DeploymentTest is Deployments, Test { tags[8] = PWNHubTags.LOAN_PROPOSAL; tags[9] = PWNHubTags.NONCE_MANAGER; - vm.prank(deployment.protocolSafe); + vm.prank(deployment.protocolTimelock); deployment.hub.setTags(addrs, tags, true); } diff --git a/test/fork/UseCases.fork.t.sol b/test/fork/UseCases.fork.t.sol index e019dfb..19a2f82 100644 --- a/test/fork/UseCases.fork.t.sol +++ b/test/fork/UseCases.fork.t.sol @@ -348,7 +348,7 @@ contract CategoryRegistryForIncompleteERCTokensTest is UseCasesTest { address catCoinBank = 0xdeDf88899D7c9025F19C6c9F188DEb98D49CD760; // Register category - vm.prank(deployment.protocolSafe); + vm.prank(deployment.protocolTimelock); deployment.categoryRegistry.registerCategoryValue(catCoinBank, uint8(MultiToken.Category.ERC721)); // Prepare collateral diff --git a/test/integration/PWNProtocolIntegrity.t.sol b/test/integration/PWNProtocolIntegrity.t.sol index 32df91c..5d21ab8 100644 --- a/test/integration/PWNProtocolIntegrity.t.sol +++ b/test/integration/PWNProtocolIntegrity.t.sol @@ -26,7 +26,7 @@ contract PWNProtocolIntegrityTest is BaseIntegrationTest { function test_shouldFailToCreateLOAN_whenLoanContractNotActive() external { // Remove ACTIVE_LOAN tag - vm.prank(deployment.protocolSafe); + vm.prank(deployment.protocolTimelock); deployment.hub.setTag(address(deployment.simpleLoan), PWNHubTags.ACTIVE_LOAN, false); // Try to create LOAN @@ -40,7 +40,7 @@ contract PWNProtocolIntegrityTest is BaseIntegrationTest { uint256 loanId = _createERC1155Loan(); // Remove ACTIVE_LOAN tag - vm.prank(deployment.protocolSafe); + vm.prank(deployment.protocolTimelock); deployment.hub.setTag(address(deployment.simpleLoan), PWNHubTags.ACTIVE_LOAN, false); // Repay loan directly to original lender @@ -70,7 +70,7 @@ contract PWNProtocolIntegrityTest is BaseIntegrationTest { deployment.loanToken.transferFrom(lender, lender2, loanId); // Remove ACTIVE_LOAN tag - vm.prank(deployment.protocolSafe); + vm.prank(deployment.protocolTimelock); deployment.hub.setTag(address(deployment.simpleLoan), PWNHubTags.ACTIVE_LOAN, false); // Repay loan directly to original lender @@ -104,7 +104,7 @@ contract PWNProtocolIntegrityTest is BaseIntegrationTest { _repayLoan(loanId); // Remove ACTIVE_LOAN tag - vm.prank(deployment.protocolSafe); + vm.prank(deployment.protocolTimelock); deployment.hub.setTag(address(deployment.simpleLoan), PWNHubTags.ACTIVE_LOAN, false); // Claim loan @@ -128,7 +128,7 @@ contract PWNProtocolIntegrityTest is BaseIntegrationTest { function test_shouldFailToCreateLOANTerms_whenCallerIsNotActiveLoan() external { // Remove ACTIVE_LOAN tag - vm.prank(deployment.protocolSafe); + vm.prank(deployment.protocolTimelock); deployment.hub.setTag(address(deployment.simpleLoan), PWNHubTags.ACTIVE_LOAN, false); bytes memory proposalData = deployment.simpleLoanSimpleProposal.encodeProposalData(simpleProposal); @@ -150,7 +150,7 @@ contract PWNProtocolIntegrityTest is BaseIntegrationTest { function test_shouldFailToCreateLOAN_whenPassingInvalidTermsFactoryContract() external { // Remove LOAN_PROPOSAL tag - vm.prank(deployment.protocolSafe); + vm.prank(deployment.protocolTimelock); deployment.hub.setTag(address(deployment.simpleLoanSimpleProposal), PWNHubTags.LOAN_PROPOSAL, false); // Try to create LOAN From 62d042492e11101af7e93ff97d5084f1b0a77352 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Thu, 25 Apr 2024 17:33:17 +0100 Subject: [PATCH 104/129] refactor: implement timelock utils --- script/PWNTimelock.s.sol | 175 +++++------------------------------ script/lib/TimelockUtils.sol | 61 ++++++++++++ 2 files changed, 85 insertions(+), 151 deletions(-) create mode 100644 script/lib/TimelockUtils.sol diff --git a/script/PWNTimelock.s.sol b/script/PWNTimelock.s.sol index 216ca0b..1869f01 100644 --- a/script/PWNTimelock.s.sol +++ b/script/PWNTimelock.s.sol @@ -3,9 +3,8 @@ pragma solidity 0.8.16; import { Script, console2 } from "forge-std/Script.sol"; -import { TimelockController } from "openzeppelin/governance/TimelockController.sol"; - import { GnosisSafeLike, GnosisSafeUtils } from "./lib/GnosisSafeUtils.sol"; +import { TimelockController, TimelockUtils } from "./lib/TimelockUtils.sol"; import { Deployments, @@ -88,17 +87,9 @@ forge script script/PWNTimelock.s.sol:Deploy \ vm.startBroadcast(); - address[] memory proposers = new address[](1); - proposers[0] = 0x0cfC62C2E82dA2f580Fd54a2f526F65B6cC8D6de; - address[] memory executors = new address[](1); - executors[0] = address(0); - address timelock = _deploy({ salt: salt, - bytecode: abi.encodePacked( - type(TimelockController).creationCode, - abi.encode(uint256(0), proposers, executors, address(0)) - ) + bytecode: hex"60806040523480156200001157600080fd5b506040516200230838038062002308833981016040819052620000349162000408565b6200004f60008051602062002288833981519152806200022d565b62000079600080516020620022a8833981519152600080516020620022888339815191526200022d565b620000a3600080516020620022c8833981519152600080516020620022888339815191526200022d565b620000cd600080516020620022e8833981519152600080516020620022888339815191526200022d565b620000e8600080516020620022888339815191523062000278565b6001600160a01b03811615620001135762000113600080516020620022888339815191528262000278565b60005b835181101562000199576200015d600080516020620022a88339815191528583815181106200014957620001496200048f565b60200260200101516200027860201b60201c565b62000186600080516020620022e88339815191528583815181106200014957620001496200048f565b6200019181620004a5565b905062000116565b5060005b8251811015620001e357620001d0600080516020620022c88339815191528483815181106200014957620001496200048f565b620001db81620004a5565b90506200019d565b5060028490556040805160008152602081018690527f11c24f4ead16507c69ac467fbd5e4eed5fb5c699626d2cc6d66421df253886d5910160405180910390a150505050620004cd565b600082815260208190526040808220600101805490849055905190918391839186917fbd79b86ffe0ab8e8776151514217cd7cacd52c909f66475c3af44e129f0b00ff9190a4505050565b62000284828262000288565b5050565b6000828152602081815260408083206001600160a01b038516845290915290205460ff1662000284576000828152602081815260408083206001600160a01b03851684529091529020805460ff19166001179055620002e43390565b6001600160a01b0316816001600160a01b0316837f2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d60405160405180910390a45050565b634e487b7160e01b600052604160045260246000fd5b80516001600160a01b03811681146200035657600080fd5b919050565b600082601f8301126200036d57600080fd5b815160206001600160401b03808311156200038c576200038c62000328565b8260051b604051601f19603f83011681018181108482111715620003b457620003b462000328565b604052938452858101830193838101925087851115620003d357600080fd5b83870191505b84821015620003fd57620003ed826200033e565b83529183019190830190620003d9565b979650505050505050565b600080600080608085870312156200041f57600080fd5b845160208601519094506001600160401b03808211156200043f57600080fd5b6200044d888389016200035b565b945060408701519150808211156200046457600080fd5b5062000473878288016200035b565b92505062000484606086016200033e565b905092959194509250565b634e487b7160e01b600052603260045260246000fd5b600060018201620004c657634e487b7160e01b600052601160045260246000fd5b5060010190565b611dab80620004dd6000396000f3fe6080604052600436106101bb5760003560e01c80638065657f116100ec578063bc197c811161008a578063d547741f11610064578063d547741f14610582578063e38335e5146105a2578063f23a6e61146105b5578063f27a0c92146105e157600080fd5b8063bc197c8114610509578063c4d252f514610535578063d45c44351461055557600080fd5b806391d14854116100c657806391d1485414610480578063a217fddf146104a0578063b08e51c0146104b5578063b1c5f427146104e957600080fd5b80638065657f1461040c5780638f2a0bb01461042c5780638f61f4f51461044c57600080fd5b8063248a9ca31161015957806331d507501161013357806331d507501461038c57806336568abe146103ac578063584b153e146103cc57806364d62353146103ec57600080fd5b8063248a9ca31461030b5780632ab0f5291461033b5780632f2ff15d1461036c57600080fd5b80630d3cf6fc116101955780630d3cf6fc14610260578063134008d31461029457806313bc9f20146102a7578063150b7a02146102c757600080fd5b806301d5062a146101c757806301ffc9a7146101e957806307bd02651461021e57600080fd5b366101c257005b600080fd5b3480156101d357600080fd5b506101e76101e23660046113c0565b6105f6565b005b3480156101f557600080fd5b50610209610204366004611434565b61068b565b60405190151581526020015b60405180910390f35b34801561022a57600080fd5b506102527fd8aa0f3194971a2a116679f7c2090f6939c8d4e01a2a8d7e41d55e5351469e6381565b604051908152602001610215565b34801561026c57600080fd5b506102527f5f58e3a2316349923ce3780f8d587db2d72378aed66a8261c916544fa6846ca581565b6101e76102a236600461145e565b6106b6565b3480156102b357600080fd5b506102096102c23660046114c9565b61076b565b3480156102d357600080fd5b506102f26102e2366004611597565b630a85bd0160e11b949350505050565b6040516001600160e01b03199091168152602001610215565b34801561031757600080fd5b506102526103263660046114c9565b60009081526020819052604090206001015490565b34801561034757600080fd5b506102096103563660046114c9565b6000908152600160208190526040909120541490565b34801561037857600080fd5b506101e76103873660046115fe565b610791565b34801561039857600080fd5b506102096103a73660046114c9565b6107bb565b3480156103b857600080fd5b506101e76103c73660046115fe565b6107d4565b3480156103d857600080fd5b506102096103e73660046114c9565b610857565b3480156103f857600080fd5b506101e76104073660046114c9565b61086d565b34801561041857600080fd5b5061025261042736600461145e565b610911565b34801561043857600080fd5b506101e761044736600461166e565b610950565b34801561045857600080fd5b506102527fb09aa5aeb3702cfd50b6b62bc4532604938f21248a27a1d5ca736082b6819cc181565b34801561048c57600080fd5b5061020961049b3660046115fe565b610aa2565b3480156104ac57600080fd5b50610252600081565b3480156104c157600080fd5b506102527ffd643c72710c63c0180259aba6b2d05451e3591a24e58b62239378085726f78381565b3480156104f557600080fd5b5061025261050436600461171f565b610acb565b34801561051557600080fd5b506102f2610524366004611846565b63bc197c8160e01b95945050505050565b34801561054157600080fd5b506101e76105503660046114c9565b610b10565b34801561056157600080fd5b506102526105703660046114c9565b60009081526001602052604090205490565b34801561058e57600080fd5b506101e761059d3660046115fe565b610be5565b6101e76105b036600461171f565b610c0a565b3480156105c157600080fd5b506102f26105d03660046118ef565b63f23a6e6160e01b95945050505050565b3480156105ed57600080fd5b50600254610252565b7fb09aa5aeb3702cfd50b6b62bc4532604938f21248a27a1d5ca736082b6819cc161062081610d94565b6000610630898989898989610911565b905061063c8184610da1565b6000817f4cf4410cc57040e44862ef0f45f3dd5a5e02db8eb8add648d4b0e236f1d07dca8b8b8b8b8b8a6040516106789695949392919061197c565b60405180910390a3505050505050505050565b60006001600160e01b03198216630271189760e51b14806106b057506106b082610e90565b92915050565b7fd8aa0f3194971a2a116679f7c2090f6939c8d4e01a2a8d7e41d55e5351469e636106e2816000610aa2565b6106f0576106f08133610ec5565b6000610700888888888888610911565b905061070c8185610f1e565b61071888888888610fba565b6000817fc2617efa69bab66782fa219543714338489c4e9e178271560a91b82c3f612b588a8a8a8a60405161075094939291906119b9565b60405180910390a36107618161108d565b5050505050505050565b60008181526001602052604081205460018111801561078a5750428111155b9392505050565b6000828152602081905260409020600101546107ac81610d94565b6107b683836110c6565b505050565b60008181526001602052604081205481905b1192915050565b6001600160a01b03811633146108495760405162461bcd60e51b815260206004820152602f60248201527f416363657373436f6e74726f6c3a2063616e206f6e6c792072656e6f756e636560448201526e103937b632b9903337b91039b2b63360891b60648201526084015b60405180910390fd5b610853828261114a565b5050565b60008181526001602081905260408220546107cd565b3330146108d05760405162461bcd60e51b815260206004820152602b60248201527f54696d656c6f636b436f6e74726f6c6c65723a2063616c6c6572206d7573742060448201526a62652074696d656c6f636b60a81b6064820152608401610840565b60025460408051918252602082018390527f11c24f4ead16507c69ac467fbd5e4eed5fb5c699626d2cc6d66421df253886d5910160405180910390a1600255565b600086868686868660405160200161092e9695949392919061197c565b6040516020818303038152906040528051906020012090509695505050505050565b7fb09aa5aeb3702cfd50b6b62bc4532604938f21248a27a1d5ca736082b6819cc161097a81610d94565b8887146109995760405162461bcd60e51b8152600401610840906119eb565b8885146109b85760405162461bcd60e51b8152600401610840906119eb565b60006109ca8b8b8b8b8b8b8b8b610acb565b90506109d68184610da1565b60005b8a811015610a945780827f4cf4410cc57040e44862ef0f45f3dd5a5e02db8eb8add648d4b0e236f1d07dca8e8e85818110610a1657610a16611a2e565b9050602002016020810190610a2b9190611a44565b8d8d86818110610a3d57610a3d611a2e565b905060200201358c8c87818110610a5657610a56611a2e565b9050602002810190610a689190611a5f565b8c8b604051610a7c9695949392919061197c565b60405180910390a3610a8d81611abb565b90506109d9565b505050505050505050505050565b6000918252602082815260408084206001600160a01b0393909316845291905290205460ff1690565b60008888888888888888604051602001610aec989796959493929190611b65565b60405160208183030381529060405280519060200120905098975050505050505050565b7ffd643c72710c63c0180259aba6b2d05451e3591a24e58b62239378085726f783610b3a81610d94565b610b4382610857565b610ba95760405162461bcd60e51b815260206004820152603160248201527f54696d656c6f636b436f6e74726f6c6c65723a206f7065726174696f6e2063616044820152701b9b9bdd0818994818d85b98d95b1b1959607a1b6064820152608401610840565b6000828152600160205260408082208290555183917fbaa1eb22f2a492ba1a5fea61b8df4d27c6c8b5f3971e63bb58fa14ff72eedb7091a25050565b600082815260208190526040902060010154610c0081610d94565b6107b6838361114a565b7fd8aa0f3194971a2a116679f7c2090f6939c8d4e01a2a8d7e41d55e5351469e63610c36816000610aa2565b610c4457610c448133610ec5565b878614610c635760405162461bcd60e51b8152600401610840906119eb565b878414610c825760405162461bcd60e51b8152600401610840906119eb565b6000610c948a8a8a8a8a8a8a8a610acb565b9050610ca08185610f1e565b60005b89811015610d7e5760008b8b83818110610cbf57610cbf611a2e565b9050602002016020810190610cd49190611a44565b905060008a8a84818110610cea57610cea611a2e565b9050602002013590503660008a8a86818110610d0857610d08611a2e565b9050602002810190610d1a9190611a5f565b91509150610d2a84848484610fba565b84867fc2617efa69bab66782fa219543714338489c4e9e178271560a91b82c3f612b5886868686604051610d6194939291906119b9565b60405180910390a35050505080610d7790611abb565b9050610ca3565b50610d888161108d565b50505050505050505050565b610d9e8133610ec5565b50565b610daa826107bb565b15610e0f5760405162461bcd60e51b815260206004820152602f60248201527f54696d656c6f636b436f6e74726f6c6c65723a206f7065726174696f6e20616c60448201526e1c9958591e481cd8da19591d5b1959608a1b6064820152608401610840565b600254811015610e705760405162461bcd60e51b815260206004820152602660248201527f54696d656c6f636b436f6e74726f6c6c65723a20696e73756666696369656e746044820152652064656c617960d01b6064820152608401610840565b610e7a8142611c06565b6000928352600160205260409092209190915550565b60006001600160e01b03198216637965db0b60e01b14806106b057506301ffc9a760e01b6001600160e01b03198316146106b0565b610ecf8282610aa2565b61085357610edc816111af565b610ee78360206111c1565b604051602001610ef8929190611c3d565b60408051601f198184030181529082905262461bcd60e51b825261084091600401611cb2565b610f278261076b565b610f435760405162461bcd60e51b815260040161084090611ce5565b801580610f5f5750600081815260016020819052604090912054145b6108535760405162461bcd60e51b815260206004820152602660248201527f54696d656c6f636b436f6e74726f6c6c65723a206d697373696e6720646570656044820152656e64656e637960d01b6064820152608401610840565b6000846001600160a01b0316848484604051610fd7929190611d2f565b60006040518083038185875af1925050503d8060008114611014576040519150601f19603f3d011682016040523d82523d6000602084013e611019565b606091505b50509050806110865760405162461bcd60e51b815260206004820152603360248201527f54696d656c6f636b436f6e74726f6c6c65723a20756e6465726c79696e6720746044820152721c985b9cd858dd1a5bdb881c995d995c9d1959606a1b6064820152608401610840565b5050505050565b6110968161076b565b6110b25760405162461bcd60e51b815260040161084090611ce5565b600090815260016020819052604090912055565b6110d08282610aa2565b610853576000828152602081815260408083206001600160a01b03851684529091529020805460ff191660011790556111063390565b6001600160a01b0316816001600160a01b0316837f2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d60405160405180910390a45050565b6111548282610aa2565b15610853576000828152602081815260408083206001600160a01b0385168085529252808320805460ff1916905551339285917ff6391f5c32d9c69d2a47ea670b442974b53935d1edc7fd64eb21e047a839171b9190a45050565b60606106b06001600160a01b03831660145b606060006111d0836002611d3f565b6111db906002611c06565b6001600160401b038111156111f2576111f26114e2565b6040519080825280601f01601f19166020018201604052801561121c576020820181803683370190505b509050600360fc1b8160008151811061123757611237611a2e565b60200101906001600160f81b031916908160001a905350600f60fb1b8160018151811061126657611266611a2e565b60200101906001600160f81b031916908160001a905350600061128a846002611d3f565b611295906001611c06565b90505b600181111561130d576f181899199a1a9b1b9c1cb0b131b232b360811b85600f16601081106112c9576112c9611a2e565b1a60f81b8282815181106112df576112df611a2e565b60200101906001600160f81b031916908160001a90535060049490941c9361130681611d5e565b9050611298565b50831561078a5760405162461bcd60e51b815260206004820181905260248201527f537472696e67733a20686578206c656e67746820696e73756666696369656e746044820152606401610840565b80356001600160a01b038116811461137357600080fd5b919050565b60008083601f84011261138a57600080fd5b5081356001600160401b038111156113a157600080fd5b6020830191508360208285010111156113b957600080fd5b9250929050565b600080600080600080600060c0888a0312156113db57600080fd5b6113e48861135c565b96506020880135955060408801356001600160401b0381111561140657600080fd5b6114128a828b01611378565b989b979a50986060810135976080820135975060a09091013595509350505050565b60006020828403121561144657600080fd5b81356001600160e01b03198116811461078a57600080fd5b60008060008060008060a0878903121561147757600080fd5b6114808761135c565b95506020870135945060408701356001600160401b038111156114a257600080fd5b6114ae89828a01611378565b979a9699509760608101359660809091013595509350505050565b6000602082840312156114db57600080fd5b5035919050565b634e487b7160e01b600052604160045260246000fd5b604051601f8201601f191681016001600160401b0381118282101715611520576115206114e2565b604052919050565b600082601f83011261153957600080fd5b81356001600160401b03811115611552576115526114e2565b611565601f8201601f19166020016114f8565b81815284602083860101111561157a57600080fd5b816020850160208301376000918101602001919091529392505050565b600080600080608085870312156115ad57600080fd5b6115b68561135c565b93506115c46020860161135c565b92506040850135915060608501356001600160401b038111156115e657600080fd5b6115f287828801611528565b91505092959194509250565b6000806040838503121561161157600080fd5b823591506116216020840161135c565b90509250929050565b60008083601f84011261163c57600080fd5b5081356001600160401b0381111561165357600080fd5b6020830191508360208260051b85010111156113b957600080fd5b600080600080600080600080600060c08a8c03121561168c57600080fd5b89356001600160401b03808211156116a357600080fd5b6116af8d838e0161162a565b909b50995060208c01359150808211156116c857600080fd5b6116d48d838e0161162a565b909950975060408c01359150808211156116ed57600080fd5b506116fa8c828d0161162a565b9a9d999c50979a969997986060880135976080810135975060a0013595509350505050565b60008060008060008060008060a0898b03121561173b57600080fd5b88356001600160401b038082111561175257600080fd5b61175e8c838d0161162a565b909a50985060208b013591508082111561177757600080fd5b6117838c838d0161162a565b909850965060408b013591508082111561179c57600080fd5b506117a98b828c0161162a565b999c989b509699959896976060870135966080013595509350505050565b600082601f8301126117d857600080fd5b813560206001600160401b038211156117f3576117f36114e2565b8160051b6118028282016114f8565b928352848101820192828101908785111561181c57600080fd5b83870192505b8483101561183b57823582529183019190830190611822565b979650505050505050565b600080600080600060a0868803121561185e57600080fd5b6118678661135c565b94506118756020870161135c565b935060408601356001600160401b038082111561189157600080fd5b61189d89838a016117c7565b945060608801359150808211156118b357600080fd5b6118bf89838a016117c7565b935060808801359150808211156118d557600080fd5b506118e288828901611528565b9150509295509295909350565b600080600080600060a0868803121561190757600080fd5b6119108661135c565b945061191e6020870161135c565b9350604086013592506060860135915060808601356001600160401b0381111561194757600080fd5b6118e288828901611528565b81835281816020850137506000828201602090810191909152601f909101601f19169091010190565b60018060a01b038716815285602082015260a0604082015260006119a460a083018688611953565b60608301949094525060800152949350505050565b60018060a01b03851681528360208201526060604082015260006119e1606083018486611953565b9695505050505050565b60208082526023908201527f54696d656c6f636b436f6e74726f6c6c65723a206c656e677468206d69736d616040820152620e8c6d60eb1b606082015260800190565b634e487b7160e01b600052603260045260246000fd5b600060208284031215611a5657600080fd5b61078a8261135c565b6000808335601e19843603018112611a7657600080fd5b8301803591506001600160401b03821115611a9057600080fd5b6020019150368190038213156113b957600080fd5b634e487b7160e01b600052601160045260246000fd5b600060018201611acd57611acd611aa5565b5060010190565b81835260006020808501808196508560051b810191508460005b87811015611b585782840389528135601e19883603018112611b0f57600080fd5b870185810190356001600160401b03811115611b2a57600080fd5b803603821315611b3957600080fd5b611b44868284611953565b9a87019a9550505090840190600101611aee565b5091979650505050505050565b60a0808252810188905260008960c08301825b8b811015611ba6576001600160a01b03611b918461135c565b16825260209283019290910190600101611b78565b5083810360208501528881526001600160fb1b03891115611bc657600080fd5b8860051b9150818a60208301370182810360209081016040850152611bee9082018789611ad4565b60608401959095525050608001529695505050505050565b808201808211156106b0576106b0611aa5565b60005b83811015611c34578181015183820152602001611c1c565b50506000910152565b7f416363657373436f6e74726f6c3a206163636f756e7420000000000000000000815260008351611c75816017850160208801611c19565b7001034b99036b4b9b9b4b733903937b6329607d1b6017918401918201528351611ca6816028840160208801611c19565b01602801949350505050565b6020815260008251806020840152611cd1816040850160208701611c19565b601f01601f19169190910160400192915050565b6020808252602a908201527f54696d656c6f636b436f6e74726f6c6c65723a206f7065726174696f6e206973604082015269206e6f7420726561647960b01b606082015260800190565b8183823760009101908152919050565b6000816000190483118215151615611d5957611d59611aa5565b500290565b600081611d6d57611d6d611aa5565b50600019019056fea264697066735822122044ea1653b64c78356f5c888e44ffaa711cea3f5e0a063c933716423ae79bbfa064736f6c634300081000335f58e3a2316349923ce3780f8d587db2d72378aed66a8261c916544fa6846ca5b09aa5aeb3702cfd50b6b62bc4532604938f21248a27a1d5ca736082b6819cc1d8aa0f3194971a2a116679f7c2090f6939c8d4e01a2a8d7e41d55e5351469e63fd643c72710c63c0180259aba6b2d05451e3591a24e58b62239378085726f7830000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000cfc62c2e82da2f580fd54a2f526f65b6cc8d6de00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000" }); console2.log("Timelock deployed:", timelock); console2.log("Used salt:"); @@ -112,6 +103,7 @@ forge script script/PWNTimelock.s.sol:Deploy \ contract Setup is Deployments, Script { using GnosisSafeUtils for GnosisSafeLike; + using TimelockUtils for TimelockController; function _protocolNotDeployedOnSelectedChain() internal pure override { revert("PWNTimelock: selected chain is not set in deployments/latest.json"); @@ -159,7 +151,12 @@ forge script script/PWNTimelock.s.sol:Setup \ payloads[2] = abi.encodeWithSignature("revokeRole(bytes32,address)", timelock.PROPOSER_ROLE(), initialProposer); payloads[3] = abi.encodeWithSignature("revokeRole(bytes32,address)", timelock.CANCELLER_ROLE(), initialProposer); - _scheduleAndExecuteBatch(timelock, payloads); + address[] memory targets = new address[](payloads.length); + for (uint256 i; i < payloads.length; ++i) { + targets[i] = address(timelock); + } + + timelock.scheduleAndExecuteBatch(targets, payloads); console2.log("Proposer role granted to:", newProposer); console2.log("Cancellor role granted to:", newProposer); @@ -169,38 +166,6 @@ forge script script/PWNTimelock.s.sol:Setup \ vm.stopBroadcast(); } - function _scheduleAndExecute(TimelockController timelock, bytes memory payload) private { - timelock.schedule({ target: address(timelock), value: 0, data: payload, predecessor: 0, salt: 0, delay: 0 }); - timelock.execute({ target: address(timelock), value: 0, payload: payload, predecessor: 0, salt: 0 }); - } - - function _scheduleAndExecuteBatch(TimelockController timelock, bytes[] memory payloads) private { - address[] memory targets = new address[](payloads.length); - for (uint256 i; i < payloads.length; ++i) { - targets[i] = address(timelock); - } - uint256[] memory values = new uint256[](payloads.length); - for (uint256 i; i < payloads.length; ++i) { - values[i] = 0; - } - - timelock.scheduleBatch({ - targets: targets, - values: values, - payloads: payloads, - predecessor: 0, - salt: 0, - delay: 0 - }); - timelock.executeBatch({ - targets: targets, - values: values, - payloads: payloads, - predecessor: 0, - salt: 0 - }); - } - /* forge script script/PWNTimelock.s.sol:Setup \ @@ -210,68 +175,10 @@ forge script script/PWNTimelock.s.sol:Setup \ --with-gas-price $(cast --to-wei 15 gwei) \ --broadcast */ - /// @dev Expecting to have protocol, protocolSafe & protocolTimelock addresses set in the `deployments.json` + /// @dev Expecting to have protocol, daoSafe & protocolTimelock addresses set in the `deployments.json` function setupProtocolTimelock() external { _loadDeployedAddresses(); - - uint256 protocolTimelockMinDelay = 4 days; - - vm.startBroadcast(); - - // set PWNConfig admin - bool success; - success = GnosisSafeLike(deployment.protocolSafe).execTransaction({ - to: address(deployment.config), - data: abi.encodeWithSignature("changeAdmin(address)", deployment.protocolTimelock) - }); - require(success, "PWN: change admin failed"); - - // transfer PWNHub owner - success = GnosisSafeLike(deployment.protocolSafe).execTransaction({ - to: address(deployment.hub), - data: abi.encodeWithSignature("transferOwnership(address)", deployment.protocolTimelock) - }); - require(success, "PWN: change owner failed"); - - // accept PWNHub owner - success = GnosisSafeLike(deployment.protocolSafe).execTransaction({ - to: address(deployment.protocolTimelock), - data: abi.encodeWithSignature( - "schedule(address,uint256,bytes,bytes32,bytes32,uint256)", - address(deployment.hub), 0, abi.encodeWithSignature("acceptOwnership()"), 0, 0, 0 - ) - }); - require(success, "PWN: schedule accept ownership failed"); - - TimelockController(payable(deployment.protocolTimelock)).execute({ - target: address(deployment.hub), - value: 0, - payload: abi.encodeWithSignature("acceptOwnership()"), - predecessor: 0, - salt: 0 - }); - - // set min delay - success = GnosisSafeLike(deployment.protocolSafe).execTransaction({ - to: address(deployment.protocolTimelock), - data: abi.encodeWithSignature( - "schedule(address,uint256,bytes,bytes32,bytes32,uint256)", - address(deployment.protocolTimelock), 0, abi.encodeWithSignature("updateDelay(uint256)", protocolTimelockMinDelay), 0, 0, 0 - ) - }); - require(success, "PWN: schedule update delay failed"); - - TimelockController(payable(deployment.protocolTimelock)).execute({ - target: deployment.protocolTimelock, - value: 0, - payload: abi.encodeWithSignature("updateDelay(uint256)", protocolTimelockMinDelay), - predecessor: 0, - salt: 0 - }); - - console2.log("Protocol timelock set"); - - vm.stopBroadcast(); + _updateMinDelay(TimelockController(payable(deployment.protocolTimelock)), 4 days); } /* @@ -282,60 +189,26 @@ forge script script/PWNTimelock.s.sol:Setup \ --with-gas-price $(cast --to-wei 15 gwei) \ --broadcast */ - /// @dev Expecting to have protocol, daoSafe & adminTimelock addresses set in the `deployments.json` - /// Expecting `0x0cfC...D6de` to be a proposer for the timelock + /// @dev Expecting to have protocol, daoSafe & adminTimelock addresses set in the `deployments.json function setAdminTimelock() external { _loadDeployedAddresses(); + _updateMinDelay(TimelockController(payable(deployment.adminTimelock)), 4 days); + } - uint256 adminTimelockMinDelay = 4 days; - + /// @dev Will schedule and execute min delay update, expecting daoSafe to be proposer of the timelock. + function _updateMinDelay(TimelockController timelock, uint256 minDelay) private { vm.startBroadcast(); - // transfer PWNConfig owner - bool success; - success = GnosisSafeLike(deployment.daoSafe).execTransaction({ - to: address(deployment.config), - data: abi.encodeWithSignature("transferOwnership(address)", deployment.adminTimelock) - }); - require(success, "PWN: change owner failed"); - - // accept PWNConfig owner - success = GnosisSafeLike(deployment.daoSafe).execTransaction({ - to: address(deployment.adminTimelock), - data: abi.encodeWithSignature( - "schedule(address,uint256,bytes,bytes32,bytes32,uint256)", - address(deployment.config), 0, abi.encodeWithSignature("acceptOwnership()"), 0, 0, 0 + timelock.scheduleAndExecute( + GnosisSafeLike(deployment.daoSafe), + address(timelock), + abi.encodeWithSelector( + TimelockController.schedule.selector, + address(timelock), 0, abi.encodeWithSignature("updateDelay(uint256)", minDelay), 0, 0, 0 ) - }); - require(success, "PWN: schedule failed"); - - TimelockController(payable(deployment.adminTimelock)).execute({ - target: address(deployment.config), - value: 0, - payload: abi.encodeWithSignature("acceptOwnership()"), - predecessor: 0, - salt: 0 - }); - - // set min delay - success = GnosisSafeLike(deployment.daoSafe).execTransaction({ - to: address(deployment.adminTimelock), - data: abi.encodeWithSignature( - "schedule(address,uint256,bytes,bytes32,bytes32,uint256)", - address(deployment.adminTimelock), 0, abi.encodeWithSignature("updateDelay(uint256)", adminTimelockMinDelay), 0, 0, 0 - ) - }); - require(success, "PWN: update delay failed"); - - TimelockController(payable(deployment.adminTimelock)).execute({ - target: deployment.adminTimelock, - value: 0, - payload: abi.encodeWithSignature("updateDelay(uint256)", adminTimelockMinDelay), - predecessor: 0, - salt: 0 - }); + ); - console2.log("Product timelock set"); + console2.log("Timelock min delay updated:", minDelay); vm.stopBroadcast(); } diff --git a/script/lib/TimelockUtils.sol b/script/lib/TimelockUtils.sol new file mode 100644 index 0000000..b0da80c --- /dev/null +++ b/script/lib/TimelockUtils.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.16; + +import { TimelockController } from "openzeppelin/governance/TimelockController.sol"; + +import { GnosisSafeLike, GnosisSafeUtils } from "./GnosisSafeUtils.sol"; + + +library TimelockUtils { + using GnosisSafeUtils for GnosisSafeLike; + + function scheduleAndExecute(TimelockController timelock, address target, bytes memory payload) internal { + timelock.schedule({ target: target, value: 0, data: payload, predecessor: 0, salt: 0, delay: 0 }); + timelock.execute({ target: target, value: 0, payload: payload, predecessor: 0, salt: 0 }); + } + + function scheduleAndExecute(TimelockController timelock, GnosisSafeLike safe, address target, bytes memory payload) internal { + bool success = safe.execTransaction({ + to: address(timelock), + data: abi.encodeWithSelector(TimelockController.schedule.selector, target, 0, payload, 0, 0, 0) + }); + require(success, "Schedule failed"); + + timelock.execute({ target: target, value: 0, payload: payload, predecessor: 0, salt: 0 }); + } + + function scheduleAndExecuteBatch( + TimelockController timelock, + address[] memory targets, + bytes[] memory payloads + ) internal { + uint256[] memory values = new uint256[](payloads.length); + for (uint256 i; i < payloads.length; ++i) { + values[i] = 0; + } + + timelock.scheduleBatch({ targets: targets, values: values, payloads: payloads, predecessor: 0, salt: 0, delay: 0 }); + timelock.executeBatch({ targets: targets, values: values, payloads: payloads, predecessor: 0, salt: 0 }); + } + + function scheduleAndExecuteBatch( + TimelockController timelock, + GnosisSafeLike safe, + address[] memory targets, + bytes[] memory payloads + ) internal { + uint256[] memory values = new uint256[](payloads.length); + for (uint256 i; i < payloads.length; ++i) { + values[i] = 0; + } + + bool success = safe.execTransaction({ + to: address(timelock), + data: abi.encodeWithSelector(TimelockController.scheduleBatch.selector, targets, values, payloads, 0, 0, 0) + }); + require(success, "Schedule batch failed"); + + timelock.executeBatch({ targets: targets, values: values, payloads: payloads, predecessor: 0, salt: 0 }); + } + +} From f5b94b95d24c8d0066fd76349e2923ba3cb068f3 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Thu, 25 Apr 2024 17:33:44 +0100 Subject: [PATCH 105/129] refactor: update pwn scripts to use timelock utils --- script/PWN.s.sol | 69 ++++++++++++++++++++++-------------------------- 1 file changed, 32 insertions(+), 37 deletions(-) diff --git a/script/PWN.s.sol b/script/PWN.s.sol index 52447b1..61e5c1a 100644 --- a/script/PWN.s.sol +++ b/script/PWN.s.sol @@ -7,6 +7,7 @@ import { TransparentUpgradeableProxy, ITransparentUpgradeableProxy } from "openzeppelin/proxy/transparent/TransparentUpgradeableProxy.sol"; import { GnosisSafeLike, GnosisSafeUtils } from "./lib/GnosisSafeUtils.sol"; +import { TimelockController, TimelockUtils } from "./lib/TimelockUtils.sol"; import { Deployments, @@ -97,7 +98,7 @@ forge script script/PWN.s.sol:Deploy \ --verify --etherscan-api-key $ETHERSCAN_API_KEY \ --broadcast */ - /// @dev Expecting to have deployer, deployerSafe, protocolSafe, daoSafe, hub & LOAN token + /// @dev Expecting to have deployer, deployerSafe, adminTimelock, protocolTimelock, daoSafe, hub & LOAN token /// addresses set in the `deployments/latest.json`. function deployNewProtocolVersion() external { _loadDeployedAddresses(); @@ -252,7 +253,7 @@ forge script script/PWN.s.sol:Deploy \ --verify --etherscan-api-key $ETHERSCAN_API_KEY \ --broadcast */ - /// @dev Expecting to have deployer, deployerSafe, protocolSafe, daoSafe & categoryRegistry + /// @dev Expecting to have deployer, deployerSafe, adminTimelock, protocolTimelock & daoSafe /// addresses set in the `deployments/latest.json`. function deployProtocol() external { _loadDeployedAddresses(); @@ -417,6 +418,7 @@ forge script script/PWN.s.sol:Deploy \ contract Setup is Deployments, Script { using GnosisSafeUtils for GnosisSafeLike; + using TimelockUtils for TimelockController; function _protocolNotDeployedOnSelectedChain() internal pure override { revert("PWN: selected chain is not set in deployments/latest.json"); @@ -425,24 +427,21 @@ contract Setup is Deployments, Script { /* forge script script/PWN.s.sol:Setup \ --sig "setupNewProtocolVersion()" \ ---rpc-url $RPC_URL \ ---private-key $PRIVATE_KEY \ ---with-gas-price $(cast --to-wei 15 gwei) \ +--rpc-url $TENDERLY_URL \ +--private-key $PRIVATE_KEY_PWN_SHARED_DEV \ --broadcast */ /// @dev Expecting to have protocol addresses set in the `deployments/latest.json` - /// Can be used only in fork tests, because protocol safe has threshold >1 and hub is owner by a timelock. - /// To set safes threshold to 1 use: cast rpc -r $TENDERLY_URL tenderly_setStorageAt {safe_address} 0x0000000000000000000000000000000000000000000000000000000000000004 0x0000000000000000000000000000000000000000000000000000000000000001 - /// To set hubs owner to protocol safe use: cast rpc -r $TENDERLY_URL tenderly_setStorageAt {hub_address} 0x0000000000000000000000000000000000000000000000000000000000000000 {protocol_safe_addr_to_32} + /// Can be used only in fork tests, because safe has threshold >1 and hub is owner by a timelock. function setupNewProtocolVersion() external { _loadDeployedAddresses(); - require(address(deployment.protocolSafe) != address(0), "Protocol safe not set"); + require(address(deployment.daoSafe) != address(0), "Protocol safe not set"); require(address(deployment.categoryRegistry) != address(0), "Category registry not set"); vm.startBroadcast(); - _acceptOwnership(deployment.protocolSafe, address(deployment.categoryRegistry)); + _acceptOwnership(deployment.daoSafe, deployment.protocolTimelock, address(deployment.categoryRegistry)); _setTags(); vm.stopBroadcast(); @@ -460,25 +459,25 @@ forge script script/PWN.s.sol:Setup \ function setupProtocol() external { _loadDeployedAddresses(); - require(address(deployment.protocolSafe) != address(0), "Protocol safe not set"); + require(address(deployment.daoSafe) != address(0), "Protocol safe not set"); + require(address(deployment.categoryRegistry) != address(0), "Category registry not set"); require(address(deployment.hub) != address(0), "Hub not set"); vm.startBroadcast(); - _acceptOwnership(deployment.protocolSafe, address(deployment.categoryRegistry)); - _acceptOwnership(deployment.protocolSafe, address(deployment.hub)); + _acceptOwnership(deployment.daoSafe, deployment.protocolTimelock, address(deployment.categoryRegistry)); + _acceptOwnership(deployment.daoSafe, deployment.protocolTimelock, address(deployment.hub)); _setTags(); vm.stopBroadcast(); } - function _acceptOwnership(address safe, address contract_) internal { - bool success = GnosisSafeLike(safe).execTransaction({ - to: contract_, - data: abi.encodeWithSignature("acceptOwnership()") - }); - - require(success, "Accept ownership tx failed"); + function _acceptOwnership(address safe, address timelock, address contract_) internal { + TimelockController(payable(timelock)).scheduleAndExecute( + GnosisSafeLike(safe), + contract_, + abi.encodeWithSignature("acceptOwnership()") + ); console2.log("Accept ownership tx succeeded"); } @@ -488,7 +487,8 @@ forge script script/PWN.s.sol:Setup \ require(address(deployment.simpleLoanListProposal) != address(0), "Simple loan list proposal not set"); require(address(deployment.simpleLoanFungibleProposal) != address(0), "Simple loan fungible proposal not set"); require(address(deployment.simpleLoanDutchAuctionProposal) != address(0), "Simple loan dutch auctin proposal not set"); - require(address(deployment.protocolSafe) != address(0), "Protocol safe not set"); + require(address(deployment.protocolTimelock) != address(0), "Protocol timelock not set"); + require(address(deployment.daoSafe) != address(0), "DAO safe not set"); require(address(deployment.hub) != address(0), "Hub not set"); address[] memory addrs = new address[](10); @@ -523,14 +523,11 @@ forge script script/PWN.s.sol:Setup \ tags[8] = PWNHubTags.LOAN_PROPOSAL; tags[9] = PWNHubTags.NONCE_MANAGER; - bool success = GnosisSafeLike(deployment.protocolSafe).execTransaction({ - to: address(deployment.hub), - data: abi.encodeWithSignature( - "setTags(address[],bytes32[],bool)", addrs, tags, true - ) - }); - - require(success, "Tags set failed"); + TimelockController(payable(deployment.protocolTimelock)).scheduleAndExecute( + GnosisSafeLike(deployment.daoSafe), + address(deployment.hub), + abi.encodeWithSignature("setTags(address[],bytes32[],bool)", addrs, tags, true) + ); console2.log("Tags set succeeded"); } @@ -547,18 +544,16 @@ forge script script/PWN.s.sol:Setup \ _loadDeployedAddresses(); require(address(deployment.daoSafe) != address(0), "DAO safe not set"); + require(address(deployment.protocolTimelock) != address(0), "Protocol timelock not set"); require(address(deployment.config) != address(0), "Config not set"); vm.startBroadcast(); - bool success = GnosisSafeLike(deployment.daoSafe).execTransaction({ - to: address(deployment.config), - data: abi.encodeWithSignature( - "setLoanMetadataUri(address,string)", address_, metadata - ) - }); - - require(success, "Set metadata failed"); + TimelockController(payable(deployment.protocolTimelock)).scheduleAndExecute( + GnosisSafeLike(deployment.daoSafe), + address(deployment.config), + abi.encodeWithSignature("setLoanMetadataUri(address,string)", address_, metadata) + ); console2.log("Metadata set:", metadata); vm.stopBroadcast(); From 754962857367c7171f0686ffbde506a4df00b104 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Thu, 25 Apr 2024 17:34:15 +0100 Subject: [PATCH 106/129] redeploy to Tenderly Sepolia fork --- deployments/latest.json | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/deployments/latest.json b/deployments/latest.json index 83eed2c..915ab33 100644 --- a/deployments/latest.json +++ b/deployments/latest.json @@ -2,24 +2,23 @@ "deployedChains": [162314], "chains": { "162314": { - "dao": "0x1B8383D2726E7e18189205337424a2631A2102F4", + "dao": "0x0000000000000000000000000000000000000000", "adminTimelock": "0xd57e72A328AB1deC6b374c2babe2dc489819B5Ea", "protocolTimelock": "0xd8dbdDf1c0FDdf9b5eCFA5C067C38DB66739FBAB", - "protocolSafe": "0x61a77B19b7F4dB82222625D7a969698894d77473", - "daoSafe": "0xd56635c0E91D31F88B89F195D3993a9e34516e59", - "deployerSafe": "0x42Cad20c964067f8e8b5c3E13fd0aa3C20a964C4", + "daoSafe": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", + "deployerSafe": "0x1B4B37738De3bb9E6a7a4f99aFe4C145734c071d", "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", - "categoryRegistry": "0x0c93666Bf6359951Ade361D6E19f2DB240dA392f", - "configSingleton": "0xD1f71974aAdc34FcC66C992E0A09b1E64D4C3E33", - "config": "0x9C432a1A229ef87b138da29dE930B3d8EC2C67Fa", + "categoryRegistry": "0x1b71A2a08A6fb54b5a8615A48b13447a7b1E891E", + "configSingleton": "0xBdac2fb31E493f92370ED5ECb3EA63e63ae32617", + "config": "0x4ca21fD91F7F6446594E0ff93dD3147fbC2ca7Bb", "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", - "revokedNonce": "0x870A65689f4AF5ecb05064D5A5C601274602d313", - "simpleLoan": "0x30f8D71f79B785A72b621DeD5665387CcE4f910C", - "simpleLoanSimpleProposal": "0x33F5b26ede842322E60433ec6e32F547cEA90A5C", - "simpleLoanListProposal": "0x94dA12676b3909BeeFbF01F66F3DFC824D67DBB5", - "simpleLoanFungibleProposal": "0x1C3911Ef03dEBeFc9b8567Ec4Dc1766F1C7Cf3f1", - "simpleLoanDutchAuctionProposal": "0x2850CB3D78389D1A5aa485EcD8D6472Fe1Dd6fa4" + "revokedNonce": "0xBAabf06494A2b1407AA740Ae7B04aF51D1Fd0d5F", + "simpleLoan": "0x18E6EeF41ef2D6B9ab9BC5A02f82E447473D0436", + "simpleLoanSimpleProposal": "0x038F9929CfD6af748EFE5384C4F29f67337F6887", + "simpleLoanListProposal": "0x1b02D009cd20e1EA9A0a9a9d37b03b0fe74Db5C6", + "simpleLoanFungibleProposal": "0xc53ec196368e8767576f80787A1924b87A101d3F", + "simpleLoanDutchAuctionProposal": "0x85670ed4a10522C0c152a34cd285c298Ac18bd55" } } } From 232ad6943802c3603b1de888040d8851d69bb036 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Fri, 26 Apr 2024 13:03:24 +0100 Subject: [PATCH 107/129] style: update timelock script names --- script/PWNTimelock.s.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/script/PWNTimelock.s.sol b/script/PWNTimelock.s.sol index 1869f01..cd3268e 100644 --- a/script/PWNTimelock.s.sol +++ b/script/PWNTimelock.s.sol @@ -169,28 +169,28 @@ forge script script/PWNTimelock.s.sol:Setup \ /* forge script script/PWNTimelock.s.sol:Setup \ ---sig "setupProtocolTimelock()" \ +--sig "updateProtocolTimelockMinDelay()" \ --rpc-url $RPC_URL \ --private-key $PRIVATE_KEY \ --with-gas-price $(cast --to-wei 15 gwei) \ --broadcast */ /// @dev Expecting to have protocol, daoSafe & protocolTimelock addresses set in the `deployments.json` - function setupProtocolTimelock() external { + function updateProtocolTimelockMinDelay() external { _loadDeployedAddresses(); _updateMinDelay(TimelockController(payable(deployment.protocolTimelock)), 4 days); } /* forge script script/PWNTimelock.s.sol:Setup \ ---sig "setAdminTimelock()" \ +--sig "updateAdminTimelockMinDelay()" \ --rpc-url $RPC_URL \ --private-key $PRIVATE_KEY \ --with-gas-price $(cast --to-wei 15 gwei) \ --broadcast */ /// @dev Expecting to have protocol, daoSafe & adminTimelock addresses set in the `deployments.json - function setAdminTimelock() external { + function updateAdminTimelockMinDelay() external { _loadDeployedAddresses(); _updateMinDelay(TimelockController(payable(deployment.adminTimelock)), 4 days); } From 91071d9ccc72acda95a4d389bc195c6e3f59f942 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Fri, 26 Apr 2024 16:25:04 +0100 Subject: [PATCH 108/129] refactor: set default metadata in script --- script/PWN.s.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/script/PWN.s.sol b/script/PWN.s.sol index 61e5c1a..8c67812 100644 --- a/script/PWN.s.sol +++ b/script/PWN.s.sol @@ -428,7 +428,7 @@ contract Setup is Deployments, Script { forge script script/PWN.s.sol:Setup \ --sig "setupNewProtocolVersion()" \ --rpc-url $TENDERLY_URL \ ---private-key $PRIVATE_KEY_PWN_SHARED_DEV \ +--private-key $PRIVATE_KEY \ --broadcast */ /// @dev Expecting to have protocol addresses set in the `deployments/latest.json` @@ -533,14 +533,14 @@ forge script script/PWN.s.sol:Setup \ /* forge script script/PWN.s.sol:Setup \ ---sig "setMetadata(address,string)" $LOAN_CONTRACT $METADATA \ +--sig "setDefaultMetadata(string)" $METADATA \ --rpc-url $RPC_URL \ --private-key $PRIVATE_KEY \ --with-gas-price $(cast --to-wei 15 gwei) \ --broadcast */ /// @dev Expecting to have daoSafe & config addresses set in the `deployments/latest.json` - function setMetadata(address address_, string memory metadata) external { + function setDefaultMetadata(string memory metadata) external { _loadDeployedAddresses(); require(address(deployment.daoSafe) != address(0), "DAO safe not set"); @@ -552,7 +552,7 @@ forge script script/PWN.s.sol:Setup \ TimelockController(payable(deployment.protocolTimelock)).scheduleAndExecute( GnosisSafeLike(deployment.daoSafe), address(deployment.config), - abi.encodeWithSignature("setLoanMetadataUri(address,string)", address_, metadata) + abi.encodeWithSignature("setDefaultLOANMetadataUri(string)", metadata) ); console2.log("Metadata set:", metadata); From e732e53d8029c0a01a7906a5be987a5af60cbcd5 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Mon, 29 Apr 2024 11:44:04 +0100 Subject: [PATCH 109/129] refactor: merge timelock deployment scripts --- script/PWNTimelock.s.sol | 87 ++++++++++++---------------------------- 1 file changed, 26 insertions(+), 61 deletions(-) diff --git a/script/PWNTimelock.s.sol b/script/PWNTimelock.s.sol index cd3268e..7ea2102 100644 --- a/script/PWNTimelock.s.sol +++ b/script/PWNTimelock.s.sol @@ -33,6 +33,7 @@ library PWNDeployerSalt { contract Deploy is Deployments, Script { using GnosisSafeUtils for GnosisSafeLike; + using TimelockUtils for TimelockController; function _protocolNotDeployedOnSelectedChain() internal pure override { revert("PWNTimelock: selected chain is not set in deployments/latest.json"); @@ -54,28 +55,16 @@ contract Deploy is Deployments, Script { /* forge script script/PWNTimelock.s.sol:Deploy \ ---sig "deployProtocolTimelock()" \ +--sig "deployTimelocks()" \ --rpc-url $RPC_URL \ --private-key $PRIVATE_KEY \ --with-gas-price $(cast --to-wei 15 gwei) \ ---verify --etherscan-api-key $ETHERSCAN_API_KEY \ --broadcast */ - function deployProtocolTimelock() external { + function deployTimelocks() external { console2.log("Deploying protocol timelock"); _deployTimelock(PWNDeployerSalt.PROTOCOL_TIMELOCK); - } -/* -forge script script/PWNTimelock.s.sol:Deploy \ ---sig "deployAdminTimelock()" \ ---rpc-url $RPC_URL \ ---private-key $PRIVATE_KEY \ ---with-gas-price $(cast --to-wei 15 gwei) \ ---verify --etherscan-api-key $ETHERSCAN_API_KEY \ ---broadcast -*/ - function deployAdminTimelock() external { console2.log("Deploying admin timelock"); _deployTimelock(PWNDeployerSalt.ADMIN_TIMELOCK); } @@ -85,65 +74,31 @@ forge script script/PWNTimelock.s.sol:Deploy \ function _deployTimelock(bytes32 salt) private { _loadDeployedAddresses(); + // Deploy new timelock with `0x0cfC...D6de` proposer + vm.startBroadcast(); - address timelock = _deploy({ + address _timelock = _deploy({ salt: salt, bytecode: hex"60806040523480156200001157600080fd5b506040516200230838038062002308833981016040819052620000349162000408565b6200004f60008051602062002288833981519152806200022d565b62000079600080516020620022a8833981519152600080516020620022888339815191526200022d565b620000a3600080516020620022c8833981519152600080516020620022888339815191526200022d565b620000cd600080516020620022e8833981519152600080516020620022888339815191526200022d565b620000e8600080516020620022888339815191523062000278565b6001600160a01b03811615620001135762000113600080516020620022888339815191528262000278565b60005b835181101562000199576200015d600080516020620022a88339815191528583815181106200014957620001496200048f565b60200260200101516200027860201b60201c565b62000186600080516020620022e88339815191528583815181106200014957620001496200048f565b6200019181620004a5565b905062000116565b5060005b8251811015620001e357620001d0600080516020620022c88339815191528483815181106200014957620001496200048f565b620001db81620004a5565b90506200019d565b5060028490556040805160008152602081018690527f11c24f4ead16507c69ac467fbd5e4eed5fb5c699626d2cc6d66421df253886d5910160405180910390a150505050620004cd565b600082815260208190526040808220600101805490849055905190918391839186917fbd79b86ffe0ab8e8776151514217cd7cacd52c909f66475c3af44e129f0b00ff9190a4505050565b62000284828262000288565b5050565b6000828152602081815260408083206001600160a01b038516845290915290205460ff1662000284576000828152602081815260408083206001600160a01b03851684529091529020805460ff19166001179055620002e43390565b6001600160a01b0316816001600160a01b0316837f2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d60405160405180910390a45050565b634e487b7160e01b600052604160045260246000fd5b80516001600160a01b03811681146200035657600080fd5b919050565b600082601f8301126200036d57600080fd5b815160206001600160401b03808311156200038c576200038c62000328565b8260051b604051601f19603f83011681018181108482111715620003b457620003b462000328565b604052938452858101830193838101925087851115620003d357600080fd5b83870191505b84821015620003fd57620003ed826200033e565b83529183019190830190620003d9565b979650505050505050565b600080600080608085870312156200041f57600080fd5b845160208601519094506001600160401b03808211156200043f57600080fd5b6200044d888389016200035b565b945060408701519150808211156200046457600080fd5b5062000473878288016200035b565b92505062000484606086016200033e565b905092959194509250565b634e487b7160e01b600052603260045260246000fd5b600060018201620004c657634e487b7160e01b600052601160045260246000fd5b5060010190565b611dab80620004dd6000396000f3fe6080604052600436106101bb5760003560e01c80638065657f116100ec578063bc197c811161008a578063d547741f11610064578063d547741f14610582578063e38335e5146105a2578063f23a6e61146105b5578063f27a0c92146105e157600080fd5b8063bc197c8114610509578063c4d252f514610535578063d45c44351461055557600080fd5b806391d14854116100c657806391d1485414610480578063a217fddf146104a0578063b08e51c0146104b5578063b1c5f427146104e957600080fd5b80638065657f1461040c5780638f2a0bb01461042c5780638f61f4f51461044c57600080fd5b8063248a9ca31161015957806331d507501161013357806331d507501461038c57806336568abe146103ac578063584b153e146103cc57806364d62353146103ec57600080fd5b8063248a9ca31461030b5780632ab0f5291461033b5780632f2ff15d1461036c57600080fd5b80630d3cf6fc116101955780630d3cf6fc14610260578063134008d31461029457806313bc9f20146102a7578063150b7a02146102c757600080fd5b806301d5062a146101c757806301ffc9a7146101e957806307bd02651461021e57600080fd5b366101c257005b600080fd5b3480156101d357600080fd5b506101e76101e23660046113c0565b6105f6565b005b3480156101f557600080fd5b50610209610204366004611434565b61068b565b60405190151581526020015b60405180910390f35b34801561022a57600080fd5b506102527fd8aa0f3194971a2a116679f7c2090f6939c8d4e01a2a8d7e41d55e5351469e6381565b604051908152602001610215565b34801561026c57600080fd5b506102527f5f58e3a2316349923ce3780f8d587db2d72378aed66a8261c916544fa6846ca581565b6101e76102a236600461145e565b6106b6565b3480156102b357600080fd5b506102096102c23660046114c9565b61076b565b3480156102d357600080fd5b506102f26102e2366004611597565b630a85bd0160e11b949350505050565b6040516001600160e01b03199091168152602001610215565b34801561031757600080fd5b506102526103263660046114c9565b60009081526020819052604090206001015490565b34801561034757600080fd5b506102096103563660046114c9565b6000908152600160208190526040909120541490565b34801561037857600080fd5b506101e76103873660046115fe565b610791565b34801561039857600080fd5b506102096103a73660046114c9565b6107bb565b3480156103b857600080fd5b506101e76103c73660046115fe565b6107d4565b3480156103d857600080fd5b506102096103e73660046114c9565b610857565b3480156103f857600080fd5b506101e76104073660046114c9565b61086d565b34801561041857600080fd5b5061025261042736600461145e565b610911565b34801561043857600080fd5b506101e761044736600461166e565b610950565b34801561045857600080fd5b506102527fb09aa5aeb3702cfd50b6b62bc4532604938f21248a27a1d5ca736082b6819cc181565b34801561048c57600080fd5b5061020961049b3660046115fe565b610aa2565b3480156104ac57600080fd5b50610252600081565b3480156104c157600080fd5b506102527ffd643c72710c63c0180259aba6b2d05451e3591a24e58b62239378085726f78381565b3480156104f557600080fd5b5061025261050436600461171f565b610acb565b34801561051557600080fd5b506102f2610524366004611846565b63bc197c8160e01b95945050505050565b34801561054157600080fd5b506101e76105503660046114c9565b610b10565b34801561056157600080fd5b506102526105703660046114c9565b60009081526001602052604090205490565b34801561058e57600080fd5b506101e761059d3660046115fe565b610be5565b6101e76105b036600461171f565b610c0a565b3480156105c157600080fd5b506102f26105d03660046118ef565b63f23a6e6160e01b95945050505050565b3480156105ed57600080fd5b50600254610252565b7fb09aa5aeb3702cfd50b6b62bc4532604938f21248a27a1d5ca736082b6819cc161062081610d94565b6000610630898989898989610911565b905061063c8184610da1565b6000817f4cf4410cc57040e44862ef0f45f3dd5a5e02db8eb8add648d4b0e236f1d07dca8b8b8b8b8b8a6040516106789695949392919061197c565b60405180910390a3505050505050505050565b60006001600160e01b03198216630271189760e51b14806106b057506106b082610e90565b92915050565b7fd8aa0f3194971a2a116679f7c2090f6939c8d4e01a2a8d7e41d55e5351469e636106e2816000610aa2565b6106f0576106f08133610ec5565b6000610700888888888888610911565b905061070c8185610f1e565b61071888888888610fba565b6000817fc2617efa69bab66782fa219543714338489c4e9e178271560a91b82c3f612b588a8a8a8a60405161075094939291906119b9565b60405180910390a36107618161108d565b5050505050505050565b60008181526001602052604081205460018111801561078a5750428111155b9392505050565b6000828152602081905260409020600101546107ac81610d94565b6107b683836110c6565b505050565b60008181526001602052604081205481905b1192915050565b6001600160a01b03811633146108495760405162461bcd60e51b815260206004820152602f60248201527f416363657373436f6e74726f6c3a2063616e206f6e6c792072656e6f756e636560448201526e103937b632b9903337b91039b2b63360891b60648201526084015b60405180910390fd5b610853828261114a565b5050565b60008181526001602081905260408220546107cd565b3330146108d05760405162461bcd60e51b815260206004820152602b60248201527f54696d656c6f636b436f6e74726f6c6c65723a2063616c6c6572206d7573742060448201526a62652074696d656c6f636b60a81b6064820152608401610840565b60025460408051918252602082018390527f11c24f4ead16507c69ac467fbd5e4eed5fb5c699626d2cc6d66421df253886d5910160405180910390a1600255565b600086868686868660405160200161092e9695949392919061197c565b6040516020818303038152906040528051906020012090509695505050505050565b7fb09aa5aeb3702cfd50b6b62bc4532604938f21248a27a1d5ca736082b6819cc161097a81610d94565b8887146109995760405162461bcd60e51b8152600401610840906119eb565b8885146109b85760405162461bcd60e51b8152600401610840906119eb565b60006109ca8b8b8b8b8b8b8b8b610acb565b90506109d68184610da1565b60005b8a811015610a945780827f4cf4410cc57040e44862ef0f45f3dd5a5e02db8eb8add648d4b0e236f1d07dca8e8e85818110610a1657610a16611a2e565b9050602002016020810190610a2b9190611a44565b8d8d86818110610a3d57610a3d611a2e565b905060200201358c8c87818110610a5657610a56611a2e565b9050602002810190610a689190611a5f565b8c8b604051610a7c9695949392919061197c565b60405180910390a3610a8d81611abb565b90506109d9565b505050505050505050505050565b6000918252602082815260408084206001600160a01b0393909316845291905290205460ff1690565b60008888888888888888604051602001610aec989796959493929190611b65565b60405160208183030381529060405280519060200120905098975050505050505050565b7ffd643c72710c63c0180259aba6b2d05451e3591a24e58b62239378085726f783610b3a81610d94565b610b4382610857565b610ba95760405162461bcd60e51b815260206004820152603160248201527f54696d656c6f636b436f6e74726f6c6c65723a206f7065726174696f6e2063616044820152701b9b9bdd0818994818d85b98d95b1b1959607a1b6064820152608401610840565b6000828152600160205260408082208290555183917fbaa1eb22f2a492ba1a5fea61b8df4d27c6c8b5f3971e63bb58fa14ff72eedb7091a25050565b600082815260208190526040902060010154610c0081610d94565b6107b6838361114a565b7fd8aa0f3194971a2a116679f7c2090f6939c8d4e01a2a8d7e41d55e5351469e63610c36816000610aa2565b610c4457610c448133610ec5565b878614610c635760405162461bcd60e51b8152600401610840906119eb565b878414610c825760405162461bcd60e51b8152600401610840906119eb565b6000610c948a8a8a8a8a8a8a8a610acb565b9050610ca08185610f1e565b60005b89811015610d7e5760008b8b83818110610cbf57610cbf611a2e565b9050602002016020810190610cd49190611a44565b905060008a8a84818110610cea57610cea611a2e565b9050602002013590503660008a8a86818110610d0857610d08611a2e565b9050602002810190610d1a9190611a5f565b91509150610d2a84848484610fba565b84867fc2617efa69bab66782fa219543714338489c4e9e178271560a91b82c3f612b5886868686604051610d6194939291906119b9565b60405180910390a35050505080610d7790611abb565b9050610ca3565b50610d888161108d565b50505050505050505050565b610d9e8133610ec5565b50565b610daa826107bb565b15610e0f5760405162461bcd60e51b815260206004820152602f60248201527f54696d656c6f636b436f6e74726f6c6c65723a206f7065726174696f6e20616c60448201526e1c9958591e481cd8da19591d5b1959608a1b6064820152608401610840565b600254811015610e705760405162461bcd60e51b815260206004820152602660248201527f54696d656c6f636b436f6e74726f6c6c65723a20696e73756666696369656e746044820152652064656c617960d01b6064820152608401610840565b610e7a8142611c06565b6000928352600160205260409092209190915550565b60006001600160e01b03198216637965db0b60e01b14806106b057506301ffc9a760e01b6001600160e01b03198316146106b0565b610ecf8282610aa2565b61085357610edc816111af565b610ee78360206111c1565b604051602001610ef8929190611c3d565b60408051601f198184030181529082905262461bcd60e51b825261084091600401611cb2565b610f278261076b565b610f435760405162461bcd60e51b815260040161084090611ce5565b801580610f5f5750600081815260016020819052604090912054145b6108535760405162461bcd60e51b815260206004820152602660248201527f54696d656c6f636b436f6e74726f6c6c65723a206d697373696e6720646570656044820152656e64656e637960d01b6064820152608401610840565b6000846001600160a01b0316848484604051610fd7929190611d2f565b60006040518083038185875af1925050503d8060008114611014576040519150601f19603f3d011682016040523d82523d6000602084013e611019565b606091505b50509050806110865760405162461bcd60e51b815260206004820152603360248201527f54696d656c6f636b436f6e74726f6c6c65723a20756e6465726c79696e6720746044820152721c985b9cd858dd1a5bdb881c995d995c9d1959606a1b6064820152608401610840565b5050505050565b6110968161076b565b6110b25760405162461bcd60e51b815260040161084090611ce5565b600090815260016020819052604090912055565b6110d08282610aa2565b610853576000828152602081815260408083206001600160a01b03851684529091529020805460ff191660011790556111063390565b6001600160a01b0316816001600160a01b0316837f2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d60405160405180910390a45050565b6111548282610aa2565b15610853576000828152602081815260408083206001600160a01b0385168085529252808320805460ff1916905551339285917ff6391f5c32d9c69d2a47ea670b442974b53935d1edc7fd64eb21e047a839171b9190a45050565b60606106b06001600160a01b03831660145b606060006111d0836002611d3f565b6111db906002611c06565b6001600160401b038111156111f2576111f26114e2565b6040519080825280601f01601f19166020018201604052801561121c576020820181803683370190505b509050600360fc1b8160008151811061123757611237611a2e565b60200101906001600160f81b031916908160001a905350600f60fb1b8160018151811061126657611266611a2e565b60200101906001600160f81b031916908160001a905350600061128a846002611d3f565b611295906001611c06565b90505b600181111561130d576f181899199a1a9b1b9c1cb0b131b232b360811b85600f16601081106112c9576112c9611a2e565b1a60f81b8282815181106112df576112df611a2e565b60200101906001600160f81b031916908160001a90535060049490941c9361130681611d5e565b9050611298565b50831561078a5760405162461bcd60e51b815260206004820181905260248201527f537472696e67733a20686578206c656e67746820696e73756666696369656e746044820152606401610840565b80356001600160a01b038116811461137357600080fd5b919050565b60008083601f84011261138a57600080fd5b5081356001600160401b038111156113a157600080fd5b6020830191508360208285010111156113b957600080fd5b9250929050565b600080600080600080600060c0888a0312156113db57600080fd5b6113e48861135c565b96506020880135955060408801356001600160401b0381111561140657600080fd5b6114128a828b01611378565b989b979a50986060810135976080820135975060a09091013595509350505050565b60006020828403121561144657600080fd5b81356001600160e01b03198116811461078a57600080fd5b60008060008060008060a0878903121561147757600080fd5b6114808761135c565b95506020870135945060408701356001600160401b038111156114a257600080fd5b6114ae89828a01611378565b979a9699509760608101359660809091013595509350505050565b6000602082840312156114db57600080fd5b5035919050565b634e487b7160e01b600052604160045260246000fd5b604051601f8201601f191681016001600160401b0381118282101715611520576115206114e2565b604052919050565b600082601f83011261153957600080fd5b81356001600160401b03811115611552576115526114e2565b611565601f8201601f19166020016114f8565b81815284602083860101111561157a57600080fd5b816020850160208301376000918101602001919091529392505050565b600080600080608085870312156115ad57600080fd5b6115b68561135c565b93506115c46020860161135c565b92506040850135915060608501356001600160401b038111156115e657600080fd5b6115f287828801611528565b91505092959194509250565b6000806040838503121561161157600080fd5b823591506116216020840161135c565b90509250929050565b60008083601f84011261163c57600080fd5b5081356001600160401b0381111561165357600080fd5b6020830191508360208260051b85010111156113b957600080fd5b600080600080600080600080600060c08a8c03121561168c57600080fd5b89356001600160401b03808211156116a357600080fd5b6116af8d838e0161162a565b909b50995060208c01359150808211156116c857600080fd5b6116d48d838e0161162a565b909950975060408c01359150808211156116ed57600080fd5b506116fa8c828d0161162a565b9a9d999c50979a969997986060880135976080810135975060a0013595509350505050565b60008060008060008060008060a0898b03121561173b57600080fd5b88356001600160401b038082111561175257600080fd5b61175e8c838d0161162a565b909a50985060208b013591508082111561177757600080fd5b6117838c838d0161162a565b909850965060408b013591508082111561179c57600080fd5b506117a98b828c0161162a565b999c989b509699959896976060870135966080013595509350505050565b600082601f8301126117d857600080fd5b813560206001600160401b038211156117f3576117f36114e2565b8160051b6118028282016114f8565b928352848101820192828101908785111561181c57600080fd5b83870192505b8483101561183b57823582529183019190830190611822565b979650505050505050565b600080600080600060a0868803121561185e57600080fd5b6118678661135c565b94506118756020870161135c565b935060408601356001600160401b038082111561189157600080fd5b61189d89838a016117c7565b945060608801359150808211156118b357600080fd5b6118bf89838a016117c7565b935060808801359150808211156118d557600080fd5b506118e288828901611528565b9150509295509295909350565b600080600080600060a0868803121561190757600080fd5b6119108661135c565b945061191e6020870161135c565b9350604086013592506060860135915060808601356001600160401b0381111561194757600080fd5b6118e288828901611528565b81835281816020850137506000828201602090810191909152601f909101601f19169091010190565b60018060a01b038716815285602082015260a0604082015260006119a460a083018688611953565b60608301949094525060800152949350505050565b60018060a01b03851681528360208201526060604082015260006119e1606083018486611953565b9695505050505050565b60208082526023908201527f54696d656c6f636b436f6e74726f6c6c65723a206c656e677468206d69736d616040820152620e8c6d60eb1b606082015260800190565b634e487b7160e01b600052603260045260246000fd5b600060208284031215611a5657600080fd5b61078a8261135c565b6000808335601e19843603018112611a7657600080fd5b8301803591506001600160401b03821115611a9057600080fd5b6020019150368190038213156113b957600080fd5b634e487b7160e01b600052601160045260246000fd5b600060018201611acd57611acd611aa5565b5060010190565b81835260006020808501808196508560051b810191508460005b87811015611b585782840389528135601e19883603018112611b0f57600080fd5b870185810190356001600160401b03811115611b2a57600080fd5b803603821315611b3957600080fd5b611b44868284611953565b9a87019a9550505090840190600101611aee565b5091979650505050505050565b60a0808252810188905260008960c08301825b8b811015611ba6576001600160a01b03611b918461135c565b16825260209283019290910190600101611b78565b5083810360208501528881526001600160fb1b03891115611bc657600080fd5b8860051b9150818a60208301370182810360209081016040850152611bee9082018789611ad4565b60608401959095525050608001529695505050505050565b808201808211156106b0576106b0611aa5565b60005b83811015611c34578181015183820152602001611c1c565b50506000910152565b7f416363657373436f6e74726f6c3a206163636f756e7420000000000000000000815260008351611c75816017850160208801611c19565b7001034b99036b4b9b9b4b733903937b6329607d1b6017918401918201528351611ca6816028840160208801611c19565b01602801949350505050565b6020815260008251806020840152611cd1816040850160208701611c19565b601f01601f19169190910160400192915050565b6020808252602a908201527f54696d656c6f636b436f6e74726f6c6c65723a206f7065726174696f6e206973604082015269206e6f7420726561647960b01b606082015260800190565b8183823760009101908152919050565b6000816000190483118215151615611d5957611d59611aa5565b500290565b600081611d6d57611d6d611aa5565b50600019019056fea264697066735822122044ea1653b64c78356f5c888e44ffaa711cea3f5e0a063c933716423ae79bbfa064736f6c634300081000335f58e3a2316349923ce3780f8d587db2d72378aed66a8261c916544fa6846ca5b09aa5aeb3702cfd50b6b62bc4532604938f21248a27a1d5ca736082b6819cc1d8aa0f3194971a2a116679f7c2090f6939c8d4e01a2a8d7e41d55e5351469e63fd643c72710c63c0180259aba6b2d05451e3591a24e58b62239378085726f7830000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000cfc62c2e82da2f580fd54a2f526f65b6cc8d6de00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000" }); - console2.log("Timelock deployed:", timelock); + console2.log("Timelock deployed:", _timelock); console2.log("Used salt:"); console2.logBytes32(salt); vm.stopBroadcast(); - } -} + // Update proposer to daoSafe + console2.log("Updating timelock proposer (%s)", _timelock); -contract Setup is Deployments, Script { - using GnosisSafeUtils for GnosisSafeLike; - using TimelockUtils for TimelockController; + TimelockController timelock = TimelockController(payable(_timelock)); + uint256 initialConfigHelper = vmSafe.envUint("INITIAL_CONFIG_HELPER"); - function _protocolNotDeployedOnSelectedChain() internal pure override { - revert("PWNTimelock: selected chain is not set in deployments/latest.json"); - } + vm.startBroadcast(initialConfigHelper); -/* -forge script script/PWNTimelock.s.sol:Setup \ ---sig "updateProtocolTimelockProposer()" \ ---rpc-url $RPC_URL \ ---private-key $PRIVATE_KEY \ ---with-gas-price $(cast --to-wei 15 gwei) \ ---broadcast -*/ - function updateProtocolTimelockProposer() external { - _loadDeployedAddresses(); - console2.log("Updating protocol timelock proposer (%s)", deployment.protocolTimelock); - _updateProposer(TimelockController(payable(deployment.protocolTimelock)), deployment.daoSafe); - } - -/* -forge script script/PWNTimelock.s.sol:Setup \ ---sig "updateAdminTimelockProposer()" \ ---rpc-url $RPC_URL \ ---private-key $PRIVATE_KEY \ ---with-gas-price $(cast --to-wei 15 gwei) \ ---broadcast -*/ - function updateAdminTimelockProposer() external { - _loadDeployedAddresses(); - console2.log("Updating product timelock proposer (%s)", deployment.adminTimelock); - _updateProposer(TimelockController(payable(deployment.adminTimelock)), deployment.daoSafe); - } - - /// @dev Will grant PROPOSER_ROLE & CANCELLOR_ROLE to the new address and revoke them from `0x0cfC...D6de`. - /// Expecting to have address loaded from the `deployments.json` file. - /// Expecting timelock to be freshly deployed with one proposer `0x0cfC...D6de` and min delay set to 0. - function _updateProposer(TimelockController timelock, address newProposer) private { - vm.startBroadcast(); - - address initialProposer = 0x0cfC62C2E82dA2f580Fd54a2f526F65B6cC8D6de; + address initialProposer = vm.addr(initialConfigHelper); + address newProposer = deployment.daoSafe; bytes[] memory payloads = new bytes[](4); payloads[0] = abi.encodeWithSignature("grantRole(bytes32,address)", timelock.PROPOSER_ROLE(), newProposer); @@ -153,7 +108,7 @@ forge script script/PWNTimelock.s.sol:Setup \ address[] memory targets = new address[](payloads.length); for (uint256 i; i < payloads.length; ++i) { - targets[i] = address(timelock); + targets[i] = _timelock; } timelock.scheduleAndExecuteBatch(targets, payloads); @@ -161,11 +116,21 @@ forge script script/PWNTimelock.s.sol:Setup \ console2.log("Proposer role granted to:", newProposer); console2.log("Cancellor role granted to:", newProposer); console2.log("Proposer role revoked from:", initialProposer); - console2.log("Proposer role revoked from:", initialProposer); + console2.log("Cancellor role revoked from:", initialProposer); vm.stopBroadcast(); } +} + + +contract Setup is Deployments, Script { + using GnosisSafeUtils for GnosisSafeLike; + using TimelockUtils for TimelockController; + + function _protocolNotDeployedOnSelectedChain() internal pure override { + revert("PWNTimelock: selected chain is not set in deployments/latest.json"); + } /* forge script script/PWNTimelock.s.sol:Setup \ From dcf5b0082e7bc1adc7accbf2f21bb0f906a096d7 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Mon, 29 Apr 2024 16:46:27 +0100 Subject: [PATCH 110/129] docs: update latest deployment addresses --- deployments/latest.json | 154 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 153 insertions(+), 1 deletion(-) diff --git a/deployments/latest.json b/deployments/latest.json index 915ab33..688c77a 100644 --- a/deployments/latest.json +++ b/deployments/latest.json @@ -1,6 +1,139 @@ { - "deployedChains": [162314], + "deployedChains": [1, 10, 25, 56, 137, 8453, 42161, 162314, 11155111], "chains": { + "1": { + "dao": "0x1B8383D2726E7e18189205337424a2631A2102F4", + "adminTimelock": "0xd57e72A328AB1deC6b374c2babe2dc489819B5Ea", + "protocolTimelock": "0xd8dbdDf1c0FDdf9b5eCFA5C067C38DB66739FBAB", + "daoSafe": "0xd56635c0E91D31F88B89F195D3993a9e34516e59", + "deployerSafe": "0x42Cad20c964067f8e8b5c3E13fd0aa3C20a964C4", + "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", + "categoryRegistry": "0x0000000000000000000000000000000000000000", + "configSingleton": "0x0000000000000000000000000000000000000000", + "config": "0x0000000000000000000000000000000000000000", + "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", + "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", + "revokedNonce": "0x0000000000000000000000000000000000000000", + "simpleLoan": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleProposal": "0x0000000000000000000000000000000000000000", + "simpleLoanListProposal": "0x0000000000000000000000000000000000000000", + "simpleLoanFungibleProposal": "0x0000000000000000000000000000000000000000", + "simpleLoanDutchAuctionProposal": "0x0000000000000000000000000000000000000000" + }, + "10": { + "dao": "0x0000000000000000000000000000000000000000", + "adminTimelock": "0xd57e72A328AB1deC6b374c2babe2dc489819B5Ea", + "protocolTimelock": "0xd8dbdDf1c0FDdf9b5eCFA5C067C38DB66739FBAB", + "daoSafe": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", + "deployerSafe": "0x1B4B37738De3bb9E6a7a4f99aFe4C145734c071d", + "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", + "categoryRegistry": "0x0000000000000000000000000000000000000000", + "configSingleton": "0x0000000000000000000000000000000000000000", + "config": "0x0000000000000000000000000000000000000000", + "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", + "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", + "revokedNonce": "0x0000000000000000000000000000000000000000", + "simpleLoan": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleProposal": "0x0000000000000000000000000000000000000000", + "simpleLoanListProposal": "0x0000000000000000000000000000000000000000", + "simpleLoanFungibleProposal": "0x0000000000000000000000000000000000000000", + "simpleLoanDutchAuctionProposal": "0x0000000000000000000000000000000000000000" + }, + "25": { + "dao": "0x0000000000000000000000000000000000000000", + "adminTimelock": "0xd57e72A328AB1deC6b374c2babe2dc489819B5Ea", + "protocolTimelock": "0xd8dbdDf1c0FDdf9b5eCFA5C067C38DB66739FBAB", + "daoSafe": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", + "deployerSafe": "0x1B4B37738De3bb9E6a7a4f99aFe4C145734c071d", + "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", + "categoryRegistry": "0x0000000000000000000000000000000000000000", + "configSingleton": "0x0000000000000000000000000000000000000000", + "config": "0x0000000000000000000000000000000000000000", + "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", + "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", + "revokedNonce": "0x0000000000000000000000000000000000000000", + "simpleLoan": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleProposal": "0x0000000000000000000000000000000000000000", + "simpleLoanListProposal": "0x0000000000000000000000000000000000000000", + "simpleLoanFungibleProposal": "0x0000000000000000000000000000000000000000", + "simpleLoanDutchAuctionProposal": "0x0000000000000000000000000000000000000000" + }, + "56": { + "dao": "0x0000000000000000000000000000000000000000", + "adminTimelock": "0xd57e72A328AB1deC6b374c2babe2dc489819B5Ea", + "protocolTimelock": "0xd8dbdDf1c0FDdf9b5eCFA5C067C38DB66739FBAB", + "daoSafe": "0xd56635c0E91D31F88B89F195D3993a9e34516e59", + "deployerSafe": "0x42Cad20c964067f8e8b5c3E13fd0aa3C20a964C4", + "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", + "categoryRegistry": "0x0000000000000000000000000000000000000000", + "configSingleton": "0x0000000000000000000000000000000000000000", + "config": "0x0000000000000000000000000000000000000000", + "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", + "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", + "revokedNonce": "0x0000000000000000000000000000000000000000", + "simpleLoan": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleProposal": "0x0000000000000000000000000000000000000000", + "simpleLoanListProposal": "0x0000000000000000000000000000000000000000", + "simpleLoanFungibleProposal": "0x0000000000000000000000000000000000000000", + "simpleLoanDutchAuctionProposal": "0x0000000000000000000000000000000000000000" + }, + "137": { + "dao": "0x0000000000000000000000000000000000000000", + "adminTimelock": "0xd57e72A328AB1deC6b374c2babe2dc489819B5Ea", + "protocolTimelock": "0xd8dbdDf1c0FDdf9b5eCFA5C067C38DB66739FBAB", + "daoSafe": "0xd56635c0E91D31F88B89F195D3993a9e34516e59", + "deployerSafe": "0x42Cad20c964067f8e8b5c3E13fd0aa3C20a964C4", + "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", + "categoryRegistry": "0x0000000000000000000000000000000000000000", + "configSingleton": "0x0000000000000000000000000000000000000000", + "config": "0x0000000000000000000000000000000000000000", + "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", + "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", + "revokedNonce": "0x0000000000000000000000000000000000000000", + "simpleLoan": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleProposal": "0x0000000000000000000000000000000000000000", + "simpleLoanListProposal": "0x0000000000000000000000000000000000000000", + "simpleLoanFungibleProposal": "0x0000000000000000000000000000000000000000", + "simpleLoanDutchAuctionProposal": "0x0000000000000000000000000000000000000000" + }, + "8453": { + "dao": "0x0000000000000000000000000000000000000000", + "adminTimelock": "0xd57e72A328AB1deC6b374c2babe2dc489819B5Ea", + "protocolTimelock": "0xd8dbdDf1c0FDdf9b5eCFA5C067C38DB66739FBAB", + "daoSafe": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", + "deployerSafe": "0x1B4B37738De3bb9E6a7a4f99aFe4C145734c071d", + "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", + "categoryRegistry": "0x0000000000000000000000000000000000000000", + "configSingleton": "0x0000000000000000000000000000000000000000", + "config": "0x0000000000000000000000000000000000000000", + "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", + "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", + "revokedNonce": "0x0000000000000000000000000000000000000000", + "simpleLoan": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleProposal": "0x0000000000000000000000000000000000000000", + "simpleLoanListProposal": "0x0000000000000000000000000000000000000000", + "simpleLoanFungibleProposal": "0x0000000000000000000000000000000000000000", + "simpleLoanDutchAuctionProposal": "0x0000000000000000000000000000000000000000" + }, + "42161": { + "dao": "0x0000000000000000000000000000000000000000", + "adminTimelock": "0xd57e72A328AB1deC6b374c2babe2dc489819B5Ea", + "protocolTimelock": "0xd8dbddf1c0fddf9b5ecfa5c067c38db66739fbab", + "daoSafe": "0xd56635c0E91D31F88B89F195D3993a9e34516e59", + "deployerSafe": "0x42Cad20c964067f8e8b5c3E13fd0aa3C20a964C4", + "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", + "categoryRegistry": "0x0000000000000000000000000000000000000000", + "configSingleton": "0x0000000000000000000000000000000000000000", + "config": "0x0000000000000000000000000000000000000000", + "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", + "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", + "revokedNonce": "0x0000000000000000000000000000000000000000", + "simpleLoan": "0x0000000000000000000000000000000000000000", + "simpleLoanSimpleProposal": "0x0000000000000000000000000000000000000000", + "simpleLoanListProposal": "0x0000000000000000000000000000000000000000", + "simpleLoanFungibleProposal": "0x0000000000000000000000000000000000000000", + "simpleLoanDutchAuctionProposal": "0x0000000000000000000000000000000000000000" + }, "162314": { "dao": "0x0000000000000000000000000000000000000000", "adminTimelock": "0xd57e72A328AB1deC6b374c2babe2dc489819B5Ea", @@ -19,6 +152,25 @@ "simpleLoanListProposal": "0x1b02D009cd20e1EA9A0a9a9d37b03b0fe74Db5C6", "simpleLoanFungibleProposal": "0xc53ec196368e8767576f80787A1924b87A101d3F", "simpleLoanDutchAuctionProposal": "0x85670ed4a10522C0c152a34cd285c298Ac18bd55" + }, + "11155111": { + "dao": "0x0000000000000000000000000000000000000000", + "adminTimelock": "0xd57e72A328AB1deC6b374c2babe2dc489819B5Ea", + "protocolTimelock": "0xd8dbdDf1c0FDdf9b5eCFA5C067C38DB66739FBAB", + "daoSafe": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", + "deployerSafe": "0x1B4B37738De3bb9E6a7a4f99aFe4C145734c071d", + "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", + "categoryRegistry": "0x1b71A2a08A6fb54b5a8615A48b13447a7b1E891E", + "configSingleton": "0xBdac2fb31E493f92370ED5ECb3EA63e63ae32617", + "config": "0x4ca21fD91F7F6446594E0ff93dD3147fbC2ca7Bb", + "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", + "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", + "revokedOfferNonce": "0xBAabf06494A2b1407AA740Ae7B04aF51D1Fd0d5F", + "revokedRequestNonce": "0x18E6EeF41ef2D6B9ab9BC5A02f82E447473D0436", + "simpleLoan": "0x038F9929CfD6af748EFE5384C4F29f67337F6887", + "simpleLoanSimpleOffer": "0x1b02D009cd20e1EA9A0a9a9d37b03b0fe74Db5C6", + "simpleLoanListOffer": "0xc53ec196368e8767576f80787A1924b87A101d3F", + "simpleLoanSimpleRequest": "0x85670ed4a10522C0c152a34cd285c298Ac18bd55" } } } From 628edebeaf443623f5c10cfd6f0c870a08b2552d Mon Sep 17 00:00:00 2001 From: ashhanai Date: Mon, 29 Apr 2024 16:46:54 +0100 Subject: [PATCH 111/129] fix: tenderly contract verification --- foundry.toml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/foundry.toml b/foundry.toml index 8219cfc..f66d91a 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,6 +2,7 @@ solc_version = '0.8.16' fs_permissions = [{ access = "read", path = "./deployments/latest.json"}] gas_reports = ["PWNSimpleLoan"] +cbor_metadata = true [rpc_endpoints] @@ -22,5 +23,8 @@ base_goerli = "${BASE_GOERLI_URL}" cronos_testnet = "${CRONOS_TESTNET_URL}" mantle_testnet = "${MANTLE_TESTNET_URL}" -tenderly = "${TENDERLY_URL}" # chain_id = 162314 +tenderly = "${TENDERLY_URL}" local = "${LOCAL_URL}" + +[etherscan] +unknown_chain = { key = "${TENDERLY_ACCESS_TOKEN}", chain = 11155111, url = "${TENDERLY_URL}" } From 4b28847681b582667e5791f8387713e4ab2edc2b Mon Sep 17 00:00:00 2001 From: ashhanai Date: Mon, 29 Apr 2024 16:47:28 +0100 Subject: [PATCH 112/129] feat: implement tenderly helper script --- script/Tenderly.s.sol | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 script/Tenderly.s.sol diff --git a/script/Tenderly.s.sol b/script/Tenderly.s.sol new file mode 100644 index 0000000..50b82e5 --- /dev/null +++ b/script/Tenderly.s.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.16; + +import { Script, console2 } from "forge-std/Script.sol"; + +import { Deployments } from "pwn/Deployments.sol"; + + +/* +forge script script/Tenderly.s.sol --ffi +*/ +contract Tenderly is Deployments, Script { + + function run() external { + vm.createSelectFork("tenderly"); + _loadDeployedAddresses(); + + console2.log("Fund deployment addresses"); + /// To fund an address use: cast rpc -r $TENDERLY_URL tenderly_addBalance {address} {hex_amount} + { + string[] memory args = new string[](7); + args[0] = "cast"; + args[1] = "rpc"; + args[2] = "--rpc-url"; + args[3] = vm.envString("TENDERLY_URL"); + args[4] = "tenderly_addBalance"; + args[5] = "0x27e3E42E96cE78C34572b70381A400DA5B6E984C"; + args[6] = "0x1000000000000000000000000"; + vm.ffi(args); + } + + /// To set safes threshold to 1 use: cast rpc -r $TENDERLY_URL tenderly_setStorageAt {safe_address} 0x0000000000000000000000000000000000000000000000000000000000000004 0x0000000000000000000000000000000000000000000000000000000000000001 + /// To set hubs owner to protocol safe use: cast rpc -r $TENDERLY_URL tenderly_setStorageAt {hub_address} 0x0000000000000000000000000000000000000000000000000000000000000000 {protocol_safe_addr_to_32} + } + +} From b3faccc5e400803bb6fad1b9d8fb0a40c75a44b6 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Mon, 20 May 2024 15:58:43 +0200 Subject: [PATCH 113/129] refactor: remove unnecessary zero address check --- src/config/PWNConfig.sol | 3 --- test/unit/PWNConfig.t.sol | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/config/PWNConfig.sol b/src/config/PWNConfig.sol index 2ebc7fd..eb9e82f 100644 --- a/src/config/PWNConfig.sol +++ b/src/config/PWNConfig.sol @@ -115,10 +115,7 @@ contract PWNConfig is Ownable2Step, Initializable { function initialize(address _owner, uint16 _fee, address _feeCollector) external initializer { require(_owner != address(0), "Owner is zero address"); _transferOwnership(_owner); - - require(_feeCollector != address(0), "Fee collector is zero address"); _setFeeCollector(_feeCollector); - _setFee(_fee); } diff --git a/test/unit/PWNConfig.t.sol b/test/unit/PWNConfig.t.sol index 57093c2..fe6a826 100644 --- a/test/unit/PWNConfig.t.sol +++ b/test/unit/PWNConfig.t.sol @@ -111,7 +111,7 @@ contract PWNConfig_Initialize_Test is PWNConfigTest { } function test_shouldFail_whenFeeCollectorIsZeroAddress() external { - vm.expectRevert("Fee collector is zero address"); + vm.expectRevert(abi.encodeWithSelector(PWNConfig.ZeroFeeCollector.selector)); config.initialize(owner, fee, address(0)); } From 824c5eb6aad35b3862e774417e405e41bad93dd6 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Mon, 20 May 2024 15:59:11 +0200 Subject: [PATCH 114/129] fix: typo --- src/config/PWNConfig.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/PWNConfig.sol b/src/config/PWNConfig.sol index eb9e82f..e0ab319 100644 --- a/src/config/PWNConfig.sol +++ b/src/config/PWNConfig.sol @@ -11,7 +11,7 @@ import { IStateFingerpringComputer } from "pwn/interfaces/IStateFingerpringCompu /** * @title PWN Config * @notice Contract holding configurable values of PWN protocol. - * @dev Is intendet to be used as a proxy via `TransparentUpgradeableProxy`. + * @dev Is intended to be used as a proxy via `TransparentUpgradeableProxy`. */ contract PWNConfig is Ownable2Step, Initializable { From dec9665ee7dcd8cf065ece1645d4f86c756a7cce Mon Sep 17 00:00:00 2001 From: ashhanai Date: Tue, 21 May 2024 13:54:18 +0200 Subject: [PATCH 115/129] feat: do not call transfer with 0 amount --- src/loan/terms/simple/loan/PWNSimpleLoan.sol | 10 +++++++-- test/unit/PWNSimpleLoan.t.sol | 23 +++++++++++++++++--- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/loan/terms/simple/loan/PWNSimpleLoan.sol b/src/loan/terms/simple/loan/PWNSimpleLoan.sol index 00b606e..1c168e7 100644 --- a/src/loan/terms/simple/loan/PWNSimpleLoan.sol +++ b/src/loan/terms/simple/loan/PWNSimpleLoan.sol @@ -886,6 +886,14 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { // Delete loan data & burn LOAN token before calling safe transfer _deleteLoan(loanId); + emit LOANClaimed({ loanId: loanId, defaulted: false }); + + // End here if the credit amount is zero + if (creditAmount == 0) + return; + + // Note: Zero credit amount can happen when the loan is refinanced by the original lender. + // Repay the original lender if (destinationOfFunds == loanOwner) { _push(repaymentCredit, loanOwner); @@ -907,8 +915,6 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { // Note: If the transfer fails, the LOAN token will remain in repaid state and the LOAN token owner // will be able to claim the repaid credit. Otherwise lender would be able to prevent borrower from // repaying the loan. - - emit LOANClaimed({ loanId: loanId, defaulted: false }); } /** diff --git a/test/unit/PWNSimpleLoan.t.sol b/test/unit/PWNSimpleLoan.t.sol index fdd990b..8203f37 100644 --- a/test/unit/PWNSimpleLoan.t.sol +++ b/test/unit/PWNSimpleLoan.t.sol @@ -1000,6 +1000,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { } function test_shouldUpdateLoanData_whenLOANOwnerIsOriginalLender_whenDirectRepaymentFails() external { + refinancedLoanTerms.credit.amount = simpleLoan.principalAmount - 1; + _mockLoanTerms(refinancedLoanTerms); _mockLOANTokenOwner(refinancingLoanId, lender); vm.mockCallRevert(simpleLoan.creditAddress, abi.encodeWithSignature("transfer(address,uint256)"), ""); @@ -1296,7 +1298,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { // Try claim repaid LOAN function testFuzz_shouldTryClaimRepaidLOAN_fullAmount_whenShouldTransferCommon(address loanOwner) external { - vm.assume(loanOwner != address(0)); + vm.assume(loanOwner != address(0) && loanOwner != lender); _mockLOANTokenOwner(refinancingLoanId, loanOwner); vm.expectCall( @@ -1320,7 +1322,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { _mockLOAN(refinancingLoanId, simpleLoan); uint256 repaymentAmount = loan.loanRepaymentAmount(refinancingLoanId); - shortage = bound(shortage, 1, repaymentAmount - 1); + shortage = bound(shortage, 0, repaymentAmount - 1); fungibleAsset.mint(borrower, shortage); @@ -1330,7 +1332,8 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { vm.expectCall( address(loan), abi.encodeWithSignature( - "tryClaimRepaidLOAN(uint256,uint256,address)", refinancingLoanId, shortage, lender + "tryClaimRepaidLOAN(uint256,uint256,address)", + refinancingLoanId, shortage, lender ) ); @@ -1996,6 +1999,20 @@ contract PWNSimpleLoan_TryClaimRepaidLOAN_Test is PWNSimpleLoanTest { _assertLOANEq(loanId, nonExistingLoan); } + function test_shouldNotCallTransfer_whenCreditAmountIsZero() external { + simpleLoan.originalSourceOfFunds = lender; + _mockLOAN(loanId, simpleLoan); + + vm.expectCall({ + callee: simpleLoan.creditAddress, + data: abi.encodeWithSignature("transfer(address,uint256)", lender, 0), + count: 0 + }); + + vm.prank(address(loan)); + loan.tryClaimRepaidLOAN(loanId, 0, lender); + } + function test_shouldTransferToOriginalLender_whenSourceOfFundsEqualToOriginalLender() external { simpleLoan.originalSourceOfFunds = lender; _mockLOAN(loanId, simpleLoan); From 031e3e1ff59e01a7330b3df887b9f2574b286234 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Tue, 21 May 2024 13:55:13 +0200 Subject: [PATCH 116/129] feat: move refinancing loan id value into LOANCreated event and remove LOANRefinanced event --- src/loan/terms/simple/loan/PWNSimpleLoan.sol | 36 ++++++-------------- test/unit/PWNSimpleLoan.t.sol | 11 +++--- 2 files changed, 16 insertions(+), 31 deletions(-) diff --git a/src/loan/terms/simple/loan/PWNSimpleLoan.sol b/src/loan/terms/simple/loan/PWNSimpleLoan.sol index 1c168e7..c56ce9d 100644 --- a/src/loan/terms/simple/loan/PWNSimpleLoan.sol +++ b/src/loan/terms/simple/loan/PWNSimpleLoan.sol @@ -198,12 +198,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { /** * @notice Emitted when a new loan in created. */ - event LOANCreated(uint256 indexed loanId, bytes32 indexed proposalHash, address indexed proposalContract, Terms terms, LenderSpec lenderSpec, bytes extra); - - /** - * @notice Emitted when a loan is refinanced. - */ - event LOANRefinanced(uint256 indexed loanId, uint256 indexed refinancedLoanId); + event LOANCreated(uint256 indexed loanId, bytes32 indexed proposalHash, address indexed proposalContract, uint256 refinancingLoanId, Terms terms, LenderSpec lenderSpec, bytes extra); /** * @notice Emitted when a loan is paid back. @@ -426,9 +421,16 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { // Create a new loan loanId = _createLoan({ + loanTerms: loanTerms, + lenderSpec: lenderSpec + }); + + emit LOANCreated({ + loanId: loanId, proposalHash: proposalHash, proposalContract: proposalSpec.proposalContract, - loanTerms: loanTerms, + refinancingLoanId: callerSpec.refinancingLoanId, + terms: loanTerms, lenderSpec: lenderSpec, extra: extra }); @@ -454,8 +456,6 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { loanTerms: loanTerms, lenderSpec: lenderSpec }); - - emit LOANRefinanced({ loanId: callerSpec.refinancingLoanId, refinancedLoanId: loanId }); } } @@ -512,17 +512,12 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { /** * @notice Mint LOAN token and store loan data under loan id. - * @param proposalHash Hash of a loan offer / request that is signed by a lender / borrower. - * @param proposalContract Address of a loan proposal contract. * @param loanTerms Loan terms struct. - * @param extra Auxiliary data that are emitted in the loan creation event. They are not used in the contract logic. + * @param lenderSpec Lender specification struct. */ function _createLoan( - bytes32 proposalHash, - address proposalContract, Terms memory loanTerms, - LenderSpec calldata lenderSpec, - bytes calldata extra + LenderSpec calldata lenderSpec ) private returns (uint256 loanId) { // Mint LOAN token for lender loanId = loanToken.mint(loanTerms.lender); @@ -542,15 +537,6 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { loan.fixedInterestAmount = loanTerms.fixedInterestAmount; loan.principalAmount = loanTerms.credit.amount; loan.collateral = loanTerms.collateral; - - emit LOANCreated({ - loanId: loanId, - proposalHash: proposalHash, - proposalContract: proposalContract, - terms: loanTerms, - lenderSpec: lenderSpec, - extra: extra - }); } /** diff --git a/test/unit/PWNSimpleLoan.t.sol b/test/unit/PWNSimpleLoan.t.sol index 8203f37..00cd2e0 100644 --- a/test/unit/PWNSimpleLoan.t.sol +++ b/test/unit/PWNSimpleLoan.t.sol @@ -57,10 +57,9 @@ abstract contract PWNSimpleLoanTest is Test { bytes32 proposalHash = keccak256("proposalHash"); - event LOANCreated(uint256 indexed loanId, bytes32 indexed proposalHash, address indexed proposalContract, PWNSimpleLoan.Terms terms, PWNSimpleLoan.LenderSpec lenderSpec, bytes extra); + event LOANCreated(uint256 indexed loanId, bytes32 indexed proposalHash, address indexed proposalContract, uint256 refinancingLoanId, PWNSimpleLoan.Terms terms, PWNSimpleLoan.LenderSpec lenderSpec, bytes extra); event LOANPaidBack(uint256 indexed loanId); event LOANClaimed(uint256 indexed loanId, bool indexed defaulted); - event LOANRefinanced(uint256 indexed loanId, uint256 indexed refinancedLoanId); event LOANExtended(uint256 indexed loanId, uint40 originalDefaultTimestamp, uint40 extendedDefaultTimestamp); event ExtensionProposalMade(bytes32 indexed extensionHash, address indexed proposer, PWNSimpleLoan.ExtensionProposal proposal); @@ -710,7 +709,7 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { function test_shouldEmit_LOANCreated() external { vm.expectEmit(); - emit LOANCreated(loanId, proposalHash, proposalContract, simpleLoanTerms, lenderSpec, "lil extra"); + emit LOANCreated(loanId, proposalHash, proposalContract, 0, simpleLoanTerms, lenderSpec, "lil extra"); loan.createLOAN({ proposalSpec: proposalSpec, @@ -945,15 +944,15 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { }); } - function test_shouldEmit_LOANRefinanced() external { + function test_shouldEmit_LOANCreated() external { vm.expectEmit(); - emit LOANRefinanced(refinancingLoanId, loanId); + emit LOANCreated(loanId, proposalHash, proposalContract, refinancingLoanId, refinancedLoanTerms, lenderSpec, "lil extra"); loan.createLOAN({ proposalSpec: proposalSpec, lenderSpec: lenderSpec, callerSpec: callerSpec, - extra: "" + extra: "lil extra" }); } From 28ef5325fc7ad87d69be3acf79bb7c5e97788e44 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Tue, 21 May 2024 18:06:50 +0200 Subject: [PATCH 117/129] feat: accrue interest per minute --- src/loan/terms/simple/loan/PWNSimpleLoan.sol | 55 +++------ test/unit/PWNSimpleLoan.t.sol | 123 +++++++++++-------- 2 files changed, 89 insertions(+), 89 deletions(-) diff --git a/src/loan/terms/simple/loan/PWNSimpleLoan.sol b/src/loan/terms/simple/loan/PWNSimpleLoan.sol index c56ce9d..7574561 100644 --- a/src/loan/terms/simple/loan/PWNSimpleLoan.sol +++ b/src/loan/terms/simple/loan/PWNSimpleLoan.sol @@ -37,13 +37,11 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { |*----------------------------------------------------------*/ uint32 public constant MIN_LOAN_DURATION = 10 minutes; - uint40 public constant MAX_ACCRUING_INTEREST_APR = 1e11; // 1,000,000% APR + uint40 public constant MAX_ACCRUING_INTEREST_APR = 1e12; // 10,000,000 APR (with 5 decimals) - uint256 public constant APR_INTEREST_DENOMINATOR = 1e4; - uint256 public constant DAILY_INTEREST_DENOMINATOR = 1e10; - - uint256 public constant APR_TO_DAILY_INTEREST_NUMERATOR = 274; - uint256 public constant APR_TO_DAILY_INTEREST_DENOMINATOR = 1e5; + uint256 public constant ACCRUING_INTEREST_APR_DECIMALS = 1e5; + uint256 public constant MINUTES_IN_YEAR = 525_600; // Note: Assuming 365 days in a year + uint256 public constant ACCRUING_INTEREST_APR_DENOMINATOR = ACCRUING_INTEREST_APR_DECIMALS * MINUTES_IN_YEAR * 100; uint256 public constant MAX_EXTENSION_DURATION = 90 days; uint256 public constant MIN_EXTENSION_DURATION = 1 days; @@ -137,7 +135,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { * @param defaultTimestamp Unix timestamp (in seconds) of a default date. * @param borrower Address of a borrower. * @param originalLender Address of a lender that funded the loan. - * @param accruingInterestDailyRate Accruing daily interest rate. + * @param accruingInterestAPR Accruing interest APR. * @param fixedInterestAmount Fixed interest amount in credit asset tokens. * It is the minimum amount of interest which has to be paid by a borrower. * This property is reused to store the final interest amount if the loan is repaid and waiting to be claimed. @@ -152,7 +150,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { uint40 defaultTimestamp; address borrower; address originalLender; - uint40 accruingInterestDailyRate; + uint40 accruingInterestAPR; uint256 fixedInterestAmount; uint256 principalAmount; MultiToken.Asset collateral; @@ -531,9 +529,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { loan.defaultTimestamp = uint40(block.timestamp) + loanTerms.duration; loan.borrower = loanTerms.borrower; loan.originalLender = loanTerms.lender; - loan.accruingInterestDailyRate = SafeCast.toUint40(Math.mulDiv( - loanTerms.accruingInterestAPR, APR_TO_DAILY_INTEREST_NUMERATOR, APR_TO_DAILY_INTEREST_DENOMINATOR - )); + loan.accruingInterestAPR = loanTerms.accruingInterestAPR; loan.fixedInterestAmount = loanTerms.fixedInterestAmount; loan.principalAmount = loanTerms.credit.amount; loan.collateral = loanTerms.collateral; @@ -591,7 +587,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { ) private { LOAN storage loan = LOANs[refinancingLoanId]; address loanOwner = loanToken.ownerOf(refinancingLoanId); - uint256 repaymentAmount = _loanRepaymentAmount(refinancingLoanId); + uint256 repaymentAmount = loanRepaymentAmount(refinancingLoanId); // Calculate fee amount and new loan amount (uint256 feeAmount, uint256 newLoanAmount) @@ -710,7 +706,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { } // Transfer the repaid credit to the Vault - uint256 repaymentAmount = _loanRepaymentAmount(loanId); + uint256 repaymentAmount = loanRepaymentAmount(loanId); _pull(loan.creditAddress.ERC20(repaymentAmount), msg.sender); // Transfer collateral back to borrower @@ -754,7 +750,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { // Update accrued interest amount loan.fixedInterestAmount = _loanAccruedInterest(loan); - loan.accruingInterestDailyRate = 0; + loan.accruingInterestAPR = 0; // Note: Reusing `fixedInterestAmount` to store accrued interest at the time of repayment // to have the value at the time of claim and stop accruing new interest. @@ -775,20 +771,9 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { function loanRepaymentAmount(uint256 loanId) public view returns (uint256) { LOAN storage loan = LOANs[loanId]; - // Check non-existent + // Check non-existent loan if (loan.status == 0) return 0; - return _loanRepaymentAmount(loanId); - } - - /** - * @notice Internal function to calculate the loan repayment amount with fixed and accrued interest. - * @param loanId Id of a loan. - * @return Repayment amount. - */ - function _loanRepaymentAmount(uint256 loanId) private view returns (uint256) { - LOAN storage loan = LOANs[loanId]; - // Return loan principal with accrued interest return loan.principalAmount + _loanAccruedInterest(loan); } @@ -799,12 +784,12 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { * @return Accrued interest amount. */ function _loanAccruedInterest(LOAN storage loan) private view returns (uint256) { - if (loan.accruingInterestDailyRate == 0) + if (loan.accruingInterestAPR == 0) return loan.fixedInterestAmount; - uint256 accruingDays = (block.timestamp - loan.startTimestamp) / 1 days; + uint256 accruingMinutes = (block.timestamp - loan.startTimestamp) / 1 minutes; uint256 accruedInterest = Math.mulDiv( - loan.principalAmount, loan.accruingInterestDailyRate * accruingDays, DAILY_INTEREST_DENOMINATOR + loan.principalAmount, uint256(loan.accruingInterestAPR) * accruingMinutes, ACCRUING_INTEREST_APR_DENOMINATOR ); return loan.fixedInterestAmount + accruedInterest; } @@ -915,7 +900,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { // Store in memory before deleting the loan MultiToken.Asset memory asset = defaulted ? loan.collateral - : loan.creditAddress.ERC20(_loanRepaymentAmount(loanId)); + : loan.creditAddress.ERC20(loanRepaymentAmount(loanId)); // Delete loan data & burn LOAN token before calling safe transfer _deleteLoan(loanId); @@ -1090,7 +1075,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { * @return borrower Address of a loan borrower. * @return originalLender Address of a loan original lender. * @return loanOwner Address of a LOAN token holder. - * @return accruingInterestDailyRate Daily interest rate in basis points. + * @return accruingInterestAPR Accruing interest APR with 5 decimal places. * @return fixedInterestAmount Fixed interest amount in credit asset tokens. * @return credit Asset used as a loan credit. For a definition see { MultiToken dependency lib }. * @return collateral Asset used as a loan collateral. For a definition see { MultiToken dependency lib }. @@ -1104,7 +1089,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { address borrower, address originalLender, address loanOwner, - uint40 accruingInterestDailyRate, + uint40 accruingInterestAPR, uint256 fixedInterestAmount, MultiToken.Asset memory credit, MultiToken.Asset memory collateral, @@ -1119,7 +1104,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { borrower = loan.borrower; originalLender = loan.originalLender; loanOwner = loan.status != 0 ? loanToken.ownerOf(loanId) : address(0); - accruingInterestDailyRate = loan.accruingInterestDailyRate; + accruingInterestAPR = loan.accruingInterestAPR; fixedInterestAmount = loan.fixedInterestAmount; credit = loan.creditAddress.ERC20(loan.principalAmount); collateral = loan.collateral; @@ -1198,13 +1183,13 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { // - status: updated for expired loans based on block.timestamp // - defaultTimestamp: updated when the loan is extended // - fixedInterestAmount: updated when the loan is repaid and waiting to be claimed - // - accruingInterestDailyRate: updated when the loan is repaid and waiting to be claimed + // - accruingInterestAPR: updated when the loan is repaid and waiting to be claimed // Others don't have to be part of the state fingerprint as it does not act as a token identification. return keccak256(abi.encode( _getLOANStatus(tokenId), loan.defaultTimestamp, loan.fixedInterestAmount, - loan.accruingInterestDailyRate + loan.accruingInterestAPR )); } diff --git a/test/unit/PWNSimpleLoan.t.sol b/test/unit/PWNSimpleLoan.t.sol index 00cd2e0..f66f9ef 100644 --- a/test/unit/PWNSimpleLoan.t.sol +++ b/test/unit/PWNSimpleLoan.t.sol @@ -107,7 +107,7 @@ abstract contract PWNSimpleLoanTest is Test { defaultTimestamp: uint40(block.timestamp + loanDurationInDays * 1 days), borrower: borrower, originalLender: lender, - accruingInterestDailyRate: 0, + accruingInterestAPR: 0, fixedInterestAmount: 6631, principalAmount: 100, collateral: MultiToken.ERC721(address(nonFungibleAsset), 2) @@ -140,7 +140,7 @@ abstract contract PWNSimpleLoanTest is Test { defaultTimestamp: 0, borrower: address(0), originalLender: address(0), - accruingInterestDailyRate: 0, + accruingInterestAPR: 0, fixedInterestAmount: 0, principalAmount: 0, collateral: MultiToken.Asset(MultiToken.Category(0), address(0), 0, 0) @@ -198,7 +198,7 @@ abstract contract PWNSimpleLoanTest is Test { assertEq(_simpleLoan1.defaultTimestamp, _simpleLoan2.defaultTimestamp); assertEq(_simpleLoan1.borrower, _simpleLoan2.borrower); assertEq(_simpleLoan1.originalLender, _simpleLoan2.originalLender); - assertEq(_simpleLoan1.accruingInterestDailyRate, _simpleLoan2.accruingInterestDailyRate); + assertEq(_simpleLoan1.accruingInterestAPR, _simpleLoan2.accruingInterestAPR); assertEq(_simpleLoan1.fixedInterestAmount, _simpleLoan2.fixedInterestAmount); assertEq(_simpleLoan1.principalAmount, _simpleLoan2.principalAmount); assertEq(uint8(_simpleLoan1.collateral.category), uint8(_simpleLoan2.collateral.category)); @@ -217,7 +217,7 @@ abstract contract PWNSimpleLoanTest is Test { // Borrower address _assertLOANWord(loanSlot + 2, abi.encodePacked(uint96(0), _simpleLoan.borrower)); // Original lender, accruing interest daily rate - _assertLOANWord(loanSlot + 3, abi.encodePacked(uint56(0), _simpleLoan.accruingInterestDailyRate, _simpleLoan.originalLender)); + _assertLOANWord(loanSlot + 3, abi.encodePacked(uint56(0), _simpleLoan.accruingInterestAPR, _simpleLoan.originalLender)); // Fixed interest amount _assertLOANWord(loanSlot + 4, abi.encodePacked(_simpleLoan.fixedInterestAmount)); // Principal amount @@ -241,7 +241,7 @@ abstract contract PWNSimpleLoanTest is Test { // Borrower address _storeLOANWord(loanSlot + 2, abi.encodePacked(uint96(0), _simpleLoan.borrower)); // Original lender, accruing interest daily rate - _storeLOANWord(loanSlot + 3, abi.encodePacked(uint56(0), _simpleLoan.accruingInterestDailyRate, _simpleLoan.originalLender)); + _storeLOANWord(loanSlot + 3, abi.encodePacked(uint56(0), _simpleLoan.accruingInterestAPR, _simpleLoan.originalLender)); // Fixed interest amount _storeLOANWord(loanSlot + 4, abi.encodePacked(_simpleLoan.fixedInterestAmount)); // Principal amount @@ -530,11 +530,7 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { }); } - function testFuzz_shouldStoreLoanData(uint40 accruingInterestAPR) external { - accruingInterestAPR = uint40(bound(accruingInterestAPR, 0, 1e11)); - simpleLoanTerms.accruingInterestAPR = accruingInterestAPR; - _mockLoanTerms(simpleLoanTerms); - + function test_shouldStoreLoanData() external { loan.createLOAN({ proposalSpec: proposalSpec, lenderSpec: lenderSpec, @@ -542,7 +538,6 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { extra: "" }); - simpleLoan.accruingInterestDailyRate = uint40(uint256(accruingInterestAPR) * 274 / 1e5); _assertLOANEq(loanId, simpleLoan); } @@ -762,7 +757,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { defaultTimestamp: uint40(block.timestamp + 40039), borrower: borrower, originalLender: lender, - accruingInterestDailyRate: 0, + accruingInterestAPR: 0, fixedInterestAmount: 6631, principalAmount: 100e18, collateral: MultiToken.ERC721(address(nonFungibleAsset), 2) @@ -994,7 +989,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { // Update loan and compare simpleLoan.status = 3; // move loan to repaid state simpleLoan.fixedInterestAmount = loan.loanRepaymentAmount(refinancingLoanId) - simpleLoan.principalAmount; // stored accrued interest at the time of repayment - simpleLoan.accruingInterestDailyRate = 0; // stop accruing interest + simpleLoan.accruingInterestAPR = 0; // stop accruing interest _assertLOANEq(refinancingLoanId, simpleLoan); } @@ -1015,7 +1010,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { // Update loan and compare simpleLoan.status = 3; // move loan to repaid state simpleLoan.fixedInterestAmount = loan.loanRepaymentAmount(refinancingLoanId) - simpleLoan.principalAmount; // stored accrued interest at the time of repayment - simpleLoan.accruingInterestDailyRate = 0; // stop accruing interest + simpleLoan.accruingInterestAPR = 0; // stop accruing interest _assertLOANEq(refinancingLoanId, simpleLoan); } @@ -1360,7 +1355,7 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { simpleLoan.status = 3; simpleLoan.fixedInterestAmount = loan.loanRepaymentAmount(refinancingLoanId) - simpleLoan.principalAmount; - simpleLoan.accruingInterestDailyRate = 0; + simpleLoan.accruingInterestAPR = 0; _assertLOANEq(refinancingLoanId, simpleLoan); } @@ -1368,16 +1363,16 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { // More overall tests function testFuzz_shouldRepayOriginalLoan( - uint256 _days, uint256 principal, uint256 fixedInterest, uint256 dailyInterest, uint256 refinanceAmount + uint256 _days, uint256 principal, uint256 fixedInterest, uint256 interestAPR, uint256 refinanceAmount ) external { _days = bound(_days, 0, loanDurationInDays - 1); principal = bound(principal, 1, 1e40); fixedInterest = bound(fixedInterest, 0, 1e40); - dailyInterest = bound(dailyInterest, 1, 274e8); + interestAPR = bound(interestAPR, 1, 1e12); simpleLoan.principalAmount = principal; simpleLoan.fixedInterestAmount = fixedInterest; - simpleLoan.accruingInterestDailyRate = uint40(dailyInterest); + simpleLoan.accruingInterestAPR = uint40(interestAPR); _mockLOAN(refinancingLoanId, simpleLoan); vm.warp(simpleLoan.startTimestamp + _days * 1 days); @@ -1412,16 +1407,16 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { } function testFuzz_shouldCollectProtocolFee( - uint256 _days, uint256 principal, uint256 fixedInterest, uint256 dailyInterest, uint256 refinanceAmount, uint256 fee + uint256 _days, uint256 principal, uint256 fixedInterest, uint256 interestAPR, uint256 refinanceAmount, uint256 fee ) external { _days = bound(_days, 0, loanDurationInDays - 1); principal = bound(principal, 1, 1e40); fixedInterest = bound(fixedInterest, 0, 1e40); - dailyInterest = bound(dailyInterest, 1, 274e8); + interestAPR = bound(interestAPR, 1, 1e12); simpleLoan.principalAmount = principal; simpleLoan.fixedInterestAmount = fixedInterest; - simpleLoan.accruingInterestDailyRate = uint40(dailyInterest); + simpleLoan.accruingInterestAPR = uint40(interestAPR); _mockLOAN(refinancingLoanId, simpleLoan); vm.warp(simpleLoan.startTimestamp + _days * 1 days); @@ -1599,18 +1594,18 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { } function testFuzz_shouldUpdateLoanData_whenLOANOwnerIsNotOriginalLender( - uint256 _days, uint256 _principal, uint256 _fixedInterest, uint256 _dailyInterest + uint256 _days, uint256 _principal, uint256 _fixedInterest, uint256 _interestAPR ) external { _mockLOANTokenOwner(loanId, notOriginalLender); _days = bound(_days, 0, loanDurationInDays - 1); _principal = bound(_principal, 1, 1e40); _fixedInterest = bound(_fixedInterest, 0, 1e40); - _dailyInterest = bound(_dailyInterest, 1, 274e8); + _interestAPR = bound(_interestAPR, 1, 1e12); simpleLoan.principalAmount = _principal; simpleLoan.fixedInterestAmount = _fixedInterest; - simpleLoan.accruingInterestDailyRate = uint40(_dailyInterest); + simpleLoan.accruingInterestAPR = uint40(_interestAPR); _mockLOAN(loanId, simpleLoan); vm.warp(simpleLoan.startTimestamp + _days * 1 days); @@ -1624,7 +1619,7 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { // Update loan and compare 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 + simpleLoan.accruingInterestAPR = 0; // stop accruing interest _assertLOANEq(loanId, simpleLoan); } @@ -1641,16 +1636,16 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { } function testFuzz_shouldTransferRepaidAmountToVault( - uint256 _days, uint256 _principal, uint256 _fixedInterest, uint256 _dailyInterest + uint256 _days, uint256 _principal, uint256 _fixedInterest, uint256 _interestAPR ) external { _days = bound(_days, 0, loanDurationInDays - 1); _principal = bound(_principal, 1, 1e40); _fixedInterest = bound(_fixedInterest, 0, 1e40); - _dailyInterest = bound(_dailyInterest, 1, 274e8); + _interestAPR = bound(_interestAPR, 1, 1e12); simpleLoan.principalAmount = _principal; simpleLoan.fixedInterestAmount = _fixedInterest; - simpleLoan.accruingInterestDailyRate = uint40(_dailyInterest); + simpleLoan.accruingInterestAPR = uint40(_interestAPR); _mockLOAN(loanId, simpleLoan); vm.warp(simpleLoan.startTimestamp + _days * 1 days); @@ -1713,7 +1708,7 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { simpleLoan.status = 3; simpleLoan.fixedInterestAmount = loan.loanRepaymentAmount(loanId) - simpleLoan.principalAmount; - simpleLoan.accruingInterestDailyRate = 0; + simpleLoan.accruingInterestAPR = 0; _assertLOANEq(loanId, simpleLoan); } @@ -1747,7 +1742,7 @@ contract PWNSimpleLoan_LoanRepaymentAmount_Test is PWNSimpleLoanTest { simpleLoan.defaultTimestamp = simpleLoan.startTimestamp + 101 * 1 days; simpleLoan.principalAmount = _principal; simpleLoan.fixedInterestAmount = _fixedInterest; - simpleLoan.accruingInterestDailyRate = 0; + simpleLoan.accruingInterestAPR = 0; _mockLOAN(loanId, simpleLoan); vm.warp(simpleLoan.startTimestamp + _days + 1 days); // should not have an effect @@ -1755,27 +1750,47 @@ contract PWNSimpleLoan_LoanRepaymentAmount_Test is PWNSimpleLoanTest { assertEq(loan.loanRepaymentAmount(loanId), _principal + _fixedInterest); } - function test_shouldReturnAccruedInterest_whenNonZeroAccruedInterest( - uint256 _days, uint256 _principal, uint256 _fixedInterest, uint256 _dailyInterest + function testFuzz_shouldReturnAccruedInterest_whenNonZeroAccruedInterest( + uint256 _minutes, uint256 _principal, uint256 _fixedInterest, uint256 _interestAPR ) external { - _days = bound(_days, 0, 2 * loanDurationInDays); // should return non zero value even after loan expiration + _minutes = bound(_minutes, 0, 2 * loanDurationInDays * 24 * 60); // should return non zero value even after loan expiration _principal = bound(_principal, 1, 1e40); _fixedInterest = bound(_fixedInterest, 0, 1e40); - _dailyInterest = bound(_dailyInterest, 1, 274e8); + _interestAPR = bound(_interestAPR, 1, 1e12); simpleLoan.defaultTimestamp = simpleLoan.startTimestamp + 101 * 1 days; simpleLoan.principalAmount = _principal; simpleLoan.fixedInterestAmount = _fixedInterest; - simpleLoan.accruingInterestDailyRate = uint40(_dailyInterest); + simpleLoan.accruingInterestAPR = uint40(_interestAPR); _mockLOAN(loanId, simpleLoan); - vm.warp(simpleLoan.startTimestamp + _days * 1 days + 1); + vm.warp(simpleLoan.startTimestamp + _minutes * 1 minutes + 1); - uint256 expectedInterest = _fixedInterest + _principal * _dailyInterest * _days / 1e10; + uint256 expectedInterest = _fixedInterest + _principal * _interestAPR * _minutes / (1e5 * 60 * 24 * 365) / 100; uint256 expectedLoanRepaymentAmount = _principal + expectedInterest; assertEq(loan.loanRepaymentAmount(loanId), expectedLoanRepaymentAmount); } + function test_shouldReturnAccuredInterest() external { + simpleLoan.defaultTimestamp = simpleLoan.startTimestamp + 101 * 1 days; + simpleLoan.principalAmount = 100e18; + simpleLoan.fixedInterestAmount = 10e18; + simpleLoan.accruingInterestAPR = uint40(365e5); + _mockLOAN(loanId, simpleLoan); + + vm.warp(simpleLoan.startTimestamp); + assertEq(loan.loanRepaymentAmount(loanId), simpleLoan.principalAmount + simpleLoan.fixedInterestAmount); + + vm.warp(simpleLoan.startTimestamp + 1 days); + assertEq(loan.loanRepaymentAmount(loanId), simpleLoan.principalAmount + simpleLoan.fixedInterestAmount + 1e18); + + simpleLoan.accruingInterestAPR = uint40(100e5); + _mockLOAN(loanId, simpleLoan); + + vm.warp(simpleLoan.startTimestamp + 365 days); + assertEq(loan.loanRepaymentAmount(loanId), 2 * simpleLoan.principalAmount + simpleLoan.fixedInterestAmount); + } + } @@ -1862,10 +1877,10 @@ contract PWNSimpleLoan_ClaimLOAN_Test is PWNSimpleLoanTest { _fixedInterest = bound(_fixedInterest, 0, 1e40); // Note: loan repayment into Vault will reuse `fixedInterestAmount` and store total interest - // at the time of repayment and set `accruingInterestDailyRate` to zero. + // at the time of repayment and set `accruingInterestAPR` to zero. simpleLoan.principalAmount = _principal; simpleLoan.fixedInterestAmount = _fixedInterest; - simpleLoan.accruingInterestDailyRate = 0; + simpleLoan.accruingInterestAPR = 0; uint256 loanRepaymentAmount = loan.loanRepaymentAmount(loanId); fungibleAsset.mint(address(loan), loanRepaymentAmount); @@ -2480,7 +2495,7 @@ contract PWNSimpleLoan_GetLOAN_Test is PWNSimpleLoanTest { uint40 _defaultTimestamp, address _borrower, address _originalLender, - uint40 _accruingInterestDailyRate, + uint40 _accruingInterestAPR, uint256 _fixedInterestAmount, address _creditAddress, uint256 _principalAmount, @@ -2491,14 +2506,14 @@ contract PWNSimpleLoan_GetLOAN_Test is PWNSimpleLoanTest { ) external { _startTimestamp = uint40(bound(_startTimestamp, 0, type(uint40).max - 1)); _defaultTimestamp = uint40(bound(_defaultTimestamp, _startTimestamp + 1, type(uint40).max)); - _accruingInterestDailyRate = uint40(bound(_accruingInterestDailyRate, 0, 274e8)); + _accruingInterestAPR = uint40(bound(_accruingInterestAPR, 0, 1e12)); _fixedInterestAmount = bound(_fixedInterestAmount, 0, type(uint256).max - _principalAmount); simpleLoan.startTimestamp = _startTimestamp; simpleLoan.defaultTimestamp = _defaultTimestamp; simpleLoan.borrower = _borrower; simpleLoan.originalLender = _originalLender; - simpleLoan.accruingInterestDailyRate = _accruingInterestDailyRate; + simpleLoan.accruingInterestAPR = _accruingInterestAPR; simpleLoan.fixedInterestAmount = _fixedInterestAmount; simpleLoan.creditAddress = _creditAddress; simpleLoan.principalAmount = _principalAmount; @@ -2528,8 +2543,8 @@ contract PWNSimpleLoan_GetLOAN_Test is PWNSimpleLoanTest { assertEq(originalLender, _originalLender); } { - (,,,,,, uint40 accruingInterestDailyRate,,,,,) = loan.getLOAN(loanId); - assertEq(accruingInterestDailyRate, _accruingInterestDailyRate); + (,,,,,, uint40 accruingInterestAPR,,,,,) = loan.getLOAN(loanId); + assertEq(accruingInterestAPR, _accruingInterestAPR); } { (,,,,,,, uint256 fixedInterestAmount,,,,) = loan.getLOAN(loanId); @@ -2592,15 +2607,15 @@ contract PWNSimpleLoan_GetLOAN_Test is PWNSimpleLoanTest { function testFuzz_shouldReturnRepaymentAmount( uint256 _days, uint256 _principalAmount, - uint40 _accruingInterestDailyRate, + uint40 _accruingInterestAPR, uint256 _fixedInterestAmount ) external { _days = bound(_days, 0, 2 * loanDurationInDays); _principalAmount = bound(_principalAmount, 1, 1e40); - _accruingInterestDailyRate = uint40(bound(_accruingInterestDailyRate, 0, 274e8)); + _accruingInterestAPR = uint40(bound(_accruingInterestAPR, 0, 1e12)); _fixedInterestAmount = bound(_fixedInterestAmount, 0, _principalAmount); - simpleLoan.accruingInterestDailyRate = _accruingInterestDailyRate; + simpleLoan.accruingInterestAPR = _accruingInterestAPR; simpleLoan.fixedInterestAmount = _fixedInterestAmount; simpleLoan.principalAmount = _principalAmount; _mockLOAN(loanId, simpleLoan); @@ -2621,7 +2636,7 @@ contract PWNSimpleLoan_GetLOAN_Test is PWNSimpleLoanTest { address borrower, address originalLender, address loanOwner, - uint40 accruingInterestDailyRate, + uint40 accruingInterestAPR, uint256 fixedInterestAmount, MultiToken.Asset memory credit, MultiToken.Asset memory collateral, @@ -2635,7 +2650,7 @@ contract PWNSimpleLoan_GetLOAN_Test is PWNSimpleLoanTest { assertEq(borrower, address(0)); assertEq(originalLender, address(0)); assertEq(loanOwner, address(0)); - assertEq(accruingInterestDailyRate, 0); + assertEq(accruingInterestAPR, 0); assertEq(fixedInterestAmount, 0); assertEq(credit.assetAddress, address(0)); assertEq(credit.amount, 0); @@ -2707,26 +2722,26 @@ contract PWNSimpleLoan_GetStateFingerprint_Test is PWNSimpleLoanTest { vm.warp(simpleLoan.defaultTimestamp - 1); assertEq( loan.getStateFingerprint(loanId), - keccak256(abi.encode(2, simpleLoan.defaultTimestamp, simpleLoan.fixedInterestAmount, simpleLoan.accruingInterestDailyRate)) + keccak256(abi.encode(2, simpleLoan.defaultTimestamp, simpleLoan.fixedInterestAmount, simpleLoan.accruingInterestAPR)) ); vm.warp(simpleLoan.defaultTimestamp); assertEq( loan.getStateFingerprint(loanId), - keccak256(abi.encode(4, simpleLoan.defaultTimestamp, simpleLoan.fixedInterestAmount, simpleLoan.accruingInterestDailyRate)) + keccak256(abi.encode(4, simpleLoan.defaultTimestamp, simpleLoan.fixedInterestAmount, simpleLoan.accruingInterestAPR)) ); } function testFuzz_shouldReturnCorrectStateFingerprint( - uint256 fixedInterestAmount, uint40 accruingInterestDailyRate + uint256 fixedInterestAmount, uint40 accruingInterestAPR ) external { simpleLoan.fixedInterestAmount = fixedInterestAmount; - simpleLoan.accruingInterestDailyRate = accruingInterestDailyRate; + simpleLoan.accruingInterestAPR = accruingInterestAPR; _mockLOAN(loanId, simpleLoan); assertEq( loan.getStateFingerprint(loanId), - keccak256(abi.encode(2, simpleLoan.defaultTimestamp, simpleLoan.fixedInterestAmount, simpleLoan.accruingInterestDailyRate)) + keccak256(abi.encode(2, simpleLoan.defaultTimestamp, simpleLoan.fixedInterestAmount, simpleLoan.accruingInterestAPR)) ); } From 1e7d561b397ea97632eddc7ed1f733a69388af97 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Tue, 21 May 2024 20:32:39 +0200 Subject: [PATCH 118/129] feat: delete legacy pwn hub access control abstract contract --- src/hub/PWNHubAccessControl.sol | 47 --------------------------------- src/loan/token/PWNLOAN.sol | 38 ++++++++++++++++++++++---- test/unit/PWNLOAN.t.sol | 5 ++-- 3 files changed, 35 insertions(+), 55 deletions(-) delete mode 100644 src/hub/PWNHubAccessControl.sol diff --git a/src/hub/PWNHubAccessControl.sol b/src/hub/PWNHubAccessControl.sol deleted file mode 100644 index 5eaf564..0000000 --- a/src/hub/PWNHubAccessControl.sol +++ /dev/null @@ -1,47 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity 0.8.16; - -import { PWNHub } from "pwn/hub/PWNHub.sol"; -import { PWNHubTags } from "pwn/hub/PWNHubTags.sol"; -import { CallerMissingHubTag } from "pwn/PWNErrors.sol"; - - -/** - * @title PWN Hub Access Control - * @notice Implement modifiers for PWN Hub access control. - */ -abstract contract PWNHubAccessControl { - - /*----------------------------------------------------------*| - |* # VARIABLES & CONSTANTS DEFINITIONS *| - |*----------------------------------------------------------*/ - - PWNHub immutable internal hub; - - - /*----------------------------------------------------------*| - |* # MODIFIERS *| - |*----------------------------------------------------------*/ - - modifier onlyActiveLoan() { - if (hub.hasTag(msg.sender, PWNHubTags.ACTIVE_LOAN) == false) - revert CallerMissingHubTag(PWNHubTags.ACTIVE_LOAN); - _; - } - - modifier onlyWithTag(bytes32 tag) { - if (hub.hasTag(msg.sender, tag) == false) - revert CallerMissingHubTag(tag); - _; - } - - - /*----------------------------------------------------------*| - |* # CONSTRUCTOR *| - |*----------------------------------------------------------*/ - - constructor(address pwnHub) { - hub = PWNHub(pwnHub); - } - -} diff --git a/src/loan/token/PWNLOAN.sol b/src/loan/token/PWNLOAN.sol index 3f02cec..dde9b31 100644 --- a/src/loan/token/PWNLOAN.sol +++ b/src/loan/token/PWNLOAN.sol @@ -3,10 +3,10 @@ pragma solidity 0.8.16; import { ERC721 } from "openzeppelin/token/ERC721/ERC721.sol"; -import { PWNHubAccessControl } from "pwn/hub/PWNHubAccessControl.sol"; +import { PWNHub } from "pwn/hub/PWNHub.sol"; +import { PWNHubTags } from "pwn/hub/PWNHubTags.sol"; import { IERC5646 } from "pwn/interfaces/IERC5646.sol"; import { IPWNLoanMetadataProvider } from "pwn/interfaces/IPWNLoanMetadataProvider.sol"; -import { InvalidLoanContractCaller } from "pwn/PWNErrors.sol"; /** @@ -15,12 +15,14 @@ import { InvalidLoanContractCaller } from "pwn/PWNErrors.sol"; * @dev Token doesn't hold any loan logic, just an address of a loan contract that minted the LOAN token. * PWN LOAN token is shared between all loan contracts. */ -contract PWNLOAN is PWNHubAccessControl, IERC5646, ERC721 { +contract PWNLOAN is ERC721, IERC5646 { /*----------------------------------------------------------*| |* # VARIABLES & CONSTANTS DEFINITIONS *| |*----------------------------------------------------------*/ + PWNHub public immutable hub; + /** * @dev Last used LOAN id. First LOAN id is 1. This value is incremental. */ @@ -48,11 +50,37 @@ contract PWNLOAN is PWNHubAccessControl, IERC5646, ERC721 { /*----------------------------------------------------------*| - |* # CONSTRUCTOR *| + |* # ERRORS DEFINITIONS *| + |*----------------------------------------------------------*/ + + /** + * @notice Thrown when `PWNLOAN.burn` caller is not a loan contract that minted the LOAN token. + */ + error InvalidLoanContractCaller(); + + /** + * @notice Thrown when caller is missing a PWN Hub tag. + */ + error CallerMissingHubTag(bytes32 tag); + + + /*----------------------------------------------------------*| + |* # MODIFIERS *| |*----------------------------------------------------------*/ - constructor(address hub) PWNHubAccessControl(hub) ERC721("PWN LOAN", "LOAN") { + modifier onlyActiveLoan() { + if (!hub.hasTag(msg.sender, PWNHubTags.ACTIVE_LOAN)) + revert CallerMissingHubTag({ tag: PWNHubTags.ACTIVE_LOAN }); + _; + } + + + /*----------------------------------------------------------*| + |* # CONSTRUCTOR *| + |*----------------------------------------------------------*/ + constructor(address _hub) ERC721("PWN LOAN", "LOAN") { + hub = PWNHub(_hub); } diff --git a/test/unit/PWNLOAN.t.sol b/test/unit/PWNLOAN.t.sol index ae0df96..a418036 100644 --- a/test/unit/PWNLOAN.t.sol +++ b/test/unit/PWNLOAN.t.sol @@ -6,7 +6,6 @@ import { Test } from "forge-std/Test.sol"; import { PWNHubTags } from "pwn/hub/PWNHubTags.sol"; import { IERC5646 } from "pwn/interfaces/IERC5646.sol"; import { PWNLOAN } from "pwn/loan/token/PWNLOAN.sol"; -import { CallerMissingHubTag, InvalidLoanContractCaller } from "pwn/PWNErrors.sol"; abstract contract PWNLOANTest is Test { @@ -74,7 +73,7 @@ contract PWNLOAN_Mint_Test is PWNLOANTest { function test_shouldFail_whenCallerIsNotActiveLoanContract() external { vm.expectRevert( - abi.encodeWithSelector(CallerMissingHubTag.selector, PWNHubTags.ACTIVE_LOAN) + abi.encodeWithSelector(PWNLOAN.CallerMissingHubTag.selector, PWNHubTags.ACTIVE_LOAN) ); vm.prank(alice); loanToken.mint(alice); @@ -148,7 +147,7 @@ contract PWNLOAN_Burn_Test is PWNLOANTest { function test_shouldFail_whenCallerIsNotStoredLoanContractForGivenLoanId() external { vm.expectRevert( - abi.encodeWithSelector(InvalidLoanContractCaller.selector) + abi.encodeWithSelector(PWNLOAN.InvalidLoanContractCaller.selector) ); vm.prank(alice); loanToken.burn(loanId); From 7ba25a0d3570f6f08b03db4438bf5711fd0fff50 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Tue, 21 May 2024 20:33:09 +0200 Subject: [PATCH 119/129] refactor: move custom errors to its contract files --- src/PWNErrors.sol | 15 --------------- src/hub/PWNHub.sol | 14 +++++++++++--- test/unit/PWNHub.t.sol | 3 +-- 3 files changed, 12 insertions(+), 20 deletions(-) diff --git a/src/PWNErrors.sol b/src/PWNErrors.sol index 97cd1dc..9ea9808 100644 --- a/src/PWNErrors.sol +++ b/src/PWNErrors.sol @@ -2,26 +2,11 @@ pragma solidity 0.8.16; -/** - * @notice Thrown when caller is missing a PWN Hub tag. - */ -error CallerMissingHubTag(bytes32); - /** * @notice Thrown when an address is missing a PWN Hub tag. */ error AddressMissingHubTag(address addr, bytes32 tag); -/** - * @notice Thrown when `PWNLOAN.burn` caller is not a loan contract that minted the LOAN token. - */ -error InvalidLoanContractCaller(); - -/** - * @notice Thrown when `PWNHub.setTags` inputs lengths are not equal. - */ -error InvalidInputData(); - /** * @notice Thrown when a proposal is expired. */ diff --git a/src/hub/PWNHub.sol b/src/hub/PWNHub.sol index d46e8f1..024bb4c 100644 --- a/src/hub/PWNHub.sol +++ b/src/hub/PWNHub.sol @@ -3,8 +3,6 @@ pragma solidity 0.8.16; import { Ownable2Step } from "openzeppelin/access/Ownable2Step.sol"; -import { InvalidInputData } from "pwn/PWNErrors.sol"; - /** * @title PWN Hub @@ -23,7 +21,7 @@ contract PWNHub is Ownable2Step { /*----------------------------------------------------------*| - |* # EVENTS & ERRORS DEFINITIONS *| + |* # EVENTS DEFINITIONS *| |*----------------------------------------------------------*/ /** @@ -32,6 +30,16 @@ contract PWNHub is Ownable2Step { event TagSet(address indexed _address, bytes32 indexed tag, bool hasTag); + /*----------------------------------------------------------*| + |* # ERRORS DEFINITIONS *| + |*----------------------------------------------------------*/ + + /** + * @notice Thrown when `PWNHub.setTags` inputs lengths are not equal. + */ + error InvalidInputData(); + + /*----------------------------------------------------------*| |* # CONSTRUCTOR *| |*----------------------------------------------------------*/ diff --git a/test/unit/PWNHub.t.sol b/test/unit/PWNHub.t.sol index d9f6cc9..e531aec 100644 --- a/test/unit/PWNHub.t.sol +++ b/test/unit/PWNHub.t.sol @@ -4,7 +4,6 @@ pragma solidity 0.8.16; import { Test } from "forge-std/Test.sol"; import { PWNHub } from "pwn/hub/PWNHub.sol"; -import { InvalidInputData } from "pwn/PWNErrors.sol"; abstract contract PWNHubTest is Test { @@ -138,7 +137,7 @@ contract PWNHub_SetTags_Test is PWNHubTest { function test_shouldFail_whenDiffInputLengths() external { address[] memory addrs_ = new address[](3); - vm.expectRevert(abi.encodeWithSelector(InvalidInputData.selector)); + vm.expectRevert(abi.encodeWithSelector(PWNHub.InvalidInputData.selector)); vm.prank(owner); hub.setTags(addrs_, tags, true); } From 1a5bd9a4cf956c4e84d4cb9cc7260e4dbf157a49 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Tue, 21 May 2024 20:34:23 +0200 Subject: [PATCH 120/129] build: update foundry config --- foundry.toml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/foundry.toml b/foundry.toml index f66d91a..586fbad 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,7 +2,6 @@ solc_version = '0.8.16' fs_permissions = [{ access = "read", path = "./deployments/latest.json"}] gas_reports = ["PWNSimpleLoan"] -cbor_metadata = true [rpc_endpoints] @@ -25,6 +24,3 @@ mantle_testnet = "${MANTLE_TESTNET_URL}" tenderly = "${TENDERLY_URL}" local = "${LOCAL_URL}" - -[etherscan] -unknown_chain = { key = "${TENDERLY_ACCESS_TOKEN}", chain = 11155111, url = "${TENDERLY_URL}" } From 8a614c7a070e8333103692cf370b4e2575a6faa0 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Wed, 22 May 2024 11:02:14 +0200 Subject: [PATCH 121/129] feat: implement revoking multiple nonces at once --- src/nonce/PWNRevokedNonce.sol | 16 ++++++--- test/unit/PWNRevokedNonce.t.sol | 64 +++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 4 deletions(-) diff --git a/src/nonce/PWNRevokedNonce.sol b/src/nonce/PWNRevokedNonce.sol index 69a90c9..8cb618d 100644 --- a/src/nonce/PWNRevokedNonce.sol +++ b/src/nonce/PWNRevokedNonce.sol @@ -97,8 +97,7 @@ contract PWNRevokedNonce { |*----------------------------------------------------------*/ /** - * @notice Revoke a nonce in the current nonce space. - * @dev Caller is used as a nonce owner. + * @notice Revoke callers nonce in the current nonce space. * @param nonce Nonce to be revoked. */ function revokeNonce(uint256 nonce) external { @@ -106,8 +105,17 @@ contract PWNRevokedNonce { } /** - * @notice Revoke a nonce in a nonce space. - * @dev Caller is used as a nonce owner. + * @notice Revoke multiple caller nonces in the current nonce space. + * @param nonces List of nonces to be revoked. + */ + function revokeNonces(uint256[] calldata nonces) external { + for (uint256 i; i < nonces.length; ++i) { + _revokeNonce(msg.sender, _nonceSpace[msg.sender], nonces[i]); + } + } + + /** + * @notice Revoke caller nonce in a nonce space. * @param nonceSpace Nonce space where a nonce will be revoked. * @param nonce Nonce to be revoked. */ diff --git a/test/unit/PWNRevokedNonce.t.sol b/test/unit/PWNRevokedNonce.t.sol index 0fa687e..4451ebb 100644 --- a/test/unit/PWNRevokedNonce.t.sol +++ b/test/unit/PWNRevokedNonce.t.sol @@ -83,6 +83,70 @@ contract PWNRevokedNonce_RevokeNonce_Test is PWNRevokedNonceTest { } +/*----------------------------------------------------------*| +|* # REVOKE NONCES *| +|*----------------------------------------------------------*/ + +contract PWNRevokedNonce_RevokeNonces_Test is PWNRevokedNonceTest { + + uint256[] nonces; + + function testFuzz_shouldFail_whenAnyNonceAlreadyRevoked(uint256 nonceSpace, uint256 nonce) external { + nonce = bound(nonce, 0, type(uint256).max - 1); + vm.store(address(revokedNonce), _nonceSpaceSlot(alice), bytes32(nonceSpace)); + vm.store(address(revokedNonce), _revokedNonceSlot(alice, nonceSpace, nonce), bytes32(uint256(1))); + + nonces = new uint256[](2); + nonces[0] = nonce; + nonces[1] = nonce + 1; + + vm.expectRevert(abi.encodeWithSelector(PWNRevokedNonce.NonceAlreadyRevoked.selector, alice, nonceSpace, nonce)); + vm.prank(alice); + revokedNonce.revokeNonces(nonces); + } + + function testFuzz_shouldStoreNoncesAsRevoked( + uint256 nonceSpace, uint256 nonce1, uint256 nonce2, uint256 nonce3 + ) external { + vm.assume(nonce1 != nonce2 && nonce2 != nonce3 && nonce1 != nonce3); + vm.store(address(revokedNonce), _nonceSpaceSlot(alice), bytes32(nonceSpace)); + + nonces = new uint256[](3); + nonces[0] = nonce1; + nonces[1] = nonce2; + nonces[2] = nonce3; + + vm.prank(alice); + revokedNonce.revokeNonces(nonces); + + assertTrue(revokedNonce.isNonceRevoked(alice, nonceSpace, nonce1)); + assertTrue(revokedNonce.isNonceRevoked(alice, nonceSpace, nonce2)); + assertTrue(revokedNonce.isNonceRevoked(alice, nonceSpace, nonce3)); + } + + function testFuzz_shouldEmit_NonceRevoked( + uint256 nonceSpace, uint256 nonce1, uint256 nonce2, uint256 nonce3 + ) external { + vm.assume(nonce1 != nonce2 && nonce2 != nonce3 && nonce1 != nonce3); + vm.store(address(revokedNonce), _nonceSpaceSlot(alice), bytes32(nonceSpace)); + + nonces = new uint256[](3); + nonces[0] = nonce1; + nonces[1] = nonce2; + nonces[2] = nonce3; + + for (uint256 i; i < nonces.length; ++i) { + vm.expectEmit(); + emit NonceRevoked(alice, nonceSpace, nonces[i]); + } + + vm.prank(alice); + revokedNonce.revokeNonces(nonces); + } + +} + + /*----------------------------------------------------------*| |* # REVOKE NONCE WITH NONCE SPACE *| |*----------------------------------------------------------*/ From e37fbc6d00741b9ec1bd0118a173544199829fec Mon Sep 17 00:00:00 2001 From: ashhanai Date: Wed, 22 May 2024 14:55:57 +0200 Subject: [PATCH 122/129] build: update multitoken lib to v3.0.1 --- lib/MultiToken | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/MultiToken b/lib/MultiToken index d4f604a..863dcd8 160000 --- a/lib/MultiToken +++ b/lib/MultiToken @@ -1 +1 @@ -Subproject commit d4f604a27caa5f22610d1a86d4d00973c819ca97 +Subproject commit 863dcd8b4c60494d1deda231fb95b48073d85659 From bdf0f8bd536700fa8b5e32b9ea2fa74a641f0db8 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Fri, 24 May 2024 15:45:40 +0200 Subject: [PATCH 123/129] refactor: update APR value type to uint24 and decrease decimals to 2 --- src/loan/terms/simple/loan/PWNSimpleLoan.sol | 16 +++---- .../PWNSimpleLoanDutchAuctionProposal.sol | 4 +- .../PWNSimpleLoanFungibleProposal.sol | 4 +- .../proposal/PWNSimpleLoanListProposal.sol | 4 +- .../proposal/PWNSimpleLoanSimpleProposal.sol | 4 +- test/unit/PWNSimpleLoan.t.sol | 48 +++++++++---------- 6 files changed, 40 insertions(+), 40 deletions(-) diff --git a/src/loan/terms/simple/loan/PWNSimpleLoan.sol b/src/loan/terms/simple/loan/PWNSimpleLoan.sol index 7574561..337b31f 100644 --- a/src/loan/terms/simple/loan/PWNSimpleLoan.sol +++ b/src/loan/terms/simple/loan/PWNSimpleLoan.sol @@ -37,9 +37,9 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { |*----------------------------------------------------------*/ uint32 public constant MIN_LOAN_DURATION = 10 minutes; - uint40 public constant MAX_ACCRUING_INTEREST_APR = 1e12; // 10,000,000 APR (with 5 decimals) + uint40 public constant MAX_ACCRUING_INTEREST_APR = 16e6; // 160,000 APR (with 2 decimals) - uint256 public constant ACCRUING_INTEREST_APR_DECIMALS = 1e5; + uint256 public constant ACCRUING_INTEREST_APR_DECIMALS = 1e2; uint256 public constant MINUTES_IN_YEAR = 525_600; // Note: Assuming 365 days in a year uint256 public constant ACCRUING_INTEREST_APR_DENOMINATOR = ACCRUING_INTEREST_APR_DECIMALS * MINUTES_IN_YEAR * 100; @@ -73,7 +73,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { * @param collateral Asset used as a loan collateral. For a definition see { MultiToken dependency lib }. * @param credit Asset used as a loan credit. For a definition see { MultiToken dependency lib }. * @param fixedInterestAmount Fixed interest amount in credit asset tokens. It is the minimum amount of interest which has to be paid by a borrower. - * @param accruingInterestAPR Accruing interest APR. + * @param accruingInterestAPR Accruing interest APR with 2 decimals. * @param lenderSpecHash Hash of a lender specification. * @param borrowerSpecHash Hash of a borrower specification. */ @@ -84,7 +84,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { MultiToken.Asset collateral; MultiToken.Asset credit; uint256 fixedInterestAmount; - uint40 accruingInterestAPR; + uint24 accruingInterestAPR; bytes32 lenderSpecHash; bytes32 borrowerSpecHash; } @@ -135,7 +135,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { * @param defaultTimestamp Unix timestamp (in seconds) of a default date. * @param borrower Address of a borrower. * @param originalLender Address of a lender that funded the loan. - * @param accruingInterestAPR Accruing interest APR. + * @param accruingInterestAPR Accruing interest APR with 2 decimals. * @param fixedInterestAmount Fixed interest amount in credit asset tokens. * It is the minimum amount of interest which has to be paid by a borrower. * This property is reused to store the final interest amount if the loan is repaid and waiting to be claimed. @@ -150,7 +150,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { uint40 defaultTimestamp; address borrower; address originalLender; - uint40 accruingInterestAPR; + uint24 accruingInterestAPR; uint256 fixedInterestAmount; uint256 principalAmount; MultiToken.Asset collateral; @@ -1075,7 +1075,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { * @return borrower Address of a loan borrower. * @return originalLender Address of a loan original lender. * @return loanOwner Address of a LOAN token holder. - * @return accruingInterestAPR Accruing interest APR with 5 decimal places. + * @return accruingInterestAPR Accruing interest APR with 2 decimal places. * @return fixedInterestAmount Fixed interest amount in credit asset tokens. * @return credit Asset used as a loan credit. For a definition see { MultiToken dependency lib }. * @return collateral Asset used as a loan collateral. For a definition see { MultiToken dependency lib }. @@ -1089,7 +1089,7 @@ contract PWNSimpleLoan is PWNVault, IERC5646, IPWNLoanMetadataProvider { address borrower, address originalLender, address loanOwner, - uint40 accruingInterestAPR, + uint24 accruingInterestAPR, uint256 fixedInterestAmount, MultiToken.Asset memory credit, MultiToken.Asset memory collateral, diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol index 3f5e0b5..de38492 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanDutchAuctionProposal.sol @@ -37,7 +37,7 @@ contract PWNSimpleLoanDutchAuctionProposal is PWNSimpleLoanProposal { * @param maxCreditAmount Maximum amount of tokens which is proposed as a loan to a borrower. If `isOffer` is true, auction will end with this amount, otherwise it will start with this amount. * @param availableCreditLimit Available credit limit for the proposal. It is the maximum amount of tokens which can be borrowed using the proposal. If non-zero, proposal can be accepted more than once, until the credit limit is reached. * @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 with 5 decimals. + * @param accruingInterestAPR Accruing interest APR with 2 decimals. * @param duration Loan duration in seconds. * @param auctionStart Auction start timestamp in seconds. * @param auctionDuration Auction duration in seconds. @@ -62,7 +62,7 @@ contract PWNSimpleLoanDutchAuctionProposal is PWNSimpleLoanProposal { uint256 maxCreditAmount; uint256 availableCreditLimit; uint256 fixedInterestAmount; - uint40 accruingInterestAPR; + uint24 accruingInterestAPR; uint32 duration; uint40 auctionStart; uint40 auctionDuration; diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol index 831ab71..df8d163 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanFungibleProposal.sol @@ -43,7 +43,7 @@ contract PWNSimpleLoanFungibleProposal is PWNSimpleLoanProposal { * @param creditPerCollateralUnit Amount of tokens which are offered per collateral unit with 38 decimals. * @param availableCreditLimit Available credit limit for the proposal. It is the maximum amount of tokens which can be borrowed using the proposal. If non-zero, proposal can be accepted more than once, until the credit limit is reached. * @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 with 5 decimals. + * @param accruingInterestAPR Accruing interest APR with 2 decimals. * @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. @@ -66,7 +66,7 @@ contract PWNSimpleLoanFungibleProposal is PWNSimpleLoanProposal { uint256 creditPerCollateralUnit; uint256 availableCreditLimit; uint256 fixedInterestAmount; - uint40 accruingInterestAPR; + uint24 accruingInterestAPR; uint32 duration; uint40 expiration; address allowedAcceptor; diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol index 7e4567d..9d34732 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol @@ -37,7 +37,7 @@ contract PWNSimpleLoanListProposal is PWNSimpleLoanProposal { * @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. If non-zero, proposal can be accepted more than once, until the credit limit is reached. * @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 with 5 decimals. + * @param accruingInterestAPR Accruing interest APR with 2 decimals. * @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. @@ -60,7 +60,7 @@ contract PWNSimpleLoanListProposal is PWNSimpleLoanProposal { uint256 creditAmount; uint256 availableCreditLimit; uint256 fixedInterestAmount; - uint40 accruingInterestAPR; + uint24 accruingInterestAPR; uint32 duration; uint40 expiration; address allowedAcceptor; diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol index c2ecda1..80d9ad1 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanSimpleProposal.sol @@ -34,7 +34,7 @@ contract PWNSimpleLoanSimpleProposal is PWNSimpleLoanProposal { * @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. If non-zero, proposal can be accepted more than once, until the credit limit is reached. * @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 with 5 decimals. + * @param accruingInterestAPR Accruing interest APR with 2 decimals. * @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. @@ -57,7 +57,7 @@ contract PWNSimpleLoanSimpleProposal is PWNSimpleLoanProposal { uint256 creditAmount; uint256 availableCreditLimit; uint256 fixedInterestAmount; - uint40 accruingInterestAPR; + uint24 accruingInterestAPR; uint32 duration; uint40 expiration; address allowedAcceptor; diff --git a/test/unit/PWNSimpleLoan.t.sol b/test/unit/PWNSimpleLoan.t.sol index f66f9ef..b6e71ff 100644 --- a/test/unit/PWNSimpleLoan.t.sol +++ b/test/unit/PWNSimpleLoan.t.sol @@ -217,7 +217,7 @@ abstract contract PWNSimpleLoanTest is Test { // Borrower address _assertLOANWord(loanSlot + 2, abi.encodePacked(uint96(0), _simpleLoan.borrower)); // Original lender, accruing interest daily rate - _assertLOANWord(loanSlot + 3, abi.encodePacked(uint56(0), _simpleLoan.accruingInterestAPR, _simpleLoan.originalLender)); + _assertLOANWord(loanSlot + 3, abi.encodePacked(uint72(0), _simpleLoan.accruingInterestAPR, _simpleLoan.originalLender)); // Fixed interest amount _assertLOANWord(loanSlot + 4, abi.encodePacked(_simpleLoan.fixedInterestAmount)); // Principal amount @@ -241,7 +241,7 @@ abstract contract PWNSimpleLoanTest is Test { // Borrower address _storeLOANWord(loanSlot + 2, abi.encodePacked(uint96(0), _simpleLoan.borrower)); // Original lender, accruing interest daily rate - _storeLOANWord(loanSlot + 3, abi.encodePacked(uint56(0), _simpleLoan.accruingInterestAPR, _simpleLoan.originalLender)); + _storeLOANWord(loanSlot + 3, abi.encodePacked(uint72(0), _simpleLoan.accruingInterestAPR, _simpleLoan.originalLender)); // Fixed interest amount _storeLOANWord(loanSlot + 4, abi.encodePacked(_simpleLoan.fixedInterestAmount)); // Principal amount @@ -456,8 +456,8 @@ contract PWNSimpleLoan_CreateLOAN_Test is PWNSimpleLoanTest { function testFuzz_shouldFail_whenLoanTermsInterestAPROutOfBounds(uint256 interestAPR) external { uint256 maxInterest = loan.MAX_ACCRUING_INTEREST_APR(); - interestAPR = bound(interestAPR, maxInterest + 1, type(uint40).max); - simpleLoanTerms.accruingInterestAPR = uint40(interestAPR); + interestAPR = bound(interestAPR, maxInterest + 1, type(uint24).max); + simpleLoanTerms.accruingInterestAPR = uint24(interestAPR); _mockLoanTerms(simpleLoanTerms); vm.expectRevert( @@ -1368,11 +1368,11 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { _days = bound(_days, 0, loanDurationInDays - 1); principal = bound(principal, 1, 1e40); fixedInterest = bound(fixedInterest, 0, 1e40); - interestAPR = bound(interestAPR, 1, 1e12); + interestAPR = bound(interestAPR, 1, 16e6); simpleLoan.principalAmount = principal; simpleLoan.fixedInterestAmount = fixedInterest; - simpleLoan.accruingInterestAPR = uint40(interestAPR); + simpleLoan.accruingInterestAPR = uint24(interestAPR); _mockLOAN(refinancingLoanId, simpleLoan); vm.warp(simpleLoan.startTimestamp + _days * 1 days); @@ -1412,11 +1412,11 @@ contract PWNSimpleLoan_RefinanceLOAN_Test is PWNSimpleLoanTest { _days = bound(_days, 0, loanDurationInDays - 1); principal = bound(principal, 1, 1e40); fixedInterest = bound(fixedInterest, 0, 1e40); - interestAPR = bound(interestAPR, 1, 1e12); + interestAPR = bound(interestAPR, 1, 16e6); simpleLoan.principalAmount = principal; simpleLoan.fixedInterestAmount = fixedInterest; - simpleLoan.accruingInterestAPR = uint40(interestAPR); + simpleLoan.accruingInterestAPR = uint24(interestAPR); _mockLOAN(refinancingLoanId, simpleLoan); vm.warp(simpleLoan.startTimestamp + _days * 1 days); @@ -1601,11 +1601,11 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { _days = bound(_days, 0, loanDurationInDays - 1); _principal = bound(_principal, 1, 1e40); _fixedInterest = bound(_fixedInterest, 0, 1e40); - _interestAPR = bound(_interestAPR, 1, 1e12); + _interestAPR = bound(_interestAPR, 1, 16e6); simpleLoan.principalAmount = _principal; simpleLoan.fixedInterestAmount = _fixedInterest; - simpleLoan.accruingInterestAPR = uint40(_interestAPR); + simpleLoan.accruingInterestAPR = uint24(_interestAPR); _mockLOAN(loanId, simpleLoan); vm.warp(simpleLoan.startTimestamp + _days * 1 days); @@ -1641,11 +1641,11 @@ contract PWNSimpleLoan_RepayLOAN_Test is PWNSimpleLoanTest { _days = bound(_days, 0, loanDurationInDays - 1); _principal = bound(_principal, 1, 1e40); _fixedInterest = bound(_fixedInterest, 0, 1e40); - _interestAPR = bound(_interestAPR, 1, 1e12); + _interestAPR = bound(_interestAPR, 1, 16e6); simpleLoan.principalAmount = _principal; simpleLoan.fixedInterestAmount = _fixedInterest; - simpleLoan.accruingInterestAPR = uint40(_interestAPR); + simpleLoan.accruingInterestAPR = uint24(_interestAPR); _mockLOAN(loanId, simpleLoan); vm.warp(simpleLoan.startTimestamp + _days * 1 days); @@ -1756,17 +1756,17 @@ contract PWNSimpleLoan_LoanRepaymentAmount_Test is PWNSimpleLoanTest { _minutes = bound(_minutes, 0, 2 * loanDurationInDays * 24 * 60); // should return non zero value even after loan expiration _principal = bound(_principal, 1, 1e40); _fixedInterest = bound(_fixedInterest, 0, 1e40); - _interestAPR = bound(_interestAPR, 1, 1e12); + _interestAPR = bound(_interestAPR, 1, 16e6); simpleLoan.defaultTimestamp = simpleLoan.startTimestamp + 101 * 1 days; simpleLoan.principalAmount = _principal; simpleLoan.fixedInterestAmount = _fixedInterest; - simpleLoan.accruingInterestAPR = uint40(_interestAPR); + simpleLoan.accruingInterestAPR = uint24(_interestAPR); _mockLOAN(loanId, simpleLoan); vm.warp(simpleLoan.startTimestamp + _minutes * 1 minutes + 1); - uint256 expectedInterest = _fixedInterest + _principal * _interestAPR * _minutes / (1e5 * 60 * 24 * 365) / 100; + uint256 expectedInterest = _fixedInterest + _principal * _interestAPR * _minutes / (1e2 * 60 * 24 * 365) / 100; uint256 expectedLoanRepaymentAmount = _principal + expectedInterest; assertEq(loan.loanRepaymentAmount(loanId), expectedLoanRepaymentAmount); } @@ -1775,7 +1775,7 @@ contract PWNSimpleLoan_LoanRepaymentAmount_Test is PWNSimpleLoanTest { simpleLoan.defaultTimestamp = simpleLoan.startTimestamp + 101 * 1 days; simpleLoan.principalAmount = 100e18; simpleLoan.fixedInterestAmount = 10e18; - simpleLoan.accruingInterestAPR = uint40(365e5); + simpleLoan.accruingInterestAPR = uint24(365e2); _mockLOAN(loanId, simpleLoan); vm.warp(simpleLoan.startTimestamp); @@ -1784,7 +1784,7 @@ contract PWNSimpleLoan_LoanRepaymentAmount_Test is PWNSimpleLoanTest { vm.warp(simpleLoan.startTimestamp + 1 days); assertEq(loan.loanRepaymentAmount(loanId), simpleLoan.principalAmount + simpleLoan.fixedInterestAmount + 1e18); - simpleLoan.accruingInterestAPR = uint40(100e5); + simpleLoan.accruingInterestAPR = uint24(100e2); _mockLOAN(loanId, simpleLoan); vm.warp(simpleLoan.startTimestamp + 365 days); @@ -2495,7 +2495,7 @@ contract PWNSimpleLoan_GetLOAN_Test is PWNSimpleLoanTest { uint40 _defaultTimestamp, address _borrower, address _originalLender, - uint40 _accruingInterestAPR, + uint24 _accruingInterestAPR, uint256 _fixedInterestAmount, address _creditAddress, uint256 _principalAmount, @@ -2506,7 +2506,7 @@ contract PWNSimpleLoan_GetLOAN_Test is PWNSimpleLoanTest { ) external { _startTimestamp = uint40(bound(_startTimestamp, 0, type(uint40).max - 1)); _defaultTimestamp = uint40(bound(_defaultTimestamp, _startTimestamp + 1, type(uint40).max)); - _accruingInterestAPR = uint40(bound(_accruingInterestAPR, 0, 1e12)); + _accruingInterestAPR = uint24(bound(_accruingInterestAPR, 0, 16e6)); _fixedInterestAmount = bound(_fixedInterestAmount, 0, type(uint256).max - _principalAmount); simpleLoan.startTimestamp = _startTimestamp; @@ -2543,7 +2543,7 @@ contract PWNSimpleLoan_GetLOAN_Test is PWNSimpleLoanTest { assertEq(originalLender, _originalLender); } { - (,,,,,, uint40 accruingInterestAPR,,,,,) = loan.getLOAN(loanId); + (,,,,,, uint24 accruingInterestAPR,,,,,) = loan.getLOAN(loanId); assertEq(accruingInterestAPR, _accruingInterestAPR); } { @@ -2607,12 +2607,12 @@ contract PWNSimpleLoan_GetLOAN_Test is PWNSimpleLoanTest { function testFuzz_shouldReturnRepaymentAmount( uint256 _days, uint256 _principalAmount, - uint40 _accruingInterestAPR, + uint24 _accruingInterestAPR, uint256 _fixedInterestAmount ) external { _days = bound(_days, 0, 2 * loanDurationInDays); _principalAmount = bound(_principalAmount, 1, 1e40); - _accruingInterestAPR = uint40(bound(_accruingInterestAPR, 0, 1e12)); + _accruingInterestAPR = uint24(bound(_accruingInterestAPR, 0, 16e6)); _fixedInterestAmount = bound(_fixedInterestAmount, 0, _principalAmount); simpleLoan.accruingInterestAPR = _accruingInterestAPR; @@ -2636,7 +2636,7 @@ contract PWNSimpleLoan_GetLOAN_Test is PWNSimpleLoanTest { address borrower, address originalLender, address loanOwner, - uint40 accruingInterestAPR, + uint24 accruingInterestAPR, uint256 fixedInterestAmount, MultiToken.Asset memory credit, MultiToken.Asset memory collateral, @@ -2733,7 +2733,7 @@ contract PWNSimpleLoan_GetStateFingerprint_Test is PWNSimpleLoanTest { } function testFuzz_shouldReturnCorrectStateFingerprint( - uint256 fixedInterestAmount, uint40 accruingInterestAPR + uint256 fixedInterestAmount, uint24 accruingInterestAPR ) external { simpleLoan.fixedInterestAmount = fixedInterestAmount; simpleLoan.accruingInterestAPR = accruingInterestAPR; From f06e6eb0faa9388cc30cf5a184aa6f98415029f3 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Tue, 11 Jun 2024 14:38:26 +0200 Subject: [PATCH 124/129] ci: update deployment script --- script/PWN.s.sol | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/script/PWN.s.sol b/script/PWN.s.sol index 8c67812..6e79349 100644 --- a/script/PWN.s.sol +++ b/script/PWN.s.sol @@ -130,17 +130,17 @@ forge script script/PWN.s.sol:Deploy \ abi.encode(deployment.deployer, vm.addr(initialConfigHelper), "") ) })); - address configSingleton = _deploy({ + deployment.configSingleton = PWNConfig(_deploy({ salt: PWNContractDeployerSalt.CONFIG, bytecode: type(PWNConfig).creationCode - }); + })); vm.stopBroadcast(); vm.startBroadcast(initialConfigHelper); ITransparentUpgradeableProxy(address(deployment.config)).upgradeToAndCall( - configSingleton, + address(deployment.configSingleton), abi.encodeWithSelector(PWNConfig.initialize.selector, deployment.protocolTimelock, 0, deployment.daoSafe) ); ITransparentUpgradeableProxy(address(deployment.config)).changeAdmin(deployment.adminTimelock); @@ -230,7 +230,7 @@ forge script script/PWN.s.sol:Deploy \ })); console2.log("MultiToken Category Registry:", address(deployment.categoryRegistry)); - console2.log("PWNConfig - singleton:", configSingleton); + console2.log("PWNConfig - singleton:", address(deployment.configSingleton)); console2.log("PWNConfig - proxy:", address(deployment.config)); console2.log("PWNHub:", address(deployment.hub)); console2.log("PWNLOAN:", address(deployment.loanToken)); @@ -427,7 +427,7 @@ contract Setup is Deployments, Script { /* forge script script/PWN.s.sol:Setup \ --sig "setupNewProtocolVersion()" \ ---rpc-url $TENDERLY_URL \ +--rpc-url $RPC_URL \ --private-key $PRIVATE_KEY \ --broadcast */ @@ -442,7 +442,7 @@ forge script script/PWN.s.sol:Setup \ vm.startBroadcast(); _acceptOwnership(deployment.daoSafe, deployment.protocolTimelock, address(deployment.categoryRegistry)); - _setTags(); + _setTags(true); vm.stopBroadcast(); } @@ -467,8 +467,23 @@ forge script script/PWN.s.sol:Setup \ _acceptOwnership(deployment.daoSafe, deployment.protocolTimelock, address(deployment.categoryRegistry)); _acceptOwnership(deployment.daoSafe, deployment.protocolTimelock, address(deployment.hub)); - _setTags(); + _setTags(true); + + vm.stopBroadcast(); + } +/* +forge script script/PWN.s.sol:Setup \ +--sig "removeCurrentLoanProposalTags()" \ +--rpc-url $RPC_URL \ +--private-key $PRIVATE_KEY \ +--broadcast +*/ + function removeCurrentLoanProposalTags() external { + _loadDeployedAddresses(); + + vm.startBroadcast(); + _setTags(false); vm.stopBroadcast(); } @@ -481,7 +496,7 @@ forge script script/PWN.s.sol:Setup \ console2.log("Accept ownership tx succeeded"); } - function _setTags() internal { + function _setTags(bool set) internal { require(address(deployment.simpleLoan) != address(0), "Simple loan not set"); require(address(deployment.simpleLoanSimpleProposal) != address(0), "Simple loan simple proposal not set"); require(address(deployment.simpleLoanListProposal) != address(0), "Simple loan list proposal not set"); @@ -526,7 +541,7 @@ forge script script/PWN.s.sol:Setup \ TimelockController(payable(deployment.protocolTimelock)).scheduleAndExecute( GnosisSafeLike(deployment.daoSafe), address(deployment.hub), - abi.encodeWithSignature("setTags(address[],bytes32[],bool)", addrs, tags, true) + abi.encodeWithSignature("setTags(address[],bytes32[],bool)", addrs, tags, set) ); console2.log("Tags set succeeded"); } From d5b8b161b202c7427dc6a31120de74a02417aded Mon Sep 17 00:00:00 2001 From: ashhanai Date: Tue, 11 Jun 2024 14:39:05 +0200 Subject: [PATCH 125/129] ci: update latest deployed addresses --- deployments/latest.json | 167 ++++++++++++++++++---------------------- 1 file changed, 74 insertions(+), 93 deletions(-) diff --git a/deployments/latest.json b/deployments/latest.json index 688c77a..214cac6 100644 --- a/deployments/latest.json +++ b/deployments/latest.json @@ -1,5 +1,5 @@ { - "deployedChains": [1, 10, 25, 56, 137, 8453, 42161, 162314, 11155111], + "deployedChains": [1, 10, 25, 56, 137, 8453, 42161, 11155111], "chains": { "1": { "dao": "0x1B8383D2726E7e18189205337424a2631A2102F4", @@ -8,17 +8,17 @@ "daoSafe": "0xd56635c0E91D31F88B89F195D3993a9e34516e59", "deployerSafe": "0x42Cad20c964067f8e8b5c3E13fd0aa3C20a964C4", "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", - "categoryRegistry": "0x0000000000000000000000000000000000000000", - "configSingleton": "0x0000000000000000000000000000000000000000", - "config": "0x0000000000000000000000000000000000000000", + "categoryRegistry": "0xbB2168d5546A94AE2DA9254e63D88F7f137B2534", + "configSingleton": "0x1f5febF0efA3aD487508b6Cc7f39a0a54DE9De72", + "config": "0xd52a2898d61636bB3eEF0d145f05352FF543bdCC", "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", - "revokedNonce": "0x0000000000000000000000000000000000000000", - "simpleLoan": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleProposal": "0x0000000000000000000000000000000000000000", - "simpleLoanListProposal": "0x0000000000000000000000000000000000000000", - "simpleLoanFungibleProposal": "0x0000000000000000000000000000000000000000", - "simpleLoanDutchAuctionProposal": "0x0000000000000000000000000000000000000000" + "revokedNonce": "0x972204fF33348ee6889B2d0A3967dB67d7b08e4c", + "simpleLoan": "0x9A93AE395F09C6F350E3306aec592763c517072e", + "simpleLoanSimpleProposal": "0xEb3e6B9B51911175F3a121b5Efb46Fa354520f41", + "simpleLoanListProposal": "0x0E6cE603d328de0D357D624F10f3f448855fFBDC", + "simpleLoanFungibleProposal": "0x0618504Fa17888ec36AC5D46A4A0Ed59436Fb77E", + "simpleLoanDutchAuctionProposal": "0x807eb2A61B2d0193b0696436BeFFcFE4d6D520CB" }, "10": { "dao": "0x0000000000000000000000000000000000000000", @@ -27,17 +27,17 @@ "daoSafe": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", "deployerSafe": "0x1B4B37738De3bb9E6a7a4f99aFe4C145734c071d", "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", - "categoryRegistry": "0x0000000000000000000000000000000000000000", - "configSingleton": "0x0000000000000000000000000000000000000000", - "config": "0x0000000000000000000000000000000000000000", + "categoryRegistry": "0xbB2168d5546A94AE2DA9254e63D88F7f137B2534", + "configSingleton": "0x1f5febF0efA3aD487508b6Cc7f39a0a54DE9De72", + "config": "0xd52a2898d61636bB3eEF0d145f05352FF543bdCC", "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", - "revokedNonce": "0x0000000000000000000000000000000000000000", - "simpleLoan": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleProposal": "0x0000000000000000000000000000000000000000", - "simpleLoanListProposal": "0x0000000000000000000000000000000000000000", - "simpleLoanFungibleProposal": "0x0000000000000000000000000000000000000000", - "simpleLoanDutchAuctionProposal": "0x0000000000000000000000000000000000000000" + "revokedNonce": "0x972204fF33348ee6889B2d0A3967dB67d7b08e4c", + "simpleLoan": "0x9A93AE395F09C6F350E3306aec592763c517072e", + "simpleLoanSimpleProposal": "0xEb3e6B9B51911175F3a121b5Efb46Fa354520f41", + "simpleLoanListProposal": "0x0E6cE603d328de0D357D624F10f3f448855fFBDC", + "simpleLoanFungibleProposal": "0x0618504Fa17888ec36AC5D46A4A0Ed59436Fb77E", + "simpleLoanDutchAuctionProposal": "0x807eb2A61B2d0193b0696436BeFFcFE4d6D520CB" }, "25": { "dao": "0x0000000000000000000000000000000000000000", @@ -46,17 +46,17 @@ "daoSafe": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", "deployerSafe": "0x1B4B37738De3bb9E6a7a4f99aFe4C145734c071d", "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", - "categoryRegistry": "0x0000000000000000000000000000000000000000", - "configSingleton": "0x0000000000000000000000000000000000000000", - "config": "0x0000000000000000000000000000000000000000", + "categoryRegistry": "0xbB2168d5546A94AE2DA9254e63D88F7f137B2534", + "configSingleton": "0x1f5febF0efA3aD487508b6Cc7f39a0a54DE9De72", + "config": "0xd52a2898d61636bB3eEF0d145f05352FF543bdCC", "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", - "revokedNonce": "0x0000000000000000000000000000000000000000", - "simpleLoan": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleProposal": "0x0000000000000000000000000000000000000000", - "simpleLoanListProposal": "0x0000000000000000000000000000000000000000", - "simpleLoanFungibleProposal": "0x0000000000000000000000000000000000000000", - "simpleLoanDutchAuctionProposal": "0x0000000000000000000000000000000000000000" + "revokedNonce": "0x972204fF33348ee6889B2d0A3967dB67d7b08e4c", + "simpleLoan": "0x9A93AE395F09C6F350E3306aec592763c517072e", + "simpleLoanSimpleProposal": "0xEb3e6B9B51911175F3a121b5Efb46Fa354520f41", + "simpleLoanListProposal": "0x0E6cE603d328de0D357D624F10f3f448855fFBDC", + "simpleLoanFungibleProposal": "0x0618504Fa17888ec36AC5D46A4A0Ed59436Fb77E", + "simpleLoanDutchAuctionProposal": "0x807eb2A61B2d0193b0696436BeFFcFE4d6D520CB" }, "56": { "dao": "0x0000000000000000000000000000000000000000", @@ -65,17 +65,17 @@ "daoSafe": "0xd56635c0E91D31F88B89F195D3993a9e34516e59", "deployerSafe": "0x42Cad20c964067f8e8b5c3E13fd0aa3C20a964C4", "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", - "categoryRegistry": "0x0000000000000000000000000000000000000000", - "configSingleton": "0x0000000000000000000000000000000000000000", - "config": "0x0000000000000000000000000000000000000000", + "categoryRegistry": "0xbB2168d5546A94AE2DA9254e63D88F7f137B2534", + "configSingleton": "0x1f5febF0efA3aD487508b6Cc7f39a0a54DE9De72", + "config": "0xd52a2898d61636bB3eEF0d145f05352FF543bdCC", "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", - "revokedNonce": "0x0000000000000000000000000000000000000000", - "simpleLoan": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleProposal": "0x0000000000000000000000000000000000000000", - "simpleLoanListProposal": "0x0000000000000000000000000000000000000000", - "simpleLoanFungibleProposal": "0x0000000000000000000000000000000000000000", - "simpleLoanDutchAuctionProposal": "0x0000000000000000000000000000000000000000" + "revokedNonce": "0x972204fF33348ee6889B2d0A3967dB67d7b08e4c", + "simpleLoan": "0x9A93AE395F09C6F350E3306aec592763c517072e", + "simpleLoanSimpleProposal": "0xEb3e6B9B51911175F3a121b5Efb46Fa354520f41", + "simpleLoanListProposal": "0x0E6cE603d328de0D357D624F10f3f448855fFBDC", + "simpleLoanFungibleProposal": "0x0618504Fa17888ec36AC5D46A4A0Ed59436Fb77E", + "simpleLoanDutchAuctionProposal": "0x807eb2A61B2d0193b0696436BeFFcFE4d6D520CB" }, "137": { "dao": "0x0000000000000000000000000000000000000000", @@ -84,17 +84,17 @@ "daoSafe": "0xd56635c0E91D31F88B89F195D3993a9e34516e59", "deployerSafe": "0x42Cad20c964067f8e8b5c3E13fd0aa3C20a964C4", "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", - "categoryRegistry": "0x0000000000000000000000000000000000000000", - "configSingleton": "0x0000000000000000000000000000000000000000", - "config": "0x0000000000000000000000000000000000000000", + "categoryRegistry": "0xbB2168d5546A94AE2DA9254e63D88F7f137B2534", + "configSingleton": "0x1f5febF0efA3aD487508b6Cc7f39a0a54DE9De72", + "config": "0xd52a2898d61636bB3eEF0d145f05352FF543bdCC", "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", - "revokedNonce": "0x0000000000000000000000000000000000000000", - "simpleLoan": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleProposal": "0x0000000000000000000000000000000000000000", - "simpleLoanListProposal": "0x0000000000000000000000000000000000000000", - "simpleLoanFungibleProposal": "0x0000000000000000000000000000000000000000", - "simpleLoanDutchAuctionProposal": "0x0000000000000000000000000000000000000000" + "revokedNonce": "0x972204fF33348ee6889B2d0A3967dB67d7b08e4c", + "simpleLoan": "0x9A93AE395F09C6F350E3306aec592763c517072e", + "simpleLoanSimpleProposal": "0xEb3e6B9B51911175F3a121b5Efb46Fa354520f41", + "simpleLoanListProposal": "0x0E6cE603d328de0D357D624F10f3f448855fFBDC", + "simpleLoanFungibleProposal": "0x0618504Fa17888ec36AC5D46A4A0Ed59436Fb77E", + "simpleLoanDutchAuctionProposal": "0x807eb2A61B2d0193b0696436BeFFcFE4d6D520CB" }, "8453": { "dao": "0x0000000000000000000000000000000000000000", @@ -103,55 +103,36 @@ "daoSafe": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", "deployerSafe": "0x1B4B37738De3bb9E6a7a4f99aFe4C145734c071d", "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", - "categoryRegistry": "0x0000000000000000000000000000000000000000", - "configSingleton": "0x0000000000000000000000000000000000000000", - "config": "0x0000000000000000000000000000000000000000", + "categoryRegistry": "0xbB2168d5546A94AE2DA9254e63D88F7f137B2534", + "configSingleton": "0x1f5febF0efA3aD487508b6Cc7f39a0a54DE9De72", + "config": "0xd52a2898d61636bB3eEF0d145f05352FF543bdCC", "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", - "revokedNonce": "0x0000000000000000000000000000000000000000", - "simpleLoan": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleProposal": "0x0000000000000000000000000000000000000000", - "simpleLoanListProposal": "0x0000000000000000000000000000000000000000", - "simpleLoanFungibleProposal": "0x0000000000000000000000000000000000000000", - "simpleLoanDutchAuctionProposal": "0x0000000000000000000000000000000000000000" + "revokedNonce": "0x972204fF33348ee6889B2d0A3967dB67d7b08e4c", + "simpleLoan": "0x9A93AE395F09C6F350E3306aec592763c517072e", + "simpleLoanSimpleProposal": "0xEb3e6B9B51911175F3a121b5Efb46Fa354520f41", + "simpleLoanListProposal": "0x0E6cE603d328de0D357D624F10f3f448855fFBDC", + "simpleLoanFungibleProposal": "0x0618504Fa17888ec36AC5D46A4A0Ed59436Fb77E", + "simpleLoanDutchAuctionProposal": "0x807eb2A61B2d0193b0696436BeFFcFE4d6D520CB" }, "42161": { "dao": "0x0000000000000000000000000000000000000000", "adminTimelock": "0xd57e72A328AB1deC6b374c2babe2dc489819B5Ea", - "protocolTimelock": "0xd8dbddf1c0fddf9b5ecfa5c067c38db66739fbab", + "protocolTimelock": "0xd8dbdDf1c0FDdf9b5eCFA5C067C38DB66739FBAB", "daoSafe": "0xd56635c0E91D31F88B89F195D3993a9e34516e59", "deployerSafe": "0x42Cad20c964067f8e8b5c3E13fd0aa3C20a964C4", "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", - "categoryRegistry": "0x0000000000000000000000000000000000000000", - "configSingleton": "0x0000000000000000000000000000000000000000", - "config": "0x0000000000000000000000000000000000000000", - "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", - "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", - "revokedNonce": "0x0000000000000000000000000000000000000000", - "simpleLoan": "0x0000000000000000000000000000000000000000", - "simpleLoanSimpleProposal": "0x0000000000000000000000000000000000000000", - "simpleLoanListProposal": "0x0000000000000000000000000000000000000000", - "simpleLoanFungibleProposal": "0x0000000000000000000000000000000000000000", - "simpleLoanDutchAuctionProposal": "0x0000000000000000000000000000000000000000" - }, - "162314": { - "dao": "0x0000000000000000000000000000000000000000", - "adminTimelock": "0xd57e72A328AB1deC6b374c2babe2dc489819B5Ea", - "protocolTimelock": "0xd8dbdDf1c0FDdf9b5eCFA5C067C38DB66739FBAB", - "daoSafe": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", - "deployerSafe": "0x1B4B37738De3bb9E6a7a4f99aFe4C145734c071d", - "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", - "categoryRegistry": "0x1b71A2a08A6fb54b5a8615A48b13447a7b1E891E", - "configSingleton": "0xBdac2fb31E493f92370ED5ECb3EA63e63ae32617", - "config": "0x4ca21fD91F7F6446594E0ff93dD3147fbC2ca7Bb", + "categoryRegistry": "0xbB2168d5546A94AE2DA9254e63D88F7f137B2534", + "configSingleton": "0x1f5febF0efA3aD487508b6Cc7f39a0a54DE9De72", + "config": "0xd52a2898d61636bB3eEF0d145f05352FF543bdCC", "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", - "revokedNonce": "0xBAabf06494A2b1407AA740Ae7B04aF51D1Fd0d5F", - "simpleLoan": "0x18E6EeF41ef2D6B9ab9BC5A02f82E447473D0436", - "simpleLoanSimpleProposal": "0x038F9929CfD6af748EFE5384C4F29f67337F6887", - "simpleLoanListProposal": "0x1b02D009cd20e1EA9A0a9a9d37b03b0fe74Db5C6", - "simpleLoanFungibleProposal": "0xc53ec196368e8767576f80787A1924b87A101d3F", - "simpleLoanDutchAuctionProposal": "0x85670ed4a10522C0c152a34cd285c298Ac18bd55" + "revokedNonce": "0x972204fF33348ee6889B2d0A3967dB67d7b08e4c", + "simpleLoan": "0x9A93AE395F09C6F350E3306aec592763c517072e", + "simpleLoanSimpleProposal": "0xEb3e6B9B51911175F3a121b5Efb46Fa354520f41", + "simpleLoanListProposal": "0x0E6cE603d328de0D357D624F10f3f448855fFBDC", + "simpleLoanFungibleProposal": "0x0618504Fa17888ec36AC5D46A4A0Ed59436Fb77E", + "simpleLoanDutchAuctionProposal": "0x807eb2A61B2d0193b0696436BeFFcFE4d6D520CB" }, "11155111": { "dao": "0x0000000000000000000000000000000000000000", @@ -160,17 +141,17 @@ "daoSafe": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", "deployerSafe": "0x1B4B37738De3bb9E6a7a4f99aFe4C145734c071d", "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", - "categoryRegistry": "0x1b71A2a08A6fb54b5a8615A48b13447a7b1E891E", - "configSingleton": "0xBdac2fb31E493f92370ED5ECb3EA63e63ae32617", - "config": "0x4ca21fD91F7F6446594E0ff93dD3147fbC2ca7Bb", + "categoryRegistry": "0xbB2168d5546A94AE2DA9254e63D88F7f137B2534", + "configSingleton": "0x1f5febF0efA3aD487508b6Cc7f39a0a54DE9De72", + "config": "0xd52a2898d61636bB3eEF0d145f05352FF543bdCC", "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", - "revokedOfferNonce": "0xBAabf06494A2b1407AA740Ae7B04aF51D1Fd0d5F", - "revokedRequestNonce": "0x18E6EeF41ef2D6B9ab9BC5A02f82E447473D0436", - "simpleLoan": "0x038F9929CfD6af748EFE5384C4F29f67337F6887", - "simpleLoanSimpleOffer": "0x1b02D009cd20e1EA9A0a9a9d37b03b0fe74Db5C6", - "simpleLoanListOffer": "0xc53ec196368e8767576f80787A1924b87A101d3F", - "simpleLoanSimpleRequest": "0x85670ed4a10522C0c152a34cd285c298Ac18bd55" + "revokedNonce": "0x972204fF33348ee6889B2d0A3967dB67d7b08e4c", + "simpleLoan": "0x9A93AE395F09C6F350E3306aec592763c517072e", + "simpleLoanSimpleProposal": "0xEb3e6B9B51911175F3a121b5Efb46Fa354520f41", + "simpleLoanListProposal": "0x0E6cE603d328de0D357D624F10f3f448855fFBDC", + "simpleLoanFungibleProposal": "0x0618504Fa17888ec36AC5D46A4A0Ed59436Fb77E", + "simpleLoanDutchAuctionProposal": "0x807eb2A61B2d0193b0696436BeFFcFE4d6D520CB" } } } From 51a1fd2f556ab89ff52438879285aa803f7b4116 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Tue, 11 Jun 2024 15:11:08 +0200 Subject: [PATCH 126/129] docs: update readme --- README.md | 44 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 81f6c8f..f592f3d 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,39 @@ -# PWN Finance -Smart contracts enabling P2P loans using arbitrary collateral (supporting ERC20, ERC721, ERC1155 standards). +# PWN Protocol -## Developer docs -For in-depth documentation about PWN contracts see [PWN Developer Docs](https://dev-docs.pwn.xyz/). +PWN is a protocol that enables peer-to-peer (P2P) loans using arbitrary collateral. Our smart contracts support ERC20, ERC721, and ERC1155 standards, making it versatile and adaptable to a wide range of use cases. -## Deployed addresses -TBD +## About -# PWN is hiring! -https://www.notion.so/PWN-is-hiring-f5a49899369045e39f41fc7e4c7b5633 +In the world of decentralized finance, PWN stands out with its unique approach to P2P loans. By allowing users to leverage different types of collateral, we provide flexibility and convenience that's unmatched in the industry. + +## Developer Documentation + +For developers interested in integrating with or building on top of PWN, we provide comprehensive documentation. You can find in-depth information about our smart contracts and their usage in the [PWN Developer Docs](https://dev-docs.pwn.xyz/). + +## Deployment + +| Name | Address | Chain | +|------------------------------------|--------------------------------------------|-------------------------| +| Config | 0xd52a2898d61636bB3eEF0d145f05352FF543bdCC | [Ethereum](https://etherscan.io/address/0xd52a2898d61636bB3eEF0d145f05352FF543bdCC) [Polygon](https://polygonscan.com/address/0xd52a2898d61636bB3eEF0d145f05352FF543bdCC) [Arbitrum](https://arbiscan.io/address/0xd52a2898d61636bB3eEF0d145f05352FF543bdCC) [Optimism](https://optimistic.etherscan.io/address/0xd52a2898d61636bB3eEF0d145f05352FF543bdCC) [Base](https://basescan.org/address/0xd52a2898d61636bB3eEF0d145f05352FF543bdCC) [Cronos](https://cronoscan.com/address/0xd52a2898d61636bB3eEF0d145f05352FF543bdCC) [BSC](https://bscscan.com/address/0xd52a2898d61636bB3eEF0d145f05352FF543bdCC) [Sepolia](https://sepolia.etherscan.io/address/0xd52a2898d61636bB3eEF0d145f05352FF543bdCC) | +| Hub | 0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5 | [Ethereum](https://etherscan.io/address/0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5) [Polygon](https://polygonscan.com/address/0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5) [Arbitrum](https://arbiscan.io/address/0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5) [Optimism](https://optimistic.etherscan.io/address/0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5) [Base](https://basescan.org/address/0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5) [Cronos](https://cronoscan.com/address/0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5) [BSC](https://bscscan.com/address/0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5) [Sepolia](https://sepolia.etherscan.io/address/0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5) | +| LOAN Token | 0x4440C069272cC34b80C7B11bEE657D0349Ba9C23 | [Ethereum](https://etherscan.io/address/0x4440C069272cC34b80C7B11bEE657D0349Ba9C23) [Polygon](https://polygonscan.com/address/0x4440C069272cC34b80C7B11bEE657D0349Ba9C23) [Arbitrum](https://arbiscan.io/address/0x4440C069272cC34b80C7B11bEE657D0349Ba9C23) [Optimism](https://optimistic.etherscan.io/address/0x4440C069272cC34b80C7B11bEE657D0349Ba9C23) [Base](https://basescan.org/address/0x4440C069272cC34b80C7B11bEE657D0349Ba9C23) [Cronos](https://cronoscan.com/address/0x4440C069272cC34b80C7B11bEE657D0349Ba9C23) [BSC](https://bscscan.com/address/0x4440C069272cC34b80C7B11bEE657D0349Ba9C23) [Sepolia](https://sepolia.etherscan.io/address/0x4440C069272cC34b80C7B11bEE657D0349Ba9C23) | +| Revoked Nonce | 0x972204fF33348ee6889B2d0A3967dB67d7b08e4c | [Ethereum](https://etherscan.io/address/0x972204fF33348ee6889B2d0A3967dB67d7b08e4c) [Polygon](https://polygonscan.com/address/0x972204fF33348ee6889B2d0A3967dB67d7b08e4c) [Arbitrum](https://arbiscan.io/address/0x972204fF33348ee6889B2d0A3967dB67d7b08e4c) [Optimism](https://optimistic.etherscan.io/address/0x972204fF33348ee6889B2d0A3967dB67d7b08e4c) [Base](https://basescan.org/address/0x972204fF33348ee6889B2d0A3967dB67d7b08e4c) [Cronos](https://cronoscan.com/address/0x972204fF33348ee6889B2d0A3967dB67d7b08e4c) [BSC](https://bscscan.com/address/0x972204fF33348ee6889B2d0A3967dB67d7b08e4c) [Sepolia](https://sepolia.etherscan.io/address/0x972204fF33348ee6889B2d0A3967dB67d7b08e4c) | +| Simple Loan | 0x9A93AE395F09C6F350E3306aec592763c517072e | [Ethereum](https://etherscan.io/address/0x9A93AE395F09C6F350E3306aec592763c517072e) [Polygon](https://polygonscan.com/address/0x9A93AE395F09C6F350E3306aec592763c517072e) [Arbitrum](https://arbiscan.io/address/0x9A93AE395F09C6F350E3306aec592763c517072e) [Optimism](https://optimistic.etherscan.io/address/0x9A93AE395F09C6F350E3306aec592763c517072e) [Base](https://basescan.org/address/0x9A93AE395F09C6F350E3306aec592763c517072e) [Cronos](https://cronoscan.com/address/0x9A93AE395F09C6F350E3306aec592763c517072e) [BSC](https://bscscan.com/address/0x9A93AE395F09C6F350E3306aec592763c517072e) [Sepolia](https://sepolia.etherscan.io/address/0x9A93AE395F09C6F350E3306aec592763c517072e) | +| Simple Loan Simple Proposal | 0xEb3e6B9B51911175F3a121b5Efb46Fa354520f41 | [Ethereum](https://etherscan.io/address/0xEb3e6B9B51911175F3a121b5Efb46Fa354520f41) [Polygon](https://polygonscan.com/address/0xEb3e6B9B51911175F3a121b5Efb46Fa354520f41) [Arbitrum](https://arbiscan.io/address/0xEb3e6B9B51911175F3a121b5Efb46Fa354520f41) [Optimism](https://optimistic.etherscan.io/address/0xEb3e6B9B51911175F3a121b5Efb46Fa354520f41) [Base](https://basescan.org/address/0xEb3e6B9B51911175F3a121b5Efb46Fa354520f41) [Cronos](https://cronoscan.com/address/0xEb3e6B9B51911175F3a121b5Efb46Fa354520f41) [BSC](https://bscscan.com/address/0xEb3e6B9B51911175F3a121b5Efb46Fa354520f41) [Sepolia](https://sepolia.etherscan.io/address/0xEb3e6B9B51911175F3a121b5Efb46Fa354520f41) | +| Simple Loan List Proposal | 0x0E6cE603d328de0D357D624F10f3f448855fFBDC | [Ethereum](https://etherscan.io/address/0x0E6cE603d328de0D357D624F10f3f448855fFBDC) [Polygon](https://polygonscan.com/address/0x0E6cE603d328de0D357D624F10f3f448855fFBDC) [Arbitrum](https://arbiscan.io/address/0x0E6cE603d328de0D357D624F10f3f448855fFBDC) [Optimism](https://optimistic.etherscan.io/address/0x0E6cE603d328de0D357D624F10f3f448855fFBDC) [Base](https://basescan.org/address/0x0E6cE603d328de0D357D624F10f3f448855fFBDC) [Cronos](https://cronoscan.com/address/0x0E6cE603d328de0D357D624F10f3f448855fFBDC) [BSC](https://bscscan.com/address/0x0E6cE603d328de0D357D624F10f3f448855fFBDC) [Sepolia](https://sepolia.etherscan.io/address/0x0E6cE603d328de0D357D624F10f3f448855fFBDC) | +| Simple Loan Fungible Proposal | 0x0618504Fa17888ec36AC5D46A4A0Ed59436Fb77E | [Ethereum](https://etherscan.io/address/0x0618504Fa17888ec36AC5D46A4A0Ed59436Fb77E) [Polygon](https://polygonscan.com/address/0x0618504Fa17888ec36AC5D46A4A0Ed59436Fb77E) [Arbitrum](https://arbiscan.io/address/0x0618504Fa17888ec36AC5D46A4A0Ed59436Fb77E) [Optimism](https://optimistic.etherscan.io/address/0x0618504Fa17888ec36AC5D46A4A0Ed59436Fb77E) [Base](https://basescan.org/address/0x0618504Fa17888ec36AC5D46A4A0Ed59436Fb77E) [Cronos](https://cronoscan.com/address/0x0618504Fa17888ec36AC5D46A4A0Ed59436Fb77E) [BSC](https://bscscan.com/address/0x0618504Fa17888ec36AC5D46A4A0Ed59436Fb77E) [Sepolia](https://sepolia.etherscan.io/address/0x0618504Fa17888ec36AC5D46A4A0Ed59436Fb77E) | +| Simple Loan Dutch Auction Proposal | 0x807eb2A61B2d0193b0696436BeFFcFE4d6D520CB | [Ethereum](https://etherscan.io/address/0x807eb2A61B2d0193b0696436BeFFcFE4d6D520CB) [Polygon](https://polygonscan.com/address/0x807eb2A61B2d0193b0696436BeFFcFE4d6D520CB) [Arbitrum](https://arbiscan.io/address/0x807eb2A61B2d0193b0696436BeFFcFE4d6D520CB) [Optimism](https://optimistic.etherscan.io/address/0x807eb2A61B2d0193b0696436BeFFcFE4d6D520CB) [Base](https://basescan.org/address/0x807eb2A61B2d0193b0696436BeFFcFE4d6D520CB) [Cronos](https://cronoscan.com/address/0x807eb2A61B2d0193b0696436BeFFcFE4d6D520CB) [BSC](https://bscscan.com/address/0x807eb2A61B2d0193b0696436BeFFcFE4d6D520CB) [Sepolia](https://sepolia.etherscan.io/address/0x807eb2A61B2d0193b0696436BeFFcFE4d6D520CB) | + +The addresses listed in the table above are the same on all deployed chains. This means that regardless of the blockchain network you are using, such as Ethereum or Arbitrum, the addresses for the PWN smart contracts remain consistent. This provides a seamless experience for developers and users who want to interact with the PWN protocol across different blockchain ecosystems. + +## Contributing + +We welcome contributions from the community. If you're a developer interested in contributing to PWN, please see our developer docs for more information. + +## PWN is Hiring! + +We're always looking for talented individuals to join our team. If you're passionate about decentralized finance and want to contribute to the future of P2P lending, check out our job postings [here](https://www.notion.so/PWN-is-hiring-f5a49899369045e39f41fc7e4c7b5633). + +## Contact + +If you have any questions or suggestions, feel free to reach out to us. We're always happy to hear from our users. From 349ab57b48a0718044de18b156093742b918aa2b Mon Sep 17 00:00:00 2001 From: ashhanai Date: Thu, 18 Jul 2024 12:51:29 +0200 Subject: [PATCH 127/129] ci: update deployment script --- script/PWN.s.sol | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/script/PWN.s.sol b/script/PWN.s.sol index 6e79349..c368a51 100644 --- a/script/PWN.s.sol +++ b/script/PWN.s.sol @@ -283,17 +283,17 @@ forge script script/PWN.s.sol:Deploy \ abi.encode(deployment.deployer, vm.addr(initialConfigHelper), "") ) })); - address configSingleton = _deploy({ + deployment.configSingleton = PWNConfig(_deploy({ salt: PWNContractDeployerSalt.CONFIG, bytecode: type(PWNConfig).creationCode - }); + })); vm.stopBroadcast(); vm.startBroadcast(initialConfigHelper); ITransparentUpgradeableProxy(address(deployment.config)).upgradeToAndCall( - configSingleton, + address(deployment.configSingleton), abi.encodeWithSelector(PWNConfig.initialize.selector, deployment.protocolTimelock, 0, deployment.daoSafe) ); ITransparentUpgradeableProxy(address(deployment.config)).changeAdmin(deployment.adminTimelock); @@ -313,16 +313,13 @@ forge script script/PWN.s.sol:Deploy \ deployment.hub = PWNHub(_deployAndTransferOwnership({ // Need ownership acceptance from the new owner salt: PWNContractDeployerSalt.HUB, owner: deployment.protocolTimelock, - bytecode: type(PWNHub).creationCode + bytecode: hex"608060405234801561001057600080fd5b5061001a3361001f565b610096565b600180546001600160a01b031916905561004381610046602090811b61035617901c565b50565b600080546001600160a01b038381166001600160a01b0319831681178455604051919092169283917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e09190a35050565b6106b6806100a56000396000f3fe608060405234801561001057600080fd5b50600436106100885760003560e01c8063d019577a1161005b578063d019577a146100dc578063e30c397814610125578063f12715a114610136578063f2fde38b1461014957600080fd5b8063715018a61461008d57806379ba5097146100975780638da5cb5b1461009f5780639cd9c520146100c9575b600080fd5b61009561015c565b005b610095610170565b6000546001600160a01b03165b6040516001600160a01b0390911681526020015b60405180910390f35b6100956100d7366004610445565b6101ef565b6101156100ea366004610481565b6001600160a01b03919091166000908152600260209081526040808320938352929052205460ff1690565b60405190151581526020016100c0565b6001546001600160a01b03166100ac565b610095610144366004610581565b610262565b610095610157366004610648565b6102e5565b6101646103a6565b61016e6000610400565b565b60015433906001600160a01b031681146101e35760405162461bcd60e51b815260206004820152602960248201527f4f776e61626c6532537465703a2063616c6c6572206973206e6f7420746865206044820152683732bb9037bbb732b960b91b60648201526084015b60405180910390fd5b6101ec81610400565b50565b6101f76103a6565b6001600160a01b0383166000818152600260209081526040808320868452825291829020805460ff191685151590811790915591519182528492917fb30f662698af140e14b21a677b92bf5a9787f9109294b3d206fa53ea23069d2b910160405180910390a3505050565b61026a6103a6565b815183511461028c57604051637016bd9b60e01b815260040160405180910390fd5b815160005b818110156102de576102d68582815181106102ae576102ae61066a565b60200260200101518583815181106102c8576102c861066a565b6020026020010151856101ef565b600101610291565b5050505050565b6102ed6103a6565b600180546001600160a01b0383166001600160a01b0319909116811790915561031e6000546001600160a01b031690565b6001600160a01b03167f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e2270060405160405180910390a350565b600080546001600160a01b038381166001600160a01b0319831681178455604051919092169283917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e09190a35050565b6000546001600160a01b0316331461016e5760405162461bcd60e51b815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e657260448201526064016101da565b600180546001600160a01b03191690556101ec81610356565b80356001600160a01b038116811461043057600080fd5b919050565b8035801515811461043057600080fd5b60008060006060848603121561045a57600080fd5b61046384610419565b92506020840135915061047860408501610435565b90509250925092565b6000806040838503121561049457600080fd5b61049d83610419565b946020939093013593505050565b634e487b7160e01b600052604160045260246000fd5b604051601f8201601f1916810167ffffffffffffffff811182821017156104ea576104ea6104ab565b604052919050565b600067ffffffffffffffff82111561050c5761050c6104ab565b5060051b60200190565b600082601f83011261052757600080fd5b8135602061053c610537836104f2565b6104c1565b82815260059290921b8401810191818101908684111561055b57600080fd5b8286015b84811015610576578035835291830191830161055f565b509695505050505050565b60008060006060848603121561059657600080fd5b833567ffffffffffffffff808211156105ae57600080fd5b818601915086601f8301126105c257600080fd5b813560206105d2610537836104f2565b82815260059290921b8401810191818101908a8411156105f157600080fd5b948201945b838610156106165761060786610419565b825294820194908201906105f6565b9750508701359250508082111561062c57600080fd5b5061063986828701610516565b92505061047860408501610435565b60006020828403121561065a57600080fd5b61066382610419565b9392505050565b634e487b7160e01b600052603260045260246000fdfea2646970667358221220d1e6160af9be44b466470083a1ab56623b8e95e11070e3d1398cf335af77500c64736f6c63430008100033" })); // - LOAN token deployment.loanToken = PWNLOAN(_deploy({ salt: PWNContractDeployerSalt.LOAN, - bytecode: abi.encodePacked( - type(PWNLOAN).creationCode, - abi.encode(address(deployment.hub)) - ) + bytecode: hex"60a06040523480156200001157600080fd5b506040516200191f3803806200191f8339810160408190526200003491620000a3565b6040805180820182526008815267282ba7102627a0a760c11b602080830191909152825180840190935260048352632627a0a760e11b908301526001600160a01b0383166080529060006200008a83826200017a565b5060016200009982826200017a565b5050505062000246565b600060208284031215620000b657600080fd5b81516001600160a01b0381168114620000ce57600080fd5b9392505050565b634e487b7160e01b600052604160045260246000fd5b600181811c908216806200010057607f821691505b6020821081036200012157634e487b7160e01b600052602260045260246000fd5b50919050565b601f8211156200017557600081815260208120601f850160051c81016020861015620001505750805b601f850160051c820191505b8181101562000171578281556001016200015c565b5050505b505050565b81516001600160401b03811115620001965762000196620000d5565b620001ae81620001a78454620000eb565b8462000127565b602080601f831160018114620001e65760008415620001cd5750858301515b600019600386901b1c1916600185901b17855562000171565b600085815260208120601f198616915b828110156200021757888601518255948401946001909101908401620001f6565b5085821015620002365787850151600019600388901b60f8161c191681555b5050505050600190811b01905550565b6080516116bd62000262600039600061062401526116bd6000f3fe608060405234801561001057600080fd5b50600436106101165760003560e01c80636a627842116100a2578063a22cb46511610071578063a22cb46514610252578063b88d4fde14610265578063c87b56dd14610278578063e985e9c51461028b578063f51123151461029e57600080fd5b80636a627842146101fb57806370a082311461020e57806395d89b4114610221578063a00d21fc1461022957600080fd5b806323b872dd116100e957806323b872dd1461019857806342842e0e146101ab57806342966c68146101be5780636352211e146101d157806368be92b4146101e457600080fd5b806301ffc9a71461011b57806306fdde0314610143578063081812fc14610158578063095ea7b314610183575b600080fd5b61012e610129366004611145565b6102b1565b60405190151581526020015b60405180910390f35b61014b6102dd565b60405161013a91906111b2565b61016b6101663660046111c5565b61036f565b6040516001600160a01b03909116815260200161013a565b6101966101913660046111fa565b610396565b005b6101966101a6366004611224565b6104b0565b6101966101b9366004611224565b6104e1565b6101966101cc3660046111c5565b6104fc565b61016b6101df3660046111c5565b610586565b6101ed60065481565b60405190815260200161013a565b6101ed610209366004611260565b6105e6565b6101ed61021c366004611260565b610756565b61014b6107dc565b61016b6102373660046111c5565b6007602052600090815260409020546001600160a01b031681565b610196610260366004611289565b6107eb565b61019661027336600461132f565b6107fa565b61014b6102863660046111c5565b610832565b61012e6102993660046113da565b6108b5565b6101ed6102ac3660046111c5565b6108e3565b60006102bc82610979565b806102d757506001600160e01b0319821663f511231560e01b145b92915050565b6060600080546102ec9061140d565b80601f01602080910402602001604051908101604052809291908181526020018280546103189061140d565b80156103655780601f1061033a57610100808354040283529160200191610365565b820191906000526020600020905b81548152906001019060200180831161034857829003601f168201915b5050505050905090565b600061037a826109c9565b506000908152600460205260409020546001600160a01b031690565b60006103a182610586565b9050806001600160a01b0316836001600160a01b0316036104135760405162461bcd60e51b815260206004820152602160248201527f4552433732313a20617070726f76616c20746f2063757272656e74206f776e656044820152603960f91b60648201526084015b60405180910390fd5b336001600160a01b038216148061042f575061042f81336108b5565b6104a15760405162461bcd60e51b815260206004820152603d60248201527f4552433732313a20617070726f76652063616c6c6572206973206e6f7420746f60448201527f6b656e206f776e6572206f7220617070726f76656420666f7220616c6c000000606482015260840161040a565b6104ab8383610a2b565b505050565b6104ba3382610a99565b6104d65760405162461bcd60e51b815260040161040a90611447565b6104ab838383610af8565b6104ab838383604051806020016040528060008152506107fa565b6000818152600760205260409020546001600160a01b03163314610533576040516374768c4960e11b815260040160405180910390fd5b600081815260076020526040902080546001600160a01b031916905561055881610c69565b60405181907f56f7da88d3aa2a8ad74b71a5b449a66a643193815eace8bbd6b089d4bc18294b90600090a250565b6000818152600260205260408120546001600160a01b0316806102d75760405162461bcd60e51b8152602060048201526018602482015277115490cdcc8c4e881a5b9d985b1a59081d1bdad95b88125160421b604482015260640161040a565b60405163680cabbd60e11b81523360048201527f9e56ea094d7a53440eef11fa42b63159fbf703b4ee579494a6ae85afc560359460248201526000907f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03169063d019577a90604401602060405180830381865afa158015610673573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906106979190611494565b15156000036106db5760405163f8932b2d60e01b81527f9e56ea094d7a53440eef11fa42b63159fbf703b4ee579494a6ae85afc5603594600482015260240161040a565b6006600081546106ea906114c7565b9182905550600081815260076020526040902080546001600160a01b0319163317905590506107198282610d0c565b6040516001600160a01b03831690339083907f2ca529bede83c064afd9331357a1ce320271c9c7ceda28ac31472d76f7aff53090600090a4919050565b60006001600160a01b0382166107c05760405162461bcd60e51b815260206004820152602960248201527f4552433732313a2061646472657373207a65726f206973206e6f7420612076616044820152683634b21037bbb732b960b91b606482015260840161040a565b506001600160a01b031660009081526003602052604090205490565b6060600180546102ec9061140d565b6107f6338383610ea5565b5050565b6108043383610a99565b6108205760405162461bcd60e51b815260040161040a90611447565b61082c84848484610f73565b50505050565b606061083d826109c9565b60008281526007602052604080822054815163111d8a1560e01b815291516001600160a01b039091169263111d8a1592600480820193918290030181865afa15801561088d573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526102d791908101906114e0565b6001600160a01b03918216600090815260056020908152604080832093909416825291909152205460ff1690565b6000818152600760205260408120546001600160a01b0316806109095750600092915050565b60405163f511231560e01b8152600481018490526001600160a01b0382169063f511231590602401602060405180830381865afa15801561094e573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906109729190611557565b9392505050565b60006001600160e01b031982166380ac58cd60e01b14806109aa57506001600160e01b03198216635b5e139f60e01b145b806102d757506301ffc9a760e01b6001600160e01b03198316146102d7565b6000818152600260205260409020546001600160a01b0316610a285760405162461bcd60e51b8152602060048201526018602482015277115490cdcc8c4e881a5b9d985b1a59081d1bdad95b88125160421b604482015260640161040a565b50565b600081815260046020526040902080546001600160a01b0319166001600160a01b0384169081179091558190610a6082610586565b6001600160a01b03167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92560405160405180910390a45050565b600080610aa583610586565b9050806001600160a01b0316846001600160a01b03161480610acc5750610acc81856108b5565b80610af05750836001600160a01b0316610ae58461036f565b6001600160a01b0316145b949350505050565b826001600160a01b0316610b0b82610586565b6001600160a01b031614610b315760405162461bcd60e51b815260040161040a90611570565b6001600160a01b038216610b935760405162461bcd60e51b8152602060048201526024808201527f4552433732313a207472616e7366657220746f20746865207a65726f206164646044820152637265737360e01b606482015260840161040a565b610ba08383836001610fa6565b826001600160a01b0316610bb382610586565b6001600160a01b031614610bd95760405162461bcd60e51b815260040161040a90611570565b600081815260046020908152604080832080546001600160a01b03199081169091556001600160a01b0387811680865260038552838620805460001901905590871680865283862080546001019055868652600290945282852080549092168417909155905184937fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef91a4505050565b6000610c7482610586565b9050610c84816000846001610fa6565b610c8d82610586565b600083815260046020908152604080832080546001600160a01b03199081169091556001600160a01b0385168085526003845282852080546000190190558785526002909352818420805490911690555192935084927fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef908390a45050565b6001600160a01b038216610d625760405162461bcd60e51b815260206004820181905260248201527f4552433732313a206d696e7420746f20746865207a65726f2061646472657373604482015260640161040a565b6000818152600260205260409020546001600160a01b031615610dc75760405162461bcd60e51b815260206004820152601c60248201527f4552433732313a20746f6b656e20616c7265616479206d696e74656400000000604482015260640161040a565b610dd5600083836001610fa6565b6000818152600260205260409020546001600160a01b031615610e3a5760405162461bcd60e51b815260206004820152601c60248201527f4552433732313a20746f6b656e20616c7265616479206d696e74656400000000604482015260640161040a565b6001600160a01b038216600081815260036020908152604080832080546001019055848352600290915280822080546001600160a01b0319168417905551839291907fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef908290a45050565b816001600160a01b0316836001600160a01b031603610f065760405162461bcd60e51b815260206004820152601960248201527f4552433732313a20617070726f766520746f2063616c6c657200000000000000604482015260640161040a565b6001600160a01b03838116600081815260056020908152604080832094871680845294825291829020805460ff191686151590811790915591519182527f17307eab39ab6107e8899845ad3d59bd9653f200f220920489ca2b5937696c31910160405180910390a3505050565b610f7e848484610af8565b610f8a8484848461102e565b61082c5760405162461bcd60e51b815260040161040a906115b5565b600181111561082c576001600160a01b03841615610fec576001600160a01b03841660009081526003602052604081208054839290610fe6908490611607565b90915550505b6001600160a01b0383161561082c576001600160a01b0383166000908152600360205260408120805483929061102390849061161a565b909155505050505050565b60006001600160a01b0384163b1561112457604051630a85bd0160e11b81526001600160a01b0385169063150b7a029061107290339089908890889060040161162d565b6020604051808303816000875af19250505080156110ad575060408051601f3d908101601f191682019092526110aa9181019061166a565b60015b61110a573d8080156110db576040519150601f19603f3d011682016040523d82523d6000602084013e6110e0565b606091505b5080516000036111025760405162461bcd60e51b815260040161040a906115b5565b805181602001fd5b6001600160e01b031916630a85bd0160e11b149050610af0565b506001949350505050565b6001600160e01b031981168114610a2857600080fd5b60006020828403121561115757600080fd5b81356109728161112f565b60005b8381101561117d578181015183820152602001611165565b50506000910152565b6000815180845261119e816020860160208601611162565b601f01601f19169290920160200192915050565b6020815260006109726020830184611186565b6000602082840312156111d757600080fd5b5035919050565b80356001600160a01b03811681146111f557600080fd5b919050565b6000806040838503121561120d57600080fd5b611216836111de565b946020939093013593505050565b60008060006060848603121561123957600080fd5b611242846111de565b9250611250602085016111de565b9150604084013590509250925092565b60006020828403121561127257600080fd5b610972826111de565b8015158114610a2857600080fd5b6000806040838503121561129c57600080fd5b6112a5836111de565b915060208301356112b58161127b565b809150509250929050565b634e487b7160e01b600052604160045260246000fd5b604051601f8201601f1916810167ffffffffffffffff811182821017156112ff576112ff6112c0565b604052919050565b600067ffffffffffffffff821115611321576113216112c0565b50601f01601f191660200190565b6000806000806080858703121561134557600080fd5b61134e856111de565b935061135c602086016111de565b925060408501359150606085013567ffffffffffffffff81111561137f57600080fd5b8501601f8101871361139057600080fd5b80356113a361139e82611307565b6112d6565b8181528860208385010111156113b857600080fd5b8160208401602083013760006020838301015280935050505092959194509250565b600080604083850312156113ed57600080fd5b6113f6836111de565b9150611404602084016111de565b90509250929050565b600181811c9082168061142157607f821691505b60208210810361144157634e487b7160e01b600052602260045260246000fd5b50919050565b6020808252602d908201527f4552433732313a2063616c6c6572206973206e6f7420746f6b656e206f776e6560408201526c1c881bdc88185c1c1c9bdd9959609a1b606082015260800190565b6000602082840312156114a657600080fd5b81516109728161127b565b634e487b7160e01b600052601160045260246000fd5b6000600182016114d9576114d96114b1565b5060010190565b6000602082840312156114f257600080fd5b815167ffffffffffffffff81111561150957600080fd5b8201601f8101841361151a57600080fd5b805161152861139e82611307565b81815285602083850101111561153d57600080fd5b61154e826020830160208601611162565b95945050505050565b60006020828403121561156957600080fd5b5051919050565b60208082526025908201527f4552433732313a207472616e736665722066726f6d20696e636f72726563742060408201526437bbb732b960d91b606082015260800190565b60208082526032908201527f4552433732313a207472616e7366657220746f206e6f6e20455243373231526560408201527131b2b4bb32b91034b6b83632b6b2b73a32b960711b606082015260800190565b818103818111156102d7576102d76114b1565b808201808211156102d7576102d76114b1565b6001600160a01b038581168252841660208201526040810183905260806060820181905260009061166090830184611186565b9695505050505050565b60006020828403121561167c57600080fd5b81516109728161112f56fea264697066735822122039ae91bd608a4faaff76f2a30605fb5f1b0a5634bce2e6aed4735db710f3dd7764736f6c6343000810003300000000000000000000000037807a2f031b3b44081f4b21500e5d70ebadadd5" })); // - Revoked nonces @@ -399,7 +396,7 @@ forge script script/PWN.s.sol:Deploy \ })); console2.log("MultiToken Category Registry:", address(deployment.categoryRegistry)); - console2.log("PWNConfig - singleton:", configSingleton); + console2.log("PWNConfig - singleton:", address(deployment.configSingleton)); console2.log("PWNConfig - proxy:", address(deployment.config)); console2.log("PWNHub:", address(deployment.hub)); console2.log("PWNLOAN:", address(deployment.loanToken)); From 1610410eae3bb06b2730b80bdfbd90ca95ac8a2a Mon Sep 17 00:00:00 2001 From: ashhanai Date: Thu, 18 Jul 2024 12:51:51 +0200 Subject: [PATCH 128/129] feat: deploy to Linea --- .github/workflows/main.yml | 1 + README.md | 18 +++++++++--------- deployments/latest.json | 21 ++++++++++++++++++++- foundry.toml | 1 + 4 files changed, 31 insertions(+), 10 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0477f36..864fcd3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,6 +15,7 @@ jobs: CRONOS_URL: ${{ secrets.CRONOS_URL }} MANTLE_URL: ${{ secrets.MANTLE_URL }} BSC_URL: ${{ secrets.BSC_URL }} + LINEA_URL: ${{ secrets.LINEA_URL }} SEPOLIA_URL: ${{ secrets.SEPOLIA_URL }} GOERLI_URL: ${{ secrets.GOERLI_URL }} BASE_GOERLI_URL: ${{ secrets.BASE_GOERLI_URL }} diff --git a/README.md b/README.md index f592f3d..e2976f7 100644 --- a/README.md +++ b/README.md @@ -14,15 +14,15 @@ For developers interested in integrating with or building on top of PWN, we prov | Name | Address | Chain | |------------------------------------|--------------------------------------------|-------------------------| -| Config | 0xd52a2898d61636bB3eEF0d145f05352FF543bdCC | [Ethereum](https://etherscan.io/address/0xd52a2898d61636bB3eEF0d145f05352FF543bdCC) [Polygon](https://polygonscan.com/address/0xd52a2898d61636bB3eEF0d145f05352FF543bdCC) [Arbitrum](https://arbiscan.io/address/0xd52a2898d61636bB3eEF0d145f05352FF543bdCC) [Optimism](https://optimistic.etherscan.io/address/0xd52a2898d61636bB3eEF0d145f05352FF543bdCC) [Base](https://basescan.org/address/0xd52a2898d61636bB3eEF0d145f05352FF543bdCC) [Cronos](https://cronoscan.com/address/0xd52a2898d61636bB3eEF0d145f05352FF543bdCC) [BSC](https://bscscan.com/address/0xd52a2898d61636bB3eEF0d145f05352FF543bdCC) [Sepolia](https://sepolia.etherscan.io/address/0xd52a2898d61636bB3eEF0d145f05352FF543bdCC) | -| Hub | 0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5 | [Ethereum](https://etherscan.io/address/0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5) [Polygon](https://polygonscan.com/address/0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5) [Arbitrum](https://arbiscan.io/address/0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5) [Optimism](https://optimistic.etherscan.io/address/0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5) [Base](https://basescan.org/address/0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5) [Cronos](https://cronoscan.com/address/0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5) [BSC](https://bscscan.com/address/0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5) [Sepolia](https://sepolia.etherscan.io/address/0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5) | -| LOAN Token | 0x4440C069272cC34b80C7B11bEE657D0349Ba9C23 | [Ethereum](https://etherscan.io/address/0x4440C069272cC34b80C7B11bEE657D0349Ba9C23) [Polygon](https://polygonscan.com/address/0x4440C069272cC34b80C7B11bEE657D0349Ba9C23) [Arbitrum](https://arbiscan.io/address/0x4440C069272cC34b80C7B11bEE657D0349Ba9C23) [Optimism](https://optimistic.etherscan.io/address/0x4440C069272cC34b80C7B11bEE657D0349Ba9C23) [Base](https://basescan.org/address/0x4440C069272cC34b80C7B11bEE657D0349Ba9C23) [Cronos](https://cronoscan.com/address/0x4440C069272cC34b80C7B11bEE657D0349Ba9C23) [BSC](https://bscscan.com/address/0x4440C069272cC34b80C7B11bEE657D0349Ba9C23) [Sepolia](https://sepolia.etherscan.io/address/0x4440C069272cC34b80C7B11bEE657D0349Ba9C23) | -| Revoked Nonce | 0x972204fF33348ee6889B2d0A3967dB67d7b08e4c | [Ethereum](https://etherscan.io/address/0x972204fF33348ee6889B2d0A3967dB67d7b08e4c) [Polygon](https://polygonscan.com/address/0x972204fF33348ee6889B2d0A3967dB67d7b08e4c) [Arbitrum](https://arbiscan.io/address/0x972204fF33348ee6889B2d0A3967dB67d7b08e4c) [Optimism](https://optimistic.etherscan.io/address/0x972204fF33348ee6889B2d0A3967dB67d7b08e4c) [Base](https://basescan.org/address/0x972204fF33348ee6889B2d0A3967dB67d7b08e4c) [Cronos](https://cronoscan.com/address/0x972204fF33348ee6889B2d0A3967dB67d7b08e4c) [BSC](https://bscscan.com/address/0x972204fF33348ee6889B2d0A3967dB67d7b08e4c) [Sepolia](https://sepolia.etherscan.io/address/0x972204fF33348ee6889B2d0A3967dB67d7b08e4c) | -| Simple Loan | 0x9A93AE395F09C6F350E3306aec592763c517072e | [Ethereum](https://etherscan.io/address/0x9A93AE395F09C6F350E3306aec592763c517072e) [Polygon](https://polygonscan.com/address/0x9A93AE395F09C6F350E3306aec592763c517072e) [Arbitrum](https://arbiscan.io/address/0x9A93AE395F09C6F350E3306aec592763c517072e) [Optimism](https://optimistic.etherscan.io/address/0x9A93AE395F09C6F350E3306aec592763c517072e) [Base](https://basescan.org/address/0x9A93AE395F09C6F350E3306aec592763c517072e) [Cronos](https://cronoscan.com/address/0x9A93AE395F09C6F350E3306aec592763c517072e) [BSC](https://bscscan.com/address/0x9A93AE395F09C6F350E3306aec592763c517072e) [Sepolia](https://sepolia.etherscan.io/address/0x9A93AE395F09C6F350E3306aec592763c517072e) | -| Simple Loan Simple Proposal | 0xEb3e6B9B51911175F3a121b5Efb46Fa354520f41 | [Ethereum](https://etherscan.io/address/0xEb3e6B9B51911175F3a121b5Efb46Fa354520f41) [Polygon](https://polygonscan.com/address/0xEb3e6B9B51911175F3a121b5Efb46Fa354520f41) [Arbitrum](https://arbiscan.io/address/0xEb3e6B9B51911175F3a121b5Efb46Fa354520f41) [Optimism](https://optimistic.etherscan.io/address/0xEb3e6B9B51911175F3a121b5Efb46Fa354520f41) [Base](https://basescan.org/address/0xEb3e6B9B51911175F3a121b5Efb46Fa354520f41) [Cronos](https://cronoscan.com/address/0xEb3e6B9B51911175F3a121b5Efb46Fa354520f41) [BSC](https://bscscan.com/address/0xEb3e6B9B51911175F3a121b5Efb46Fa354520f41) [Sepolia](https://sepolia.etherscan.io/address/0xEb3e6B9B51911175F3a121b5Efb46Fa354520f41) | -| Simple Loan List Proposal | 0x0E6cE603d328de0D357D624F10f3f448855fFBDC | [Ethereum](https://etherscan.io/address/0x0E6cE603d328de0D357D624F10f3f448855fFBDC) [Polygon](https://polygonscan.com/address/0x0E6cE603d328de0D357D624F10f3f448855fFBDC) [Arbitrum](https://arbiscan.io/address/0x0E6cE603d328de0D357D624F10f3f448855fFBDC) [Optimism](https://optimistic.etherscan.io/address/0x0E6cE603d328de0D357D624F10f3f448855fFBDC) [Base](https://basescan.org/address/0x0E6cE603d328de0D357D624F10f3f448855fFBDC) [Cronos](https://cronoscan.com/address/0x0E6cE603d328de0D357D624F10f3f448855fFBDC) [BSC](https://bscscan.com/address/0x0E6cE603d328de0D357D624F10f3f448855fFBDC) [Sepolia](https://sepolia.etherscan.io/address/0x0E6cE603d328de0D357D624F10f3f448855fFBDC) | -| Simple Loan Fungible Proposal | 0x0618504Fa17888ec36AC5D46A4A0Ed59436Fb77E | [Ethereum](https://etherscan.io/address/0x0618504Fa17888ec36AC5D46A4A0Ed59436Fb77E) [Polygon](https://polygonscan.com/address/0x0618504Fa17888ec36AC5D46A4A0Ed59436Fb77E) [Arbitrum](https://arbiscan.io/address/0x0618504Fa17888ec36AC5D46A4A0Ed59436Fb77E) [Optimism](https://optimistic.etherscan.io/address/0x0618504Fa17888ec36AC5D46A4A0Ed59436Fb77E) [Base](https://basescan.org/address/0x0618504Fa17888ec36AC5D46A4A0Ed59436Fb77E) [Cronos](https://cronoscan.com/address/0x0618504Fa17888ec36AC5D46A4A0Ed59436Fb77E) [BSC](https://bscscan.com/address/0x0618504Fa17888ec36AC5D46A4A0Ed59436Fb77E) [Sepolia](https://sepolia.etherscan.io/address/0x0618504Fa17888ec36AC5D46A4A0Ed59436Fb77E) | -| Simple Loan Dutch Auction Proposal | 0x807eb2A61B2d0193b0696436BeFFcFE4d6D520CB | [Ethereum](https://etherscan.io/address/0x807eb2A61B2d0193b0696436BeFFcFE4d6D520CB) [Polygon](https://polygonscan.com/address/0x807eb2A61B2d0193b0696436BeFFcFE4d6D520CB) [Arbitrum](https://arbiscan.io/address/0x807eb2A61B2d0193b0696436BeFFcFE4d6D520CB) [Optimism](https://optimistic.etherscan.io/address/0x807eb2A61B2d0193b0696436BeFFcFE4d6D520CB) [Base](https://basescan.org/address/0x807eb2A61B2d0193b0696436BeFFcFE4d6D520CB) [Cronos](https://cronoscan.com/address/0x807eb2A61B2d0193b0696436BeFFcFE4d6D520CB) [BSC](https://bscscan.com/address/0x807eb2A61B2d0193b0696436BeFFcFE4d6D520CB) [Sepolia](https://sepolia.etherscan.io/address/0x807eb2A61B2d0193b0696436BeFFcFE4d6D520CB) | +| Config | 0xd52a2898d61636bB3eEF0d145f05352FF543bdCC | [Ethereum](https://etherscan.io/address/0xd52a2898d61636bB3eEF0d145f05352FF543bdCC) [Polygon](https://polygonscan.com/address/0xd52a2898d61636bB3eEF0d145f05352FF543bdCC) [Arbitrum](https://arbiscan.io/address/0xd52a2898d61636bB3eEF0d145f05352FF543bdCC) [Optimism](https://optimistic.etherscan.io/address/0xd52a2898d61636bB3eEF0d145f05352FF543bdCC) [Base](https://basescan.org/address/0xd52a2898d61636bB3eEF0d145f05352FF543bdCC) [Cronos](https://cronoscan.com/address/0xd52a2898d61636bB3eEF0d145f05352FF543bdCC) [BSC](https://bscscan.com/address/0xd52a2898d61636bB3eEF0d145f05352FF543bdCC) [Linea](https://lineascan.build/address/0xd52a2898d61636bB3eEF0d145f05352FF543bdCC) [Sepolia](https://sepolia.etherscan.io/address/0xd52a2898d61636bB3eEF0d145f05352FF543bdCC) | +| Hub | 0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5 | [Ethereum](https://etherscan.io/address/0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5) [Polygon](https://polygonscan.com/address/0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5) [Arbitrum](https://arbiscan.io/address/0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5) [Optimism](https://optimistic.etherscan.io/address/0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5) [Base](https://basescan.org/address/0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5) [Cronos](https://cronoscan.com/address/0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5) [BSC](https://bscscan.com/address/0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5) [Linea](https://lineascan.build/address/0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5) [Sepolia](https://sepolia.etherscan.io/address/0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5) | +| LOAN Token | 0x4440C069272cC34b80C7B11bEE657D0349Ba9C23 | [Ethereum](https://etherscan.io/address/0x4440C069272cC34b80C7B11bEE657D0349Ba9C23) [Polygon](https://polygonscan.com/address/0x4440C069272cC34b80C7B11bEE657D0349Ba9C23) [Arbitrum](https://arbiscan.io/address/0x4440C069272cC34b80C7B11bEE657D0349Ba9C23) [Optimism](https://optimistic.etherscan.io/address/0x4440C069272cC34b80C7B11bEE657D0349Ba9C23) [Base](https://basescan.org/address/0x4440C069272cC34b80C7B11bEE657D0349Ba9C23) [Cronos](https://cronoscan.com/address/0x4440C069272cC34b80C7B11bEE657D0349Ba9C23) [BSC](https://bscscan.com/address/0x4440C069272cC34b80C7B11bEE657D0349Ba9C23) [Linea](https://lineascan.build/address/0x4440C069272cC34b80C7B11bEE657D0349Ba9C23) [Sepolia](https://sepolia.etherscan.io/address/0x4440C069272cC34b80C7B11bEE657D0349Ba9C23) | +| Revoked Nonce | 0x972204fF33348ee6889B2d0A3967dB67d7b08e4c | [Ethereum](https://etherscan.io/address/0x972204fF33348ee6889B2d0A3967dB67d7b08e4c) [Polygon](https://polygonscan.com/address/0x972204fF33348ee6889B2d0A3967dB67d7b08e4c) [Arbitrum](https://arbiscan.io/address/0x972204fF33348ee6889B2d0A3967dB67d7b08e4c) [Optimism](https://optimistic.etherscan.io/address/0x972204fF33348ee6889B2d0A3967dB67d7b08e4c) [Base](https://basescan.org/address/0x972204fF33348ee6889B2d0A3967dB67d7b08e4c) [Cronos](https://cronoscan.com/address/0x972204fF33348ee6889B2d0A3967dB67d7b08e4c) [BSC](https://bscscan.com/address/0x972204fF33348ee6889B2d0A3967dB67d7b08e4c) [Linea](https://lineascan.build/address/0x972204fF33348ee6889B2d0A3967dB67d7b08e4c) [Sepolia](https://sepolia.etherscan.io/address/0x972204fF33348ee6889B2d0A3967dB67d7b08e4c) | +| Simple Loan | 0x9A93AE395F09C6F350E3306aec592763c517072e | [Ethereum](https://etherscan.io/address/0x9A93AE395F09C6F350E3306aec592763c517072e) [Polygon](https://polygonscan.com/address/0x9A93AE395F09C6F350E3306aec592763c517072e) [Arbitrum](https://arbiscan.io/address/0x9A93AE395F09C6F350E3306aec592763c517072e) [Optimism](https://optimistic.etherscan.io/address/0x9A93AE395F09C6F350E3306aec592763c517072e) [Base](https://basescan.org/address/0x9A93AE395F09C6F350E3306aec592763c517072e) [Cronos](https://cronoscan.com/address/0x9A93AE395F09C6F350E3306aec592763c517072e) [BSC](https://bscscan.com/address/0x9A93AE395F09C6F350E3306aec592763c517072e) [Linea](https://lineascan.build/address/0x9A93AE395F09C6F350E3306aec592763c517072e) [Sepolia](https://sepolia.etherscan.io/address/0x9A93AE395F09C6F350E3306aec592763c517072e) | +| Simple Loan Simple Proposal | 0xEb3e6B9B51911175F3a121b5Efb46Fa354520f41 | [Ethereum](https://etherscan.io/address/0xEb3e6B9B51911175F3a121b5Efb46Fa354520f41) [Polygon](https://polygonscan.com/address/0xEb3e6B9B51911175F3a121b5Efb46Fa354520f41) [Arbitrum](https://arbiscan.io/address/0xEb3e6B9B51911175F3a121b5Efb46Fa354520f41) [Optimism](https://optimistic.etherscan.io/address/0xEb3e6B9B51911175F3a121b5Efb46Fa354520f41) [Base](https://basescan.org/address/0xEb3e6B9B51911175F3a121b5Efb46Fa354520f41) [Cronos](https://cronoscan.com/address/0xEb3e6B9B51911175F3a121b5Efb46Fa354520f41) [BSC](https://bscscan.com/address/0xEb3e6B9B51911175F3a121b5Efb46Fa354520f41) [Linea](https://lineascan.build/address/0xEb3e6B9B51911175F3a121b5Efb46Fa354520f41) [Sepolia](https://sepolia.etherscan.io/address/0xEb3e6B9B51911175F3a121b5Efb46Fa354520f41) | +| Simple Loan List Proposal | 0x0E6cE603d328de0D357D624F10f3f448855fFBDC | [Ethereum](https://etherscan.io/address/0x0E6cE603d328de0D357D624F10f3f448855fFBDC) [Polygon](https://polygonscan.com/address/0x0E6cE603d328de0D357D624F10f3f448855fFBDC) [Arbitrum](https://arbiscan.io/address/0x0E6cE603d328de0D357D624F10f3f448855fFBDC) [Optimism](https://optimistic.etherscan.io/address/0x0E6cE603d328de0D357D624F10f3f448855fFBDC) [Base](https://basescan.org/address/0x0E6cE603d328de0D357D624F10f3f448855fFBDC) [Cronos](https://cronoscan.com/address/0x0E6cE603d328de0D357D624F10f3f448855fFBDC) [BSC](https://bscscan.com/address/0x0E6cE603d328de0D357D624F10f3f448855fFBDC) [Linea](https://lineascan.build/address/0x0E6cE603d328de0D357D624F10f3f448855fFBDC) [Sepolia](https://sepolia.etherscan.io/address/0x0E6cE603d328de0D357D624F10f3f448855fFBDC) | +| Simple Loan Fungible Proposal | 0x0618504Fa17888ec36AC5D46A4A0Ed59436Fb77E | [Ethereum](https://etherscan.io/address/0x0618504Fa17888ec36AC5D46A4A0Ed59436Fb77E) [Polygon](https://polygonscan.com/address/0x0618504Fa17888ec36AC5D46A4A0Ed59436Fb77E) [Arbitrum](https://arbiscan.io/address/0x0618504Fa17888ec36AC5D46A4A0Ed59436Fb77E) [Optimism](https://optimistic.etherscan.io/address/0x0618504Fa17888ec36AC5D46A4A0Ed59436Fb77E) [Base](https://basescan.org/address/0x0618504Fa17888ec36AC5D46A4A0Ed59436Fb77E) [Cronos](https://cronoscan.com/address/0x0618504Fa17888ec36AC5D46A4A0Ed59436Fb77E) [BSC](https://bscscan.com/address/0x0618504Fa17888ec36AC5D46A4A0Ed59436Fb77E) [Linea](https://lineascan.build/address/0x0618504Fa17888ec36AC5D46A4A0Ed59436Fb77E) [Sepolia](https://sepolia.etherscan.io/address/0x0618504Fa17888ec36AC5D46A4A0Ed59436Fb77E) | +| Simple Loan Dutch Auction Proposal | 0x807eb2A61B2d0193b0696436BeFFcFE4d6D520CB | [Ethereum](https://etherscan.io/address/0x807eb2A61B2d0193b0696436BeFFcFE4d6D520CB) [Polygon](https://polygonscan.com/address/0x807eb2A61B2d0193b0696436BeFFcFE4d6D520CB) [Arbitrum](https://arbiscan.io/address/0x807eb2A61B2d0193b0696436BeFFcFE4d6D520CB) [Optimism](https://optimistic.etherscan.io/address/0x807eb2A61B2d0193b0696436BeFFcFE4d6D520CB) [Base](https://basescan.org/address/0x807eb2A61B2d0193b0696436BeFFcFE4d6D520CB) [Cronos](https://cronoscan.com/address/0x807eb2A61B2d0193b0696436BeFFcFE4d6D520CB) [BSC](https://bscscan.com/address/0x807eb2A61B2d0193b0696436BeFFcFE4d6D520CB) [Linea](https://lineascan.build/address/0x807eb2A61B2d0193b0696436BeFFcFE4d6D520CB) [Sepolia](https://sepolia.etherscan.io/address/0x807eb2A61B2d0193b0696436BeFFcFE4d6D520CB) | The addresses listed in the table above are the same on all deployed chains. This means that regardless of the blockchain network you are using, such as Ethereum or Arbitrum, the addresses for the PWN smart contracts remain consistent. This provides a seamless experience for developers and users who want to interact with the PWN protocol across different blockchain ecosystems. diff --git a/deployments/latest.json b/deployments/latest.json index 214cac6..91e9caa 100644 --- a/deployments/latest.json +++ b/deployments/latest.json @@ -1,5 +1,5 @@ { - "deployedChains": [1, 10, 25, 56, 137, 8453, 42161, 11155111], + "deployedChains": [1, 10, 25, 56, 137, 8453, 42161, 59144, 11155111], "chains": { "1": { "dao": "0x1B8383D2726E7e18189205337424a2631A2102F4", @@ -134,6 +134,25 @@ "simpleLoanFungibleProposal": "0x0618504Fa17888ec36AC5D46A4A0Ed59436Fb77E", "simpleLoanDutchAuctionProposal": "0x807eb2A61B2d0193b0696436BeFFcFE4d6D520CB" }, + "59144": { + "dao": "0x0000000000000000000000000000000000000000", + "adminTimelock": "0xd57e72A328AB1deC6b374c2babe2dc489819B5Ea", + "protocolTimelock": "0xd8dbdDf1c0FDdf9b5eCFA5C067C38DB66739FBAB", + "daoSafe": "0x282D9663815b1F9929a3C84a9a1290BE882E125f", + "deployerSafe": "0x1B4B37738De3bb9E6a7a4f99aFe4C145734c071d", + "deployer": "0x706c9F2dd328E2C01483eCF705D2D9708F4aB727", + "categoryRegistry": "0xbB2168d5546A94AE2DA9254e63D88F7f137B2534", + "configSingleton": "0x1f5febF0efA3aD487508b6Cc7f39a0a54DE9De72", + "config": "0xd52a2898d61636bB3eEF0d145f05352FF543bdCC", + "hub": "0x37807A2F031b3B44081F4b21500E5D70EbaDAdd5", + "loanToken": "0x4440C069272cC34b80C7B11bEE657D0349Ba9C23", + "revokedNonce": "0x972204fF33348ee6889B2d0A3967dB67d7b08e4c", + "simpleLoan": "0x9A93AE395F09C6F350E3306aec592763c517072e", + "simpleLoanSimpleProposal": "0xEb3e6B9B51911175F3a121b5Efb46Fa354520f41", + "simpleLoanListProposal": "0x0E6cE603d328de0D357D624F10f3f448855fFBDC", + "simpleLoanFungibleProposal": "0x0618504Fa17888ec36AC5D46A4A0Ed59436Fb77E", + "simpleLoanDutchAuctionProposal": "0x807eb2A61B2d0193b0696436BeFFcFE4d6D520CB" + }, "11155111": { "dao": "0x0000000000000000000000000000000000000000", "adminTimelock": "0xd57e72A328AB1deC6b374c2babe2dc489819B5Ea", diff --git a/foundry.toml b/foundry.toml index 586fbad..f190e03 100644 --- a/foundry.toml +++ b/foundry.toml @@ -14,6 +14,7 @@ base = "${BASE_URL}" cronos = "${CRONOS_URL}" mantle = "${MANTLE_URL}" bsc = "${BSC_URL}" +linea = "${LINEA_URL}" # Testnets sepolia = "${SEPOLIA_URL}" From 7e813a4614962f11181818248386e0b8851eef4a Mon Sep 17 00:00:00 2001 From: ashhanai Date: Thu, 18 Jul 2024 12:52:17 +0200 Subject: [PATCH 129/129] test: test deployed protocols --- test/fork/DeployedProtocol.fork.t.sol | 44 ++++++++------------------- 1 file changed, 12 insertions(+), 32 deletions(-) diff --git a/test/fork/DeployedProtocol.fork.t.sol b/test/fork/DeployedProtocol.fork.t.sol index 0728fc2..793533a 100644 --- a/test/fork/DeployedProtocol.fork.t.sol +++ b/test/fork/DeployedProtocol.fork.t.sol @@ -21,34 +21,6 @@ contract DeployedProtocolTest is DeploymentTest { vm.createSelectFork(urlOrAlias); super.setUp(); - // DEPLOYER - // - owner is deployer safe - if (deployment.deployerSafe != address(0)) { - assertEq(deployment.deployer.owner(), deployment.deployerSafe); - } - - // TIMELOCK CONTROLLERS - address timelockOwner = deployment.dao == address(0) ? deployment.daoSafe : deployment.dao; - TimelockController protocolTimelockController = TimelockController(payable(deployment.protocolTimelock)); - // - protocol timelock has min delay of 4 days - assertEq(protocolTimelockController.getMinDelay(), 4 days); - // - dao or dao safe has PROPOSER role in protocol timelock - assertTrue(protocolTimelockController.hasRole(PROPOSER_ROLE, timelockOwner)); - // - dao or dao safe has CANCELLER role in protocol timelock - assertTrue(protocolTimelockController.hasRole(CANCELLER_ROLE, timelockOwner)); - // - everybody has EXECUTOR role in protocol timelock - assertTrue(protocolTimelockController.hasRole(EXECUTOR_ROLE, address(0))); - - TimelockController adminTimelockController = TimelockController(payable(deployment.adminTimelock)); - // - admin timelock has min delay of 4 days - assertEq(adminTimelockController.getMinDelay(), 4 days); - // - dao or dao safe has PROPOSER role in product timelock - assertTrue(adminTimelockController.hasRole(PROPOSER_ROLE, timelockOwner)); - // - dao or dao safe has CANCELLER role in product timelock - assertTrue(adminTimelockController.hasRole(CANCELLER_ROLE, timelockOwner)); - // - everybody has EXECUTOR role in product timelock - assertTrue(adminTimelockController.hasRole(EXECUTOR_ROLE, address(0))); - // CONFIG // - admin is admin timelock assertEq(vm.load(address(deployment.config), PROXY_ADMIN_SLOT), bytes32(uint256(uint160(deployment.adminTimelock)))); @@ -58,13 +30,13 @@ contract DeployedProtocolTest is DeploymentTest { assertEq(deployment.config.feeCollector(), deployment.daoSafe); // - is initialized assertEq(vm.load(address(deployment.config), bytes32(uint256(1))) << 88 >> 248, bytes32(uint256(1))); - // - implementation is initialized + // - implementation initialization is disabled address configImplementation = address(uint160(uint256(vm.load(address(deployment.config), PROXY_IMPLEMENTATION_SLOT)))); - assertEq(vm.load(configImplementation, bytes32(uint256(1))) << 88 >> 248, bytes32(uint256(1))); + assertEq(vm.load(configImplementation, bytes32(uint256(1))) << 88 >> 248, bytes32(uint256(type(uint8).max))); // CATEGORY REGISTRY // - owner is protocol timelock - // assertTrue(deployment.categoryRegistry.owner(), deployment.protocolTimelock); + assertEq(deployment.categoryRegistry.owner(), deployment.protocolTimelock); // HUB // - owner is protocol timelock @@ -89,6 +61,14 @@ contract DeployedProtocolTest is DeploymentTest { } - // todo: function test_deployedProtocol_ethereum() external { _test_deployedProtocol("mainnet"); } + function test_deployedProtocol_ethereum() external { _test_deployedProtocol("mainnet"); } + function test_deployedProtocol_polygon() external { _test_deployedProtocol("polygon"); } + function test_deployedProtocol_arbitrum() external { _test_deployedProtocol("arbitrum"); } + function test_deployedProtocol_optimism() external { _test_deployedProtocol("optimism"); } + function test_deployedProtocol_base() external { _test_deployedProtocol("base"); } + function test_deployedProtocol_cronos() external { _test_deployedProtocol("cronos"); } + function test_deployedProtocol_mantle() external { _test_deployedProtocol("mantle"); } + function test_deployedProtocol_bsc() external { _test_deployedProtocol("bsc"); } + function test_deployedProtocol_linea() external { _test_deployedProtocol("linea"); } }