Skip to main content

Getting Started

The example below explains how a smart contract in an EVM chain with EIP-1186 can communicate with Tezos contracts.

Contract Implementation and Deployment

EVM

First, we implement and deploy the EVM contract, which will be a simple counter.

The contract will have two methods, one for incrementing and another for decrementing the counter. In this example, the contract will be callable by anyone, but you could add an ACL to restrict the accounts allowed to call it.

Deploy with Remix (Use Goerli)

client.sol
pragma solidity ^0.8.17;

library Action {
string constant INCREMENT = "INCREMENT";
string constant DECREMENT = "DECREMENT";
}

contract IBCF_Client {
// The action counter is used in the Tezos blockchain for ordering
// the actions sequentially and as replay attack prevention.
uint action_counter;
// We will generate proofs of the entries contained in this mapping.
// - These proofs are then used to validate that a given action occurred in this contract.
mapping(uint => string) action_registry;

/**
* Modifier to increase the action counter.
* Every function using this modifier will increase the action counter when called.
*/
modifier increase_counter() {
action_counter += 1;
_;
}

/**
* @dev Create a increment action.
*/
function increment() public increase_counter {
// Update registry (The registry is used for proof generation)
action_registry[action_counter] = Action.INCREMENT;
}

/**
* @dev Create a decrement action.
*/
function decrement() public increase_counter {
// Update registry (The registry is used for proof generation)
action_registry[action_counter] = Action.DECREMENT;
}
}

Tezos

The contract on Tezos will be capable of receiving and validating storage proofs from the EVM contract deployed above. For this to be possible, the EVM contract deployed above needs to be stored and used in the proof validation.

Deploy with SmartPy IDE (Use Ghostnet)

client.py
import smartpy as sp

Utils = sp.io.import_script_from_url(
"https://raw.githubusercontent.com/Acurast/acurast-hyperdrive/main/contracts/tezos/libs/utils.py"
)

class Constant:
# Memory index of `mapping(uint => string) action_registry`
EVM_ACTION_REGISTRY_INDEX = sp.bytes(
"0x0000000000000000000000000000000000000000000000000000000000000001"
)


class Error:
INVALID_VIEW = "INVALID_VIEW"


class Type:
Validate_storage_proof_argument = sp.TRecord(
account=sp.TBytes,
block_number=sp.TNat,
account_proof_rlp=sp.TBytes,
storage_slot=sp.TBytes,
storage_proof_rlp=sp.TBytes,
).right_comb()
ActionArgument = sp.TRecord(
block_number=sp.TNat,
account_proof_rlp=sp.TBytes,
action_proof_rlp=sp.TBytes,
).right_comb()


class IBCF_Client(sp.Contract):
def __init__(self):
self.init_type(
sp.TRecord(
ibcf=sp.TRecord(
# Replay attack prevention
action_counter=sp.TNat,
# The proof validation contract
proof_validator=sp.TAddress,
# The EVM address of the contract we are receiving proofs from
evm_address=sp.TBytes,
),
# This variable will store all actions sent from the EVM contract
performed_actions=sp.TList(sp.TString),
)
)

@sp.entry_point(parameter_type=Type.ActionArgument)
def perform(self, param):
write_uint_slot_lambda = sp.compute(sp.build_lambda(Utils.EvmStorage.write_uint_slot))
decode_string_lambda = sp.compute(sp.build_lambda(Utils.EvmStorage.read_string_slot))

# New action, increase action counter
self.data.ibcf.action_counter += 1

# Compute the storage slot for the proof
action_counter_evm_storage_slot = write_uint_slot_lambda(
self.data.ibcf.action_counter
)
action_slot = sp.compute(
sp.keccak(
action_counter_evm_storage_slot + Constant.EVM_ACTION_REGISTRY_INDEX
)
)

# Validate proof and extract storage value (The action to be performed)
rlp_action = sp.view(
"validate_storage_proof",
self.data.ibcf.proof_validator,
sp.set_type_expr(
sp.record(
block_number=param.block_number,
account=self.data.ibcf.evm_address,
account_proof_rlp=param.account_proof_rlp,
storage_slot=action_slot,
storage_proof_rlp=param.action_proof_rlp,
),
Type.Validate_storage_proof_argument,
),
t=sp.TBytes,
).open_some(Error.INVALID_VIEW)

# Decode action
action = decode_string_lambda(rlp_action)

self.data.performed_actions.push(action)

For deploying the contract above, you should use the following initial storage.

sp.record(
ibcf=sp.record(
# First action will be 1
action_counter = 0,
# The ghostnet validator
proof_validator = sp.address("KT1QfWxeYitE4NXvC4caBJpwimzxZygqPJ9F"),
# The address of the EVM contract deployed in the previous step
evm_address = sp.bytes("<evm_counter_address>"),
),
# An empty list of received actions
performed_actions=[],
)

