Alert Source Discuss
📢 Last Call Standards Track: SRC

SRC-5453: Endorsement - Permit for Any Functions

A general protocol for approving function calls in the same transaction relying on SRC-5750.

Authors Zainan Victor Zhou (@xinbenlv)
Created 2022-08-12
Last Call Deadline 2023-09-27
Requires SIP-165, SIP-712, SIP-1271, SIP-5750

Abstract

This SIP establishes a general protocol for permitting and approving function calls in the same transaction relying on SRC-5750. Unlike a few prior art (SRC-2612 for SRC-20, SRC-4494 for SRC-721 that usually only permit for a single behavior (transfer for SRC-20 and safeTransferFrom for SRC-721) and a single approver in two transactions (first a permit(...) TX, then a transfer-like TX), this SIP provides a way to permit arbitrary behaviors and aggregating multiple approvals from arbitrary number of approvers in the same transaction, allowing for Multi-Sig or Threshold Signing behavior.

Motivation

  1. Support permit(approval) alongside a function call.
  2. Support a second approval from another user.
  3. Support pay-for-by another user
  4. Support multi-sig
  5. Support persons acting in concert by endorsements
  6. Support accumulated voting
  7. Support off-line signatures

Specification

The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “NOT RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119 and RFC 8174.

Interfaces

The interfaces and structures referenced here are as follows

pragma solidity ^0.8.9;

struct ValidityBound {
    bytes32 functionParamStructHash;
    uint256 validSince;
    uint256 validBy;
    uint256 nonce;
}

struct SingleEndorsementData {
    address endorserAddress; // 32
    bytes sig; // dynamic = 65
}

struct GeneralExtensionDataStruct {
    bytes32 src5453MagicWord;
    uint256 src5453Type;
    uint256 nonce;
    uint256 validSince;
    uint256 validBy;
    bytes endorsementPayload;
}

interface ISRC5453EndorsementCore {
    function sip5453Nonce(address endorser) external view returns (uint256);
    function isEligibleEndorser(address endorser) external view returns (bool);
}

interface ISRC5453EndorsementDigest {
    function computeValidityDigest(
        bytes32 _functionParamStructHash,
        uint256 _validSince,
        uint256 _validBy,
        uint256 _nonce
    ) external view returns (bytes32);

    function computeFunctionParamHash(
        string memory _functionName,
        bytes memory _functionParamPacked
    ) external view returns (bytes32);
}

interface ISRC5453EndorsementDataTypeA {
    function computeExtensionDataTypeA(
        uint256 nonce,
        uint256 validSince,
        uint256 validBy,
        address endorserAddress,
        bytes calldata sig
    ) external view returns (bytes memory);
}


interface ISRC5453EndorsementDataTypeB {
    function computeExtensionDataTypeB(
        uint256 nonce,
        uint256 validSince,
        uint256 validBy,
        address[] calldata endorserAddress,
        bytes[] calldata sigs
    ) external view returns (bytes memory);
}

See ISRC5453.sol.

Behavior specification

As specified in SRC-5750 General Extensibility for Method Behaviors, any compliant method that has a bytes extraData parameter as its last designated parameter for extending behaviors can conform to SRC-5453 as the way to indicate a permit from a certain user.

  1. Any compliant method of this SIP MUST be a SRC-5750 compliant method.
  2. Caller MUST pass in the last parameter bytes extraData conforming to Solidity memory-encoded bytes of GeneralExtensionDataStruct specified in Section Interfaces. The following descriptions are based on when decoding bytes extraData into a GeneralExtensionDataStruct
  3. In the GeneralExtensionDataStruct-decoded extraData, caller MUST set the value of GeneralExtensionDataStruct.src5453MagicWord to be the keccak256("SRC5453-ENDORSEMENT").
  4. Caller MUST set the value of GeneralExtensionDataStruct.src5453Type to be one of the supported values.
uint256 constant SRC5453_TYPE_A = 1;
uint256 constant SRC5453_TYPE_B = 2;
  1. When the value of GeneralExtensionDataStruct.src5453Type is set to be SRC5453_TYPE_A, GeneralExtensionDataStruct.endorsementPayload MUST be abi encoded bytes of a SingleEndorsementData.
  2. When the value of GeneralExtensionDataStruct.src5453Type is set to be SRC5453_TYPE_B, GeneralExtensionDataStruct.endorsementPayload MUST be abi encoded bytes of SingleEndorsementData[] (a dynamic array).

  3. Each SingleEndorsementData MUST have a address endorserAddress; and a 65-bytes bytes sig signature.

  4. Each bytes sig MUST be an ECDSA (secp256k1) signature using private key of signer whose corresponding address is endorserAddress signing validityDigest which is the hashTypeDataV4 of SIP-712 of hashStruct of ValidityBound data structure as follows:
bytes32 validityDigest =
    sip712HashTypedDataV4(
        keccak256(
            abi.encode(
                keccak256(
                    "ValidityBound(bytes32 functionParamStructHash,uint256 validSince,uint256 validBy,uint256 nonce)"
                ),
                functionParamStructHash,
                _validSince,
                _validBy,
                _nonce
            )
        )
    );
  1. The functionParamStructHash MUST be computed as follows
        bytes32 functionParamStructHash = keccak256(
            abi.encodePacked(
                keccak256(bytes(_functionStructure)),
                _functionParamPacked
            )
        );
        return functionParamStructHash;

whereas

  • _functionStructure MUST be computed as function methodName(type1 param1, type2 param2, ...).
  • _functionParamPacked MUST be computed as enc(param1) || enco(param2) ...
  1. Upon validating that endorserAddress == ecrecover(validityDigest, signature) or SIP1271(endorserAddress).isValidSignature(validityDigest, signature) == SRC1271.MAGICVALUE, the single endorsement MUST be deemed valid.
  2. Compliant method MAY choose to impose a threshold for a number of endorsements needs to be valid in the same SRC5453_TYPE_B kind of endorsementPayload.

  3. The validSince and validBy are both inclusive. Implementer MAY choose to use blocknumber or timestamp. Implementors SHOULD find a way to indicate whether validSince and validBy is blocknumber or timestamp.

Rationale

  1. We chose to have both SRC5453_TYPE_A(single-endorsement) and SRC5453_TYPE_B(multiple-endorsements, same nonce for entire contract) so we could balance a wider range of use cases. E.g. the same use cases of SRC-2612 and SRC-4494 can be supported by SRC5453_TYPE_A. And threshold approvals can be done via SRC5453_TYPE_B. More complicated approval types can also be extended by defining new SRC5453_TYPE_?

  2. We chose to include both validSince and validBy to allow maximum flexibility in expiration. This can also be supported natively by the SVM if SRC-5081 is adopted, but SRC-5081 will not be adopted anytime soon, so we choose to add these two numbers in our protocol to allow smart contract level support.

Backwards Compatibility

The design assumes a bytes calldata extraData to maximize the flexibility of future extensions. This assumption is compatible with SRC-721, SRC-1155 and many other SRC-track SIPs. Those that aren’t, such as SRC-20, can also be updated to support it, such as using a wrapper contract or proxy upgrade.

Reference Implementation

In addition to the specified algorithm for validating endorser signatures, we also present the following reference implementations.

pragma solidity ^0.8.9;

import "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol";
import "@openzeppelin/contracts/utils/cryptography/SIP712.sol";

import "./ISRC5453.sol";

abstract contract ASRC5453Endorsible is SIP712,
    ISRC5453EndorsementCore, ISRC5453EndorsementDigest, ISRC5453EndorsementDataTypeA, ISRC5453EndorsementDataTypeB {
    // ...

    function _validate(
        bytes32 msgDigest,
        SingleEndorsementData memory endersement
    ) internal virtual {
        require(
            endersement.sig.length == 65,
            "ASRC5453Endorsible: wrong signature length"
        );
        require(
            SignatureChecker.isValidSignatureNow(
                endersement.endorserAddress,
                msgDigest,
                endersement.sig
            ),
            "ASRC5453Endorsible: invalid signature"
        );
    }
    // ...

    modifier onlyEndorsed(
        bytes32 _functionParamStructHash,
        bytes calldata _extensionData
    ) {
        require(_isEndorsed(_functionParamStructHash, _extensionData));
        _;
    }

    function computeExtensionDataTypeB(
        uint256 nonce,
        uint256 validSince,
        uint256 validBy,
        address[] calldata endorserAddress,
        bytes[] calldata sigs
    ) external pure override returns (bytes memory) {
        require(endorserAddress.length == sigs.length);
        SingleEndorsementData[]
            memory endorsements = new SingleEndorsementData[](
                endorserAddress.length
            );
        for (uint256 i = 0; i < endorserAddress.length; ++i) {
            endorsements[i] = SingleEndorsementData(
                endorserAddress[i],
                sigs[i]
            );
        }
        return
            abi.encode(
                GeneralExtensionDataStruct(
                    MAGIC_WORLD,
                    SRC5453_TYPE_B,
                    nonce,
                    validSince,
                    validBy,
                    abi.encode(endorsements)
                )
            );
    }
}

See ASRC5453.sol

Reference Implementation of EndorsableSRC721

Here is a reference implementation of EndorsableSRC721 that achieves similar behavior to SRC-4494.

pragma solidity ^0.8.9;

contract EndorsableSRC721 is SRC721, ASRC5453Endorsible {
    //...

    function mint(
        address _to,
        uint256 _tokenId,
        bytes calldata _extraData
    )
        external
        onlyEndorsed(
            _computeFunctionParamHash(
                "function mint(address _to,uint256 _tokenId)",
                abi.encode(_to, _tokenId)
            ),
            _extraData
        )
    {
        _mint(_to, _tokenId);
    }
}

See EndorsableSRC721.sol

Reference Implementation of ThresholdMultiSigForwarder

Here is a reference implementation of ThresholdMultiSigForwarder that achieves similar behavior of multi-sig threshold approval remote contract call like a Gnosis-Safe wallet.

pragma solidity ^0.8.9;

contract ThresholdMultiSigForwarder is ASRC5453Endorsible {
    //...
    function forward(
        address _dest,
        uint256 _value,
        uint256 _gasLimit,
        bytes calldata _calldata,
        bytes calldata _extraData
    )
        external
        onlyEndorsed(
            _computeFunctionParamHash(
                "function forward(address _dest,uint256 _value,uint256 _gasLimit,bytes calldata _calldata)",
                abi.encode(_dest, _value, _gasLimit, keccak256(_calldata))
            ),
            _extraData
        )
    {
        string memory errorMessage = "Fail to call remote contract";
        (bool success, bytes memory returndata) = _dest.call{value: _value}(
            _calldata
        );
        Address.verifyCallResult(success, returndata, errorMessage);
    }

}

See ThresholdMultiSigForwarder.sol

Security Considerations

Replay Attacks

A replay attack is a type of attack on cryptography authentication. In a narrow sense, it usually refers to a type of attack that circumvents the cryptographically signature verification by reusing an existing signature for a message being signed again. Any implementations relying on this SIP must realize that all smart endorsements described here are cryptographic signatures that are public and can be obtained by anyone. They must foresee the possibility of a replay of the transactions not only at the exact deployment of the same smart contract, but also other deployments of similar smart contracts, or of a version of the same contract on another chainId, or any other similar attack surfaces. The nonce, validSince, and validBy fields are meant to restrict the surface of attack but might not fully eliminate the risk of all such attacks, e.g. see the Phishing section.

Phishing

It’s worth pointing out a special form of replay attack by phishing. An adversary can design another smart contract in a way that the user may be tricked into signing a smart endorsement for a seemingly legitimate purpose, but the data-to-designed matches the target application

Copyright and related rights waived via CC0.

Citation

Please cite this document as:

Zainan Victor Zhou (@xinbenlv), "SRC-5453: Endorsement - Permit for Any Functions [LAST CALL]," Sila Improvement Proposals, no. 5453, August 2022. Available: https://sips.sila.org/SIPS/sip-5453.