New versions of the Qredo API and Signing Agent are now available! To get started, just contact us here.
Qredo Logo

QSign

Get started

Overview

Welcome to the ultimate guide on how to interact with the QSign Smart Contract.

Here you'll learn how to create and use an instructor smart contract that interacts with the QSign Smart Contract via contract-to-contract communication.

In this example, the purpose of the instructor contract is to instruct the Qsign Smart Contract on the source chain (Sepolia) to transfer ERC20 tokens on the destinaton chain (Mumbai) from an MPC-generated wallet:

Mumbai transfer

To take a closer look at the QSign Smart Contract and review transactions, see QSign Smart Contract on Etherscan.

MPC-generated wallets have the same functionality as regular externally owned accounts. Therefore, potential use cases include anything that can be enabled by cross-chain interoperability: token transfers, NFT transfers, remote staking possibilities, DAO participation, etc.

disclaimer

The focus of this QSign Technology Preview is to request feedback on the Smart Contract environment. In the architecture of QSign, there are also two off-chain components which are responsible for creating signatures and for relaying the messages respectively. These off-chain components are provided by Qredo right now and for the purpose of the Technology Preview.

Step 1: Deploy an instructor smart contract

In this step we're setting up an instructor smart contract, or QSignCrossChainExchange. It's a smart contract that interacts with the QSign Smart Contracts through provided interfaces.

1.1. Get Sepolia ETH

To fund the instructor contract, you need to get some ETH in the Ethereum Sepolia testnet:

  1. If you don't have a Metamask wallet, create one.

  2. Then make sure you have some Sepolia ETH ready in your account. We recommend using the following faucets:

  3. (Optional) If you want to see your balance in Metamask, select Sepolia test network from the drop-down menu on the top. If you don't see it on the list, go to Settings > Advanced and turn on the Show test networks switch.

1.2. Create a Solidity contract

You need to create and compile a Solidity smart contract using the template code we provide:

  1. Go to the Remix IDE online tool: remix.ethereum.org.
  2. Create a new file and paste the following code: Template smart contract.
  3. In the icon panel on the left, switch to Solidity Compiler.
  4. Click Compile to make sure there are no errors. You may need to change the compiler version to 8.13+commit.abaa5c0e.

1.3. Prepare to deploy

Next, prepare to deploy the contract on the Sepolia network:

  1. In the Remix icon panel on the left, switch to Deploy & Run.
  2. In the Environment drop-down menu, select Injected Provider - Metamask.
  3. In Metamask, select Sepolia test network from the drop-down menu on the top. If you don't see it on the list, go to Settings > Advanced and turn on the Show test networks switch.
  4. In the Contract drop-down menu, select QSignCrossChainExchange:QSignCrossChainExchange

In this step you imported your accounts hosted in Metamask. We need a real testnet version because we're going to talk with the already deployed QSign Smart Contract and our MPC and Cross-Chain Oracle instances.

1.4. Deploy the contract

Now you can deploy the contract:

  1. Click Deploy.
  2. Approve the transaction in Metamask.

Under Deployed Contracts, Remix will show a new instance: QSIGNCROSSCHAINEXCHANGE

Congratulations! You've successfully deployed the instructor contract to interact with the QSign Smart Contract on Sepolia.

If you click QSIGNCROSSCHAINEXCHANGE, you'll see the available functions of the instructor contract. We're going to invoke some of them in the next steps.

Step 2: Generate a Mumbai wallet

In this step we're going to create an MPC-generated wallet on the Polygon Mumbai testnet. Note that it requires paying a fee in Sepolia ETH.

Later the instructor smart contract deployed on the Ethereum Sepolia network will use this Mumbai wallet to interact on the Polygon Mumbai network.

2.1. Set the QSign fee

To initialize the contract, you need to set the QSign Smart Contract fee:

  1. Click QSIGNCROSSCHAINEXCHANGE in Remix to expand the list of functions.
  2. Click setDefaultFee or setCustomFee.

You can set either the default or a custom fee:

  • The setDefaultFee function allows you to query the default QSign fee (currently 100 wei) and set it in the instructor contract.
  • The setCustomFee function allows setting any value in wei. However, if the value you set is lower that the QSign fee, your request won't execute.

