Simple Loan

1. Summary

PWNSimpleLoan.sol contract manages the simple loan type in the PWN protocol. This contract also acts as a Vault for all assets used in simple loans.

3. Contract details

  • PWNSimpleLoan.sol is written in Solidity version 0.8.16

Features

  • Manages simple loan flow

    • Creation

    • Repayment

    • Claim

  • Implements an option for the lender to extend the expiration (maturity) date of a loan

  • Acts as a vault for all assets used in simple loans

The expiration of a loan can be extended by a maximum of 90 days into the future. This is a measure to protect lenders from accidentally extending a loan maturity date too far. Lenders can extend a loan expiration date an unlimited amount of times meaning a loan expiration date can be extended indefinitely. Minumum duration of a simple loan is 10 minutes.

Inherited contracts, implemented Interfaces and ERCs

Functions

createLOAN

Overview

Users use this function to start a simple loan in the PWN protocol.

The function assumes a prior token approval to a contract address or signed permits.

This function takes four arguments supplied by the caller:

  • ProposalSpec calldataproposalSpec - Proposal specification struct

  • LenderSpec calldatalenderSpec - Lender specification struct

  • CallerSpec calldatacallerSpec - Caller specification struct

  • bytes calldataextra - Auxiliary data that are emitted in the LOANCreated event. They are not used in the contract logic.

Implementation

function createLOAN(
    ProposalSpec calldata proposalSpec,
    LenderSpec calldata lenderSpec,
    CallerSpec calldata callerSpec,
    bytes calldata extra
) external returns (uint256 loanId) {
    // 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,
            proposalInclusionProof: proposalSpec.proposalInclusionProof,
            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 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 InterestAPROutOfBounds({ current: loanTerms.accruingInterestAPR, limit: MAX_ACCRUING_INTEREST_APR });
    }

    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({
        loanTerms: loanTerms,
        lenderSpec: lenderSpec
    });

    emit LOANCreated({
        loanId: loanId,
        proposalHash: proposalHash,
        proposalContract: proposalSpec.proposalContract,
        refinancingLoanId: callerSpec.refinancingLoanId,
        terms: loanTerms,
        lenderSpec: lenderSpec,
        extra: extra
    });

    // Execute permit for the caller
    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) {
        // Transfer collateral to Vault and credit to borrower
        _settleNewLoan(loanTerms, lenderSpec);
    } else {
        // 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
        });
    }
}
repayLOAN

Overview

Borrowers use this function to repay simple loans in the PWN Protocol.

This function takes two arguments supplied by the caller:

  • uint256loanId - ID of the loan that is being repaid

  • bytes calldatapermitData - Permit data for a loan asset signed by borrower

Implementation

function repayLOAN(
    uint256 loanId,
    bytes calldata permitData
) external {
    LOAN storage loan = LOANs[loanId];

    _checkLoanCanBeRepaid(loan.status, loan.defaultTimestamp);

    // Update loan to repaid state
    _updateRepaidLoan(loanId);

    // Execute permit for the caller
    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
    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.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.
    }
}
claimLOAN

Overview

Holders of LOAN tokens (lenders) use this function to claim a repaid loan or defaulted collateral. The claimed asset is transferred to the LOAN token holder and the LOAN token is burned.

This function takes one argument supplied by the caller:

  • uint256loanId - ID of the loan that is being claimed

Implementation

function claimLOAN(uint256 loanId) external {
    LOAN storage loan = LOANs[loanId];

    // Check that caller is LOAN token holder
    if (loanToken.ownerOf(loanId) != msg.sender)
        revert CallerNotLOANTokenHolder();

    if (loan.status == 0)
        // Loan is not existing or from a different loan contract
        revert NonExistingLoan();
    else if (loan.status == 3)
        // Loan has been paid back
        _settleLoanClaim({ loanId: loanId, loanOwner: msg.sender, defaulted: false });
    else if (loan.status == 2 && loan.defaultTimestamp <= block.timestamp)
        // Loan is running but expired
        _settleLoanClaim({ loanId: loanId, loanOwner: msg.sender, defaulted: true });
    else
        // Loan is in wrong state
        revert LoanRunning();
}
extendLOAN

Overview

This function extends loan default date with signed extension proposal signed by borrower or the LOAN token owner (usually the lender).

This function takes three arguments supplied by the caller:

  • ExtensionProposal calldataextension - Loan extension proposal struct

  • bytes calldatasignature - Signature of the extension proposal

  • bytes calldatapermitData - Callers credit permit data

Implementation

