Skip to main content

Telos Zero Multisig Proposal Example

The Telos EVM runs in one smart contract on the Telos Zero blockchain, the eosio.evm contract. Calling a function of a Telos EVM smart contract from Telos Zero requires the use of the eosio.evm contract's raw(eosio::name &ram_payer,std::vector<int8_t> &tx, bool &estimate_gas,std::optional<eosio::checksum160> &sender) action.

This action takes in the Telos Zero account that will pay the RAM, the serialized EVM Transaction data and the sender address which the transaction will be sent from on EVM.

This guide will go over preparing and send a Telos Zero multisig that can call a function of an EVM contract. An example implementation is available in our zero-to-evm-multisig-example repository and will serve for our examples here.

/!\ Make sure that the sender address has sufficient TLOS to pay for the gas of that function call

Requirements

This guide makes use of nodeJS, ethers, telosevm-js as well as cleos.

1. Prepare the data using a script

The script will use a library like ethers or web3js to populate & serialize the EVM transaction into a hash which we need in order call to eosio.evm raw(...) method.

We can refer to our zero-to-evm-escrow-example repository for an example, with the generateEVMTransaction.js script

We get the linked EVM address & its nonce

try {
    var evmAccount = await evmApi.telos.getEthAccountByTelosAccount(TelosZeroAccount);
    var evmAddress = evmAccount.address;
    var nonce = parseInt(await evmApi.telos.getNonce(evmAddress), 16);
} catch(e) {
    console.log(e.message);
    return;
}

We get gas data

const feeData = await provider.getFeeData()
const gasPrice = BigNumber.from(`0x${await evmApi.telos.getGasPrice()}`)
const gasLimit = 26166;

We populate the transaction with the relevant method call, in our case setLockDuration

// Use ether to populate the transaction with a call to the relevant method
try {
    var unsignedTrx =  await contract.populateTransaction.setLockDuration(process.env.DURATION);
} catch(e) {
    console.log(e.message);
    return;
}

Then we serialize the data using ethers utils

unsignedTrx.nonce = nonce;
unsignedTrx.gasLimit = gasLimit;
unsignedTrx.gasPrice = gasPrice;

try {
    var encodedTrx = await ethers.utils.serializeTransaction(unsignedTrx);
} catch(e) {
    console.log(e.message);
    return;
}
encodedTrx = encodedTrx.replace(/^0x/, '');

You can print the encodedTrx variable and use cleos to call eosio.evm raw() method with it in order to check if your settings are correct.

For our purpose we need that encoded transaction string into a Telos Zero transaction then exported to a JSON file, which takes some tinkering. We will continue following our example repository to do just that next.

// We first save the Telos Zero transaction to file using cleos
exec('cleos --url '+ process.env.NETWORK_ENDPOINT +' push action eosio.evm raw \'{"ram_payer": '+TelosZeroAccount+', "tx": "'+ raw +'" , "estimate_gas": false, "sender": "'+ evmAddress.replace(/^0x/, '') +'"}\' --expiration 86400 -sjd --json-file output/transaction.json', (err, stdout, stderr) => {
    if (err) {
        console.error(err)
    } else {
        // We wait to let the command we launched finish
        await new Promise((resolve) => {
            setTimeout(resolve, 5000);
        });
        // We open and edit the transaction file
        fs.readFile('output/transaction.json', 'utf8', (err, data) => {
            if (err) {
                console.error(err);
            } else {
                // We parse the data to JSON and correct some rows
                var jsonData = JSON.parse(data);
                jsonData.actions[0].data = jsonData.actions[0].hex_data;
                delete jsonData.actions[0].hex_data;
                jsonData.transaction_extensions = [];
                
                // We write the file to disk
                fs.writeFile('output/transaction.json', JSON.stringify(jsonData), (err) => {
                    if (err) {
                        console.error(err);
                    } else {
                        console.log('Transaction JSON generated in output/transaction.json')
                    }

                });
            }
        });
    }
});

2. Prepare the permissions

To send a multisig, we need to setup the list of the needed signers' permissions. In our case, we will set our active BPs as the signer which requires a script, but you could just as well skip that step and use a static array of permission on step 3.

We will refer to our zero-to-evm-escrow-example example repository here, with its generatePermissions.js script that fetches active BPs and writes their permission to a file on disk for later use.

import axios from 'axios';
import fs from 'fs';

const opts = [
  'telos.caleos.io', 
  '/v1/chain/get_table_rows?code=eosio&scope=eosio&table=producers', 
  {
    'accept': 'application/json',
    'content-type':'application/json'
  }
]

axios({
  method: 'post',
  url: 'https://telos.caleos.io/v1/chain/get_producers',
  data: {
    'limit':'20000',
    'json':true
  }
}).then((res) => {
  let bps = [];
  for (let i = 0; i < res.data.rows.length; i++) {
    let row = res.data.rows[i];
    if (row.is_active !== 1)
      continue

    bps.push({actor: row.owner, permission: 'active'})
  }
  fs.writeFile('output/permissions.json', JSON.stringify(bps, null, 4), {flag: 'a'}, err => {
    if (err) {
      console.error(err);
    } else {
      console.log('Permissions written to output/permissions.json')
    }
  });
})

3. Send your proposal using cleos

Using the files generated in the last 2 steps you can now run the following cleos command, replacing YOUR_Zero_ACCOUNT with your Telos Zero account name and PROPOSAL by your proposal's name.

cleos --url https://testnet.telos.net multisig propose_trx PROPOSAL ./output/permissions.json ./output/transaction.json YOUR_Zero_ACCOUNT

You could also use the EOSJS library to create the Multisig proposal directly from your script.