Ethereum integration
Table of contents
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.
- Derive an intermediate EVM address from the receiving Substrate address.
- Send tokens using web3 to this intermediate EVM address.
- 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
-
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)); }
-
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);
-
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';
-
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);
-
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
anddave
are given 51, 29 and 20 tokens respectively. This makes the total supply of theMiniMeToken
as 100 wherebob
,carol
anddave
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]]);
-
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
anddave
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);
-
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 andincrementScript
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);
-
As
bob
has 51% of the voting power, it can create a new vote by calling contract methodnewVote
// 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))}`);
-
As
carol
anddave
together hold less than 51%, they cannot increment the counter by voting. Herecarol
creates a new vote by calling contract methodnewVote
which returns the vote id and dave uses votes in approval ofcarol
by calling contract methodvote
and passing the vote id andtrue
. The othertrue
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");
Chainlink
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 pathscripts/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 pathscripts/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 pathscripts/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 scriptdeviation-flag-validator.js
at pathscripts/eth/chainlink/deviation-flag-validator.js
. The set the validator address while deployingAccessControlledAggregator
. - To setup a contract for an Oracle (not needed for price feed though), check script
oracle.js
inscripts/eth/chainlink/oracle.js
.