Ethereum integration

Table of contents

  1. Intro
  2. Dock and EVM accounts
  3. Deploying a DAO
  4. Chainlink integration

This document assumes hands-on experience with Ethereum and interaction with smart contracts using libraries like web3 or ethers-js.

Intro

The chain allows you to deploy and interact with existing EVM smart contracts by using popular Ethereum client libraries like web3 or ethers-js. You can directly send contract's bytecode as well if you don't want to use these libraries. This is possible because the chain integrates 2 modules pallet-evm and pallet-ethereum from Parity's frontier project. pallet-evm allows the chain to execute EVM bytecode and persist state like contract storage but does not understand how Ethereum transactions, blocks, etc are created and have to be parsed. Handling that is the job of pallet-ethereum which uses pallet-evm for executing the bytecode. More detailed docs of these pallets are available here.

The motivation for this integration was to support Chainlink for providing price feed of the DOCK/USD pair which can then be used by the chain to charge transactions at a USD price.

The document will however show how to deploy and use a different set of contracts. A DAO, which replicates Aragon's voting app. The app lets token holders vote in proportion to the tokens they hold and the winning vote executes an action by calling a method on another contract.

Most of the examples use web-3 but there is a test using ethers-js as well.

Dock and EVM accounts

Accounts in Dock are 32 bytes (excluding network identifier and checksum) but EVM accounts are 20 bytes (last 20 bytes of the public key). As there is no direct conversion possible between these two and we don't support binding these two together in an onchain mapping, a separate Ethereum address has to be created and funded with tokens to send Ethereum style transactions. pallet-evm derives a Dock address from this Ethereum address and expects that Dock address to have tokens. The test native-balance-to-eth.test shows an Ethereum account carol created using web3 being given some tokens using function endowEVMAddress.

  // An API object which will connect to node and send non-EVM transactions like balance transfer
  const dock = new DockAPI();
  await dock.init({
    address: FullNodeEndpoint,
  });

  // Jacob has Dock tokens and will send tokens to Carol.
  const jacob = dock.keyring.addFromUri(EndowedSecretURI);
  dock.setAccount(jacob);

  // ...
  // ....

  const carol = "<Account created from web3>";
  await endowEVMAddress(dock, carol.address);

The substrate address can also be generated by function evmAddrToSubstrateAddr. Its balance can be queried either using web3 or polkadot-js.

  // Every EVM address has a mapping to Substrate address whose balance is deducted for fee when the EVM address does a transaction.
  const carolSubsAddr = evmAddrToSubstrateAddr(carol.address);
  console.log(`Querying balance of Carol's address using web3 ${(await web3.eth.getBalance(carol.address))}`);
  console.log(`Querying balance of Carol's address using polkadot-js ${(await getBalance(dock.api, carolSubsAddr, false))}`);

endowEVMAddress uses evmAddrToSubstrateAddr to covert the passed EVM address to the Substrate address and do a transfer as shown below

// Give `amount` of Dock tokens to EVM address. `amount` defaults to the number of tokens required to pay of maximum gas
export function endowEVMAddress(dock, evmAddr, amount) {
  //  Convert EVM address to a Substrate address
  const substrateAddr = evmAddrToSubstrateAddr(evmAddr);

  // Selecting the amount such that it can pay fees for the upto the maximum gas allowed and some extra
  const amt = amount !== undefined ? amount : bnToBn(MinGasPrice).mul(bnToBn(MaxGas)).muln(2);

  // Transfer to the Substrate address created above
  const transfer = dock.api.tx.balances.transfer(substrateAddr, amt);
  return dock.signAndSend(transfer, false);
}

To send arbitrary EVM transactions and deploy contracts using web3, look at the functions sendEVMTxn and deployContract respectively in scripts/eth/helpers.js.

Withdrawing tokens back from an EVM address to a Substrate address is a 3-step process.

  1. Derive an intermediate EVM address from the receiving Substrate address.
  2. Send tokens using web3 to this intermediate EVM address.
  3. Send tokens using polkadot-js from intermediate address to target address.
  // Withdraw some tokens from EVM address, i.e. Carol to Jacob.

  // Jacob's account is set as signer in the API object `dock`

  // Step-1
  // Create an intermediate EVM address
  const intermediateAddress = substrateAddrToEVMAddr(jacob.address);

  // Step-2
  // Carol sends 100 tokens to the intermediate EVM address. `sendTokensToEVMAddress` uses web3 to send an Ethereum style
  // transfer transaction, i.e. `data` field is set to 0 and `value` field specifies the transfer amount.
  await sendTokensToEVMAddress(web3, carol, intermediateAddress, 1000);

  // Step-3
  // Withdraw from the intermediate address to the Substrate address sending this transaction, i.e. Jacob
  const withdraw = dock.api.tx.evm.withdraw(intermediateAddress, 1000);
  await dock.signAndSend(withdraw, false);

The second step above of sending tokens in EVM requires to specify minimum gas price and maximum allowed gas. The function sendTokensToEVMAddress needs to know these values and accepts them as arguments. If not provided it will check for environment variables MinGasPrice and MaxGas. This behavior is common to all script helpers.

DAO

