Virtual Signer Approvals with Smart Contracts

When configured for Smart Contract mode for approvals, the Virtual Signer will invoke the functions of
a smart contract to approve or reject requests.
ApprovalNodeRPCAddress in the Virtual Signer configuration refers to the RPC URL of an Ethereum or io.network node.
ApprovalSmartContractAddress in configuration refers to the address of the smart contract that implements the approval logic.

The smart contract must have three functions with the following signatures:

function approvedKeygenReshare(string memory encodedKeygenReshare) public returns (bool)
function approvedTx(string memory encodedTx) public returns (bool)
function approvedSignOp(string memory encodedSignOp) public returns (bool)

The functions are invoked by the Virtual Signer with ABI encoded tuples that represent a keygen or reshare request, a
transaction signing request or a sign operation request. The function returns true to approve the request,
and false to reject it.

Sample Smart Contract

Considering that the Virtual Signer encodes request data using ABI, it is important for the smart contract to have structs that match the data encoding performed by the Virtual Signer. These are the structs that the smart contract must have:

    struct KeygenReshareRequestOriginalSignersSC {
        uint Weight ;
        string Uiid ;
        string Id   ;
    }

    struct KeygenReshareRequestNewSignersSC {
        uint Weight ;
        string Uiid ;
        string Id   ;
    }

    struct KeygenReshareRequestSC {
        string Typename             ;
        string Id                   ;
        string ExpiresAt            ;
        string OrganisationId       ;
        string ReshareRequestStatus ;
        string VaultId              ;
        string VaultName            ;
        uint ReshareNonce         ;
        uint NewThreshold         ;
        KeygenReshareRequestOriginalSignersSC[] OriginalSigners;
        KeygenReshareRequestNewSignersSC[] NewSigners;
    }

    struct SigningTransactionRequestMetadataSC  {
        string MaxFees            ;
        uint MaxFeePerGas         ;
        uint MaxPriorityFeePerGas ;
        string Source          ;
        string ContractData     ;
    }

    struct SigningTransactionRequestSC {
        string Typename                 ;
        string Id                       ;
        string CreatedAt ;
        string UpdatedAt ;
        string ExpiresAt ;
        string CreatedByUserId ;
        string AmountOriginal           ;
        string AmountUsd                ;
        string ReceivingAddress ;
        string SendingAddressId ;
        string VaultId                  ;
        string VaultName                ;
        string AssetId                  ;
        string OrganisationId           ;
        string RawTransaction           ;
        string Status ;
        string AssetExecutionType       ;
        SigningTransactionRequestMetadataSC Metadata;
    }

    struct NodeOpSignRequestSC {
        string Typename            ;
        string Id                  ;
        string OrganisationId      ;
        string OperationSignStatus ;
        string VaultId             ;
        string VaultName           ;
        string Data                ;
        string ContentType         ;
        uint ChainId             ;
    }

The smart contract can decode the data as follows:

    function decodeSignOp(bytes memory data) public pure returns (string memory, uint) {
        NodeOpSignRequestSC memory sopReq = abi.decode(data, (NodeOpSignRequestSC));
        return (sopReq.Data, sopReq.ChainId);
    }

After ABI decoding, the smart contract can approve or reject the request by referring to the decoded data. For example:

    function approvedSignOp(bytes memory encodedSignOp) public returns (bool){
        (string memory data, uint chainId) = decodeSignOp(encodedSignOp);
        return keccak256(abi.encodePacked(data)) != keccak256(abi.encodePacked("do fail the request"));
    }

Sample Approval Smart Contract

We present below a sample approval smart contract. The functions that approve or reject requests are only illustrative and should be changed according to real business cases.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;
/**
 * @title Approval
 */
contract Approval {

    struct KeygenReshareRequestOriginalSignersSC {
        uint Weight ;
        string Uiid ;
        string Id   ;
    }

    struct KeygenReshareRequestNewSignersSC {
        uint Weight ;
        string Uiid ;
        string Id   ;
    }

    struct KeygenReshareRequestSC {
        string Typename             ;
        string Id                   ;
        string ExpiresAt            ;
        string OrganisationId       ;
        string ReshareRequestStatus ;
        string VaultId              ;
        string VaultName            ;
        uint ReshareNonce         ;
        uint NewThreshold         ;
        KeygenReshareRequestOriginalSignersSC[] OriginalSigners;
        KeygenReshareRequestNewSignersSC[] NewSigners;
    }

    struct SigningTransactionRequestMetadataSC  {
        string MaxFees            ;
        uint MaxFeePerGas         ;
        uint MaxPriorityFeePerGas ;
        string Source          ;
        string ContractData     ;
    }

    struct SigningTransactionRequestSC {
        string Typename                 ;
        string Id                       ;
        string CreatedAt ;
        string UpdatedAt ;
        string ExpiresAt ;
        string CreatedByUserId ;
        string AmountOriginal           ;
        string AmountUsd                ;
        string ReceivingAddress ;
        string SendingAddressId ;
        string VaultId                  ;
        string VaultName                ;
        string AssetId                  ;
        string OrganisationId           ;
        string RawTransaction           ;
        string Status ;
        string AssetExecutionType       ;
        SigningTransactionRequestMetadataSC Metadata;
    }

    struct NodeOpSignRequestSC {
        string Typename            ;
        string Id                  ;
        string OrganisationId      ;
        string OperationSignStatus ;
        string VaultId             ;
        string VaultName           ;
        string Data                ;
        string ContentType         ;
        uint ChainId             ;
    }

    event stringEvent(
        string s
    );

    event uintEvent(
        uint i
    );

    function decodeKeygenReshare(bytes memory data) public pure returns (string memory, uint) {
        KeygenReshareRequestSC memory kgReq = abi.decode(data, (KeygenReshareRequestSC));
        return (kgReq.Id, kgReq.NewThreshold);
    }

    function decodeTx(bytes memory data) public pure returns (string memory) {
        SigningTransactionRequestSC memory txReq = abi.decode(data, (SigningTransactionRequestSC));
        return (txReq.AmountOriginal);
    }

    function decodeSignOp(bytes memory data) public pure returns (string memory, uint) {
        NodeOpSignRequestSC memory sopReq = abi.decode(data, (NodeOpSignRequestSC));
        return (sopReq.Data, sopReq.ChainId);
    }

    function approvedKeygenReshare(bytes memory encodedKeygenReshare) public returns (bool){
        (string memory id, uint t) = decodeKeygenReshare(encodedKeygenReshare);
        emit stringEvent(id);
        emit uintEvent(t);
        return t < 10;
    }

    function approvedTx(bytes memory encodedTx) public returns (bool){
        (string memory amountOriginal) = decodeTx(encodedTx);
        emit stringEvent(amountOriginal);
        return keccak256(abi.encodePacked(amountOriginal)) != keccak256(abi.encodePacked("0.00666"));
    }

    function approvedSignOp(bytes memory encodedSignOp) public returns (bool){
        (string memory data, uint chainId) = decodeSignOp(encodedSignOp);
        emit stringEvent(data);
        emit uintEvent(chainId);
        return keccak256(abi.encodePacked(data)) != keccak256(abi.encodePacked("do fail the test"));
    }
}