function extendLOAN(
    ExtensionProposal calldata extension,
    bytes calldata signature,
    bytes calldata permitData
) 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 LoanRepaid();

    // Check extension validity
    bytes32 extensionHash = getExtensionHash(extension);
    if (!extensionProposalsMade[extensionHash])
        if (!PWNSignatureChecker.isValidSignatureNow(extension.proposer, extensionHash, signature))
            revert PWNSignatureChecker.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 PWNRevokedNonce.NonceNotUsable({
            addr: extension.proposer,
            nonceSpace: extension.nonceSpace,
            nonce: extension.nonce
        });

    // 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 proposal nonce
    revokedNonce.revokeNonce(extension.proposer, extension.nonceSpace, 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
    });

    // Skip compensation transfer if it's not set
    if (extension.compensationAddress != address(0) && extension.compensationAmount > 0) {
        MultiToken.Asset memory compensation = extension.compensationAddress.ERC20(extension.compensationAmount);

        // Check compensation asset validity
        _checkValidAsset(compensation);

        // Transfer compensation to the loan owner
        if (permitData.length > 0) {
            Permit memory permit = abi.decode(permitData, (Permit));
            _checkPermit(msg.sender, extension.compensationAddress, permit);
            _tryPermit(permit);
        }
        _pushFrom(compensation, loan.borrower, loanOwner);
    }
}
makeExtensionProposal

Overview

This function an on-chain extension proposal.

This function takes one argument supplied by the caller:

  • ExtensionProposal calldataextension - Loan extension proposal struct

Implementation

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 proposal as made
    bytes32 extensionHash = getExtensionHash(extension);
    extensionProposalsMade[extensionHash] = true;

    emit ExtensionProposalMade(extensionHash, extension.proposer, extension);
}

View Functions

getLOAN

Overview

Returns a tuple with information about a supplied loan ID.

This function takes one argument supplied by the caller:

  • uint256loanId - ID of the loan to get parameters for

Implementation

function getLOAN(uint256 loanId) external view returns (
    uint8 status,
    uint40 startTimestamp,
    uint40 defaultTimestamp,
    address borrower,
    address originalLender,
    address loanOwner,
    uint24 accruingInterestAPR,
    uint256 fixedInterestAmount,
    MultiToken.Asset memory credit,
    MultiToken.Asset memory collateral,
    address originalSourceOfFunds,
    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);
    accruingInterestAPR = loan.accruingInterestAPR;
    fixedInterestAmount = loan.fixedInterestAmount;
    credit = loan.creditAddress.ERC20(loan.principalAmount);
    collateral = loan.collateral;
    originalSourceOfFunds = loan.originalSourceOfFunds;
    repaymentAmount = loanRepaymentAmount(loanId);
}
loanRepaymentAmount

Overview

Calculates the loan repayment amount for supplied loan ID.

This function takes one argument supplied by the caller:

  • uint256loanId - ID of the loan to get loan repayment amount for

Implementation

function loanRepaymentAmount(uint256 loanId) public view returns (uint256) {
    LOAN storage loan = LOANs[loanId];

    // Check non-existent loan
    if (loan.status == 0) return 0;

    // Return loan principal with accrued interest
    return loan.principalAmount + _loanAccruedInterest(loan);
}
getExtensionHash

Overview

Returns hash of the supplied loan extension proposal struct.

This function takes one argument supplied by the caller:

  • ExtensionProposal calldataextension - Loan extension proposal struct

Implementation

function getExtensionHash(ExtensionProposal calldata extension) public view returns (bytes32) {
    return keccak256(abi.encodePacked(
        hex"1901",
        DOMAIN_SEPARATOR,
        keccak256(abi.encodePacked(
            EXTENSION_PROPOSAL_TYPEHASH,
            abi.encode(extension)
        ))
    ));
}
isValidAsset

Overview

Checks if the supplied asset is valid with the MultiToken dependency lib and the category registry.

This function takes one argument supplied by the caller:

  • MultiToken.Assetasset - The asset to check (see MultiToken)

Implementation

function isValidAsset(MultiToken.Asset memory asset) public view returns (bool) {
    return MultiToken.isValid(asset, categoryRegistry);
}
loanMetadataUri

Overview

Returns a metadata URI for LOAN tokens. This URI is defined in PWN Config.

This function doesn't take any arguments.

Implementation

function loanMetadataUri() override external view returns (string memory) {
    return config.loanMetadataUri(address(this));
}
getStateFingerprint

Overview

This function returns the current token state fingerprint for a supplied token ID. See ERC-5646 standard specification for more detailed information.

