Simple Loan Proposal

1. Summary

PWNSimpleLoanProposal.sol is an abstract contract inherited by Simple Loan Proposal types.

3. Contract details

  • PWNSimpleLoanProposal.sol is written in Solidity version 0.8.16

Features

  • Defines base interface for Simple Loan Proposals

  • Defines ProposalBase struct

  • Implements _makeProposal and _acceptProposal functions

Functions

revokeNonce

Overview

A helper function for revoking a proposal nonce on behalf of a caller.

This function takes two arguments supplied by the caller:

  • uint256nonceSpace - Nonce space of a proposal nonce to be revoked

  • uint256nonce - Proposal nonce to be revoked

Implementation

function revokeNonce(uint256 nonceSpace, uint256 nonce) external {
    revokedNonce.revokeNonce(msg.sender, nonceSpace, nonce);
}

Internal Functions

_makeProposal

Overview

Function to make an on-chain proposal.

This function takes two arguments:

  • bytes32proposalHash - hash of the respective proposal type struct

  • addressproposer - Address of a proposal proposer

Implementation

function _makeProposal(bytes32 proposalHash, address proposer) internal {
    if (msg.sender != proposer) {
        revert CallerIsNotStatedProposer({ addr: proposer });
    }

    proposalsMade[proposalHash] = true;
}
_acceptProposal

Overview

Makes necessary checks for accepting a proposal and reverts if any loan parameters are not valid.

This function takes six arguments:

  • addressacceptor - Address of a proposal acceptor

  • uint256refinancingLoanId - Refinancing loan ID

  • bytes32proposalHash - Proposal hash

  • bytes32[] calldataproposalInclusionProof - Multiproposal inclusion proof. Empty if single proposal

  • bytes calldatasignature - Signature of a proposal

  • ProposalBase memoryproposal - ProposalBase struct

Implementation

function _acceptProposal(
    address acceptor,
    uint256 refinancingLoanId,
    bytes32 proposalHash,
    bytes32[] calldata proposalInclusionProof,
    bytes calldata 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 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 PWNSignatureChecker.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 PWNSignatureChecker.InvalidSignature({ signer: proposal.proposer, digest: multiproposalHash });
        }
    }

    // Check proposer is not acceptor
    if (proposal.proposer == acceptor) {
        revert AcceptorIsProposer({ addr: acceptor});
    }

    // 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 >= proposal.expiration) {
        revert Expired({ current: block.timestamp, expiration: proposal.expiration });
    }

    // Check proposal is not revoked
    if (!revokedNonce.isNonceUsable(proposal.proposer, proposal.nonceSpace, proposal.nonce)) {
        revert PWNRevokedNonce.NonceNotUsable({
            addr: proposal.proposer,
            nonceSpace: proposal.nonceSpace,
            nonce: proposal.nonce
        });
    }

    // Check propsal is accepted by an allowed address
    if (proposal.allowedAcceptor != address(0) && acceptor != proposal.allowedAcceptor) {
        revert CallerNotAllowedAcceptor({ current: acceptor, allowed: proposal.allowedAcceptor });
    }

    if (proposal.availableCreditLimit == 0) {
        // Revoke nonce if credit limit is 0, proposal can be accepted only once
        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] += proposal.creditAmount;
    } else {
        // Revert if credit limit is exceeded
        revert AvailableCreditLimitExceeded({
            used: creditUsed[proposalHash] + proposal.creditAmount,
            limit: proposal.availableCreditLimit
        });
    }

    // Check collateral state fingerprint if needed
    if (proposal.checkCollateralStateFingerprint) {
        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();
        }

        if (proposal.collateralStateFingerprint != currentFingerprint) {
            // Fingerprint mismatch
            revert InvalidCollateralStateFingerprint({
                current: currentFingerprint,
                proposed: proposal.collateralStateFingerprint
            });
        }
    }
}

}

View Functions

