Skip to main content

Call a EVM function from Telos Native

The Telos EVM runs in one smart contract on the Telos Native blockchain, the eosio.evm contract. Calling a function of a Telos EVM smart contract from Telos Native 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 native 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 sending a Telos Native transaction that can call a function of an EVM contract. Example implementations are available in our native-to-evm-transaction and rng-oracle-bridge repositories.

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

Requirements

This requires a Telos Native account with a linked EVM address (hereby refered to as the sender)

Get required static variables

You first need to get the address of the EVM contract and the function signature of the EVM function you need to call, as well as its gas limit.

1) Get the EVM contract address

Save the address after deployment of the contract on EVM or copy it from a block explorer

2) Get the function signature

Function calls in the Ethereum Virtual Machine are specified by the first four bytes of data sent with a transaction. These 4-byte signatures are defined as the first four bytes of the Keccak hash (SHA3) of the canonical representation of the function signature.

The following is an example using ethersJS, for a reply(uint, uint) EVM function call:

cont fnSig = await contract.interface.getSighash("reply(uint, uint)")

3) Get the gas limit

The gas limit can be derived by doing tests calling the EVM function. Adding a margin to it is always recommended. You could also estimate that gas limit at runtime.

Get required dynamic variables

1) Get the sender's nonce

The nonce of an address being incremented at each transaction, you need to retreive it right before your call to eosio.evm raw() method

A - Using a script

The following is an example using @telosnetwork/telosevm-js:

const nonce = parseInt(await evmApi.telos.getNonce(linkedAddress), 16)

B - Using a smart contract

You can get the nonce of a linked EVM address from the eosio.evm accounts table, like so:

// find account
account_table _accounts("eosio.evm", "eosio.evm"_n);
auto accounts_byaccount = _accounts.get_index<"byaccount"_n>();
auto account = accounts_byaccount.require_find("MY NATIVE ACCOUNT", "Account not found");

// Get the nonce
const nonce = account->nonce;

2) Get the gas price

A - Using a script

The following is an example using @telosnetwork/telosevm-js:

const gasPrice = BigNumber.from('0x${await evmApi.telos.getGasPrice()}')

A - Using a smart contract

You can get the EVM gas price from the eosio.evm config singleton

3) Get the encoded transaction data

A - Using a script

Using the previously obtained EVM contract address and function signature as well as the sender's nonce, the gas price and the gas limit values, get the encoded transaction data using a script. Libraries such as web3js and ethers have utilities that help a lot here.

Refer to our native-to-evm-transaction repository's generateEVMTransaction script for an example.

B - Using a smart contract

Using the previously obtained EVM contract address, function signature and gas limit saved in your native contract, for example in a singleton (recommended) or by hard coding them as constants, as well as the dynamic nonce and gas price variable retreived in your contract at runtime you can get the encoded transaction data using the RLP library included in eosio.evm

// CONTRACT ADDRESS
std::vector<uint8_t> to;
to.insert(to.end(), evm_contract.begin(), evm_contract.end()); // Our evm contract address

// FUNCTION PARAMETERS (function signature + argument)
std::vector<uint8_t> data;
data.insert(data.end(), fnsig.begin(), fnsig.end()); // Our function signature
data.insert(data.end(), argument.begin(), argument.end()); // Our argument for that function

const tx = rlp::encode(NONCE, GAS_PRICE, GAS_LIMIT, to, uint256_t(0), data, CHAIN_ID, 0, 0);

NONCE is the nonce of the sender EVM address we retreived

GAS_PRICE and GAS_LIMIT are the corresponding variables we retreived

to is our EVM contract address formatted to a vector

uint256_t(0) is the value of the EVM transaction, here set at 0 (no value sent)

data is our EVM transaction data we retreived and formatted to a vector

CHAIN_ID is the ID of our chain (41 for Telos EVM Testnet, 40 for Telos EVM Mainnet)

Refer to our rng-oracle-bridge repository for an example.

Call the eosio.evm raw() method

Use that encoded transaction data, as well as the ram payer native account and EVM sender address to call the raw() action of the eosio.evm contract

A - Using cleos

cleos --url https://testnet.telos.net/ push action eosio.evm raw '{"ram_payer": 'NATIVE_ACCOUNT', "tx": "ENCODED_TX_DATA" , "estimate_gas": false, "sender": "EVM_SENDER_ADDRESS"' -p NATIVE_ACCOUNT

Note that both the tx and sender arguments take hashes without '0x'

Refer to our native-to-evm-transaction repository's generateEVMTransaction script for an example.

B - Using a smart contract

// Send it using eosio.evm
action(
permission_level {get_self(), "active"_n},
EVM_SYSTEM_CONTRACT,
"raw"_n,
std::make_tuple(NATIVE_RAM_PAYER, RLP_ENCODED_TX_DATA, false, std::optional<eosio::checksum160> (SENDER_EVM_ADDRESS))
).send();

Refer to our rng-oracle-bridge repository for an example.