Communication

Now that both contracts got deployed, you can start transmitting actions from the EVM chain to Tezos.

  • Install the SDK's:

    npm install --save ibcf-sdk @taquito/taquito
  • First, you need to create an action on the EVM contract by calling the increment() function;

  • Then wait for a snapshot with a block_number greater or equal to the block_number of the increment() transaction. You can do that by querying the latest snapshot as shown in the example below.

    import { Tezos } from "ibcf-sdk";
    import { TezosToolkit } from "@taquito/taquito";

    const TEZOS_RPC = "https://tezos-ghostnet-node-1.diamond.papers.tech";
    const TEZOS_VALIDATOR = "KT1QfWxeYitE4NXvC4caBJpwimzxZygqPJ9F";

    const tezosSdk = new TezosToolkit(TEZOS_RPC);
    const validator = new Tezos.Contracts.Validator.Contract(
    tezosSdk,
    TEZOS_VALIDATOR
    );

    validator.latestSnapshot().then(console.log);
  • Once a snapshot has been produced, you can generate a proof of the storage:

    import { Ethereum } from "ibcf-sdk";
    import { ethers } from "ethers";

    const ETHEREUM_RPC = "https://goerli.infura.io/v3/75829a5785c844bc9c9e6e891130ee6f";
    const ETHEREUM_DAPP = "0x715E4360a220a5e021dE5413F5c6314EDC234AC3";
    const BLOCK_NUMBER = 8339150;

    const provider = new ethers.providers.JsonRpcProvider(ETHEREUM_RPC);

    const proofGenerator = new Ethereum.ProofGenerator(provider);

    // EVM storage slot indexes (Each slot index is identified by 32 bytes)
    // - storage index 0 ...
    // - storage index 1 -> mapping(uint => string) action_registry;
    const actionRegistryIndex = "1".padStart(64, "0");

    // Key encoded as an EVM storage slot (32 bytes)
    const action_counter = 1; // First action
    const key = String(action_counter).padStart(64, "0");

    // Storage slots
    // - Each mapping slot is the result of keccak256(<key> + <slot_index>)
    const actionSlot = ethers.utils.keccak256("0x" + key + actionRegistryIndex);
    proofGenerator
    .generateStorageProof(ETHEREUM_DAPP, [actionSlot], BLOCK_NUMBER)
    .then(console.log);
  • To finalize, you just need to submit the proof by calling the Tezos contract.

    import { TezosToolkit } from "@taquito/taquito";

    const TEZOS_RPC = "https://tezos-ghostnet-node-1.diamond.papers.tech";
    const TEZOS_DAPP = "KT1QhWvNebpWBAesb8LHVYJN2EG5WygnUUrb";

    const tezosSdk = new TezosToolkit(TEZOS_RPC);
    // Setup signer
    tezosSdk.setProvider({ signer: ... });

    const contract = await tezosSdk.contract.at(TEZOS_DAPP);

    const BLOCK_NUMBER = 8339150;
    const ACCOUNT_PROOF_RLP = "0xf90c21f90211a0d525049652a349fedab7db787a928687a291e14dee66a6fb454c04495af25c20a0fd9cb3c7af7bcafd7b84c306071d2ec3fd71378dc69f1b8eebfa923d0bba4344a022bcccf8a3537b1f9680da0ac4a44590437b928dde6beabb6448210fe503e84da0fd87faae3994f8c102211286d6215debf89a9c7a2e086f3d5a36c24f53dc0c27a0f86402a7c7155e278da369dce68566fd9c934b4078caaac2262e82e5838316c1a04e3a3979968ab779f5002862968e45fe31e2e9a9717b1f3c89d622df7bf6d5b1a03f25a707e0bf9d397a45cecb93e124d3f903c059e3bec5b97d13d95ec83dfb0aa0d32041a11b04cdebc32c48d20e43648af702767adab6a89232b44e2466a58e04a0eeb367e5a66098c7c2987f29bd6a31531be054a6f98fdb04377dc1141205c90aa0aa3dff79188f32d254d7198494e6bbcced145f3ca356b022efbb051fcf50b50fa0d7245650c44cf19ef894aebbf287cdcba82db7fa26425d14e87aea40a46d2f79a0fc6ef5ff8eddef065d0282bb6d285b24abcfe91585296304d3b4ccc461a097eca0c83bf0be6f1b91e151e8c63811124d3600b335da3f54202439a58bb7c670f4d4a0bd336791e8f6158d3983954f6b5c43a5228ffad16ca8d8541135cbfd13e47c6ea041fa6c634bbb212dbda7450943c8ea608d1e549cd6527b5f6c4a905bd15367aca073180995094c874b8f3dcacd24d187819d999e02599dfad5e9219b351a86a58380f90211a0092209d716c5a7210d4a80e512e7a61d938694a0d4e083ce7b311a8a45a058cba0eded0e5bc8c00f6ab2fa6f062e81a717f83bd264afa78b504056cf4be417928fa05cbbbbd377322c5b4f3bbe24d8c059e3af084381894c95665a50f7eaee7e7457a0ebec32321f0210839cc12b37fe97adca5065632daa711cada3237746a475d141a06afba5172ab317cba61329c62ff596976136b7afeecc7fcce217ebc86f44ed99a0fc1d58261037076ab66abd32f98811652140f9d64eb4abb48c313d957f838408a0c62995bc2ee2162ffaa88cc22fb2f4f6be571786dfbfe982ccd5607eca8cbdaaa0d5d7b13cb8b1d5d72d2fc4fb3f96a2e78f7d3e3012d14571ee2e7239e3c0eb7fa0df8dcb45f1fcd134ed8d12913a05e0a80bc1bb4488af0ab0fe2ce853222f85b7a0a445d6ee2667d1dbf62dac276916a9c5730c5388528104b98ec8f293a2d92c07a03c95e27d0beeff176e2d61a5a0552cd0a012e94a27e619bd37d2bc1b7d3c3d42a094c0982abd2f236a42d8dc79accc730c32871c30749802ad931c0739e4136627a0670abd4f56cf7b6781ba1fc5de28df2f51b9347406fbeb8e7ba04f550f32c911a09d17a0684446d8f02efc538c5dc74c210a6485c5bb55293d33970c23e2c4d705a08bd456cd22a25102e50273f2990a9b3575c696cf65e12e1039cab338e4e6f841a082890b442114c1879950cfd80f63589e4b26e6005b1d2b651dcb5d50bffeac7a80f90211a01018dd339544ea490fa1b46c6f973f84cd92396daaba13be55ea208a208ef399a00405adfb7b2923e3427965ee68d21a65ed14da384c45ae3deab6ea5aa6775eb0a086f0e28728ba6a53e37dce4e9471a2ed4d47843beca7914be0ce5863f341d788a0c398b8923c081632fc0e7e0f858d1a8019245f2fcaaa390d202256ad18bad527a0982e9921685613795e6350b1988610b21fe84490a23345ced1b19629660c3799a01d1f450d8575e988369fe6bb7571fff43978be36800b66c1b2c4dd697ff99c76a0a08c9ba6951075013ac111d994514116cdc4f8783d6c0d076856b09519c9da12a06ba39ed29f26996dd016723dd3da8e7de88df9e662a9d60503876e1dcfa299b3a05c40dfc446155314b3c69502ec58147a2bcb7c046a16361dd7084f93209edad6a08356366ca19e7a012cd8c52e2869e09ebb43a9352c5414bbfa476d11a6132caea0c2d9490e127832e5ebe149cbde3caefef361177f2f3a572215c5588180822d54a028e026757fd8ad824e8a4dff882f79f3ac9194ba3a7ef2bd5bef54805db913eaa00410b583993523d8866d09269103cf050410708a626b51aebc0f5344e7524e27a0d7f6a085250b242b18c0b06fc1f72a8afc4f6579ebbeb721287225d6edad0e84a070ecf5a3be788104818a74cbd7eb05b33c4c14998fd0d922c5ae979929109069a06734c90473adfff71e54ed52f3ddfc349fb3d199bd8ea9a26d3e9d574010de4780f90211a0070d54dc357c5e79a5af3d1078f9ee48911f248c9061c3e5affedbc285fd914ca0edd50e358240e2ee6d0dc2447473873bea73fbbb6347887f0ec059c9974d6c04a0bae43c662033a7d8f2fea67b8f811327627f04d8b575579982e63b638f5494e4a0fa2157165bbe4e2243df28c282305700cd235de4df642ed46ea46463d1d45c4ca04b4f4de99eee04ac0109fd077c33ec75e3c3465b015cbe388782bf24ad739ed9a016cd92234e9c6e74dce5912cd877eb64b40baea3638aa44edfcaee8e9ad4e3c9a0b69ec8b03c8d81bdf3b48aeabb58558574ba23a93a70f363961b2169e6b2eb6ea0b3944329a62940319bd077447804f930053e934af2327bc2959685ce4e6646c5a0180fab853e8657b3a55b12960ba8783c37534f0202e1bf1e8bd350dc30cc5eaaa0248df944dc5afb146a74ddb2ebf16e69f6f33f335d84cd90487ebdafe06fa20fa046cc30acf280d6d3da05dfa937aae3cee1f044674f98b329f6536d7bbe2199e5a0e718ba8ae52d4b4175429d814638164ba1e0b26b8b8f800dbc7a4bf936074b9ba0f87a931563105d7e4936edd8de17eaacc97bd65c9a204b822ba1ec453dfedecba0e82dd43b29940f0f7ea430ec745bba835298ce36985ef7fdbc6243f5c5c36d9ca035b6f9e29d694e20347e5fa252f66a3a0ab73c9c917742fc02811f3ee5c2d171a0ad3b678e9936d5b5edaea35772ef6637006a77116729c14b7564ece054ab684f80f90211a0da2a1d3745c752f49a021af89950ce412fb32afbca78dd576bc339f0e54d813da0a0cced22421ee347c39f8d2620e9e5288daf772ef979ff0c2acbe2a6b20d7920a0fe5671c7c4880bd3eb329c38d7f6fb6d938dc6311f758c3f16ba8ad19a6270afa000ec671c46d85e520e922379723b08da4c52cb1244562753b81be3ca3d596f00a0ca5a117a114992c9dc618088e6f5be6ef31cd444014ebb70dfc18e770c80cbb8a0bfd9a006b6272559bab3d2e154ca6f0d2d0822c230f58b790d9409a558b1dad6a0f0df757a442591eb88c5145885d4dcbcbad45a21db72eda7c9b71e7fe6bb0c05a09e1f725af7ea7d73b441ccb52cbaed46e49d96d34d85353c90c53ca79c279913a06db1d5a69588bff72287c775606747c856a03dd18ff60bc429e20ff9c12f3015a049bd0d8417ceb06480b13a7602c90e59eee5e1d4095e2c11f51581675e57d496a02b73205826e216c7d72d597481c5bacc4effc4bdbde223610799c52eac5207b1a0a17afa14304b4fadceb3f4c9f1cb7f8dfe4f7785a5c5ecb691cba3d5bb466ad8a00414101ace57b5167f63a525c8a7a05afccd5d6dd118c5c0da37242473263edea03966dacd864c0c938ed7ab87593445462aea20a4ddb758421882aca272059bc3a0de8bf739688d87de55d115fddbc2c74903e8d28ce24c61a9fbe68ed64a9e02bda0475f63ca9e82ce467390a7dc588751447d692db9e95a5190b815fd3ed6c90c7880f9015180a0afaf518ebaa9a7043db9a2e7595a9498faa2262ec97c3546a75f53a6a9530ed4a0c57b06c4c3b853cc335da7706ac61de6956ee4e9cc6b92a599193a2f82a97cdba0663e1268a245bc67339199d395668bd5a7faa23de45580bf4a654217b67094cc80a081f78eec004c33938d62abccdc68626514f413b83f515e2e8091caa8670b2e7da0d34df61ebf10b2056e7c147a849f7700d7f0f751463a005ccb1f0888e4c81937808080a06d7311d4de4eadda5daeab6884d5334ead68b52180e942e24e95099987fa324b80a0698a333f2e0c2f0c18955a47ec1caafbeb3bb498169d24013500c754a671d1bca06b21b63c96ce078df602d4db10b389536d8097f8215fc1a75cae441ce14139dba00bd18bbee09461506ec2dc0b9fd01a5ff30f50decd1b8f9ecf0f63828c828742a0a5b026f6b2907e4c2ee220942b8d7a7fc986c7714e217084297f77ebe9fa777180f8679e2007ebdf168585d2280190be66a7ffbc6143ed885c9322c7ec96d8d32c01b846f8440180a0f07b2fce9729d2ce5b8e1d27960b2b40a90139519ac122a267b3a8f0285105c9a003644ecee9c13aa77193d5a4ad89b2700c0e59442e082c52e6df1cdc3445ff6f";
    const STORAGE_PROOF_RLP = "0xf8b8f8718080a04e918b76be51be2f02df0ac6191ec2765d401d2229e47291806815da755f5b5e808080a0b01f9cbc6d7940cae9809affc8fd140cb605613952adb215365a9f76ff96a7258080a0902c1b26e70fcdc4d44e19f92f5db5c134ee0da5857e0bb8f9401a5b84b1782f80808080808080f843a036b32740ad8041bcc3b909c72d7e1afe60094ec55e3cde329b4b3a28501d826ca1a0494e4352454d454e540000000000000000000000000000000000000000000012";

    // Call perform entrypoint
    contract.methods.default(
    ACCOUNT_PROOF_RLP,
    STORAGE_PROOF_RLP,
    BLOCK_NUMBER
    ).send().then(console.log);