This function takes one argument supplied by the caller:

  • uint256tokenId - ID of the LOAN token to get a fingerprint for

Implementation

function getStateFingerprint(uint256 tokenId) external view virtual override returns (bytes32) {
    LOAN storage loan = LOANs[tokenId];

    if (loan.status == 0)
        return bytes32(0);

    // The only mutable state properties are:
    // - 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
    // - 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.accruingInterestAPR
    ));
}

Events

The PWN Simple Loan contract defines one event and no custom errors.

event LOANCreated(uint256 indexed loanId, bytes32 indexed proposalHash, address indexed proposalContract, uint256 refinancingLoanId, Terms terms, LenderSpec lenderSpec, bytes extra);
event LOANPaidBack(uint256 indexed loanId);
event LOANClaimed(uint256 indexed loanId, bool indexed defaulted);
event LOANExtended(uint256 indexed loanId, uint40 originalDefaultTimestamp, uint40 extendedDefaultTimestamp);
event ExtensionProposalMade(bytes32 indexed extensionHash, address indexed proposer,  ExtensionProposal proposal);
LOANCreated

LOANCreated is emitted when a new simple loan is created.

This event has two parameters:

  • uint256 indexedloanId - ID of the LOAN token that is associated with the created loan

  • bytes32 indexedproposalHash - Hash of the proposal struct

  • address indexedproposalContract - Address of the proposal contract

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

  • Termsterms - Terms struct defining simple loan parameters

  • LenderSpeclenderSpec - LenderSpec struct

  • bytesextra - Auxiliary data provided by the caller to the createLOAN function

LOANPaidBack

LOANPaidBack event is emitted when a borrower repays a simple loan.

This event has one parameter:

  • uint256 indexedloanId - ID of the LOAN token that is associated with the repaid loan

LOANClaimed

LOANClaimed event is emitted when a lender claims repaid asset or defaulted collateral.

This event has two parameters:

  • uint256 indexedloanId - ID of the LOAN token that is associated with the claimed loan

  • bool indexeddefaulted - Boolean determining if the claimed loan was defaulted or properly repaid

LOANExtended

LOANExtended event is emitted when a LOAN token holder extends a loan.

This event has three parameters:

  • uint256 indexedloanId - ID of the LOAN token that is associated with the loan being extended

  • uint40originalDefaultTimestamp - Original timestamp

  • uint40extendedDefaultTimestamp - New timestamp

ExtensionProposalMade

ExtensionProposalMade event is emitted when an on-chain loan extension proposal is made.

This event has two parameters:

  • bytes32 indexedextensionHash - Hash of the created extension proposal

  • address indexedproposer - Address of the account that created the extension proposal

  • ExtensionProposalproposal - Extension proposal struct

Errors

error LoanNotRunning();
error LoanRunning();
error LoanRepaid();
error LoanDefaulted(uint40);
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 InterestAPROutOfBounds(uint256 current, uint256 limit);
error CallerNotVault();
error InvalidSourceOfFunds(address sourceOfFunds);
error InvalidExtensionCaller();
error InvalidExtensionSigner(address allowed, address current);
error InvalidExtensionDuration(uint256 duration, uint256 limit);
error InvalidMultiTokenAsset(uint8 category, address addr, uint256 id, uint256 amount);
LoanNotRunning

A LoanNotRunning error is thrown when managed loan is not running.

This error has doesn't define any parameters.

LoanRunning

A LoanRunning error is thrown when managed loan is running.

This error has doesn't define any parameters.

LoanRepaid

A LoanRepaid error is thrown when managed loan is repaid.

This error has doesn't define any parameters.

LoanDefaulted

A NonceAlreadyRevoked error is thrown when managed loan is defaulted.

This error has one parameter:

  • uint40 - Default timestamp of the loan

NonExistingLoan

A NonExistingLoan error is thrown when loan doesn't exist.

This error has doesn't define any parameters.

CallerNotLOANTokenHolder

A CallerNotLOANTokenHolder error is thrown when caller is not a LOAN token holder.

This error has doesn't define any parameters.

RefinanceBorrowerMismatch

A RefinanceBorrowerMismatch error is thrown when refinancing loan terms have different borrower than the original loan.

This error has two parameters:

  • addresscurrentBorrower

  • addressnewBorrower

RefinanceCreditMismatch

A RefinanceCreditMismatch error is thrown when refinancing loan terms have different credit asset than the original loan.

This error has doesn't define any parameters.

RefinanceCollateralMismatch

A RefinanceCollateralMismatch error is thrown when refinancing loan terms have different collateral asset than the original loan.

This error has doesn't define any parameters.