2.2. Fund the contract

Next, you need to make a little deposit to the instructor contract to pay the fee for the QSign Smart Contract:

  1. Go to Deploy & Run in Remix and enter 1000 wei in the Value input box:

    Fund the instructor contract
  2. Click QSIGNCROSSCHAINEXCHANGE below to expand the list of functions.

  3. Click the red button depositToContract.

  4. Approve the transaction in Metamask.

This will transfer 1000 wei from your personal Sepolia account to the smart contract to cover transaction fees. Note that you need to keep your smart contract sufficiently funded.

2.3. Request a wallet

Then you need to request an address (wallet) from Polygon Mumbai:

  1. Click QSIGNCROSSCHAINEXCHANGE in Remix to expand the list of functions.
  2. Click the red button requestNewEVMWallet.
  3. Approve the transaction in MetaMask.

The requestNewEVMWallet function internally calls the requestNewWallet function of the instructor contract, passing the EVM wallet type as a parameter.

Then requestNewWallet calls the requestPublicKey function of the QSign Smart Contract, which triggers an event. The MPCs are subscribed to this type of events and will generate and return a public key.

To learn more, see the QSign functions reference: requestPublicKey.

2.4. Get the wallet address

Next, we need to find out which address has been generated for us. The best way to do this is the following:

  1. Click QSIGNCROSSCHAINEXCHANGE in Remix to expand the list of functions.
  2. Click getEVMWallets.

The Remix will show the address of your Mumbai wallet next to the getEVMWallets button. This may take a few seconds. You can either copy and save the address now or call the getEVMWallets function again whenever you need it in the next steps.

The getEVMWallets function internally calls the getWallets function of the instructor contract.

Then getWallets calls the getWallets function of the QSign Smart Contract and returns all previously generated EVM addresses for the address invoking the function.

Step 3: Fund the Mumbai wallet

In this step we're going to fund the MPC-generated Mumbai wallet with ERC20 tokens. Note that before you need to get some MATIC in the Polygon Mumbai testnet to be able to submit valid transactions.

3.1. Get Mumbai MATIC

First you need to fund two addresses on the Polygon Mumbai testnet with some MATIC:

  1. Fund your personal wallet. We recommend the same address you used to get Sepolia ETH in step 1.1. You can get funds in the following faucets:

    Note that if you want to view your balance in Metamask, you may need to manually add the Mumbai testnet.

  2. Fund the MPC-generated wallet. Use the address retrieved in the step 2.4. You can do the following to get funds:

3.2. Connect to the Qredo ETH contract

We provide a mocked Qredo ETH Contract that maps 1:1 to Sepolia ETH.

To connect to the contract, do the following:

  1. Connect your personal wallet to Mumbai on chainlist.org.
  2. On Polygon Scan, go to Qredo ETH Contract > Contract > Write Contract.
  3. Click Connect to Web3 (over the first function).
  4. Connect your MetaMask to the contract.

3.3. Mint ERC20 tokens

Now we're going to mint some Qredo ETH to the MPC-generated Mumbai wallet:

  1. On Polygon Scan, expand the mint function and enter the parameters:

    • mint: This one can be left empty, just enter 0.

    • to (address): Enter the MPC-generated address you retreived in the step 2.4.

    • value (uint256): Enter the amount of Qredo ETH you want to send.

      Note: It's in wei, so it should be a large number. For example, 10 Qredo ETH equals 10000000000000000000 wei.

  2. Click Write.

  3. Execute the transaction in Metamask.

Step 4: Send funds from the Mumbai wallet

In this step we're going to initiate a transfer of ERC20 tokens (Qredo ETH) from the MPC-generated Mumbai wallet.

You'll send some Sepolia ETH to the instructor contract. The instructor contract will interact with QSign, and then QSign will transfer the equivalent amount of ERC20 tokens from the MPC-generated wallet to another address on Mumbai.

4.1. Get the instructor contract address

