From 1f1a1cca2f5924b427fd319e8c056424a677b4d6 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Mon, 16 Dec 2024 16:34:41 +0100 Subject: [PATCH] fix(chainlink-elastic-proposal): erc712 proposal array encoding --- .../PWNSimpleLoanElasticChainlinkProposal.sol | 45 +++++++- ...pleLoanElasticChainlinkProposalHarness.sol | 34 ++++++ ...WNSimpleLoanElasticChainlinkProposal.t.sol | 100 ++++++++++++++++-- 3 files changed, 166 insertions(+), 13 deletions(-) create mode 100644 test/harness/PWNSimpleLoanElasticChainlinkProposalHarness.sol diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanElasticChainlinkProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanElasticChainlinkProposal.sol index 94b8744..1671617 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanElasticChainlinkProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanElasticChainlinkProposal.sol @@ -162,7 +162,7 @@ contract PWNSimpleLoanElasticChainlinkProposal is PWNSimpleLoanProposal { * @return Proposal struct hash. */ function getProposalHash(Proposal calldata proposal) public view returns (bytes32) { - return _getProposalHash(PROPOSAL_TYPEHASH, abi.encode(proposal)); + return _getProposalHash(PROPOSAL_TYPEHASH, _erc712EncodeProposal(proposal)); } /** @@ -270,7 +270,7 @@ contract PWNSimpleLoanElasticChainlinkProposal is PWNSimpleLoanProposal { (Proposal memory proposal, ProposalValues memory proposalValues) = decodeProposalData(proposalData); // Make proposal hash - proposalHash = _getProposalHash(PROPOSAL_TYPEHASH, abi.encode(proposal)); + proposalHash = _getProposalHash(PROPOSAL_TYPEHASH, _erc712EncodeProposal(proposal)); // Check min credit amount if (proposal.minCreditAmount == 0) { @@ -340,4 +340,45 @@ contract PWNSimpleLoanElasticChainlinkProposal is PWNSimpleLoanProposal { }); } + + /** + * @notice Encode proposal data for EIP-712. + * @param proposal Proposal struct to be encoded. + * @return encodedProposal Encoded proposal data. + */ + function _erc712EncodeProposal(Proposal memory proposal) internal pure returns (bytes memory encodedProposal) { + encodedProposal = abi.encode( + proposal.collateralCategory, + proposal.collateralAddress, + proposal.collateralId, + proposal.checkCollateralStateFingerprint, + proposal.collateralStateFingerprint, + proposal.creditAddress, + keccak256(abi.encodePacked(proposal.feedIntermediaryDenominations)), + keccak256(abi.encodePacked(proposal.feedInvertFlags)), + proposal.loanToValue, + proposal.minCreditAmount, + proposal.availableCreditLimit, + proposal.utilizedCreditId + ); + + encodedProposal = abi.encodePacked( + encodedProposal, + abi.encode( + proposal.fixedInterestAmount, + proposal.accruingInterestAPR, + proposal.durationOrDate, + proposal.expiration, + proposal.allowedAcceptor, + proposal.proposer, + proposal.proposerSpecHash, + proposal.isOffer, + proposal.refinancingLoanId, + proposal.nonceSpace, + proposal.nonce, + proposal.loanContract + ) + ); + } + } diff --git a/test/harness/PWNSimpleLoanElasticChainlinkProposalHarness.sol b/test/harness/PWNSimpleLoanElasticChainlinkProposalHarness.sol new file mode 100644 index 0000000..80639a2 --- /dev/null +++ b/test/harness/PWNSimpleLoanElasticChainlinkProposalHarness.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.16; + +import { + PWNSimpleLoanElasticChainlinkProposal +} from "pwn/loan/terms/simple/proposal/PWNSimpleLoanElasticChainlinkProposal.sol"; + + +contract PWNSimpleLoanElasticChainlinkProposalHarness is PWNSimpleLoanElasticChainlinkProposal { + + constructor( + address _hub, + address _revokedNonce, + address _config, + address _utilizedCredit, + address _chainlinkFeedRegistry, + address _l2SequencerUptimeFeed, + address _weth + ) PWNSimpleLoanElasticChainlinkProposal( + _hub, + _revokedNonce, + _config, + _utilizedCredit, + _chainlinkFeedRegistry, + _l2SequencerUptimeFeed, + _weth + ) {} + + + function exposed_erc712EncodeProposal(Proposal memory proposal) external pure returns (bytes memory) { + return _erc712EncodeProposal(proposal); + } + +} diff --git a/test/unit/PWNSimpleLoanElasticChainlinkProposal.t.sol b/test/unit/PWNSimpleLoanElasticChainlinkProposal.t.sol index 01970f4..9a222a2 100644 --- a/test/unit/PWNSimpleLoanElasticChainlinkProposal.t.sol +++ b/test/unit/PWNSimpleLoanElasticChainlinkProposal.t.sol @@ -11,6 +11,7 @@ import { Chainlink } from "pwn/loan/terms/simple/proposal/PWNSimpleLoanElasticChainlinkProposal.sol"; +import { PWNSimpleLoanElasticChainlinkProposalHarness } from "test/harness/PWNSimpleLoanElasticChainlinkProposalHarness.sol"; import { ChainlinkDenominations } from "test/helper/ChainlinkDenominations.sol"; import { MultiToken, @@ -22,7 +23,7 @@ import { abstract contract PWNSimpleLoanElasticChainlinkProposalTest is PWNSimpleLoanProposalTest { - PWNSimpleLoanElasticChainlinkProposal proposalContract; + PWNSimpleLoanElasticChainlinkProposalHarness proposalContract; PWNSimpleLoanElasticChainlinkProposal.Proposal proposal; PWNSimpleLoanElasticChainlinkProposal.ProposalValues proposalValues; @@ -38,7 +39,7 @@ abstract contract PWNSimpleLoanElasticChainlinkProposalTest is PWNSimpleLoanProp vm.etch(token, "bytes"); - proposalContract = new PWNSimpleLoanElasticChainlinkProposal(hub, revokedNonce, config, utilizedCredit, feedRegistry, address(0), weth); + proposalContract = new PWNSimpleLoanElasticChainlinkProposalHarness(hub, revokedNonce, config, utilizedCredit, feedRegistry, address(0), weth); proposalContractAddr = PWNSimpleLoanProposal(proposalContract); bool[] memory feedInvertFlags = new bool[](1); @@ -94,7 +95,7 @@ abstract contract PWNSimpleLoanElasticChainlinkProposalTest is PWNSimpleLoanProp )), keccak256(abi.encodePacked( keccak256("Proposal(uint8 collateralCategory,address collateralAddress,uint256 collateralId,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address creditAddress,address[] feedIntermediaryDenominations,bool[] feedInvertFlags,uint256 loanToValue,uint256 minCreditAmount,uint256 availableCreditLimit,bytes32 utilizedCreditId,uint256 fixedInterestAmount,uint24 accruingInterestAPR,uint32 durationOrDate,uint40 expiration,address allowedAcceptor,address proposer,bytes32 proposerSpecHash,bool isOffer,uint256 refinancingLoanId,uint256 nonceSpace,uint256 nonce,address loanContract)"), - abi.encode(_proposal) + proposalContract.exposed_erc712EncodeProposal(_proposal) )) )); } @@ -350,7 +351,7 @@ contract PWNSimpleLoanElasticChainlinkProposal_GetCollateralAmount_Test is PWNSi function test_shouldFetchSequencerUptimeFeed_whenFeedSet() external { vm.warp(1e9); - proposalContract = new PWNSimpleLoanElasticChainlinkProposal(hub, revokedNonce, config, utilizedCredit, feedRegistry, l2SequencerUptimeFeed, weth); + proposalContract = new PWNSimpleLoanElasticChainlinkProposalHarness(hub, revokedNonce, config, utilizedCredit, feedRegistry, l2SequencerUptimeFeed, weth); _mockSequencerUptimeFeed(true, block.timestamp - L2_GRACE_PERIOD - 1); _mockLastRoundData(feed, 1e18, block.timestamp); @@ -365,7 +366,7 @@ contract PWNSimpleLoanElasticChainlinkProposal_GetCollateralAmount_Test is PWNSi function test_shouldFail_whenL2SequencerDown_whenFeedSet() external { vm.warp(1e9); - proposalContract = new PWNSimpleLoanElasticChainlinkProposal(hub, revokedNonce, config, utilizedCredit, feedRegistry, l2SequencerUptimeFeed, weth); + proposalContract = new PWNSimpleLoanElasticChainlinkProposalHarness(hub, revokedNonce, config, utilizedCredit, feedRegistry, l2SequencerUptimeFeed, weth); _mockSequencerUptimeFeed(false, block.timestamp - L2_GRACE_PERIOD - 1); vm.expectRevert(abi.encodeWithSelector(Chainlink.L2SequencerDown.selector)); @@ -376,7 +377,7 @@ contract PWNSimpleLoanElasticChainlinkProposal_GetCollateralAmount_Test is PWNSi vm.warp(1e9); startedAt = bound(startedAt, block.timestamp - L2_GRACE_PERIOD, block.timestamp); - proposalContract = new PWNSimpleLoanElasticChainlinkProposal(hub, revokedNonce, config, utilizedCredit, feedRegistry, l2SequencerUptimeFeed, weth); + proposalContract = new PWNSimpleLoanElasticChainlinkProposalHarness(hub, revokedNonce, config, utilizedCredit, feedRegistry, l2SequencerUptimeFeed, weth); _mockSequencerUptimeFeed(true, startedAt); vm.expectRevert( @@ -521,6 +522,8 @@ contract PWNSimpleLoanElasticChainlinkProposal_AcceptProposal_Test is PWNSimpleL function test_shouldFail_whenZeroMinCreditAmount() external { proposal.minCreditAmount = 0; + bytes32 proposalHash = _proposalHash(proposal); + vm.expectRevert(abi.encodeWithSelector(PWNSimpleLoanElasticChainlinkProposal.MinCreditAmountNotSet.selector)); vm.prank(activeLoanContract); proposalContract.acceptProposal({ @@ -528,7 +531,7 @@ contract PWNSimpleLoanElasticChainlinkProposal_AcceptProposal_Test is PWNSimpleL refinancingLoanId: 0, proposalData: abi.encode(proposal, proposalValues), proposalInclusionProof: new bytes32[](0), - signature: _sign(proposerPK, _proposalHash(proposal)) + signature: _sign(proposerPK, proposalHash) }); } @@ -538,6 +541,8 @@ contract PWNSimpleLoanElasticChainlinkProposal_AcceptProposal_Test is PWNSimpleL proposal.minCreditAmount = bound(minCreditAmount, 1, type(uint256).max); proposalValues.creditAmount = bound(creditAmount, 0, proposal.minCreditAmount - 1); + bytes32 proposalHash = _proposalHash(proposal); + vm.expectRevert( abi.encodeWithSelector( PWNSimpleLoanElasticChainlinkProposal.InsufficientCreditAmount.selector, @@ -551,7 +556,7 @@ contract PWNSimpleLoanElasticChainlinkProposal_AcceptProposal_Test is PWNSimpleL refinancingLoanId: 0, proposalData: abi.encode(proposal, proposalValues), proposalInclusionProof: new bytes32[](0), - signature: _sign(proposerPK, _proposalHash(proposal)) + signature: _sign(proposerPK, proposalHash) }); } @@ -559,16 +564,18 @@ contract PWNSimpleLoanElasticChainlinkProposal_AcceptProposal_Test is PWNSimpleL proposalValues.creditAmount = bound(creditAmount, proposal.minCreditAmount, 1e40); proposal.isOffer = isOffer; + bytes32 proposalHash = _proposalHash(proposal); + vm.prank(activeLoanContract); - (bytes32 proposalHash, PWNSimpleLoan.Terms memory terms) = proposalContract.acceptProposal({ + (bytes32 proposalHash_, PWNSimpleLoan.Terms memory terms) = proposalContract.acceptProposal({ acceptor: acceptor, refinancingLoanId: 0, proposalData: abi.encode(proposal, proposalValues), proposalInclusionProof: new bytes32[](0), - signature: _sign(proposerPK, _proposalHash(proposal)) + signature: _sign(proposerPK, proposalHash) }); - assertEq(proposalHash, _proposalHash(proposal)); + assertEq(proposalHash_, proposalHash); assertEq(terms.lender, isOffer ? proposal.proposer : acceptor); assertEq(terms.borrower, isOffer ? acceptor : proposal.proposer); assertEq(terms.duration, proposal.durationOrDate); @@ -587,3 +594,74 @@ contract PWNSimpleLoanElasticChainlinkProposal_AcceptProposal_Test is PWNSimpleL } } + + +/*----------------------------------------------------------*| +|* # ERC712 ENCODE PROPOSAL *| +|*----------------------------------------------------------*/ + +contract PWNSimpleLoanElasticChainlinkProposal_Erc712EncodeProposal_Test is PWNSimpleLoanElasticChainlinkProposalTest { + + struct Proposal_ERC712 { + MultiToken.Category collateralCategory; + address collateralAddress; + uint256 collateralId; + bool checkCollateralStateFingerprint; + bytes32 collateralStateFingerprint; + address creditAddress; + bytes32 feedIntermediaryDenominationsHash; // encode array hash + bytes32 feedInvertFlagsHash; // encode array hash + uint256 loanToValue; + uint256 minCreditAmount; + uint256 availableCreditLimit; + bytes32 utilizedCreditId; + uint256 fixedInterestAmount; + uint24 accruingInterestAPR; + uint32 durationOrDate; + uint40 expiration; + address allowedAcceptor; + address proposer; + bytes32 proposerSpecHash; + bool isOffer; + uint256 refinancingLoanId; + uint256 nonceSpace; + uint256 nonce; + address loanContract; + } + + + function test_shouldERC712EncodeProposal() external { + Proposal_ERC712 memory proposalErc712 = Proposal_ERC712( + proposal.collateralCategory, + proposal.collateralAddress, + proposal.collateralId, + proposal.checkCollateralStateFingerprint, + proposal.collateralStateFingerprint, + proposal.creditAddress, + keccak256(abi.encodePacked(proposal.feedIntermediaryDenominations)), + keccak256(abi.encodePacked(proposal.feedInvertFlags)), + proposal.loanToValue, + proposal.minCreditAmount, + proposal.availableCreditLimit, + proposal.utilizedCreditId, + proposal.fixedInterestAmount, + proposal.accruingInterestAPR, + proposal.durationOrDate, + proposal.expiration, + proposal.allowedAcceptor, + proposal.proposer, + proposal.proposerSpecHash, + proposal.isOffer, + proposal.refinancingLoanId, + proposal.nonceSpace, + proposal.nonce, + proposal.loanContract + ); + + assertEq( + keccak256(abi.encode(proposalErc712)), + keccak256(proposalContract.exposed_erc712EncodeProposal(proposal)) + ); + } + +}