Skip to content

Commit

Permalink
feat(multiproposal): implement multiproposal
Browse files Browse the repository at this point in the history
  • Loading branch information
ashhanai committed Apr 5, 2024
1 parent d1291d6 commit 21774f7
Show file tree
Hide file tree
Showing 12 changed files with 340 additions and 155 deletions.
3 changes: 3 additions & 0 deletions src/loan/terms/simple/loan/PWNSimpleLoan.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -252,6 +253,7 @@ contract PWNSimpleLoanDutchAuctionProposal is PWNSimpleLoanProposal {
acceptor,
refinancingLoanId,
proposalHash,
proposalInclusionProof,
signature,
ProposalBase({
collateralAddress: proposal.collateralAddress,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -188,6 +189,7 @@ contract PWNSimpleLoanFungibleProposal is PWNSimpleLoanProposal {
acceptor,
refinancingLoanId,
proposalHash,
proposalInclusionProof,
signature,
ProposalBase({
collateralAddress: proposal.collateralAddress,
Expand Down
2 changes: 2 additions & 0 deletions src/loan/terms/simple/proposal/PWNSimpleLoanListProposal.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -176,6 +177,7 @@ contract PWNSimpleLoanListProposal is PWNSimpleLoanProposal {
acceptor,
refinancingLoanId,
proposalHash,
proposalInclusionProof,
signature,
ProposalBase({
collateralAddress: proposal.collateralAddress,
Expand Down
57 changes: 52 additions & 5 deletions src/loan/terms/simple/proposal/PWNSimpleLoanProposal.sol
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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;
Expand Down Expand Up @@ -69,13 +78,31 @@ abstract contract PWNSimpleLoanProposal {
block.chainid,
address(this)
));

MULTIPROPOSAL_DOMAIN_SEPARATOR = keccak256(abi.encode(
keccak256("EIP712Domain(string name)"),
keccak256("PWNMultiproposal")
));
}


/*----------------------------------------------------------*|
|* # 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.
Expand All @@ -91,13 +118,15 @@ 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.
*/
function acceptProposal(
address acceptor,
uint256 refinancingLoanId,
bytes calldata proposalData,
bytes32[] calldata proposalInclusionProof,
bytes calldata signature
) virtual external returns (bytes32 proposalHash, PWNSimpleLoan.Terms memory loanTerms);

Expand Down Expand Up @@ -141,14 +170,16 @@ 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.
*/
function _acceptProposal(
address acceptor,
uint256 refinancingLoanId,
bytes32 proposalHash,
bytes memory signature,
bytes32[] calldata proposalInclusionProof,
bytes calldata signature,
ProposalBase memory proposal
) internal {
// Check loan contract
Expand All @@ -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 });
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -142,6 +143,7 @@ contract PWNSimpleLoanSimpleProposal is PWNSimpleLoanProposal {
acceptor,
refinancingLoanId,
proposalHash,
proposalInclusionProof,
signature,
ProposalBase({
collateralAddress: proposal.collateralAddress,
Expand Down
18 changes: 14 additions & 4 deletions test/unit/PWNSimpleLoan.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -114,6 +114,7 @@ abstract contract PWNSimpleLoanTest is Test {
proposalSpec = PWNSimpleLoan.ProposalSpec({
proposalContract: proposalContract,
proposalData: proposalData,
proposalInclusionProof: new bytes32[](0),
signature: signature
});

Expand Down Expand Up @@ -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)
);
}
Expand Down Expand Up @@ -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);
Expand Down
60 changes: 27 additions & 33 deletions test/unit/PWNSimpleLoanDutchAuctionProposal.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -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))
});
}

Expand Down Expand Up @@ -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))
});
}

Expand Down Expand Up @@ -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));
Expand Down
Loading

0 comments on commit 21774f7

Please sign in to comment.