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 rng.bridge
// 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.