Before you initiate a transfer, you need to copy the instructor contract address:

  1. In Remix, go to Deploy & Run > Deployed Contracts
  2. Navigate to QSIGNCROSSCHAINEXCHANGE and click the copy button next to it.

4.2. Send Sepolia ETH

To initiate a transfer of ERC20 tokens, you need to send some Sepolia ETH to the instructor contract:

  1. Open Metamask, connect with Sepolia, and click Send.

  2. In the Send to input box, paste the instructor contract address.

  3. In the Amount input box, enter the amount of ETH to transfer.

    The amount of Sepolia ETH you can send is limited by the amount of Qredo ETH in the MPC-generated wallet. You specified it when you minted Qredo ETH in the step 3.3.

  4. Click the Next button below:

    Transfer tokens
  5. On the next screen, you need to set the gas limit.

    Click Market (on the top):

    Transfer tokens

    Then click Advanced:

    Edit gas fee 1

    Go to Gas Limit, click Edit, and enter 300000:

    Edit gas fee 2
  6. Click Save and then Confirm to submit the transaction.

4.3. QSign will send ERC20 tokens

After you submit the Sepolia ETH transaction, QSign will transfer an equivalent amount of ERC20 tokens from your MPC-generated Mumbai wallet. The transaction will be signed and broadcasted.

Note that in this example the destination address is your personal Mumbai address you used in steps 3.1. and 3.2. It's set as msg.sender in the exchange() function of the instructor contract.

The instructor contract calls the requestSignatureForTransaction function of the QSign Smart Contract, passing the transaction payload, the broadcast flag, and other data as parameters. In this example the broadcast flag is set to true.

To learn more, see the QSign functions reference: requestSignatureForTransaction.

Next, the QSign Smart Contract emits an event with the information provided. This event gets picked up by the Qredo MPC network, which creates the signature and returns the signed transaction to the smart contract. Then the ResolveSignature event gets emitted. It includes the singed transaction and the broadcast flag.

The Cross-Chain Oracle (CCO) is subscribed to ResolveSignature. If the event has the broadcast flag set to true, the CCO takes the raw transaction and the signature from the event, embeds the signature into the transaction, and broadcasts it to the destination blockchain. Here, it'll enter the mempool, get confirmed and included in the block.

4.4. Check the result