getMultiproposalHash

Overview

This function returns a multiproposal hash according to EIP-712.

This function takes one argument supplied by the caller:

Implementation

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)
        ))
    ));
}
_getProposalHash

Overview

This function returns a proposal hash according to EIP-712.

This function takes two arguments supplied by the caller:

  • bytes32proposalTypehash - Hash of the respective proposal type

  • bytes memoryencodedProposal - Encoded respective proposal type struct

Implementation

function _getProposalHash(
    bytes32 proposalTypehash,
    bytes memory encodedProposal
) internal view returns (bytes32) {
    return keccak256(abi.encodePacked(
        hex"1901", DOMAIN_SEPARATOR, keccak256(abi.encodePacked(
            proposalTypehash, encodedProposal
        ))
    ));
}

Errors

The PWN Simple Loan Offer contract defines eight errors and no events.

error CallerNotLoanContract(address caller, address loanContract);
error MissingStateFingerprintComputer();
error InvalidCollateralStateFingerprint(bytes32 current, bytes32 proposed);
error CallerIsNotStatedProposer(address addr);
error AcceptorIsProposer(address addr);
error InvalidRefinancingLoanId(uint256 refinancingLoanId);
error AvailableCreditLimitExceeded(uint256 used, uint256 limit);
error CallerNotAllowedAcceptor(address current, address allowed);
CallerNotLoanContract

A CallerNotLoanContract error is thrown when a caller is missing a required hub tag.

This error has two parameters:

  • addresscaller

  • addressloanContract

MissingStateFingerprintComputer

A MissingStateFingerprintComputer error is thrown when a state fingerprint computer is not registered.

This error doesn't define any parameters.

InvalidCollateralStateFingerprint

A InvalidCollateralStateFingerprint error is thrown when a proposed collateral state fingerprint doesn't match the current state.

This error has two parameters:

  • bytes32current

  • bytes32proposed

CallerIsNotStatedProposer

A CallerIsNotStatedProposer error is thrown when a caller is not a stated proposer.

This error has one parameter:

  • addressaddr

AcceptorIsProposer

An AcceptorIsProposer error is thrown when proposal acceptor and proposer are the same.

This error has one parameter:

  • addressaddr

InvalidRefinancingLoanId

An InvalidRefinancingLoanId error is thrown when provided refinance loan id cannot be used.

This error has one parameter:

  • uint256refinancingLoanId

AvailableCreditLimitExceeded

An AvailableCreditLimitExceeded error is thrown when a proposal would exceed the available credit limit.

This error has two parameters:

  • uint256used

  • uint256limit

CallerNotAllowedAcceptor

A CallerNotAllowedAcceptor error is thrown when caller is not allowed to accept a proposal.

This error has two parameters:

  • addresscurrent

  • addressallowed

ProposalBase Struct

TypeNameComment

address

collateralAddress

Address of a loan collateral

uint256

collateralId

ID of a collateral. Zero if ERC-20

bool

checkCollateralStateFingerprint

Flag to enable check of collaterals state fingerprint (see ERC-5646)

bytes32

collateralStateFingerprint

A collateral state fingerprint (see ERC-5646)

uint256

creditAmount

Amount of credit asset

uint256

availableCreditLimit

Maximum credit limit of credit asset

uint40

expiration

Proposal expiration unix timestamp in seconds

address

allowedAcceptor

Allowed acceptor address. Zero address if propsal can be accepted by any account

address

proposer

Proposer address

bool

isOffer

Flag to determine if a proposal is an offer or loan request

uint256

refinancingLoanId

ID of a loan to be refinanced. Zero if creating a new loan.

uint256

nonceSpace

Nonce space of the proposal

uint256

nonce

Nonce of the proposal

address

loanContract

Loan type contract

Multiproposal Struct

TypeNameComment

bytes32

multiproposalMerkleRoot

Root of the multiproposal merkle tree

Last updated