This section shows how to deploy a voting DAO where tokens holder can vote to execute certain actions. This replicates Aragon's voting app. The complete script is here and below is an explainer of the script

  1. Create some accounts that will send Ethereum style transactions and fund them with Dock tokens. The accounts generated in the code are only for testing so create your own accounts for real world apps.

    const web3 = getWeb3();
    
    // Create some test accounts. Alice will be the manager of the DAO while Bob, Carol and Dave will be voters.
    const [alice, bob, carol, dave] = getTestEVMAccountsFromWeb3(web3);
    
    // Endow accounts with tokens so they can pay fees for transactions
    await endowEVMAddressWithDefault(alice.address);
    await endowEVMAddressWithDefault(bob.address);
    await endowEVMAddressWithDefault(carol.address);
    await endowEVMAddressWithDefault(dave.address);
    

    getTestEVMAccountsFromWeb3 uses web3 to create EVM accounts using some test private keys.

    // Returns some test EVM accounts
    export function getTestEVMAccountsFromWeb3(web3) {
      return getTestPrivKeysForEVMAccounts().map((k) => web3.eth.accounts.privateKeyToAccount(k));
    }
    
  2. Create a DAO factory contract which will then be used to initialize a new DAO instance. Also setup the access control list for the DAO and set the DAO manager (an admin role)

    // Create a contract factory to create new DAO instance.
    const [, , , daoFactContractAddr] = await createDaoFactory(web3, alice);
    
    // Create a new DAO instance
    const daoAddr = await createNewDao(web3, alice, alice.address, daoFactContractAddr);
    
    // Set access control and set Alice as DAO's manager
    const aclAddr = await setupAcl(web3, alice, alice.address, daoAddr);
    
  3. A DAO can install several apps but here we will have only one app; for voting. Choose a unique app id.

    // Some unique app id
    const appId = '0x0000000000000000000000000000000000000000000000000000000000000100';
    
  4. Create a voting app (contract) with the above app id. Install the app in the DAO and allow any token holder to vote using the DAO's access control list (ACL).

    // Create a voting contract, install it as an app in the DAO and allow any token holder to vote
    const votingAppAddress = await setupVotingApp(web3, alice, alice.address, appId, daoAddr, aclAddr);
    const votingApp = new web3.eth.Contract(VotingDAOABI, votingAppAddress);
    
  5. Voting in this DAO requires voters to have tokens and their vote will carry weight proportional to their token balance. Deploy a token contract. This token contract is Aragon's MiniMeToken that extends ERC-20 interface. After deploying token, accounts bob, carol and dave are given 51, 29 and 20 tokens respectively. This makes the total supply of the MiniMeToken as 100 where bob, carol and dave hold 51%, 29% and 20% supply respectively.

    // Deploy a token contract where Bob, Carol and Dave will have 51%, 29% and 20% tokens as thus proportional voting power.
    const tokenContractAddr = await deployToken(web3, alice, [[bob.address, 51], [carol.address, 29], [dave.address, 20]]);
    
  6. Now initialize the voting app by setting the token contract address and thresholds for voting. The example scripts set the winning percentage to 51%. As bob, carol and dave hold 51%, 29% and 20% token supply, they will have 51%, 29% and 20% voting power respectively.

    // Initialize the voting by supplying the token contract and thresholds for voting.
    await initializeVotingApp(web3, alice, votingAppAddress, tokenContractAddr);
    
  7. For this example, we want successful voting to increment a counter in a contract. However, this contract is for demo purpose only. counterAddr is the address of the demo contract and incrementScript is the encoded call to a function to increment the counter.

    // A Counter contract as an example executor. In practice, the executor methods will only allow calls by the voting contract.
    const [counterAddr, incrementScript] = await setupVotingExecutor(web3, alice);
    
  8. As bob has 51% of the voting power, it can create a new vote by calling contract method newVote

    // Bob alone can increment the Counter as he has 51% tokens
    console.log(`Counter before increment from Bob ${(await getCounter(web3, counterAddr))}`);
    await sendEVMTxn(web3, bob, votingAppAddress, votingApp.methods.newVote(incrementScript, '').encodeABI());
    console.log(`Counter after increment from Bob ${(await getCounter(web3, counterAddr))}`);
    
  9. As carol and dave together hold less than 51%, they cannot increment the counter by voting. Here carol creates a new vote by calling contract method newVote which returns the vote id and dave uses votes in approval of carol by calling contract method vote and passing the vote id and true. The other true indicates the vote should trigger the execution if successful.

    // Carol creates a new vote
    const voteId = await createNewVote(web3, carol, votingAppAddress, incrementScript);
    // Dave seconds Carol's vote
    await sendEVMTxn(web3, dave, votingAppAddress, votingApp.methods.vote(voteId, true, true).encodeABI());
    console.log("Counter after attempted increment from Carol and Dave. Counter will not change as Bob and Carol don't have enough voting power");
    

The chain will have Chainlink contracts for price feed in addition to Link token and others. The contracts and scripts to interact with Chainlink contracts are at scripts/eth/chainlink in the repo. The scripts have some comments to explain the working.

  • To deploy the Link token, check the script link-token.js at path scripts/eth/chainlink/link-token.js in the repo.
  • To deploy the FluxAggregator contract that is used by oracles to submit prices, check script flux-aggegator.js at path scripts/eth/chainlink/flux-aggegator.js in the repo.
  • To deploy aggregator with access control on reads and with proxy, AccessControlledAggregator and EACAggregatorProxy are deployed. Check script access-controlled-aggregator-proxy.js at path scripts/eth/chainlink/access-controlled-aggregator-proxy.js in the repo. To deploy with DeviationFlaggingValidator that raises flag on price off by a threshold in either direction, use script deviation-flag-validator.js at path scripts/eth/chainlink/deviation-flag-validator.js. The set the validator address while deploying AccessControlledAggregator.
  • To setup a contract for an Oracle (not needed for price feed though), check script oracle.js in scripts/eth/chainlink/oracle.js.