After some seconds, check the result on Polygon Scan. You can check one of these wallets:

  • The MPC-generated wallet
  • The destination wallet (in this example it's your personal wallet)

That's it! You'll see the transfer of Qredo ETH on Polygon Mumbai.

Template smart contract

// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2023 Qredo Ltd.

pragma solidity 0.8.13;


library Lib_RLPWriter {
    /**********************
     * Internal Functions *
     **********************/

    /**
     * RLP encodes a byte string.
     * @param _in The byte string to encode.
     * @return The RLP encoded string in bytes.
     */
    function writeBytes(bytes memory _in) internal pure returns (bytes memory) {
        bytes memory encoded;

        if (_in.length == 1 && uint8(_in[0]) < 128) {
            encoded = _in;
        } else {
            encoded = abi.encodePacked(_writeLength(_in.length, 128), _in);
        }

        return encoded;
    }

    /**
     * RLP encodes a list of RLP encoded byte byte strings.
     * @param _in The list of RLP encoded byte strings.
     * @return The RLP encoded list of items in bytes.
     */
    function writeList(bytes[] memory _in) internal pure returns (bytes memory) {
        bytes memory list = _flatten(_in);
        return abi.encodePacked(_writeLength(list.length, 192), list);
    }

    /**
     * RLP encodes a string.
     * @param _in The string to encode.
     * @return The RLP encoded string in bytes.
     */
    function writeString(string memory _in) internal pure returns (bytes memory) {
        return writeBytes(bytes(_in));
    }

    /**
     * RLP encodes an address.
     * @param _in The address to encode.
     * @return The RLP encoded address in bytes.
     */
    function writeAddress(address _in) internal pure returns (bytes memory) {
        return writeBytes(abi.encodePacked(_in));
    }

    /**
     * RLP encodes a uint.
     * @param _in The uint256 to encode.
     * @return The RLP encoded uint256 in bytes.
     */
    function writeUint(uint256 _in) internal pure returns (bytes memory) {
        return writeBytes(_toBinary(_in));
    }

    /**
     * RLP encodes a bool.
     * @param _in The bool to encode.
     * @return The RLP encoded bool in bytes.
     */
    function writeBool(bool _in) internal pure returns (bytes memory) {
        bytes memory encoded = new bytes(1);
        encoded[0] = (_in ? bytes1(0x01) : bytes1(0x80));
        return encoded;
    }

    /*********************
     * Private Functions *
     *********************/

    /**
     * Encode the first byte, followed by the `len` in binary form if `length` is more than 55.
     * @param _len The length of the string or the payload.
     * @param _offset 128 if item is string, 192 if item is list.
     * @return RLP encoded bytes.
     */
    function _writeLength(uint256 _len, uint256 _offset) private pure returns (bytes memory) {
        bytes memory encoded;

        if (_len < 56) {
            encoded = new bytes(1);
            encoded[0] = bytes1(uint8(_len) + uint8(_offset));
        } else {
            uint256 lenLen;
            uint256 i = 1;
            while (_len / i != 0) {
                lenLen++;
                i *= 256;
            }

            encoded = new bytes(lenLen + 1);
            encoded[0] = bytes1(uint8(lenLen) + uint8(_offset) + 55);
            for (i = 1; i <= lenLen; i++) {
                encoded[i] = bytes1(uint8((_len / (256**(lenLen - i))) % 256));
            }
        }

        return encoded;
    }

    /**
     * Encode integer in big endian binary form with no leading zeroes.
     * @notice TODO: This should be optimized with assembly to save gas costs.
     * @param _x The integer to encode.
     * @return RLP encoded bytes.
     */
    function _toBinary(uint256 _x) internal pure returns (bytes memory) {
        bytes memory b = abi.encodePacked(_x);

        uint256 i = 0;
        for (; i < 32; i++) {
            if (b[i] != 0) {
                break;
            }
        }

        bytes memory res = new bytes(32 - i);
        for (uint256 j = 0; j < res.length; j++) {
            res[j] = b[i++];
        }

        return res;
    }

    /**
     * Copies a piece of memory to another location.
     * @notice From: https://github.com/Arachnid/solidity-stringutils/blob/master/src/strings.sol.
     * @param _dest Destination location.
     * @param _src Source location.
     * @param _len Length of memory to copy.
     */
    function _memcpy(
        uint256 _dest,
        uint256 _src,
        uint256 _len
    ) private pure {
        uint256 dest = _dest;
        uint256 src = _src;
        uint256 len = _len;

        for (; len >= 32; len -= 32) {
            assembly {
                mstore(dest, mload(src))
            }
            dest += 32;
            src += 32;
        }

        uint256 mask;
        unchecked {
            mask = 256**(32 - len) - 1;
        }
        assembly {
            let srcpart := and(mload(src), not(mask))
            let destpart := and(mload(dest), mask)
            mstore(dest, or(destpart, srcpart))
        }
    }

    /**
     * Flattens a list of byte strings into one byte string.
     * @notice From: https://github.com/sammayo/solidity-rlp-encoder/blob/master/RLPEncode.sol.
     * @param _list List of byte strings to flatten.
     * @return The flattened byte string.
     */
    function _flatten(bytes[] memory _list) private pure returns (bytes memory) {
        if (_list.length == 0) {
            return new bytes(0);
        }

        uint256 len;
        uint256 i = 0;
        for (; i < _list.length; i++) {
            len += _list[i].length;
        }

        bytes memory flattened = new bytes(len);
        uint256 flattenedPtr;
        assembly {
            flattenedPtr := add(flattened, 0x20)
        }

        for (i = 0; i < _list.length; i++) {
            bytes memory item = _list[i];

            uint256 listPtr;
            assembly {
                listPtr := add(item, 0x20)
            }

            _memcpy(flattenedPtr, listPtr, item.length);
            flattenedPtr += _list[i].length;
        }

        return flattened;
    }
}

interface IQSign {
    function requestPublicKey(bytes32 walletTypeId) external payable;
    function requestSignatureForHash(bytes32 walletTypeId,uint256 publicKeyIndex, bytes32 dstChainId,bytes32 payloadHash) external payable;
    function requestSignatureForData(bytes32 walletTypeId, uint256 publicKeyIndex, bytes32 dstChainId, bytes memory payload)external payable;
    function requestSignatureForTransaction(bytes32 walletTypeId, uint256 publicKeyIndex, bytes32 dstChainId, bytes memory payload, bool broadcast)external payable;    
    function getWallets(bytes32 walletTypeId, address owner) external view returns(string[] memory);
    function getWalletByIndex(bytes32 walletTypeId, address owner, uint256 index) external view returns(string memory);
    function getFee() external view returns (uint256);
}

/**
 * @dev Interface of the ERC20 standard as defined in the EIP.
 */
interface IERC20 {
    /**
     * @dev Emitted when `value` tokens are moved from one account (`from`) to
     * another (`to`).
     *
     * Note that `value` may be zero.
     */
    event Transfer(address indexed from, address indexed to, uint256 value);

    /**
     * @dev Emitted when the allowance of a `spender` for an `owner` is set by
     * a call to {approve}. `value` is the new allowance.
     */
    event Approval(address indexed owner, address indexed spender, uint256 value);

    /**
     * @dev Returns the amount of tokens in existence.
     */
    function totalSupply() external view returns (uint256);

    /**
     * @dev Returns the amount of tokens owned by `account`.
     */
    function balanceOf(address account) external view returns (uint256);

    /**
     * @dev Moves `amount` tokens from the caller's account to `to`.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transfer(address to, uint256 amount) external returns (bool);

    /**
     * @dev Returns the remaining number of tokens that `spender` will be
     * allowed to spend on behalf of `owner` through {transferFrom}. This is
     * zero by default.
     *
     * This value changes when {approve} or {transferFrom} are called.
     */
    function allowance(address owner, address spender) external view returns (uint256);

    /**
     * @dev Sets `amount` as the allowance of `spender` over the caller's tokens.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * IMPORTANT: Beware that changing an allowance with this method brings the risk
     * that someone may use both the old and the new allowance by unfortunate
     * transaction ordering. One possible solution to mitigate this race
     * condition is to first reduce the spender's allowance to 0 and set the
     * desired value afterwards:
     * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
     *
     * Emits an {Approval} event.
     */
    function approve(address spender, uint256 amount) external returns (bool);

    /**
     * @dev Moves `amount` tokens from `from` to `to` using the
     * allowance mechanism. `amount` is then deducted from the caller's
     * allowance.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transferFrom(address from, address to, uint256 amount) external returns (bool);

    function mint(address account, uint256 amount) external;
}

/// @author vlp
//DO NOT USE THIS CODE IN PRODUCTION ONLY FOR DEMO PURPOSES!!!!!
abstract contract QSignConnection {
    using Lib_RLPWriter for address;
    using Lib_RLPWriter for uint256;
    using Lib_RLPWriter for bytes;
    using Lib_RLPWriter for bytes[];

    address internal qSign  = payable(address(0xF6B22AcbA6D4b2887B36387ebDD81D17887aD652));
    uint256 internal _qSignFee = 0;

    event Deposit(address indexed sender, uint amount, uint balance);

    function depositToContract() public virtual payable {
        emit Deposit(msg.sender, msg.value, address(this).balance);
    }

    function requestNewWallet(bytes32 walletTypeId) public virtual payable  {
        IQSign(qSign).requestPublicKey{value: _qSignFee}(walletTypeId);
    }

    function getWallets(bytes32 walletTypeId) public virtual view returns(string[] memory) {
        return IQSign(qSign).getWallets(walletTypeId, address(this));
    }
    
    function getWalletByIndex(bytes32 walletTypeId, uint256 index) public virtual view returns(string memory){
        return IQSign(qSign).getWalletByIndex(walletTypeId, address(this), index);
    }

    function setCustomFee(uint256 fee) public virtual {
        _qSignFee = fee;
    }

    function setDefaultFee() public virtual {
        uint256 newFee = IQSign(qSign).getFee();
        setCustomFee(newFee);
    }
    
    function requestSignatureForHash(
        bytes32 walletTypeId, 
        uint256 publicKeyIndex, 
        bytes32 dstChainId, 
        bytes32 payloadHash
    ) public virtual payable {
        IQSign(qSign).requestSignatureForHash{value: _qSignFee}(walletTypeId, publicKeyIndex, dstChainId, payloadHash);
    }

    function requestSignatureForData(
        bytes32 walletTypeId, 
        uint256 publicKeyIndex, 
        bytes32 dstChainId, 
        bytes memory payload
    ) public virtual payable {
        IQSign(qSign).requestSignatureForData{value: _qSignFee}(walletTypeId, publicKeyIndex, dstChainId, payload);
    }

    function requestSignatureForTransaction(
        bytes32 walletTypeId, 
        uint256 publicKeyIndex, 
        bytes32 dstChainId, 
        bytes memory rlpPayload, 
        bool broadcast
    ) public virtual payable {
        IQSign(qSign).requestSignatureForTransaction{value: _qSignFee}(walletTypeId, publicKeyIndex, dstChainId, rlpPayload, broadcast);
    }

    function getQSignFee() public virtual view returns(uint256) {
        return _qSignFee;
    }

    function rlpEncodeData(bytes memory data) public virtual pure returns(bytes memory){
        return data.writeBytes();
    }

    function rlpEncodeTransaction(
        uint256 nonce,
        uint256 gasPrice,
        uint256 gasLimit,
        address to,
        uint256 value,
        bytes memory data
    ) public virtual pure returns (bytes memory) {
        bytes memory nb = nonce.writeUint();
        bytes memory gp = gasPrice.writeUint();
        bytes memory gl = gasLimit.writeUint();
        bytes memory t = to.writeAddress();
        bytes memory v = value.writeUint();
        return _encodeTransaction(
            nb, 
            gp,
            gl,
            t,
            v,
            data);
    }

    function _encodeTransaction(
        bytes memory nonce,
        bytes memory gasPrice,
        bytes memory gasLimit,
        bytes memory to,
        bytes memory value,
        bytes memory data
    ) internal virtual pure returns (bytes memory) {
        bytes memory zb = uint256(0).writeUint();
        bytes[] memory payload = new bytes[](9);
        payload[0] = nonce;
        payload[1] = gasPrice;
        payload[2] = gasLimit;
        payload[3] = to;
        payload[4] = value;
        payload[5] = data;
        payload[6] = zb;
        payload[7] = zb;
        payload[8] = zb;
        return payload.writeList();
    }
}
//DO NOT USE THIS CODE IN PRODUCTION ONLY FOR POC!!!!!

contract QSignCrossChainExchange is QSignConnection {
    bytes32 public constant EVMWalletType = 0xe146c2986893c43af5ff396310220be92058fb9f4ce76b929b80ef0d5307100a;
    bytes32 internal constant mumbaiChainId = 0xa24f2e4ffab961d4f74844398efaab23f70f2830a83e1ea4f58097ea0408d254;
    address public constant qAssetMumbai = address(0xfe17c51b86fc407B48548775B30d4a528AEA6D42);
    uint256 public _gasPrice = 30000000000;
    uint256 internal _nonce = 0;

    function configGasPrice(uint256 gasPrice_) public virtual {
        _gasPrice = gasPrice_;
    }
    
    function requestNewEVMWallet() public virtual payable  {
        requestNewWallet(EVMWalletType);
    }

    function getEVMWallets() public virtual view returns(string[] memory) {
        return getWallets(EVMWalletType);
    }

    receive() external payable {
        exchange();
    }

    function exchange() public payable {
        bytes memory call = abi.encodeCall(IERC20.transfer, (msg.sender, msg.value));
        bytes memory data = rlpEncodeData(call);
        bytes memory rlpTransactionData = rlpEncodeTransaction(_nonce, _gasPrice, 250000, qAssetMumbai, 0, data);
        requestSignatureForTransaction(EVMWalletType, 0, mumbaiChainId, rlpTransactionData, true);
        _nonce++;
    }
}
Previous
Introduction