InvalidLenderSpecHash

A InvalidLenderSpecHash error is thrown when hash of provided lender spec doesn't match the one in loan terms.

This error has two parameters:

  • bytes32current - Provided lender spec hash

  • bytes32expected - Expected lender spec hash

InvalidDuration

A InvalidDuration error is thrown when loan duration is below the minimum (10 minutes).

This error has two parameters:

  • uint256current - Provided loan duration

  • uint256limit - Provided loan duration

InterestAPROutOfBounds

A InterestAPROutOfBounds error is thrown when accruing interest APR is above the maximum.

This error has two parameters:

  • uint256current - Current accrued interest

  • uint256limit - Maximum accrued interest

CallerNotVault

A CallerNotVault error is thrown when caller is not a vault.

This error has doesn't define any parameters.

InvalidSourceOfFunds

A InvalidSourceOfFunds error is thrown when pool based source of funds doesn't have a registered adapter.

This error has one parameter:

  • addresssourceOfFunds

InvalidExtensionCaller

A InvalidExtensionCaller error is thrown when caller is not a loan borrower or lender.

This error has doesn't define any parameters.

InvalidExtensionSigner

A InvalidExtensionSigner error is thrown when signer is not a loan extension proposer.

This error has two parameters:

  • addressallowed

  • addresscurrent

InvalidExtensionDuration

A InvalidExtensionDuration error is thrown when loan extension duration is out of bounds.

This error has two parameters:

  • uint256duration

  • uint256limit

InvalidMultiTokenAsset

A InvalidMultiTokenAsset error is thrown when MultiToken Asset struct is invalid which can happen because of invalid category, address, id or amount.

See MultiToken for more information about the Asset struct.

This error has four parameters:

  • uint8category

  • addressaddr

  • uint256id

  • uint256amount

Terms Struct

Type
Name
Comment

address

lender

Address of a lender

address

borrower

Address of a borrower

uint32

duration

Loan duration in seconds

MultiToken.Asset (see Asset struct)

collateral

Asset used as a loan collateral

MultiToken.Asset (see Asset struct)

credit

Asset used as a loan credit

uint256

fixedInterestAmount

Fixed interest amount in credit asset tokens. It is the minimum amount of interest which has to be paid by a borrower

uint24

accruingInterestAPR

Accruing interest APR with 2 decimals

bytes32

lenderSpecHash

Hash of a lender specification struct

bytes32

borrowerSpecHash

Hash of a borrower specification struct

ProposalSpec Struct

Type
Name
Comment

address

proposalContract

Address of a loan proposal contract

bytes

proposalData

Encoded proposal data that is passed to the loan proposal contract

bytes32[]

proposalInclusionProof

Inclusion proof of the proposal in the proposal contract

bytes

signature

Signature of the proposal

LenderSpec Struct

Type
Name
Comment

address

sourceOfFunds

Address of a source of funds. This can be the lenders address, if the loan is funded directly, or a pool address from which the funds are withdrawn on the lenders behalf

CallerSpec Struct

Type
Name
Comment

uint256

refinancingLoanId

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

bool

revokeNonce

Flag if the callers nonce should be revoked

uint256

nonce

Callers nonce to be revoked. Nonce is revoked from the current nonce space

bytes

permitData

Callers permit data for a loans credit asset

LOAN Struct

Type
Name
Comment

uint8

status

0 -> None/Dead

2 -> Running

3 -> Repaid

4 -> Expired

address

creditAddress

Address of an asset used as a loan credit

address

originalSourceOfFunds

Address of a source of funds that was used to fund the loan

uint40

startTimestamp

Unix timestamp (in seconds) of a start date

uint40

defaultTimestamp

Unix timestamp (in seconds) of a default date

address

borrower

Address of a borrower

address

originalLender

Address of a lender that funded the loan

uint24

accruingInterestAPR

Accruing interest APR with 2 decimals

uint256

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.

uint256

principalAmount

Principal amount in credit asset tokens

MultiToken.Asset (see Asset struct)

collateral

Asset used as a loan collateral

ExtensionProposal Struct

Type
Name
Comment

uint256

loanId

ID of a loan to be extended

address

compensationAddress

Address of a compensation asset

uint256

compensationAmount

Amount of a compensation asset that a borrower has to pay to a lender

uint40

duration

Duration of the extension in seconds

uint40

expiration

Unix timestamp (in seconds) of an expiration date

address

proposer

Address of a proposer that signed the extension proposal

uint256

nonceSpace

Nonce space of the extension proposal nonce

uint256

nonce

Nonce of the extension proposal

Last updated